Cinque insidie di scalabilità da evitare con la tua applicazione Kafka

Uomo d'affari con occhiali che utilizza un laptop in un ufficio moderno

Apache Kafka è una piattaforma di event streaming ad alte prestazioni e altamente scalabile. Per sbloccare il pieno potenziale di Kafka, devi considerare attentamente il design della tua applicazione. È fin troppo facile scrivere applicazioni Kafka che funzionano male o che, alla fine, incontrano un muro di scalabilità. Dal 2015, IBM fornisce il servizio IBM Event Streams, un servizio Apache Kafka completamente gestito che gira su IBM Cloud. Da allora, il servizio ha aiutato molti clienti, così come diversi team all'interno di IBM, a risolvere problemi di scalabilità e prestazioni con le applicazioni Kafka che hanno scritto.

Questo articolo descrive alcuni dei problemi comuni di Apache Kafka e fornisce alcune raccomandazioni su come evitare di incontrare problemi di scalabilità con le tue applicazioni.

1. Ridurre al minimo l'attesa per i round-trip della rete

Alcune operazioni Kafka funzionano con l'invio dei dati al broker e l'attesa di una risposta. Un intero viaggio di andata e ritorno potrebbe durare 10 millisecondi, il che sembra veloce, ma limita al massimo 100 operazioni al secondo. Per questo motivo, si consiglia di evitare questo tipo di operazioni ogni volta che è possibile. Fortunatamente, i client Kafka offrono la possibilità di evitare di dover attendere i tempi di andata e ritorno. Devi solo assicurarti di sfruttarli al meglio.

Suggerimenti per massimizzare la produttività:

  1. Non controllare ogni messaggio inviato se è andato a buon fine. L'API di Kafka consente di disaccoppiare l'invio di un messaggio dalla verifica della ricezione del messaggio da parte del broker. Aspettare la conferma che un messaggio è stato ricevuto può introdurre una latenza di andata e ritorno di rete all'interno dell'applicazione, quindi cerca di minimizzarla quando possibile. Questo potrebbe significare inviare quanti più messaggi possibile, prima di verificare che siano stati tutti ricevuti. Oppure potrebbe significare delegare il controllo dell'avvenuta consegna dei messaggi a un altro thread di esecuzione all'interno della sua applicazione, in modo che possa essere eseguito in parallelo con l'invio di altri messaggi.
  2. Non seguire l'elaborazione di ogni messaggio con un commit offset. Il commit degli offset (in modo sincrono) è implementato come un round-trip di rete con il server. È possibile eseguire il commit degli offset con minore frequenza oppure utilizzare la funzione di commit degli offset asincroni per evitare di pagare il prezzo di questo round-trip per ogni messaggio elaborato. Tenga presente che impegnare gli offset con minore frequenza può significare che è necessario rielaborare più dati se la sua applicazione fallisce.

Se hai letto quanto sopra e hai pensato: "Ehi, questo non renderà la mia applicazione più complessa?", la risposta è sì, probabilmente sì. C'è un compromesso tra throughput e complessità dell'applicazione. Ciò che rende il tempo di andata e ritorno della rete una trappola particolarmente insidiosa è che, una volta raggiunto questo limite, possono essere necessarie ampie modifiche alle applicazioni per ottenere ulteriori miglioramenti della produttività.

2. Non permettere che i tempi di elaborazione aumentati vengano scambiati per errori dei consumer

Una caratteristica utile di Kafka è che monitora la "vitalità" delle applicazioni che consumano e disconnette quelle che potrebbero essere guaste. Funziona facendo in modo che il broker tenga traccia dell'ultima chiamata di ciascun client consumatore tramite "poll" (terminologia di Kafka per richiedere più messaggi). Se un client non esegue il polling con sufficiente frequenza, il broker a cui è connesso conclude che deve essersi verificato un errore e lo disconnette. Ciò è stato progettato per consentire ai clienti che non hanno riscontrato problemi di intervenire e riprendere il lavoro del cliente che ha fallito.

Sfortunatamente, con questo schema il broker Kafka non riesce a distinguere tra un client che impiega molto tempo per elaborare i messaggi ricevuti e un client che in realtà ha fallito. Consideriamo un'applicazione di consumo che esegue un loop: 1) chiama il poll e riceve un batch di messaggi; oppure 2) elabora ogni messaggio del batch, impiegando 1 secondo per elaborare ogni messaggio.

