Главная › Блог › Партиции, офсеты и consumer lag в Kafka: как не терять сообщения

Партиции, офсеты и 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 плюс идемпотентность на уровне обработчика — это дешевле, чем городить распределённые транзакции.

Чем отличается auto-commit от ручного коммита офсетов?
Auto-commit коммитит офсет по таймеру в фоне, независимо от того, успешно ли обработано сообщение — это риск потери данных при падении между коммитом и обработкой. Ручной commitSync/commitAsync вызывается явно после обработки батча, что даёт контроль над гарантиями доставки.

Rebalance и потеря позиции

Когда в группе меняется состав консьюмеров (кто-то упал, кто-то добавился), происходит ребалансировка партиций между оставшимися консьюмерами. Если офсет не закоммичен до начала ребалансировки, новый владелец партиции начнёт читать с последнего committed offset — и часть уже обработанных, но не закоммиченных сообщений обработается повторно. Отсюда правило: коммитить офсет нужно как можно скорее после фактической обработки, а не батчами "раз в минуту для производительности", если важна consistency.

Частая ошибка — коммитить офсет батчем из poll(), не дожидаясь, пока обработаются все сообщения батча. Если упадёт на середине батча — половина сообщений потеряется, потому что офсет уже сдвинут вперёд.

Как читать график lag на практике

Lag, который колеблется вокруг нуля — норма. Lag, который монотонно растёт — consumer не успевает, нужно либо скейлить consumer group, либо смотреть, что тормозит обработку (блокирующий I/O, медленная БД, GC-паузы). Lag, который резко подскочил и держится — скорее всего, consumer упал и не перезапускается, или rebalance завис. На собесе полезно показать, что ты умеешь не просто назвать термин, а объяснить, какие действия предпринимать при каждом паттерне.

Ещё разборы