Messaging & Events

Kafka, fronty, pub/sub, delivery garance, DLQ.

Zatím každé volání v téhle učebnici vypadalo tak, že klient počká na odpověď. Ale spousta práce takhle běžet nemusí — a často by ani neměla. Když si u registrace objednáš uvítací e-mail, uživatel nemá důvod čekat, až se opravdu odešle. Tahle kapitola je o asynchronní komunikaci: jak si služby předávají práci, aniž by na sebe přímo čekaly. Získáš odolnost a škálovatelnost, zaplatíš složitějším přemýšlením o pořadí a duplicitách.


Pár pojmů na úvod

  • Synchronní = pošlu požadavek a čekám na odpověď (jako telefonát — držíš linku, dokud druhý nezvedne a neodpoví).
  • Asynchronní = pošlu zprávu a nečekám (jako SMS — odešleš a jdeš dál, druhý si to přečte, až bude moct).
  • Zpráva (message) / event = balíček informace, který někomu pošleš („nová objednávka #42").
  • Producent (producer) = kdo zprávu vytvoří a pošle.
  • Konzument (consumer) / worker = kdo ji přijme a zpracuje.
  • Fronta / broker = prostředník, který zprávy podrží, dokud si je konzument nevyzvedne (např. RabbitMQ, Kafka).

Synchronní vs asynchronní — základní volba

Synchronní (HTTP)Asynchronní (fronta/zpráva)
ProvázanostTěsná (druhá strana musí běžet teď hned)Volná (příjemce může být zrovna dole)
Čekání klientaČeká na výsledekVrátí se hned, zpracuje se na pozadí
Když příjemce spadneSpadne i voláníZpráva v klidu počká ve frontě
Kdy použítPotřebuju odpověď hnedPošli a zapomeň, dlouhé úlohy, zvládání špiček

Důležitý nápad: fronta funguje jako nárazník (buffer). Když ti najednou přijde nápor a producent chrlí 10 000 zpráv za sekundu, ale konzument jich zvládne 1 000 za sekundu, fronta ten rozdíl podrží a práce se zpracuje postupně — místo aby se systém pod náporem složil. Říká se tomu load leveling (vyrovnávání zátěže).


Queue vs Pub/Sub — dva základní modely

Existují dva způsoby, jak zprávu doručit:

  • Queue (fronta, point-to-point): zprávu zpracuje právě jeden konzument. Slouží k rozdělení práce mezi víc workerů (jako lísteček s úkolem, který si vezme jeden volný pracovník).
  • Pub/Sub (publish/subscribe): zprávu dostane každý odběratel. Slouží k oznámení události víc službám naráz (jako newsletter — rozešle se všem přihlášeným).

Kafka — jak na ni myslet

Kafka je hodně používaný nástroj pro zprávy, ale chová se trochu jinak než klasická fronta. Mysli na ni jako na trvalý záznamník (log), do kterého se zprávy připisují a zůstávají tam i po přečtení. Pár pojmů:

  • Topic (téma) je rozdělené na partitions (části) → tím se dá zpracování rozložit a zrychlit.
  • Pořadí zpráv je zaručené jen uvnitř jedné partition, ne napříč celým topicem. Zprávy se stejným klíčem jdou vždy do stejné partition → takže pro daný klíč (např. všechny události jednoho uživatele) pořadí drží.
  • Každý konzument si pamatuje offset = kde naposledy skončil. Protože se zprávy nemažou, můžeš historii přehrát znovu nebo přidat nového konzumenta, který si projde i staré zprávy.
  • Consumer group = skupina konzumentů, mezi které se partitiony rozdělí. Tím škáluješ čtení — ale paralelismus je omezený počtem partition (víc aktivních konzumentů než partition nemá co dělat). A když konzument přibude/zmizí, proběhne rebalance (přerozdělení partition), které na chvíli přeruší zpracování.

⚠️ Cena „klíč pro pořadí": všechny zprávy jednoho hot klíče (populární user) jdou do jedné partition → ta se stane bottleneckem a jedna pomalá/vadná zpráva blokuje celou partition za sebou (head-of-line blocking). Pořadí vs průtok je trade-off, který musíš vybalancovat.

Kafka vs RabbitMQ ve zkratce: Kafka = trvalý záznam, přehrávání, obří průtok. RabbitMQ = klasická chytrá fronta s bohatým rozesíláním úkolů. (Víc o volbě v Tech Choices.)


Garance doručení (klíčová věc)

Z Distribuovaných systémů už víš, že zprávy se po nespolehlivé síti ztrácejí i duplikují. V praxi se skoro vždy používá at-least-once (aspoň jednou): zpráva se nikdy neztratí, ale může dorazit vícekrát (konzument ji zpracuje a spadne dřív, než stihne potvrdit přijetí → doručí se znovu).

Důsledek: konzument musí být idempotentní — umět stejnou zprávu dostat dvakrát, aniž by udělal akci (e-mail, platbu) dvakrát. Typicky se to řeší deduplikací podle ID zprávy (viz Patterny).

Nuance: Kafka umí tzv. exactly-once semantics (EOS) — ale jen uvnitř Kafky (čtu z jednoho topicu, zapisuju do jiného, atomicky). Jakmile sáhneš ven (pošli e-mail, zapiš do cizí DB), jsi zpátky u at-least-once + idempotence. Takže „exactly-once" v rámci jednoho systému existuje, end-to-end přes externí svět ne.


Event-Driven Architecture (architektura řízená událostmi)

Místo aby si služby dávaly přímé příkazy, oznamují události — fakta, co se stala (ObjednavkaVytvorena, PlatbaPrijata). Producent přitom neví, kdo všechno poslouchá, a je mu to jedno. Tím se služby maximálně oddělí: přidáš novou službu, která na událost reaguje, a původní nemusíš vůbec měnit.

Pojítko na patterny: outbox (Patterny) je most mezi uložením dat do databáze a spolehlivým odesláním události.


Evoluce schémat zpráv — jak měnit formát a nerozbít konzumenty

Každá zpráva má nějaký tvar (schéma) — jaká pole obsahuje. A teď ten zádrhel: producent a konzumenti se nasazují nezávisle, takže v jednu chvíli běží stará i nová verze naráz — a navíc díky přehrávání můžeš dostat i staré zprávy. Schéma proto musí jít měnit zpětně kompatibilně, úplně stejně jako u verzování API a expand-contract migrací:

  • Přidat nové volitelné pole = bezpečné (starý konzument ho ignoruje).
  • Odebrat nebo přejmenovat pole, změnit typ = breaking → starý konzument spadne.

Aby se na to nezapomnělo, používají se formáty s kontrolou kompatibility (Avro, Protobuf) a schema registry — centrální evidence schémat, která nové schéma rovnou ověří, jestli je kompatibilní se starým, a nepustí breaking změnu.


Dead Letter Queue (DLQ)

Co když je nějaká zpráva vadná a opakovaně se ji nepodaří zpracovat (tzv. poison message, „otrávená zpráva")? Nesmí donekonečna blokovat frontu. Po několika neúspěšných pokusech se odloží stranou do Dead Letter Queue — místa pro problematické zprávy, na které se pak člověk podívá.

DLQ není černá díra: alertuj na neprázdnou DLQ (tichá DLQ = tiše ztracená data) a měj redrive — schopnost zprávy po opravě bugu přehrát zpět ke zpracování. Pozor, že replay z DLQ může rozhodit pořadí.


Failure modes — jak to v praxi praská

  • Duplicitní zpracování → bez idempotence dvojitý e-mail nebo platba.
  • Špatné pořadí → spoléháš na celkové pořadí, ale Kafka ho garantuje jen uvnitř partition.
  • Poison message → jedna vadná zpráva blokuje konzumenta donekonečna (proto DLQ).
  • Consumer lag → konzument nestíhá, fronta roste. Měř to a přidej konzumenty.
  • Backpressure → producent chrlí rychleji, než stíháš; potřebuješ tlak zpět nebo frontu s limitem (viz Concurrency).

🛠️ Cvičení

  1. Synchronní, nebo asynchronní? U tří operací — odeslání uvítacího e-mailu, ověření platby u platební brány, vygenerování PDF faktury — rozhodni a zdůvodni.
  2. Idempotentní konzument. Konzument dostane stejnou zprávu dvakrát (at-least-once). Napiš, jak ji podle ID rozpoznáš jako duplicitu, aby se akce (e-mail) neprovedla dvakrát.
  3. Kafka pořadí. Máš události OrderCreated, OrderPaid, OrderShipped pro jednoho uživatele. Navrhni klíč zprávy tak, aby šly ve správném pořadí, a vysvětli proč.
  4. Queue vs pub/sub. Pro „zpracuj nahraný soubor" a „oznam třem službám novou objednávku" vyber model a nakresli, kdo zprávu dostane.
  5. Poison message. Zpráva při zpracování opakovaně padá. Navrhni limit pokusů + DLQ tak, aby nezablokovala frontu, a co se zprávou pak.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
  1. Synchronní, nebo asynchronní? — uvítací e-mail = async (uživatel nemá důvod čekat na odeslání), ověření platby u brány = synchronní (potřebuješ výsledek hned, abys mohl pokračovat), PDF faktura = async (dlouhá úloha, pošli a zapomeň). Pointa: synchronně jen tam, kde výsledek potřebuješ teď hned. Pozor nepřehodit platbu na async, kde návazný krok čeká na její výsledek.
  2. Idempotentní konzument — měj tabulku zpracovaných ID; na začátku zkus atomicky vložit ID zprávy (UNIQUE), a když už tam je, zprávu zahoď bez akce. Tím se e-mail při at-least-once doručení nepošle dvakrát. Past: dedup okno (TTL ID) musí být delší než retry okno producenta, jinak duplikát po expiraci tiše projde.
  3. Kafka pořadí — jako klíč zprávy dej ID uživatele (resp. objednávky), takže OrderCreated/OrderPaid/OrderShipped jdou do stejné partition a pořadí pro daný klíč drží. Pořadí Kafka garantuje jen uvnitř partition, ne napříč topicem. Past: hot klíč (populární user) zahltí jednu partition a jedna vadná zpráva blokuje vše za sebou (head-of-line blocking).
  4. Queue vs pub/sub — „zpracuj nahraný soubor" = queue (úkol zpracuje právě jeden worker), „oznam třem službám novou objednávku" = pub/sub (událost dostanou všichni tři odběratelé). Pointa: queue = úkol pro jednoho, pub/sub = oznámení všem. Past: u pub/sub si každý odběratel musí ošetřit duplicity sám.
  5. Poison message — počítej pokusy a po N neúspěších zprávu přesuň do DLQ, ať neblokuje frontu donekonečna; pak ji člověk prohlédne, opraví bug a přes redrive přehraje zpět. Past: alertuj na neprázdnou DLQ (tichá DLQ = tiše ztracená data) a počítej s tím, že replay z DLQ může rozhodit pořadí.

🧠 Otázky & odpovědi

Proč asynchronní fronta pomáhá zvládnout špičky?

Funguje jako nárazník mezi producentem a konzumentem. Producent může poslat 10 000 zpráv za sekundu, konzument jich zvládne 1 000 — fronta ten rozdíl podrží a práce se zpracuje postupně, místo aby se konzument pod náporem složil. Cena: výsledek není hned (latence) a musíš hlídat, ať fronta neroste donekonečna.

Jaký je rozdíl mezi queue a pub/sub?

Queue (fronta): zprávu zpracuje právě jeden konzument — slouží k rozdělení práce mezi workery. Pub/Sub: zprávu dostane každý odběratel — slouží k oznámení události víc nezávislým službám. Zjednodušeně: queue = „udělej tenhle úkol", pub/sub = „stalo se tohle, ať na to reaguje, kdo chce".

Proč at-least-once vyžaduje idempotentního konzumenta?

At-least-once zaručí, že se zpráva neztratí, ale může přijít vícekrát (konzument ji zpracuje a spadne dřív, než stihne potvrdit přijetí → doručí se znovu). Když konzument není idempotentní, duplicitní doručení znamená dvojitý e-mail nebo platbu. Řešení: rozpoznat duplicitu podle stabilního ID zprávy, nebo dělat operaci idempotentně.

Jak Kafka garantuje pořadí a jak toho využiješ?

Kafka garantuje pořadí jen uvnitř jedné partition, ne napříč celým topicem. Zprávy se stejným klíčem jdou vždy do stejné partition → pro daný klíč (např. všechny události jednoho uživatele) máš pořadí zaručené. Když dáš jako klíč ID uživatele, jeho události se nikdy nepřeházejí, i když systém jinak běží paralelně.

Co je consumer lag a proč ho sledovat?

Je to, jak moc je konzument pozadu — u Kafky přesně rozdíl mezi koncem logu a tím, kde má konzument svůj offset. Rostoucí lag znamená, že nestíhá a zpoždění narůstá. Pozor: zprávy v Kafce nemizí kvůli lagu — mizí podle retention (po nastaveném čase/velikosti), takže když lag naroste tak, že nezpracované zprávy starší než retention vypadnou, ztratíš je. Sleduj lag jako klíčovou metriku a podle něj přidávej konzumenty.

Proč musí být změna schématu zprávy zpětně kompatibilní?

Producent a konzumenti se nasazují nezávisle, takže běží stará i nová verze naráz — a díky přehrávání můžeš dostat i staré zprávy. Když schéma změníš breaking způsobem (odebereš/přejmenuješ pole), starý konzument na nové (nebo nová na staré) spadne. Bezpečné je přidat volitelné pole; odebrání nebo přejmenování je breaking. Hlídá to schema registry s kontrolou kompatibility (Avro/Protobuf). Stejný princip jako u verzování API.