Se questo consumer riceve batch di 10 messaggi, passeranno circa 10 secondi tra le chiamate al polling. Di default, Kafka permette fino a 300 secondi (5 minuti) tra un sondaggio e l'altro prima di disconnettere il client quindi, in questo scenario, tutto funzionerebbe bene. Ma cosa succede in una giornata molto intensa, quando inizia ad accumularsi un arretrato di messaggi sull'argomento che l'applicazione sta consumando? Invece di ricevere solo 10 messaggi di risposta da ogni chiamata, la tua applicazione riceve 500 messaggi (di default questo è il numero massimo di record che può essere restituito tramite una chiamata di poll). Questo comporterebbe abbastanza tempo di elaborazione perché Kafka decida che l'istanza dell'applicazione è fallita e la disconnetta. E questa è una brutta notizia.

Ma ti farà piacere sapere che la situazione può anche peggiorare. È possibile che si verifichi una sorta di ciclo di feedback. Man mano che Kafka inizia a disconnettere i client perché non chiamano i poll abbastanza frequentemente, ci sono meno istanze in cui l'applicazione elabora i messaggi. La probabilità che ci sia un elevato arretrato di messaggi sull'argomento aumenta, con conseguente aumento della probabilità che più clienti ricevano grandi quantità di messaggi e impieghino troppo tempo per elaborarli. Alla fine, tutte le istanze dell'applicazione di consumo entrano in un ciclo di riavvio e non viene svolto alcun lavoro utile.

Quali misure puoi adottare per evitare che ciò accada anche a te?

  1. Il tempo massimo tra le chiamate di poll può essere impostato utilizzando la configurazione consumer di Kafka “max.poll.interval.ms”. Il numero massimo di messaggi che possono essere restituiti da un singolo poll è configurabile anche utilizzando “max.poll.records”. Come regola generale, cerca di ridurre il valore di "max.poll.records" nelle preferenze aumentando il valore di "max.poll.interval.ms" perché, impostando un intervallo massimo di polling elevato, Kafka impiegherà più tempo a identificare i consumer che non sono andati a buon fine.
  2. I consumer Kafka possono anche ricevere istruzioni per mettere in pausa e riprendere il flusso dei messaggi. La sospensione del consumo impedisce al metodo di polling di restituire messaggi, ma reimposta comunque il timer utilizzato per determinare se il client ha avuto un errore. Mettere in pausa e riprendere è una tattica utile se: a) ci si aspetta che i singoli messaggi richiedano potenzialmente molto tempo per essere elaborati; e b) vogliamo che Kafka sia in grado di rilevare un guasto del client durante l'elaborazione di un singolo messaggio.
  3. Non trascurare l'utilità delle metriche client di Kafka. L'argomento delle metriche potrebbe riempire un intero articolo a sé stante, ma in questo contesto il consumer espone le metriche sia per il tempo medio che per quello massimo tra i sondaggi. Il monitoraggio di queste metriche aiuta a identificare situazioni in cui un sistema a valle è la causa per cui ogni messaggio ricevuto da Kafka impiega più tempo del previsto per essere elaborato.

Torneremo sul tema dei fallimenti dei consumer più avanti in questo articolo, quando vedremo come possono innescare un riequilibrio tra i gruppi di consumer e l'effetto dirompente che ciò può avere.

3. Ridurre al minimo i costi dei consumer inattivi

Dietro le quinte, il protocollo utilizzato dal consumer Kafka per ricevere messaggi funziona inviando una richiesta di “fetch” a un broker Kafka. Come parte di questa richiesta, il client indica cosa deve fare il broker se non ci sono messaggi da restituire, incluso quanto tempo il broker deve attendere prima di inviare una risposta vuota. Per impostazione predefinita, i consumer Kafka indicano ai broker di attendere fino a 500 millisecondi (controllati dalla configurazione del consumer "fetch.max.wait.ms") affinché almeno 1 byte di dati del messaggio diventi disponibile (controllato con la configurazione "fetch.min.bytes").

Attendere 500 millisecondi non sembra irragionevole, ma se la tua applicazione ha consumer per lo più inattivi e si espande fino a 5.000 istanze, significa potenzialmente 2.500 richieste al secondo che non fanno assolutamente nulla. Ognuna di queste richieste richiede tempo di elaborazione da parte del broker e, nei casi più gravi, può avere un impatto negativo sulle prestazioni e sulla stabilità dei client Kafka che desiderano svolgere un lavoro utile.

