Demistificare la Semantica Exactly-Once di Kafka: Una Guida Completa
Comprendi la semantica exactly-once di Kafka con produttori idempotenti, transazioni, consumatori read_committed e commit degli offset.
Demistificare la Semantica Exactly-Once di Kafka: Una Guida Completa
La semantica exactly-once di Kafka può proteggere una pipeline di elaborazione in streaming da record di output duplicati quando i produttori ritentano, i broker falliscono o un'applicazione si riavvia. La garanzia è potente, ma è più limitata di quanto sembri: Kafka può rendere transazionali le scritture in Kafka e gli offset consumati. Non può rendere automaticamente exactly-once il tuo database esterno, gateway di pagamento o API HTTP.
Usa la semantica exactly-once quando l'output duplicato sarebbe costoso o difficile da pulire, come rettifiche di inventario, eventi di saldo conto o topic di stato derivati consumati da altri servizi.
Garanzie di Consegna in Parole Semplici
Le applicazioni Kafka di solito parlano di tre modelli di consegna.
- At-most-once: La tua app può perdere record, ma non dovrebbe elaborare lo stesso record due volte. Questo può accadere quando gli offset vengono impegnati prima che l'elaborazione sia completata.
- At-least-once: La tua app non dovrebbe perdere record, ma potrebbe elaborare un record più di una volta dopo un nuovo tentativo o riavvio.
- Exactly-once: Un ciclo di lettura-elaborazione-scrittura di Kafka impegna i suoi record di output e i suoi offset consumati come una singola transazione.
L'ultimo punto è la chiave. La semantica exactly-once è più forte quando l'applicazione legge da Kafka, scrive i risultati in Kafka e impegna gli offset all'interno della stessa transazione.
Produttori Idempotenti
Un produttore idempotente previene scritture duplicate causate da nuovi tentativi del produttore. Kafka assegna al produttore un ID e tiene traccia dei numeri di sequenza per ogni produttore e partizione. Se il broker ha già accettato un batch e poi riceve il nuovo tentativo, può rifiutare il duplicato invece di aggiungerlo di nuovo.
Per i client Kafka attuali, l'idempotenza è abilitata per impostazione predefinita quando non configuri impostazioni del produttore in conflitto. Puoi comunque impostarla esplicitamente:
enable.idempotence=true
acks=all
acks=all significa che il leader attende tutte le repliche in-sync prima di riconoscere la scrittura. L'idempotenza dipende anche da impostazioni compatibili di retry e richieste in volo, quindi evita di sovrascrivere le impostazioni di affidabilità del produttore a meno che tu non conosca l'effetto nella tua versione del client.
L'idempotenza protegge dai nuovi tentativi del produttore, ma non rende atomico un intero flusso di lavoro di elaborazione. Se la tua app consuma da un topic e produce in un altro, hai bisogno di transazioni per legare insieme l'output e il commit dell'offset.
Transazioni Kafka
Le transazioni permettono a un gruppo di produttori di raggruppare più scritture in un'unità atomica. Il produttore ha bisogno di un transactional.id stabile.
transactional.id=inventory-adjuster-0
enable.idempotence=true
acks=all
Un flusso di transazione tipico è:
- Inizializza le transazioni quando l'applicazione si avvia.
- Inizia una transazione.
- Consuma record dal topic di input.
- Produce record di output.
- Invia gli offset consumati alla transazione.
- Impegna la transazione o annullala in caso di errore.
Se il processo si blocca prima del commit, Kafka non espone l'output non impegnato ai consumatori read_committed. Al riavvio, l'applicazione può leggere di nuovo gli stessi record di input e produrre un risultato impegnato.
Impostazioni del Consumatore che Contano
I consumatori che leggono output transazionali dovrebbero usare:
isolation.level=read_committed
enable.auto.commit=false
read_committed nasconde i record delle transazioni annullate. enable.auto.commit=false impedisce al consumatore di impegnare offset al di fuori della transazione.
Il nome della proprietà è importante. L'impostazione del consumatore Kafka è enable.auto.commit, non auto.commit.enable.
Per un'applicazione manuale consumatore-produttore, il commit dell'offset deve far parte della transazione del produttore. Nel client Java, ciò significa usare le API del produttore transazionale, incluso l'invio degli offset alla transazione prima di impegnarla.
Uno Scenario Concreto
Immagina un topic orders e un topic di output inventory-events. Il tuo servizio legge un ordine, controlla lo SKU e scrive un evento di detrazione inventario.
Senza transazioni, un crash dopo aver scritto l'output ma prima di impegnare l'offset di input può creare una detrazione duplicata dopo il riavvio. Con le transazioni, l'evento di output e il commit dell'offset di input riescono o falliscono insieme. Un riavvio può rileggere l'ordine, ma solo un evento di inventario impegnato diventa visibile ai consumatori read_committed a valle.
Limiti da Tenere a Mente
La semantica exactly-once di Kafka non copre gli effetti collaterali al di fuori di Kafka a meno che tu non li progetti appositamente. Se lo stesso servizio scrive anche in PostgreSQL o chiama un'API di fatturazione, quell'effetto collaterale esterno ha bisogno della propria chiave di idempotenza, vincolo di unicità, strategia di transazione o pattern outbox.
Le transazioni aggiungono anche overhead di coordinamento. Per una semplice acquisizione di log dove i duplicati sono accettabili, i produttori idempotenti più consumatori at-least-once potrebbero essere sufficienti.
Checklist Pratica
Usa un transactional.id stabile per istanza o attività dell'applicazione. Non permettere a due produttori attivi di usare lo stesso transactional.id contemporaneamente.
Imposta i consumatori di output transazionali su read_committed. Disabilita i commit automatici degli offset nei cicli di elaborazione transazionale.
Mantieni le transazioni brevi. Transazioni grandi possono aumentare la latenza e rallentare il recupero.
Tratta i sistemi esterni separatamente. Kafka può proteggere lo stato di Kafka, ma le tue scritture nel database hanno ancora bisogno di un design idempotente.
Il punto utile: la semantica exactly-once non è un interruttore magico. Sono un insieme di scelte di produttore, consumatore e transazione che funzionano meglio per l'elaborazione di flussi da Kafka a Kafka.