راهنمای جامع Ceph
Englishشروع کنید
در این صفحه
لایه‌های پایینی · فصل ۲۸

لایه‌های پایینی (۶): ‏CRUSH‏ در سطح بیت، از ‏rjenkins‏ تا ‏upmap‏

۴۳ دقیقه مطالعهویرایش این صفحه
حوصله ندارم این فصل در چند خط
  • ‏CRUSH‏ قطعی است: ‏hash‏ از نوع ‏rjenkins‏ روی ورودی، شناسه‌ی ‏bucket‏ و شماره‌ی ‏replica‏ هر انتخاب را می‌سازد.
  • ‏straw2‏ برای هر عضو یک «نی» وزن‌دار می‌کشد و بلندترین را برمی‌دارد، پس تغییر وزن فقط داده‌ی مربوط به همان عضو را جابه‌جا می‌کند.
  • ‏firstn‏ برای ‏replication‏ و ‏indep‏ برای ‏erasure coding‏ مناسب است، چون جای ‏shard‏ها نباید جابه‌جا شود؛ ‏bucket‏های ‏uniform‏، ‏list‏ و ‏tree‏ داده‌ی بیشتری جابه‌جا می‌کنند.
  • مدخل‌های ‏upmap‏ در ‏OSDMap‏ برای تک‌تک ‏PG‏ها ‏CRUSH‏ را کنار می‌زنند و ‏balancer‏ از آن‌ها استفاده می‌کند.

۲۸-۱ مقدمه: ‏CRUSH‏ چگونه «حساب» می‌کند؟#

در فصل‌های ۱ و ۸ ‏CRUSH‏ را به‌عنوان الگوریتمی دیدیم که بدون جدول مرکزی جای هر ‏PG‏ را تعیین می‌کند. این فصل تا سطح بیت پایین می‌رود: ‏hash‏های ‏rjenkins‏ که هر تصمیم را تعیین‌پذیر می‌کنند، الگوریتم ‏straw2‏ با جدول لگاریتم ‏fixed-point‏، اجرای گام‌به‌گام ‏crush_do_rule‏، تلاش‌های مجدد هنگام تصادم، و در پایان این‌که ‏choose_args‏، ‏weight-set‏ و ‏upmap‏ نتیجه‌ی خام ‏CRUSH‏ را چگونه اصلاح می‌کنند. یک مثال کامل را از نام شیء تا انتخاب ‏host‏ با اعدادی دنبال می‌کنیم که با کامپایل همان کد ‏C‏ مربوط به ‏Ceph‏ (‏src/crush/hash.c‏ و ‏mapper.c‏، ‏LGPL‏) در یک برنامه‌ی آزمایشی کوچک محاسبه شده‌اند.

در این بخش
  • نام شیء با ‏ceph_str_hash_rjenkins‏ ‏hash‏ می‌شود. ‏ceph_stable_mod‏ آن را به یک ‏PG‏ نگاشت می‌کند و با ‏HASHPSPOOL‏، ورودی ‏CRUSH‏ برابر ‏crush_hash32_2(ps, pool)‏ است.
  • ‏straw2‏ برای هر آیتم یک «قرعه» محاسبه می‌کند: ‏draw‏ = ‏ln‏(u) / ‏w‏، که ‏u‏ یک ‏hash‏ شانزده‌بیتی است و ‏ln‏ از جدول ‏fixed-point‏ به دست می‌آید. بیشترین ‏draw‏ برنده است. احتمال برنده شدن هر آیتم دقیقاً با وزنش متناسب است.
  • تصادم (انتخاب تکراری) با افزایش ‏r‏ حل می‌شود (‏r‏ = ‏rep‏ + ‏ftotal‏) تا ‏choose_total_tries=50‏.
  • ‏reweight‏ با ‏is_out‏ احتمالی کار می‌کند. ‏weight-set‏ در ‏choose_args‏ وزن جایگزین برای ‏CRUSH‏ است و ‏upmap‏ نتیجه را پس از ‏CRUSH‏ صریحاً جایگزین می‌کند.

۲۸-۲ از نام شیء تا ‏x‏#

شکل ۲۸-۱: خط لوله‌ی کامل نگاشت. نام شیء با rjenkins به ۰ xD1895615 hash می‌شود. با pg_num=128 ، ceph_stable_mod نتیجه‌ی ۰ x15 را می‌دهد، یعنی PG 3.15 . با پرچم HASHPSPOOL ، شماره‌ی pool هم در hash دخیل می‌شود تا PG های pool های مختلف روی هم نیفتند، و x = ۰ x36D15C32 ورودی CRUSH است. crush_do_rule فهرست خام OSD ها را برمی‌گرداند، سپس OSD های ناموجود حذف و upmap ها اعمال می‌شوند و در پایان pg_temp مجموعه‌ی acting را تعیین می‌کند
شکل ۲۸-۱: خط لوله‌ی کامل نگاشت. نام شیء با ‏rjenkins‏ به ۰‏xD1895615 hash‏ می‌شود. با ‏pg_num=128‏، ‏ceph_stable_mod‏ نتیجه‌ی ۰‏x15‏ را می‌دهد، یعنی ‏PG 3.15‏. با پرچم ‏HASHPSPOOL‏، شماره‌ی ‏pool‏ هم در ‏hash‏ دخیل می‌شود تا ‏PG‏های ‏pool‏های مختلف روی هم نیفتند، و ‏x‏ = ۰‏x36D15C32‏ ورودی ‏CRUSH‏ است. ‏crush_do_rule‏ فهرست خام ‏OSD‏ها را برمی‌گرداند، سپس ‏OSD‏های ناموجود حذف و ‏upmap‏ها اعمال می‌شوند و در پایان ‏pg_temp‏ مجموعه‌ی ‏acting‏ را تعیین می‌کند

‏ceph_stable_mod(x, b, bmask)‏ در ‏include/rados.h‏ تعریف شده است: اگر ‏x & bmask‏ کمتر از ‏b‏ باشد، همان را برمی‌گرداند، وگرنه ‏x & (bmask >> 1)‏. این تابع «پایدار» است، چون وقتی ‏pg_num‏ از ۱۲۸ به ۱۹۲ افزایش می‌یابد، فقط ‏PG‏هایی که بیت جدید را دارند جابه‌جا می‌شوند (‏split‏) و بقیه همان شماره را نگه می‌دارند. این دلیل ریاضی آن است که ‏split‏ فقط بخشی از داده را حرکت می‌دهد.

گاممقدار در مثالتابع
‏hash‏ نام۰‏xD1895615‏‏ceph_str_hash_rjenkins‏ (‏common/ceph_hash.cc‏)
‏ps‏۰‏x15‏ (= ۰xD۱۸۹۵۶۱۵ & ۱۲۷)‏ceph_stable_mod(h, 128, 127)‏
‏PG‏۳.۱۵‏pg_t(ps, pool)‏
‏x‏ (pps)۰‏x36D15C32‏‏crush_hash32_2(RJENKINS1, ps, 3)‏ در ‏pg_pool_t::raw_pg_to_pps‏

۲۸-۲-۱ ‏hash‏ مربوط به ‏rjenkins‏#

‏crush_hashmix(a, b, c)‏ نُه دور تفریق، ‏XOR‏ و شیفت روی سه کلمه‌ی ۳۲ بیتی است، برگرفته از تابع ترکیب ‏Bob Jenkins‏. نسخه‌ی سه‌ورودی ‏crush_hash32_rjenkins1_3(a, b, c)‏ با ‏hash = 1315423911 ^ a ^ b ^ c‏ (ثابت ‏crush_hash_seed‏) شروع می‌کند و پنج بار ‏mix‏ را با ثابت‌های ‏x = 231232‏ و ‏y = 1232‏ اجرا می‌کند. ویژگی مهم آن تعیین‌پذیری کامل روی همه‌ی معماری‌ها است: فقط عملیات صحیح بدون علامت ۳۲ بیتی دارد، بدون اعشار. به همین دلیل کلاینت هسته‌ی لینوکس (که همین فایل را در ‏net/ceph/crush/‏ دارد) و ‏librados‏ دقیقاً همان نتیجه را می‌گیرند.

