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

Як працює компресія повідомлень

На відміну від класичних черг повідомлень (JMS, RabbitMQ), де стиснення найчастіше налаштовується вручну на рівні тіла кожного окремого повідомлення, у Kafka стиснення є вбудова...


🟢 Junior Level

Що таке компресія повідомлень у Kafka

Компресія (стиснення даних) в Apache Kafka — це процес зменшення обсягу переданих та збережених даних шляхом застосування алгоритмів стиснення без втрат (lossless compression) до пакетів повідомлень (батчів, ProducerBatch).

На відміну від класичних черг повідомлень (JMS, RabbitMQ), де стиснення найчастіше налаштовується вручну на рівні тіла кожного окремого повідомлення, у Kafka стиснення є вбудованим платформеним механізмом, який застосовується на стороні Producer до цілого батча записів.

Відправка без компресії:
App ──send(100 повідомлень)──> [Пакет 100 КБ] ──Мережа──> Брокер (Диск: 100 КБ) ──Мережа──> Consumer

Відправка з компресією (наприклад, LZ4):
App ──send(100 повідомлень)──> [Пакет 100 КБ] 
                                    │ (стиснення на клієнті)
                                    ▼
                             [Пакет 25 КБ]  ──Мережа──> Брокер (Диск: 25 КБ)  ──Мережа──> Consumer
                                                                                        │ (розпакування)
                                                                                        ▼
                                                                                 [Пакет 100 КБ]

Навіщо потрібна компресія

  1. Зниження мережевого трафіку (Network Bandwidth): Скорочення обсягу переданих байтів між продюсерами, брокерами та консьюмерами в 2–7 разів, що критично при міжсегментній передачі (cross-AZ, cross-region у хмарних провайдерів, де за вихідний трафік стягується плата).
  2. Економія дискового простору: Логи партицій (.log) зберігаються на брокерах у стиснутому вигляді, що багаторазово збільшує глибину зберігання (retention period) без закупівлі додаткових NVMe/SSD дисків.
  3. Ефективне використання Page Cache: В ОС брокера вміщується значно більше стиснутих сегментів лога, що підвищує кеш-хіти та знижує фізичні читання з диска.
  4. Зростання загальної пропускної здатності (Throughput): Мережеві інтерфейси та шини передачі даних перестають бути вузьким місцем.

Доступні кодеки стиснення

Kafka Producer підтримує чотири алгоритми стиснення (параметр compression.type):

  • none — стиснення вимкнено (за замовчуванням).
  • lz4 — рекомендований стандарт індустрії для більшості enterprise-систем. Забезпечує екстремально швидку компресію та декомпресію при дуже низькому споживанні CPU.
  • snappy — розроблений Google. Відмінна швидкість роботи при помірному стисненні, історичний фаворит для low-latency систем.
  • zstd (Zstandard) — розроблений Meta (Facebook). Забезпечує рекордний коефіцієнт стиснення (на рівні GZIP) при швидкості декомпресії, близькій до Snappy/LZ4. Підтримує тонке налаштування рівнів компресії.
  • gzip — класичний алгоритм з найвищим коефіцієнтом стиснення, але вкрай повільний і ненажерливий до CPU (створює серйозний bottleneck).

Базове налаштування Producer у Java

Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092,kafka-broker2:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

// 1. Увімкнення кодека компресії (lz4, zstd, snappy, gzip)
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");

// 2. Компресія працює ефективно тільки за наявності батчингу!
// Задаємо обсяг батча 64 КБ і затримку накопичення 10 мс
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 65536);
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

🟡 Middle Level

Порівняльний аналіз алгоритмів стиснення

Вибір кодека компресії завжди є компромісом (trade-off) між навантаженням на CPU та економією мережі / диска:

