Deep Engineering

MEASUREMENT

bench/durability/practice.py

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/interview/sre/page-cache-fsync
How to run it
python3 bench/durability/fsync.py    > bench/durability/runs/fsync.txt
python3 bench/durability/practice.py > bench/durability/runs/practice.txt

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

Замеры для урока «Page cache и fsync»

Файл Что делает
fsync.py четыре блока: цена одной записи без сброса, с fdatasync и с fsync (с самопроверкой, различим ли разрыв между двумя последними); что остаётся в файле, если писавший процесс убит SIGKILL; сто записей со сбросом после каждой против одного сброса в конце; отдельная цена сброса каталога
practice.py ответы к задачам урока: содержимое файла после убийства автора, то же глазами постороннего процесса, и кратность «сброс против записи»

Запуск из корня репозитория:

python3 bench/durability/fsync.py    > bench/durability/runs/fsync.txt
python3 bench/durability/practice.py > bench/durability/runs/practice.txt

Что в этих числах воспроизводимо, а что нет

Воспроизводимо везде: данные, записанные без fsync, переживают SIGKILL автора и видны другому процессу; сброс дороже записи на два порядка; пачка дешевле поштучности в разы.

Не воспроизводимо и будет другим: все абсолютные времена. Они сняты на виртуальном диске контейнера; на NVMe запись со сбросом дешевле, на сетевом томе дороже. Кратности тоже поедут — содержателен их порядок, а не цифра.

Отдельно про fsync против fdatasync. На этой машине разрыв между ними меньше разброса между кругами, и скрипт печатает difference resolvable on this machine: no. Это не поломка замера, а его результат: заявлять кратность, которой не видно, нельзя. На машине, где метаданные дороже, строка станет yes.

Требования к среде

Права на запись во временный каталог — и больше ничего. Скрипт работает в /tmp/de_durability и /tmp/de_durability_practice и не трогает ничего за их пределами.

Числа сняты на CPython 3.11.15, Linux 6.18.44 (x86-64).

Script

101 lines
"""Практика к уроку про page cache и fsync: два ответа и одна кратность.

ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ. Задачи урока проверяются сборкой
(scripts/validate-practice.mjs): показанный читателю код обязан построчно быть
в скрипте, верный вариант — дословно встречаться в записи прогона, а
заявленная кратность — печататься самой программой.

ЧТО ЗДЕСЬ ПРОВЕРЯЕТСЯ. Ровно то место, где интуиция расходится с устройством:
незаписанные на диск данные видны другим процессам и переживают смерть автора,
потому что лежат они не у автора, а в кеше ядра. И цена обещания
долговечности — та самая кратность, ради которой существует групповая фиксация
в базах данных.

ЗАПУСК: python3 bench/durability/practice.py
Вывод: runs/practice.txt
"""

import os
import statistics
import subprocess
import sys
import tempfile
import time

RECORD = b"x" * 4096
ROUNDS = 200
WORK = os.path.join(tempfile.gettempdir(), "de_durability_practice")

WRITER = """
import os, sys, time
fd = os.open(sys.argv[1], os.O_CREAT | os.O_WRONLY | os.O_TRUNC)
os.write(fd, b'survived the kill')
time.sleep(30)
"""

READER = "import sys\nprint(open(sys.argv[1]).read())\n"


def timed_writes(path: str, mode: str) -> float:
    """Медианное время одной записи в миллисекундах при заданной строгости."""
    fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC)
    samples = []
    try:
        for _ in range(ROUNDS):
            started = time.perf_counter()
            os.write(fd, RECORD)
            if mode == "fsync":
                os.fsync(fd)
            samples.append((time.perf_counter() - started) * 1000)
    finally:
        os.close(fd)
    return statistics.median(samples)


def content_after_kill(path: str) -> str:
    """Что лежит в файле, если писавший процесс убит SIGKILL и fsync не звал."""
    proc = subprocess.Popen([sys.executable, "-c", WRITER, path])
    time.sleep(0.7)
    proc.kill()
    proc.wait()
    with open(path, encoding="utf-8") as fh:
        return fh.read()


def seen_by_another_process(path: str) -> str:
    """Видит ли посторонний процесс данные, которые на диск ещё не сброшены."""
    done = subprocess.run(
        [sys.executable, "-c", READER, path], capture_output=True, text=True
    )
    return done.stdout.strip()


def main() -> None:
    print(f"Python {sys.version.split()[0]} · Linux {os.uname().release}")
    print()

    os.makedirs(WORK, exist_ok=True)
    path = os.path.join(WORK, "killed.bin")

    # --- Часть 1: два ответа о поведении --------------------------------
    print(content_after_kill(path) or "(empty)")
    print(seen_by_another_process(path) or "(empty)")

    # --- Часть 2: во сколько раз дороже обещание долговечности -----------
    #
    # ПОЧЕМУ КРАТНОСТЬ, А НЕ МИЛЛИСЕКУНДЫ. Абсолютные времена на вашем диске
    # будут другими на порядок: у NVMe одни, у сетевого тома другие. А вот
    # порядок отношения — «сброс дороже записи в сотню раз» — переживает
    # смену железа, потому что сравниваются память и устройство.
    bench = os.path.join(WORK, "records.bin")
    plain = timed_writes(bench, "none")
    synced = timed_writes(bench, "fsync")
    print()
    print(f"write only (4 KiB), median, us       {plain * 1000:.1f}")
    print(f"write + fsync, median, us            {synced * 1000:.1f}")
    print(f"fsync over plain write               {synced / plain:.1f}")


if __name__ == "__main__":
    main()