Ciao, Habr! Negli ultimi mesi abbiamo vissuto una situazione molto interessante e vorrei condividere la nostra storia di scalabilità dell'infrastruttura. In questo periodo, SberMarket ha visto un aumento degli ordini di 4 volte e ha lanciato il servizio in 17 nuove città. L'esplosione della domanda per la consegna di prodotti ci ha richiesto di scalare l'infrastruttura. Leggi di più sulle conclusioni più interessanti e utili qui sotto.

Mi chiamo Dima Bobylev, sono il direttore tecnico di SberMarket. Poiché questo è il primo post nel nostro blog, dirò qualche parola su di me e sull'azienda. Lo scorso autunno ho partecipato a un concorso per giovani leader di Runet. Per il concorso ho su come vediamo in SberMarket la cultura interna e l'approccio allo sviluppo del servizio. E anche se non sono riuscito a vincere il concorso, ho comunque formulato per me stesso i principi fondamentali dello sviluppo dell'ecosistema IT.
Gestire un team richiede di comprendere e trovare un equilibrio tra le esigenze del business e le necessità di ogni singolo sviluppatore. Attualmente, SberMarket sta crescendo di 13 volte anno per anno, e questo influisce sul prodotto, richiedendo un costante aumento delle quantità e della velocità di sviluppo. Nonostante ciò, dedichiamo tempo sufficiente agli sviluppatori per un'analisi preliminare e per una scrittura del codice di qualità. Un approccio strutturato aiuta non solo a creare un prodotto funzionante, ma anche a garantirne la scalabilità e l'evoluzione nel tempo. A seguito di questa crescita, SberMarket è diventato già un leader tra i servizi di consegna di prodotti: consegniamo quotidianamente circa 18.000 ordini, anche se all'inizio di febbraio erano circa 3.500.

