У меня есть файл журнала, поддерживаемый сценарием PHP. Скрипт PHP подвергается параллельной обработке. Я не могу получить flock()
механизм работы с лог-файлом: в моем случае flock()
не предотвращает одновременный доступ к файлу журнала, совместно используемому сценариями PHP, работающими параллельно, и иногда перезаписывается.
Я хочу быть в состоянии прочитать файл, выполнить некоторую обработку, изменить данные и записать обратно без того же кода, работающего параллельно на сервере, выполняющего то же самое в то же время. Чтение, изменение, запись должны быть в последовательности.
На одном из моих общих хостингов (OVH France) он работает не так, как ожидалось. В этом случае мы видим, что счетчик $c
имеет одинаковое значение в разных iframe
s, что не должно быть возможно, если блокировка работает так, как ожидалось, что она делает на другом виртуальном хостинге.
Любые предложения, чтобы сделать эту работу, или для альтернативного метода?
погуглить "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";
}
}
Есть предупреждение о flock()
ограничения в PHP: flock — Руководство, но речь идет о ISAPI (Windows) и FAT (Windows). Моя конфигурация сервера:
Версия PHP 7.2.5
система: Linux cluster026.gra.hosting.ovh.net
Серверный API: CGI / FastCGI
Используя файлы для управления данными, координируемые только обработчиками запросов PHP, вы отправляетесь в мир боли — пока вы только что окунули пальцы в воду.
Используя LOCK_EX, ваш писатель должен дождаться освобождения любого (и каждого) экземпляра LOCK_SH, прежде чем он получит блокировку. Здесь вы устанавливаете flock для блокировки, пока блокировка не будет получена. В относительно загруженной системе писатель может быть заблокирован на неопределенный срок. В большинстве ОС нет приоритетной очереди блокировок, которая бы помещала любого последующего читателя, запрашивающего блокировку, позади процесса, ожидающего блокировки записи.
Еще одним осложнением является то, что вы можете использовать стадо только на открыть дескриптор файла. Это означает, что открытие файла и получение блокировки не являются атомарными, далее вам нужно очистить кэш статистики, чтобы определить возраст файла после получения блокировки.
Любые записи в файл (даже с использованием 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);
}
(обратите внимание, что это действительно уродливый хак)
Способ сделать атомарный тест и установить инструкцию в 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);
Есть один 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);
Это хорошая идея, чтобы проверить это, например, используя код в вопросе.
Многие люди полагаются на блокировку, как описано в документации, но неожиданности могут появиться во время тестирования или производства под нагрузкой (параллельных запросов от одного браузера может быть достаточно).