Репликация и ISR
Kafka может пережить потерю брокера без потери данных. Это не магия — это точно определённый протокол репликации на основе ISR (In-Sync Replicas). Понимание этого протокола критически важно: неправильная конфигурация acks и min.insync.replicas — одна из самых частых причин потери данных в production.
Модель лидер/фолловер: все записи через лидера
Каждая партиция имеет ровно одного лидера. Все продюсерские запросы (ProduceRequest) и потребительские запросы (FetchRequest) обрабатываются лидером.
Фолловеры не взаимодействуют с клиентами. Они постоянно посылают лидеру FetchRequest с указанием своего текущего LEO (Log End Offset):
Follower → Leader: FetchRequest { offset: 100, max_bytes: 1MB }
Leader → Follower: FetchResponse { records: [offset 100..150] }
Это pull-модель репликации: фолловер сам запрашивает данные, лидер не толкает их. Преимущество: фолловер контролирует темп репликации, нет риска перегрузки медленной реплики.
ISR: In-Sync Replicas — реплики в синхронизации
ISR (In-Sync Replicas) — это множество реплик, которые “достаточно синхронизированы” с лидером. Определение “достаточно”: реплика находится в ISR, если она запрашивала данные у лидера в пределах последних replica.lag.time.max.ms миллисекунд (по умолчанию 30 секунд).
Если фолловер перестал запрашивать данные (брокер упал, сеть пропала, GC pause) — через 30 секунд он выбывает из ISR. Лидер обновляет список ISR в metadata log через KRaft-контроллер.
Список ISR для партиции виден в выводе kafka-topics.sh --describe:
Topic: orders Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3
# ^^^^^^^^^^^^
# ISR = все три реплики
Если брокер 2 упал:
Topic: orders Partition: 0 Leader: 1 Replicas: 1,2,3 Isr: 1,3
# ^^^^^^^
# Брокер 2 выбыл из ISR
LEO: Log End Offset — граница каждой реплики
LEO (Log End Offset) — следующий свободный offset в реплике. Если реплика содержит записи 0..99, её LEO = 100.
LEO у каждой реплики свой. Лидер знает LEO всех фолловеров из их FetchRequest-запросов. После получения запроса FetchRequest { offset: 95 } лидер знает: у этого фолловера LEO = 95 (он прочитал всё до offset 95).
High Watermark: граница безопасного чтения
High Watermark (HW) — это minimum LEO среди всех реплик в ISR. Потребители могут читать записи только до HW.
Формально: HW = min(LEO_leader, LEO_isr_replica_1, LEO_isr_replica_2, ...)
Зачем это нужно? Запись, которую прочитал потребитель, должна быть гарантированно не потеряна при смене лидера. Если потребитель прочитал offset 99, но фолловер реплицировал только до offset 95 — и в этот момент лидер упал, новым лидером стал фолловер с LEO=95 — потребитель увидит данные, которых нет в новом лидере. Это разрыв консистентности.
HW предотвращает это: потребитель читает только то, что гарантированно есть у всех ISR-реплик.
Leader (Broker 1)
Leader (Broker 1): LEO = 103. Принял записи 100, 101, 102. Ждёт подтверждения от ISR-реплик.Follower 1 (Broker 2)
Follower 1 (Broker 2): LEO = 101. Получил записи 100, ещё не реплицировал 101 и 102. Отстаёт от лидера.Follower 2 (Broker 3)
Follower 2 (Broker 3): LEO = 103. Полностью синхронизирован с лидером.HW = min(103, 101, 103) = 101
High Watermark = min(103, 101, 103) = 101. Потребители могут читать только до offset 100 включительно. Записи 101 и 102 ещё не 'committed' с точки зрения потребителей.Consumer: читает 0..100
Consumer: читает только до HW-1 = offset 100. Записи 101 и 102 невидимы до тех пор, пока Follower 1 их не реплицирует и HW не сдвинется.HW продвигается вперёд: когда фолловер реплицирует очередную запись и присылает FetchRequest с обновлённым LEO, лидер пересчитывает HW. Если все ISR-реплики дошли до LEO=103, HW сдвигается до 103, и потребители могут читать записи 101 и 102.
acks: гарантии подтверждения для продюсера
Параметр acks (acknowledgements) определяет, от скольких реплик продюсер должен получить подтверждение перед тем, как считать отправку успешной:
acks=0 — продюсер не ждёт никакого подтверждения. Fire-and-forget. Максимальная скорость, нулевые гарантии. Даже если брокер упал в момент записи — продюсер об этом не узнает. Использовать только для метрик, логов, где потеря допустима.
acks=1 — продюсер ждёт подтверждения от лидера. Лидер записал в свой лог — ОК. Фолловеры могут отставать. Если лидер упадёт до репликации — данные потеряны. Это значение по умолчанию до Kafka 3.0; в Kafka 4.0 — acks=all.
acks=all (или acks=-1) — продюсер ждёт подтверждения от всех ISR-реплик. Лидер отвечает только после того, как все ISR-реплики записали и подтвердили. Максимальная надёжность.
from kafka import KafkaProducer
producer = KafkaProducer(
bootstrap_servers=['localhost:9092'],
acks='all', # ждать всех ISR реплик
retries=10, # повторять при временных сбоях
retry_backoff_ms=100 # 100ms между попытками
)
min.insync.replicas: нижняя граница ISR
Вот ловушка: acks=all требует подтверждения от всех реплик в ISR. Если ISR содержит только лидера (все фолловеры выбыли) — acks=all требует подтверждения только от лидера. Это эквивалентно acks=1.
Решение: min.insync.replicas (min.isr) — минимальное число реплик в ISR, при котором лидер принимает запись. Если ISR меньше min.isr — лидер отклоняет ProduceRequest с NotEnoughReplicasException.
# Настройка на уровне топика
kafka-topics.sh --alter \
--topic orders \
--config min.insync.replicas=2 \
--bootstrap-server localhost:9092
# Или в server.properties (глобальный default)
min.insync.replicas=2
Рекомендуемая конфигурация для production (RF=3):
# Гарантии durability
acks=all
min.insync.replicas=2
Это означает: запись считается успешной, только если она записана минимум на 2 из 3 реплик (лидер + хотя бы 1 фолловер). Кластер переживает потерю 1 брокера без потери записанных данных.
Если ISR сжимается до 1 реплики (лидер) и min.insync.replicas=1 (default) — acks=all требует только одного подтверждения, что неотличимо от acks=1. Устанавливайте min.insync.replicas=2 для топиков с требованиями надёжности, независимо от acks. Без этого гарантии durability иллюзорны.
Eligible Leader Replicas (ELR): предварительный просмотр Kafka 4.0
Kafka 4.0 вводит концепцию ELR (Eligible Leader Replicas) — реплик, которые квалифицированы стать лидером даже при выходе за пределы ISR.
Классическая проблема: если все ISR-реплики потеряны (например, все 3 брокера перезагрузились), Kafka отказывается выбрать нового лидера из оставшихся реплик (они не в ISR) — это защита от потери данных. Администратор должен вручную разрешить “unclean leader election”. ELR расширяет множество кандидатов: реплики, которые были в ISR незадолго до сбоя, остаются “eligible” для election на короткий период. Это уменьшает необходимость в ручном вмешательстве при массовых перезагрузках.
ELR — preview в Kafka 4.0, полная GA запланирована в 4.x.
Ключевые выводы
- ISR = реплики, синхронизированные с лидером в пределах
replica.lag.time.max.ms. Лидер всегда в ISR. - LEO (Log End Offset) — next offset для записи, уникален для каждой реплики.
- HW (High Watermark) =
min(LEO всех ISR-реплик). Потребитель читает только до HW. - acks=all + min.insync.replicas=2 — правильная комбинация для production durability.
- Если ISR = 1 и min.isr = 1 — acks=all эквивалентен acks=1. Это ловушка.