راهنمای جامع Ceph
Englishشروع کنید
در این صفحه
فصل ۲۱

تنظیم کارایی ‏Ceph‏: از ‏mClock‏ تا هسته و شبکه

۴۳ دقیقه مطالعهویرایش این صفحه
حوصله ندارم این فصل در چند خط
  • اول پایه‌ها را بررسی کنید: اشباع شبکه، دستگاه ‏block.db‏ و حالت صرفه‌جویی انرژی ‏CPU‏ بیشتر مشکلات کارایی را حل می‌کنند.
  • ‏OSD‏، ‏PG‏ها را میان ‏shard‏ها و ‏thread‏ها پخش می‌کند؛ ‏mClock‏ (زمان‌بند پیش‌فرض از ‏Quincy‏) ظرفیت را میان ‏I/O‏ کلاینت، ‏recovery‏ و ‏scrub‏ تقسیم می‌کند.
  • ‏osd_memory_target‏ (به‌طور پیش‌فرض ۴ گیگابایت) بودجه‌ی حافظه‌ی هر ‏OSD‏ است و تنظیم خودکار ‏cache‏ در ‏BlueStore‏ از آن پیروی می‌کند.
  • سیستم‌عامل، شبکه (‏MTU‏ یکسان) و ‏CPU‏ را تنظیم کنید و همیشه صدک‌های تأخیر (‏p99‏ و ‏p99.9‏) را گزارش کنید، نه فقط میانگین را.

۲۱-۱ پیش از شروع#

۲۱-۱-۱ این فصل به چه سؤال‌هایی جواب می‌دهد؟#

تنظیم کارایی در ‏Ceph‏ بیش از هر چیز یک روش است: هدف را تعریف کنید، خط پایه بگیرید، یک تغییر بدهید و دوباره بسنجید. بسیاری از «توصیه‌های تنظیم» رایج در اینترنت مربوط به ‏FileStore‏، ‏WPQ‏ یا نسخه‌های پیش از ‏Quincy‏ هستند و امروز یا بی‌اثرند یا آسیب می‌زنند. این فصل بر سازوکارهای امروزی تمرکز دارد:

  • لایه‌های کارایی کدام‌اند و از کجا شروع کنیم؟
  • ‏OSD‏ چگونه درخواست‌ها را بین شاردها و ‏thread‏ها پخش می‌کند؟
  • زمان‌بند ‏mClock‏ با چه الگوریتمی بین کلاینت و بازیابی تعادل برقرار می‌کند و پروفایل‌ها چه می‌کنند؟
  • حافظه‌ی ‏OSD‏ و کش ‏BlueStore‏ را چگونه تنظیم کنیم؟
  • تنظیمات سیستم‌عامل، شبکه و ‏CPU‏ کدام‌اند؟
  • چگونه با ‏fio‏، ‏rados bench‏ و ‏CBT‏ اندازه بگیریم؟

۲۱-۲ روش و لایه‌ها#

شکل ۲۱-۱: چرخه‌ی تنظیم کارایی: تعریف هدف قابل اندازه‌گیری، گرفتن خط پایه با ابزارهای سنجش، اعمال تنها یک تغییر، سنجش دوباره با همان بار و همان مدت، و ثبت تغییر در صورت بهبود یا بازگشت با ceph config reset در غیر این صورت
شکل ۲۱-۱: چرخه‌ی تنظیم کارایی: تعریف هدف قابل اندازه‌گیری، گرفتن خط پایه با ابزارهای سنجش، اعمال تنها یک تغییر، سنجش دوباره با همان بار و همان مدت، و ثبت تغییر در صورت بهبود یا بازگشت با ‏ceph config reset‏ در غیر این صورت
شکل ۲۱-۲: چهار لایه‌ی کارایی از پایین به بالا: سخت‌افزار و شبکه، سیستم‌عامل، تنظیمات OSD شامل حافظه و شاردها و mClock ، و در بالاترین سطح طراحی pool و تنظیمات کلاینت؛ بیشترین اثر معمولاً از لایه‌های پایین‌تر می‌آید
شکل ۲۱-۲: چهار لایه‌ی کارایی از پایین به بالا: سخت‌افزار و شبکه، سیستم‌عامل، تنظیمات ‏OSD‏ شامل حافظه و شاردها و ‏mClock‏، و در بالاترین سطح طراحی ‏pool‏ و تنظیمات کلاینت؛ بیشترین اثر معمولاً از لایه‌های پایین‌تر می‌آید
💡 نکته

پیش از هر تنظیم نرم‌افزاری، سه پرسش را پاسخ دهید: آیا شبکه‌ی ‏cluster‏ اشباع است؟ آیا دیسک‌ها (به‌ویژه ‏block.db‏) گلوگاه‌اند؟ آیا ‏CPU‏ در حالت ذخیره‌ی انرژی است؟ در تجربه‌ی عملی، بیشتر مشکلات کارایی با پاسخ به همین سه پرسش حل می‌شوند.

۲۱-۳ از درون کد: شاردها و ‏thread‏ها#

۲۱-۳-۱ مسیر درخواست در ‏OSD‏#

شکل ۲۱-۳: مسیر درخواست درون OSD: Messenger پیام را دریافت می‌کند و بر اساس hash شناسه‌ی PG یکی از شاردها انتخاب می‌شود؛ هر شارد صف mClock خود را دارد و thread های همان شارد درخواست را با گرفتن قفل PG در PrimaryLogPG::do_op اجرا و تراکنش را به BlueStore می‌سپارند
شکل ۲۱-۳: مسیر درخواست درون ‏OSD: Messenger‏ پیام را دریافت می‌کند و بر اساس ‏hash‏ شناسه‌ی ‏PG‏ یکی از شاردها انتخاب می‌شود؛ هر شارد صف ‏mClock‏ خود را دارد و ‏thread‏های همان شارد درخواست را با گرفتن قفل ‏PG‏ در ‏PrimaryLogPG::do_op‏ اجرا و تراکنش را به ‏BlueStore‏ می‌سپارند

‏OSD‏ درخواست‌ها را بین چند شارد (‏struct OSDShard‏ در ‏src/osd/OSD.h‏) پخش می‌کند. شارد هر ‏PG‏ با ‏pgid.hash_to_shard(num_shards)‏ تعیین می‌شود؛ بنابراین همه‌ی عملیات یک ‏PG‏ همیشه در یک شارد پردازش می‌شوند و ترتیب حفظ می‌شود. هر شارد صف زمان‌بندی خود و چند ‏thread‏ دارد.

گزینهپیش‌فرض از ‏v19.2.1‏ (Squid) و در ‏Tentacle‏ و ‏Umbrella‏پیش‌فرض در ‏v19.2.0‏
‏osd_op_num_shards_hdd‏۱۵
‏osd_op_num_threads_per_shard_hdd‏۵۱
‏osd_op_num_shards_ssd‏۸۸
‏osd_op_num_threads_per_shard_ssd‏۲۲
✎ یادداشت

وابسته به نسخه. مقادیر جدول از ‏src/common/options/osd.yaml.in‏ در برچسب‌های ‏v19.2.0‏ تا ‏v19.2.3‏، ‏v20.2.0‏ و ‏v21.2.0‏ خوانده شده‌اند. برای ‏HDD‏، یک شارد با پنج ‏thread‏ جایگزین پنج شارد تک‌‏thread‏ی شده است. این تغییر در نسخه‌ی نقطه‌ای ‏v19.2.1‏ به ‏Squid‏ هم رسیده است، پس فقط ‏v19.2.0‏ مقدار قدیمی را دارد. دلیل منطقی: با یک شارد، زمان‌بند ‏mClock‏ دید کامل از همه‌ی درخواست‌های آن ‏OSD‏ دارد و می‌تواند رزرو و سهم کلاینت و بازیابی را دقیق‌تر رعایت کند؛ با چند شارد، هر شارد فقط بخشی از ظرفیت را می‌بیند. این گزینه‌ها فقط با راه‌اندازی دوباره‌ی ‏OSD‏ اعمال می‌شوند.

⚠ هشدار

افزایش کورکورانه‌ی شاردها یا ‏thread‏ها روی ‏NVMe‏ سریع، اغلب فقط مصرف ‏CPU‏ و رقابت قفل را بالا می‌برد. اگر ‏CPU‏ اشباع نیست و دیسک هم نیست، گلوگاه احتمالاً جای دیگری است (شبکه، کلاینت، یا یک ‏PG‏ داغ).

۲۱-۴ از درون کد: ‏mClock‏#