Normalmente, l'approccio di Kafka alla scalabilità consiste nell'aggiungere altri broker e poi riequilibrare in modo uniforme le partizioni degli argomenti su tutti i broker, sia vecchi che nuovi. Purtroppo, questo approccio potrebbe non aiutare se i tuoi clienti bombardano Kafka con richieste di recupero inutili. Ogni cliente invierà richieste di recupero a tutti i broker che gestiscono una partizione tematica da cui il cliente sta consumando messaggi. Quindi è possibile che anche dopo aver scalato il cluster Kafka e ridistribuito le partizioni, la maggior parte dei tuoi client invierà richieste di fetch alla maggior parte dei broker.

Quindi cosa si può fare?

  1. Modificare la configurazione di Kafka per i consumer può aiutare a ridurre questo effetto. Desideri ricevere i messaggi non appena arrivano, il valore predefinito di "fetch.min.bytes" deve rimanere 1; tuttavia, l'impostazione "fetch.max.wait.ms" può essere aumentata a un valore maggiore e così facendo si ridurrà il numero di richieste effettuate dai consumer inattivi.
  2. In un contesto più ampio, la tua applicazione deve avere potenzialmente migliaia di istanze, ciascuna delle quali utilizza molto raramente dati da Kafka? Potrebbero esserci ottime ragioni per cui ciò avviene, ma forse ci sono progettazioni in grado di sfruttare Kafka in modo più efficiente. Tratteremo alcune di queste considerazioni nella prossima sezione.

4. Scegliere un numero appropriato di argomenti e partizioni

Se si arriva a Kafka da un background con altri sistemi publish-abbonarsi (ad esempio Message Queuing Telemetry Transport, o MQTT in breve), ci si potrebbe aspettare che gli argomenti di Kafka siano leggeri, quasi effimeri. Non lo sono. Kafka si trova molto più a suo agio con un numero di argomenti nell'ordine delle migliaia. Anche gli argomenti di Kafka sono destinati ad avere una vita relativamente lunga. Pratiche come la creazione di un argomento per ricevere un singolo messaggio di risposta, e poi l'eliminazione dell'argomento, sono poco comuni con Kafka e non sfruttano i suoi punti di forza.

Piuttosto, pianifica argomenti che durino a lungo. Forse condividono la durata di un'applicazione o di un'attività. Cerca anche di limitare il numero di argomenti a centinaia o magari a poche migliaia. Ciò potrebbe richiedere di adottare una prospettiva diversa sui messaggi intervallati da un argomento specifico.

Una domanda correlata che spesso ci si pone è: "Quante partizioni deve avere il mio argomento?" Tradizionalmente, il consiglio è quello di valutare per eccesso, perché aggiungere partizioni dopo la creazione di un argomento non cambia il partizionamento dei dati esistenti sull'argomento (e quindi può influenzare i consumer che si affidano al partizionamento per offrire l'ordinamento dei messaggi all'interno di una partizione). Questo è un buon consiglio. Tuttavia, vorremmo suggerire alcune considerazioni aggiuntive:

  1. Per argomenti che possono aspettarsi una velocità misurata in MB/secondo, o dove la velocità può crescere con l'espansione dell'applicazione, raccomandiamo vivamente di avere più di una partizione, così che il carico possa essere distribuito su più broker. Il servizio Event Streams gestisce sempre Kafka con un multiplo di 3 broker. Al momento in cui scriviamo, il numero massimo di broker è 9, ma forse in futuro questo numero aumenterà. Se scegli un multiplo di 3 per il numero di partizioni nel tuo argomento, allora può essere bilanciato equamente tra tutti i broker.
  2. Il numero di partizioni in un argomento è il limite del numero di consumer Kafka che possono utilmente condividere i messaggi di consumo dell'argomento con i gruppi di consumer Kafka (ne parleremo più avanti). Se aggiungono più consumer a un gruppo rispetto alle partizioni presenti nell'argomento, alcuni consumer rimarranno inattivi senza consumare i dati dei messaggi.
  3. Non c'è nulla di intrinsecamente sbagliato nell'avere argomenti a partizione singola, purché tu sia assolutamente sicuro che non riceveranno mai un traffico significativo di messaggi, altrimenti non ti affiderai all'ordine all'interno di un argomento e sarai felice di aggiungere altre partizioni in seguito.

5. Riequilibrare i gruppi di consumer può essere sorprendentemente destabilizzante

