Dal 2019, in Russia è in vigore una legge sulla marcatura obbligatoria. La legge non si applica a tutti i gruppi di prodotti e le scadenze per l'entrata in vigore della marcatura obbligatoria variano a seconda dei gruppi di prodotti. I primi ad essere soggetti alla marcatura obbligatoria sono stati tabacco, calzature, medicinali; successivamente verranno inclusi anche altri prodotti, come profumi, tessuti e latte. Questa novità legislativa ha stimolato lo sviluppo di nuove soluzioni IT che consentiranno di monitorare l'intera catena di vita del prodotto, dal momento della produzione fino all'acquisto da parte del consumatore finale, a tutti i partecipanti al processo: sia lo Stato che tutte le organizzazioni che vendono prodotti con marcatura obbligatoria.
In X5, il sistema che traccerà i prodotti con marcatura e scambierà dati con lo Stato e i fornitori è stato denominato “Markus”. Spiegheremo nel dettaglio come e chi lo ha sviluppato, quale è la sua tecnologia e perché possiamo essere orgogliosi.

Un vero HighLoad
“Markus” risolve numerosi compiti, il principale dei quali è l'integrazione tra i sistemi informatici di X5 e il sistema informativo statale per il monitoraggio dei prodotti marcati (GIS MP) per seguire il movimento dei prodotti marcati. Inoltre, la piattaforma conserva tutti i codici di marcatura ricevuti e l'intera storia dei movimenti di questi codici tra gli oggetti, aiutando a eliminare eventuali errori nei prodotti marcati. Ad esempio, nel caso del tabacco, che ha fatto parte dei primi gruppi di prodotti marcati, solo un camion di sigarette contiene circa 600.000 pacchetti, ognuno dei quali ha il proprio codice unico. La nostra missione è tracciare e verificare la legalità degli spostamenti di ciascun pacchetto tra magazzini e negozi e, infine, verificare la possibilità della loro vendita al consumatore finale. Inoltre, registriamo circa 125.000 operazioni di cassa all'ora, e dobbiamo anche registrare come ogni pacchetto è arrivato nel negozio. Così, considerando tutti gli spostamenti tra gli oggetti, ci aspettiamo decine di miliardi di registrazioni all'anno.
Il Team M
Nonostante il fatto che "Markus" sia considerato un progetto all'interno di X5, viene realizzato secondo un approccio di prodotto. Il team lavora secondo Scrum. L'avvio del progetto è avvenuto nell'estate dello scorso anno, ma i primi risultati sono stati ottenuti solo in ottobre: è stato completamente formato un team interno, è stata sviluppata l'architettura del sistema e sono stati acquistati i dispositivi. Attualmente nel team ci sono 16 persone, sei delle quali si occupano dello sviluppo backend e frontend, tre dell'analisi dei sistemi. Altri sei membri si occupano di test manuali, di carico e automatizzati, oltre che della manutenzione del prodotto. Inoltre, abbiamo uno specialista SRE.
Nel nostro team, il codice è scritto non solo dagli sviluppatori; praticamente tutti i ragazzi sanno programmare e scrivono test automatici, script di carico e script di automazione. Prestiamo particolare attenzione a questo, poiché anche il supporto del prodotto richiede un alto livello di automazione. Cerchiamo sempre di offrire supporto e aiuto ai colleghi che non hanno precedentemente programmato, assegnando loro piccoli compiti.
A causa della pandemia da coronavirus, abbiamo trasferito tutto il team al lavoro remoto; la disponibilità di tutti gli strumenti per la gestione dello sviluppo, il workflow impostato in Jira e GitLab hanno permesso di superare facilmente questa fase. I mesi trascorsi in remoto hanno dimostrato che la produttività del team non è stata compromessa; per molti, il comfort lavorativo è aumentato, l'unico aspetto mancante è la comunicazione in presenza.
Incontro del team prima del lavoro remoto

Incontri durante il lavoro remoto