۲۱-۴-۱ الگوریتم ‏dmClock‏#

از ‏Quincy‏، زمان‌بند پیش‌فرض ‏OSD‏ ‏mclock_scheduler‏ است (‏osd_op_queue‏). این زمان‌بند بر پایه‌ی کتابخانه‌ی ‏dmclock‏ (‏src/dmclock/‏) است که الگوریتم ‏mClock‏ را پیاده می‌کند. هر کلاس سرویس سه پارامتر دارد:

  • ‏reservation‏: حداقل سهم تضمین‌شده (‏IOPS‏)،
  • ‏weight‏: سهم نسبی از ظرفیت اضافه،
  • ‏limit‏: سقف سهم.

هر درخواست سه برچسب زمانی (‏R‏، ‏P‏، ‏L‏) می‌گیرد. محاسبه‌ی برچسب در ‏dmclock‏:

cpp
// from ceph/ceph, LGPL — src/dmclock/src/dmclock_server.h, RequestTag::tag_calc (excerpt)
if (0.0 == increment) {
  return extreme_is_high ? max_tag : min_tag;
} else {
  double tag_increment = increment * (uint64_t(dist_req_val) + cost);
  return std::max(time, prev + tag_increment);
}

‏increment‏ معکوس پارامتر است (۱/‏reservation‏، ۱/‏weight‏ یا ۱/‏limit‏) و ‏cost‏ هزینه‌ی درخواست. بنابراین کلاسی با ‏reservation‏ بزرگ‌تر، برچسب‌های ‏R‏ نزدیک‌تر به هم می‌گیرد و درخواست‌هایش زودتر «سررسید» می‌شوند. ‏max(time, ...)‏ تضمین می‌کند کلاسی که مدتی بی‌کار بوده، با برچسب‌های قدیمی «اعتبار انباشته» نداشته باشد.

شکل ۲۱-۴: الگوریتم dmClock در سه بخش: هر درخواست بر اساس پارامترهای کلاس خود سه برچسب R ، P و L می‌گیرد؛ زمان‌بند ابتدا در مرحله‌ی رزرو درخواستی را اجرا می‌کند که برچسب R آن سررسید شده، و در غیر این صورت میان درخواست‌هایی که از سقف خود عبور نکرده‌اند کمترین برچسب P را انتخاب می‌کند؛ درخواست‌های immediate و پراولویت مانند peering پیش از صف mClock پردازش می‌شوند
شکل ۲۱-۴: الگوریتم ‏dmClock‏ در سه بخش: هر درخواست بر اساس پارامترهای کلاس خود سه برچسب ‏R‏، ‏P‏ و ‏L‏ می‌گیرد؛ زمان‌بند ابتدا در مرحله‌ی رزرو درخواستی را اجرا می‌کند که برچسب ‏R‏ آن سررسید شده، و در غیر این صورت میان درخواست‌هایی که از سقف خود عبور نکرده‌اند کمترین برچسب ‏P‏ را انتخاب می‌کند؛ درخواست‌های ‏immediate‏ و پراولویت مانند ‏peering‏ پیش از صف ‏mClock‏ پردازش می‌شوند
text
pseudocode: انتخاب درخواست بعدی (خلاصه‌ی این کتاب از dmclock)
  now = clock()
  r = queue.min_by(R)
  if r.R <= now:            return r            # مرحله‌ی رزرو: تضمین حداقل‌ها
  eligible = [q for q in queue if q.L <= now]   # از سقف عبور نکرده‌اند
  if eligible:              return eligible.min_by(P)   # تقسیم ظرفیت اضافه بر اساس وزن
  return none               # همه به سقف رسیده‌اند → صبر تا سررسید

۲۱-۴-۲ پروفایل‌ها#

پروفایل ‏mClock‏ پارامترهای سه کلاس (‏client‏، ‏background recovery‏، ‏background best-effort‏) را به‌صورت درصدی از ظرفیت ‏OSD‏ تنظیم می‌کند. مقادیر زیر از صفحه‌ی ‏mClock Config Reference‏ مستندات ‏latest‏ است:

پروفایل‏client‏ (R / ‏W‏ / ‏L‏)‏recovery‏ (R / ‏W‏ / ‏L‏)‏best-effort‏ (R / ‏W‏ / ‏L‏)
‏balanced‏ (پیش‌فرض)۵۰٪ / ۱ / ‏MAX‏۵۰٪ / ۱ / ‏MAX‏۵٪ / ۲ / ۹۰٪
‏high_client_ops‏۶۰٪ / ۲ / ‏MAX‏۴۰٪ / ۱ / ‏MAX‏۵٪ / ۴ / ۷۰٪
‏high_recovery_ops‏۳۰٪ / ۱ / ‏MAX‏۷۰٪ / ۲ / ‏MAX‏۵٪ / ۲ / ‏MAX‏
‏custom‏دستیدستیدستی
bashReef+
ceph config get osd osd_mclock_profile
ceph config set osd osd_mclock_profile high_client_ops      # ساعات اوج
ceph config set osd osd_mclock_profile high_recovery_ops    # بازیابی سریع در شب
ceph config show osd.0 | grep -E "osd_mclock_max_capacity_iops|osd_mclock_scheduler"
✎ یادداشت

برای ظرفیت، ‏mClock‏ عدد ‏IOPS‏ هر ‏OSD‏ را لازم دارد. هنگام شروع ‏OSD‏، اگر مقدار ‏osd_mclock_max_capacity_iops_[hdd|ssd]‏ پیش‌فرض باشد و ‏osd_mclock_skip_benchmark‏ خاموش باشد، ‏OSD‏ یک سنجش داخلی اجرا می‌کند (طبق توضیح کد در ‏OSD.cc‏: نوشتن ۱۰۰ شیء ۴ مگابایتی با بلوک ۴ کیلوبایتی). اگر نتیجه بیرون از بازه‌ی آستانه‌ها باشد (برای ‏HDD‏ بین ۵۰ و ۵۰۰، برای ‏SSD‏ بین ۱۰۰۰ و ۸۰۰۰۰؛ گزینه‌های ‏osd_mclock_iops_capacity_[low_]threshold_*‏)، نتیجه کنار گذاشته و مقدار پیش‌فرض (۳۱۵ برای ‏HDD‏ و ۲۱۵۰۰ برای ‏SSD‏) به کار می‌رود.

⚠ هشدار

در پروفایل‌های داخلی، ‏mClock‏ تنظیمات بازیابی مانند ‏osd_max_backfills‏ و ‏osd_recovery_max_active‏ را خودش مدیریت می‌کند و تغییر مستقیم آن‌ها نادیده گرفته می‌شود، مگر اینکه ‏osd_mclock_override_recovery_settings‏ را فعال کنید (پیش‌فرض ‏false‏). همچنین طبق مستندات، با فعال بودن هر پروفایل ‏mClock‏ (حتی ‏custom‏)، گزینه‌های ‏sleep‏ قدیمی مانند ‏osd_recovery_sleep‏ غیرفعال (صفر) می‌شوند. این یکی از رایج‌ترین سردرگمی‌ها پس از ارتقا از نسخه‌های ‏WPQ‏ است: «‏osd_max_backfills‏ را بالا بردم ولی سرعت ‏backfill‏ تغییر نکرد.»

◆ عمیق‌تر

صف ‏immediate‏: برخی درخواست‌ها (مانند ‏peering‏ و زیرعملیات ‏EC‏ بین ‏shard‏ها) پیش از صف ‏mClock‏ و مستقل از پارامترهای آن پردازش می‌شوند. مستندات ‏latest‏ توضیح می‌دهد که زیر بار سنگین مداوم، این صف ممکن بود همیشه پر بماند و صف ‏mClock‏ هرگز نوبت نگیرد؛ در ‏pool EC‏ این به گرسنگی کامل کارهای ‏best-effort‏ مانند ‏deep-scrub‏ منجر می‌شد (‏tracker‏ شماره‌ی ۶۹۰۷۸) و سازوکار جلوگیری از گرسنگی برای آن افزوده شده است. اگر روی نسخه‌ی قدیمی‌تر هستید و ‏deep scrub‏ در ‏pool EC‏ پرکار عقب می‌افتد، این مسئله را در نظر داشته باشید.

۲۱-۵ حافظه‌ی ‏OSD‏#