⚠ هشدار

هر تغییری در این توابع، در جدول لگاریتم یا در ‏tunable‏ها (مثل ‏chooseleaf_stable‏) نگاشت تقریباً همه‌ی ‏PG‏ها را عوض می‌کند. به همین دلیل ‏tunable‏ها پروفایل‌های نام‌دار دارند (‏ceph osd crush tunables optimal‏) و تغییر آن‌ها روی خوشه‌ی پر، حرکت داده‌ی عظیمی ایجاد می‌کند.

۲۸-۳ ‏straw2‏: قرعه‌کشی نمایی#

فرض کنید هر آیتم ‏i‏ با وزن ‏w_i‏ یک متغیر تصادفی نمایی با نرخ ‏w_i‏ بکشد. احتمال این‌که آیتم ‏i‏ کمترین مقدار را داشته باشد، دقیقاً ‏wi/∑ww_i / \sum w‏ است. ‏straw2‏ همین را بدون عدد اعشاری پیاده می‌کند:

text
straw2 (منطق mapper.c، LGPL؛ نمایش ساده‌شده از این کتاب)
  for each item i in bucket:
      u    = crush_hash32_3(bucket.hash, x, id_i, r) & 0xFFFF       # 16 بیت
      ln   = crush_ln(u) − 2^48          # ≈ 2^44 · log2((u+1)/2^16) ≤ 0
      draw = ln / weight_i                # weight در 16.16 fixed-point
  return item with max draw              # کمترین −ln/w = نمایی با نرخ w

‏crush_ln(x)‏ مقدار ‏244⋅log⁡2(x+1)2^{44} \cdot \log_2(x+1)‏ را با دو جدول ‏__RH_LH_tbl‏ و ‏__LL_tbl‏ (در ‏crush_ln_table.h‏) و فقط با عملیات صحیح محاسبه می‌کند. ابتدا ورودی را نرمال می‌کند (توان)، سپس با ۸ بیت بالای ‏mantissa‏ در جدول اول (تقریب ۱/‏x‏ و ‏log2‏) و با ۸ بیت بعدی در جدول دوم (تصحیح) جست‌وجو می‌کند. نتیجه‌ها در برنامه‌ی آزمایشی ما: ‏crush_ln(0) = 0‏، ‏crush_ln(0x7FFF) = 0xF00000000000‏ (یعنی ‏15⋅24415 \cdot 2^{44}‏) و ‏crush_ln(0xFFFF) = 0xFFFFF0000000‏ (تقریباً ‏16⋅244=24816 \cdot 2^{44} = 2^{48}‏).

شکل ۲۸-۲: اجرای واقعی straw2 برای x=0x36D15C32 در یک bucket ریشه با سه host : -۳ و -۴ هرکدام با وزن ۱۰ و -۵ با وزن ۲۰. در r=0 ، host -۴ با بیشترین draw (−۸.۹ M ) برنده است. در r=1 ، host -۵ برنده است. برای replica سوم، r=2 تا r=5 به host های تکراری می‌رسند و هر بار ftotal افزایش می‌یابد، تا این‌که r=6 به host -۳ می‌رسد. سقف تلاش choose_total_tries=50 است
شکل ۲۸-۲: اجرای واقعی ‏straw2‏ برای ‏x=0x36D15C32‏ در یک ‏bucket‏ ریشه با سه ‏host‏: -۳ و -۴ هرکدام با وزن ۱۰ و -۵ با وزن ۲۰. در ‏r=0‏، ‏host‏ -۴ با بیشترین ‏draw‏ (−۸.۹‏M‏) برنده است. در ‏r=1‏، ‏host‏ -۵ برنده است. برای ‏replica‏ سوم، ‏r=2‏ تا ‏r=5‏ به ‏host‏های تکراری می‌رسند و هر بار ‏ftotal‏ افزایش می‌یابد، تا این‌که ‏r=6‏ به ‏host‏ -۳ می‌رسد. سقف تلاش ‏choose_total_tries=50‏ است
‏r‏‏draw‏(-۳)‏draw‏(-۴)‏draw‏(-۵)برنده
۰−۷۷,۲۵۹,۳۸۸−۸,۸۸۰,۳۶۱−۳۱,۲۴۸,۹۸۵-۴
۱−۷,۱۷۷,۹۸۱−۱۴,۵۸۲,۲۸۷−۳,۹۱۸,۶۳۵-۵
۲−۱۱,۸۵۴,۳۸۴−۵۹,۷۶۰,۶۹۳−۲,۴۱۳,۶۲۹-۵ (تکراری)
۳−۶۳,۸۷۸,۱۳۸−۱۲۵,۳۴۹,۲۵۸−۱۴,۷۰۱,۳۰۰-۵ (تکراری)

بررسی توزیع: در همان برنامه، با سه آیتم با وزن‌های ۱، ۱ و ۲ و ۳۰۰٬۰۰۰ مقدار ‏x‏، شمارش برنده‌ها ۷۵,۲۷۴، ۷۴,۷۵۳ و ۱۴۹,۹۷۳ بود، یعنی ۲۵٪، ۲۵٪ و ۵۰٪، همان‌طور که نظریه پیش‌بینی می‌کند.

◆ عمیق‌تر

چرا ‏straw2‏ و نه ‏straw‏؟ در ‏straw‏ قدیمی، «طول نی» هر آیتم از روی وزن‌ها به‌صورت نسبی و وابسته به سایر آیتم‌ها محاسبه می‌شد. بنابراین تغییر وزن یک آیتم، ‏draw‏ آیتم‌های دیگر را هم تغییر می‌داد و داده بین آیتم‌هایی جابه‌جا می‌شد که وزنشان تغییر نکرده بود. در ‏straw2‏، ‏draw‏ هر آیتم فقط به (‏x‏، ‏id‏، ‏r‏، وزن خودش) بستگی دارد. اگر وزن آیتم ‏A‏ زیاد شود، فقط برخی ‏x‏‌ها از سایر آیتم‌ها به ‏A‏ منتقل می‌شوند و هیچ داده‌ای بین ‏B‏ و ‏C‏ جابه‌جا نمی‌شود. این خاصیت حداقل حرکت (‏optimal reorganization‏) است.

۲۸-۴ ‏crush_do_rule‏ گام‌به‌گام#

شکل ۲۸-۳: اجرای replicated_rule. TAKE ریشه را در بردار کاری w می‌گذارد. CHOOSELEAF_FIRSTN با numrep=0 (یعنی برابر size ) برای هر replica یک host انتخاب می‌کند و سپس به‌صورت بازگشتی یک OSD درون آن. EMIT نتیجه را به خروجی می‌افزاید. در هر انتخاب، r = rep + ftotal است. اگر نتیجه تکراری باشد یا OSD با is_out رد شود، ftotal افزایش می‌یابد و تلاش تکرار می‌شود. is_out برای وزن reweight بین ۰ و ۱، با hash دیگری از ( x ، osd ) تصمیم احتمالی می‌گیرد
شکل ۲۸-۳: اجرای ‏replicated_rule. TAKE‏ ریشه را در بردار کاری ‏w‏ می‌گذارد. ‏CHOOSELEAF_FIRSTN‏ با ‏numrep=0‏ (یعنی برابر ‏size‏) برای هر ‏replica‏ یک ‏host‏ انتخاب می‌کند و سپس به‌صورت بازگشتی یک ‏OSD‏ درون آن. ‏EMIT‏ نتیجه را به خروجی می‌افزاید. در هر انتخاب، ‏r‏ = ‏rep‏ + ‏ftotal‏ است. اگر نتیجه تکراری باشد یا ‏OSD‏ با ‏is_out‏ رد شود، ‏ftotal‏ افزایش می‌یابد و تلاش تکرار می‌شود. ‏is_out‏ برای وزن ‏reweight‏ بین ۰ و ۱، با ‏hash‏ دیگری از (‏x‏، ‏osd‏) تصمیم احتمالی می‌گیرد

