Вопрос «переживёт ли Redis перезапуск?» звучит просто, но правильный ответ — «зависит от того, как настроена персистентность и что вы готовы потерять». У Redis два механизма долговременного хранения — снимки RDB и журнал операций AOF — и оба дают разные гарантии. Между «данные точно на диске в момент подтверждения записи» и «данные в памяти, а на диск попадут когда-нибудь» лежит целый спектр компромиссов, и именно здесь проходит граница между Redis-кэшем и Redis-источником-истины.
Это третья статья серии «Redis: глубокое погружение» и центральная точка сквозной нити «Redis как кэш vs Redis как источник истины». Мы не будем говорить «Redis персистентный» или «Redis непостоянный» — обе формулировки неточны. Вместо этого разберём, какие именно гарантии даёт каждый режим, что теряется при разных типах сбоя и как AOF по устройству перекликается с журналами упреждающей записи в классических СУБД.
В статье
- RDB: снимки состояния
- AOF: журнал операций
- Гибрид: преамбула RDB плюс хвост AOF
- Что теряется при
kill -9: живая матрица - Цена
always: на порядок медленнее - Где проходит граница «кэш или источник истины»
- Что дальше
- Источники
RDB: снимки состояния
RDB — это периодический снимок всего датасета в компактный бинарный файл. Механика простая: Redis форкает процесс, дочерний пишет дамп на диск, родительский продолжает обслуживать клиентов, а расхождение между ними покрывается copy-on-write ядра. Отсюда две характерные черты: рестарт из RDB быстрый (файл читается последовательно, без проигрывания истории), а цена снимка — всплеск памяти на изменённых страницах и пауза на самом форке.
Снимок делается не непрерывно, а по правилу save <секунды> <изменений>: «сделать снимок, если за такое-то время накопилось столько-то изменений». Правило, с которым развёрнут стенд этой статьи, — save 60 1: снимок через 60 секунд после первого изменения. Штатный дефолт Redis шире — набор правил 3600 1 / 300 100 / 60 10000, то есть час после единственного изменения, пять минут после сотни, минута после десяти тысяч. Это и есть главная тонкость RDB, из которой растёт всё остальное: между снимками данные существуют только в памяти. Сбой в промежутке — и всё, что накопилось после последнего снимка, не существует.
Формулировать это надо аккуратно. RDB не «ненадёжен» — он даёт ровно ту гарантию, которую обещает: состояние на момент последнего успешного снимка. Вопрос не в надёжности механизма, а в том, укладывается ли ваша терпимость к потере в интервал между снимками.
AOF: журнал операций
AOF идёт с другой стороны: вместо периодического снимка состояния он ведёт журнал изменяющих команд. Каждая команда, меняющая данные, дописывается в конец файла; при старте Redis проигрывает журнал и восстанавливает состояние. Идея ровно та же, что лежит в основе журналов упреждающей записи в классических СУБД — сначала записать намерение в последовательный лог, потом менять состояние. Эта параллель разобрана подробно в серии «WAL и его аналоги»; здесь достаточно отметить родство подхода.
Журнал растёт бесконечно, поэтому Redis периодически его переписывает (rewrite): вместо накопленной истории «положили, изменили, изменили ещё раз» в новый файл пишется минимальный набор команд, дающий текущее состояние.
Ключевой параметр — не сам AOF, а политика appendfsync, и вот здесь начинается то, ради чего написана эта статья. Три режима:
always—fsyncна каждую запись перед тем, как клиент получитOK;everysec—fsyncраз в секунду;no—fsyncна усмотрение операционной системы.
Естественное чтение этого списка — «always не теряет ничего, everysec теряет до секунды, no теряет непонятно сколько». Это чтение верно ровно для одного класса сбоев и неверно для другого, и различие между ними — самое практически важное в статье.
Гибрид: преамбула RDB плюс хвост AOF
Гибридный режим объединяет оба механизма: файл начинается с компактной RDB-преамбулы (состояние на момент последнего rewrite), дальше идёт AOF-хвост с командами, случившимися после. Рестарт быстрый, потому что основная масса данных читается как снимок, а не проигрывается командами; окно потери при этом определяется хвостом, то есть политикой appendfsync, а не интервалом снимков.
Это разумный дефолт для случаев, когда персистентность вообще нужна: он забирает у RDB скорость рестарта, а у AOF — свежесть.
Что теряется при kill -9: живая матрица
Теория закончилась — дальше числа с развёрнутых redis:8.8 и valkey/valkey:8.1.
Методика: FLUSHALL, затем SET dur:<i> без пайплайна, одним клиентом. На середине — ровно после того, как очередной SET подтвердил запись ответом OK, — по контейнеру прилетает docker kill -s SIGKILL. Именно SIGKILL, а не docker stop: последний посылает SIGTERM, Redis успевает аккуратно сохраниться, и получился бы фальшивый результат «ничего не потеряно». Кил подтверждается через docker inspect — ExitCode=137 (это 128+9) однозначно говорит, что процесс убили сигналом 9, а не завершили как-то иначе. Итог каждого прогона — 2501 подтверждённая запись (индексы 0..2500). Дальше контейнер поднимается заново на том же volume, и DBSIZE показывает, сколько из подтверждённого пережило сбой.
Каждая ячейка прогнана дважды на каждом образе — четыре независимых полных прохода.
Матрица потерь под kill -9 процесса (не при падении ОС или потере питания), из 2501 подтверждённой записи
| Режим | Redis 8.8, #1 | Redis 8.8, #2 | Valkey 8.1, #1 | Valkey 8.1, #2 |
|---|---|---|---|---|
rdb-only |
2501† | 2501† | 2501† | 2501† |
aof-always |
0* | 0* | 0* | 0* |
aof-everysec |
0* | 0* | 0* | 0* |
aof-no |
0* | 0* | 0* | 0* |
hybrid |
0* | 0* | 0* | 0* |
* — ноль здесь это свойство краха процесса, а не доказательство надёжности режима. Разберём почему, потому что это самый неочевидный результат стенда.
kill -9 убивает процесс, но не операционную систему. Вызов write() отдаёт данные ядру — в page cache — синхронно, в момент выполнения самой команды, независимо от политики appendfsync. Redis вызывает write() в AOF-файл до того, как ответить клиенту OK; fsync управляет только тем, когда page cache физически сбросится на диск, а не тем, когда данные покинут процесс. Page cache — это память ядра, а не память процесса Redis. Убийство процесса её не трогает: ядро живо и спокойно допишет содержимое на диск само. Поэтому все уже подтверждённые записи переживают kill -9 в любом режиме fsync и видны сразу после рестарта.
Разница между always, everysec и no проявилась бы там, где теряется сам page cache — при падении ОС или отключении питания. everysec потерял бы до секунды записей, no — сколько успело накопиться. Этот стенд такой сбой не воспроизводит: для него нужен реальный краш хоста, а не убийство процесса. Нельзя цитировать эту строку как «appendfsync no ничего не теряет» — она про другое.
Отсюда же следует и структурное ограничение матрицы, которое честнее назвать вслух: по построению она не различает always, everysec и no. Единственный класс отказа, который она вызывает, все три режима переживают одинаково. Её содержательный результат — не ранжирование durability, а два факта: снапшот RDB не сработал, и AOF-write() переживает крах процесса.
† — эта цифра о соотношении окна теста и интервала снапшота, а не свойство RDB. Правило save 60 1 требует 60 секунд после первого изменения, а весь тест укладывается примерно в 0.8 секунды — снимок физически не успевает сработать ни разу. При киле через 61 секунду потеря была бы принципиально другой. Не цитировать как «RDB теряет 100%». Честная формулировка: RDB как единственный механизм персистентности не защищает от сбоя, случившегося раньше очередного запланированного снимка, — и в этом тесте сбой случился раньше.
Ещё одна вещь, которую этот стенд принципиально показать не может, — рваный, усечённый AOF. По построению кил прилетает строго после подтверждённого SET, когда ни одна команда не находится «в полёте», поэтому хвост журнала всегда остаётся целым: проверка последних десяти ключей перед килом даёт 10/10 в каждом режиме без потерь. Это следствие методики, а не свойство AOF. Усечённый хвост — вполне реальное явление, на то в Redis и существует aof-load-truncated; для его воспроизведения нужен кил посреди незавершённой записи, чего этот стенд намеренно не делает. Выводить отсюда «крах никогда не рвёт AOF» нельзя.
Цена always: на порядок медленнее
Побочный результат того же прогона — скорость записи до кила:
| Режим | Redis 8.8 | Valkey 8.1 |
|---|---|---|
rdb-only |
760–791ms (≈3160–3290 ops/s) | 736–754ms (≈3320–3400 ops/s) |
aof-always |
9.52–9.53s (≈262–263 ops/s) | 9.38–9.61s (≈260–267 ops/s) |
aof-everysec |
799–831ms (≈3010–3130 ops/s) | 829–853ms (≈2930–3020 ops/s) |
aof-no |
840–895ms (≈2790–2980 ops/s) | 717–786ms (≈3180–3490 ops/s) |
hybrid |
821–896ms (≈2790–3050 ops/s) | 799–830ms (≈3010–3130 ops/s) |
Как и в предыдущей статье про событийный цикл, абсолютные числа здесь несут оверхед виртуализованного стека Docker Desktop на Windows и не являются цифрами bare-metal Linux. Файловая система тоже виртуализована, поэтому семантика fsync теоретически могла бы отличаться от голого железа — правда, наблюдаемое поведение (write() переживает kill -9 независимо от fsync) полностью согласуется с задокументированной моделью page cache, так что виртуализация здесь не выглядит источником искажения. Но это предположение, а не проверенный факт: проверка потребовала бы реального падения хоста.
Относительное же сравнение внутри прогона в силе, и оно красноречиво: always без пайплайна медленнее остальных режимов на порядок — примерно 9.5 секунды против 0.7–0.9. Причина прямая: fsync на каждую команду синхронно, перед ответом OK.
Сложите это с матрицей выше — и получится точная формулировка компромисса. always покупает защиту только от последнего класса сбоев (падение ОС или питания, не процесса) ценой замедления записи на порядок. Против убийства процесса он не даёт ничего сверх того, что и так даёт everysec бесплатно. Выбирать между ними надо, понимая, от чего именно вы защищаетесь.
Где проходит граница «кэш или источник истины»
Теперь можно сформулировать то, ради чего затевалась сквозная нить серии.
Как кэш Redis/Valkey не требует ничего: персистентность опциональна, потеря приемлема по определению (данные восстановимы из источника), приоритет — скорость. Здесь rdb-only или вовсе отсутствие персистентности — осознанный и правильный выбор, а не халатность.
Как источник истины — картина меняется, и меняется она сильнее, чем кажется по одной таблице. Локальная персистентность защищает только от рестарта процесса. Она не защищает от отказа самого узла: диск умер, машина не поднялась — и AOF на ней не поможет никак. Настоящая durability распределённой системы — это ещё и реплики, и подтверждение записи на них, и бэкапы. Здесь эта статья заканчивается, а начинается следующая, про репликацию и Cluster: там выяснится, что репликация в Redis асинхронна, и подтверждённая запись может быть потеряна при failover — то есть граница durability не исчезает при добавлении реплик, а сдвигается.
Практический вывод — частью со стенда, частью из модели, которую он подтвердил в своей части:
- Если данные восстановимы из другого источника — Redis как кэш, персистентность по вкусу.
- Если данные критичны —
everysecдаёт разумный компромисс (окно потери до секунды при падении ОС, а не при крахе процесса),alwaysпокупает последний класс защиты ценой порядка производительности. Плюс реплики. Плюс бэкапы. - Если данные критичны, а терять нельзя вообще ничего — вопрос не в настройке Redis, а в том, тот ли это инструмент. Полноценная ACID-СУБД даёт эту гарантию по построению, а не настройкой.
Формулировка «AOF everysec теряет до секунды» верна — но только про падение ОС или питания. Под kill -9 процесса, как показал стенд, не теряется ничего ни в одном AOF-режиме, и разница между режимами проявляется исключительно при потере page cache ядра.
Что дальше
Персистентность — первая из трёх границ, которые серия проводит явно. Вторая — репликация: статья про Cluster и Sentinel покажет живьём, сколько подтверждённых записей перестают быть видны клиентам при разделении сети — и почему «перестают быть видны» здесь точнее, чем «теряются». Третья — память: что происходит на границе maxmemory, когда данные некуда класть, и как Redis выбирает, кем пожертвовать.
Механика транзакций (MULTI/EXEC/WATCH), которая часто идёт в связке с разговором о надёжности, разобрана отдельно — в статье «KV и документные: транзакций почти нет». Клиентские библиотеки и их поведение при недоступности сервера — в «Redis: клиенты на Go, Java, Rust». Операционная сторона — бэкап, восстановление и типовые инциденты — в завершающей статье «Redis: эксплуатация и принятие решений», и там же обнаружится ловушка, тесно связанная с этой статьёй: при включённом AOF подложенный вручную dump.rdb сервер проигнорирует полностью и молча.
Про Redis и Valkey здесь можно сказать ровно одно: матрица потерь совпала на обоих образах во всех четырёх проходах — ни один режим не дал расхождения. Скорость записи близка, но двух прогонов на ячейку мало, чтобы утверждать различие между образами в ту или другую сторону: на aof-no, например, диапазоны не пересекаются, а разброс внутри одного образа того же порядка, что и разрыв между образами. Это не «движки одинаковы по скорости» и не «различаются» — это отсутствие данных для такого вывода. Контекст форка и лицензий — в отдельной статье про ValkeyСкоро.
Источники
- Официальная документация: redis.io, раздел redis.io/docs.
- Персистентность: redis.io/docs — Persistence.
- Журналы упреждающей записи как общий приём: серия «WAL и его аналоги» — что такое WAL и что он гарантирует, WAL в разных СУБД, репликация и CDC поверх журнала.
- Стенд: digital-cookbook/databases/redis/deep-dive (
redis:8.8,valkey/valkey:8.1, модульpersistence: сценарииdurability-loss,count-recovered; оркестрацияops/persistence-kill-matrix.sh).
Комментарии