‏osd_memory_target‏ (پیش‌فرض ۴ گیگابایت) هدف مصرف حافظه‌ی هر ‏OSD‏ است و ‏BlueStore‏ با ‏bluestore_cache_autotune‏ (پیش‌فرض ‏true‏) کش‌های ‏onode‏، داده و ‏RocksDB‏ را درون این بودجه تنظیم می‌کند (فصل ۱۲). در ‏cephadm‏، طبق مستندات ‏OSD‏، ‏osd_memory_target_autotune‏ به‌طور پیش‌فرض برای ‏OSD‏های جدید فعال می‌شود و ‏cephadm‏ هدف هر ‏OSD‏ را از حافظه‌ی میزبان ضرب در ‏mgr/cephadm/autotune_memory_target_ratio‏ (پیش‌فرض ۰.۷ در کد ‏cephadm‏)، منهای حافظه‌ی سایر ‏daemon‏ها، تقسیم بر تعداد ‏OSD‏ها تعیین می‌کند.

bashReef+
ceph config get osd osd_memory_target
ceph orch host ls --detail                     # حافظه‌ی میزبان‌ها
ceph config set osd/host:stor-a1 osd_memory_target 8G
ceph daemon osd.3 dump_mempools | jq '.mempool.by_pool | to_entries | sort_by(-.value.bytes) | .[0:5]'
💡 نکته

برای ‏OSD‏های ‏NVMe‏ با بار متادیتای سنگین (‏RGW‏ با اشیای کوچک، ‏CephFS‏)، حافظه‌ی بیشتر (۸ تا ۱۶ گیگابایت) اثر محسوسی دارد، چون ‏onode‏های بیشتری در کش می‌مانند. برای ‏HDD‏، معمولاً پیش‌فرض کافی است و گلوگاه خود دیسک است.

۲۱-۶ لایه‌ی پایین‌تر: حلقه‌ی کنترل حافظه و ‏throttle‏ در ‏BlueStore‏#

دو حلقه‌ی کنترل کمتر دیده‌شده بخش بزرگی از رفتار کارایی یک ‏OSD‏ را تعیین می‌کنند. اولی حافظه‌ی ‏cache‏ها را با مصرف واقعی ‏heap‏ تطبیق می‌دهد. دومی تعیین می‌کند چند تراکنش هم‌زمان می‌تواند وارد ‏BlueStore‏ شود. هر دو در کد چند خط ساده‌اند، ولی اثرشان در سطح خوشه بزرگ است.

۲۱-۶-۱ ‏osd_memory_target‏ چگونه اعمال می‌شود؟#

‏osd_memory_target‏ سقف سخت نیست، هدف یک حلقه‌ی کنترل است. ‏thread‏ حافظه‌ی ‏BlueStore‏ (‏MempoolThread‏) ابتدا مرزهای بودجه‌ی ‏cache‏ را حساب می‌کند (در ‏BlueStore.cc‏) و سپس ‏PriorityCache::Manager::tune_memory‏ در هر دور بودجه را به سمت هدف هل می‌دهد.

شکل ۲۱-۵: حلقه‌ی کنترل حافظه‌ی OSD با مقادیر پیش‌فرض. مرزهای min و max از target ، fragmentation و base محاسبه می‌شوند. tune_memory هر ثانیه بودجه را بر اساس فاصله‌ی حافظه‌ی mapped از target کم یا زیاد می‌کند. بودجه‌ی حاصل هر ۵ ثانیه بر اساس اولویت بین cache های RocksDB ، onode و داده تقسیم می‌شود
شکل ۲۱-۵: حلقه‌ی کنترل حافظه‌ی ‏OSD‏ با مقادیر پیش‌فرض. مرزهای ‏min‏ و ‏max‏ از ‏target‏، ‏fragmentation‏ و ‏base‏ محاسبه می‌شوند. ‏tune_memory‏ هر ثانیه بودجه را بر اساس فاصله‌ی حافظه‌ی ‏mapped‏ از ‏target‏ کم یا زیاد می‌کند. بودجه‌ی حاصل هر ۵ ثانیه بر اساس اولویت بین ‏cache‏های ‏RocksDB‏، ‏onode‏ و داده تقسیم می‌شود
text
pseudocode: مرزها و یک گام tune_memory (بازنویسی این کتاب)
  ltarget = (1 - expected_fragmentation) * target
  min     = cache_min
  max     = ltarget - base     if ltarget > base + min   else min
  new     = clamp(tuned, min, max)
  if mapped < target:  new += (1 - mapped/target) * (max - new)   # رشد، کندتر وقتی به هدف نزدیک است
  else:                new -= (1 - target/mapped) * (new - min)   # کوچک شدن متناسب با بیش‌مصرف
  tuned = new

مثال عددی با پیش‌فرض‌ها#

با ‏osd_memory_target=4G‏، ‏osd_memory_base=768M‏، ‏osd_memory_cache_min=128M‏ و ‏osd_memory_expected_fragmentation=0.15‏:

گامورودیمحاسبهنتیجه
مرز‏target=4096 MiB‏۰.۸۵×۴۰۹۶‏ltarget=3481.6 MiB‏
مرز‏base=768‏۳۴۸۱.۶−۷۶۸‏max=2713.6 MiB‏ و ‏min=128 MiB‏
گام ۱‏tuned=1500‏، ‏mapped=3000‏‏r=1‏−۳۰۰۰/۴۰۹۶=۰.۲۶۷۶؛ ۱۵۰۰+۰.۲۶۷۶×(۲۷۱۳.۶−۱۵۰۰)۱۸۲۴.۷ ‏MiB‏
گام ۲‏mapped=4500‏ (بیش‌مصرف)‏r=1‏−۴۰۹۶/۴۵۰۰=۰.۰۸۹۸؛ ۱۸۲۴.۷−۰.۰۸۹۸×(۱۸۲۴.۷−۱۲۸)۱۶۷۲.۴ ‏MiB‏

از این مثال سه نتیجه‌ی عملی گرفته می‌شود:

  1. ۱. ‏cache‏ هرگز تمام ‏target‏ را نمی‌گیرد. حتی در بهترین حالت، حدود ۲.۷ ‏GiB‏ از ۴ ‏GiB‏ به ‏cache‏ می‌رسد. بقیه سهم ‏base‏ (حافظه‌ی غیر ‏cache‏ مثل ‏pg log‏، اتصال‌ها و ‏buffer‏ها) و حاشیه‌ی ‏fragmentation‏ است.
  2. ۲. واکنش به بیش‌مصرف تدریجی است. با ۱۰٪ بیش‌مصرف، بودجه فقط حدود ۹٪ از فاصله‌ی تا ‏min‏ کم می‌شود. اگر چیزی بیرون از ‏cache‏ (مثلاً ‏pg log‏ بسیار بلند در ‏backfill‏) رشد کند، ‏RSS‏ می‌تواند مدتی از ‏target‏ بالاتر بماند. برای جلوگیری از ‏OOM‏، حاشیه‌ی حافظه‌ی میزبان را بر این اساس برنامه‌ریزی کنید.
  3. ۳. ‏mapped‏ از ‏tcmalloc‏ خوانده می‌شود. اگر ‏OSD‏ با ‏allocator‏ دیگری ساخته شده باشد، این حلقه عملاً کار نمی‌کند و مقدار ‏cache‏ ثابت ‏bluestore_cache_size_hdd/ssd‏ (به‌ترتیب ۱‏G‏ و ۳‏G‏) معنا پیدا می‌کند.
💡 نکته

برای دیدن حلقه در عمل: ‏ceph daemon osd.N dump_mempools‏ حافظه‌ی هر ‏mempool‏ را نشان می‌دهد (فصل ۲۳) و شمارنده‌های ‏prioritycache‏ در ‏perf dump‏ (مثل ‏target_bytes‏، ‏mapped_bytes‏ و ‏heap_bytes‏) وضعیت حلقه را. اگر ‏mapped_bytes‏ مدام بالاتر از ‏target_bytes‏ است، مشکل در ‏cache‏ نیست. دنبال مصرف‌کننده‌ی بیرون از ‏cache‏ باشید.

◆ عمیق‌تر

تقسیم بودجه بین ‏cache‏ها اولویت‌بندی‌شده است (‏PRI0‏ تا ‏PRI11‏ در ‏PriorityCache.h‏). هر ‏cache‏ در هر اولویت درخواست می‌دهد و مدیر از بالاترین اولویت شروع به تخصیص می‌کند. نسبت‌های ‏bluestore_cache_kv_ratio=0.45‏، ‏bluestore_cache_meta_ratio=0.45‏ و ‏bluestore_cache_kv_onode_ratio=0.04‏ تنها نقطه‌ی شروع‌اند. با ‏bluestore_cache_autotune=true‏، سهم واقعی بر اساس درخواست هر ‏cache‏ جابه‌جا می‌شود. معمولاً ‏onode‏های داغ و بلوک‌های ‏index‏ و ‏filter‏ مربوط به ‏RocksDB‏ جلوتر از داده‌ی کاربر قرار می‌گیرند.