یک قاعده آرایه‌ای از گام‌هاست (‏crush_rule_step‏ با ‏op‏، ‏arg1‏ و ‏arg2‏). ‏crush_do_rule‏ یک بردار کاری ‏w‏ و یک بردار خروجی ‏o‏ دارد:

‏op‏مقدارعمل
‏TAKE‏۱‏w‏ = [‏arg1]‏ (مثلاً ‏id‏ ریشه یا یک ‏device class‏ در ‏shadow tree‏)
‏CHOOSE_FIRSTN‏۲از هر ‏bucket‏ در ‏w‏، ‏arg1‏ آیتم از نوع ‏arg2‏ انتخاب کن (برای ‏replicated‏)
‏CHOOSE_INDEP‏۳همان، با جایگاه‌های ثابت (برای ‏EC‏؛ ‏shard‏ گم‌شده جای خالی می‌ماند)
‏EMIT‏۴‏o‏ += ‏w‏؛ ‏w‏ خالی می‌شود
‏CHOOSELEAF_FIRSTN‏ / ‏_INDEP‏۶ / ۷انتخاب ‏bucket‏ از نوع ‏arg2‏ و سپس یک ‏leaf‏ (OSD) در آن
‏SET_CHOOSE_TRIES‏ و موارد مشابه۸–۱۳‏override‏ کردن ‏tunable‏ها برای همین قاعده
‏SET_MSR_DESCENTS‏ / ‏SET_MSR_COLLISION_TRIES‏ / ‏CHOOSE_MSR‏۱۴ / ۱۵ / ۱۶قواعد ‏MSR‏ (چند ‏OSD‏ در هر ‏failure domain‏) — در ‏Squid‏ و بالاتر، نه در ‏Reef‏

‏numrep‏: اگر ‏arg1‏ ≤ ۰ باشد، ‏numrep‏ = ‏result_max‏ + ‏arg1‏. پس ‏0‏ یعنی «به تعداد ‏size‏» و ‏-1‏ یعنی «‏size‏ منهای یک». این برای قواعدی مثل «یکی در ‏DC‏ اول، بقیه در ‏DC‏ دوم» استفاده می‌شود.

۲۸-۴-۱ ‏firstn‏ در برابر ‏indep‏#

در ‏firstn‏، ترتیب مهم نیست. اگر ‏replica‏ دوم ناموجود شود، ‏replica‏ سوم جایش را می‌گیرد و همه‌چیز یک خانه به جلو می‌رود. برای ‏replication‏ این بهینه است، چون فقط یک کپی تازه لازم است. در ‏indep‏، جایگاه ‏r‏ ام همیشه ‏shard r‏ ام است. اگر ‏OSD‏ یک ‏shard‏ از دست برود، فقط همان جایگاه با ‏r‏ تازه (‏r‏ + ‏numrep‏ · ‏ftotal‏) دوباره انتخاب می‌شود و بقیه‌ی ‏shard‏ها جابه‌جا نمی‌شوند. برای ‏EC‏ این حیاتی است، چون ‏shard‏ها قابل‌جابه‌جایی نیستند.

‏tunable‏ (پروفایل ‏jewel/optimal‏)مقداراثر
‏choose_local_tries‏۰تلاش محلی قدیمی خاموش
‏choose_local_fallback_tries‏۰خاموش
‏choose_total_tries‏۵۰سقف تلاش‌ها؛ در مثال ما ۴ تلاش ناموفق داشتیم
‏chooseleaf_descend_once‏۱در ‏chooseleaf‏، یک بار پایین برو و اگر شکست خورد از بالا تلاش کن
‏chooseleaf_vary_r‏۱‏r‏ در زیرانتخاب ‏leaf‏ به ‏r‏ والد وابسته است تا تلاش‌های مجدد نتیجه‌ی متفاوت بدهند
‏chooseleaf_stable‏۱با ‏out‏ شدن یک ‏OSD‏، ‏replica‏های دیگر جابه‌جا نمی‌شوند
‏straw_calc_version‏۱اصلاح محاسبه‌ی ‏straw‏ قدیمی
✎ یادداشت

تصادم‌ها وقتی تعداد ‏failure domain‏ها به ‏size‏ نزدیک است زیاد می‌شوند. در مثال ما با سه ‏host‏ و ‏size=3‏، ‏replica‏ سوم پس از ۴ تلاش پیدا شد. اگر یکی از ‏host‏ها وزن بسیار کمی داشته باشد، ممکن است ۵۰ تلاش هم کافی نباشد و ‏PG‏ با کمتر از ‏size‏ عضو در وضعیت ‏undersized‏ بماند. ‏crushtool --test --show-bad-mappings‏ همین را آزمایش می‌کند.

bashReef+
ceph osd getcrushmap -o cm.bin && crushtool -d cm.bin -o cm.txt
crushtool -i cm.bin --test --rule 0 --num-rep 3 --min-x 0 --max-x 1023 --show-utilization
crushtool -i cm.bin --test --rule 0 --num-rep 3 --x 0x36d15c32 --show-mappings
crushtool -i cm.bin --test --show-bad-mappings --rule 0 --num-rep 3
ceph osd map rbd rbd_data.10ab2c3d4e5f.0000000000000000     # pg و up/acting

۲۸-۵ ‏choose_args‏، ‏weight-set‏ و ‏upmap‏#

شکل ۲۸-۴: سه ابزار اصلاح توزیع و ترتیب اعمال آن‌ها. reweight با is_out احتمالی کار می‌کند و جابه‌جایی گسترده ایجاد می‌کند. choose_args (weight-set) وزن‌های جایگزینی برای straw2 فراهم می‌کند که حتی می‌تواند برای هر position (replica اول، دوم و ...) متفاوت باشد و balancer در حالت crush-compat از آن استفاده می‌کند. upmap پس از CRUSH نتیجه را صریحاً جایگزین می‌کند. در OSDMap ترتیب اعمال این است: crush_do_rule با choose_args ، حذف OSD های ناموجود، pg_upmap ، pg_upmap_items و pg_upmap_primary
شکل ۲۸-۴: سه ابزار اصلاح توزیع و ترتیب اعمال آن‌ها. ‏reweight‏ با ‏is_out‏ احتمالی کار می‌کند و جابه‌جایی گسترده ایجاد می‌کند. ‏choose_args‏ (weight-set) وزن‌های جایگزینی برای ‏straw2‏ فراهم می‌کند که حتی می‌تواند برای هر ‏position‏ (replica اول، دوم و ...) متفاوت باشد و ‏balancer‏ در حالت ‏crush-compat‏ از آن استفاده می‌کند. ‏upmap‏ پس از ‏CRUSH‏ نتیجه را صریحاً جایگزین می‌کند. در ‏OSDMap‏ ترتیب اعمال این است: ‏crush_do_rule‏ با ‏choose_args‏، حذف ‏OSD‏های ناموجود، ‏pg_upmap‏، ‏pg_upmap_items‏ و ‏pg_upmap_primary‏

‏choose_args‏: در ‏bucket_straw2_choose‏، تابع ‏get_choose_arg_weights‏ اگر ‏weight_set‏ موجود باشد، به‌جای ‏item_weights‏ از ‏weight_set[position]‏ استفاده می‌کند. ‏position‏ شماره‌ی ‏replica‏ در حال انتخاب است. این امکان اصلاح یک مشکل ظریف را می‌دهد: در ‏straw2‏ با وزن‌های نابرابر، ‏replica‏ دوم و سوم کمی از نسبت وزن منحرف می‌شوند، چون انتخاب‌های قبلی کنار گذاشته شده‌اند. ‏weight-set‏ برای هر ‏position‏ وزن کمی متفاوتی می‌دهد. ‏ids‏ جایگزین هم وجود دارد، تا بتوان ‏hash‏ را بدون تغییر شناسه‌ی واقعی ‏bucket‏ عوض کرد.

‏upmap‏: ‏OSDMap::_apply_upmap‏ ابتدا ‏pg_upmap‏ (جایگزینی کل فهرست) را بررسی می‌کند، سپس ‏pg_upmap_items‏ (جفت‌های ‏from‏→‏to‏) و در پایان ‏pg_upmap_primaries‏ (فقط تعیین ‏primary‏، برای ‏read balancer‏). ‏upmap‏هایی که قاعده‌ی ‏CRUSH‏ را نقض کنند (مثلاً دو ‏replica‏ در یک ‏host‏) توسط ‏mon‏ بررسی و حذف می‌شوند (‏clean_pg_upmaps‏).

حالت ‏balancer‏مکانیزمنسخه
‏crush-compat‏‏weight-set‏ در ‏choose_args‏از ‏Luminous‏؛ برای کلاینت‌های قدیمی
‏upmap‏‏pg_upmap_items‏پیش‌فرض؛ نیازمند ‏require-min-compat-client luminous‏
‏read‏‏pg_upmap_primaries‏ (فقط ‏primary‏)‏Reef‏ و بالاتر (طبق مستندات)
‏upmap-read‏هر دو‏Reef‏ و بالاتر
bashReef+
ceph osd pg-upmap-items 3.15 4 11        # جایگزینی osd.4 با osd.11 برای PG 3.15
ceph osd rm-pg-upmap-items 3.15
ceph osd crush weight-set ls
ceph balancer status && ceph balancer eval
ceph osd dump | grep -E '^pg_upmap(_items|_primary)?' | head

۲۸-۶ ‏is_out‏ و ‏reweight‏: یک مثال عددی#

‏is_out‏ در ‏mapper.c‏ وزن ‏reweight‏ یک ‏OSD‏ (عددی ۱۶.۱۶ بین ۰ و ۰‏x10000‏) را این‌طور تفسیر می‌کند: اگر وزن کامل باشد، ‏OSD‏ داخل است. اگر صفر باشد، بیرون است. در غیر این صورت، ‏crush_hash32_2(x, osd) & 0xFFFF‏ با وزن مقایسه می‌شود و اگر کمتر باشد، ‏OSD‏ برای این ‏x‏ داخل است. پس ‏reweight‏ برابر ۰.۵ یعنی تقریباً نیمی از ‏PG‏هایی که ‏CRUSH‏ به این ‏OSD‏ داده بود، رد می‌شوند و با تلاش مجدد به جای دیگری می‌روند.

مثالمقدار
‏x‏۰‏x36D15C32‏ (همان ‏PG 3.15‏)
‏osd‏۷
‏crush_hash32_2(x, 7) & 0xFFFF‏۰‏xC425‏
‏reweight 0.5‏ (۰x۸۰۰۰)۰‏xC425‏ ≥ ۰‏x8000‏ → ‏out‏ برای این ‏PG‏
‏reweight 0.9‏ (۰xE۶۶۶)۰‏xC425‏ < ۰‏xE666‏ → ‏in‏
نسبت ‏in‏ با ‏reweight 0.5‏ در ۱۰۰٬۰۰۰ ‏x‏۵۰,۰۶۹ (≈ ۵۰٪)
💡 نکته

تفاوت ‏ceph osd reweight‏ (همین ‏is_out‏ احتمالی، که پس از انتخاب ‏straw2‏ اعمال می‌شود) با ‏ceph osd crush reweight‏ (وزن ‏straw2‏ در خود ‏bucket‏) مهم است. ‏reweight‏ کردن با ‏is_out‏ باعث تلاش‌های مجدد بیشتر و انحراف ظریف توزیع می‌شود، به همین دلیل ‏balancer‏ مبتنی بر ‏upmap‏ جایگزین بهتری است. مقادیر نزدیک ۱ (مثلاً ۰.۹۵) جابه‌جایی کمی ایجاد می‌کنند، ولی مقادیر کوچک (مثلاً ۰.۳) می‌توانند تلاش‌ها را به سقف ۵۰ نزدیک کنند.

۲۸-۷ ‏device class‏ و ‏shadow tree‏#

وقتی ‏OSD‏ها کلاس دارند (‏hdd‏، ‏ssd‏، ‏nvme‏)، ‏CrushWrapper‏ برای هر ‏bucket‏ و هر کلاس یک «سایه» با ‏id‏ منفی جدید و نامی مثل ‏default~ssd‏ می‌سازد که فقط ‏OSD‏های آن کلاس را دارد. قاعده‌ای مثل ‏step take default class ssd‏ در واقع ‏TAKE‏ روی ‏id‏ سایه است. چون سایه‌ها ‏id‏های خودشان را دارند، ‏hash‏ مربوط به ‏straw2‏ (که ‏id‏ آیتم در آن دخیل است) برای سایه‌ها مستقل از درخت اصلی است. افزودن یک ‏OSD‏ از نوع ‏hdd‏ هیچ ‏PG‏ی را در ‏pool‏های ‏ssd‏ جابه‌جا نمی‌کند.

bashReef+
ceph osd crush tree --show-shadow
ceph osd crush rule create-replicated fast default host ssd
ceph osd crush class ls-osd ssd
◆ عمیق‌تر

چرا ‏id‏های سایه پایدار می‌مانند؟ نگاشت کلاس به ‏id‏ سایه در خود ‏CRUSH map‏ ذخیره می‌شود (‏class_bucket‏). بنابراین بازسازی سایه‌ها پس از افزودن ‏OSD‏ یا تغییر وزن، همان ‏id‏ها را نگه می‌دارد. اگر این ‏id‏ها تغییر می‌کردند، ‏x‏ مربوط به همه‌ی ‏PG‏های ‏pool‏های وابسته به آن کلاس ‏hash‏ متفاوتی می‌گرفت و کل داده جابه‌جا می‌شد. ابزار ‏crushtool --reclassify‏ برای تبدیل نقشه‌های قدیمی (با ریشه‌های جداگانه برای ‏ssd‏) به کلاس‌ها، دقیقاً همین ‏id‏ها را طوری تنظیم می‌کند که نگاشت تغییر نکند.

۲۸-۸ سه نوع ‏bucket‏ دیگر: ‏uniform‏، ‏list‏ و ‏tree‏#

‏straw2‏ امروز پیش‌فرض است، اما ‏CRUSH‏ از ابتدا پنج الگوریتم ‏bucket‏ داشت: ‏uniform‏، ‏list‏، ‏tree‏، ‏straw‏ و ‏straw2‏. هرکدام مصالحه‌ای میان سرعت انتخاب و میزان جابه‌جایی داده هنگام تغییر است. نگاشت‌های قدیمی و نقشه‌های دست‌ساز هنوز ممکن است آن‌ها را داشته باشند. برای فهم اینکه چرا ‏straw2‏ برنده شد، هر سه را با ‏hash‏ واقعی ‏rjenkins‏ اجرا کرده‌ایم. همه‌ی اعداد این بخش با یک برنامه‌ی کوچک ‏C‏ که ‏hash.c‏ و ‏crush_ln‏ را از کد ‏Ceph‏ می‌گیرد محاسبه شده‌اند.

۲۸-۸-۱ ‏uniform‏: جایگشت شبه‌تصادفی#

∑ الگوریتم: انتخاب در ‏bucket‏ از نوع ‏uniform‏ با جایگشت تدریجی

منبع در کد: src/crush/mapper.c :: bucket_perm_choose و bucket_uniform_choose

پیچیدگی: ‏r=0‏ در ‏O‏(۱)؛ ‏r=k‏ در ‏O‏(k)؛ کار مشترک در ‏crush_work_bucket.perm‏

