1

我有一个由 PHP 脚本维护的日志文件。PHP 脚本需要进行并行处理。我无法获得flock()处理日志文件的机制:在我的情况下,flock()不会阻止并行运行的 PHP 脚本共享的日志文件同时被访问并且有时被覆盖。

我希望能够读取文件,进行一些处理,修改数据并写回,而无需在服务器上并行运行相同的代码同时执行相同的操作。读修改写必须按顺序进行。

在我的一个共享主机(OVH France)上,它没有按预期工作。在这种情况下,我们看到计数器$c在不同的 s 中具有相同的值,iframe如果锁按预期工作,这应该是不可能的,它在其他共享主机上会这样做。

有什么建议可以使这项工作或替代方法吗?

谷歌搜索"read modify write" phpfetch and add没有test and set提供有用的信息:所有解决方案都基于一个有效的flock()。

这是一些独立运行的演示代码来说明。它从浏览器向服务器生成多个并行请求并显示结果。很容易直观地观察到故障:如果您的网络服务器不像我的一个那样支持flock(),则计数器值和日志行数在某些帧中将是相同的。

<!DOCTYPE html>
<html lang="en">
<title>File lock test</title>
<style>
iframe {
    width: 10em;
    height: 300px;
}
</style>
<?php
$timeStart = microtime(true);
if ($_GET) { // iframe
    // GET
    $time = $_GET['time'] ?? 'no time';
    $instance = $_GET['instance'] ?? 'no instance';

    // open file
    // $mode = 'w+'; // no read
    // $mode = 'r+'; // does not create file, we have to lock file creation also
    $mode = 'c+'; // read, write, create
    $fhandle = fopen(__FILE__ .'.rwtestfile.txt', $mode) or exit('fopen');
    // lock
    flock($fhandle, LOCK_EX) or exit('flock');
    // start of file (optional, only some modes like require it)
    rewind($fhandle);
    // read file (or default initial value if new file)
    $fcontent = fread($fhandle, 10000) or ' 0';
    // counter value from previous write is last integer value of file
    $c = strrchr($fcontent, ' ') + 1;
    // new line for file
    $fcontent .= "<br />\n$time $instance $c";
    // reset once in a while
    if ($c > 20) {
        $fcontent = ' 0'; // avoid long content
    }
    // simulate other activity
    usleep(rand(1000, 2000));
    // start of file
    rewind($fhandle);
    // write
    fwrite($fhandle, $fcontent) or exit('fwrite');
    // truncate (in unexpected case file is shorter now)
    ftruncate($fhandle, ftell($fhandle)) or exit('ftruncate');
    // close
    fclose($fhandle) or exit('fclose');
    // echo
    echo "instance:$instance c:$c<br />";
    echo $timeStart ."<br />";
    echo microtime(true) - $timeStart ."<br />";
    echo $fcontent ."<br />";
} else {
    echo 'File lock test<br />';
    // iframes that will be requested in parallel, to check flock
    for ($i = 0; $i < 14; $i++) {
        echo '<iframe src="?instance='. $i .'&time='. date('H:i:s') .'"></iframe>'."\n";
    }
}

PHP 中有一个关于flock()限制的警告:flock - Manual,但它是关于 ISAPI (Windows) 和 FAT (Windows)。我的服务器配置是:
PHP 版本 7.2.5
系统:Linux cluster026.gra.hosting.ovh.net
服务器 API:CGI/FastCGI

4

3 回答 3

1

在 PHP 中进行原子测试和设置指令的一种方法是使用mkdir(). 使用目录而不是文件有点奇怪,但mkdir()如果它已经存在,则会创建一个目录或返回一个错误(和一个抑制警告)。fopen()像, fwrite(),之类的文件命令file_put_contents()不会在一条指令中进行测试和设置。

<?php
// lock
$fnLock = __FILE__ .'.lock'; // lock directory filename
$lockLooping = 0; // counter can be used for tuning depending on lock duration
do {
    if (@mkdir($fnLock, 0777)) { // mkdir is a test and set command
        $lockLooping = 0;
    } else {
        $lockLooping += 1;
        $lockAge = time() - filemtime($fnLock);
        if ($lockAge > 10) {
            rmdir($fnLock); // robustness, in case a lock was not erased                
        } else {
            // wait without consuming CPU before try again
            usleep(rand(2500, 25000)); // random to avoid parallel process conflict again
        }
    }
} while ($lockLooping > 0);

