Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Cosa può spingere una grande azienda come Lamoda, con un processo collaudato e decine di servizi interconnessi, a cambiare radicalmente approccio? Le motivazioni possono essere le più varie: dalla legislazione al desiderio innato di ogni programmatore di sperimentare.

Ma questo non significa affatto che non si possa contare su un ulteriore vantaggio. In cosa si può guadagnare concretamente introducendo un API basato su eventi su Kafka, lo racconterà Sergey Zaika (fewald). Ci saranno sicuramente anche esperienze di errori e scoperte interessanti — un esperimento non può prescinderne.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Disclaimer: Questo articolo è basato sui materiali di un meetup che Sergey ha tenuto a novembre 2018 su HighLoad++. L'esperienza pratica di Lamoda nell'utilizzo di Kafka ha attratto il pubblico non meno degli altri interventi in programma. Ci sembra un ottimo esempio di come sia sempre possibile e necessario trovare affinità, e gli organizzatori di HighLoad++ continueranno a lavorare per creare un'atmosfera ideale per questo.

Sul processo

Lamoda è una grande piattaforma e-commerce che dispone di un proprio centro contatti, un servizio di consegna (e numerose partnership), uno studio fotografico, un enorme magazzino e tutto ciò è gestito con il proprio software. Sono disponibili decine di metodi di pagamento, partner B2B che possono utilizzare parte o tutti questi servizi e vogliono avere informazioni aggiornate sui propri prodotti. Inoltre, Lamoda opera in tre paesi oltre alla Russia, e lì tutto funziona in modo leggermente diverso. In totale, ci sono probabilmente più di un centinaio di modi per configurare un nuovo ordine, il quale deve essere elaborato in modo specifico. Tutto questo funziona attraverso decine di servizi che comunicano in modi a volte non evidenti. C'è anche un sistema centrale, la cui principale responsabilità è gestire gli stati degli ordini. Noi la chiamiamo BOB, e io ci lavoro.

Strumento di rimborso con API basata su eventi

La parola "basata su eventi" è piuttosto inflazionata, e più avanti definiremo meglio cosa si intende. Inizierò dal contesto in cui abbiamo deciso di testare l'approccio dell'API basata su eventi con Kafka.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

In qualsiasi negozio, oltre agli ordini per i quali i clienti pagano, ci sono momenti in cui è necessario restituire denaro perché il prodotto non è adatto al cliente. Questo è un processo relativamente breve: verifichiamo le informazioni, se necessario, e trasferiamo il denaro.

Ma il processo di restituzione è diventato complicato a causa delle modifiche legislative, e abbiamo dovuto implementare un microservizio separato per gestirlo.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

La nostra motivazione:

  1. Legge FZ-54 — in sintesi, la legge richiede di comunicare all'agenzia fiscale ogni operazione monetaria, che si tratti di un rimborso o di un incasso, in un SLA piuttosto breve di alcuni minuti. Noi, come e-commerce, svolgiamo molte operazioni. Questo significa una nuova responsabilità (e quindi un nuovo servizio) e modifiche in tutti i sistemi coinvolti.
  2. BOB split — un progetto interno dell'azienda per liberare BOB da un gran numero di responsabilità non core e ridurre la sua complessità complessiva.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

In questo schema sono rappresentati i principali sistemi di Lamoda. Attualmente, la maggior parte di essi rappresenta piuttosto una costellazione di 5-10 microservizi attorno a un monolite in riduzione.. Crescono lentamente, ma ci sforziamo di ridurli, perché è spaventoso eseguire il deploy di un frammento critico — non possiamo permettere che si interrompa. Siamo costretti a riservare tutti gli scambi (le frecce) e a considerarli sempre disponibili.

In BOB ci sono anche molti scambi: sistemi di pagamento, consegne, notifiche, ecc.

Tecnicamente BOB è:

  • ~150k righe di codice + ~100k righe di test;
  • php7.2 + Zend 1 & Symfony Components 3;
  • >100 API & ~50 integrazioni in uscita;
  • 4 paesi con la propria logica di business.

Eseguire il deploy di BOB è costoso e doloroso, la quantità di codice e le sfide coinvolte sono tali che nessuno può tenere tutto in mente. Insomma, ci sono molti motivi per semplificarlo.

Processo di rimborso

Inizialmente sono coinvolti due sistemi: BOB e Payment. Ora ne emergono altri due:

  • Fiscalization Service, che si occuperà dei problemi di fiscalizzazione e della comunicazione con i servizi esterni.
  • Refund Tool, dove vengono semplicemente gestiti i nuovi scambi, per non gonfiare BOB.

Ora il processo appare così:

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

  1. A BOB arriva una richiesta di rimborso.
  2. BOB informa il Refund Tool.
  3. Il Refund Tool comunica a Payment: «Restituisci i soldi».
  4. Payment restituisce i soldi.
  5. Refund Tool e BOB sincronizzano i loro stati perché entrambi ne hanno bisogno. Non siamo ancora pronti a passare completamente a Refund Tool, poiché BOB ha un'interfaccia utente, report per la contabilità e molti dati che non si possono trasferire così facilmente. Dobbiamo restare su due fronti.
  6. Viene inviata una richiesta di fiscalizzazione.

Alla fine abbiamo creato su Kafka un certo bus di eventi — un event-bus, sul quale tutto si basa. Evviva, ora abbiamo un punto unico di fallimento (sarcasmo).

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Pro e contro sono piuttosto evidenti. Abbiamo creato un bus, quindi ora tutti i servizi ne dipendono. Questo semplifica la progettazione, ma introduce un punto unico di fallimento nel sistema. Se Kafka crolla, il processo si ferma.

Cos'è l'API event-driven?

Una buona risposta a questa domanda si trova nella presentazione di Martin Fowler (GOTO 2017) «The Many Meanings of Event-Driven Architecture».

In sintesi, ciò che abbiamo fatto:

  1. Abbiamo incapsulato tutti gli scambi asincroni tramite storage degli eventi. Invece di comunicare in rete a ogni consumatore interessato una modifica di stato, scriviamo in un archivio centralizzato un evento di cambiamento di stato, e i consumatori interessati all'argomento leggono da lì tutto ciò che appare.
  2. L'evento in questo caso è una notifica (notifiche) che indica che qualcosa è cambiato da qualche parte. Ad esempio, è cambiato lo stato di un ordine. Il consumatore, che ha bisogno di ulteriori dati sul cambiamento di stato che non sono presenti nella notifica, può verificare il proprio stato autonomamente.
  3. La soluzione massima è un event sourcing completo, state transfer, in cui l'evento contiene tutte le informazioni necessarie per il trattamento: da dove e in quale stato si è passati, come sono stati modificati i dati, e così via. La questione riguarda solo la fattibilità e il volume di informazioni che si può permettere di conservare.

Nell'ambito del lancio di Refund Tool abbiamo utilizzato la terza opzione. Questo ha semplificato l'elaborazione degli eventi, poiché non è necessario estrarre informazioni dettagliate, inoltre ha escluso il scenario in cui ogni nuovo evento genera un aumento di richieste get di chiarimento da parte dei consumatori.

Il servizio Refund Tool non è sovraccarico, quindi Kafka è più una prova di concetto che una necessità. Non penso che se il servizio di rimborso diventasse un progetto ad alto carico, il business sarebbe contento.

Async exchange AS IS

Per gli scambi asincroni, il dipartimento PHP utilizza solitamente RabbitMQ. Abbiamo raccolto i dati per la richiesta, li abbiamo messi in coda e il consumatore dello stesso servizio li ha letti e inviati (o non inviati). Per l'API, Lamoda utilizza attivamente Swagger. Progettiamo l'API, la descriviamo in Swagger, generiamo codice client e server. Utilizziamo anche un JSON RPC 2.0 leggermente esteso.

In alcuni casi vengono utilizzate le bus ESB, qualcuno vive su ActiveMQ, ma in generale, RabbitMQ è lo standard.

Scambio asincrono TO BE

Progettando lo scambio tramite events-bus, si traccia un'analogia. Descriviamo in modo simile il futuro scambio di dati attraverso le descrizioni della struttura dell'evento. Il formato è YAML, abbiamo dovuto scrivere noi la code generation, il generatore secondo la specifica crea DTO e insegna ai client e ai server a lavorare con essi. La generazione avviene in due lingue - golang e php. Questo consente di mantenere le librerie allineate. Il generatore è scritto in golang, da cui il nome gogi.

Event-sourcing su Kafka è una pratica comune. Esiste una soluzione dalla principale versione enterprise Kafka Confluent, ci sono nakadi, una soluzione dai nostri 'fratelli' nel campo di Zalando. La nostra motivazione per iniziare con vanilla Kafka — è lasciare la soluzione gratuita finché non decideremo se utilizzarla ovunque, oltre a mantenere spazio per manovre e miglioramenti: vogliamo supportare il nostro JSON RPC 2.0, generatori per due lingue e vediamo cos'altro.

Ironico che anche in un caso così felice, quando c'è un'attività simile come Zalando che ha fatto una soluzione simile, non possiamo utilizzarla in modo efficace.

Architettonicamente, all'avvio il pattern è il seguente: leggiamo direttamente da Kafka, ma scriviamo solo tramite events-bus. Per la lettura in Kafka ci sono molte soluzioni pronte: broker, bilanciatori e è più o meno pronta per la scalabilità orizzontale, questo volevamo mantenere. Per la scrittura, invece, abbiamo voluto avvolgerla tramite un Gateway alias Events-bus, ed ecco perché.

Events-bus

O bus degli eventi. È semplicemente un gateway http stateless che assume su di sé diversi ruoli importanti:

  • Validazione della produzione — controlliamo che gli eventi corrispondano alla nostra specifica.
  • Sistema master per gli eventi, cioè è l'unico e principale sistema dell'azienda che risponde alla domanda su quali eventi con quali strutture siano considerati validi. La validazione include semplicemente i tipi di dati e gli enum per una specifica rigorosa del contenuto.
  • Funzione di hash per il partizionamento — la struttura del messaggio Kafka è key-value e si calcola dove metterlo in base all'hash della chiave.

Perché

Lavoriamo in una grande azienda con un processo ben definito. Perché cambiare qualcosa? È un esperimento, e ci aspettiamo di ottenere diversi vantaggi.

Scambi 1:n+1 (uno a molti)

Con Kafka è molto semplice connettere nuove API dei consumatori.

Supponiamo di avere un catalogo che deve essere mantenuto aggiornato in più sistemi contemporaneamente (anche in nuovi sistemi). In precedenza, inventavamo un bundle che implementava le set-API, e comunicavamo gli indirizzi dei consumatori al sistema principale. Ora il sistema principale invia aggiornamenti al topic, e tutti quelli interessati possono leggerli. È stato creato un nuovo sistema — l'abbiamo iscritto al topic. Sì, anche questo è un bundle, ma più semplice.

Nel caso dello strumento di rimborso, che è essenzialmente un pezzo di BOB, ci è comodo tenerli sincronizzati tramite Kafka. Il sistema di pagamento comunica che i soldi sono stati restituiti: BOB e RT ne sono stati informati, hanno aggiornato i loro stati, il Servizio di Fiscalizzazione è stato avvisato e ha emesso lo scontrino.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Abbiamo piani per creare un unico Servizio Notifiche, che informerà il cliente sulle novità riguardanti il suo ordine/ritorni. Attualmente questa responsabilità è sparsa tra i diversi sistemi. Sarà sufficiente insegnare al Servizio Notifiche a monitorare Kafka per le informazioni pertinenti e reagire di conseguenza (e disabilitare queste notifiche negli altri sistemi). Non saranno necessari nuovi scambi diretti.

Data-driven

Le informazioni tra i sistemi diventano trasparenti, indipendentemente da quale 'enterprise draconiano' abbiate o quanto sia consistente il vostro backlog. In Lamoda esiste un dipartimento di Data Analytics che raccoglie dati dai sistemi e li rende riutilizzabili, sia per il business che per i sistemi intelligenti. Kafka consente di fornire rapidamente a loro una grande quantità di dati e di mantenere questo flusso informativo aggiornato.

Registro di replicazione

I messaggi non scompaiono dopo essere stati letti, come in RabbitMQ. Quando un evento contiene informazioni sufficienti per essere elaborato, abbiamo una cronologia degli ultimi cambiamenti dell'oggetto e, se lo desideriamo, la possibilità di applicare queste modifiche.

Il periodo di conservazione del replication log dipende dall'intensità della scrittura in questo argomento; Kafka consente di configurare flessibilmente i limiti di tempo di conservazione e di volume dei dati. Per argomenti intensivi, è importante che tutti i consumatori riescano a leggere le informazioni prima che scompaiano, anche in caso di brevi periodi di inattività. Di solito riesce a conservare i dati per un numero di giorni, il che è più che sufficiente per il supporto.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Di seguito un breve riassunto della documentazione, per coloro che non sono familiari con Kafka (l'immagine è anch'essa tratta dalla documentazione)

In AMQP ci sono code: scriviamo messaggi in una coda per il consumatore. In generale, una coda è gestita da un solo sistema con la stessa logica di business. Se è necessario avvertire più sistemi, è possibile insegnare all'applicazione a scrivere in più code o configurare uno exchange con meccanismo fanout, che le clona automaticamente.

In Kafka esiste un'astrazione simile topic, in cui scrivi messaggi che non scompaiono dopo la lettura. Per impostazione predefinita, collegandoti a Kafka, ricevi tutti i messaggi e hai la possibilità di salvare il punto in cui ti sei fermato. Puoi quindi leggere in modo sequenziale, senza contrassegnare il messaggio come letto, ma conservando l'id da cui continuerai a leggere. L'id, dove ti sei fermato, si chiama offset, e il meccanismo è il commit offset.

Di conseguenza, è possibile implementare logiche diverse. Ad esempio, abbiamo BOB che esiste in 4 istanze per diversi paesi: Lamoda è presente in Russia, Kazakistan, Ucraina e Bielorussia. Poiché vengono distribuiti separatamente, hanno leggermente le loro configurazioni e la loro logica aziendale. Indichiamo nel messaggio a quale paese si riferisce. Ogni consumatore BOB in ogni paese legge con groupId diversi e, se il messaggio non lo riguarda, lo salta, ovvero committa immediatamente offset +1. Se lo stesso topic viene letto dal nostro Payment Service, allora lo fa con un gruppo separato, quindi gli offset non si sovrappongono.

Requisiti per gli eventi:

  • Completezza dei dati. Vorremmo che ci fossero abbastanza dati nell'evento affinché possa essere elaborato.

  • Integrità. Delega a Events-bus il compito di verificare che l'evento sia coerente e che possa elaborarlo.
  • L'ordine è importante. Nel caso di un ritorno, dobbiamo fare i conti con la storia. Con le notifiche, invece, l'ordine non ha importanza, se si tratta di notifiche omogenee; l'email sarà la stessa indipendentemente da quale ordine è arrivato per primo. Per un ritorno, c'è un processo chiaro: cambiare l'ordine può portare a eccezioni, il rimborso non verrà creato o elaborato, e finiremo in uno stato diverso.
  • Coerenza. Abbiamo uno storage e ora stiamo creando eventi invece di API. Abbiamo bisogno di un modo per trasmettere rapidamente e a basso costo informazioni sui nuovi eventi e sulle modifiche a quelli esistenti ai nostri servizi. Questo viene raggiunto grazie a una specifica comune in un repository git separato e a generatori di codice. Così i clienti e i server in diversi servizi sono allineati.

Kafka in Lamoda

Abbiamo tre installazioni di Kafka:

  1. Logs;
  2. R&D;
  3. Events-bus.

