PHP flock () для чтения-изменения-записи не работает - PullRequest
0 голосов
/ 03 июля 2018

У меня есть файл журнала, поддерживаемый скриптом PHP. Скрипт PHP подвергается параллельной обработке. Я не могу заставить механизм flock() работать с файлом журнала: в моем случае flock() не препятствует одновременному доступу к файлу журнала, совместно используемому сценариями PHP, работающим параллельно, и иногда перезаписываемому.

Я хочу быть в состоянии прочитать файл, выполнить некоторую обработку, изменить данные и записать обратно без того же кода, работающего параллельно на сервере, выполняющего то же самое в то же время. Запись read модификации должна быть в последовательности.

На одном из моих общих хостингов (OVH France) он не работает должным образом. В этом случае мы видим, что счетчик $c имеет одно и то же значение в разных iframe с, что не должно быть возможно, если блокировка работает должным образом, как это происходит на другом виртуальном хостинге.

Любые предложения, чтобы сделать эту работу, или для альтернативного метода?

Googling "read modify write" php или fetch and add или test and set не предоставили полезной информации: все решения основаны на рабочем стаде ().

Вот несколько автономных демонстрационных кодов для иллюстрации. Он генерирует ряд параллельных запросов от браузера к серверу и отображает результаты. Легко визуально наблюдать за дисфункцией: если ваш веб-сервер не поддерживает 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

Ответы [ 3 ]

0 голосов
/ 03 июля 2018

Способ сделать атомарный тест и установить инструкцию в 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);
0 голосов
/ 06 июля 2018

Существует один fopen() режим тестирования и настройки: режим x.

x Создать и открыть только для записи; поместите указатель файла в начале файла. Если файл уже существует, вызов fopen() завершится ошибкой, вернув FALSE и вызвав ошибку уровня 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);

Хорошая идея проверить это, например, используя код в вопросе. Многие люди используют блокировку в соответствии с документацией, но могут возникнуть неожиданности во время тестирования или производства под нагрузкой (параллельных запросов от одного браузера может быть достаточно).

0 голосов
/ 03 июля 2018

Используя файлы для управления данными, координируемые только обработчиками запросов PHP, вы отправляетесь в мир боли - вы только что опустили пальцы в воду.

Используя LOCK_EX, вашему автору записи нужно дождаться освобождения любого (и каждого) экземпляра LOCK_SH, прежде чем он получит блокировку. Здесь вы устанавливаете flock для блокировки, пока блокировка не будет получена. В относительно загруженной системе писатель может быть заблокирован на неопределенный срок. В большинстве ОС нет приоритетных очередей блокировок, в которых любой последующий считыватель, запрашивающий блокировку, будет находиться позади процесса, ожидающего блокировки записи.

Еще одним осложнением является то, что вы можете использовать flock только для дескриптора файла open . Это означает, что открытие файла и получение блокировки не являются атомарными. Кроме того, необходимо очистить кэш статистики, чтобы определить возраст файла после получения блокировки.

Любые записи в файл (даже с использованием file_put_contents ()) не являются атомарными. Поэтому в отсутствие исключительной блокировки вы не можете быть уверены, что никто не будет читать частичный файл.

В отсутствие дополнительных компонентов (например, демон, обеспечивающий механизм организации очереди блокировки, или обратный прокси-сервер кэширования перед веб-сервером, или реляционная база данных), тогда вы можете предположить, что вы не можете гарантировать исключительный доступ и использовать атомарные операции для семафора файла, что-то вроде:

 $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);
 }

(обратите внимание, что это действительно безобразный хак)

...