Fișierul de swap lipsă care ne-a blocat serverele
Pe 24 septembrie, unul dintre serverele noastre de producție a încetat să mai răspundă. Load balancer-ul l-a marcat ca nesănătos, iar instrumentul nostru de comenzi de la distanță nu a reușit să ajungă la el — dar nimic nu se prăbușise și nimic nu fusese oprit pentru că folosea prea multă memorie. Serverul era în thrashing: fără spațiu de swap și cu containere care aveau voie să folosească mai multă memorie decât avea mașina, Linux își petrecea timpul evacuând și recitind fișierele de care avea nevoie ca să ruleze, fără aproape niciun progres. Adăugarea unui fișier de swap de 2 GB pe fiecare server a rezolvat problema. Lecția mai interesantă a fost despre health check-uri.
Cum arăta „blocarea”
- Health check-urile load balancer-ului au expirat pe acel server.
- Comenzile trimise prin AWS Systems Manager au rămas în „Pending” și nu au rulat niciodată.
- Nu exista nicio oprire din cauza memoriei în loguri. Niciun proces nu murise.
Cu o săptămână mai devreme mutasem producția pe servere ARM mai mici, cu câte 2 GB RAM fiecare. Serverul nostru de staging — și mai mic — înghețase în același mod, în timp ce dezarhiva o imagine de container în timpul unui deploy.
De ce nicio eroare de memorie?
Linux oprește un proces doar când chiar rămâne fără memorie pe care o poate recupera. Înainte de acel punct, recuperează memorie din page cache — copiile fișierelor pe care le ține în RAM, inclusiv codul fiecărui program care rulează.
Pe serverele noastre, limitele de memorie ale containerelor, adunate, ajungeau la aproximativ 3,3 GB pe o mașină cu aproximativ 1,8 GB utilizabili. De obicei nu le folosesc pe toate la limită în același timp. Când o făceau, kernelul evacua continuu codul programelor din cache ca să facă loc, iar apoi trebuia să-l citească din nou de pe disc în momentul în care acel cod rula din nou. Acest ciclu — thrashing-ul — poate face o mașină aproape complet nereactivă fără să rămână tehnic fără memorie. Așadar nimic nu este oprit și nimic nu își revine.
Fără swap, kernelul nu are unde să pună memoria alocată, dar rar folosită, așa că trebuie să o ia din cache.
Remedierea
Scriptul de bootstrap al serverelor creează acum un fișier de swap de 2 GB cu o setare swappiness scăzută (10), de fiecare dată când un server pornește sau este reparat. Swappiness scăzut înseamnă că kernelul preferă în continuare să păstreze memoria de lucru în RAM, dar paginile rar atinse pot fi mutate pe disc, în loc să fie forțat codul programelor să iasă din cache. Am aplicat schimbarea în timp real pe ambele servere de producție și pe staging, iar pipeline-ul de deploy republică scriptul de bootstrap la fiecare deploy în producție, astfel încât un server înlocuit să o primească și el.
Partea care ne-a îngrijorat mai mult: serverul nesănătos a rămas
Grupul nostru de servere folosea health check-uri EC2 — „mașina rulează?” — și nu health check-uri ale load balancer-ului — „răspunde la cereri?”. Așadar, un server pe care load balancer-ul îl abandonase rămânea sănătos din perspectiva grupului. Nu avea să fie înlocuit niciodată. Producția ar fi rulat discret pe un singur server, până când cineva ar fi observat și ar fi repornit celălalt.
Trecerea grupului la health check-urile load balancer-ului pare soluția evidentă, dar la noi nu este simplă: în timpul unui deploy, serverele răspund intenționat cu o eroare la endpoint-ul de health, ca balancer-ul să le scoată din rotație (vezi cum am ajuns la zero 502 la deploy-uri). Un grup care înlocuiește servere pe baza acestui semnal ar înlocui servere chiar în mijlocul fiecărui deploy.
Deocamdată, așadar, am adăugat un pas în lista noastră de verificare a deploy-urilor: după fiecare deploy, verificăm starea fiecărei ținte pe load balancer, nu doar dacă job-ul de deploy a reușit. Dacă comenzile către un server rămân în „Pending”, serverul este în thrashing — îl repornim; oricum este deja scos din rotație.
Ce am învățat
- „Nu se prăbușește” nu înseamnă „sănătos”. Thrashing-ul nu produce nicio eroare, doar o lentoare atât de extremă încât mașina nu mai răspunde.
- Serverele mici au nevoie de swap. Chiar și puțin, cu swappiness scăzut, îi dă kernelului un loc unde să pună memoria rece.
- Limitele de memorie care, adunate, depășesc mașina sunt un pariu. Uneori pierzi pariul. Important este să știi când l-ai făcut.
- Află ce health check are încredere autoscaling-ul tău. Un health check care întreabă doar „mașina virtuală e pornită?” nu va înlocui un server care a încetat să funcționeze.