Кодек Коефіцієнт стиснення Швидкість стиснення Швидкість розпакування Навантаження на CPU Рекомендований сценарій
none 1.0 (немає) Миттєво Миттєво 0% Локальне налагодження, CPU-bound продюсери з величезним запасом мережі
lz4 Середній (~2.5–4.5x) Екстремально висока Екстремально висока Мінімальне (~3–5%) Вибір за замовчуванням для 90% мікросервісів та high-throughput потоків
snappy Помірний (~2.0–3.5x) Дуже висока Дуже висока Низьке (~5–8%) Системи з жорсткими SLA щодо latency, легасі-середовища
zstd Максимальний (~3.5–7.0x) Налаштовувана Дуже висока Середнє (~10–20%) Міжрегіональна реплікація (MirrorMaker), довгий retention, економія хмарного трафіку
gzip Дуже високий (~3.0–6.0x) Низька Помірна Дуже високе (>35%) Не рекомендується для сучасних систем через сильну деградацію пропускної здатності

Чому стиснення виконується саме на батчі (Batch-level Compression)

Словникові алгоритми (LZ4, ZSTD, Deflate) знаходять повторювані підрядки і замінюють їх посиланнями на раніше знайдені фрагменти.

  • Стиснення поодинокого повідомлення (наприклад, JSON на 200 байт): Містить заголовки та ключі {"userId": 12345, "event": "CLICK"}. Усередині одного JSON повторюваності майже немає. Заголовок самого алгоритму компресії займає 15–30 байт. У результаті стиснуте повідомлення може виявитися більшим за оригінал!
  • Стиснення батча зі 100–500 таких повідомлень (40–100 КБ): Ключі JSON ("userId", "timestamp", "eventType"), метадані та схеми повторюються сотні разів. Алгоритм формує єдиний словник повторів усередині батча, стискаючи дані в 3–5 разів.

Важливо: Без увімкненого батчингу (linger.ms > 0 та адекватного batch.size) компресія практично втрачає ефективність!

Конфігурація брокера та топіка: compression.type

Поведінка брокера при прийомі повідомлень керується параметром compression.type на рівні топіка або всього кластера (server.properties):

# 1. За замовчуванням на брокері встановлено значення "producer"
# Брокер НЕ розпаковує батч, а зберігає його в лог у форматі продюсера
kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name orders \
  --alter --add-config compression.type=producer

# 2. Примусове задання алгоритму на рівні топіка (наприклад, lz4)
kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name telemetry \
  --alter --add-config compression.type=lz4

Можливі значення compression.type на брокері:

  • producer (рекомендується): брокер приймає стиснутий батч і записує його на диск без змін.
  • uncompressed: брокер примусово розпаковує вхідний батч і зберігає його на диску нестиснутим.
  • lz4 / snappy / zstd / gzip: брокер перевіряє кодек батча. Якщо продюсер надіслав батч в іншому кодеку (або нестиснутим), брокер зобов’язаний розпакувати його і заново стиснути вказаним алгоритмом.

🔴 Senior Level

Наскрізний життєвий цикл даних та архітектура Zero-Copy

Головна інженерна перевага Kafka щодо компресії — це збереження принципу Zero-Copy транзиту даних через брокер.

+──────────────────────────────────────────────────────────────────────────────+
|                           PRODUCER (JVM + JNI)                               |
| 1. Серіалізація Record -> RecordAccumulator -> Наповнення ProducerBatch      |
| 2. Завершення батча -> Виклик JNI (liblz4 / libzstd) в Off-Heap буфері       |
| 3. Формування RecordBatch Header (Magic 2, Attributes: bits 0-2 = Codec)     |
| 4. Відправка через Java NIO SocketChannel -> Мережа                          |
+──────────────────────────────────────────────────────────────────────────────+
                                        │
                                        ▼ (Стиснутий мережевий пакет TCP)