La maggior parte delle applicazioni Kafka che consumano messaggi utilizza al meglio le funzionalità di Kafka per i gruppi di consumer per coordinare quali client consumano da quali partizioni di argomento. Se il tuo ricordo dei gruppi di consumer è un po' confuso, ecco un rapido riepilogo dei punti chiave:

  • I gruppi di consumer coordinano un gruppo di client Kafka in modo che solo un client riceva messaggi da una particolare partizione di argomenti in qualsiasi momento. Questo è utile se vuoi condividere i messaggi su un argomento tra più istanze di un'applicazione.
  • Quando un client Kafka si unisce a un gruppo di consumer o ne abbandona uno gruppo a cui aveva precedentemente aderito, il gruppo di consumer viene riequilibrato. Comunemente, i clienti si uniscono a un gruppo di consumer quando l'applicazione di cui fanno parte viene avviata e se ne vanno perché l'applicazione viene chiusa, riavviata o si blocca.
  • Quando un gruppo viene riequilibrato, le partizioni degli argomenti vengono ridistribuite tra i membri del gruppo. Ad esempio, se un client si unisce a un gruppo, ad alcuni dei client già presenti nel gruppo potrebbero essere sottratte partizioni di argomenti (o "revocate" nella terminologia di Kafka) da assegnare al client che si unisce di recente. Vale anche il contrario: quando un client lascia un gruppo, le partizioni di argomento assegnate vengono ridistribuite tra i membri rimanenti.

Con la maturazione di Kafka, sono stati (e continuano ad essere) ideati algoritmi di ribilanciamento sempre più sofisticati. Nelle prime versioni di Kafka, quando un gruppo di consumer veniva ribilanciato, tutti i client del gruppo dovevano smettere di consumare, le partizioni degli argomenti venivano ridistribuite tra i nuovi membri del gruppo e tutti i client ricominciavano a consumare. Questo approccio presenta due svantaggi (che nel frattempo sono stati migliorati):

  1. Tutti i clienti del gruppo smettono di consumare messaggi durante il riequilibrio. Ciò ha ovvie ripercussioni sulla produttività.
  2. I client Kafka in genere cercano di mantenere un buffer di messaggi che devono ancora essere consegnati all'applicazione e di recuperare altri messaggi dal broker prima che il buffer venga svuotato. L'intento è impedire che la consegna dei messaggi all'applicazione si blocchi mentre altri messaggi vengono recuperati dal broker Kafka (sì, come già scritto in questo articolo, il client Kafka cerca anche di evitare di aspettare i viaggi di andata e ritorno di rete). Sfortunatamente, quando un ribilanciamento provoca la revoca delle partizioni da un client, tutti i dati memorizzati nel buffer per la partizione devono essere eliminati. Allo stesso modo, quando il ribilanciamento fa sì che una nuova partizione venga assegnata a un client, il client inizierà a bufferizzare i dati a partire dall'ultimo offset commesso per la partizione, causando potenzialmente un picco di throughput di rete da broker a client. Ciò è dovuto al fatto che il client a cui è stata assegnata di nuovo la partizione rilegge i dati dei messaggi che erano stati precedentemente bufferizzati dal client a cui è stata revocata la partizione.

Gli algoritmi di ribilanciamento più recenti hanno apportato miglioramenti significativi, aggiungendo, per usare la terminologia di Kafka, "viscosità" e "cooperazione":

  • Gli algoritmi "sticky" cercano di garantire che, dopo un ribilanciamento, il maggior numero possibile di membri del gruppo mantenga le stesse partizioni che avevano prima del ribilanciamento. In questo modo si riduce al minimo la quantità di dati dei messaggi memorizzati nel buffer che vengono scartati o riletti da Kafka quando si verifica il ribilanciamento.
  • Gli algoritmi "cooperativi" consentono ai client di continuare a consumare messaggi mentre si verifica un ribilanciamento. Quando a un client viene assegnata una partizione prima di un ribilanciamento e la mantiene anche dopo che il ribilanciamento è avvenuto, può continuare a consumare dalle partizioni ininterrotte tramite il ribilanciamento. Questo è sinergico con la “viscosità”, che agisce mantenendo le partizioni assegnate allo stesso client.

