حالم بده، Load Averageم بالازده!

با روی کار اومدن سلسله‌ی نوسکوئلیان (NoSQLیان!)، برای حل مسائل مقیاس‌پذیری توجه‌ها به سمت این پایگاه‌داده‌های خاص منظوره جلب شد. MongoDB یکی از پرطرفدارترین پایگاه‌های ذخیره‌سازی document است که ما در برخی از پروژه‌ها از اون استفاده می‌کنیم.

قبل‌تر هم اشاره کردم که مأموریت تیم ما، تولید راهکارهای مقیاس‌پذیره و برای اطمینان از مقیاس‌پذیری [تقریبا] خطی، نرم‌افزارهایی که تولید می‌کنیم رو تحت فشار می‌گذاریم و هر بار با شکستی مواجه می‌شیم و سعی می‌کنیم از شکست‌هامون درس بگیریم. این بار دوست داشتیم تعداد سندهای موجود در پایگاه‌داده رو به نیم میلیارد برسونیم؛ هنوز ۱۰درصد راه طی نشده بود که دیدم:

[با صدای نامجو بخوانید] یک روز از خواب پا میشی میبینی Load average رفته بالا؛ هیچ processای مصرف CPUش بالا نیست، همه ریلکس و در حال صفا. چند تا درخواست دیگه هم timeout شد، ای admin بی‌نوا؛

حالم بده؟!

در لینوکس میزان مشغله‌ی سیستم با شاخصی به نام Load average بیان می‌شه. میانگین بار نشون می‌ده که چه تعداد process منتظر منابع لازم برای اجرا هستند و به همین خاطر مقدار نرمال‌شده‌اش (= load average / cores count) می‌تونه بیشتر از ۱ باشه. میانگین بار برای ۱، ۵ و ۱۵ دقیقه‌ی گذشته محاسبه می‌شه و با نگاه کردن به این سه عدد می‌تونین متوجه روند تغییرات بشین (که آیا در حال افزایشه یا در حال کاهش). با دستور uptime یا cat /proc/loadavg میانگین بار رو می‌تونین ببینین؛ فقط حواستون باشه که اعدادی که می‌بینین،‌ نرمال‌شده نیستن. برای سیستم ما با ۴ هسته، نتیجه‌ی uptime همچین چیزی بود:

$ uptime
10:32:39 up 149 days, 17:25,  1 user,  load average: 30.35, 21.91, 10.10

میانگین ۱دقیقه بیشتر از ۵ دقیقه و میانگین ۵ دقیقه بیشتر از ۱۵ دقیقه است؛ این یعنی بار داره بیشتر و بیشتر میشه! قدم بعدی این بود که بفهمیم «چرا؟».


چرا میانگین بار زیاده؟

ریشه‌ی بار سه چیز مختلف می‌تونه باشه: پردازش، حافظه و دیسک (I/O). برای شناسایی ریشه‌ی مشکل، با استفاده از ابزار top (یا htop) نگاهی به وضعیت سیستم و برنامه‌های در حال اجرا انداختیم. به نظر می‌رسید CPU زمان کمی رو صرف کارهای پردازشی می‌کنه (مجموع us و sy حدود ۳۰درصد). کمتر از نصف حافظه (RAM) هم استفاده شده بود؛ پس مشکل از کمبود حافظه و Swapping هم نباید باشه. اما نظرمون به میزان انتظار برای  I/O جلب شد: حدود ۷۰درصد! (wa = 66.7)

%Cpu(s): 26.8 us,  3.9 sy,  0.0 ni,  0.1 id, 66.7 wa,  0.0 hi,  2.6 si,  0.0 st
GiB Mem :    3.859 total,    0.106 free,    1.614 used,    2.139 buff/cache
GiB Swap:    1.998 total,    1.914 free,    0.084 used.    1.957 avail Mem

برای بررسی وضعیت دیسک‌ها و اینکه کدام دیسک زیر بار است،‌ از iostat کمک گرفتیم. اوضاع دیسکی که مشغول خدمت‌رسانی به MongoDB بود خیلی بد بود (%util = 100 و avgqu-sz = 24)

Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
sdb               0.00    16.00  234.00   45.00  3568.00   488.00    29.08    24.41  159.24  182.89   36.27   3.58 100.00

تا اینجا فهمیدیم بالا بودن Load average به دلیل استفاده‌ی زیاد پایگاه‌داده از دیسکه؛ حالا وقت این بود که بپرسیم «چرا؟».

