Het ontbrekende swapbestand dat onze servers liet vastlopen
Op 24 september stopte een van onze productieservers met antwoorden. De load balancer markeerde hem als ongezond en ons tool voor externe commando's kon hem niet bereiken, maar er was niets gecrasht en niets gedood omdat het te veel geheugen gebruikte. De server thrashte: zonder swapruimte en met containers die meer geheugen mochten gebruiken dan de machine had, besteedde Linux zijn tijd aan het verwijderen en opnieuw inlezen van de bestanden die het nodig had om te draaien, en boekte het nauwelijks voortgang. Een swapbestand van 2 GB op elke server loste het op. De interessantere les ging over health checks.
Hoe „vastgelopen” eruitzag
- De health checks van de load balancer liepen op die server in een time-out.
- Commando's via AWS Systems Manager bleven op „Pending” staan en werden nooit uitgevoerd.
- Er was geen out-of-memory-kill in de logs. Geen enkel proces stierf.
Een week eerder hadden we productie verhuisd naar kleinere ARM-servers met elk 2 GB RAM. Onze stagingserver, nog kleiner, was op dezelfde manier vastgelopen tijdens het uitpakken van een containerimage bij een deploy.
Waarom geen geheugenfout?
Linux doodt een proces alleen wanneer het echt geen geheugen meer heeft dat het kan terugwinnen. Voor dat punt haalt het geheugen terug uit de page cache: de kopieën van bestanden die het in RAM bewaart, inclusief de code van elk draaiend programma.
Op onze servers kwamen de gecombineerde geheugenlimieten van de containers uit op ongeveer 3,3 GB, op een machine met ongeveer 1,8 GB bruikbaar. Normaal gebruiken ze niet allemaal tegelijk hun limiet. Wanneer ze dat wel deden, bleef de kernel programmacode uit de cache verwijderen om ruimte te maken, en moest die code daarna van schijf worden teruggelezen zodra hij weer draaide. Die cyclus, thrashing, kan een machine bijna volledig onresponsief maken terwijl hij technisch nooit tekortkomt aan geheugen. Dus wordt niets gedood, en herstelt niets.
Zonder swap heeft de kernel geen plek om geheugen te leggen dat wel is toegewezen maar zelden wordt gebruikt, dus moet het dat uit de cache halen.
De oplossing
Het bootstrapscript van de server maakt nu bij elke start of reparatie van een server een swapbestand van 2 GB aan met een lage swappiness-instelling (10). Lage swappiness betekent dat de kernel werkgeheugen nog steeds liever in RAM houdt, maar zelden aangeraakte pagina's naar schijf mogen verhuizen in plaats van programmacode uit de cache te dwingen. We hebben het live toegepast op beide productieservers en op staging, en de deploypipeline publiceert het bootstrapscript bij elke productiedeploy opnieuw, zodat een vervangen server het ook krijgt.
Het deel dat ons meer zorgen baarde: de ongezonde server bleef staan
Onze servergroep gebruikte EC2-health checks, „draait de machine?”, en niet load balancer-health checks, „beantwoordt hij verzoeken?”. Dus was een server die de load balancer had opgegeven voor de groep nog steeds gezond. Hij zou nooit worden vervangen. Productie draaide stilletjes op één server tot iemand het merkte en de andere opnieuw opstartte.
De groep omzetten naar load balancer-health checks lijkt de voor de hand liggende oplossing, maar voor ons is het niet eenvoudig: tijdens een deploy laten servers bewust hun health-endpoint een fout geven zodat de balancer ze leegtrekt (zie hoe we deploys op nul 502’s kregen). Een groep die servers vervangt op dat signaal, zou ze midden in elke deploy vervangen.
Dus hebben we voorlopig een stap aan onze deploychecklist toegevoegd: na elke deploy controleren we de health van elk target op de load balancer, niet alleen of de deployjob slaagde. Als commando's naar een server op „Pending” blijven staan, thrast hij; start hem opnieuw op, hij staat al uit de rotatie.
Wat we eruit haalden
- „Niet crashen” is niet „gezond”. Thrashing levert geen fout op, alleen een traagheid die zo extreem is dat de machine stopt met antwoorden.
- Kleine servers hebben swap nodig. Zelfs een beetje, met lage swappiness, geeft de kernel een plek om koud geheugen naartoe te schrijven.
- Geheugenlimieten die samen meer zijn dan de machine, zijn een weddenschap. Soms verlies je die. Weet wanneer je hem hebt geplaatst.
- Weet welke health check je autoscaling vertrouwt. Een health check die alleen vraagt „staat de VM aan?” vervangt geen server die is gestopt met werken.