text
pseudocode (برگرفته از bucket_perm_choose)
pr = r mod size
if x changed:
    if pr == 0: return items[hash3(x, bucket_id, 0) mod size]        # میان‌بر رایج
    perm = [0..size-1]; perm_n = 0
while perm_n ≤ pr:                         # Fisher–Yates تدریجی با hash به‌جای random
    p = perm_n
    if p < size-1:
        i = hash3(x, bucket_id, p) mod (size − p)
        swap(perm[p], perm[p+i])
    perm_n += 1
return items[perm[pr]]
شکل ۲۸-۵: اجرای واقعی bucket_perm_choose برای bucket با شماره‌ی -۵ و چهار item با x=0x1234ABCD . سه گام Fisher – Yates جایگشت [۳ ۲ ۰ ۱] را می‌سازند. پس r=0 به item 13 ، r=1 به ۱۲، r=2 به ۱۰ و r=3 به ۱۱ می‌رسد. جایگشت در crush_work نگه داشته می‌شود تا تلاش‌های بعدی با همان x از نو محاسبه نشوند
شکل ۲۸-۵: اجرای واقعی ‏bucket_perm_choose‏ برای ‏bucket‏ با شماره‌ی -۵ و چهار ‏item‏ با ‏x=0x1234ABCD‏. سه گام ‏Fisher‏–‏Yates‏ جایگشت [۳ ۲ ۰ ۱] را می‌سازند. پس ‏r=0‏ به ‏item 13‏، ‏r=1‏ به ۱۲، ‏r=2‏ به ۱۰ و ‏r=3‏ به ۱۱ می‌رسد. جایگشت در ‏crush_work‏ نگه داشته می‌شود تا تلاش‌های بعدی با همان ‏x‏ از نو محاسبه نشوند

‏uniform‏ فقط وقتی درست کار می‌کند که همه‌ی ‏item‏ها وزن برابر داشته باشند، چون وزن اصلاً در محاسبه نیست. مشکل بزرگ‌تر این است که تغییر ‏size‏ پیمانه‌ی همه‌ی ‏hash‏ها را عوض می‌کند. شبیه‌سازی ما نشان می‌دهد افزودن ‏item‏ پنجم به چهار ‏item‏، ۸۰٪ ورودی‌ها را جابه‌جا می‌کند، در حالی که حداقل لازم ۲۰٪ است. این ‏bucket‏ برای سخت‌افزاری طراحی شد که همیشه یک‌جا و هم‌اندازه خریده و جایگزین می‌شود.

۲۸-۸-۲ ‏list‏: پیمایش از آخر#

∑ الگوریتم: انتخاب در ‏bucket‏ از نوع ‏list‏

منبع در کد: src/crush/mapper.c :: bucket_list_choose

پیچیدگی: ‏O‏(n) در بدترین حالت؛ افزودن به انتها بهینه است

text
for i = size-1 down to 0:
    h = hash4(x, items[i], r, bucket_id) & 0xFFFF                   # عدد 16 بیتی
    if (h × sum_weights[i]) >> 16 < item_weights[i]: return items[i]
return items[0]
# sum_weights[i] = Σ_{j ≤ i} item_weights[j]  ⇒ احتمال انتخاب i به شرط رسیدن به آن = w_i / sum_i
شکل ۲۸-۶: اجرای واقعی bucket_list_choose برای bucket با شماره‌ی -۶ با وزن‌های ۱، ۱ و ۲. برای r=0 مقدار مقیاس‌شده‌ی item 22 برابر ۳.۹۰ است که از وزن ۲ بیشتر است، پس رد می‌شود. item 21 با ۰.۳۶ < ۱ انتخاب می‌شود. برای r=1 و r=2 هر دو item آخر رد می‌شوند و item 20 انتخاب می‌شود
شکل ۲۸-۶: اجرای واقعی ‏bucket_list_choose‏ برای ‏bucket‏ با شماره‌ی -۶ با وزن‌های ۱، ۱ و ۲. برای ‏r=0‏ مقدار مقیاس‌شده‌ی ‏item 22‏ برابر ۳.۹۰ است که از وزن ۲ بیشتر است، پس رد می‌شود. ‏item 21‏ با ۰.۳۶ < ۱ انتخاب می‌شود. برای ‏r=1‏ و ‏r=2‏ هر دو ‏item‏ آخر رد می‌شوند و ‏item 20‏ انتخاب می‌شود

چرا درست است؟ احتمال انتخاب ‏item‏ آخر ‏w‏₂/(‏w‏₀+‏w‏₁+‏w‏₂) = ۲/۴ است. اگر رد شود، احتمال ‏item‏ میانی ‏w‏₁/(‏w‏₀+‏w‏₁) = ۱/۲ است. پس ‏P‏(۲۱) = (۱ − ۱/۲) × ۱/۲ = ۱/۴ و ‏P‏(۲۰) = ۱/۴. شمارش روی ۴۰۰ هزار ورودی همین نسبت ۱:۱:۲ را داد (۱۰۰۳۳۶، ۹۹۹۷۰، ۱۹۹۶۹۴). افزودن ‏item‏ به انتها فقط داده‌ی لازم را جابه‌جا می‌کند (۲۰٫۱٪). اما حذف یک ‏item‏ از میانه ترتیب را می‌شکند (در شبیه‌سازی ما ۳۶٪ به‌جای ۲۵٪).

۲۸-۸-۳ ‏tree‏: درخت دودویی وزن‌دار#

∑ الگوریتم: انتخاب در ‏bucket‏ از نوع ‏tree‏

منبع در کد: src/crush/mapper.c :: bucket_tree_choose؛ src/crush/builder.c :: crush_make_tree_bucket

پیچیدگی: ‏O‏(log n)؛ شماره‌ی گره‌ها با افزودن ‏item‏ ثابت می‌ماند

text
node(i) = 2(i+1) − 1   (برگ‌ها فرد)؛ height(n) = تعداد صفرهای انتهایی n
n = num_nodes / 2                          # ریشه
while n is even:                           # گره داخلی
    t = (hash4(x, n, r, bucket_id) × node_weights[n]) >> 32     # عدد یکنواخت در [0, w)
    n = t < node_weights[left(n)] ? left(n) : right(n)
return items[n >> 1]
شکل ۲۸-۷: اجرای واقعی bucket_tree_choose برای چهار item با وزن‌های ۱، ۲، ۳ و ۴. ریشه n4 وزن ۱۰ دارد. t=4.22 از وزن زیردرخت چپ (۳) بیشتر است، پس مسیر به راست ( n6 ، وزن ۷) می‌رود. آنجا t=0.78 < ۳، پس برگ n5 یعنی item 32 انتخاب می‌شود. برای r=1 هم همین مسیر طی شد. توزیع روی ۴۰۰ هزار ورودی تقریباً ۱:۲:۳:۴ بود
شکل ۲۸-۷: اجرای واقعی ‏bucket_tree_choose‏ برای چهار ‏item‏ با وزن‌های ۱، ۲، ۳ و ۴. ریشه ‏n4‏ وزن ۱۰ دارد. ‏t=4.22‏ از وزن زیردرخت چپ (۳) بیشتر است، پس مسیر به راست (‏n6‏، وزن ۷) می‌رود. آنجا ‏t=0.78‏ < ۳، پس برگ ‏n5‏ یعنی ‏item 32‏ انتخاب می‌شود. برای ‏r=1‏ هم همین مسیر طی شد. توزیع روی ۴۰۰ هزار ورودی تقریباً ۱:۲:۳:۴ بود

ترفند زیبای ‏tree‏ شماره‌گذاری گره‌هاست. چون ‏hash‏ به شماره‌ی گره وابسته است و شماره‌ی گره‌های موجود با بزرگ شدن درخت تغییر نمی‌کند (ریشه‌ی قدیم ‏n4‏ در درخت هشت‌برگی فرزند چپ ‏n8‏ می‌شود)، افزودن ‏item‏ پنجم دقیقاً ۲۰٪ داده را جابه‌جا کرد. اما حذف از میانه و بازسازی درخت، چیدمان برگ‌ها را عوض می‌کند و در شبیه‌سازی ما ۵۴٪ جابه‌جایی داشت.

