Blog · Transazioni
I messaggi fantasma di Apache Kafka: transazioni annullate e read_uncommitted
Un produttore apre una transazione, scrive 20 messaggi, qualcosa va storto e la transazione viene annullata. Per l'applicazione quei 20 messaggi non sono mai esistiti. Eppure apri il topic con il tuo client preferito e li vedi lì, belli in fila, come se niente fosse.
Non è un bug del broker. È una questione di isolation level, e vale la pena capirla prima di passare un'ora a cercare un ordine che «c'è nel topic ma non nel database».
Cosa resta nel log
In Apache Kafka i messaggi di una transazione finiscono nel log subito, mentre vengono scritti. Il broker non aspetta il commit per salvarli. Alla fine della transazione aggiunge un marcatore (control record): COMMIT oppure ABORT.
Quindi una transazione annullata lascia nel log:
- i suoi messaggi, che restano lì fino alla retention o alla compattazione;
- un marcatore ABORT, che non si vede come messaggio ma occupa un offset.
Chi legge decide cosa fare di quei messaggi.
I due modi di leggere
L'impostazione è isolation.level del consumer:
| Valore | Cosa vede |
|---|---|
read_uncommitted | tutto quello che è nel log, comprese le transazioni annullate e quelle ancora aperte |
read_committed | solo i messaggi non transazionali e quelli di transazioni confermate; si ferma al last stable offset (LSO) |
Il dettaglio che sorprende: i default non sono uguali fra le librerie. Il consumer Java di Apache Kafka parte da read_uncommitted. librdkafka, la libreria in C usata da molti client in altri linguaggi, parte da read_committed.
Quindi due programmi che leggono lo stesso topic, entrambi «con le impostazioni di base», possono mostrare numeri diversi.
Una prova concreta
Nel nostro banco di correttezza abbiamo scritto un topic con 40 messaggi confermati e 20 di una transazione annullata, poi abbiamo chiesto a due client di contarli:
| Domanda | Atteso | Client con read_committed | Client con read_uncommitted |
|---|---|---|---|
| Messaggi della transazione annullata | 0 | 0 | 20 |
| Tutto il topic | 40 | 40 | 60 |
Il secondo client non sbaglia a leggere: legge tutto il log. Ma se stai cercando di capire cosa è successo davvero nella tua applicazione, quei 20 messaggi sono rumore, e a volte rumore pericoloso.
Il last stable offset
Con read_committed c'è un effetto collaterale da conoscere: il consumer non va oltre il last stable offset, cioè il primo offset che appartiene a una transazione ancora aperta. Se un produttore apre una transazione e resta appeso, chi legge in read_committed si ferma lì, anche se dopo ci sono messaggi già confermati di altri produttori.
Se una partizione «non avanza» per un consumer transazionale ma il suo high watermark cresce, la prima cosa da guardare è proprio una transazione rimasta aperta. Si chiude da sola allo scadere di transaction.timeout.ms del produttore.
Come accorgersene
- Controlla l'isolation level del client con cui stai guardando i dati, non solo quello dell'applicazione.
- Gli offset hanno dei buchi: ogni marcatore di transazione occupa un offset. Se fra un messaggio e il successivo manca un numero, lì c'è un marcatore.
- Confronta i conteggi: se «fine meno inizio» non torna con il numero di messaggi letti, in mezzo ci sono marcatori, transazioni annullate o messaggi compattati.
In Topicup
Topicup legge in read_committed: quello che vedi è quello che le applicazioni consumer con le impostazioni di default di librdkafka vedono. Un messaggio di una transazione annullata non compare né nella lettura né nella ricerca.