Oggi parliamo solo dell'ultimo punto. Nel nostro events-bus non abbiamo installazioni molto grandi: 3 broker (server) e solo 27 topic. Di solito, un topic corrisponde a un processo. Ma è un momento delicato, e ora lo affronteremo.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Sopra è il grafico rps. Il processo di restituzione è contrassegnato dalla linea turchese (sì, quella che si trova sull'asse X), e rosa - il processo di aggiornamento dei contenuti.

Il catalogo Lamoda contiene milioni di articoli, e i dati vengono aggiornati continuamente. Alcune collezioni escono di moda, mentre altre nuove entrano in catalogo. Continuiamo a cercare di prevedere cosa potrebbe interessare ai nostri clienti domani, quindi acquistiamo costantemente nuovi articoli, li fotografiamo e aggiorniamo la vetrina.

I picchi rosa rappresentano gli aggiornamenti dei prodotti, ovvero modifiche relative agli articoli. È evidente che il team ha continuato a fotografare e a un certo punto ha caricato un mucchio di eventi.

Casi d'uso di Lamoda Events

Utilizziamo l'architettura costruita per i seguenti operazioni:

  • Monitoraggio degli stati di restituzione: call-to-action e tracciamento degli stati da tutti i sistemi coinvolti. Pagamenti, stati, fiscalizzazione, notifiche. Qui abbiamo testato l'approccio, creato gli strumenti, raccolto tutti i bug, scritto la documentazione e spiegato ai colleghi come usarli.
  • Aggiornamento delle schede prodotto: configurazione, metadati, caratteristiche. Viene letta da un sistema (che visualizza), mentre ne scrivono diversi.
  • Email, push e sms: ordine raccolto, ordine consegnato, reso accettato, ecc., ce ne sono molti.
  • Stock, aggiornamento magazzino — aggiornamento quantitativo delle denominazioni, solo numeri: arrivi in magazzino, resi. Tutti i sistemi legati alla riserva del prodotto devono operare con i dati più aggiornati. Attualmente, il sistema di aggiornamento dello stock è piuttosto complesso, Kafka lo semplificherà.
  • Analisi dei dati (dipartimento R&D), strumenti di ML, analisi, statistiche. Vogliamo che le informazioni siano trasparenti — per questo Kafka è molto adatto.

Ora la parte più interessante riguarda le esperienze e le scoperte che sono avvenute nel corso di sei mesi.

Problemi di progettazione

Supponiamo di voler introdurre una novità: ad esempio, trasferire l'intero processo di consegna su Kafka. Attualmente, parte del processo è gestita nell'Order Processing di BOB. Dietro il passaggio dell'ordine al servizio di consegna, il movimento verso un magazzino intermedio e altro, ci sono modelli di stato. Esiste un intero monolite, addirittura due, oltre a una serie di API dedicate alla consegna. Queste sanno molto di più sulla consegna.

Sembra che queste siano aree simili, ma gli stati per l'Order Processing in BOB e per il sistema di consegna sono differenti. Ad esempio, alcuni servizi di corriere non inviano stati intermedi, ma solo finali: "consegnato" o "perso". Altri, al contrario, forniscono dettagli molto accurati sul movimento della merce. Ognuno ha le proprie regole di validazione: per qualcuno un'email valida significa che verrà elaborata; per altri, anche se non è valida, l'ordine verrà comunque gestito perché c'è un numero di telefono per contattare, mentre qualcun altro dirà che un tale ordine non verrà elaborato affatto.

Flusso di dati

Nel caso di Kafka, emerge la questione dell'organizzazione del flusso di dati. Questa attività è legata alla scelta di una strategia su diversi punti, esamineremo tutti questi aspetti.

In un topic o in diversi?

Abbiamo una specifica dell'evento. In BOB scriviamo che un determinato ordine deve essere consegnato, indicando: il numero dell'ordine, il suo contenuto, alcuni SKU e codici a barre, ecc. Quando la merce arriva al magazzino, la consegna può ricevere stati, timestamp e tutto ciò che serve. Ma poi desideriamo ricevere aggiornamenti su questi dati in BOB. Si genera un processo inverso per ottenere i dati dalla consegna. È lo stesso evento? O è uno scambio separato che merita un tema a parte?