+──────────────────────────────────────────────────────────────────────────────+
|                               BROKER (Linux)                                 |
| 1. Читання TCP сокета в мережевий буфер                                      |
| 2. Валідація заголовка батча: базова перевірка CRC, Offset, Attributes       |
| 3. НЕМАЄ ДЕКОМПРЕСІЇ: стиснуті байти записуються напряму в Page Cache через   |
|    FileChannel.write() -> Сегмент лога *.log                                 |
| 4. Запит Consumer (FetchRequest):                                            |
|    Брокер використовує системний виклик sendfile(2) (Zero-Copy):             |
|    Дані з Page Cache передаються напряму в сокет мережевої карти (NIC),      |
|    минаючи User Space брокера і не навантажуючи його CPU!                    |
+──────────────────────────────────────────────────────────────────────────────+
                                        │
                                        ▼ (Стиснутий мережевий пакет TCP)
+──────────────────────────────────────────────────────────────────────────────+
|                           CONSUMER (JVM + JNI)                               |
| 1. Отримання стиснутого RecordBatch через мережевий сокет                    |
| 2. Парсинг заголовка: читання бітів компресії в полі Attributes             |
| 3. Розпакування через JNI в пам'ять JVM                                      |
| 4. Ітерація по записах ConsumerRecord і передача в poll()                    |
+──────────────────────────────────────────────────────────────────────────────+

Формат запису RecordBatch та біти атрибутів (Magic Byte 2)

Починаючи з Kafka 0.11 (KIP-98 / KIP-101), використовується формат повідомлень RecordBatch. У заголовку пакета присутнє 16-бітне поле attributes:

  • Біти 0–2 (Compression Codec):
    • 0b000 (0): No compression
    • 0b001 (1): GZIP
    • 0b010 (2): Snappy
    • 0b011 (3): LZ4
    • 0b100 (4): Zstandard (ZSTD)
  • Бит 3 (Timestamp Type): CreateTime (0) або LogAppendTime (1).
  • Бит 4 (Is Transactional): 0 — ні, 1 — транзакційний батч.
  • Бит 5 (Is Control Batch): маркер коміту/аборту транзакції.

Оскільки тип компресії закодований у заголовку кожного батча, один і той самий топік і навіть одна партиція теоретично можуть містити батчі, стиснуті різними кодеками, і консьюмер без проблем їх прочитає.

Небезпека перекомпресії на брокері (Broker Recompression Trap)

Коли брокер працює в штатному режимі compression.type = producer, навантаження на його процесор близьке до нуля (працює лише I/O).

Проте виникає сценарій катастрофічної деградації кластера — Broker Recompression:

  1. Невідповідність налаштувань: Топік налаштований з compression.type=zstd, а група продюсерів шле дані з compression.type=lz4 або none.
  2. Перехід на LogAppendTime: Якщо на топіку виставлено message.timestamp.type=LogAppendTime, брокер зобов’язаний перезаписати мітку часу в кожному внутрішньому записі батча.
  3. Даунгрейд версій протоколу (Message Format Down-Conversion): Якщо старий консьюмер (до Kafka 0.11, v0/v1) запитує дані з кластера з форматом v2, брокер зобов’язаний розпакувати RecordBatch, переконвертувати повідомлення в MessageSet v0/v1 і заново запакувати їх на льоту.

Наслідки перекомпресії на брокері:

  • Брокер декомпресує батч у Heap/Off-Heap пам’ять.
  • Розпаковані повідомлення валідуються, оновлюються заголовки та зсуви (baseOffset).
  • Брокер запускає власний компресор для повторного стиснення.
  • Zero-Copy повністю вимикається.
  • Утилізація CPU брокерів злітає до 100%, latency запитів Produce і Fetch зростає в сотні разів, кластер ризикує випасти з ISR за таймаутами.

Низькорівнева реалізація: JNI та Off-Heap пам’ять

Клієнти Kafka на Java використовують нативні C/C++ бібліотеки через JNI:

  • LZ4: org.lz4:lz4-java (використовує JNI-біндінги або pure Java / unsafe fallback, якщо нативний бінарник недоступний).
  • Zstandard: com.github.luben:zstd-jni (прямі JNI-виклики до нативної реалізації Zstandard).
  • Snappy: org.xerial.snappy:snappy-java.