۲۱-۶-۲ ‏throttle‏ در ورودی ‏BlueStore‏#

هر تراکنش (‏TransContext‏) پیش از ورود به ‏BlueStore‏ از ‏throttle_bytes‏ با سقف ‏bluestore_throttle_bytes=64M‏ «هزینه» برمی‌دارد. هزینه فقط حجم داده نیست. ‏_txc_calc_cost‏ هزینه را این‌گونه حساب می‌کند:

text
ios  = 1 + (تعداد aioهای داده)        # یکی برای کامیت kv
cost = ios × throttle_cost_per_io + bytes

مقدار ‏throttle_cost_per_io‏ برای ‏HDD‏ برابر ۶۷۰۰۰۰ و برای ‏SSD‏ برابر ۴۰۰۰ است، مگر اینکه ‏bluestore_throttle_cost_per_io‏ صریحاً تنظیم شده باشد. این عدد در واقع «معادل بایتی» هزینه‌ی یک ‏seek‏ است.

شکل ۲۱-۶: محاسبه‌ی هزینه‌ی تراکنش و ظرفیت هم‌زمانی BlueStore برای نوشتن‌های ۴ K . روی HDD حدود ۵۰ تراکنش مستقیم یا ۱۰۰ تراکنش deferred در صف جا می‌شود و روی SSD بیش از ۵۵۰۰. صف کوتاه در BlueStore باعث می‌شود درخواست‌ها بیشتر در صف mClock منتظر بمانند، جایی که زمان‌بند هنوز می‌تواند بین کلاینت و بازیابی اولویت‌بندی کند
شکل ۲۱-۶: محاسبه‌ی هزینه‌ی تراکنش و ظرفیت هم‌زمانی ‏BlueStore‏ برای نوشتن‌های ۴‏K‏. روی ‏HDD‏ حدود ۵۰ تراکنش مستقیم یا ۱۰۰ تراکنش ‏deferred‏ در صف جا می‌شود و روی ‏SSD‏ بیش از ۵۵۰۰. صف کوتاه در ‏BlueStore‏ باعث می‌شود درخواست‌ها بیشتر در صف ‏mClock‏ منتظر بمانند، جایی که زمان‌بند هنوز می‌تواند بین کلاینت و بازیابی اولویت‌بندی کند
سناریو (نوشتن ۴‏K‏)‏ios‏‏cost‏ (بایت معادل)تعداد هم‌زمان در ۶۴‏MiB‏
‏HDD‏، نوشتن مستقیم۲۲×۶۷۰۰۰۰+۴۰۹۶ = ۱,۳۴۴,۰۹۶≈ ۴۹
‏HDD‏، ‏deferred‏ (داده در ‏WAL‏)۱۶۷۰۰۰۰+۴۰۹۶ = ۶۷۴,۰۹۶≈ ۹۹
‏SSD‏، نوشتن مستقیم۲۲×۴۰۰۰+۴۰۹۶ = ۱۲,۰۹۶≈ ۵۵۴۸
⚠ هشدار

چرا ‏throttle‏ را بزرگ نکنیم؟ وسوسه‌انگیز است که برای ‏IOPS‏ بیشتر ‏bluestore_throttle_bytes‏ را چند برابر کنید. ولی هر درخواستی که از ‏throttle‏ عبور کند از کنترل ‏mClock‏ خارج شده است. یک صف عمیق در ‏BlueStore‏ یعنی درخواست کلاینتی که دیرتر رسیده باید پشت ده‌ها درخواست بازیابی منتظر بماند، بدون اینکه زمان‌بند بتواند کاری کند. پس ‏throttle‏ بزرگ‌تر لزوماً تأخیر کلاینت را بهتر نمی‌کند و ممکن است آن را در زمان بازیابی بدتر کند. روش درست، اندازه‌گیری تأخیر کلاینت در زیر بار بازیابی پیش و پس از تغییر است. اگر مقدار را تغییر دادید، آن را همراه با ‏bluestore_throttle_deferred_bytes=128M‏ و هر دو را برای همه‌ی ‏OSD‏های یک کلاس یکسان تنظیم کنید.

۲۱-۶-۳ ‏Messenger‏ و ‏thread‏#

در هر ‏daemon‏، ‏ms_async_op_threads=3‏ ‏thread‏ اجرای رویداد (‏epoll‏) برای همه‌ی اتصال‌ها کار می‌کنند. هر اتصال به یکی از آن‌ها چسبیده است. روی یک ‏OSD NVMe‏ با صدها کلاینت، این سه ‏thread‏ می‌توانند پیش از دیسک اشباع شوند. نشانه‌اش این است که در ‏top -H‏ سه ‏thread‏ با نام ‏msgr-worker-N‏ نزدیک ۱۰۰٪ ‏CPU‏ کار می‌کنند. ‏ms_tcp_nodelay=true‏ (خاموش کردن ‏Nagle‏) برای پیام‌های کوچک لازم است. ‏ms_tcp_rcvbuf=0‏ یعنی اندازه‌ی بافر دریافت به ‏autotuning‏ هسته سپرده می‌شود (‏net.ipv4.tcp_rmem‏)، که معمولاً بهتر از هر عدد ثابت است.

bashReef+
ceph daemon osd.3 perf dump | jq '.prioritycache'                 # target/mapped/heap
ceph daemon osd.3 dump_mempools | jq '.mempool.total'
ceph daemon osd.3 perf dump | jq '.bluestore | {kv_flush_lat, kv_commit_lat, kv_sync_lat, state_kv_queued_lat}'
ceph config show osd.3 bluestore_throttle_cost_per_io             # 0 ⇒ مقدار hdd/ssd استفاده می‌شود
top -H -p $(pgrep -f 'ceph-osd.*--id 3') -b -n1 | grep msgr-worker
✎ یادداشت

نام دقیق برخی شمارنده‌های ‏BlueStore‏ بین نسخه‌ها تغییر کرده است. پیش از ساختن داشبورد، کلیدها را با ‏perf schema‏ روی نسخه‌ی خودتان بررسی کنید.

۲۱-۷ تنظیم کارایی با عدد: شارد، حافظه، مسیر نوشتن و ‏dmClock‏#

بخش‌های قبل این فصل گفتند هر لایه‌ی ‏OSD‏ چه پارامتری دارد. این بخش نشان می‌دهد چهار تصمیم مهم با عدد چگونه گرفته می‌شوند: کدام شارد صف یک ‏PG‏ را نگه می‌دارد، ‏cephadm‏ به هر ‏OSD‏ چقدر حافظه می‌دهد، ‏BlueStore‏ کدام نوشتن را ‏deferred‏ می‌کند و ‏dmClock‏ در هر لحظه کدام درخواست را جلو می‌اندازد. همه‌ی ارجاع‌ها به ‏main‏ در نوامبر ۲۰۲۶ است. پیش‌فرض‌های ‏mClock‏ و شاردها در ‏Reef‏ و ‏Squid‏ تغییر کرده‌اند، پس ‏ceph config show osd.N‏ روی کلاستر خودتان معیار نهایی است. عددها با بازپیاده‌سازی پایتونی حساب شده‌اند.

۲۱-۷-۱ نگاشت ‏PG‏ به شارد صف#

∑ الگوریتم: انتخاب ‏OSDShard‏ برای یک درخواست

منبع در کد: src/osd/OSD.cc :: OSD::ShardedOpWQ::_enqueue، OSD::get_num_op_shards؛ src/osd/osd_types.h :: spg_t::hash_to_shard

پیچیدگی: ‏O‏(۱)

text
pseudocode
num_shards = osd_op_num_shards  اگر ≠ 0  وگرنه (rotational ? osd_op_num_shards_hdd = 1 : osd_op_num_shards_ssd = 8)
threads_per_shard = osd_op_num_threads_per_shard  یا  (hdd: 5 ، ssd: 2)
shard(item) = item.ordering_token.pgid.ps() mod num_shards      # همه‌ی درخواست‌های یک PG در یک شارد
هر شارد: یک صف mClock جدا و threads_per_shard thread