۲۸-۸-۴ مقایسه‌ی جابه‌جایی#

شکل ۲۸-۸: درصد ورودی‌هایی که با تغییر bucket مقصد دیگری گرفتند، روی ۲۰۰ هزار مقدار x با hash واقعی. ستون آبی: افزودن item پنجم به چهار item هم‌وزن (حداقل نظری ۲۰٪). ستون نارنجی: حذف item دوم و ساخت دوباره‌ی bucket (حداقل نظری ۲۵٪). فقط straw2 در هر دو حالت به حداقل نظری می‌رسد
شکل ۲۸-۸: درصد ورودی‌هایی که با تغییر ‏bucket‏ مقصد دیگری گرفتند، روی ۲۰۰ هزار مقدار ‏x‏ با ‏hash‏ واقعی. ستون آبی: افزودن ‏item‏ پنجم به چهار ‏item‏ هم‌وزن (حداقل نظری ۲۰٪). ستون نارنجی: حذف ‏item‏ دوم و ساخت دوباره‌ی ‏bucket‏ (حداقل نظری ۲۵٪). فقط ‏straw2‏ در هر دو حالت به حداقل نظری می‌رسد
نوع ‏bucket‏هزینه‌ی انتخابافزودنحذف یا تغییر وزنکاربرد امروز
‏uniform‏‏O‏(۱)بد (۸۰٪ در شبیه‌سازی)بدتقریباً هیچ
‏list‏‏O‏(n)بهینه اگر در انتهابد در میانهخوشه‌های فقط‌رشدی قدیمی
‏tree‏‏O‏(log n)بهینهمتوسط‏bucket‏های خیلی بزرگ قدیمی
‏straw‏‏O‏(n)تقریباً بهینهتقریباً بهینه، با خطای شناخته‌شده در وزن‌هامنسوخ، جایگزینش ‏straw2‏
‏straw2‏‏O‏(n)بهینهبهینه (فقط جابه‌جایی به/از ‏item‏ تغییرکرده)پیش‌فرض
✎ یادداشت

شبیه‌سازی حذف، ‏bucket‏ را بدون ‏item‏ حذف‌شده از نو می‌سازد. در عمل، با ‏ceph osd crush reweight‏ به صفر یا ‏crush remove‏، رفتار به نوع ‏bucket‏ و جای ‏item‏ بستگی دارد. برای ‏straw2‏، صفر کردن وزن همان اثر حذف را دارد، چون هر ‏item‏ قرعه‌ی مستقل خودش را می‌کشد.

آزمایشگاه

زمین بازی CRUSH

یک root، چند host و OSD بسازید، قاعده را انتخاب کنید و ببینید straw2 هر PG را کجا می‌گذارد؛ یک OSD را out یا down کنید یا وزنش را تغییر دهید و ببینید کدام PGها جابه‌جا می‌شوند.

۶۴ × ۳PG × نسخه
۹٫۹٪داده‌ای که تغییر آخر جابه‌جا کرد
۱۹PGهایی که مجموعهٔ upشان عوض شد
۰PGهایی با نسخه‌ای کمتر از size
۰جای خالی (NONE)

تغییر آخر: osd.4 out شد. shardهای جابه‌جاشده در فهرست برجسته شده‌اند.

osd.0: 29 (≈24)029osd.1: 14 (≈24)114osd.2: 21 (≈24)221osd.3: 0 (≈0)30osd.4: 38 (≈24)438osd.5: 26 (≈24)526osd.6: 27 (≈24)627osd.7: 14 (≈24)714osd.8: 23 (≈24)823
shardهای PG روی هر OSD (ستون) در برابر سهمی که وزنش می‌دهد (خط). قرمز: out؛ کهربایی: down.
$ ceph osd map pool1 rbd_data.10226b8b4567.0000000000000000ceph_str_hash_rjenkins → 0x87bd7b9a · ceph_stable_mod(·, 64, 63) → pg 1.1a · pps 0x8d2145b9 · up [4,2,8] p4
PG ← مجموعهٔ up (۶۴ PG؛ primary زیرخط‌دار)
  1. 1.0[0,6,5]
  2. 1.1[7,4,0]
  3. 1.2[4,7,0]
  4. 1.3[4,8,2]
  5. 1.4[1,8,5]
  6. 1.5[4,8,0]
  7. 1.6[6,4,2]
  8. 1.7[1,7,5]
  9. 1.8[6,0,4]
  10. 1.9[8,4,2]
  11. 1.a[1,5,7]
  12. 1.b[1,8,5]
  13. 1.c[6,5,2]
  14. 1.d[7,4,2]
  15. 1.e[1,8,4]
  16. 1.f[6,2,4]
  17. 1.10[5,2,8]
  18. 1.11[4,8,0]
  19. 1.12[0,6,5]
  20. 1.13[4,6,1]
  21. 1.14[5,8,1]
  22. 1.15[8,5,2]
  23. 1.16[5,0,6]
  24. 1.17[8,0,5]
  25. 1.18[6,2,4]
  26. 1.19[6,0,4]
  27. 1.1a[4,2,8]
  28. 1.1b[8,1,4]
  29. 1.1c[8,4,1]
  30. 1.1d[6,0,5]
  31. 1.1e[8,4,0]
  32. 1.1f[8,5,2]
  33. 1.20[2,6,5]
  34. 1.21[6,0,4]
  35. 1.22[2,4,6]
  36. 1.23[0,5,7]
  37. 1.24[0,4,6]
  38. 1.25[8,5,0]
  39. 1.26[1,4,6]
  40. 1.27[1,7,5]
  41. 1.28[5,7,0]
  42. 1.29[8,2,4]
  43. 1.2a[5,2,6]
  44. 1.2b[8,0,4]
  45. 1.2c[7,0,4]
  46. 1.2d[2,7,4]
  47. 1.2e[6,2,4]
  48. 1.2f[0,4,6]
  49. 1.30[6,5,0]
  50. 1.31[0,6,4]
  51. 1.32[0,6,4]
  52. 1.33[4,6,2]
  53. 1.34[8,0,5]
  54. 1.35[8,2,4]
  55. 1.36[7,0,4]
  56. 1.37[4,7,0]
  57. 1.38[6,4,2]
  58. 1.39[0,5,6]
  59. 1.3a[0,8,4]
  60. 1.3b[4,1,6]
  61. 1.3c[5,6,0]
  62. 1.3d[5,8,2]
  63. 1.3e[5,1,7]
  64. 1.3f[7,4,1]

مطابق با:

  • src/crush/mapper.c :: crush_do_rule اجرای گام‌های قاعده (take، chooseleaf، emit)
  • src/crush/mapper.c :: crush_choose_firstn, crush_choose_indep حلقه‌های انتخاب replicated (firstn) و EC (indep) با تلاش دوباره
  • src/crush/mapper.c :: bucket_straw2_choose, generate_exponential_distribution, crush_ln straw2: بزرگ‌ترین ln(hash)/weight برنده است
  • src/crush/mapper.c :: is_out بررسی reweight که OSD خارج‌شده را رد می‌کند
  • src/crush/hash.c :: crush_hash32_rjenkins1_2, _3 hash جنکینز که CRUSH به کار می‌برد
  • src/osd/osd_types.cc :: pg_pool_t::raw_pg_to_pps, hash_key بذر PG که وارد CRUSH می‌شود
  • src/include/rados.h :: ceph_stable_mod نگاشت hash شیء به PG وقتی pg_num توانی از ۲ نیست
  • src/common/ceph_hash.cc :: ceph_str_hash_rjenkins hash نام شیء
  • src/osd/OSDMap.cc :: _pg_to_raw_osds, _raw_to_up_osds از مجموعهٔ خام تا مجموعهٔ up (OSDهای down حذف یا NONE می‌شوند)
  • src/crush/CrushWrapper.cc :: add_simple_rule_at, insert_item; CrushWrapper.h :: set_tunables_jewel قاعده‌ای که ceph osd pool create می‌سازد و tunables نسخهٔ jewel
