Давайте рассмотрим классический пример записи в файл в PHP:

function writeFile() {
    $success = false;
    $file = fopen('sample.txt', 'wb');
    $data = "Hello\nthis is a test file!";
    if (fwrite($file, $data) !== false) {
        $success = true;
    }
    fclose($file);
    return $success;
}

Вы вызываете функцию записи в файл, и она возвращает статус true — ура! Действительно ли файл был успешно записан на диск? Запись должна произойти даже в случае отключения питания одновременно с возвратом статуса функции, ведь так?

К сожалению, нет.

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

Я уже слышу ваши возражения: “Разве в PHP нет функции fflush как раз для этой цели? Если сразу после fwrite добавить fflush($file) в приведенный выше пример, несомненно содержимое буфера будет записано на диск, а файл немедленно сохранен на диске?”.

К сожалению, нет. Давайте объясню почему. 

Операционная система, которая очень любит работать с вашим диском максимально эффективно, в свою очередь также сохраняет некоторые записи в своем собственном буфере, чтобы физически записать их на диск позже.

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

Если у вас есть опыт работы с базами данных, скорее всего, вам знакома аббревиатура ACID — атомарность, согласованность, изоляция, надежность (atomicity, consistency, isolation, durability). Это те свойства, которые должна предоставлять СУБД для гарантии транзакционности операции. Надежность означает, что после того, как транзакция базы данных будет успешно совершена, эти изменения будут зафиксированы, даже в том случае, если ваша система сразу после этого выйдет из строя. Другими словами, ваши изменения будут физически записаны на диск.

Я не предлагаю вам писать с нуля собственную СУБД на PHP, но определенно есть случаи, когда вам может понадобится надежность при записи в файл. Критически важные системные логи, журналы аудита, возможно, что-то, не связанное с интернетом; PHP по-прежнему ориентирован на интернет, но теперь не всецело. В PHP 8 мы можем отметить языковые фичи более общего назначения, которые можно применять в программировании.

Поприветствуйте fsync!

fsync — это системный вызов для POSIX-систем, который синхронизирует файл с хранилищем. Другими словами, fsync дает инструкцию операционной системе, чтобы любые изменения или записи, сделанные в файле, который находится в буфере, немедленно сохранялись на диске и были успешно сохранены на диске к моменту возврата вызова, и была возможность восстановить данные даже в случае сбоя системы или потери питания. Это и есть надежность ACID.

В языке C fsync() является частью стандартной библиотеки. В Java это — io.File.sync(), в Python — os.fsync(). До сих пор PHP оставался единственным крупным языком программирования, который я использовал, у которого не было интерфейса для системного вызова fsync.

fsync в PHP

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

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

function writeFile() {
    $success = false;
    $file = fopen('sample.txt', 'wb');
    $data = "Hello\nthis is a test file!";
    if (fwrite($file, $data) !== false) {
        $success = fsync($file); // that's better!
    }
    fclose($file);
    return $success;
}

PHP 8.1 также предоставляет fdatasync, синхронизация с помощью которой (теоретически) немного быстрее, поскольку fsync() попытается полностью синхронизировать как изменения данных файла, так и метаданные о файле (время последнего изменения и т.д.), что технически выливается в две записи на диск. Идея fdatasync() заключается в том, что она синхронизирует только сами данные.

Мой совет — сильно об этом не заморачивайтесь. На практике современные файловые системы Linux делают одно и то же в рамках fsync и fdatasync, и обе включают обновление метаданных. Что касается Windows, то здесь нет встроенной реализации fsync, функция в PHP представляет собой обертку FlushFileBuffers API, которая выполняет ту же работу. В Windows fsync() и fdatasync() являются синонимами одного и того же системного вызова, поэтому не имеет значения, какой из них вы используете.

Предупреждения

Предупреждение первое: fsync() не подходит для высокопроизводительной и интенсивной записи в файл. Если вам нужно делать сотни или тысячи записей в секунду, использовать fsync не следует, производительность ввода-вывода упадет. Единственный надежный способ справиться с такой ситуацией — использовать не буферизованную прямую запись на диск, а это слишком низкий уровень для PHP.

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

Предупреждение третье: В наши дни даже в самих дисках есть внутренние буферы. Это буферы самого низкого уровня. Операционные системы умны, и они знают, что многие диски будут врать об успешной записи данных в постоянное хранилище, поэтому реализация fsync() в большинстве систем скажет диску также сбросить свои собственные буферы. Некоторые USB-накопители печально известны этим — будут просто врать ОС о завершении записи, и с этим ничего нельзя поделать. Так что будьте внимательны, в некоторых случаях технически возможно вернуть true из fsync и обнаружить, что ваши данные не были сохранены в этот момент. Это случается редко, но случается.

Дополнительные материалы

PHP Watch справился с написанием моих собственных изменений для PHP 8.1 fsync лучше, чем я.

Linux man page для Fsync дает техническое описание системного вызова.

RFC на php.net содержит более подробную информацию и ссылку на реализацию.


Материал подготовлен в рамках курса «PHP Developer. Professional».

Всех желающих приглашаем на бесплатное demo-занятие «PHP 8.1 — Что нового?». На этом занятии мы посмотрим на нововведения в языке и применим все это на практике.