نتیجه‌ی مستقیم: یک ‏PG‏ هرگز از بیش از یک شارد سرویس نمی‌گیرد. روی ‏SSD‏ با پیش‌فرض ۸ × ۲، یک ‏PG‏ بسیار داغ (مثلاً شیء سرصفحه‌ی یک ‏bucket‏ یا یک ‏image‏ با بار سنگین روی چند شیء کم) حداکثر ۲ ‏thread‏ از ۱۶ ‏thread‏ را مشغول می‌کند. توزیع ‏PG‏ها میان شاردها هم یکنواخت نیست. شبیه‌سازی یک ‏OSD‏ با ۱۰۰ ‏PG‏ که شماره‌ی ‏ps‏ آن‌ها تصادفی از ‏pg_num‏ ‏۴۰۹۶ انتخاب شده، تعداد ‏PG‏ هر شارد را ۹، ۹، ۱۶، ۱۷، ۹، ۱۲، ۱۰ و ۱۸ داد. میانگین ۱۲.۵ است، اما پرترین شارد ۴۴ درصد بیشتر از میانگین بار دارد. در ۲۰۰۰ ‏OSD‏ تصادفی، نسبت پرترین شارد به میانگین در میانه ۱.۳۶ و در صدک ۹۵ حدود ۱.۶۸ بود. این یکی از دلیل‌های این است که روی ‏NVMe‏ بالا بردن تعداد ‏PG‏ هر ‏OSD‏ (و در نتیجه هموارتر شدن این توزیع) گاهی کارایی را بیشتر از تغییر تعداد ‏thread‏ بهبود می‌دهد. روی ‏HDD‏ با یک شارد این مسئله وجود ندارد: همه‌ی ‏PG‏ها در یک صف ‏mClock‏ هستند و پنج ‏thread‏ آن را می‌کشند.

شکل ۲۱-۷: سمت چپ: تعداد PG های یک OSD SSD نمونه در هر یک از هشت شارد صف، با ۱۰۰ PG و ps تصادفی. خط‌چین میانگین ۱۲.۵ است. سمت راست: توزیع نسبت پرترین شارد به میانگین در ۲۰۰۰ OSD شبیه‌سازی‌شده. حتی وقتی بار هر PG برابر است، معمولاً یک شارد ۳۰ تا ۷۰ درصد بیش از میانگین PG دارد. این عددها شبیه‌سازی هستند و فرض می‌کنند ps هر PG تصادفی و مستقل است
شکل ۲۱-۷: سمت چپ: تعداد ‏PG‏های یک ‏OSD SSD‏ نمونه در هر یک از هشت شارد صف، با ۱۰۰ ‏PG‏ و ‏ps‏ تصادفی. خط‌چین میانگین ۱۲.۵ است. سمت راست: توزیع نسبت پرترین شارد به میانگین در ۲۰۰۰ ‏OSD‏ شبیه‌سازی‌شده. حتی وقتی بار هر ‏PG‏ برابر است، معمولاً یک شارد ۳۰ تا ۷۰ درصد بیش از میانگین ‏PG‏ دارد. این عددها شبیه‌سازی هستند و فرض می‌کنند ‏ps‏ هر ‏PG‏ تصادفی و مستقل است

۲۱-۷-۲ سهم حافظه‌ی هر ‏OSD‏ در ‏cephadm‏#

∑ الگوریتم: تنظیم خودکار ‏osd_memory_target‏

منبع در کد: src/pybind/mgr/cephadm/autotune.py :: MemoryAutotuner.tune؛ src/pybind/mgr/cephadm/serve.py :: CephadmServe._autotune_host_memory

پیچیدگی: ‏O‏(D) برای ‏D daemon‏ روی میزبان؛ هر ‏autotune_interval‏ (۶۰۰ ثانیه)

text
pseudocode
total = memory_total_kb × 1024 × autotune_memory_target_ratio (0.7)
for d in daemonهای میزبان:
    mds: total −= mds_cache_memory_limit
    OSD با osd_memory_target_autotune = false: total −= osd_memory_target آن
    OSD دیگر: به فهرست tuned
    هر نوع دیگر: total −= max(min_size_by_type[type] یا 1 GiB، memory_usage فعلی)
        (mgr، mds و nvmeof: 4 GiB ؛ mon: 1 GiB ؛ crash، haproxy، keepalived: 128 MiB)
if total < 0 یا tuned خالی: کاری نکن
per = ⌊total / |tuned|⌋
تنظیم‌های per-OSD قبلی را پاک کن ؛ config set osd/host:<host> osd_memory_target per

مثال عددی. میزبانی ۲۵۶ ‏GiB‏ حافظه، یک ‏mon‏، یک ‏mgr‏، ‏crash‏، ‏node-exporter‏، یک ‏RGW‏ و ۲۰ ‏OSD‏ دارد. ‏total‏ = ۲۵۶ × ۰.۷ = ۱۷۹.۲ ‏GiB‏. فرض کنید مصرف فعلی ‏mon‏ ‏۲.۱ ‏GiB‏ و ‏RGW‏ ‏۱.۵ ‏GiB‏ باشد (عددهای مثال). ‏mgr‏ با مصرف ۳ ‏GiB‏ حداقل ۴ ‏GiB‏ حساب می‌شود، ‏crash‏ ‏۱۲۸ ‏MiB‏ و ‏node-exporter‏ (که در جدول نیست) ۱ ‏GiB‏. جمع این‌ها ۸.۷۲۵ ‏GiB‏ است و برای ‏OSD‏ها ۱۷۰.۴۷۵ ‏GiB‏ می‌ماند. هر ‏OSD‏ ‏⌊۱۷۰.۴۷۵ ‏GiB‏ / ۲۰⌋ = ۹۱۵۲۳۰۶۸۷۲ بایت، حدود ۸.۵۲ ‏GiB‏، می‌گیرد. چون مقدار با ماسک ‏osd/host:<host>‏ نوشته می‌شود، همه‌ی ‏OSD‏های یک میزبان یک عدد دارند. اگر یک ‏OSD‏ را با ‏osd_memory_target_autotune = false‏ و مقدار دستی از این محاسبه بیرون بگذارید، مقدار آن از کل کم می‌شود و بقیه بین ‏OSD‏های خودکار تقسیم می‌شود.

دو تله وجود دارد. اول، مصرف فعلی ‏daemon‏ها در محاسبه می‌آید، پس اگر ‏RGW‏ ناگهان ۱۰ ‏GiB‏ مصرف کند، ده دقیقه بعد سهم هر ‏OSD‏ حدود ۰.۴ ‏GiB‏ کم می‌شود. دوم، ‏osd_memory_target‏ یک هدف برای حافظه‌ی نگاشت‌شده‌ی ‏heap‏ است، نه سقف ‏RSS‏. توضیح خود گزینه می‌گوید ‏RSS‏ ممکن است تا حدود ۲۰ درصد بیشتر شود. سی درصدی که ‏ratio‏ کنار می‌گذارد برای همین، برای سیستم‌عامل و برای ‏page cache‏ است.

شکل ۲۱-۸: تقسیم حافظه‌ی یک میزبان ۲۵۶ گیگابایتی با MemoryAutotuner . نوار بالا کل حافظه است. ۳۰ درصد هاشورخورده برای سیستم‌عامل و سربار RSS کنار گذاشته می‌شود. نوار پایین سهم daemon های غیر OSD (با حداقل‌های جدول min_size_by_type یا مصرف فعلی، هر کدام بیشتر باشد) و سپس ۲۰ سهم برابر OSD ها را نشان می‌دهد. مصرف mon و RGW در این مثال فرضی است
شکل ۲۱-۸: تقسیم حافظه‌ی یک میزبان ۲۵۶ گیگابایتی با ‏MemoryAutotuner‏. نوار بالا کل حافظه است. ۳۰ درصد هاشورخورده برای سیستم‌عامل و سربار ‏RSS‏ کنار گذاشته می‌شود. نوار پایین سهم ‏daemon‏های غیر ‏OSD‏ (با حداقل‌های جدول ‏min_size_by_type‏ یا مصرف فعلی، هر کدام بیشتر باشد) و سپس ۲۰ سهم برابر ‏OSD‏ها را نشان می‌دهد. مصرف ‏mon‏ و ‏RGW‏ در این مثال فرضی است

۲۱-۷-۳ مسیر نوشتن ‏BlueStore: small‏، ‏big‏ و ‏deferred‏#

∑ الگوریتم: تقسیم نوشتن و تصمیم ‏deferred‏ در مسیر کلاسیک

منبع در کد: src/os/bluestore/BlueStore.cc :: BlueStore::_do_write_data، _do_write_big، _do_alloc_write

