Вівторок, 21 Квітня, 2026

Банківські системи на Kafka Streams: як вчаться на помилках з exactly-once, консистентністю та шифруванням

У подкасті Confluent Developer 28‑річний інженер з Кіто, Еквадор, Матео Рохас розповідає про свій досвід побудови реальних банківських систем на ранніх версіях Kafka Streams. Почавши кар’єру з Kafka приблизно у 20 років, він опинився в ситуації, коли потрібно було керувати грошима клієнтів на технології, яка ще тільки формувалася. Сьогодні Матео працює в LittleHorse над workflow‑рушієм поверх Kafka, але саме перші проєкти з політиками ануїтетів дали йому найжорсткіші уроки — від join‑вікон і відсутності exactly‑once до болючих питань безпеки та шифрування.

a rack of electronic equipment in a dark room

Ці уроки важливі не лише як історія «як не треба робити», а й як практичний орієнтир для команд, які й досі намагаються перетворити Kafka на джерело істини для чутливих фінансових даних.

Вікна join у Kafka Streams: коли «на всяк випадок» означає «на роки»

Одна з ключових задач платформи ануїтетів, над якою працював Матео, полягала в тому, щоб активувати поліс лише тоді, коли виконані всі перевірки: KYC, AML та очищення коштів банком. Архітектурно це виглядало як три окремі потоки подій, кожен з яких сигналізував про завершення свого етапу. Для ухвалення рішення про активацію потрібно було «зібрати» три події з різних топіків за одним і тим самим ідентифікатором поліса.

Команда обрала природний для Kafka Streams підхід — join потоків за ключем (policy ID). Проблема виявилася в тому, що join у Kafka Streams працює лише в межах конфігурованого вікна часу. Якщо одна з подій приходить надто пізно, за межами цього вікна, join просто не відбудеться, навіть якщо всі три повідомлення фізично присутні в топіках.

Для банківської системи це означало реальний ризик: поліс міг так і не активуватися лише через те, що, наприклад, зовнішній AML‑сервіс відповів із затримкою, яка виходить за межі вікна. Або, навпаки, система могла поводитися непередбачувано, якщо логіка обробки не враховує такі «провалені» join‑и.

Щоб уникнути пропущених join‑ів, команда пішла шляхом, який сьогодні сам Матео називає далеким від ідеалу: вікно join було налаштоване на гігантське значення, вимірюване роками. Ідея була простою — зробити вікно настільки великим, щоб жодна реалістична затримка зовнішніх сервісів не могла призвести до пропуску join.

Технічно це працювало: якщо подія KYC приходила через тиждень, місяць чи навіть значно пізніше, вона все ще потрапляла в межі вікна, і join відбувався. Але така конфігурація мала очевидні мінуси. Величезне вікно означає довготривале зберігання проміжного стану, більший обсяг state store, складніший дебаг і менш передбачувану поведінку в разі збоїв. До того ж це маскувало справжню проблему — відсутність чітко визначених SLA для зовнішніх систем і відсутність явної бізнес‑логіки для обробки надто пізніх подій.

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

Життя без exactly‑once: як KTable‑дедуплікація замінювала гарантії

Якщо проблема з join‑вікнами була неприємною, то відсутність exactly‑once семантики в Kafka Streams на той момент була відверто небезпечною. Мова йшла про систему, яка нараховує відсотки за реальними грошима. Подвійна обробка одного й того самого повідомлення могла означати подвоєне нарахування відсотків або інші фінансові аномалії.

На той час Kafka Streams не надавала вбудованої exactly‑once семантики. Команда була змушена самостійно наближатися до цієї гарантії, будуючи дедуплікацію поверх наявних примітивів. Вибір упав на KTable як механізм зберігання стану.

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

Цей підхід дозволяв захиститися від повторної доставки, яка могла виникнути, наприклад, якщо споживач не встиг закомітити офсет до збою, і Kafka повторно відправляла те саме повідомлення. Завдяки KTable‑дедуплікації повторна подія не призводила до повторної бізнес‑операції.

Однак ціна такого рішення була високою. Платформа складалася приблизно з двадцяти мікросервісів, кожен з яких споживав багато топіків і мав власну логіку дедуплікації. Фактично кожен сервіс реалізовував свій варіант «саморобного exactly‑once», що збільшувало складність системи, ускладнювало супровід і тестування.

До того ж дедуплікація на рівні KTable не є повним еквівалентом exactly‑once семантики, яку сьогодні надає Kafka Streams. Вона захищає від повторної обробки за конкретним ID, але не вирішує всіх можливих сценаріїв, пов’язаних із транзакційністю, консистентністю між топіками та атомарністю записів.

Попри це, для того часу це був практичний компроміс: використати наявні інструменти Kafka Streams, щоб максимально наблизитися до поведінки exactly‑once у критичній фінансовій системі.

Мікросервіси, eventual consistency і тестування, яке ловить гонки

Архітектура платформи ануїтетів була типовою для епохи масового захоплення мікросервісами: близько двадцяти сервісів, кожен з яких споживає події, реагує на них і генерує нові. У теорії це мало забезпечити гнучкість і масштабованість. На практиці команда зіткнулася з класичними проблемами eventual consistency.

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

Матео згадує, що гонки виникали не лише навколо KTable‑ів, а й загалом по всій системі. Команда тільки починала розуміти, як працює подієво‑орієнтована архітектура, і зіткнулася з тим, що «час» у розподіленій системі — це неочевидне поняття. Подія, яка логічно «сталася раніше», могла фізично бути оброблена пізніше, і навпаки.