При стисненні дані з пулу BufferPool продюсера (ByteBuffer) передаються через JNI напряму в нативну пам’ять. Це запобігає проміжному створенню масивів byte[] у Java Heap, захищаючи збирач сміття (GC) від навантаження та виключаючи Stop-the-World паузи при високих RPS.

Тонке налаштування ZSTD: Рівні стиснення

Алгоритм ZSTD підтримує рівні компресії від 1 до 22 (за замовчуванням у Kafka — 3). Починаючи з Kafka 2.1, продюсер дозволяє задавати рівень стиснення:

props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "zstd");
// Тонке налаштування рівня компресії Zstandard
props.put("compression.zstd.level", 3); // 3 — оптимальний баланс CPU/Ratio
  • Рівень 1 забезпечує продуктивність, близьку до LZ4, зі стисненням кращим за Snappy.
  • Рівні 7–9 підходять для пакетних ETL-процесів та архівних логів.
  • Рівні вище 12 не рекомендуються для real-time систем через експоненційне зростання затримки на стороні Producer.

4 Tricky Questions

1. Як компресія впливає на обмеження max.message.bytes на брокері та max.request.size на продюсері?

Відповідь: Перевірка лімітів на клієнті та брокері виконується над стиснутим розміром батча:

  1. На продюсері: параметр max.request.size (за замовчуванням 1 МБ) обмежує сумарний розмір серіалізованого та стиснутого мережевого запиту.
  2. На брокері: параметр топіка max.message.bytes (за замовчуванням 1 048 588 байт ~ 1 МБ) перевіряє розмір вхідного RecordBatch після стиснення.
  3. Пастка при декомпресії на консьюмері: Якщо продюсер зібрав батч із нестиснутих повідомлень сумарним обсягом 4 МБ, стиснув його за допомогою ZSTD до 800 КБ, брокер успішно прийме і запише цей батч (800 КБ < 1 МБ). Проте коли консьюмер прочитає цей батч, йому знадобиться виділити 4 МБ пам’яті в JVM Heap для розпакування. Якщо у консьюмера параметр max.partition.fetch.bytes менший за 800 КБ, консьюмер взагалі не зможе отримати цей батч і зависне в нескінченному циклі.

2. Чи може ввімкнення компресії зменшити затримку (end-to-end latency) повідомлень у високонавантаженій системі?

Відповідь: Так, парадоксальним чином може. Хоча компресія додає мікросекунди на етапі стиснення в CPU продюсера та декомпресії на консьюмері:

  1. У high-throughput системах мережевий стек (TCP socket buffers, bandwidth) та дисковий контролер часто є головним вузьким місцем (bottleneck).
  2. Стиснення в 3–4 рази зменшує розмір TCP-фреймів, скорочує кількість round-trips та window scaling затримок у протоколі TCP.
  3. Стиснутий батч передається по мережі в рази швидше, ніж сумарний час, витрачений CPU на його стиснення.
  4. На брокері стиснутий пакет швидше скидається в Page Cache. У результаті виграш у часі передачі по мережі та запису на диск часто значно перевершує накладні витрати процесора на компресію.

3. Що станеться, якщо продюсер відправляє вже попередньо стиснуті файли (наприклад, архіви .zip, картинки .jpg або відеопотоки) з compression.type=lz4?

Відповідь: Відбудеться деградація продуктивності системи (negative impact):

  1. Попередньо стиснуті бінарні формати (JPEG, PNG, GZIP, ZIP, MP4) мають максимальну інформаційну ентропію — у них практично відсутні повторювані послідовності байтів.
  2. Алгоритм LZ4 просканує весь масив даних, не знайде збігів у словнику і запише дані у вихідному вигляді, додавши службовий заголовок стиснутого фрейму.
  3. Підсумок:
    • Коефіцієнт стиснення дорівнює 1.0 (або розмір навіть збільшиться на 0.1–1% через метадані LZ4).
    • CPU продюсера буде марно витрачати такти на сканування пам’яті.
    • CPU консьюмера буде марно витрачати ресурси на розпакування.
    • Рішення: Для топіків, що передають медіафайли або попередньо стиснуті бінарні блоби, компресію продюсера слід вимикати (compression.type=none).