پیچیدگی: ‏O‏(تعداد ‏blob‏ها) برای هر نوشتن

text
pseudocode
# bluestore_write_v2 = false (پیش‌فرض در main)
if offset و end−1 در یک بلوک min_alloc_size باشند و length ≠ min_alloc_size: _do_write_small(کل)
else:
    head = [offset, p2roundup(offset, mas))      → _do_write_small
    tail = [p2align(end, mas), end)               → _do_write_small
    middle = بقیه                                  → _do_write_big (در صورت segment_size، تکه‌تکه)
_do_write_big: اگر l ≤ 2 × prefer_deferred_size: تلاش برای نوشتن در blobهای موجود به‌صورت deferred
_do_alloc_write برای blob تازه: اگر data_size < prefer_deferred_size ⇒ deferred (ابتدا در WAL و RocksDB، بعد روی دستگاه)
                                 وگرنه ⇒ نوشتن مستقیم روی دستگاه، سپس commit فراداده
prefer_deferred_size = bluestore_prefer_deferred_size یا hdd: 64K ، ssd: 0

مثال عددی. ‏OSD‏ روی ‏HDD‏ است، با ‏min_alloc_size‏ ‏۴ ‏KiB‏ (پیش‌فرض ‏main‏ برای ‏hdd‏ و ‏ssd‏) و ‏prefer_deferred_size‏ ‏۶۴ ‏KiB‏.

  • ۱۶ ‏KiB‏ در ‏offset‏ صفر یک شیء تازه: ‏offset‏ و ‏end‏ − ۱ در بلوک‌های ۰ و ۳ هستند، پس ‏head‏ و ‏tail‏ صفرند و کل ۱۶ ‏KiB‏ به مسیر ‏big‏ می‌رود. ‏blob‏ تازه است و ۱۶ ‏KiB‏ < ۶۴ ‏KiB‏، پس ‏deferred‏ نوشته می‌شود: داده ابتدا در تراکنش ‏RocksDB‏ (روی دستگاه ‏DB/WAL‏ اگر جدا باشد) می‌نشیند و بعد روی ‏HDD‏ اعمال می‌شود.
  • ۶۴ ‏KiB‏ در ‏offset‏ صفر: ۶۴ ‏KiB‏ < ۶۴ ‏KiB‏ برقرار نیست، پس مستقیم روی ‏HDD‏ نوشته می‌شود و فقط فراداده به ‏RocksDB‏ می‌رود.
  • ۷۰ ‏KiB‏ در ‏offset‏ ‏۶ ‏KiB‏: ‏head‏ ‏۲ ‏KiB‏ (۶ تا ۸ ‏KiB‏) به مسیر ‏small‏، و ‏middle‏ ‏۶۸ ‏KiB‏ (۸ تا ۷۶ ‏KiB‏) به ‏big‏ و مستقیم می‌رود. چون ‏end‏ = ۷۶ ‏KiB‏ هم‌تراز است، ‏tail‏ صفر است.
  • ۱ ‏KiB‏ در ‏offset‏ ‏۱۰۰: درون یک بلوک است و به مسیر ‏small‏ می‌رود.
  • ۴ ‏KiB‏ هم‌تراز در ‏offset‏ ‏۸ ‏KiB‏: درون یک بلوک است، اما چون ‏length‏ = ‏min_alloc_size‏ است، به شاخه‌ی ‏else‏ و مسیر ‏big‏ می‌رود و ‏deferred‏ می‌شود.

روی ‏SSD‏ با ‏prefer_deferred_size‏ ‏۰، شرط ‏data_size < 0‏ هرگز برقرار نیست و هیچ نوشتن ‏big deferred‏ نمی‌شود. ‏HDD‏ نمی‌تواند نوشتن‌های کوچک تصادفی را سریع انجام دهد، پس ‏BlueStore‏ آن‌ها را ابتدا ترتیبی در ‏WAL‏ می‌نویسد و بعد در پس‌زمینه اعمال می‌کند. هزینه‌ی این کار دو بار نوشتن است. اگر ‏DB/WAL‏ روی ‏NVMe‏ باشد، این دو بار نوشتن عملاً تأخیر نوشتن کوچک را به تأخیر ‏NVMe‏ نزدیک می‌کند. مسیر ‏small‏ خودش منطق جداگانه‌ای (خواندن-تغییر-نوشتن و ‏deferred‏ روی ‏blob‏ موجود) دارد که در فصل‌های ۱۲ و ۲۴ آمده است. مسیر جدید ‏bluestore_write_v2‏ در ‏main‏ وجود دارد، اما پیش‌فرض آن ‏false‏ است و این مثال‌ها را پوشش نمی‌دهد.

شکل ۲۱-۹: پنج نوشتن نمونه روی یک OSD HDD و این‌که _ do_write_data هر کدام را چگونه تقسیم می‌کند. شبکه‌ی خاکستری بلوک‌های ۴ KiB است. زرد بخشی است که به مسیر small می‌رود. قرمز بخشی از مسیر big است که چون کمتر از ۶۴ KiB است deferred می‌شود. آبی نوشتن مستقیم است. سطر آخر حالت مرزی ۴ KiB هم‌تراز را نشان می‌دهد که با وجود قرار گرفتن در یک بلوک، به‌خاطر شرط length ≠ min_alloc_size به مسیر big می‌رود
شکل ۲۱-۹: پنج نوشتن نمونه روی یک ‏OSD HDD‏ و این‌که _‏do_write_data‏ هر کدام را چگونه تقسیم می‌کند. شبکه‌ی خاکستری بلوک‌های ۴ ‏KiB‏ است. زرد بخشی است که به مسیر ‏small‏ می‌رود. قرمز بخشی از مسیر ‏big‏ است که چون کمتر از ۶۴ ‏KiB‏ است ‏deferred‏ می‌شود. آبی نوشتن مستقیم است. سطر آخر حالت مرزی ۴ ‏KiB‏ هم‌تراز را نشان می‌دهد که با وجود قرار گرفتن در یک بلوک، به‌خاطر شرط ‏length‏ ≠ ‏min_alloc_size‏ به مسیر ‏big‏ می‌رود

۲۱-۷-۴ ‏dmClock‏ با عدد#

∑ الگوریتم: برچسب‌گذاری و انتخاب درخواست در ‏dmClock‏

منبع در کد: src/dmclock/src/dmclock_server.h :: RequestTag::RequestTag، tag_calc، PriorityQueueBase::do_next_request، reduce_reservation_tags؛ src/osd/scheduler/mClockScheduler.h

پیچیدگی: ‏O‏(log C) برای ‏C‏ کلاینت (سه ‏heap‏)

text
pseudocode
tag_calc(t, prev, inv, extreme_high): inv == 0 ? (high ? max_tag : min_tag) : max(t, prev + inv × cost)
ورود یا رسیدن به سر صف (IsDelayed = true در OSD):
    R = tag_calc(t, R_prev, 1/res, high) ; P = tag_calc(t, P_prev, 1/wgt, high) ; L = tag_calc(t, L_prev, 1/lim, low)
do_next_request(now):
    if کمینه‌ی R ≤ now: سرویس از فاز reservation
    else: هر سری با L ≤ now را ready کن ; اگر ready با P کمینه هست: سرویس از فاز weight
          و R سر صف همان کلاینت را 1/res × cost کم کن      # سرویس weight جزو reservation حساب نشود
    else (AtLimit::Wait در OSD): صبر تا کمینه‌ی R یا L بعدی

در ‏OSD‏، ‏cost‏ هر درخواست ‏max(اندازه به بایت، osd_bandwidth_cost_per_io)‏ است و رزرو و حد برحسب بایت در ثانیه بیان می‌شوند (فصل ۱۰). برای روشن شدن منطق، در این مثال ‏cost‏ هر درخواست را ۱ و واحدها را درخواست در ثانیه گرفته‌ایم.