// do stuff under atomic protection
// don't take too long, because parallel processes are waiting for the unlock (rmdir)

$content = file_get_contents($protected_file_name);  // example read
$content = $modified_content; // example modify
file_put_contents($protected_file_name, $modified_content); // example write

// unlock
rmdir($fnLock);
于 2018-07-03T16:38:46.253 回答
0

使用仅由 PHP 请求处理程序协调的文件进行数据管理,您正走向一个痛苦的世界——到目前为止,您只是将脚趾浸入水中。

使用 LOCK_EX,您的编写器需要等待 LOCK_SH 的任何(和每个)实例被释放,然后才能获取锁。在这里,您将flock 设置为阻塞,直到可以获取锁为止。在相对繁忙的系统上,写入器可能会被无限期地阻止。大多数操作系统上没有锁的优先级排队,这会将任何后续请求锁的读取器置于等待写锁的进程后面。

更复杂的是,您只能在打开的文件句柄上使用flock。这意味着打开文件并获取锁不是原子的,进一步您需要刷新统计缓存以确定获取锁后文件的年龄。

对文件的任何写入(即使使用 file_put_contents())都不是原子的。因此,在没有独占锁定的情况下,您无法确定没有人会读取部分文件。

在没有其他组件的情况下(例如,提供锁队列机制的守护进程,或 Web 服务器前的缓存反向代理,或关系数据库),那么您唯一的选择是假设您无法确保独占访问并使用原子操作信号量的文件,如:

 $lock_age=time()-filectime(dirname(CACHE_FILE) . "/lock");
 if (filemtime(CACHE_FILE)>time()-CACHE_TTL 
       && $lock_age>MAX_LOCK_TIME) {
          rmdir(dirname(CACHE_FILE) . "/lock");
          mkdir(dirname(CACHE_FILE) . "/lock") || die "I give up";
      }
      $content=generate_content(); // might want to add specific timing checks around this
      file_put_contents(CACHE_FILE, $content);
      rmdir(dirname(CACHE_FILE) . "/lock");
 } else if (is_dir(dirname(CACHE_FILE) . "/lock") {
      $snooze=MAX_LOCK_TIME-$lock_age;
      sleep($snooze);
      $content=file_get_contents(CACHE_FILE);
 } else {
      $content=file_get_contents(CACHE_FILE);
 }

(请注意,这是一个非常丑陋的黑客)

于 2018-07-03T15:34:03.143 回答
0

有一种fopen()测试和设置模式:x模式。

x创建和开放仅用于写作;将文件指针放在文件的开头。如果文件已经存在,fopen()调用将失败,返回FALSE并生成 level 错误E_WARNING。如果该文件不存在,请尝试创建它。

行为与以下fopen($filename ,'x')相同,mkdir()并且可以以相同的方式使用:

<?php
// lock
$fnLock = __FILE__ .'.lock'; // lock file filename
$lockLooping = 0; // counter can be used for tuning depending on lock duration
do {
    if ($lockHandle = @fopen($fnLock, 'x')) { // test and set command
        $lockLooping = 0;
    } else {
        $lockLooping += 1;
        $lockAge = time() - filemtime($fnLock);
        if ($lockAge > 10) {
            rmdir($fnLock); // robustness, in case a lock was not erased                
        } else {
            // wait without consuming CPU before try again
            usleep(rand(2500, 25000)); // random to avoid parallel process conflict again
        }
    }
} while ($lockLooping > 0);

// do stuff under atomic protection
// don't take too long, because parallel processes are waiting for the unlock (rmdir)

$content = file_get_contents($protected_file_name);  // example read
$content = $modified_content; // example modify
file_put_contents($protected_file_name, $modified_content); // example write

// unlock
fclose($lockHandle);
unlink($fnLock);

对此进行测试是个好主意,例如使用问题中的代码。许多人依赖于文档中的锁定,但是在负载下的测试或生产期间可能会出现意外(来自一个浏览器的并行请求可能就足够了)。

于 2018-07-06T12:15:49.590 回答