Un giorno un cliente ha chiesto al corriere di SberMarket di consegnare i prodotti in modalità contactless, direttamente sul balcone.
Ma passiamo ai dettagli. Negli ultimi mesi, abbiamo lavorato attivamente all'espansione dell'infrastruttura della nostra azienda. Questa necessità è stata motivata da fattori esterni e interni. Contemporaneamente all'aumento della base clienti, il numero di negozi collegati è cresciuto da 90 all'inizio dell'anno a più di 200 a metà maggio. Abbiamo, ovviamente, preso le dovute precauzioni, riservando l'infrastruttura principale e prevedendo la possibilità di scalabilità verticale e orizzontale per tutte le macchine virtuali ospitate nel cloud di Yandex. Tuttavia, la pratica ha dimostrato: "Qualunque cosa possa andare storta andrà storta." E oggi voglio condividere le situazioni più interessanti che si sono verificate in queste settimane. Spero che la nostra esperienza sarà utile per voi.
Slave è pronto per l'azione
Già prima dell'inizio della pandemia, abbiamo assistito a un aumento del numero di richieste sui nostri server backend. La tendenza a ordinare prodotti con consegna a domicilio ha iniziato a prendere piede e con l'introduzione delle prime misure di auto-isolamento legate al COVID-19, il carico è aumentato drammaticamente nel corso della giornata. Si è presentata la necessità di scaricare rapidamente i master server del database principale e trasferire parte delle richieste di lettura sui server replica (slave).
Ci eravamo preparati in anticipo a questo passo e per questo tipo di manovra erano già stati avviati 2 server slave. Su di essi lavoravano principalmente attività batch per la generazione di feed informativi per lo scambio di dati con i partner. Questi processi creavano un carico aggiuntivo e sono stati giustamente esclusi 'dai conti' due mesi prima.
Poiché la replica avveniva sul Slave, abbiamo aderito al concetto che le applicazioni potessero interagire con essi solo in modalità di sola lettura. Il Piano di Recupero da Disastri prevedeva che, in caso di catastrofe, potessimo semplicemente montare lo Slave al posto del Master e reindirizzare tutte le richieste di scrittura e lettura allo Slave. Tuttavia, volevamo anche utilizzare le repliche per le esigenze del dipartimento analisi, quindi i server non erano completamente impostati in modalità di sola lettura, e ogni host aveva il proprio insieme di utenti, alcuni dei quali disponevano di diritti di scrittura per salvare i risultati intermedi dei calcoli.
Fino a un certo livello di carico, ci bastava il master sia per la scrittura che per la lettura nell'elaborazione delle richieste http. A metà marzo, proprio quando Sbermarket ha deciso di passare completamente al lavoro remoto, abbiamo iniziato a registrare una crescita esponenziale degli RPS. Sempre più clienti si isolavano o lavoravano da casa, il che ha influito sui nostri indicatori di carico.
Le prestazioni del "master" non erano più sufficienti, quindi abbiamo iniziato a spostare una parte delle query di lettura più pesanti su una replica. Per reindirizzare in modo trasparente le query di scrittura al master e quelle di lettura allo slave, abbiamo utilizzato la gemma ruby «». Abbiamo creato un utente speciale con il suffisso _readonly privo di diritti di scrittura. Tuttavia, a causa di un errore di configurazione su uno degli host, alcune query di scrittura sono andate su un server slave con un nome utente che aveva i diritti appropriati.
Il problema non si è manifestato subito, poiché il carico aumentato ha incrementato il ritardo degli slave. L'incoerenza dei dati è stata scoperta la mattina, quando dopo le importazioni notturne, gli slave non hanno "raggiunto" il master. Abbiamo attribuito questo alla forte pressione sul servizio stesso e agli import legati all'apertura di nuovi negozi. Ma fornire dati con un ritardo di ore era inaccettabile, quindi abbiamo trasferito i processi su un secondo slave analitico, poiché disponeva diunrisorse maggiori ed era privo di query di lettura (il che ha spiegato a noi stessi l'assenza di lag nella replica).
Dopo aver analizzato le cause dell'«espansione» del slave principale, anche l'analitico ha smesso di funzionare per la stessa ragione. Nonostante ci fossero due server aggiuntivi su cui prevedevamo di spostare il carico in caso di crash del master, un errore sfortunato ha portato a scoprire che al momento critico non ce n'era nemmeno uno.
Poiché non abbiamo creato solo un dump del database (il ripristino in quel momento richiedeva circa 5 ore), ma anche uno snapshot del server master, siamo riusciti a far partire la replica nell'arco di 2 ore. Tuttavia, dopo ciò, ci aspettava un lungo processo di applicazione del log di replicazione (poiché il processo avviene in modalità monothread, ma questa è un'altra storia).
Risultato: Dopo un incidente del genere, è diventato chiaro che dovevamo rinunciare alla pratica di limitare la scrittura per gli utenti e dichiarare l'intero server in sola lettura. Con questo approccio, non si può dubitare che le repliche saranno disponibili nei momenti critici.
L'ottimizzazione anche di una sola query pesante può «riportare in vita» il database.
Sebbene aggiorniamo costantemente il catalogo sul sito, le richieste che inviavamo ai server Slave presentavano un leggero ritardo rispetto al Master. Il tempo necessario per individuare e risolvere il problema dei Slave 'che si disconnettevano improvvisamente' superava il 'confine psicologico' (in quel lasso di tempo potevano verificarsi aggiornamenti dei prezzi, e i clienti avrebbero visto dati obsoleti), costringendoci a reindirizzare tutte le richieste al server principale del DB. Di conseguenza, il sito funzionava lentamente... ma almeno funzionava. E mentre il Slave si ripristinava, non ci restava altro da fare se non ottimizzare.
Mentre i server Slave si ripristinavano, i minuti si trascinavano lentamente, il Master rimaneva sovraccarico e abbiamo concentrato tutte le forze sull'ottimizzazione delle attività attive secondo la 'Regola di Pareto': abbiamo selezionato le richieste TOP, che generavano la maggior parte del carico, e abbiamo iniziato a effettuare il tuning. Questo veniva fatto 'in corsa'.
L'effetto interessante è stato che un MySQL completamente carico rispondere anche a un miglioramento minimo dei processi. L'ottimizzazione di un paio di query, che generavano solo il 5% del carico totale, ha già mostrato un notevole alleviamento della CPU. Di conseguenza, siamo riusciti a garantire una riserva di risorse accettabile per l'esecuzione di Master con il database e ottenere il tempo necessario per il ripristino delle repliche.
Risultato: Anche una piccola ottimizzazione consente di "sopravvivere" a un sovraccarico per diverse ore. Questo ci è stato sufficiente per il tempo di ripristino dei server con repliche. Tra l'altro, discuteremo il lato tecnico dell'ottimizzazione delle query in uno dei prossimi post. Quindi, iscrivetevi al nostro blog se potrebbe esservi utile.
Organizzate il monitoraggio della disponibilità dei servizi partner.
Gestiamo gli ordini dei clienti e, pertanto, i nostri servizi interagiscono costantemente con API esterne — queste includono gateway per l'invio di SMS, piattaforme di pagamento, sistemi di routing, geocodificatori, servizi fiscali e molte altre soluzioni. Quando il carico è aumentato rapidamente, abbiamo iniziato a incontrare i limiti delle API dei nostri partner, di cui non ci eravamo mai preoccupati prima.
Un'improvvisa violazione delle quote dei servizi partner può causare downtime per il tuo. Molti API bloccano i clienti che superano i limiti, e in alcuni casi, un eccesso di richieste può sovraccaricare l'infrastruttura del partner.
Ad esempio, durante l'aumento delle consegne, i servizi di supporto non riuscivano a gestire le attività di distribuzione e determinazione dei percorsi. Di conseguenza, è successo che gli ordini erano stati effettuati, ma il servizio che creava il percorso non funzionava. Va detto che i nostri logisti hanno fatto praticamente l'impossibile in queste condizioni e una chiara collaborazione del team ha contribuito a compensare i guasti temporanei dei servizi. Tuttavia, un volume così elevato di richieste non può essere gestito manualmente in modo costante, e dopo un po' ci saremmo trovati di fronte a una inaccettabile discrepanza tra gli ordini e la loro esecuzione.
Sono state adottate diverse misure organizzative e un lavoro coordinato del team ha permesso di guadagnare tempo, mentre ci si metteva d'accordo su nuove condizioni e si attendeva la modernizzazione dei servizi da parte di alcuni partner. Esistono anche altri API che offrono una grande resilienza e tariffe competitive in caso di alto traffico. Ad esempio, inizialmente abbiamo utilizzato un noto API cartografico per determinare l'indirizzo del punto di consegna. Ma alla fine del mese abbiamo ricevuto una fattura di quasi 2 milioni di rubli. Dopo questo, abbiamo deciso di sostituirlo rapidamente. Non farò pubblicità, ma posso dire che le nostre spese si sono notevolmente ridotte.