4. Чому при compression.type = producer брокер не може перевірити цілісність CRC окремих повідомлень усередині батча?

Відповідь: У форматі RecordBatch (починаючи з Kafka 0.11) контрольна сума CRC32C обчислюється для всього батча цілком, включаючи його стиснуте тіло (payload).

  1. Окремі записи (Record) усередині стиснутого батча не мають власних контрольних сум у заголовках.
  2. При отриманні запиту ProduceRequest брокер перевіряє CRC32C заголовка батча над стиснутими байтами.
  3. Якби брокер валідував CRC кожного окремого повідомлення, йому довелося б декомпресувати весь батч в оперативну пам’ять. Це порушило б фундаментальний принцип нульових накладних витрат (Zero-Copy) на брокері.
  4. Цілісність конкретних повідомлень гарантується наскрізною перевіркою: брокер гарантує, що стиснутий батч не пошкоджено при передачі по мережі та запису на диск, а перевірку розпакованих записів здійснює вже кінцевий Consumer при декомпресії.

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

30-секундна відповідь (Elevator Pitch)

«Компресія в Kafka — це платформений механізм стиснення батчів повідомлень на стороні Producer без втрат. Kafka підтримує алгоритми lz4, snappy, zstd та gzip. Стандартом індустрії є LZ4 завдяки мінімальному навантаженню на CPU при високому стисненні; для максимального стиснення та економії міжрегіонального трафіку використовується ZSTD. Стиснення застосовується не до окремих повідомлень, а до цілого батча, що дозволяє словниковим алгоритмам ефективно стискати повторювані структури (JSON, Protobuf, Avro). Брокер зберігає дані у стиснутому вигляді та передає їх консьюмеру через Zero-Copy (sendfile) без декомпресії, а розпакування відбувається на стороні Consumer. Критична помилка — зміна compression.type на брокері, що викликає примусову перекомпресію та 100% утилізацію CPU кластера».

Ключові метрики для Production-моніторингу

  • kafka.producer:type=producer-metrics,client-id={id} -> compression-rate-avg: середній коефіцієнт стиснення (відношення нестиснутого розміру до стиснутого). Значення 0.3 означає, що розмір даних зменшився до 30% від оригіналу (стиснення в 3.3 раза). Якщо близько до 1.0 — батчинг не працює або дані не піддаються стисненню.
  • kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec / BytesOutPerSec: вхідний та вихідний трафік брокера в байтах.
  • kafka.server:type=BrokerTopicMetrics,name=RecompressionRate: частота перекомпресії повідомлень брокером. Повинна бути суворо 0.
  • jvm_cpu_load на брокерах: різке зростання при незмінному трафіку часто сигналізує про ввімкнену перекомпресію або даунгрейд формату повідомлень.

Червоні прапорці (Чого говорити НЕ МОЖНА)

  • ❌ «Kafka стискає кожне повідомлення окремо перед розміщенням у батч» — Груба помилка. Стискається вже повністю сформований ProducerBatch. Поштучне стиснення мікро-повідомлень неефективне.
  • ❌ «Брокер розпаковує кожне повідомлення, зберігає в базу, а при відправці консьюмеру знову стискає» — Брокер зберігає стиснутий батч на диск “as is” і відправляє через sendfile(2) Zero-Copy.
  • ❌ «GZIP — найкращий вибір для production, тому що він сильніше за всіх стискає» — GZIP створює величезне навантаження на CPU (>30-40%) і перетворює продюсери на вузьке горлечко. Стандарт — LZ4 або ZSTD.
  • ❌ «Компресія безкоштовна і її потрібно вмикати всюди» — Вимагає ресурсів CPU. Для вже стиснутих даних (картинки, архіви) вона марна і шкідлива.

Пов’язані теми