Stack tecnologico della soluzione
Il repository standard e lo strumento CI/CD per X5 è GitLab. Lo utilizziamo per conservare il codice, per il testing continuo e per il deployment sui server di test e di produzione. Utilizziamo anche la prassi del code review, in cui è necessario che almeno 2 colleghi approvino le modifiche apportate dal sviluppatore al codice. Gli analizzatori di codice statici SonarQube e JaCoCo ci aiutano a mantenere il codice pulito e a garantire il livello richiesto di copertura dei test unitari. Tutte le modifiche al codice devono necessariamente passare attraverso queste verifiche. Tutti gli scenari di test eseguiti manualmente vengono successivamente automatizzati.
Per completare con successo i processi aziendali con "Markus", abbiamo dovuto affrontare una serie di sfide tecnologiche, una per una.
Compito 1. Necessità di scalabilità orizzontale del sistema
Per risolvere questo problema, abbiamo scelto un approccio microservizi all'architettura. Era molto importante comprendere le aree di responsabilità dei servizi. Abbiamo cercato di suddividerli per operazioni aziendali tenendo conto delle specificità dei processi. Ad esempio, la ricezione in magazzino è un'operazione non molto comune, ma molto articolata, durante la quale è necessario ottenere il più rapidamente possibile dall'ente regolatore le informazioni sulle unità di prodotto ricevute, il cui numero in una singola fornitura può arrivare fino a 600000, verificare la possibilità di ricevere questo prodotto in magazzino e fornire tutte le informazioni necessarie al sistema di automazione del magazzino. Tuttavia, la spedizione dai magazzini ha una intensità molto maggiore, ma opera con volumi di dati più piccoli.
Tutti i servizi vengono realizzati secondo il principio stateless e anche le operazioni interne cerchiamo di suddividerle in fasi, utilizzando quelli che chiamiamo self-topics Kafka. Questo consente al microservizio di inviare messaggi a se stesso, bilanciando così il carico su operazioni più intensive dal punto di vista delle risorse e semplificando la manutenzione del prodotto, ma di questo parleremo più avanti.
Abbiamo deciso di separare in servizi distinti i moduli di interazione con i sistemi esterni. Questo ha consentito di affrontare il problema degli API dei sistemi esterni che cambiano frequentemente, praticamente senza influire sui servizi con funzionalità aziendale.

Tutti i microservizi vengono distribuiti nel cluster OpenShift, che risolve sia il problema della scalabilità di ogni microservizio, sia ci consente di non utilizzare strumenti di Service Discovery di terze parti.
Compito 2. Necessità di supportare carichi elevati e uno scambio di dati molto intenso tra i servizi della piattaforma: solo nella fase di avvio del progetto vengono eseguite circa 600 operazioni al secondo. Prevediamo un aumento di questo valore fino a 5000 opp/sec man mano che colleghiamo gli oggetti commerciali alla nostra piattaforma.
Questo problema è stato risolto implementando un cluster Kafka e praticamente abbandonando l'interazione sincrona tra i micrservizi della piattaforma. Ciò richiede un'analisi molto attenta dei requisiti di sistema, poiché non tutte le operazioni possono essere asincrone. Inoltre, non ci limitiamo a trasmettere eventi tramite il broker, ma inviamo nel messaggio tutte le informazioni aziendali necessarie. Pertanto, la dimensione del messaggio può arrivare a diverse centinaia di kilobyte. Il limite sulla dimensione dei messaggi in Kafka richiede una previsione precisa della grandezza dei messaggi, e, se necessario, li dividiamo, ma questa divisione è logica e legata alle operazioni aziendali.
Ad esempio, per i prodotti arrivati in auto, li dividiamo per scatole. Per le operazioni sincrone vengono individuati micrservizi separati e viene effettuato un accurato test di carico. L'uso di Kafka ha presentato una nuova sfida: il controllo del funzionamento del nostro servizio considerando l'integrazione con Kafka rende tutti i nostri test unitari asincroni. Per risolvere questo compito, abbiamo scritto nostri metodi utili utilizzando l'Embedded Kafka Broker. Questo non esclude la necessità di scrivere test unitari per i singoli metodi, ma i casi complessi preferiamo testarli utilizzando Kafka.
Abbiamo dedicato molta attenzione alla tracciabilità dei log, affinché i loro TraceId non venissero persi in caso di eccezioni durante l'esecuzione dei servizi o quando si lavora con i batch di Kafka. Se nel primo caso non ci sono state particolari difficoltà, nel secondo abbiamo dovuto registrare nel log tutti i TraceId con cui è arrivato il batch, e selezionare uno per proseguire la tracciabilità. Così, cercando l'originale TraceId, l'utente può facilmente scoprire quale sia stata la continuazione della tracciabilità.
Compito 3. Necessità di conservare una grande quantità di dati: oltre 1 miliardo di marchi all'anno solo per il tabacco viene gestito da X5. È necessario un accesso costante e veloce. In totale, il sistema deve elaborare circa 10 miliardi di registrazioni sulla storia del movimento dei prodotti marcati.
Per risolvere il terzo compito è stato scelto il database NoSQL MongoDB. Abbiamo allestito uno shard di 5 nodi e in ogni nodo un Replica Set di 3 server. Questo consente di scalare orizzontalmente il sistema, aggiungendo nuovi server in cluster e garantire la sua resilienza. Qui ci siamo scontrati con un altro problema: garantire la transazionalità nel cluster mongo considerando l'uso di microservizi scalabili orizzontalmente. Ad esempio, uno dei compiti del nostro sistema è identificare i tentativi di rivendita di prodotti con codici di marcatura uguali. Qui sorgono conflitti a causa di scansioni errate o di operazioni errate da parte dei cassieri. Abbiamo scoperto che tali duplicati possono verificarsi sia all'interno di un batch Kafka elaborato sia tra due batch elaborati in parallelo. Pertanto, la verifica dei duplicati tramite una query al database non ha dato risultati. Abbiamo risolto il problema per ciascun microservizio separatamente in base alla logica aziendale di quel servizio. Ad esempio, per le ricevute, abbiamo aggiunto un controllo all'interno del batch e un'elaborazione separata per la comparsa di duplicati durante l'inserimento.
Affinché il lavoro degli utenti con la cronologia delle operazioni non influenzasse la cosa più importante — il funzionamento dei nostri processi aziendali, abbiamo separato tutti i dati storici in un servizio dedicato con un proprio database, che riceve anche informazioni tramite Kafka. In questo modo, gli utenti lavorano con un servizio isolato, senza influenzare i servizi che elaborano dati sulle operazioni correnti.
Compito 4. Rielaborazione delle code e monitoraggio:
Nei sistemi distribuiti sorgono inevitabilmente problemi e errori di disponibilità di database, code e fonti di dati esterne. Nel caso di "Markus", la fonte di tali errori è l'integrazione con sistemi esterni. Era necessario trovare una soluzione che permettesse di effettuare richieste ripetute per risposte errate con un certo timeout prestabilito, senza però interrompere l'elaborazione delle richieste concluse nella coda principale. A tal fine è stata scelta la cosiddetta concezione di “retry basata su topic”. Per ogni topic principale vengono creati uno o più topic di retry, nei quali vengono indirizzati i messaggi errati e allo stesso tempo è escluso il ritardo nell'elaborazione dei messaggi dal topic principale. Schema di interazione —

Per realizzare questo schema, abbiamo avuto bisogno di integrare questa soluzione con Spring ed evitare la duplicazione del codice. In rete abbiamo trovato una soluzione simile basata su Spring BeanPostProcessor, ma ci è sembrata eccessivamente complessa. Il nostro team ha creato una soluzione più semplice che permette di inserirsi nel ciclo di creazione dei consumer di Spring e aggiungere ulteriormente i Retry Consumer. Abbiamo proposto il prototipo della nostra soluzione al team di Spring, che può essere visualizzato . Il numero di Retry Consumer e il numero di tentativi di ciascun consumer possono essere configurati tramite parametri, in base alle esigenze del processo aziendale, e, affinché tutto funzioni, è sufficiente aggiungere l'annotazione già nota a tutti gli sviluppatori Spring org.springframework.kafka.annotation.KafkaListener.
Nel caso in cui il messaggio non possa essere elaborato dopo tutti i tentativi di retry, esso viene inviato nel DLT (dead letter topic) tramite il Spring DeadLetterPublishingRecoverer. Su richiesta del supporto, abbiamo ampliato questa funzionalità e creato un servizio separato che consente di visualizzare i messaggi finiti nel DLT, stackTrace, traceId e altre informazioni utili su di essi. Inoltre, sono stati aggiunti monitoraggi e allerte per tutti i topic DLT e ora, essenzialmente, l'arrivo di un messaggio nel topic DLT è motivo di indagine e apertura di un difetto. Questo è molto conveniente: dal nome del topic capiamo subito in quale fase del processo si è verificato il problema, il che accelera notevolmente la ricerca della causa principale.

Recentemente abbiamo implementato un'interfaccia che consente di reinviare i messaggi con l'assistenza del nostro supporto, dopo che le loro cause sono state risolte (ad esempio, il ripristino della funzionalità di un sistema esterno) e, ovviamente, l'apertura del difetto corrispondente per l'analisi. In questo caso, i nostri self-topic sono stati utili, in modo da non dover riavviare una lunga catena di elaborazione, è possibile riprenderla dal passaggio desiderato.

Operazione della piattaforma
La piattaforma è già in uso produttivo, ogni giorno eseguiamo consegne e spedizioni, connettiamo nuovi centri di distribuzione e negozi. Nell'ambito del pilota, il sistema funziona con i gruppi di prodotti "Tabacco" e "Calzature".
Tutto il nostro team partecipa alla conduzione dei piloti, analizza i problemi che sorgono e propone miglioramenti al nostro prodotto, dall'ottimizzazione dei log a modifiche nei processi.
Per non ripetere gli stessi errori, tutti i casi riscontrati durante il pilota vengono riflessi nei test automatizzati. L'ampia quantità di test automatici e test unitari consente di svolgere test di regressione e di implementare hotfix in poche ore.
Attualmente continuiamo a sviluppare e migliorare la nostra piattaforma, affrontando costantemente nuove sfide. Se sei interessato, possiamo raccontarti le nostre soluzioni nei prossimi articoli.
Fonte: habr.com