Risultato: È fondamentale monitorare le condizioni di lavoro di tutti i servizi partner e tenerle a mente. Anche se oggi sembrano avere 'un grande margine', non significa che domani non diventino un ostacolo alla crescita. E, naturalmente, è meglio concordare in anticipo le condizioni finanziarie per le richieste crescenti al servizio.
A volte si scopre che "" (cit) non aiuta
Ci siamo abituati ai "colli di bottiglia" nel database principale o nei server applicativi, ma durante la scalabilità possono emergere problemi dove meno ce li si aspetta. Per la ricerca testuale completa sul sito utilizziamo il motore Apache Solr. Con l'aumento del carico, abbiamo notato un calo dei tempi di risposta e il carico della CPU del server ha raggiunto il 100%. Cosa potrebbe essere più semplice: diamo più risorse al contenitore con Solr.
Invece di un aumento previsto delle prestazioni, il server semplicemente "è morto". Si caricava immediatamente al 100% e rispondeva ancora più lentamente. Inizialmente avevamo 2 core e 2 GB di RAM. Abbiamo deciso di fare ciò che di solito funziona: abbiamo dato al server 8 core e 32 GB. Tutto è peggiorato notevolmente (come e perché lo racconteremo in un post separato).
In pochi giorni abbiamo compreso le sottigliezze di questo problema e abbiamo ottenuto prestazioni ottimali con 8 core e 32 GB di RAM. Questa configurazione permette ancora oggi di continuare a incrementare il carico, il che è molto importante, perché la crescita non riguarda solo i clienti, ma anche il numero di negozi connessi: in 2 mesi il loro numero è raddoppiato.
Risultato: I metodi tradizionali come "aggiungere più hardware" non funzionano sempre. Pertanto, quando si scala qualsiasi servizio, è importante comprendere come utilizza le risorse e testare in anticipo il suo funzionamento in nuove condizioni.
Stateless — la chiave per una semplice scalabilità orizzontale
In generale, il nostro team segue un approccio ben noto: i servizi non dovrebbero avere uno stato interno (stateless) e devono essere indipendenti dall'ambiente di esecuzione. Questo ci ha permesso di affrontare l'aumento del carico con una semplice scalabilità orizzontale. Tuttavia, abbiamo avuto un servizio eccezione: il gestore di compiti di lunga durata. Si occupava dell'invio di email e sms, della gestione di eventi, della generazione di feed, dell'importazione di prezzi e stock, e dell'elaborazione delle immagini. Sfortunatamente, dipendeva da un'archiviazione locale e si trovava in un singolo esemplare.
Con l'aumento del numero di attività nella coda del gestore (che è naturalmente aumentato con la crescita degli ordini), le prestazioni dell'host su cui erano ospitati il gestore e lo storage dei file sono diventate il fattore limitante. Di conseguenza, si è fermato l'aggiornamento dell'assortimento e dei prezzi, l'invio di notifiche agli utenti e molte altre funzioni critiche, bloccate in coda. Il team Ops ha rapidamente migrato lo storage dei file in uno storage di rete simile a S3, permettendoci di attivare diverse macchine potenti per scalare il gestore delle attività di back-end.
Risultato: La regola Stateless deve essere rispettata per tutti i componenti senza eccezioni, anche se sembra 'che qui non ci troveremo mai in difficoltà'. È meglio dedicare un po' di tempo a organizzare correttamente il funzionamento di tutti i sistemi, piuttosto che poi dover riscrivere il codice e riparare un servizio sottoposto a sovraccarico in fretta.
7 principi per una crescita intensa
Nonostante la disponibilità di risorse aggiuntive, abbiamo incontrato alcuni ostacoli nel processo di crescita. In questo periodo, il numero di ordini è aumentato di oltre 4 volte. Ora consegniamo più di 17.000 ordini al giorno in 62 città e pianifichiamo di espandere ulteriormente la nostra geografia — nel primo semestre del 2020 è prevista l'introduzione del servizio in tutta la Russia. Per affrontare il crescente carico, considerando gli errori già commessi, abbiamo definito 7 principi fondamentali per lavorare in un contesto di crescita continua:
- Incident Management. Abbiamo creato una bacheca in Jira, dove ogni incidente è rappresentato come un ticket. Questo aiuterà a prioritizzare e portare a termine i compiti associati all'incidente. Infondamentalmente, non è spaventoso commettere errori — è spaventoso ripetere gli stessi errori. Per i casi in cui gli incidenti si ripetono prima che si riesca a correggere la causa, deve essere pronta un'istruzione operativa, poiché durante un grosso carico è fondamentale reagire immediatamente.
- Monitoraggio è necessario per tutti gli elementi dell'infrastruttura senza eccezione. Grazie ad esso, siamo stati in grado di prevedere l'aumento del carico e di identificare correttamente i "colli di bottiglia" da prioritizzare per la risoluzione. Probabilmente, sotto un carico elevato, tutto ciò che non ti aspetteresti si romperà o inizierà a rallentare. Pertanto, è meglio creare nuovi allerta subito dopo i primi incidenti, per monitorarli e anticiparli.
- Allerta corrette sono semplicemente necessarie in caso di aumento repentino del carico. In primo luogo, devono segnalare esattamente ciò che si è rotto. In secondo luogo, non devono essere troppi, poiché un eccesso di allerta non critiche porta all'ignorare completamente tutte le notifiche.
- Le applicazioni devono essere stateless. Ci siamo assicurati che per questa regola non ci siano eccezioni. È necessaria una totale indipendenza dal runtime. A tal fine, è possibile memorizzare i dati condivisi nel DB o, ad esempio, direttamente in S3. E ancora meglio seguire le linee guida.. Durante una crescita rapida, ottimizzare il codice diventa impossibile, e per gestire il carico si deve ricorrere all'aumento diretto delle risorse computazionali e alla scalabilità orizzontale.
- Quote e prestazioni dei servizi esterni. In caso di rapida crescita, il problema può sorgere non solo nella vostra infrastruttura, ma anche nel servizio esterno. È particolarmente frustrante quando ciò avviene non per un guasto, ma a causa del raggiungimento di quote o limiti. Pertanto, i servizi esterni devono scalare altrettanto bene quanto voi.
- Separare i processi e le code. Questo è molto utile quando uno dei gateway si intasa. Non avremmo riscontrato ritardi nei trasferimenti di dati se le code di invio SMS non avessero interferito con lo scambio di notifiche tra i sistemi informativi. Inoltre, sarebbe stato più semplice aumentare il numero di worker se avessero operato separatamente.
- Realtà finanziarie. Quando si verifica una crescita esplosiva del flusso di dati, non c'è tempo per riflettere su tariffe e abbonamenti. Ma è importante tenerne conto, soprattutto se si è una piccola azienda. Un grosso conto può arrivare sia dal proprietario di qualsiasi API sia dal vostro fornitore di hosting. Quindi, è fondamentale leggere attentamente i contratti.
Conclusione
Non senza perdite, ma abbiamo superato questa fase e oggi cerchiamo di seguire tutti i principi che abbiamo trovato. Ogni macchina ha la possibilità di aumentare facilmente le prestazioni fino a 4 volte per affrontare eventuali imprevisti.
Nei prossimi post condivideremo la nostra esperienza nell'indagine sulla diminuzione delle prestazioni in Apache Solr e parleremo di ottimizzazione delle query e di come l'interazione con l'agenzia fiscale aiuti l'azienda a risparmiare denaro. Iscriviti al nostro blog per non perdere nulla e facci sapere nei commenti se hai mai avuto problemi simili durante un aumento del traffico.

Solo gli utenti registrati possono partecipare al sondaggio. , per favore.
Hai mai subito rallentamenti o interruzioni del servizio durante un brusco aumento del carico a causa di:
55,6%Impossibilità di aggiungere rapidamente risorse computazionali
16,7%Limiti dell'infrastruttura del fornitore di hosting
33,3%Limiti delle API di terze parti
27,8%Violazioni dei principi stateless delle proprie applicazioni
88,9%Ottimizzazione del codice dei propri servizi
Hanno votato 18 utenti. 6 si sono astenuti.
Fonte: habr.com