مثال عددی. سه کلاینت پیوسته درخواست می‌فرستند: ‏client‏ با ‏res‏ = ۳۰۰، ‏wgt‏ = ۱ و بدون حد؛ ‏recovery‏ با ‏res‏ = ۱۰۰، ‏wgt‏ = ۱ و ‏lim‏ = ۲۰۰؛ ‏scrub‏ با ‏res‏ = ۰ و ‏wgt‏ = ۲. شبیه‌سازی ما همین الگوریتم را با بازه‌های ۱ میلی‌ثانیه‌ای اجرا کرد.

  • ظرفیت ۱۰۰۰ درخواست در ثانیه: ‏client‏ ‏۳۰۰ درخواست در ثانیه گرفت که همه در فاز ‏reservation‏ بود. ‏recovery‏ ‏۲۰۰ گرفت: ۱۰۰ از ‏reservation‏ و ۱۰۰ از ‏weight‏، و حد ۲۰۰ آن را متوقف کرد. ‏scrub‏ بقیه، یعنی ۵۰۰، را در فاز ‏weight‏ گرفت. چرا ‏client‏ از فاز ‏weight‏ سهمی نگرفت؟ برچسب ‏P‏ هر کلاینت با هر درخواست سرویس‌شده جلو می‌رود، حتی در فاز ‏reservation. 300‏ درخواست ‏client‏ برچسب ‏P‏ آن را ۳۰۰ ثانیه جلو برده است، در حالی که ‏scrub‏ با وزن ۲ هر درخواست فقط ۰.۵ جلو می‌رود. سهم وزنی ‏client‏ از ۸۰۰ درخواستی که برای ‏client‏ و ‏scrub‏ می‌ماند ‏۸۰۰ / ۳ ≈ ۲۶۷ است و از رزرو ۳۰۰ کمتر است. پس ‏client‏ همان ۳۰۰ را می‌گیرد. این همان رفتار مورد انتظار ‏mClock‏ است: هر کلاینت تقریباً ‏max‏(رزرو، سهم وزنی) را می‌گیرد.
  • ظرفیت ۳۵۰ درخواست در ثانیه (کمتر از ۴۰۰ درخواست رزروشده): ‏client‏ حدود ۲۶۳ و ‏recovery‏ حدود ۸۸ گرفت، یعنی تقریباً نسبت ۳ به ۱ رزروها. ‏scrub‏ تقریباً هیچ نگرفت. اگر ظرفیت واقعی ‏OSD‏ از مجموع رزروها کمتر باشد، رزروها به نسبت کوچک می‌شوند و کلاینت‌های بدون رزرو گرسنه می‌مانند. به همین دلیل ‏osd_mclock_max_capacity_iops_*‏ اگر بیش از واقعیت تخمین زده شده باشد (فصل ۱۰)، تضمین‌های پروفایل را بی‌معنا می‌کند.
شکل ۲۱-۱۰: خروجی شبیه‌سازی dmClock برای سه کلاینت با رزرو، وزن و حد متفاوت. سمت چپ، ظرفیت ۱۰۰۰ درخواست در ثانیه: آبی سرویس در فاز reservation و نارنجی سرویس در فاز weight است. client فقط رزرو خود را می‌گیرد، recovery به حد ۲۰۰ می‌رسد و scrub بقیه را می‌گیرد. سمت راست، ظرفیت ۳۵۰ درخواست در ثانیه که از مجموع رزروها کمتر است: رزروها به نسبت ۳ به ۱ کوچک می‌شوند و scrub تقریباً صفر می‌گیرد. cost هر درخواست ۱ فرض شده است
شکل ۲۱-۱۰: خروجی شبیه‌سازی ‏dmClock‏ برای سه کلاینت با رزرو، وزن و حد متفاوت. سمت چپ، ظرفیت ۱۰۰۰ درخواست در ثانیه: آبی سرویس در فاز ‏reservation‏ و نارنجی سرویس در فاز ‏weight‏ است. ‏client‏ فقط رزرو خود را می‌گیرد، ‏recovery‏ به حد ۲۰۰ می‌رسد و ‏scrub‏ بقیه را می‌گیرد. سمت راست، ظرفیت ۳۵۰ درخواست در ثانیه که از مجموع رزروها کمتر است: رزروها به نسبت ۳ به ۱ کوچک می‌شوند و ‏scrub‏ تقریباً صفر می‌گیرد. ‏cost‏ هر درخواست ۱ فرض شده است
شکل ۲۱-۱۱: منطق دو مرحله‌ای dmClock . هنگام رسیدن هر درخواست به سر صف کلاینت، سه برچسب R ، P و L از برچسب قبلی همان کلاینت و زمان رسیدن ساخته می‌شوند. در هر فراخوانی do_next_request ، ابتدا رزروهای سررسیدشده سرویس می‌گیرند. اگر رزروی سررسید نشده بود، از میان کلاینت‌هایی که از حدشان عبور نکرده‌اند، کمینه‌ی P انتخاب می‌شود و برچسب R آن کلاینت به عقب برمی‌گردد. OSD با AtLimit::Wait ساخته می‌شود، یعنی اگر همه به حد رسیده باشند، هیچ درخواستی بیرون نمی‌آید
شکل ۲۱-۱۱: منطق دو مرحله‌ای ‏dmClock‏. هنگام رسیدن هر درخواست به سر صف کلاینت، سه برچسب ‏R‏، ‏P‏ و ‏L‏ از برچسب قبلی همان کلاینت و زمان رسیدن ساخته می‌شوند. در هر فراخوانی ‏do_next_request‏، ابتدا رزروهای سررسیدشده سرویس می‌گیرند. اگر رزروی سررسید نشده بود، از میان کلاینت‌هایی که از حدشان عبور نکرده‌اند، کمینه‌ی ‏P‏ انتخاب می‌شود و برچسب ‏R‏ آن کلاینت به عقب برمی‌گردد. ‏OSD‏ با ‏AtLimit::Wait‏ ساخته می‌شود، یعنی اگر همه به حد رسیده باشند، هیچ درخواستی بیرون نمی‌آید

۲۱-۸ سیستم‌عامل، شبکه و ‏CPU‏#

حوزهتنظیم پیشنهادیچرا
‏CPU‏‏tuned-adm profile throughput-performance‏ یا ‏latency-performance‏؛ غیرفعال کردن ‏C-state‏های عمیق در ‏BIOS‏ برای ‏NVMe‏بیدار شدن هسته‌ها از ‏C-state‏ عمیق ده‌ها میکروثانیه تأخیر می‌افزاید
زمان‌بند ‏I/O‏‏none‏ برای ‏NVMe‏، ‏mq-deadline‏ برای ‏HDD‏زمان‌بند اضافه برای ‏NVMe‏ سربار است
شبکهشبکه‌ی ‏cluster‏ جدا یا پهنای باند کافی؛ ‏MTU‏ یکسان در کل مسیربازیابی و تکثیر از شبکه‌ی ‏cluster‏ عبور می‌کنند
‏MTU‏اگر ‏jumbo frame‏ (۹۰۰۰) به کار می‌رود، روی همه‌ی سوئیچ‌ها و میزبان‌ها‏MTU‏ ناهماهنگ باعث از دست رفتن بسته و ‏heartbeat failure‏ می‌شود
‏NUMA‏نگه داشتن ‏OSD‏ نزدیک کارت شبکه و ‏NVMe‏ همان ‏NUMA node‏کاهش ترافیک بین سوکت‌ها
‏swap‏حداقل یا غیرفعال؛ پایش ‏OOM‏‏swap‏ شدن ‏OSD‏ تأخیر را غیرقابل پیش‌بینی می‌کند
bash
# بررسی سریع روی هر میزبان
cat /sys/block/nvme0n1/queue/scheduler
tuned-adm active
ip -d link show bond0 | grep -E "mtu|bond"
ping -M do -s 8972 -c 3 10.20.0.12          # آزمون مسیر jumbo frame
ethtool -S ens1f0 | grep -iE "drop|err" | grep -v ": 0"
⚠ هشدار

هشدار ‏ceph-mixin‏ با نام ‏CephNodeInconsistentMTU‏ و ‏CephNodeNetworkPacketDrops‏ دقیقاً برای همین مسائل است. اگر ‏OSD‏ها «‏wrongly marked me down‏» گزارش می‌دهند و همه‌ی میزبان‌ها سالم به نظر می‌رسند، ‏MTU‏ و خطاهای کارت شبکه را اول بررسی کنید.

۲۱-۹ طراحی ‏pool‏ و کلاینت#

  • تکثیری در مقابل ‏EC‏: برای نوشتن‌های کوچک تصادفی (پایگاه داده روی ‏RBD‏)، تکثیری تأخیر کمتری دارد؛ ‏EC‏ برای داده‌ی بزرگ و ترتیبی. در ‏Tentacle‏، ‏FastEC‏ فاصله را برای بارهای ترکیبی کم کرده است (فصل ۱۳).
  • تعداد ‏PG‏: ‏PG‏ کم یعنی موازی‌سازی کم در ‏OSD‏های سریع؛ ‏autoscaler‏ را با ‏bulk‏ و ‏target_size_ratio‏ راهنمایی کنید (فصل ۱۹).
  • ‏RBD‏: کش ‏librbd‏ برای ماشین‌های مجازی، ‏rbd_cache_policy‏ و در صورت نیاز کش ‏persistent write-back‏؛ اندازه‌ی شیء (پیش‌فرض ۴ مگابایت) و ‏striping‏ برای بارهای ترتیبی.
  • ‏RGW‏: ‏shard‏های شاخص ‏bucket‏ (resharding پویا به‌صورت پیش‌فرض فعال است)، تعداد ‏thread‏های ‏frontend‏ و جداسازی ‏pool‏ شاخص روی ‏NVMe‏.
  • ‏CephFS‏: ‏pin‏ کردن زیردرخت‌ها به ‏MDS rank‏ها و چند ‏MDS‏ فعال برای بار متادیتای سنگین.

۲۱-۱۰ سنجش#

۲۱-۱۰-۱ ابزارها#

ابزارلایهنمونه
‏rados bench‏‏RADOS‏ (بدون ‏librbd‏)‏rados bench -p bench 60 write -b 4M -t 16 --no-cleanup‏
‏ceph tell osd.N bench‏یک ‏OSD‏ منفرد‏ceph tell osd.3 bench 1073741824 4194304‏
‏rbd bench‏‏librbd‏‏rbd bench --io-type write --io-size 4K --io-pattern rand vm-fast/test‏
‏fio‏ با ‏ioengine=rbd‏‏librbd‏ از دید برنامهفایل ‏job‏ با ‏ioengine=rbd‏
‏fio‏ روی ‏krbd‏ یا ‏VM‏مسیر کامل کاربر‏fio --rw=randwrite --bs=4k --iodepth=32 --direct=1‏
‏CBT‏سنجش خودکار و تکرارپذیر‏github.com/ceph/cbt‏
ini
; fio-rbd.job — نمونه‌ی این کتاب
[global]
ioengine=rbd
clientname=bench
pool=vm-fast
rbdname=fio-test
direct=1
time_based=1
runtime=300
ramp_time=30
group_reporting=1

[randwrite-4k]
rw=randwrite
bs=4k
iodepth=32
numjobs=4
💡 نکته

همیشه صدک‌ها (‏p99‏، ‏p99.9‏) را گزارش کنید، نه فقط میانگین. ‏Ceph‏ در میانگین ممکن است خوب باشد ولی دم توزیع تأخیر (مثلاً به دلیل ‏deep scrub‏ یا یک ‏OSD‏ کند) تجربه‌ی کاربر را خراب کند. سنجش را دست‌کم چند دقیقه با ‏ramp-up‏ اجرا کنید تا کش‌ها و ‏BlueStore‏ به حالت پایدار برسند.

⚠ هشدار

سنجش روی کلاستر تولیدی بار واقعی کاربران را مختل می‌کند و ‏rados bench‏ با ‏--no-cleanup‏ داده باقی می‌گذارد. در ‏pool‏ جدا اجرا و پس از آن ‏rados -p bench cleanup‏ کنید. ‏ceph tell osd.N bench‏ روی همان دیسک ‏OSD‏ می‌نویسد.

نکات ‏SRE‏ این فصل
  • یک تغییر در هر زمان؛ هر تغییر را با ‏ceph config‏ و قابل بازگشت ثبت کنید.
  • پیش از تنظیم نرم‌افزار، ‏CPU‏، دیسک و شبکه را بررسی کنید.
  • پروفایل ‏mClock‏ را بر اساس زمان روز تغییر دهید، نه ‏osd_max_backfills‏ را.
  • ‏osd_mclock_max_capacity_iops_*‏ را پس از سنجش واقعی، در صورت نادرست بودن سنجش خودکار، تنظیم کنید.
  • صدک‌ها را بسنجید و گزارش کنید.

۲۱-۱۱ تکمیلی و به‌روز#

  • ‏WPQ‏ دیگر پیش‌فرض نیست؛ از ‏Quincy‏، ‏mClock‏ زمان‌بند پیش‌فرض است و بیشتر راهنماهای تنظیم قدیمی (‏sleep‏های بازیابی، ‏osd_max_backfills‏) با آن کار نمی‌کنند.
  • پیش‌فرض شاردهای ‏HDD‏ در نسخه‌ی نقطه‌ای ۱۹.۲.۱ از ‏Squid‏ از ۵×۱ به ۱×۵ تغییر کرد (‏issue 66289‏) و ‏Tentacle‏ و ‏Umbrella‏ هم همین را نگه داشته‌اند.
  • سازوکار جلوگیری از گرسنگی صف ‏immediate‏ در ‏mClock‏ (tracker ۶۹۰۷۸) در مستندات ‏latest‏ آمده است.
  • ‏FileStore‏ در ‏Reef‏ پشتیبانی نمی‌شود؛ همه‌ی تنظیمات این فصل برای ‏BlueStore‏ است.
  • ‏Crimson/SeaStore‏: بازنویسی ‏OSD‏ بر پایه‌ی ‏Seastar‏ در حال توسعه است؛ وضعیت پشتیبانی آن را برای نسخه‌ی خود در مستندات بررسی کنید و آن را در تولید بدون اطمینان از پشتیبانی رسمی به کار نبرید.

۲۱-۱۲ تمرین عملی#

  1. ۱. با ‏rados bench‏ و ‏rbd bench‏ خط پایه بگیرید و نتایج را با صدک‌ها ثبت کنید.
  2. ۲. پروفایل ‏mClock‏ را بین ‏balanced‏ و ‏high_client_ops‏ تغییر دهید و تأخیر کلاینت را در حین ‏backfill‏ یک ‏OSD‏ مقایسه کنید.
  3. ۳. ‏osd_max_backfills‏ را بدون ‏override‏ تغییر دهید و ببینید اثری ندارد؛ سپس با ‏override‏ آزمایش کنید و برگردانید.
  4. ۴. مقدار ‏osd_mclock_max_capacity_iops_ssd‏ تعیین‌شده برای هر ‏OSD‏ را با ‏ceph config dump‏ ببینید و با ‏ceph tell osd.N bench‏ مقایسه کنید.
  5. ۵. روی یک میزبان ‏MTU‏ را ناهماهنگ کنید (در آزمایشگاه) و اثر را روی ‏heartbeat‏ و هشدارها ببینید.
  6. ۶. در ‏dmclock_server.h‏، تابع انتخاب درخواست بعدی را پیدا و با شبه‌کد این فصل مقایسه کنید.

۲۱-۱۳ پرسش‌های مرور#

  1. ۱. چرا تنظیم کارایی باید با خط پایه و یک تغییر در هر زمان انجام شود؟
  2. ۲. شارد هر ‏PG‏ چگونه تعیین می‌شود و چرا همه‌ی عملیات یک ‏PG‏ در یک شارد است؟
  3. ۳. ‏Squid 19.2.1‏ چه تغییری در پیش‌فرض شاردهای ‏HDD‏ داد و چرا معقول است؟
  4. ۴. سه پارامتر ‏mClock‏ را تعریف کنید.
  5. ۵. برچسب ‏R‏ در ‏dmclock‏ چگونه محاسبه می‌شود و ‏max‏(now, ...) چه نقشی دارد؟
  6. ۶. پروفایل ‏high_client_ops‏ چه تفاوتی با ‏balanced‏ دارد؟
  7. ۷. چرا تغییر ‏osd_max_backfills‏ در پروفایل‌های داخلی اثری ندارد؟
  8. ۸. ‏OSD‏ ظرفیت ‏IOPS‏ خود را چگونه تعیین می‌کند و چه زمانی نتیجه را کنار می‌گذارد؟
  9. ۹. سه تنظیم سیستم‌عامل یا شبکه که بیشترین اثر را دارند نام ببرید.
  10. ۱۰. چرا باید صدک‌ها را به جای میانگین گزارش کرد؟

پاسخ این پرسش‌ها در پیوست ه: پاسخ پرسش‌ها آمده است. پاسخ پرسش‌های این فصل →

۲۱-۱۴ منابع این فصل#

راهنمای جامع Ceph · ۱٫۶ · آبان ۱۴۰۵
© 2026 Ramin Najarbashi · CC BY-NC-SA 4.0

حریم خصوصی: این سایت کوکی نمی‌گذارد. برای شمارش بازدیدها از Cloudflare Web Analytics استفاده می‌شود که کوکی ندارد و افراد را دنبال نمی‌کند.