Репликация: master → replicas и границы гарантий
Предпосылки: репликация (sync/async, replication lag, failover, split brain, кворум), RDB, AOF, сеть (клиент/сервер).
← Проектирование ключей | Sentinel →
В Redis: master и replicas
Один Redis-сервер — единая точка отказа: при его падении приложение теряет и данные, и доступ к кешу. Репликация создаёт копии данных на других серверах. В Redis мастер принимает запись, а реплики получают snapshot + поток команд. Это даёт две вещи: «горячую» копию для переключения при сбое (автоматически — через Sentinel) и возможность разгрузить мастер чтениями. Общая теория и компромиссы репликации — в отдельной заметке.
flowchart LR CW["clients (writes)"] --> M["master"] CR["clients (reads)"] -.->|"опционально"| R["replicas"] M -->|"snapshot + поток команд"| R
Поток репликации идёт только от мастера к репликам. Поэтому реплики по умолчанию read-only (replica-read-only yes): запись на реплике не попадёт на мастер и останется только на этой реплике. Следующее обновление из мастера перезапишет состояние реплики, и такая запись пропадёт.
Важно: данные на каждом сервере — полная копия. Репликация масштабирует чтение и отказоустойчивость, но не увеличивает ёмкость хранения и не масштабирует запись.
Реплика подключается к мастеру командой REPLICAOF host port (или через конфигурацию replicaof). Дальше репликация проходит через несколько стадий: сначала реплика получает полный снимок данных мастера, затем выходит на штатный режим — мастер потоково отправляет ей команды записи. Если связь обрывается и восстанавливается, мастер пытается отправить только пропущенные команды из буфера. Если буфера не хватает — реплика получает полную копию заново.
Полная синхронизация: snapshot + догон по буферу
При первом подключении реплики (или после длительного рассинхрона) мастер должен передать ей все данные целиком — это полная синхронизация. Мастер создаёт snapshot текущего состояния и отправляет его реплике по сети, а все команды записи, поступающие за время создания и передачи snapshot, буферизирует. Реплика загружает snapshot, получает буферизированные команды и выходит на актуальное состояние.
Создание snapshot — самая затратная часть. Мастер запускает фоновое сохранение (BGSAVE), которое создаёт дочерний процесс через fork() с copy-on-write — физические страницы не копируются, пока родитель не изменит их. Чем активнее запись в мастер во время BGSAVE, тем больше страниц копируется: растёт потребление RAM пропорционально объёму изменений. Помимо памяти, передача snapshot нагружает сеть и (в disk-based варианте) диск.
Если диск медленный, а сеть быстрая, Redis может обойти запись на диск: diskless-репликация (repl-diskless-sync yes) стримит RDB-данные напрямую в сокет реплики.
Полная синхронизация настолько дорога, что Redis старается избежать её при кратковременных разрывах связи.
Частичная ресинхронизация: replication offset и backlog
При кратковременном разрыве связи повторять полную синхронизацию расточительно — достаточно отправить реплике только те команды, которые она пропустила. Это частичная ресинхронизация.
Чтобы она работала, и мастер, и реплика отслеживают позицию в потоке команд записи — replication offset. Каждая выполненная команда увеличивает offset. Разница между offset мастера и реплики показывает, насколько реплика отстала.
Сами команды мастер хранит в кольцевом буфере — replication backlog (repl-backlog-size, по умолчанию 1 МБ). Когда реплика переподключается и сообщает свой offset, мастер проверяет: если пропущенные команды ещё в backlog — отправляет их. Если backlog уже перезаписался (реплика отсутствовала слишком долго) — частичная ресинхронизация невозможна, и запускается полная.
Помимо offset, реплика передаёт replication ID — уникальный идентификатор потока репликации конкретного мастера. Если реплика подключается к тому же мастеру, ID совпадает и достаточно проверить backlog. Но если мастер сменился (например, старый мастер вышел из строя и его место заняла другая реплика — как это происходит автоматически, описано в Sentinel), у нового мастера будет другой replication ID. Redis хранит два ID (текущий и предыдущий), что позволяет провести частичную ресинхронизацию с репликами, которые были подключены к тому же потоку данных до смены мастера. Если и это не помогает — полная синхронизация неизбежна.
Размер backlog стоит подбирать исходя из объёма записи и допустимого времени отключения: repl-backlog-size = средний_объём_записи_в_секунду × допустимое_время_отключения. Если backlog слишком мал, это проявляется в логах Redis как полная ресинхронизация (Full resync) при кратковременных разрывах связи, и в метрике sync_full из INFO replication, которая растёт при каждой полной синхронизации. Мастер создаёт backlog при подключении первой реплики. Если все реплики отключились, backlog сохраняется в течение repl-backlog-ttl секунд (по умолчанию 3600), после чего освобождается.
Асинхронность: устаревшие чтения и окно потери
В Redis репликация по умолчанию асинхронная: мастер отвечает клиенту, не дожидаясь подтверждения от реплик. Общие последствия (lag и окно потери вокруг failover) описаны в репликации.
Практическое следствие в Redis: если приложение читает с реплик или опирается на «подтверждённая запись не потеряется», нужно явно сузить окно риска инструментами ниже.
WAIT: сузить окно потери
Приложение записало критичную операцию (подтверждение заказа, перевод денег). Как убедиться, что запись дошла хотя бы до одной реплики до того, как клиент получит OK?
Команда WAIT numreplicas timeout блокирует клиента до тех пор, пока указанное количество реплик не подтвердит получение всех предшествующих команд, или до истечения таймаута.
redis-cli SET order:789 confirmed
redis-cli WAIT 1 5000
# → 1 (одна реплика подтвердила)
# или → 0 (таймаут 5 секунд истёк, ни одна реплика не подтвердила)WAIT не превращает Redis в синхронно реплицируемую систему. Даже если WAIT вернул подтверждение, при failover может быть выбрана другая реплика — та, которая не успела получить команду. Тем не менее WAIT заметно сужает окно потери и полезен для операций, где потеря подтверждённой записи критична.
На стороне сервера есть ещё один рычаг: можно запретить мастеру принимать записи, если «живых и свежих» реплик меньше заданного порога (min-replicas-to-write + min-replicas-max-lag). Это повышает шанс сохранить подтверждённые записи при failover, но снижает доступность: при проблемах с репликами мастер начнёт возвращать ошибки на запись.
Каскадная репликация: разгрузить мастер
При большом количестве реплик нагрузка на мастер растёт линейно: каждая реплика — это отдельный поток команд и, при полной синхронизации, отдельный RDB-снимок. Реплика может быть мастером для другой реплики (REPLICAOF replica-host replica-port), образуя цепочку или дерево. Типичная топология: мастер синхронизирует 2–3 реплики первого уровня, каждая из которых синхронизирует ещё несколько. Это снижает нагрузку на мастер, но увеличивает лаг репликации — команда проходит больше узлов, прежде чем дойдёт до реплики на конце цепочки. Для сценариев, где допустим лаг в единицы секунд (кеширование, аналитические запросы), это приемлемый компромисс.
Автоматическая смена мастера при его падении — задача Sentinel. Шардинг и горизонтальное масштабирование записи — задача Redis Cluster.
Sources
- Redis Documentation: Replication (Redis OSS 8.2). https://redis.io/docs/management/replication/
- Redis config (Redis OSS 8.2):
min-replicas-to-write,min-replicas-max-lag,repl-backlog-size,repl-diskless-sync. https://redis.io/docs/latest/operate/oss_and_stack/management/config/ - Redis source:
src/replication.c