Nonostante questi miglioramenti agli algoritmi di ribilanciamento più recenti, se le tue applicazioni sono frequentemente soggette a ribilanciamenti dei gruppi di consumer, vedrai comunque un impatto sulla velocità complessiva dei messaggi e sprecherai banda di rete mentre i client scartano e recuperano i dati dei messaggi in buffer. Ecco alcuni suggerimenti su cosa puoi fare:

  1. Assicurati di riuscire a individuare quando avviene il riequilibrio. Su larga scala, raccogli e visualizza metriche è la scelta migliore. Questa è una situazione in cui una vasta gamma di fonti metriche aiuta a costruire il quadro completo. Il broker Kafka dispone di metriche sia per la quantità di byte di dati inviati ai clienti, sia per il numero di gruppi di consumer che vengono ribilanciati. Se stai raccogliendo metriche dalla tua applicazione, o dal suo tempo di esecuzione, che mostrano quando si verificano i riavvii, la correlazione con le metriche del broker può fornire un'ulteriore conferma che il ribilanciamento è un problema per te.
  2. Evita i riavvii inutili delle applicazioni quando, ad esempio, un'applicazione si blocca. Se si verificano problemi di stabilità con la tua applicazione, ciò potrebbe comportare un ri-bilanciamento molto più frequente del previsto. Cercare nei log delle applicazioni messaggi di errore comuni emessi da un crash dell'applicazione, ad esempio le tracce dello stack, può aiutare a identificare con quale frequenza si verificano i problemi e fornire informazioni utili per debugare il problema sottostante.
  3. Stai usando il miglior algoritmo di ribilanciamento per la tua applicazione? Al momento della stesura, il punto di riferimento è il "CooperativeStickyAssignor"; tuttavia, il predefinito (a partire da Kafka 3.0) è utilizzare il "RangeAssignor" (e l'algoritmo di assegnazione precedente) in preferenza rispetto all'assegnatore cooperativo sticky. La documentazione Kafka descrive i passaggi di migrazione necessari affinché i tuoi clienti possano ritirare l'assegnatore cooperativo e sticky. Vale anche la pena notare che, sebbene il cedente cooperativo permanente sia una buona scelta a tutto tondo, ci sono altri assegnatori su misura per casi d'uso specifici.
  4. I membri di un gruppo di consumer sono fissi? Ad esempio, forse esegui sempre 4 istanze altamente disponibili e distinte di un'applicazione. Potresti riuscire a utilizzare al meglio la caratteristiche di appartenenza a gruppi statici di Kafka. Assegnando ID unici a ogni istanza della tua applicazione, l'appartenenza statica ai gruppi ti permette di evitare completamente il ribilanciamento.
  5. Impegna l'offset corrente quando una partizione viene revocata dall'istanza della sua applicazione. Il client consumer di Kafka fornisce un listener per gli eventi di riequilibrio. Se un'istanza della tua applicazione sta per subire la revoca di una partizione, l'ascoltatore offre l'opportunità di effettuare un commit offset per la partizione che sta per essere rimossa. Il vantaggio di effettuare un commit di offset nel momento in cui la partizione viene revocata è che garantisce che il membro del gruppo assegnato alla partizione riprenda da questo punto, invece di rielaborare potenzialmente alcuni messaggi dalla partizione.

Le ultime notizie nel campo della tecnologia, supportate dalle analisi degli esperti

Resta al passo con le tendenze più importanti e interessanti del settore relative ad AI, automazione, dati e oltre con la newsletter Think. Leggi l' Informativa sulla privacy IBM.

Grazie per aver effettuato l'iscrizione!

L'abbonamento sarà fornito in lingua inglese. Troverai un link per annullare l'iscrizione in tutte le newsletter. Puoi gestire i tuoi abbonamenti o annullarli qui. Per ulteriori informazioni, consulta l'Informativa sulla privacy IBM.

Per il futuro:

Ora sei un esperto nella scalabilità delle applicazioni Kafka. Sei invitato a mettere in pratica questi punti e provare l'offerta completamente gestita di Kafka su IBM Cloud. Per qualsiasi problema di configurazione, consulti la Guida introduttiva e le FAQ.

 
Soluzioni correlate
IBM Event Streams

IBM Event Streams è un software per lo streaming di eventi basato sull'open source Apache Kafka. È disponibile come servizio totalmente gestito su IBM Cloud o in self-hosting.

Esplora Event Streams
Software e soluzioni di integrazione

Sblocca il potenziale aziendale con le soluzioni di integrazione di IBM, collegando applicazioni e sistemi per accedere rapidamente e in modo sicuro ai dati d'importanza critica.

Esplora le soluzioni di integrazione
Servizi di consulenza cloud

Sblocca nuove funzionalità e promuovi l'agilità aziendale con i servizi di consulenza cloud di IBM.

Esplora i servizi di consulenza cloud
Prossimi passi

IBM Event Streams è un software per lo streaming di eventi basato sull'open source Apache Kafka. È disponibile come servizio totalmente gestito su IBM Cloud o in self-hosting.

Esplora Event Streams Ottieni maggiori informazioni