Probabilmente saranno molto simili, e la tentazione di creare un unico argomento non è infondata, poiché un tema separato comporterebbe consumatori distinti, configurazioni diverse e la generazione separata di tutto ciò. Ma non è un dato di fatto.

Campo nuovo o evento nuovo?

Ma se utilizziamo gli stessi eventi, emerge un altro problema. Ad esempio, non tutti i sistemi di consegna possono generare un DTO che possa essere elaborato da BOB. Inviamo loro l'ID, ma loro non lo memorizzano perché non ne hanno bisogno, mentre dal punto di vista dell'inizio del processo dell'event-bus, questo campo è obbligatorio.

Se imponiamo una regola per l'event-bus che rende questo campo obbligatorio, allora siamo costretti a inserire regole di validazione aggiuntive in BOB o nel gestore dell'evento di avvio. La validazione tende a diffondersi nel servizio — il che non è molto comodo.

Un altro problema è la tentazione dello sviluppo incrementale. Ci dicono che dobbiamo aggiungere qualcosa all'evento, e, forse, se ci pensiamo bene, avrebbe dovuto essere un evento separato. Ma nel nostro schema un evento separato è un topic separato. Un topic separato rappresenta l'intero processo che ho descritto sopra. Il programmatore è tentato di aggiungere semplicemente un altro campo nello schema JSON e rigenerarlo.

Nel caso dei rimborsi, in sei mesi siamo arrivati all'evento eventi. Avevamo un meta-evento chiamato aggiornamento rimborso, nel quale era presente il campo tipo, che descriveva di cosa si trattasse questo aggiornamento. Da ciò abbiamo avuto 'fantastici' switch con i validatori che indicavano come validare questo evento con questo tipo.

Versioning degli eventi

Per la validazione dei messaggi in Kafka si può usare Avro, ma era necessario pianificare fin da subito e utilizzare Confluent. Nel nostro caso con la versione bisogna essere cauti. Non sempre sarà possibile rileggere i messaggi dal replication log, perché il modello potrebbe “andare in confusione”. In generale, si riesce a costruire versioni in modo che il modello sia retrocompatibile: ad esempio, rendere temporaneamente un campo non obbligatorio. Se le differenze sono troppo forti, iniziamo a scrivere in un nuovo topic e migriamo i clienti quando hanno terminato di leggere quello vecchio.

Garanzia dell'ordine di lettura delle partitions

I topic all'interno di Kafka sono suddivisi in partitions. Questo non è molto importante mentre progettiamo entità e scambi, ma lo diventa quando decidiamo come consumarli e scalare.

In a typical scenario, you write to a single topic in Kafka. By default, one partition is used, and all messages for this topic go into it. The consumer, in turn, reads these messages sequentially. Now, suppose you need to scale the system so that two different consumers read the messages. For example, when you send an SMS, you can instruct Kafka to create an additional partition, and Kafka will start distributing messages into two parts—half here, half there.

How does Kafka divide them? Each message has a body (where we store JSON) and a key. A hash function can be applied to this key, which will determine which partition the message will enter.

In our case with refunds, this is important; if we take two partitions, there is a chance that a parallel consumer will process the second event before the first, which could lead to issues. The hash function ensures that messages with the same key land in the same partition.

Events vs commands

Questo è un altro problema con cui ci siamo imbattuti. Un evento è un verificarsi di qualcosa: diciamo che qualcosa è successo (something_happened), ad esempio, un articolo è stato annullato o c'è stato un rimborso. Se qualcuno ascolta questi eventi, allora per "articolo annullato" verrà creata un'entità di rimborso, e "c'è stato un rimborso" verrà registrato da qualche parte nelle impostazioni.