Щоб мати хоч якийсь контроль над цією eventual consistency, команда побудувала автоматизовані тести консистентності, які порівнювали дані між мікросервісами. Ідея полягала в тому, щоб через певний час після обробки подій перевіряти, чи всі сервіси врешті‑решт дійшли до узгодженого стану. Це не усувало гонки, але дозволяло виявляти сценарії, де «остаточна» консистентність так і не досягалася.

Такий підхід підкреслює важливу практичну істину: у подієво‑орієнтованих мікросервісних системах eventual consistency — не просто теоретична властивість, а джерело дуже конкретних багів. І без явних механізмів перевірки узгодженості між сервісами команда ризикує роками жити з прихованими розбіжностями в даних.

Цей досвід також вплинув на погляд Матео на роль Kafka в архітектурі. У тому проєкті Kafka фактично виконувала роль джерела істини, а відтворення топіків використовувалося для відновлення стану застосунків. Але чим складнішою ставала система, тим очевиднішими ставали ризики такого підходу — особливо в поєднанні з питаннями безпеки.

Шифрування та незмінність: чому Kafka — погане місце для вічних секретів

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

Команда довго боролася з питанням, як шифрувати ці дані. Варіант із шифруванням на рівні застосунку — коли payload кожного повідомлення шифрується власним ключем — виглядав привабливо з точки зору безпеки. Але він мав критичний недолік: якщо ключ шифрування колись «витече», перевести вже записані в Kafka події на новий ключ практично неможливо.

Через незмінність топіків немає простого способу «пере‑зашифрувати» історичні дані. Потрібно було б читати всі події, розшифровувати їх старим ключем, шифрувати новим і записувати в новий топік, паралельно мігруючи всі споживачі. Для системи, яка вже працює в продакшені, це колосальний ризик і операційний кошмар.

У підсумку команда обрала більш «інфраструктурний» підхід до безпеки: шифрування дисків на рівні AWS EC2 у поєднанні з TLS для трафіку. Тобто дані в Kafka зберігалися у відкритому вигляді на рівні payload, але були захищені на транспортному рівні та на рівні фізичного зберігання.

Це рішення не ідеальне з точки зору моделі загроз, але воно враховувало реальність незмінних логів Kafka. Матео робить із цього чіткий висновок: якщо застосунковий ключ шифрування для payload‑ів Kafka колись буде скомпрометований, пере‑шифрування незмінних топіків — надзвичайно складне завдання. Саме тому він радить обережно ставитися до ідеї зберігати чутливі дані в Kafka надовго.

Звідси випливає ще одна рекомендація: Kafka краще підходить як транспортний шар із коротким retention, а не як довгострокове джерело істини для секретних даних. Короткі періоди зберігання в поєднанні з TLS і шифруванням дисків зменшують «вікно вразливості» у разі витоку ключів або компрометації інфраструктури.

Чи повинна Kafka бути джерелом істини?

Досвід Матео з ранніми банківськими системами на Kafka Streams підводить до фундаментального питання, яке й сьогодні хвилює багато команд: чи варто робити Kafka джерелом істини?

У проєкті з ануїтетами відповідь була фактично «так»: Kafka розглядалася як основне сховище подій, з якого можна відтворити стан усієї системи. Це давало потужні можливості для відновлення після збоїв і аналітики, але водночас створювало серйозні виклики:

подієві join‑и з гігантськими вікнами, які складно дебажити;

відсутність вбудованої exactly‑once семантики, що змушувала будувати складні схеми дедуплікації на KTable‑ах у кожному з приблизно двадцяти мікросервісів;

гонки та eventual consistency між сервісами, які доводилося ловити автоматизованими тестами консистентності;

питання шифрування чутливих банківських даних у незмінних топіках, які врешті‑решт вирішувалися інфраструктурним, а не застосунковим шифруванням.

Сьогодні, працюючи над workflow‑рушієм поверх Kafka в LittleHorse, Матео дивиться на ці рішення значно критичніше. Він схиляється до моделі, де Kafka виконує роль потужного транспортного шару з відносно коротким retention, а довгострокове джерело істини для чутливих даних розміщується в інших системах, які краще пристосовані до ротації ключів, пере‑шифрування та керування життєвим циклом даних.

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

Висновок: уроки ранніх банківських систем на Kafka Streams

Історія Матео Рохаса — це концентрат уроків, які багато команд проходять роками:

join‑и в Kafka Streams мають часові вікна, і спроба «підстрахуватися» вікном на роки лише маскує архітектурні проблеми;

відсутність exactly‑once у критичних системах змушує будувати саморобні механізми дедуплікації, що різко підвищує складність;

eventual consistency між десятками мікросервісів — не абстракція, а джерело реальних гонок, які потребують окремих інструментів для виявлення;

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

Звідси народжується більш стриманий, прагматичний підхід: використовувати Kafka як високопродуктивний транспортний шар із TLS і шифруванням дисків, обмежувати retention, обережно ставитися до join‑ів у грошових сценаріях і не покладатися на Kafka як єдине довгострокове джерело істини для секретних даних.

Ці висновки не зменшують потужності Kafka Streams, але нагадують: у світі реальних грошей і регуляцій технологічні можливості завжди мають узгоджуватися з безпекою, простотою супроводу та здоровим глуздом.


Джерело

Building Banking Systems with Kafka Streams with Mateo Rojas | Ep. 28 | Confluent Developer Podcast

НАПИСАТИ ВІДПОВІДЬ

Коментуйте, будь-ласка!
Будь ласка введіть ваше ім'я

Ai Bot
Ai Bot
AI-журналіст у стилі кіберпанк: швидко, точно, без води.

Vodafone

Залишайтеся з нами

10,052Фанитак
1,445Послідовникислідувати
105Абонентипідписуватися

Статті