یه باگ کرنل که overcommit سختگیرانه پستگرس رو خراب کرد
خلاصهٔ کاملتر
نویسنده، بوراک یوجهسوی از تیم اوبیکلاود، توضیح میده که لینوکس به پردازشها اجازه میده بیشتر از حافظه فیزیکی موجود، حافظه مجازی درخواست کنن (اورکامیت). وقتی این فرض جواب نده، کرنل با اُومکیلر یه پردازش رو میکشه تا حافظه آزاد کنه. برای بیشتر برنامهها این مشکلی نداره، پردازش دوباره بالا میاد و کارشو ادامه میده.
اما پستگرس فرق میکنه. postmaster یه پردازش بکاند جدا برای هر کانکشن باز میکنه که همهشون حافظه مشترک (shared buffers، بافر WAL و جدول قفلها) رو با هم به اشتراک میذارن. اگه اُومکیلر وسط نوشتن رو یکی از این بکاندها بکشتش، حافظه مشترک ممکنه نصفهنیمه بمونه و باعث خراب شدن داده بشه. برای همین postmaster وقتی بفهمه یکی از بچههاش کشته شده، همه کانکشنهای دیگه رو هم قطع میکنه و دیتابیس میره تو ریکاوری -- یعنی یه کیل کوچیک، یه قطعی سراسری میسازه.
لینوکس سهتا سیاست overcommit داره: حالت پیشفرض (heuristic) که فقط درخواستهای خیلی بزرگ رو رد میکنه؛ حالت always که هیچوقت درخواستی رو رد نمیکنه و همهچیزو میسپاره دست اُومکیلر؛ و حالت strict که کرنل مجموع حافظه commit شده رو (Committed_AS) پیگیری میکنه و هر درخواستی که از یه سقف مشخص (CommitLimit) رد بشه رو فوراً با خطای ENOMEM رد میکنه. تیم اوبیکلاود همیشه از همین حالت strict استفاده کرده، چون بهجای کشتهشدن فاجعهبار پردازش، پستگرس فقط یه خطای معمولی به کلاینت برمیگردونه و کانکشنهای دیگه دستنخورده باقی میمونن.
چند هفته بعد از فعال کردن دوباره strict overcommit، بعضی سرورا با وجود حافظه آزاد فراوون، خطای کمبود حافظه میگرفتن. تیم رفت سراغ /proc/meminfo و دید رو یه سرور ۸ گیگی، مقدار Committed_AS چیزی حدود ۶۵۱ گیگابایت نشون میده -- در حالی که یه سرور سالم همونسایز، فقط حدود ۲.۷ گیگ رو نشون میداد.
cat /proc/meminfo | grep "Committed_AS"
Committed_AS: 683547672 kBاول فکر کردن مشکل از huge pages میاد، ولی با چک کردن پرچمهای VMA تو /proc/pid/smaps، این فرضیه رد شد و وقتی مجموع حافظهای که واقعاً باید حساب میشد رو رو کل سرور جمع زدن، فقط ۲.۴۳ گیگ دراومد -- یعنی حدود ۶۴۸ گیگ حافظه «فانتوم» تو شمارش بود. با مقایسه نسبت Committed_AS به کل حافظه رو ناوگان سرورها، تیم فهمید سرورهایی که هنوز کرنل ۶.۵ داشتن، ۵۲ برابر بیشتر از سرورهای ۶.۸ در معرض این نشتی بودن. با کمک یه مدل زبانی که تکتک کامیتهای بین نسخه ۶.۵ و ۶.۸ رو بررسی کرد، بالاخره به کامیتی رسیدن که قرارداد مقدار برگشتی do_vmi_align_munmap() رو عوض کرده بود؛ تو یه جای دیگه از کد (تابع move_vma) شرط بررسی خطا اشتباه برعکس شده بود.
if (do_vmi_munmap(&vmi, mm, old_addr, old_len, uf_unmap, false) < 0) {
if (vm_flags & VM_ACCOUNT && !(flags & MREMAP_DONTUNMAP))
vm_acct_memory(old_len >> PAGE_SHIFT);
}تو نسخه باگدار، این شرط با یه علامت تعجب (!) نوشته شده بود، یعنی برعکس حالت درستش: بهجای اینکه فقط موقع شکست unmap دوباره شارژ حافظه برگرده، این کد رو هر بار mremap موفق هم اجرا میشد و شمارنده Committed_AS مدام و بیدلیل بالا میرفت. لینوس توروالدز خودش این باگ رو با یه تغییر یکخطی درست کرد و تو توضیحش نوشت این باگ هیچ رفتار واقعی VM رو عوض نمیکرد، فقط حسابوکتاب حافظه رو بههم میریخت -- برای همین هفتهها بدون سروصدا زیر پوست کرنل مونده بود تا بالاخره از سقف CommitLimit رد شده و خطاها شروع شده بودن.
بعد از رفع باگ، تیم اوبیکلاود دوباره strict overcommit رو فعال کردن و فرمولی برای تعیین سقف commit به کار بردن: هشتاد درصد حافظه فیزیکی بهعلاوه دو گیگابایت. اون بیست درصد نگهداشتهشده برای ساختارهای داخلی کرنل مثل page table و بافرهای شبکهست، و دو گیگ اضافی هم برای پردازشهای جانبی مثل prometheus و wal-g در نظر گرفته شده که با Go نوشته شدن و حافظه مجازی رو زودتر از حافظه واقعی رزرو میکنن.
نتیجهای که نویسنده میگیره اینه که strict overcommit یه تغییر تنظیمات سادهست ولی امنیت قابلتوجهی به پستگرس میده -- کیلهای فاجعهبار رو تبدیل میکنه به خطاهای مؤدبانه تخصیص حافظه. توصیهشون اینه که قبل از فعال کردنش، مصرف حافظه واقعی سیستم رو یه مدت پایش کنید تا سقف درستی انتخاب بشه.
نکات کلیدی:
- کشته شدن یه بکاند پستگرس با اُومکیلر میتونه کل سرور رو قطع کنه چون postmaster همه کانکشنها رو میبنده
- overcommit سختگیرانه (Mode 2) بهجای کشتن پردازش، فقط با خطای ENOMEM درخواست رو رد میکنه
- یه باگ یککاراکتری تو کرنل لینوکس ۶.۵ باعث نشتی ۶۴۸ گیگابایتی تو شمارش Committed_AS شده بود
- لینوس توروالدز باگ رو با برگردوندن شرط < 0 درست کرد
- فرمول پیشنهادی اوبیکلاود برای سقف commit: ۸۰٪ حافظه فیزیکی بهعلاوه ۲ گیگابایت