Ma di solito, quando progetti eventi, non vuoi scriverli a vuoto - stai contando sul fatto che qualcuno li leggerà. C'è una forte tentazione di non scrivere something_happened (item_canceled, refund_refunded), ma something_should_be_done. Ad esempio, l'articolo è pronto per il rimborso.

Da un lato, questo suggerisce come verrà utilizzato l'evento. Dall'altro lato, assomiglia molto meno a un normale nome di evento. Inoltre, da qui è solo un passo fino al comando do_something. Ma non hai la garanzia che questo evento sia stato letto da qualcuno; e se è stato letto, che sia stato letto con successo; e se è stato letto con successo, che sia stata compiuta un'azione, e che questa azione sia andata a buon fine. Nel momento in cui l'evento diventa do_something, è necessaria una retroazione, e questo è un problema.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Nella comunicazione asincrona in RabbitMQ, quando leggi un messaggio e fai una richiesta http, hai una risposta — almeno quella che il messaggio è stato ricevuto. Quando scrivi in Kafka, hai un messaggio che attesta che hai scritto in Kafka, ma non sai nulla su come sia stato elaborato.

Pertanto, nel nostro caso, è stato necessario introdurre un evento di risposta e configurare il monitoraggio affinché, se si verifica un certo numero di eventi entro un determinato intervallo di tempo, dovrebbero arrivare altrettanti eventi di risposta. Se ciò non accade, sembra che qualcosa sia andato storto. Ad esempio, se inviamo l'evento «item_ready_to_refund», ci aspettiamo che venga creata una refund, il cliente riceverà indietro i soldi, e noi riceveremo l'evento «money_refunded». Ma non è certo, quindi è necessario il monitoraggio.

Sfumature

C'è un problema piuttosto ovvio: se leggi i messaggi da un topic in modo sequenziale e hai un messaggio problematico, il consumer si blocca e non puoi più andare avanti. Devi fermare tutti i consumer, committare l'offset più avanti per poter continuare a leggere.

Ne eravamo a conoscenza, lo avevamo previsto, eppure è successo. Questo è accaduto perché l'evento era valido secondo l'events-bus, l'evento era valido secondo il validatore dell'applicazione, ma non era valido secondo PostgreSQL, poiché in un sistema abbiamo MySQL con UNSIGNED INT, mentre nel sistema appena scritto c'era PostgreSQL con INT. Ha una dimensione leggermente più piccola, e l'Id non è passato. Symfony è andato in errore con un'eccezione. Ovviamente abbiamo catturato l'eccezione, perché eravamo già preparati, e volevamo commettere questo offset, ma prima volevamo incrementare il contatore dei problemi, visto che il messaggio è stato elaborato con esito negativo. I contatori in questo progetto sono anch'essi memorizzati nel database, e Symfony aveva già interrotto la comunicazione con il database, e la seconda eccezione ha ucciso il processo senza possibilità di commettere l'offset.

Un po' di tempo, il servizio è rimasto inattivo - per fortuna, con Kafka non è poi così grave, poiché i messaggi rimangono. Quando il lavoro riprenderà, sarà possibile leggerli di nuovo. Questo è comodo.

Kafka offre la possibilità di impostare un offset arbitrario tramite tooling. Tuttavia, per farlo, è necessario fermare tutti i consumatori — nel nostro caso, preparare una release separata in cui non ci siano consumatori, redeployments. A quel punto, potremo spostare l'offset in Kafka tramite tooling e il messaggio verrà elaborato.

Un altro aspetto — registro di replicazione vs rdkafka.so — è legato alle specificità del nostro progetto. Noi utilizziamo PHP, e in PHP, di solito, tutte le librerie comunicano con Kafka tramite il repository rdkafka.so, e successivamente c'è qualche tipo di wrapper. Potrebbe essere una nostra difficoltà personale, ma ci siamo resi conto che rileggere un pezzo già letto non è affatto semplice. In generale, ci sono stati problemi di programmazione.

