Prima di tutto, mi rendo conto che questa non è una domanda perfetta in stile domande e risposte con una risposta assoluta, ma non riesco a pensare a nessuna formulazione per farlo funzionare meglio. Non penso che ci sia una soluzione assoluta a questo e questo è uno dei motivi per cui sto pubblicando qui invece di Stack Overflow.
Nell'ultimo mese ho riscritto un pezzo piuttosto vecchio di codice server (mmorpg) per essere più moderno e più facile da estendere / mod. Ho iniziato con la parte di rete e ho implementato una libreria di terze parti (libevent) per gestire le cose per me. Con tutte le modifiche al re-factoring e al codice ho introdotto la corruzione della memoria da qualche parte e ho avuto difficoltà a scoprire dove succede.
Non riesco a riprodurlo in modo affidabile sul mio ambiente di sviluppo / test, anche quando si implementano bot primitivi per simulare un carico non si verificano più arresti anomali (ho risolto un problema di libevent che causava alcune cose)
Ho provato finora:
Valgrinding the hell out of it it - No scritture non valide fino a quando la cosa non si blocca (che potrebbe richiedere più di 1 giorno in produzione ... o solo un'ora) il che mi sta davvero sconcertando, sicuramente a un certo punto accederebbe alla memoria non valida e non sovrascriverebbe cose da opportunità? (C'è un modo per "allargare" l'intervallo di indirizzi?)
Strumenti di analisi del codice, ovvero copertura e verifica. Mentre hanno sottolineato alcuni ... cattiverie e casi limite nel codice, non c'era nulla di grave.
Registrare il processo fino a quando non si arresta in modo anomalo con gdb (tramite undodb) e quindi tornare indietro. Questo / sembra / come dovrebbe essere fattibile, ma o finisco per mandare in crash gdb usando la funzione di completamento automatico o finisco in una struttura libevent interna dove mi perdo perché ci sono troppi rami possibili (una corruzione che ne causa un'altra e così su). Immagino che sarebbe bello se potessi vedere a che cosa appartiene originariamente un puntatore / dove è stato allocato, ciò eliminerebbe la maggior parte dei problemi di ramificazione. Tuttavia, non posso eseguire valgrind con undodb, e il normale record gdb è insolitamente lento (se funziona anche in combinazione con valgrind).
Revisione del codice! Da solo (accuratamente) e avendo alcuni amici a guardare il mio codice, anche se dubito che sia stato abbastanza approfondito. Stavo pensando di assumere un dev per fare un po 'di revisione del codice / debugging con me, ma non posso permettermi di metterci troppi soldi e non saprei dove cercare qualcuno che sarebbe disposto a lavorare per poco- senza soldi se non trova il problema o qualcuno qualificato.
Dovrei anche notare: di solito ricevo backtrace coerenti. Ci sono alcuni punti in cui si verifica l'arresto anomalo, principalmente legato alla corruzione della classe socket in qualche modo corrotta. Che si tratti di un puntatore non valido che punta a qualcosa che non è un socket o la classe socket stessa viene sovrascritta (parzialmente?) Con incomprensioni. Anche se sospetto che si blocchi di più lì poiché è una delle parti maggiormente utilizzate, quindi è la prima memoria corrotta che viene utilizzata.
Tutto sommato questo problema mi ha impegnato per quasi 2 mesi (acceso e spento, più di un progetto di hobby) e mi sta davvero frustrando al punto in cui divento IRL scontroso e penso di arrendermi. Non riesco proprio a pensare a cos'altro dovrei fare per trovare il problema.
Ci sono delle tecniche utili che mi sono perso? Come lo affronti? (Non può essere così comune dal momento che non ci sono molte informazioni su questo .. o sono davvero cieco?)
Modificare:
Alcune specifiche nel caso sia importante:
Utilizzo di c ++ (11) tramite gcc 4.7 (versione fornita da debian wheezy)
La base di codice è di circa 150.000 righe
Modifica in risposta a post david.pfx: (scusate la risposta lenta)
Tieni un registro accurato degli arresti anomali, per cercare schemi?
Sì, ho ancora discariche dei recenti incidenti in giro
I pochi posti sono davvero simili? In quale modo?
Bene, nella versione più recente (sembrano cambiare ogni volta che aggiungo / rimuovo codice o cambio strutture correlate) verrebbe sempre catturato in un metodo timer elemento. Fondamentalmente un articolo ha un tempo specifico dopo il quale scade e invia informazioni aggiornate al client. Il puntatore socket non valido sarebbe nella classe Player (ancora valida per quanto posso dire), principalmente correlata a quella. Inoltre sto sperimentando un sacco di crash nella fase di cleanup, dopo il normale arresto in cui sta distruggendo tutte le classi statiche che non sono state esplicitamente distrutte ( __run_exit_handlers
nella backtrace). Per lo più coinvolgente std::map
di una classe, supponendo che sia solo la prima cosa che viene fuori però.
Che aspetto hanno i dati corrotti? Zeros? Ascii? Modelli?
Non ho ancora trovato alcun modello, mi sembra in qualche modo casuale. È difficile da dire poiché non so dove sia iniziata la corruzione.
È legato all'heap?
È interamente legato all'heap (ho abilitato la protezione dello stack di gcc e questo non ha catturato nulla).
La corruzione si verifica dopo un
free()
?
Dovrai approfondire un po 'quello. Intendi avere puntatori di oggetti già liberi in giro? Sto impostando ogni riferimento su null una volta che l'oggetto viene distrutto, quindi a meno che non mi sia perso qualcosa da qualche parte, no. Ciò dovrebbe apparire in Valgrind anche se non è così.
Esiste qualcosa di distintivo nel traffico di rete (dimensione del buffer, ciclo di recupero)?
Il traffico di rete è costituito da dati non elaborati. Quindi, array di caratteri, (u) intX_t o pacchetti impacchettati (per rimuovere il riempimento) per cose più complesse, ogni pacchetto ha un'intestazione composta da un id e dalle dimensioni del pacchetto stesso che viene convalidato rispetto alla dimensione prevista. Sono circa 10-60 byte con il più grande (pacchetto 'bootup' interno, lanciato una volta all'avvio) con una dimensione di pochi Mb.
Molte affermazioni sulla produzione. Schiantarsi presto e prevedibilmente prima che il danno si propaghi.
Una volta ho avuto un incidente legato alla std::map
corruzione, ogni entità ha una mappa della sua "vista", ogni entità che può vederlo e viceversa è in quello. Ho aggiunto un buffer da 200byte prima e dopo, riempito con 0x33 e controllato prima di ogni accesso. La corruzione è svanita magicamente, devo aver spostato qualcosa che ha reso corrotto qualcos'altro.
Registrazione strategica, in modo da sapere esattamente cosa stava succedendo poco prima. Aggiungi alla registrazione man mano che ti avvicini a una risposta.
Funziona .. fino ad un certo punto.
Nella disperazione, puoi salvare lo stato e il riavvio automatico? Posso pensare ad alcuni software di produzione che lo fanno.
Lo faccio in qualche modo. Il software consiste in un processo "cache" principale e in alcuni altri processi che accedono alla cache per ottenere e salvare elementi. Quindi per incidente non perdo molti progressi, disconnette ancora tutti gli utenti e così via, non è sicuramente una soluzione.
Concorrenza: threading, condizioni di gara, ecc
C'è un thread mysql per eseguire query "asincrone", che non è stato toccato e condivide solo le informazioni con la classe del database tramite funzioni con tutti i blocchi.
interrupt
C'è un timer di interruzione per impedirne il blocco che si interrompe se non ha completato un ciclo per 30 secondi, tuttavia quel codice dovrebbe essere sicuro:
if (!tics) {
abort();
} else
tics = 0;
il tic è volatile int tics = 0;
aumentato ogni volta che un ciclo viene completato. Anche il vecchio codice.
eventi / callback / eccezioni: stato corrotto o stack imprevedibile
Vengono utilizzati molti callback (I / O di rete asincroni, timer), ma non dovrebbero fare nulla di male.
Dati insoliti: dati di input / tempistica / stato insoliti
Ho avuto alcuni casi limite legati a questo. La disconnessione di un socket mentre i pacchetti sono ancora in fase di elaborazione ha comportato l'accesso a nullptr e simili, ma quelli sono stati facili da individuare finora poiché ogni riferimento viene ripulito subito dopo aver detto alla classe stessa che è stato fatto. (La distruzione stessa è gestita da un ciclo che elimina tutti gli oggetti distrutti ogni ciclo)
Dipendenza da un processo esterno asincrono.
Ti interessa elaborare? Questo è un po 'il caso, il processo di cache sopra menzionato. L'unica cosa che potrei immaginare dalla cima della mia testa sarebbe che non finisse abbastanza velocemente e usando i dati della spazzatura, ma non è così dal momento che sta usando anche la rete. Stesso modello di pacchetto.
/analyze
) di Microsoft e Malloc e Scribble di Apple. È inoltre necessario utilizzare il maggior numero possibile di compilatori utilizzando il maggior numero possibile di standard poiché gli avvisi del compilatore sono diagnostici e migliorano nel tempo. Non esiste un proiettile d'argento e una taglia non va bene per tutti. Più strumenti e compilatori utilizzi, più completa la copertura perché ogni strumento ha i suoi punti di forza e di debolezza.