Il file di swap mancante che ha bloccato i nostri server
Il 24 settembre, uno dei nostri server di produzione ha smesso di rispondere. Il load balancer lo ha segnato come non integro e il nostro strumento di comandi remoti non riusciva a raggiungerlo, ma nulla era andato in crash e nessun processo era stato terminato per uso eccessivo di memoria. Il server era in thrashing: senza spazio di swap e con container autorizzati a usare più memoria di quella disponibile sulla macchina, Linux passava il tempo a rimuovere e rileggere i file necessari per girare, con quasi nessun progresso. Aggiungere un file di swap da 2 GB su ogni server ha risolto il problema. La lezione più interessante riguardava i controlli di integrità.
Come appariva il «blocco»
- I controlli di integrità del load balancer andavano in timeout su quel server.
- I comandi inviati tramite AWS Systems Manager restavano in «Pending» e non venivano mai eseguiti.
- Nei log non c'era alcun kill per esaurimento di memoria. Nessun processo era morto.
Una settimana prima avevamo spostato la produzione su server ARM più piccoli con 2 GB di RAM ciascuno. Il nostro server di staging, ancora più piccolo, si era bloccato allo stesso modo durante la decompressione di un'immagine container in un deploy.
Perché nessun errore di memoria?
Linux uccide un processo solo quando esaurisce davvero la memoria recuperabile. Prima di quel punto, recupera memoria dalla page cache, cioè le copie dei file che tiene in RAM, incluso il codice di ogni programma in esecuzione.
Sui nostri server, i limiti di memoria combinati dei container sommavano circa 3,3 GB su una macchina con circa 1,8 GB utilizzabili. Normalmente non li usano tutti al massimo contemporaneamente. Quando lo facevano, il kernel continuava a rimuovere il codice dei programmi dalla cache per fare spazio, e poi doveva rileggerlo dal disco nel momento in cui quel codice tornava a girare. Quel ciclo, il thrashing, può rendere una macchina quasi del tutto non reattiva senza mai esaurire formalmente la memoria. Quindi non viene ucciso nulla, e nulla si riprende.
Senza swap, il kernel non ha dove mettere la memoria allocata ma usata raramente, quindi deve prenderla dalla cache.
La soluzione
Lo script di bootstrap del server ora crea un file di swap da 2 GB con un valore di swappiness basso (10) ogni volta che un server viene avviato o riparato. Una swappiness bassa significa che il kernel preferisce comunque tenere la memoria di lavoro in RAM, ma le pagine usate di rado possono spostarsi su disco invece di costringere il codice dei programmi fuori dalla cache. L'abbiamo applicato a caldo a entrambi i server di produzione e allo staging, e la pipeline di deploy ripubblica lo script di bootstrap a ogni deploy di produzione, così anche un server sostituito lo riceve.
La parte che ci preoccupava di più: il server non integro restava lì
Il nostro gruppo di server usava i controlli di integrità EC2, cioè «la macchina è in esecuzione?», e non i controlli di integrità del load balancer, cioè «sta rispondendo alle richieste?». Quindi un server che il load balancer aveva abbandonato risultava comunque integro per il gruppo. Non sarebbe mai stato sostituito. La produzione avrebbe girato silenziosamente su un solo server finché qualcuno non se ne fosse accorto e avesse riavviato l'altro.
Passare il gruppo ai controlli di integrità del load balancer sembra la soluzione ovvia, ma per noi non è semplice: durante un deploy i server rispondono di proposito con un errore al loro endpoint di integrità, così il load balancer li svuota (vedi come abbiamo portato i deploy a zero errori 502). Un gruppo che sostituisce i server su quel segnale li sostituirebbe nel mezzo di ogni deploy.
Quindi per ora abbiamo aggiunto una fase alla nostra checklist di deploy: dopo ogni deploy, controllare l'integrità di ogni target sul load balancer, non solo se il job di deploy è andato a buon fine. Se i comandi verso un server restano in «Pending», sta facendo thrashing: riavvialo, tanto è già fuori dalla rotazione.
Cosa ci siamo portati a casa
- «Non andare in crash» non vuol dire «sano». Il thrashing non produce errori, solo una lentezza così estrema che la macchina smette di rispondere.
- I server piccoli hanno bisogno di swap. Anche poco, con swappiness bassa, dà al kernel un posto dove mettere la memoria fredda.
- Limiti di memoria che sommano più della macchina sono una scommessa. A volte la perdi. Sappi quando l'hai fatta.
- Sappi quale controllo di integrità si fida la tua scalabilità automatica. Un controllo che chiede solo «la VM è accesa?» non sostituirà un server che ha smesso di funzionare.