📨 Розділ 15 · Питання #14

У чому різниця між auto commit і manual commit

Різниця полягає в тому, хто та в який саме момент фіксує прогрес вичитування повідомлень у брокері:


🟢 Junior Level

30-секундна відповідь

Різниця полягає в тому, хто та в який саме момент фіксує прогрес вичитування повідомлень у брокері:

  • Auto Commit (enable.auto.commit = true): клієнтська бібліотека Kafka автоматично зберігає оффсети (зсуви) через фіксовані часові інтервали (auto.commit.interval.ms, типово 5 секунд). Зберігаються оффсети отриманих, але ще не обов’язково успішно оброблених повідомлень.
  • Manual Commit (enable.auto.commit = false): застосунок бере повний програмний контроль на себе та відправляє команду коміту (commitSync() / commitAsync()) суворо після того, як бізнес-логіка успішно відпрацювала та зафіксувала результат (наприклад, зберегла транзакцію в БД).

Наочна аналогія з життя

Уявіть автозбереження документа в текстовому процесорі:

  • Auto Commit: програма кожні 5 секунд автоматично натискає Ctrl+S, навіть якщо ви випадково вставили пошкоджений фрагмент чи стерли потрібний розділ. Якщо комп’ютер раптово вимкнеться посеред запису, документ залишиться в непередбачуваному стані.
  • Manual Commit: ви натискаєте кнопку «Зберегти» лише тоді, коли особисто перечитали весь звіт, виправили помилки та переконалися в його ідеальній якості й цілісності.

Порівняння поведінки при збої

Сценарій: Обробка повідомлення завершилася аварійним падінням JVM (Crash)

1. При Auto Commit:
   Вичитано Offset 10 -> Спрацював таймер 5 сек (Offset 10 закомічено в Kafka) ->
   JVM впала -> Рестарт сервісу -> Читання починається з Offset 11!
   ❌ Підсумок: Повідомлення 10 назавжди втрачено (Data Loss / At-Most-Once).

2. При Manual Commit:
   Вичитано Offset 10 -> Обробка впала -> Коміт НЕ викликався ->
   JVM впала -> Рестарт сервісу -> Читання починається з Offset 10!
   ✅ Підсумок: Повідомлення 10 перечитується та обробляється заново (At-Least-Once).

🟡 Middle Level

Коли насправді спрацьовує Auto Commit?

Поширена помилка: багато розробників вважають, що автокоміт виконується окремим фоновим демоном в окремому потоці. Це не так!

  • Усі операції автокоміту виконуються всередині клієнтського виклику consumer.poll().
  • Під час кожного виклику poll() клієнт перевіряє умову: System.currentTimeMillis() - lastCommitTime >= autoCommitIntervalMs.
  • Якщо умова істинна, клієнт надсилає асинхронний запит коміту останнього повернутого оффсету.
  • Наслідок: якщо ваш цикл обробки одного повідомлення завис на 30 секунд (наприклад, важкий запит до БД або повільний HTTP-виклик), жоден автокоміт не спрацює доти, доки керування знову не повернеться в метод poll().

Варіанти Manual Commit у Java

Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "order-service");
// 1. Обов'язково вимикаємо автокоміт
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");

try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
    consumer.subscribe(List.of("orders"));

    while (running) {
        ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(200));
        for (ConsumerRecord<String, String> record : records) {
            processOrder(record.value());
        }

        // 2. Ручний коміт суворо після завершення обробки всієї пачки
        try {
            consumer.commitSync(); // або consumer.commitAsync()
        } catch (CommitFailedException e) {
            log.error("Ребалансування групи перехопило партицію!", e);
        }
    }
}

Режими AckMode в Spring Kafka

В екосистемі Spring Boot розробники зазвичай не взаємодіють напряму з KafkaConsumer, а налаштовують ContainerProperties.AckMode:

Режим Spring AckMode Опис механіки
RECORD Коміт відправляється брокеру після обробки кожного окремого повідомлення.
BATCH (типово) Коміт відправляється після обробки всієї пачки повідомлень, поверненої методом poll().
TIME Коміт виконується, якщо з моменту попереднього коміту минув заданий час ackTime.
COUNT Коміт виконується після обробки визначеної кількості повідомлень ackCount.
MANUAL Розробник викликає acknowledgment.acknowledge(), коміт акумулюється до кінця батча.
MANUAL_IMMEDIATE Коміт негайно надсилається брокеру в момент виклику acknowledgment.acknowledge().

🔴 Senior Level

Архітектурні гарантії доставки: At-Most-Once vs At-Least-Once vs EOS

Вибір між Auto та Manual Commit визначає базову семантику доставки всієї розподіленої системи:

  1. At-Most-Once (щонайбільше один раз):
    • Характерно для Auto Commit.
    • Оффсет фіксується до завершення надійної обробки. У разі аварій повідомлення втрачаються, проте дублікати виключені.
    • Застосовується виключно для некритичних даних з високою частотою: метрики, клікстрім, телеметрія IoT.
  2. At-Least-Once (щонайменше один раз):
    • Досягається через Manual Commit суворо після успішної обробки.
    • Втрата даних виключена, проте в разі аварій можлива повторна обробка повідомлень.
    • Вимагає обов’язкової ідемпотентності на стороні споживача (дедуплікація за messageId у Redis/PostgreSQL).
  3. Exactly-Once Semantics (рівно один раз):
    • Неможливо досягти простим викликом commitSync().
    • Вимагає об’єднання читання з Kafka та запису в Kafka в єдину розподілену транзакцію за допомогою координатора транзакцій та методу KafkaProducer.sendOffsetsToTransaction().

Transactional Offset Commit (Read-Process-Write)

Коли сервіс вичитує повідомлення з топіка $A$ і записує результат у топік $B$, оффсет топіка $A$ фіксується не через commitSync(), а включається в транзакцію продюсера:

producer.beginTransaction();
try {
    for (ConsumerRecord<String, String> record : records) {
        String result = transform(record.value());
        producer.send(new ProducerRecord<>("output-topic", record.key(), result));
    }

    // Коміт оффсетів консьюмера в межах розподіленої транзакції продюсера!
    Map<TopicPartition, OffsetAndMetadata> offsetsToCommit = Map.of(
        new TopicPartition(record.topic(), record.partition()),
        new OffsetAndMetadata(record.offset() + 1)
    );
    
    producer.sendOffsetsToTransaction(offsetsToCommit, consumer.groupMetadata());
    producer.commitTransaction(); // Атомарна фіксація даних та оффсетів (2PC)
} catch (ProducerFencedException | OutOfOrderSequenceException e) {
    producer.abortTransaction();
}

4 Tricky Questions

1. В якому потоці фізично виконується Auto Commit, і чому налаштування auto.commit.interval.ms = 1000 не зафіксує оффсет через 1 секунду, якщо цикл обробки повідомлень триває 20 секунд?

Відповідь:

  • У KafkaConsumer немає фонового потоку для автокомітів. Клієнт Kafka працює в однопотоковій моделі.
  • Перевірка та надсилання запиту автокоміту вшиті всередину реалізації методу poll().
  • Якщо потік вичитав батч і витрачає 20 секунд на обробку (наприклад, блокуючий I/O або повільний виклик мікросервісу), метод poll() не викликається протягом усіх цих 20 секунд.
  • Жодного коміту через 1 секунду не відбудеться. Оффсет буде надіслано брокеру тільки тоді, коли цикл завершить роботу і знову викличе consumer.poll().

2. Чому Auto Commit може призвести до втрати даних (At-Most-Once), навіть якщо розробник не викликав жодних методів коміту вручну?

Відповідь:

  • Розглянемо сценарій:
    1. Виклик poll() повернув 100 повідомлень (оффсети 1–100).
    2. Обробка зайняла 6 секунд (що перевищує типовий інтервал auto.commit.interval.ms = 5000).
    3. Цикл завершився, викликається наступний poll(). У цей момент консьюмер бачить, що інтервал 5000 мс минув, і автоматично надсилає в брокер коміт оффсету 101.
    4. Новий виклик poll() повертає наступну сотню повідомлень (оффсети 101–200).
    5. Посеред обробки повідомлення 105 застосунок аварійно завершує роботу (OOM, kill -9, відключення живлення).
    6. При перезапуску група починає читання з оффсету 101, але якщо збій стався ще на кроці 3 перед виконанням бізнес-логіки нового батча, ці дані вже вважаються брокером зафіксованими і будуть назавжди пропущені.

