Партиции, офсеты и consumer lag в Kafka: как не терять сообщения
Зачем вообще партиции
Kafka масштабируется не за счёт одного большого лога, а за счёт разбиения топика на партиции. Каждая партиция — это независимый упорядоченный, неизменяемый лог записей на диске. Порядок гарантируется только внутри партиции, между партициями его нет и быть не может. Это первое, что проверяют на собесе: если нужен строгий порядок сообщений — все они должны лежать в одной партиции, то есть иметь одинаковый ключ (или null-ключ с ручным выбором партиции).
Офсет — это не ID сообщения
Каждая запись в партиции получает офсет — монотонно растущее число, уникальное в пределах партиции. Офсет не глобален для топика, а привязан именно к партиции. Consumer хранит свою позицию чтения (committed offset) — это точка, до которой он гарантированно обработал сообщения. При рестарте consumer продолжает именно с неё.
Partition 0: [msg0][msg1][msg2][msg3] <- offset 0..3 Partition 1: [msg0][msg1][msg2] <- offset 0..2 Consumer group "orders-service": partition 0 -> committed offset 2 (lag = 1, не прочитан msg3) partition 1 -> committed offset 2 (lag = 0)
Consumer lag — главный показатель здоровья системы
Lag — это разница между последним записанным офсетом в партиции (log end offset) и committed offset консьюмера. Если lag растёт — консьюмер не успевает обрабатывать поток, и это первый сигнал проблемы: либо consumer медленный, либо продюсеры льют больше, чем система способна переварить, либо партиций слишком мало для нужного параллелизма.
На собесе часто спрашивают: как увеличить throughput чтения? Ответ — увеличить число партиций (до числа консьюмеров в группе, больше смысла нет, лишние консьюмеры просто простаивают) или ускорить обработку внутри одного consumer (батчинг, асинхронная запись в БД, отдельный пул для тяжёлой логики).
Когда коммитить офсет
Это ключевая развилка, которую спрашивают почти всегда:
- At-most-once — коммитишь офсет до обработки сообщения. Если упал во время обработки — сообщение потеряно, но дублей не будет.
- At-least-once — коммитишь офсет после успешной обработки. Если упал после обработки, но до коммита — при рестарте сообщение прочитается заново. Дубли возможны, потерь нет.
- Exactly-once — достигается транзакциями Kafka или идемпотентной обработкой на стороне consumer (например, upsert по бизнес-ключу в БД).
В 95% реальных систем выбирают at-least-once плюс идемпотентность на уровне обработчика — это дешевле, чем городить распределённые транзакции.
Rebalance и потеря позиции
Когда в группе меняется состав консьюмеров (кто-то упал, кто-то добавился), происходит ребалансировка партиций между оставшимися консьюмерами. Если офсет не закоммичен до начала ребалансировки, новый владелец партиции начнёт читать с последнего committed offset — и часть уже обработанных, но не закоммиченных сообщений обработается повторно. Отсюда правило: коммитить офсет нужно как можно скорее после фактической обработки, а не батчами "раз в минуту для производительности", если важна consistency.
Как читать график lag на практике
Lag, который колеблется вокруг нуля — норма. Lag, который монотонно растёт — consumer не успевает, нужно либо скейлить consumer group, либо смотреть, что тормозит обработку (блокирующий I/O, медленная БД, GC-паузы). Lag, который резко подскочил и держится — скорее всего, consumer упал и не перезапускается, или rebalance завис. На собесе полезно показать, что ты умеешь не просто назвать термин, а объяснить, какие действия предпринимать при каждом паттерне.