تصویری ثابت از شبیه‌ساز؛ نسخهٔ تعاملی در آزمایشگاه نسخهٔ وب است. آزمایشگاه تعاملی: زمین بازی CRUSH

۲۸-۹ ‏balancer‏ در حالت ‏upmap: calc_pg_upmaps‏#

‏balancer‏ مربوط به ‏mgr‏ در حالت ‏upmap‏ (فصل ۱۹) کار اصلی را به ‏OSDMap::calc_pg_upmaps‏ در ‏C++‏ می‌سپارد. این تابع یک کپی از ‏OSDMap‏ می‌سازد، با جفت‌های ‏pg_upmap_items‏ آزمایش می‌کند و نتیجه را در یک ‏Incremental‏ برمی‌گرداند.

شکل ۲۸-۹: ساختار OSDMap::calc_pg_upmaps . پس از محاسبه‌ی PG های هر OSD و انحراف هر OSD از هدف وزنی، در هر دور مجموعه‌های overfull و underfull ساخته می‌شوند. سپس از پرترین OSD ، نخست حذف upmap های قبلی و بعد افزودن یک جفت تازه آزموده می‌شود. هر تغییر فقط وقتی پذیرفته می‌شود که امتیاز توازن بدتر نشود
شکل ۲۸-۹: ساختار ‏OSDMap::calc_pg_upmaps‏. پس از محاسبه‌ی ‏PG‏های هر ‏OSD‏ و انحراف هر ‏OSD‏ از هدف وزنی، در هر دور مجموعه‌های ‏overfull‏ و ‏underfull‏ ساخته می‌شوند. سپس از پرترین ‏OSD‏، نخست حذف ‏upmap‏های قبلی و بعد افزودن یک جفت تازه آزموده می‌شود. هر تغییر فقط وقتی پذیرفته می‌شود که امتیاز توازن بدتر نشود
∑ الگوریتم: انتخاب ‏upmap‏ حریصانه با آزمون امتیاز

منبع در کد: src/osd/OSDMap.cc :: OSDMap::calc_pg_upmaps، calc_deviations، fill_overfull_underfull، try_pg_upmap، find_best_remap

پیچیدگی: هر دور ‏O‏(OSD × ‏PG‏) برای پیمایش و یک محاسبه‌ی ‏CRUSH‏ برای هر نامزد؛ حداکثر ‏max‏ دور

text
ppw = total_pgs / Σ weight
dev(osd) = pgs(osd) − weight(osd) × ppw
bal_score = Σ weight × (dev / target)²                  # 0 یعنی کامل
repeat up to max times:
    overfull  = {dev > max_deviation};   more_overfull  = {0 < dev ≤ max_deviation}
    underfull = {dev < −max_deviation};  more_underfull = {−max_deviation ≤ dev < 0}
    if overfull and underfull both empty: break
    for osd in OSDs by dev descending (dev > max_deviation):
        if try_drop_remap_overfull(osd): goto test       # اول upmapهای قدیمی را پس بگیر
        for pg on osd (بدون pg_upmap دستی):
            out = try_pg_upmap(pg, overfull, underfull)   # CRUSH را با وزن صفر برای overfullها دوباره اجرا کن
            pos = find_best_remap(orig, out)            # بهترین یک جایگزینی
            if pos ≥ 0: add pair (orig[pos] → out[pos]); goto test
    for osd in underfull: try_drop_remap_underfull(osd) → test
    break
test: if new_bal_score > bal_score: reject (پرش از آن PG)  else: apply

مثال عددی. چهار ‏OSD‏ هم‌وزن، یک ‏pool‏ با ۶۴ ‏PG‏ و ‏size=3‏، پس ۱۹۲ نسخه و هدف ۴۸ برای هر ‏OSD‏. شمارش‌ها [۵۶، ۵۰، ۴۴، ۴۲] و انحراف‌ها [+۸، +۲، −۴، −۶] است. با ‏upmap_max_deviation=5‏ (پیش‌فرض ماژول ‏balancer‏): ‏overfull={osd.0}‏، ‏underfull={osd.3}‏. امتیاز = (۶۴ + ۴ + ۱۶ + ۳۶)/۴۸² = ۰.۰۵۲۱.

گامجفت ‏upmap‏شمارش‌هاانحراف‌ها‏bal_score‏
۰—۵۶، ۵۰، ۴۴، ۴۲+۸، +۲، −۴، −۶۰.۰۵۲۱
۱(۰ → ۳)۵۵، ۵۰، ۴۴، ۴۳+۷، +۲، −۴، −۵۰.۰۴۰۸
۲(۰ → ۳) برای ‏PG‏ دیگر۵۴، ۵۰، ۴۴، ۴۴+۶، +۲، −۴، −۴۰.۰۳۱۳
۳(۰ → ۲)۵۳، ۵۰، ۴۵، ۴۴+۵، +۲، −۳، −۴۰.۰۲۳۴

پس از گام ۳، هیچ ‏OSD‏ی بیش از ۵ انحراف ندارد، پس ‏overfull‏ و ‏underfull‏ خالی‌اند و حلقه پایان می‌یابد. از گام ۲ به بعد ‏underfull‏ خالی است و جایگزین‌ها از ‏more_underfull‏ می‌آیند. سه ورودی ‏pg_upmap_items‏ در ‏OSDMap‏ ثبت می‌شوند. هر تغییر فقط یک جفت اضافه می‌کند. توضیح کد می‌گوید این کار عمداً «آرام» است تا هر گام کوچک توزیع را واقعاً به سمت حالت کامل ببرد. فرض این مثال آن است که قانون ‏CRUSH‏ اجازه‌ی این جایگزینی‌ها را بدهد (مثلاً ‏OSD‏ها در ‏host‏های متفاوت باشند). اگر جایگزینی قانون ‏failure domain‏ را بشکند، ‏try_pg_upmap‏ آن را رد می‌کند.

✎ یادداشت

‏osd_calc_pg_upmaps_aggressively‏ (و نسخه‌ی ‏fast‏ آن) فهرست ‏PG‏ها را مخلوط و جست‌وجوی جامع‌تری می‌کند. در ‏main‏، ‏bal_score‏ با ‏double‏ جمع می‌شود. توضیح کد می‌گوید در ‏float‏، تغییر یک ‏PG‏ در خوشه‌های بزرگ ممکن بود کمتر از دقت مجموع باشد و ‏balancer‏ متوقف شود.

۲۸-۱۰ قانون‌های ‏MSR‏: چند ‏OSD‏ در هر ‏failure domain‏#

در ‏EC‏ با ‏k+m‏ بزرگ‌تر از تعداد ‏host‏ها (مثلاً ۴+۲ روی سه ‏host‏)، قانون کلاسیک ‏choose indep 3 type host‏ و سپس ‏chooseleaf indep 2 type osd‏ یک مشکل دارد. اگر یک ‏OSD out‏ شود، مرحله‌ی دوم فقط در همان ‏host‏ دنبال جایگزین می‌گردد. اگر آن ‏host OSD‏ سالم دیگری نداشته باشد، ‏shard‏ بی‌جا می‌ماند، چون ‏CRUSH‏ کلاسیک نمی‌تواند به مرحله‌ی قبل برگردد. قانون‌های ‏MSR‏ (‏Multi-Step Retry‏) از ‏Squid‏ با نوع‌های ‏msr_firstn‏ و ‏msr_indep‏ و گام ‏choosemsr‏ این را حل می‌کنند: کل نزول از ریشه دوباره تلاش می‌شود.

