Проблеми з Azure Event Hubs в режимі сумісності з Kafka

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Як відомо, Azure Event Hubs підтримує протокол Kafka, що дозволяє використовувати потужну екосистему готових бібліотек та інструментів. Свого часу ми обрали цей шлях, мігрувавши з Self-managed Kafka на Azure Event Hubs. Оскільки вся наша інфраструктура вже була в Azure, це рішення виглядало логічним і «природним».

Перехід пройшов гладко, а стабільність керованого сервісу значно перевищила результати нашої попередньої конфігурації. Проте навіть у надійних рішеннях бувають нюанси. Нещодавно ми зіткнулися з двома цікавими проблемами, якими і хочу поділитися з ком’юніті.

Проблема з консюмером

Сервіс раптово перестав консюмити повідомлення. Перезапуск сервісу не допомагав, а в INFO-логах не було нічого підозрілого.

Після увімкнення DEBUG-логів Kafka-клієнта з’явився запис про те, що консюмер намагається читати з дуже великого офсета, якого фактично не існує в топіку. Водночас у Azure Portal було видно, що в топіку на той момент є лише незначна кількість повідомлень.

З логів також було помітно, що перед появою проблеми проводилось масштабне тестування, під час якого все працювало коректно. Але після такого тестування в топіку мало б залишитися багато повідомлень, чого насправді не було.

Після уточнення у тестерів з’ясувалося, що після завершення тестування вони попросили перестворити топіки. Девопс це зробив, але сервіс-консюмер у цей момент продовжував працювати.

Додатково з логів видно, що консюмер періодично комітить офсет, закешований у пам’яті, навіть за відсутності нових повідомлень.

Гіпотеза була такою (і вона підтвердилася):

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

Перезапуск сервісу ситуацію не виправляв, оскільки під час старту консюмер знову зчитував той самий невалідний офсет.

Цікаво, що на нативній Kafka така поведінка не відтворюється. Для перевірки був написаний тест із використанням Testcontainers. У класичній Kafka при перестворенні топіка брокер нотифікує консюмера, і ця ситуація обробляється коректно.

Висновок і рішення

Коректна послідовність дій для перестворення топіка в Event Hubs така:

  1. Зупинити сервіс, який читає з топіка
  2. Перестворити топік
  3. Запустити сервіс знову

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

Проблема з продюсером

Дуже рідко, зазвичай після великої паузи між відправками, виникає помилка:

InvalidPidMappingException: The producer attempted to use a producer id which is not currently assigned to its transactional id.

При цьому з логів видно, що повторної спроби (retry) не відбувається, і в результаті повідомлення втрачається.

Початковий конфіг продюсера

max.in.flight.requests.per.connection: 5 
enable.idempotence: true
  • max.in.flight.requests.per.connection: 5

Кількість запитів, які одночасно відправляють батчі повідомлень брокеру.

  • enable.idempotence: true

Запобігає дублюванню та можливому реордерінгу при ретраях, якщо max.in.flight.requests.per.connection > 1.
Саме так, у кафці можливо отримати неправильний порядок повідомлень!

При цьому:

  • ми не використовуємо транзакції;
  • transactional.id (або spring.cloud.stream.kafka.binder.transaction.transactionIdPrefix у spring cloud stram) не заданий.

Аналіз причини

Я завантажив вихідні коди kafka-clients версії 3.9.1, яку ми використовуємо, і проаналізував їх за допомогою LLM-агента, щоб з’ясувати:

  • за яких умов виникає InvalidPidMappingException;
  • як ця помилка оброблятись.
  • як зробити, щоб вона не виникала, або як обійти.

З аналізу коду випливає, що:

  • TransactionManager створюється при включеній ідемпотентності, але він має метод:
    public boolean isTransactional() { return transactionalId != null; }
    Тож без transactionalId він все одно він не транзакційний.
  • InvalidPidMappingException має виникати лише при використанні транзакцій, коли Kafka broker втрачає мапінг між Producer Id та Transactional Id, який зберігається на його стороні.
  • Якщо ж увімкнено тільки enable.idempotence: true (що є коректним сценарієм, адже ідемпотентність може бути потрібна і без транзакцій), то в кожному батчі клієнт передає лише Producer Id.

У випадку втрати цього стану брокером має повертатися інша помилка:

UNKNOWN_PRODUCER_ID(59, "This exception is raised by the broker if it could not locate the producer metadata " + "associated with the producerId in question. This could happen if, for instance, the producer's records " + "were deleted because their retention time had elapsed. Once the last records of the producerId are " + "removed, the producer's metadata is removed from the broker, and future appends by the producer will " + "return this exception.", UnknownProducerIdException::new)

Ця помилка означає, що брокер не може знайти метадані продюсера, пов’язані з переданим producerId. Наприклад, це може статись, якщо записи продюсера були видалені через завершення retention-періоду.

У такому випадку Kafka client коректно виконує retry відправки батча.

Чому повідомлення втрачається

При InvalidPidMappingException retry не відбувається, оскільки клієнт вважає, що для відновлення необхідно відкрити нову транзакцію.

Оскільки в нашому випадку:

  • транзакції не використовуються;
  • старої транзакції не існує,

повідомлення просто губиться.

Відомі проблеми з Azure Event Hubs

Виглядає, що це відома проблема Azure Event Hubs у режимі сумісності з Kafka:

Можливі варіанти вирішення

1. Використовувати Event Hub binder замість Kafka binder

Ймовірно, це могло б вирішити проблему, але немає гарантії, що з Event Hub binder не з’являться нові обмеження або нестабільності. Тому варіант вважається ризикованим.

2. Увімкнути транзакції

Найімовірніше, це усунуло б проблему, але:

  • транзакції нам не потрібні — необхідна лише ідемпотентність;
  • довелось би змінювати конфігурацію на стороні консюмерів (читання лише committed повідомлень).

3. Відмовитись від ідемпотентності та прибрати паралельну відправку

  • max.in.flight.requests.per.connection: 1 — уникаємо реордерінгу повідомлень;
  • enable.idempotence: false — допускаємо можливе дублювання.

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

Результат

Ми обрали третій варіант як найпростіший і найменш ризикований.

Після цього проблема більше не відтворювалась.

👍ПодобаєтьсяСподобалось2
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Навіщо ви цю шляпу юзали взагалі? Це ж для івентів в ажур клауд системі. Чому не сервіс бас із топіками?

Бо нам потрібен високий throughput та прядок FIFO (в ASB є сессії для цього, але з ними теж не усе гладко). Взагалі, і плане зрілості SDK у майскрософт далеко не все чудово. Ми ловили купу багів і дивної поведінки в java SDK для ASB.

дивна радити комусь меседж брокер, коли потрібен івент стрім

Підписатись на коментарі