3. Чому небезпечно викликати consumer.commitSync() вручну всередині методу Spring @KafkaListener?

Відповідь:

  • Spring Kafka (KafkaMessageListenerContainer) повністю інкапсулює життєвий цикл консьюмера та керування потоком poll().
  • Якщо всередині @KafkaListener викликати нативний commitSync() в обхід механізмів контейнера:
    1. Виникає конфлікт синхронізації з внутрішніми механізмами AckMode контейнера.
    2. Spring може спробувати закомітити оффсет повторно наприкінці ітерації батча, викликаючи надлишкове мережеве навантаження.
    3. У разі виникнення помилки в бізнес-логіці Spring не зможе коректно застосувати обробник помилок (DefaultErrorHandler, DLT), оскільки ви вже закомітили оффсет у брокері, і повторне перенаправлення в Dead Letter Topic стане неможливим.
  • Правильне рішення у Spring: використовувати аргумент методу Acknowledgment.acknowledge() разом з AckMode.MANUAL або MANUAL_IMMEDIATE.

4. У чому різниця між AckMode.MANUAL та AckMode.MANUAL_IMMEDIATE у Spring Kafka, і коли вибір критичний для продуктивності?

Відповідь:

  • MANUAL: виклик acknowledgment.acknowledge() лише виставляє в пам’яті прапорець готовності коміту для конкретного повідомлення. Фізичний мережевий запит до Kafka надсилається контейнером лише після того, як усі записи з поточного батча poll() будуть оброблені. Це мінімізує мережеві витрати (один коміт на весь батч).
  • MANUAL_IMMEDIATE: виклик acknowledgment.acknowledge() негайно ініціює мережевий виклик commitSync / commitAsync на кожен виклик методу.
  • Коли критично: якщо батч містить 500 повідомлень, MANUAL_IMMEDIATE згенерує 500 мережевих звернень до брокера поспіль, що обвалить пропускну здатність консьюмера в десятки разів. Його варто використовувати лише для рідкісних, критичних транзакцій з поодинокими повідомленнями.

🎯 Шпаргалка для інтерв’ю

Головне про порівняння

  • Auto Commit: фоновий у межах виклику poll(), за таймером auto.commit.interval.ms. Не знає, чи успішна бізнес-логіка. Ризик втрати даних.
  • Manual Commit: повний контроль розробника. Коміт лише після гарантії успішної обробки та збереження в БД.
  • At-Least-Once: досягається через ручний коміт + ідемпотентна обробка на приймачі.
  • Spring Kafka: використовуйте AckMode.MANUAL з інтерфейсом Acknowledgment, ніколи не викликайте нативний consumer.commitSync() вручну всередині слухача.

Порівняльна таблиця

| Параметр | Auto Commit | Manual Commit | | :— | :— | :— | | Параметр конфігурації | enable.auto.commit = true | enable.auto.commit = false | | Момент коміту | Всередині poll() за таймером | Явний виклик у коді розробника | | Гарантія доставки | At-Most-Once (можлива втрата) | At-Least-Once (можливі дублікати) | | Складність коду | Мінімальна (працює «з коробки») | Вимагає обробки помилок і ребалансування | | Застосовність у Prod | ❌ Метрики, логи, аналітика | ✅ Фінанси, замовлення, критичні бізнес-дані |

Червоні прапорці на співбесіді (Чого говорити не можна)

  • ❌ «Автокоміт виконується незалежним фоновим демоном раз на 5 секунд» — він виконується суворо всередині потоку poll().
  • ❌ «Для надійної системи в продакшені достатньо ввімкнути автокоміт» — автокоміт гарантує втрату повідомлень під час аварійних падінь.
  • ❌ «У методі @KafkaListener я завжди викликаю consumer.commitSync()» — груба помилка архітектури Spring Kafka, яка ламає DefaultErrorHandler.

Пов’язані запитання