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()