فایل swap که نبود و سرورهای ما را فلج کرد
در ۲۴ سپتامبر، یکی از سرورهای production ما از پاسخدادن ایستاد. load balancer آن را ناسالم علامت زد و ابزار دستور از راه دور ما نمیتوانست به آن برسد — اما هیچ چیزی کرش نکرده بود و هیچ پروسهای بهخاطر مصرف زیاد حافظه کشته نشده بود. سرور thrash میکرد: بدون فضای swap، و با کانتینرهایی که اجازه داشتند بیشتر از حافظهی واقعی ماشین مصرف کنند، لینوکس وقتش را صرف بیرون انداختن و دوباره خواندن فایلهایی میکرد که برای اجرا لازم داشت، و تقریباً هیچ پیشرفتی نمیکرد. اضافه کردن یک فایل swap ۲ گیگابایتی روی هر سرور مشکل را حل کرد. درس جالبتر دربارهی health checkها بود.
«هنگ کردن» چه شکلی بود
- health checkهای load balancer روی آن سرور تایماوت میشدند.
- دستورهایی که از طریق AWS Systems Manager فرستاده میشدند در وضعیت «Pending» میماندند و هرگز اجرا نمیشدند.
- هیچ کشته شدن بهخاطر کمبود حافظهای در لاگها نبود. هیچ پروسهای نمرده بود.
یک هفته قبلتر production را به سرورهای کوچکتر ARM با ۲ گیگابایت RAM برده بودیم. سرور staging ما — که حتی کوچکتر بود — همینطور هنگ کرده بود، موقع باز کردن یک image کانتینر در یک استقرار.
چرا خطای حافظهای نبود؟
لینوکس فقط وقتی پروسهای را میکشد که واقعاً حافظهای را که میتواند پس بگیرد تمام کرده باشد. پیش از آن نقطه، از page cache حافظه پس میگیرد — نسخههایی از فایلها که در RAM نگه میدارد، از جمله کد هر برنامهی در حال اجرا.
روی سرورهای ما، مجموع محدودیتهای حافظهی کانتینرها حدود ۳٫۳ گیگابایت میشد، روی ماشینی با حدود ۱٫۸ گیگابایت قابل استفاده. معمولاً همهشان همزمان به سقف نمیرسیدند. وقتی رسیدند، کرنل مدام کد برنامه را از cache بیرون میانداخت تا جا باز کند، و درست لحظهای که آن کد دوباره اجرا میشد، باید دوباره آن را از دیسک میخواند. آن چرخه — thrashing — میتواند ماشینی را تقریباً کاملاً از پاسخگویی بیندازد، در حالی که از نظر فنی هرگز حافظه تمام نشده است. پس چیزی کشته نمیشود و چیزی هم بهبود پیدا نمیکند.
بدون swap، کرنل جایی برای حافظهای که تخصیص داده شده اما بهندرت استفاده میشود ندارد، پس باید آن را از cache بگیرد.
رفع
اسکریپت راهاندازی سرور حالا هر بار که سروری بوت یا تعمیر میشود یک فایل swap ۲ گیگابایتی با swappiness پایین (۱۰) میسازد. swappiness پایین یعنی کرنل همچنان ترجیح میدهد حافظهی کاری در RAM بماند، ولی صفحات کماستفاده میتوانند به دیسک بروند، بهجای اینکه کد برنامه را به زور از cache بیرون بیندازند. آن را بهصورت زنده روی هر دو سرور production و staging اعمال کردیم، و pipeline استقرار اسکریپت راهاندازی را در هر استقرار production دوباره منتشر میکند تا سروری که جایگزین میشود آن را هم بگیرد.
بخشی که بیشتر نگرانمان کرد: سرور ناسالم ماند
گروه سرورهای ما از health checkهای EC2 استفاده میکرد — «آیا ماشین روشن است؟» — نه health checkهای load balancer — «آیا به درخواستها جواب میدهد؟». پس سروری که load balancer از آن دست کشیده بود، برای گروه همچنان سالم بود. هرگز جایگزین نمیشد. production بیسروصدا روی یک سرور اجرا میشد، تا وقتی کسی متوجه شود و دومی را ریبوت کند.
تغییر گروه به health checkهای load balancer راهحل بدیهی به نظر میرسد، اما برای ما ساده نیست: وقتی در حال استقرار هستیم، سرورها عمداً endpoint سلامتشان را با خطا پاسخ میدهند تا balancer آنها را تخلیه کند (ببینید چطور استقرارها را به صفر خطای ۵۰۲ رساندیم). گروهی که روی آن سیگنال سرورها را جایگزین کند، در وسط هر استقرار آنها را جایگزین میکند.
پس فعلاً یک مرحله به چکلیست استقرار اضافه کردیم: بعد از هر استقرار، سلامت هر هدف را روی load balancer بررسی کنید، نه فقط اینکه کار استقرار موفق بوده یا نه. اگر دستورها به یک سرور در وضعیت «Pending» میمانند، آن thrash میکند — ریبوتش کنید؛ از قبل از چرخه بیرون است.
آنچه از آن برداشتیم
- «کرش نکردن» معادل «سالم بودن» نیست. thrashing هیچ خطایی تولید نمیکند، فقط کندی آنقدر شدید که ماشین پاسخ دادن را متوقف کند.
- سرورهای کوچک به swap نیاز دارند. حتی کمی، با swappiness پایین، به کرنل جایی برای گذاشتن حافظهی سرد میدهد.
- محدودیتهای حافظهای که جمعشان بیشتر از ماشین است یک شرطبندی است. گاهی میبازید. بدانید کی شرط بستهاید.
- بدانید مقیاسدهی خودکارتان به کدام health check اعتماد میکند. health checkی که فقط میپرسد «آیا VM روشن است؟» سروری را که از کار افتاده جایگزین نمیکند.