چرا MongoDB دیسک‌خوره گرفته؟!

ما برای درخواست‌ها نمایه (index) مناسب گذاشته بودیم (و اگر اینطور نبود، در میزان استفاده از CPU یا log پایگاه‌داده خودش را نشان می‌داد)، نوع درخواست به گونه‌ای بود که با اطلاعات موجود در نمایه (و بدون نیاز به مراجعه به سند/document) می‌شد به اون جواب داد. ما می‌دونستیم که اگر کل نمایه روی حافظه جا نشه، MongoDB مجبوره به دیسک مراجعه کنه؛ ولی از طرفی انتظار نداشتیم انقدر غیرهوشمندانه باشه که منجر به چنین عملکرد بدی بشه و از طرف دیگه MongoDB حتی نصف RAM رو هم استفاده نمی‌کرد!!

برای اینکه بفهمیم ایراد کار کجاست،‌ به تنظیمات توصیه شده برای محیط عملیاتی سر زدیم و نظرمون به دو مورد جلب شد:

Turn off atime for the storage volume containing the database files.

و

Set the readahead setting to 0 regardless of storage media type (spinning, SSD, etc.).

مورد اول، atime، زمان آخرین دسترسی به فایل رو نشون می‌ده و این یعنی هر عملیات خواندن از فایل (که خیلی پرهزینه نیست) یک عملیات نوشتن تحمیل می‌کنه که پرهزینه است! البته چون برخی نرم‌افزارها به این اطلاعات نیاز دارن، نمیشه همیشه و همه‌جا غیرفعالش کرد.

مورد دوم، پیش‌خوانی (Read ahead)، کاریه که سیستم‌عامل برای بهینه‌سازی دسترسی به دیسک انجام می‌ده. وقتی شما از سیستم‌عامل تقاضا می‌کنین قسمتی از دیسک رو بخونه، سیستم‌عامل علاوه بر بخش (sector) مورد نظر شما، چند sector بعدی رو هم می‌خونه و در حافظه (به عنوان cache)‌ ذخیره می‌کنه. تا وقتی دسترسی به دیسک به صورت متوالی (sequential) باشه،‌ پیش‌خوانی باعث کاهش دسترسی‌های آتی به دیسک میشه. ما به طور معمول وقتی یک فایل رو باز می‌کنیم، به ترتیب تا انتهای اون می‌خونیم و به همین خاطر در شرایط عادی، ترجیح داده می‌شه مقدار بیشتری پیش‌خوانی صورت بگیره؛ اما دسترسی MongoDB به دیسک تصادفیه و در این شرایط پیش‌خوانی نه تنها باعث بهبود عملکرد نمی‌شه، بلکه باعث افت کارآیی می‌شه! چون هربار کلی داده‌ی به‌دردنخور پیش‌خوانی می‌شه و اغلب جواب درخواست بعدی بین این داده‌ها نیست (= افزایش IO).

اعمال تغییرات و ارزیابی

با تغییر تنظیمات mount، ثبت زمان دسترسی (atime) رو غیر فعال کردیم:

UUID=c709853e-3811-45b2-ad82-fbb1f35205b8       /var/lib/mongodb        ext4    defaults,noatime  0       0

مقدار مناسب پیش‌خوانی با حساب سرانگشتی باید حدودا برابر متوسط حجم یک سند باشه (که به اندازه‌ی لازم پیش‌خوانی کنیم و نه بیشتر) و به طور معمول حجم سندها کمتر از ۱کیلوبایته؛ احتمالاً به همین خاطر مقدار صفر در مستندات MongoDB توصیه شده. ما برای شروع ۱۶ سکتور (= ۸کیلوبایت) رو انتخاب کردیم (به طور پیش‌فرض ۲۵۶ بود) و با استفاده از دستور blockdev، مقدار پیش‌خوانی رو کاهش دادیم:

$ blockdev --setra 16 /dev/sda

و دوباره تست رو اجرا کردیم. اگر چه هنوز هم متوسط طول صف دیسک کمی بالاست، اما نسبت به حالت قبل، اوضاع به مراتب بهتر شد :)

قبل از اعمال تغییرات
قبل از اعمال تغییرات
بعد از اعمال تغییرات
بعد از اعمال تغییرات

خواندنی‌های بیشتر: