Come si riproducono le condizioni di errore e si visualizza cosa sta succedendo mentre l'applicazione viene eseguita?
Come si visualizzano le interazioni tra le diverse parti simultanee dell'applicazione?
Sulla base della mia esperienza, la risposta a questi due aspetti è la seguente:
Traccia distribuita
La traccia distribuita è una tecnologia che acquisisce i dati di temporizzazione per ogni singolo componente simultaneo del sistema e li presenta in formato grafico. Le rappresentazioni di esecuzioni simultanee sono sempre intercalate, consentendo di vedere cosa sta funzionando in parallelo e cosa no.
La traccia distribuita deve le sue origini a sistemi (ovviamente) distribuiti, che sono per definizione asincroni e altamente concorrenti. Un sistema distribuito con traccia distribuita consente alle persone di:
a) identificare importanti colli di bottiglia, b) ottenere una rappresentazione visiva delle "esecuzioni" ideali dell'applicazione e c) fornire visibilità su quale comportamento concorrente viene eseguito, d) ottenere dati temporali che possono essere utilizzati per valutare le differenze tra i cambiamenti nella propria sistema (estremamente importante se si dispone di SLA forti).
Le conseguenze della traccia distribuita sono tuttavia:
Aggiunge un overhead a tutti i processi simultanei, poiché si traduce in più codice per l'esecuzione e l'invio potenzialmente su una rete. In alcuni casi, questo sovraccarico è molto significativo: anche Google utilizza il proprio sistema di traccia Dapper su un piccolo sottoinsieme di tutte le richieste per non rovinare l'esperienza dell'utente.
Esistono molti strumenti diversi, non tutti interoperabili tra loro. Ciò è in qualche modo migliorato da standard come OpenTracing, ma non completamente risolto.
Non ti dice nulla sulle risorse condivise e sul loro stato attuale. Potresti essere in grado di indovinare, in base al codice dell'applicazione e a ciò che ti mostra il grafico che vedi, ma non è uno strumento utile in questo senso.
Gli strumenti attuali presuppongono che tu abbia memoria e spazio di archiviazione da risparmiare. L'hosting di un server di timeseries potrebbe non essere economico, a seconda dei vincoli.
Software di tracciamento errori
Mi collego a Sentry sopra principalmente perché è lo strumento più usato là fuori, e per una buona ragione - il software di tracciamento degli errori come Sentry dirotta l'esecuzione del runtime per inoltrare simultaneamente una traccia dello stack degli errori riscontrati su un server centrale.
Il vantaggio netto di tale software dedicato nel codice simultaneo:
- Gli errori duplicati non vengono duplicati . In altre parole, se uno o più sistemi simultanei riscontrano la stessa eccezione, Sentry incrementerà un rapporto sull'incidente, ma non invierà due copie dell'incidente.
Questo significa che puoi capire quale sistema simultaneo sta vivendo che tipo di errore senza dover passare attraverso innumerevoli segnalazioni di errori simultanee. Se hai mai subito spam e-mail da un sistema distribuito, sai come si sente l'inferno.
Puoi persino "taggare" diversi aspetti del tuo sistema concorrente (anche se questo presuppone che non hai un lavoro intercalato su esattamente un thread, che tecnicamente non è simultaneo poiché il thread sta semplicemente saltando tra le attività in modo efficiente ma deve comunque elaborare i gestori di eventi fino al completamento) e vedere una suddivisione degli errori per tag.
- È possibile modificare questo software di gestione degli errori per fornire ulteriori dettagli con le eccezioni di runtime. Quali risorse aperte ha avuto il processo? Esiste una risorsa condivisa che stava trattando questo processo? Quale utente ha riscontrato questo problema?
Questo, oltre alle meticolose tracce dello stack (e alle mappe di origine, se devi fornire una versione ridotta dei tuoi file), semplifica la determinazione di ciò che non va gran parte del tempo.
- (Specifico per Sentry) È possibile disporre di un dashboard di report Sentry separato per le esecuzioni di test del sistema, che consente di rilevare errori nei test.
Gli svantaggi di tale software includono:
Come tutto, aggiungono alla rinfusa. Ad esempio, potresti non voler un tale sistema su hardware incorporato. Consiglio vivamente di eseguire una versione di prova di tale software, confrontando una semplice esecuzione con e senza il campionamento di alcune centinaia di esecuzioni su una macchina inattiva.
Non tutte le lingue sono ugualmente supportate, poiché molti di questi sistemi si basano sulla cattura implicita di un'eccezione e non tutte le lingue presentano solide eccezioni. Detto questo, ci sono clienti per molti sistemi.
Possono essere sollevati come un rischio per la sicurezza, dal momento che molti di questi sistemi sono essenzialmente di tipo chiuso. In tali casi, fai la tua dovuta diligenza nella ricerca o, se preferisci, fai il tuo.
Potrebbero non fornirti sempre le informazioni di cui hai bisogno. Questo è un rischio con tutti i tentativi di aggiungere visibilità.
La maggior parte di questi servizi sono stati progettati per applicazioni Web altamente concorrenti, quindi non tutti gli strumenti possono essere perfetti per il tuo caso d'uso.
In breve : avere visibilità è la parte più cruciale di qualsiasi sistema concorrente. I due metodi che descrivo sopra, insieme a dashboard dedicati su hardware e dati per ottenere un'immagine holidtic del sistema in un dato punto temporale, sono ampiamente utilizzati in tutto il settore proprio per affrontare tale aspetto.
Alcuni suggerimenti aggiuntivi
Ho trascorso più tempo di quanto mi occupi di riparare il codice da persone che hanno cercato di risolvere problemi simultanei in modi terribili. Ogni volta, ho trovato casi in cui le seguenti cose potrebbero migliorare notevolmente l'esperienza degli sviluppatori (che è altrettanto importante dell'esperienza dell'utente):
Affidati ai tipi . La digitazione esiste per convalidare il codice e può essere utilizzata in fase di esecuzione come protezione aggiuntiva. Laddove la digitazione non esiste, fare affidamento su asserzioni e un gestore degli errori adatto per rilevare gli errori. Il codice simultaneo richiede un codice difensivo e i tipi servono come il miglior tipo di convalida disponibile.
- Verifica i collegamenti tra i componenti del codice , non solo il componente stesso. Non confonderlo con un test di integrazione completo, che verifica ogni collegamento tra ogni componente e anche in questo caso cerca solo una convalida globale dello stato finale. Questo è un modo terribile per rilevare errori.
Un buon test di collegamento verifica se, quando un componente comunica con un altro componente in modo isolato , il messaggio ricevuto e il messaggio inviato sono gli stessi che ci si aspetta. Se hai due o più componenti che fanno affidamento su un servizio condiviso per comunicare, girali tutti, invitali a scambiare messaggi tramite il servizio centrale e vedi se alla fine ottengono tutti ciò che ti aspetti.
Rompere i test che coinvolgono molti componenti in un test dei componenti stessi e un test su come comunicano anche ciascuno dei componenti ti dà maggiore fiducia nella validità del tuo codice. Avere una serie di test così rigorosi ti consente di far rispettare i contratti tra i servizi e di rilevare errori imprevisti che si verificano quando sono in esecuzione contemporaneamente.
- Utilizzare gli algoritmi giusti per convalidare lo stato dell'applicazione. Sto parlando di cose semplici, come quando hai un processo principale in attesa che tutti i suoi lavoratori finiscano un compito e desideri passare al passaggio successivo solo se tutti i lavoratori sono completamente fatti, questo è un esempio di rilevamento globale terminazione, per la quale esistono metodologie note come l'algoritmo di Safra.
Alcuni di questi strumenti sono forniti in bundle con le lingue: Rust, ad esempio, garantisce che il tuo codice non avrà condizioni di gara in fase di compilazione, mentre Go presenta un rilevatore di deadlock integrato che gira anche in fase di compilazione. Se riesci a rilevare i problemi prima che colpiscano la produzione, è sempre una vittoria.
Una regola generale: progettare per guasti nei sistemi concorrenti . Anticipare che i servizi comuni andranno in crash o si rompano. Questo vale anche per il codice che non è distribuito tra macchine: il codice simultaneo su una singola macchina può fare affidamento su dipendenze esterne (come un file di registro condiviso, un server Redis, un maledetto server MySQL) che potrebbero scomparire o essere rimosse in qualsiasi momento .
Il modo migliore per farlo è convalidare di tanto in tanto lo stato dell'applicazione - fare controlli di integrità per ciascun servizio e assicurarsi che gli utenti di quel servizio vengano informati di cattiva salute. I moderni strumenti per container come Docker lo fanno abbastanza bene e dovrebbero essere utilizzati per sandbox.
Come si fa a capire cosa può essere reso simultaneo e cosa può essere reso sequenziale?
Uno dei più grandi lezioni che ho imparato lavorando su un sistema altamente concomitante è questo: non si può mai avere abbastanza metriche . Le metriche dovrebbero guidare assolutamente tutto nella tua applicazione - non sei un ingegnere se non stai misurando tutto.
Senza metriche, non puoi fare alcune cose molto importanti:
Valuta la differenza apportata dalle modifiche al sistema. Se non sai se la manopola di sintonia A ha fatto aumentare la metrica B e la metrica C diminuisce, non sai come riparare il tuo sistema quando le persone spingono codice inaspettatamente maligno sul tuo sistema (e lo spingeranno sul tuo sistema) .
Scopri cosa devi fare dopo per migliorare le cose. Fino a quando non sai che le applicazioni stanno esaurendo la memoria, non puoi discernere se dovresti ottenere più memoria o acquistare più disco per i tuoi server.
Le metriche sono così cruciali ed essenziali che ho fatto uno sforzo consapevole per pianificare ciò che voglio misurare prima ancora di pensare a ciò che un sistema richiederà. In effetti, le metriche sono così cruciali che credo che siano la risposta giusta a questa domanda: sai solo cosa può essere reso sequenziale o simultaneo quando misuri ciò che stanno facendo i bit nel tuo programma. La progettazione corretta utilizza numeri, non congetture.
Detto questo, ci sono sicuramente alcune regole pratiche:
Sequenziale implica dipendenza. Due processi dovrebbero essere sequenziali se l'uno dipende dall'altro in qualche modo. I processi senza dipendenze dovrebbero essere simultanei. Tuttavia, pianificare un modo per gestire il fallimento del flusso che non impedisce ai processi a valle di attendere indefinitamente.
Non mescolare mai un'attività associata I / O con un'attività associata alla CPU sullo stesso core. Non (ad esempio) non scrivere un crawler Web che avvia dieci richieste simultanee nello stesso thread, non le raschia non appena arrivano e non si prevede di ridimensionarle a cinquecento: le richieste I / O vanno in coda in parallelo, ma la CPU li attraverserà comunque in serie. (Questo modello basato su eventi a thread singolo è popolare, ma è limitato a causa di questo aspetto - piuttosto che capirlo, le persone semplicemente si danno la mano e dicono che Nodo non si ridimensiona, per darvi un esempio).
Un singolo thread può eseguire molte operazioni di I / O. Ma per sfruttare appieno la concorrenza del tuo hardware, usa threadpools che insieme occupano tutti i core. Nell'esempio sopra, l'avvio di cinque processi Python (ognuno dei quali può utilizzare un core su una macchina a sei core) solo per il lavoro con la CPU e un sesto thread Python solo per il lavoro di I / O si ridimensioneranno molto più velocemente di quanto si pensi.
L'unico modo per sfruttare la concorrenza della CPU è attraverso un threadpool dedicato. Un singolo thread è spesso abbastanza buono per un sacco di lavoro di I / O associato. Questo è il motivo per cui i server Web basati su eventi come Nginx si adattano meglio (fanno semplicemente il lavoro di I / O associato) di Apache (che unisce il lavoro di I / O associato a qualcosa che richiede CPU e avvia un processo per richiesta), ma perché usare Node per eseguire decine di migliaia di calcoli GPU ricevuti in parallelo è un'idea terribile .