PHP flock() для чтения-изменения-записи не работает

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

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

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

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

Поиск в Google "read modify write" php или fetch 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 - Manual есть предупреждение об ограничениях flock(), но речь идет об ISAPI (Windows) и FAT (Windows). Конфигурация моего сервера:
Версия PHP 7.2.5
Система: Linux cluster026.gra.hosting.ovh.net
API сервера: CGI/FastCGI


person PaulH    schedule 03.07.2018    source источник
comment
(Учитывая большое предупреждение в руководстве для flock относительно вещей, связанных с API и ОС, вы не слишком удивлены прямо сейчас, верно?)   -  person CBroe    schedule 03.07.2018
comment
@CBroe Предупреждение действительно есть, но оно не очень конкретное, и я не знал, применимо ли оно. Как правило, такого рода предупреждения относятся к системам Windows. Причина, по которой я разместил это, заключается в том, что было трудно найти альтернативу. В ответах, которые я нашел по этому вопросу, многие пользователи работают с flock() без проблем.   -  person PaulH    schedule 03.07.2018
comment
La вопрос даты mais j'ai ле même problème автомобиль Чез OVH aussi. La raison vient sûrement de là: community.ovh.com/t/ Можно ли найти альтернативу, которая работает?   -  person Jean    schedule 25.03.2020
comment
@Jean Да, рабочая и проверенная альтернатива - это принятый ответ ниже. Он использует mkdir(), потому что это одна инструкция, которая выполняет и проверку, и установку. Я протестировал решение с помощью file_get_contents(), показанного в community.ovh.com/t/ не работает.   -  person PaulH    schedule 28.03.2020


Ответы (3)


Используя файлы для управления данными, координируемые только обработчиками запросов 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);
 }

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

person symcbean    schedule 03.07.2018
comment
Спасибо за этот ответ. Однако это лишь частично решает проблему: я хочу писать каждый раз, когда осуществляется доступ к файлу сценария. например 20 раз в секунду и то не сутки. Я разработал идею использования mkdir в отдельном ответе. - person PaulH; 03.07.2018
comment
Есть ли конкретная причина использовать filectime для каталога и filemtime для файла? - person PaulH; 03.07.2018
comment
Я хочу писать каждый раз при доступе к файлу сценария. например 20 раз в секунду - тогда используйте базу данных или демон. Как я уже сказал, это уродливый хак. Он будет работать с гораздо большей емкостью, чем флок, но все равно укусит вас. - person symcbean; 03.07.2018

Один из способов выполнить атомарный тест и установить инструкцию в PHP — использовать mkdir(). Немного странно использовать для этого каталог вместо файла, но mkdir() создаст каталог или вернет false (и подавляющее предупреждение), если он уже существует. Файловые команды, такие как 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);
person PaulH    schedule 03.07.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);

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

person PaulH    schedule 06.07.2018
comment
Привет, Пол, я думаю, вы пропустили правку здесь: rmdir($fnLock); -- должно ли быть закрыто? - person Fred Polli; 22.10.2018