شکل ۲۸-۱۰: قانون MSR برای EC 4+2 روی سه host . خروجی شش‌خانه‌ای به سه stride دوتایی تقسیم می‌شود و host های هر stride از stride های دیگر جدا نگه داشته می‌شوند. وقتی osd.7 out است، شاخص ۳ در این دور خالی می‌ماند و در دور بعدی نزول با شماره‌ی تلاش تازه از ریشه تکرار می‌شود. جایگزین می‌تواند در همان host یا در host ی باشد که در stride دیگری استفاده نشده است
شکل ۲۸-۱۰: قانون ‏MSR‏ برای ‏EC 4+2‏ روی سه ‏host‏. خروجی شش‌خانه‌ای به سه ‏stride‏ دوتایی تقسیم می‌شود و ‏host‏های هر ‏stride‏ از ‏stride‏های دیگر جدا نگه داشته می‌شوند. وقتی ‏osd.7 out‏ است، شاخص ۳ در این دور خالی می‌ماند و در دور بعدی نزول با شماره‌ی تلاش تازه از ریشه تکرار می‌شود. جایگزین می‌تواند در همان ‏host‏ یا در ‏host‏ی باشد که در ‏stride‏ دیگری استفاده نشده است
∑ الگوریتم: نزول چندمرحله‌ای با تلاش دوباره (‏MSR‏)

منبع در کد: src/crush/mapper.c :: crush_msr_do_rule، crush_msr_choose، crush_msr_descend، crush_msr_get_retry_value

پیچیدگی: ‏O‏(msr_descents × ‏result_max‏ × عمق × اندازه‌ی ‏bucket‏) در بدترین حالت؛ پیش‌فرض ‏msr_descents‏ = ‏msr_collision_tries‏ = ۱۰۰

text
pseudocode
total = Π(arg1 of choosemsr steps)      # مثلاً 3 × 2 = 6
for descent in 0 .. msr_descents-1 while خروجی کامل نیست:
    crush_msr_choose(root, [0, total), step0, tryno = descent)
crush_msr_choose(bucket, [start, end), step):
    num_strides = step.arg1 (یا result_max اگر 0)
    stride_length = (end − start) / num_strides
    for each stride:
        if همه‌ی شاخص‌های stride پر شده‌اند: continue
        for local_try in 0 .. msr_collision_tries-1:
            r = ((descent × result_max + index) << 16) + local_try
            cand = crush_msr_descend(bucket, step.type, r)          # straw2 و ... تا نوع هدف
            if cand در stride دیگری استفاده شده (collision): continue
            if آخرین گام: اگر OSD out است ⇒ شاخص خالی بماند (undo پس از دور)
            else: crush_msr_choose(cand, [stride], step+1) برای سطح بعد

مثال عددی. با ‏choosemsr 3 host‏ و ‏choosemsr 2 osd‏، ‏total_descendants‏ = ۶، ‏num_strides‏ = ۳ و ‏stride_length‏ = ۲. در فراخوانی بازگشتی برای هر ‏host‏، ‏total_descendants‏ = ۲ و ‏stride_length‏ = ۱ است (این‌ها همان اعداد توضیح بالای ‏crush_msr_choose‏ در کد هستند). مقدار تلاش برای ‏descent=1‏، ‏result_max=6‏، ‏index=3‏ و ‏local_try=0‏ برابر ((۱×۶+۳) ≪ ۱۶) + ۰ = ۹ × ۶۵۵۳۶ = ۵۸۹۸۲۴ است. پس ‏hash‏ها در هر دور کاملاً متفاوت‌اند. تضمین گام ‏choosemsr 3 host‏ «دست‌کم» سه ‏host‏ است، نه «دقیقاً» سه: یک ‏stride‏ ممکن است پس از ‏out‏ شدن یک ‏OSD‏ از دو ‏host‏ استفاده کند، به شرط آنکه آن ‏host‏ها در ‏stride‏های دیگر نباشند.

⚠ هشدار

قانون‌های ‏MSR‏ به ویژگی ‏CRUSH_MSR‏ (بیت ۳۸ در ‏ceph_features.h‏) نیاز دارند و فقط کلاینت‌ها و ‏OSD‏هایی که آن را می‌شناسند (‏Squid‏ و بالاتر) می‌توانند نگاشت را حساب کنند. در زمان نگارش، ‏net/ceph/crush/mapper.c‏ در شاخه‌ی اصلی هسته‌ی لینوکس هیچ پیاده‌سازی ‏MSR‏ ندارد، پس ‏krbd‏ و ‏ceph.ko‏ نمی‌توانند از ‏pool‏ی با قانون ‏MSR‏ استفاده کنند. پیش از ساختن قانون ‏MSR‏، ‏ceph features‏ را بررسی کنید.

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

  • قواعد ‏MSR‏ (‏CRUSH_RULE_CHOOSE_MSR‏ و انواع ‏CRUSH_RULE_TYPE_MSR_FIRSTN‏ و ‏MSR_INDEP‏) در کد ‏Squid‏ و ‏main‏ وجود دارند و در ‏Reef‏ نیستند. هدف آن‌ها قرار دادن چند ‏shard‏ در یک ‏failure domain‏ با کنترل دقیق است (مثلاً ‏EC 8+6‏ روی ۴ ‏rack‏).
  • هسته‌ی لینوکس همین ‏mapper.c‏، ‏hash.c‏ و ‏crush_ln_table.h‏ را در ‏net/ceph/crush/‏ دارد. پشتیبانی آن از قابلیت‌های جدید ‏CRUSH‏ (مثل ‏MSR‏) به نسخه‌ی هسته بستگی دارد و قابلیت‌های خوشه باید با ‏ceph osd set-require-min-compat-client‏ هماهنگ شود.

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

  1. ۱. برنامه‌ی آزمایشی این فصل را بازسازی کنید: فایل‌های ‏hash.c‏ و ‏crush_ln_table.h‏ را از کد ‏Ceph‏ بردارید و ‏bucket_straw2_choose‏ را برای ‏bucket‏ی با پنج آیتم و وزن‌های دلخواه اجرا کنید. توزیع را برای یک میلیون ‏x‏ بسنجید.
  2. ۲. با ‏ceph osd map‏ چند شیء را نگاشت کنید و ‏ps‏ را با محاسبه‌ی دستی ‏ceph_stable_mod‏ تطبیق دهید.
  3. ۳. وزن یک ‏host‏ را در یک نقشه‌ی آزمایشی ۱۰٪ زیاد کنید و با ‏crushtool --compare‏ تعداد ‏PG‏های جابه‌جاشده و مقصد آن‌ها را ببینید (باید فقط به همان ‏host‏ منتقل شوند).
  4. ۴. یک ‏pg-upmap-items‏ دستی بسازید که دو ‏replica‏ را در یک ‏host‏ قرار دهد و ببینید ‏mon‏ با آن چه می‌کند.

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

  1. ۱. ‏ceph_stable_mod‏ چگونه حرکت داده را هنگام افزایش ‏pg_num‏ محدود می‌کند؟
  2. ۲. چرا ‏HASHPSPOOL‏ لازم است؟
  3. ۳. چرا ‏CRUSH‏ فقط از عملیات صحیح استفاده می‌کند؟
  4. ۴. در ‏straw2‏، ‏draw‏ چگونه محاسبه می‌شود و چرا بیشترین ‏draw‏ برنده است؟
  5. ۵. ‏crush_ln‏ چه چیزی را با چه دقتی برمی‌گرداند؟
  6. ۶. چرا ‏straw2‏ حرکت حداقلی دارد و ‏straw‏ قدیمی نداشت؟
  7. ۷. تصادم در ‏firstn‏ چگونه حل می‌شود و سقف آن چیست؟
  8. ۸. تفاوت ‏firstn‏ و ‏indep‏ برای ‏EC‏ چیست؟
  9. ۹. ‏weight-set‏ در ‏choose_args‏ چه مشکلی را حل می‌کند؟
  10. ۱۰. ترتیب اعمال ‏upmap‏ها چیست و ‏mon‏ با ‏upmap‏ نامعتبر چه می‌کند؟

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

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

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

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