Ritornando alle peculiarità del lavoro con le partizioni, è scritto chiaramente nella documentazione consumers >= topic partitions. Ma ho appreso di questo molto più tardi di quanto avrei voluto. Se desiderate scalare e avere due consumatori, è necessario avere almeno due partizioni. Cioè, se avevate una sola partizione, in cui si sono accumulati 20.000 messaggi, e avete creato una nuova partizione, il numero di messaggi si equilibrerà ugualmente solo dopo un bel po'. Quindi, per avere due consumatori paralleli, è necessario lavorare sulle partizioni.

Monitoraggio

Penso che, in base a come monitoriamo, sarà ancora più chiaro quali problemi ci siano nell'approccio attuale.

Ad esempio, contiamo quanti prodotti nello stock hanno recentemente cambiato stato e, di conseguenza, in base a queste modifiche, dovrebbero verificarsi eventi, e inviamo questo numero al nostro sistema di monitoraggio. Poi, da Kafka, riceviamo un secondo numero che indica quanti eventi sono effettivamente stati registrati. È evidente che la differenza tra questi due numeri dovrebbe sempre essere zero.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Inoltre, dobbiamo monitorare come sta andando il produttore, se l'events-bus ha ricevuto i messaggi, e come sta andando il consumatore. Ad esempio, nei grafici qui sotto, per il Refund Tool tutto va bene, mentre per BOB ci sono chiaramente problemi (picchi blu).

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Ho già menzionato il consumer-group lag. In termini semplici, si tratta di quanti messaggi non sono stati letti. In generale, i nostri consumatori lavorano rapidamente, quindi il lag è solitamente pari a 0, ma a volte può esserci un picco temporaneo. Kafka gestisce questo in modo nativo, ma è necessario impostare un intervallo.

C'è un progetto Burrow, che ti darà maggiori informazioni su Kafka. Restituisce semplicemente lo stato della consumer-group tramite API, come sta andando quel gruppo. Oltre a OK e Failed, ci sono anche warning, e potrai scoprire che i tuoi consumer non riescono a tenere il passo con la produzione — non riescono a leggere ciò che viene scritto. Il sistema è piuttosto intelligente e facile da usare.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Ecco come appare la risposta tramite API. Qui ci sono gruppo bob-live-fifa, partizione refund.update.v1, stato OK, lag 0 — l'ultimo offset finale è questo.

Esperienza nello sviluppo del servizio Refund Tool con API asincroni su Kafka

Monitoraggio updated_at SLA (stuck) l'ho già menzionato. Ad esempio, il prodotto è passato allo stato di essere pronto per il reso. Impostiamo un Cron che dice che se dopo 5 minuti questo oggetto non è passato a refund (restituiamo i soldi attraverso i sistemi di pagamento molto rapidamente), allora qualcosa sicuramente è andato storto, e questo è sicuramente un caso per il supporto. Quindi prendiamo semplicemente un Cron che legge queste cose, e se sono più di 0, invia un alert.

In sintesi, è comodo utilizzare eventi quando:

  • l'informazione è necessaria a più sistemi;
  • il risultato dell'elaborazione non è importante;
  • ci sono pochi eventi o gli eventi sono di piccole dimensioni.

A prima vista, l'articolo ha un tema molto specifico: l'API asincrono su Kafka, ma mi vengono subito in mente molte raccomandazioni da fare al riguardo.
Innanzitutto, il prossimo HighLoad++ si dovrà aspettare fino a novembre, ma già ad aprile ci sarà la sua versione a San Pietroburgo, e a giugno discuteremo di carichi elevati a Novosibirsk.
In secondo luogo, l'autore della relazione, Sergey Zaika, fa parte del Comitato Programma della nostra nuova conferenza sulla gestione della conoscenza KnowledgeConf. La conferenza è di un giorno e si terrà il 26 aprile, ma il programma è molto ricco.
E a maggio ci sarà PHP RussiaRIT++ (con DevOpsConf inclusa) – lì si può ancora proporre un proprio tema, raccontare la propria esperienza e lamentarsi delle proprie difficoltà.

Fonte: habr.com

Acquista un hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista un hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster