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

لایه‌های پایینی (۵): ریاضیات ‏erasure coding‏، از ‏GF‏(۲⁸) تا ‏FastEC‏

۴۴ دقیقه مطالعهویرایش این صفحه
حوصله ندارم این فصل در چند خط
  • ‏erasure coding‏ در ‏Ceph‏ حساب در میدان ‏GF‏(۲⁸) است: هر بایت یک عضو میدان است، جمع همان ‏XOR‏ است و ضرب با جدول لگاریتم انجام می‌شود.
  • ‏encode‏ تکه‌های داده را در یک ماتریس مولد ضرب می‌کند؛ ماتریس‌های ‏Reed‏–‏Solomon‏ تضمین می‌کنند هر ‏k‏ تکه برای ‏decode‏ کافی باشد (ویژگی ‏MDS‏).
  • ‏decode‏ پس از خرابی، سطرهای متناظر ماتریس را وارون می‌کند؛ به‌روزرسانی ‏parity-delta‏ بدون خواندن کل ‏stripe‏، ‏parity‏ را عوض می‌کند.
  • ‏FastEC‏ در ‏Tentacle‏ همین را برای خواندن و نوشتن جزئی، پشت ‏allow_ec_optimizations‏، به کار می‌برد.

۲۷-۱ مقدمه: از ‏XOR‏ تا ‏Reed-Solomon‏#

در فصل ۱۳ دیدیم یک ‏pool‏ با ‏erasure coding‏ چگونه ساخته می‌شود و پروفایل‌ها چه معنایی دارند. این فصل یک لایه پایین‌تر می‌رود: ریاضیات میدان متناهی ‏GF(28)\mathrm{GF}(2^8)‏، ساخت ماتریس تولید ‏Reed-Solomon‏ (به روش ‏Vandermonde‏ و ‏Cauchy‏)، پیاده‌سازی ‏SIMD‏ در ‏ISA-L‏، و این‌که ‏Ceph‏ چگونه شیء را به ‏stripe‏ و ‏chunk‏ تقسیم می‌کند، نوشتن جزئی را با ‏read-modify-write‏ انجام می‌دهد و در ‏Tentacle‏ با ‏EC‏ بهینه‌شده (که معمولاً ‏FastEC‏ نامیده می‌شود) چه چیزی تغییر کرده است. یک مثال عددی کامل ‏encode‏ و ‏decode‏ هم با دست حل می‌کنیم.

در این بخش
  • همه‌ی پلاگین‌های اصلی (‏isa‏، ‏jerasure‏ و پایه‌ی ‏clay‏) در ‏GF(28)\mathrm{GF}(2^8)‏ کار می‌کنند. در این میدان جمع برابر ‏XOR‏ است و ضرب با جدول ‏log/exp‏ یا ‏SIMD‏ انجام می‌شود.
  • پروفایل پیش‌فرض در ‏Tentacle‏ و ‏main‏ ‏plugin=isa technique=reed_sol_van k=2 m=2‏ است. در ‏Squid‏ پیش‌فرض ‏plugin=jerasure‏ بود.
  • در ‏ISA-L‏، ‏Vandermonde‏ فقط برای ‏m‏ ≤ ۴ (و برای ‏m=4‏ تنها با ‏k‏ ≤ ۲۱) ‏MDS‏ تضمین‌شده است. در غیر این صورت ‏Ceph‏ خودکار به ‏cauchy‏ تغییر می‌دهد.
  • ‏stripe_unit‏ پیش‌فرض ۴‏K‏ است و برای ‏pool‏های با ‏allow_ec_optimizations‏ برابر ۱۶‏K‏ می‌شود.
  • ‏EC‏ بهینه در ‏Tentacle‏ (‏ECSwitch‏ بین ‏backend‏ قدیمی و جدید) خواندن جزئی مستقیم و به‌روزرسانی ‏parity‏ با ‏delta‏ را ممکن می‌کند. پس از فعال شدن، غیرفعال‌کردنی نیست.

۲۷-۲ ‏GF‏(۲⁸): حساب بایت‌ها#

‏erasure coding‏ باید روی بایت‌ها «ضرب و تقسیم» کند، به‌طوری که هر عدد غیرصفر معکوس داشته باشد و نتیجه همیشه یک بایت بماند. میدان گالوا ‏GF(28)\mathrm{GF}(2^8)‏ دقیقاً همین را فراهم می‌کند. عناصر آن ۲۵۶ بایت‌اند که به‌صورت چندجمله‌ای‌هایی از درجه‌ی کمتر از ۸ با ضرایب ۰ و ۱ دیده می‌شوند.

  • جمع و تفریق: هر دو ‏XOR‏ هستند، پس ‏a‏ + ‏a‏ = ۰.
  • ضرب: ضرب چندجمله‌ای‌ها و سپس باقیمانده بر یک چندجمله‌ای تحویل‌ناپذیر درجه ۸ گرفته می‌شود. ‏ISA-L‏ از ۰‏x11D‏ (x⁸ + ‏x‏⁴ + ‏x‏³ + ‏x‏² + ۱) استفاده می‌کند: در هر شیفت به چپ، اگر بیت ۸ روشن شد، با ۰‏x1D XOR‏ می‌شود (در ‏ec_base.c‏ همین الگو برای ضرب در ۲ دیده می‌شود).
  • جدول ‏log/exp‏: چون عدد ۲ مولد گروه ضربی است، هر بایت غیرصفر برابر ‏2i2^i‏ است. پس ‏a⋅b=exp⁡[(log⁡a+log⁡b) mod 255]a \cdot b = \exp[(\log a + \log b) \bmod 255]‏ و ‏a−1=exp⁡[255−log⁡a]a^{-1} = \exp[255 - \log a]‏.
ضرب نمونه در ‏GF(28)\mathrm{GF}(2^8)‏ با ۰‏x11D‏نتیجهشرح
۰‏x02‏ × ۰‏x04‏۰‏x08‏بدون سرریز؛ فقط شیفت
۰‏x34‏ × ۰‏x02‏۰‏x68‏۰‏x34‏ << ۱ = ۰‏x68‏
۰‏x56‏ × ۰‏x04‏۰‏x45‏۰‏x56‏ << ۲ = ۰‏x158‏ → بیت ۸ روشن → ۰‏x58‏ ⊕ ۰‏x1D‏ = ۰‏x45‏
۰‏x78‏ × ۰‏x08‏۰‏xE7‏سه شیفت با دو سرریز
شکل ۲۷-۱: سه لایه‌ی محاسبه. در بالا، حساب GF (۲⁸): جمع برابر XOR است و ضرب با شیفت و XOR با ۰ x1D هنگام سرریز، یا با جدول log/exp انجام می‌شود. در وسط، ماتریس تولید سیستماتیک ISA-L برای k=4 و m=2 به روش Vandermonde : چهار سطر اول همانی‌اند (داده دست‌نخورده می‌ماند)، سطر P0 همه ۱ است (همان XOR ساده) و سطر P1 توان‌های ۲ است. در پایین، پیاده‌سازی SIMD: ec_init_tables برای هر ضریب دو جدول ۱۶ خانه‌ای ( nibble پایین و بالا) می‌سازد و دستور PSHUFB با این جدول‌ها ۳۲ یا ۶۴ بایت را در یک دستور ضرب می‌کند
شکل ۲۷-۱: سه لایه‌ی محاسبه. در بالا، حساب ‏GF‏(۲⁸): جمع برابر ‏XOR‏ است و ضرب با شیفت و ‏XOR‏ با ۰‏x1D‏ هنگام سرریز، یا با جدول ‏log/exp‏ انجام می‌شود. در وسط، ماتریس تولید سیستماتیک ‏ISA-L‏ برای ‏k=4‏ و ‏m=2‏ به روش ‏Vandermonde‏: چهار سطر اول همانی‌اند (داده دست‌نخورده می‌ماند)، سطر ‏P0‏ همه ۱ است (همان ‏XOR‏ ساده) و سطر ‏P1‏ توان‌های ۲ است. در پایین، پیاده‌سازی ‏SIMD: ec_init_tables‏ برای هر ضریب دو جدول ۱۶ خانه‌ای (‏nibble‏ پایین و بالا) می‌سازد و دستور ‏PSHUFB‏ با این جدول‌ها ۳۲ یا ۶۴ بایت را در یک دستور ضرب می‌کند

۲۷-۳ ‏Reed-Solomon‏ سیستماتیک#

در یک کد سیستماتیک (‏k‏، ‏m‏)، ‏k chunk‏ داده بدون تغییر ذخیره و ‏m chunk parity‏ از آن‌ها محاسبه می‌شود. اگر ‏D‏ بردار داده باشد، ‏chunk‏ها از ضرب ماتریس تولید ‏G‏ با اندازه‌ی (‏k+m‏)×‏k‏ در ‏D‏ به دست می‌آیند: ‏C‏ = ‏G‏ · ‏D‏. چهار سطر اول ‏G‏ همانی‌اند. کد ‏MDS‏ است، یعنی هر ‏k chunk‏ از ‏k+m chunk‏ برای بازسازی کافی است، به شرط این‌که هر زیرماتریس ‏k‏×‏k‏ از ‏G‏ معکوس‌پذیر باشد.

‏gf_gen_rs_matrix‏ در ‏ISA-L‏ سطر ‏i‏ ام ‏parity‏ را با توان‌های ‏gen=2i−k\mathit{gen} = 2^{i-k}‏ می‌سازد. این یک ماتریس «‏Vandermonde‏» ساده است که برای همه‌ی (‏k‏، ‏m‏)ها ‏MDS‏ نیست. به همین دلیل ‏ErasureCodeIsa.cc‏ اگر ‏m‏ > ۴ باشد یا ‏m‏ = ۴ و ‏k‏ > ۲۱، با پیامی در لاگ به ‏gf_gen_cauchy1_matrix‏ تغییر می‌دهد که درایه‌هایش ۱/(‏i‏ ⊕ ‏j‏) هستند و همیشه ‏MDS‏‌اند. سقف ‏k‏ هم ‏MAX_K = 32‏ است.

ماتریس ‏parity‏ برای ‏k=4‏، ‏m=2‏سطر ‏P0‏سطر ‏P1‏
‏ISA-L reed_sol_van‏۱ ۱ ۱ ۱۱ ۲ ۴ ۸
‏ISA-L cauchy‏۰‏x47 0xA7 0x7A 0xBA‏۰‏xA7 0x47 0xBA 0x7A‏

۲۷-۳-۱ مثال کامل: ‏encode‏ یک ‏stripe‏ کوچک#

فرض کنید هر ‏chunk‏ فقط یک بایت دارد و داده ‏D‏ = (۰‏x12, 0x34, 0x56, 0x78‏) است (در واقع همین محاسبه برای هر بایت از ‏chunk‏های ۴‏K‏ تکرار می‌شود):

text
P0 = 1·12 ⊕ 1·34 ⊕ 1·56 ⊕ 1·78 = 12 ⊕ 34 ⊕ 56 ⊕ 78           = 0x08
P1 = 1·12 ⊕ 2·34 ⊕ 4·56 ⊕ 8·78 = 12 ⊕ 68 ⊕ 45 ⊕ E7           = 0xD8
chunks = [12, 34, 56, 78 | 08, D8]   (shard 0..5)

۲۷-۳-۲ ‏decode‏: از دست رفتن دو ‏shard‏#

فرض کنید ‏OSD‏های حاوی ‏D1‏ و ‏D3‏ از دست رفته‌اند و ‏shard‏های سالم {‏D0‏، ‏D2‏، ‏P0‏، ‏P1}‏ هستند. سطرهای متناظر از ‏G‏ یک ماتریس ۴×۴ می‌سازند. معکوس آن در ‏GF(28)\mathrm{GF}(2^8)‏ حساب می‌شود (‏ISA-L‏ از ‏gf_invert_matrix‏ استفاده می‌کند) و سطرهای مربوط به داده‌های گم‌شده با بایت‌های سالم ضرب می‌شوند:

text
D1 = 7B·D0 ⊕ F5·D2 ⊕ A6·P0 ⊕ DD·P1 = 0x34   ✓
D3 = 7A·D0 ⊕ F4·D2 ⊕ A7·P0 ⊕ DD·P1 = 0x78   ✓

همه‌ی اعداد این دو بخش با یک پیاده‌سازی پایتونی مستقل از ‏GF(28)\mathrm{GF}(2^8)‏ با ۰‏x11D‏ و همان الگوریتم ساخت ماتریس ‏ISA-L‏ محاسبه و بررسی شده‌اند. ضرایب ‏decode‏ فقط یک بار برای هر الگوی خرابی حساب می‌شوند و پلاگین ‏isa‏ آن‌ها را در ‏ErasureCodeIsaTableCache‏ نگه می‌دارد، چون معکوس کردن ماتریس از ‏encode‏ گران‌تر است.

◆ عمیق‌تر

چرا ‏SIMD‏ این‌قدر سریع است؟ ضرب یک بایت ‏x‏ در ثابت ‏c‏ را می‌توان به دو ‏nibble‏ شکست: ‏c‏·‏x‏ = ‏c‏·(‏x_hi‏ << ۴) ⊕ ‏c‏·‏x_lo‏. هر کدام فقط ۱۶ حالت دارد، پس دو جدول ۱۶ بایتی کافی است. دستور ‏PSHUFB‏ (یا ‏VPSHUFB‏ در ‏AVX2/AVX-512‏) می‌تواند ۱۶، ۳۲ یا ۶۴ جست‌وجوی هم‌زمان در یک جدول ۱۶ بایتی انجام دهد. ‏ISA-L‏ این جدول‌ها را با ‏ec_init_tables‏ برای همه‌ی ضرایب می‌سازد و ‏ec_encode_data‏ برای هر بلوک ورودی، ضرب و ‏XOR‏ انباشتی را با این دستورها اجرا می‌کند. نتیجه، سرعت ‏encode‏ در حد چند گیگابایت در ثانیه در هر هسته است. ‏jerasure‏ (با ‏gf-complete‏) از روش مشابهی استفاده می‌کند، ولی ‏ISA-L‏ روی ‏x86‏ معمولاً سریع‌تر است. پلاگین ‏isa‏ روی ‏Arm‏ هم با کدهای ‏NEON‏ و ‏SVE‏ مربوط به ‏ISA-L‏ کار می‌کند.

۲۷-۴ ‏stripe‏، ‏chunk‏ و چیدمان روی ‏OSD‏#

شکل ۲۷-۲: تقسیم یک شیء ۶۴ K در pool با k=4 ، m=2 و stripe_unit=4K. stripe_width برابر ۱۶ K است، پس شیء چهار stripe دارد. در هر stripe ، چهار ناحیه‌ی ۴ K پشت‌سرهم به shard های داده ۰ تا ۳ می‌روند و دو parity محاسبه و در shard های ۴ و ۵ نوشته می‌شوند. هر OSD یک «شیء shard » با shard_id خودش و اندازه‌ی ۱۶ K نگه می‌دارد
شکل ۲۷-۲: تقسیم یک شیء ۶۴‏K‏ در ‏pool‏ با ‏k=4‏، ‏m=2‏ و ‏stripe_unit=4K. stripe_width‏ برابر ۱۶‏K‏ است، پس شیء چهار ‏stripe‏ دارد. در هر ‏stripe‏، چهار ناحیه‌ی ۴‏K‏ پشت‌سرهم به ‏shard‏های داده ۰ تا ۳ می‌روند و دو ‏parity‏ محاسبه و در ‏shard‏های ۴ و ۵ نوشته می‌شوند. هر ‏OSD‏ یک «شیء ‏shard‏» با ‏shard_id‏ خودش و اندازه‌ی ۱۶‏K‏ نگه می‌دارد
مفهومتعریفپیش‌فرض
‏stripe_unit‏ (‏chunk size‏)بایت‌های داده‌ی یک ‏shard‏ در یک ‏stripe‏۰ در ‏osd_pool_erasure_code_stripe_unit‏ یعنی ۴‏K‏، یا ۱۶‏K‏ اگر ‏pool‏ بهینه باشد و پلاگین پشتیبانی کند
‏stripe_width‏‏k‏ × ‏stripe_unit‏برای ‏k=4: 16K‏ یا ۶۴‏K‏
شیء ‏shard‏‏ghobject_t‏ با ‏shard_id‏ برابر ۰..‏k+m‏−۱روی ‏OSD‏ با کلید ‏onode‏ دارای ‏shard‏ + ۰‏x80‏ (فصل ۲۴)
✎ یادداشت

‏ceph osd pool create‏ اگر ‏stripe_unit‏ مضرب ۴‏K‏ نباشد، بدون ‏--yes-i-really-mean-it‏ پروفایل را رد می‌کند. انتخاب ‏stripe_unit‏ بزرگ‌تر یعنی نوشتن‌های کوچک‌تر از یک ‏stripe‏ بیشتر، ولی خواندن ترتیبی کارآمدتر. با ‏EC‏ بهینه این بده‌بستان به سمت ۱۶‏K‏ رفته است، چون نوشتن جزئی دیگر کل ‏stripe‏ را نمی‌خواند.

۲۷-۵ ‏read-modify-write‏ در ‏EC‏ قدیمی#

تا ‏Squid‏، و در ‏pool‏های بدون بهینه‌سازی در نسخه‌های بعدی، ‏backend‏ مربوط به ‏EC‏ برای هر نوشتنی که کل ‏stripe‏ را نپوشاند و روی داده‌ی موجود باشد (‏allow_ec_overwrites=true‏، لازم برای ‏RBD‏ و ‏CephFS‏) این کارها را انجام می‌دهد:

text
pseudocode: overwrite جزئی در ECBackend قدیمی (خلاصه‌ی این کتاب)
  extent = round_out(write, stripe_width)              # گسترش به مرز stripe
  if cache miss: read k chunks of extent from shards   # چند رفت‌وبرگشت شبکه
  merge new bytes into stripe buffer
  encode → m parity chunks for whole stripe
  send sub-writes to all k+m shards (هر shard، تراکنش خودش)
  wait all commits → reply
شکل ۲۷-۳: نوشتن جزئی ۴ K در مقایسه‌ی دو backend . در EC قدیمی، primary کل stripe را از k shard می‌خواند، داده‌ی جدید را ادغام، دوباره encode و همه‌ی shard ها را بازنویسی می‌کند. در EC بهینه ( Tentacle با allow_ec_optimizations )، فقط همان ۴ K قدیمی و parity ها خوانده می‌شوند، delta برابر قدیم ⊕ جدید محاسبه و هر parity با ضریب مربوط در delta به‌روز می‌شود. خواندن کوچک هم به‌جای decode از k shard ، مستقیم از یک shard داده انجام می‌شود
شکل ۲۷-۳: نوشتن جزئی ۴‏K‏ در مقایسه‌ی دو ‏backend‏. در ‏EC‏ قدیمی، ‏primary‏ کل ‏stripe‏ را از ‏k shard‏ می‌خواند، داده‌ی جدید را ادغام، دوباره ‏encode‏ و همه‌ی ‏shard‏ها را بازنویسی می‌کند. در ‏EC‏ بهینه (‏Tentacle‏ با ‏allow_ec_optimizations‏)، فقط همان ۴‏K‏ قدیمی و ‏parity‏ها خوانده می‌شوند، ‏delta‏ برابر قدیم ⊕ جدید محاسبه و هر ‏parity‏ با ضریب مربوط در ‏delta‏ به‌روز می‌شود. خواندن کوچک هم به‌جای ‏decode‏ از ‏k shard‏، مستقیم از یک ‏shard‏ داده انجام می‌شود

چرا ‏parity delta‏ درست است؟ چون کد خطی است: ‏Pi=∑jgij⋅DjP_i = \sum_j g_{ij} \cdot D_j‏. اگر فقط ‏DjD_j‏ به ‏Dj′D_j'‏ تغییر کند، ‏Pi′=Pi⊕gij⋅(Dj⊕Dj′)P_i' = P_i \oplus g_{ij} \cdot (D_j \oplus D_j')‏. با مثال بخش قبل، اگر ‏D1‏ از ۰‏x34‏ به ۰‏x35‏ تغییر کند، ‏delta‏ برابر ۰‏x01‏ است، پس ‏P0‏′ = ۰‏x08‏ ⊕ ۱·۰‏x01‏ = ۰‏x09‏ و ‏P1‏′ = ۰‏xD8‏ ⊕ ۲·۰‏x01‏ = ۰‏xDA‏. نیازی به خواندن ‏D0‏، ‏D2‏ و ‏D3‏ نیست.

پرچم پلاگین (‏ErasureCodeInterface.h‏)معنی
‏FLAG_EC_PLUGIN_PARTIAL_READ_OPTIMIZATION‏کد سیستماتیک است؛ خواندن کوچک مستقیم از ‏shard‏ داده
‏FLAG_EC_PLUGIN_PARTIAL_WRITE_OPTIMIZATION‏نوشتن کوچک‌تر از یک ‏chunk‏ فقط همان قطعه را از هر ‏shard‏ داده لازم دارد
‏FLAG_EC_PLUGIN_ZERO_INPUT_ZERO_OUTPUT_OPTIMIZATION‏داده‌ی تماماً صفر ‏parity‏ صفر می‌دهد (برای ناحیه‌های خالی)
‏FLAG_EC_PLUGIN_ZERO_PADDING_OPTIMIZATION‏بافرهای با طول صفر مجازند
‏FLAG_EC_PLUGIN_PARITY_DELTA_OPTIMIZATION‏به‌روزرسانی ‏parity‏ با ‏delta‏
‏FLAG_EC_PLUGIN_REQUIRE_SUB_CHUNKS‏پلاگین ‏sub-chunk‏ لازم دارد (‏CLAY‏)
‏FLAG_EC_PLUGIN_OPTIMIZED_SUPPORTED‏می‌تواند در ‏pool‏ بهینه استفاده شود: ‏isa‏، و ‏jerasure‏ فقط با ‏reed_sol_van‏
bashTentacle+
# Tentacle و بالاتر:
ceph osd erasure-code-profile set ec42 plugin=isa technique=reed_sol_van k=4 m=2 crush-failure-domain=host
ceph osd pool create ecdata erasure ec42
ceph osd pool set ecdata allow_ec_overwrites true
ceph osd pool set ecdata allow_ec_optimizations true     # برگشت‌ناپذیر
ceph osd pool get ecdata all | grep -E 'ec_optim|stripe'
⚠ هشدار

‏FLAG_EC_OPTIMIZATIONS‏ در ‏osd_types.h‏ با این توضیح تعریف شده است: «‏once enabled, cannot be disabled‏». پیش از فعال کردن آن روی ‏pool‏ تولیدی، مطمئن شوید همه‌ی ‏OSD‏ها به نسخه‌ای ارتقا یافته‌اند که آن را پشتیبانی می‌کند. آن را ابتدا روی یک ‏pool‏ آزمایشی امتحان کنید. ‏osd_pool_default_flag_ec_optimizations‏ (پیش‌فرض ‏false‏) فعال شدن خودکار آن را برای ‏pool‏های جدید کنترل می‌کند.

۲۷-۶ کدهای با پهنای باند ترمیم کمتر#

در ‏RS‏، بازسازی یک ‏shard‏ گم‌شده به خواندن ‏k chunk‏ کامل نیاز دارد. برای ‏k=8‏، این یعنی ۸ برابر داده‌ی از دست رفته از شبکه می‌گذرد. سه پلاگین ‏Ceph‏ این هزینه را به روش‌های متفاوت کم می‌کنند:

شکل ۲۷-۴: ترافیک بازسازی یک shard به اندازه‌ی S. RS با k=4 به ۴ S نیاز دارد. LRC با l=3 فقط از گروه محلی (۳ chunk ) می‌خواند. CLAY با d=5 از ۵ helper ، هرکدام کسر ۱/( d − k+1 ) = ۱/۲ از chunk را می‌خواند، یعنی ۲.۵ S. SHEC parity ها را روی زیرمجموعه‌های هم‌پوشان داده می‌سازد تا خرابی تکی با خواندن کمتر از k ترمیم شود
شکل ۲۷-۴: ترافیک بازسازی یک ‏shard‏ به اندازه‌ی ‏S. RS‏ با ‏k=4‏ به ۴‏S‏ نیاز دارد. ‏LRC‏ با ‏l=3‏ فقط از گروه محلی (۳ ‏chunk‏) می‌خواند. ‏CLAY‏ با ‏d=5‏ از ۵ ‏helper‏، هرکدام کسر ۱/(‏d‏−‏k+1‏) = ۱/۲ از ‏chunk‏ را می‌خواند، یعنی ۲.۵‏S. SHEC parity‏ها را روی زیرمجموعه‌های هم‌پوشان داده می‌سازد تا خرابی تکی با خواندن کمتر از ‏k‏ ترمیم شود
پلاگینپارامترها (پیش‌فرض)ایدهبده‌بستان
‏lrc‏‏k‏، ‏m‏، ‏l‏افزودن ‏parity‏ محلی برای هر ‏l chunk‏؛ معمولاً هر گروه در یک ‏rack‏سربار فضای بیشتر: (‏k+m‏)/‏k‏ + (‏k+m‏)/(‏l‏·‏k‏)
‏clay‏‏k=4‏، ‏m=2‏، ‏d=k+m‏−۱ (پیش‌فرض)کد ‏coupled-layer‏ با ‏sub-chunk‏: ترمیم با خواندن کسری از ‏d helper‏ترمیم کمینه (‏MSR‏)؛ تعداد ‏sub-chunk‏ برابر ‏qtq^t‏ است که با ‏k+m‏ به‌سرعت بزرگ می‌شود و خواندن‌ها ریز می‌شوند
‏shec‏‏k=4‏، ‏m=3‏، ‏c=2‏هر ‏parity‏ فقط بخشی از داده را می‌پوشاند؛ ‏c‏ = تعداد خرابی تحمل‌پذیر تضمینیکارایی فضا بین ‏RS‏ و ‏replication‏؛ ترمیم تکی ارزان‌تر

مثال عددی ‏CLAY‏: پهنای باند ترمیم برای یک ‏shard‏ به اندازه‌ی ‏S‏ برابر ‏d⋅S/(d−k+1)d \cdot S / (d - k + 1)‏ است. با ‏k=8‏، ‏m=4‏ و ‏d=11‏، این مقدار ۱۱‏S/4‏ = ۲.۷۵‏S‏ است، در حالی که ‏RS‏ معادل ۸‏S‏ می‌خواند (حدود ۶۶٪ صرفه‌جویی). ولی ‏q‏ = ‏d‏ − ‏k‏ + ۱ = ۴ و ‏t‏ = (‏k+m‏)/‏q‏ = ۳، پس هر ‏chunk‏ به ‏43=644^3 = 64‏ ‏sub-chunk‏ تقسیم می‌شود. با ‏stripe_unit‏ برابر ۴‏K‏، ‏sub-chunk‏ها ۶۴ بایتی می‌شوند و خواندن ترمیمی به ده‌ها خواندن کوچک و پراکنده تبدیل می‌شود. برای همین ‏CLAY‏ با ‏stripe_unit‏ بزرگ‌تر معنا پیدا می‌کند.

۲۷-۷ ‏crc‏ و یکپارچگی در ‏EC‏#

در ‏EC‏ قدیمی، برای ‏pool‏های ‏append-only‏ (بدون ‏overwrite‏)، ‏hash‏ هر ‏shard‏ (‏hinfo_key‏ در ‏xattr‏) نگه داشته می‌شد و ‏scrub‏ عمیق آن را بررسی می‌کرد. با ‏allow_ec_overwrites‏، این ‏hash‏ دیگر به‌روز نمی‌شود و یکپارچگی بر ‏checksum‏های ‏BlueStore‏ در هر ‏shard‏ (فصل ۲۳) تکیه دارد. ‏scrub‏ عمیق در ‏EC‏ هر ‏shard‏ را می‌خواند و سازگاری ‏parity‏ را با ‏encode‏ دوباره بررسی می‌کند. پرچم ‏FLAG_EC_PLUGIN_CRC_ENCODE_DECODE_SUPPORT‏ در ‏main‏ نشان می‌دهد پلاگین می‌تواند ‏crc‏ را از روی ‏crc‏ داده‌ها محاسبه کند، یعنی خاصیت خطی بودن ‏crc‏ را با کد ترکیب کند. این بخشی از کارهای در جریان روی ‏scrub‏ سریع‌تر است. پیش از تکیه بر آن، نسخه‌ی خود را بررسی کنید.

bashReef+
ceph osd erasure-code-profile get default
ceph osd pool ls detail | grep -E 'erasure|stripe_width'
ceph pg <pgid> query | jq '.info.stats.stat_sum.num_objects_degraded'
ceph tell osd.4 config get osd_recovery_max_active_hdd

۲۷-۸ حساب ‏GF‏(۲⁸) در سطح دستور: جدول‌های ‏log/exp‏ و ترفند نیم‌بایت#

بخش‌های پیشین ضرب در ‏GF(28)\mathrm{GF}(2^8)‏ را تعریف کردند. این بخش نشان می‌دهد ‏ISA-L‏، که پلاگین ‏isa‏ در ‏Ceph‏ بر آن تکیه دارد، این ضرب را در حلقه‌ی داغ چگونه انجام می‌دهد. چندجمله‌ی کاهش در ‏ISA-L‏ همان ۰‏x11D‏ (یعنی ‏x8+x4+x3+x2+1x^8+x^4+x^3+x^2+1‏) است. در کد این عدد به شکل ‏0x1d‏ دیده می‌شود: هر بار که بیت ۷ پیش از شیفت روشن باشد، حاصل با ۰‏x1d XOR‏ می‌شود.

∑ الگوریتم: ضرب با جدول ‏log/exp‏

منبع در کد: isa-l/erasure_code/ec_base.c :: gf_mul، gf_inv

پیچیدگی: ‏O‏(۱): دو جست‌وجو، یک جمع و یک مقایسه؛ جدول‌ها ۲۵۶ + ۲۵۶ بایت

text
pseudocode
# ساخت یک‌باره: exp[i] = 2^i و log[2^i] = i برای i در 0..254
x = 1
for i in 0..254: exp[i] = x; log[x] = i; x = x·2 (یعنی x≪1 و اگر سرریز شد ⊕ 0x11D)
gf_mul(a, b): if a == 0 or b == 0: return 0
              i = log[a] + log[b]; if i > 254: i −= 255
              return exp[i]
gf_inv(a):   return exp[255 − log[a]]

مثال عددی. ‏exp[0..15]‏ = ۰۱ ۰۲ ۰۴ ۰۸ ۱۰ ۲۰ ۴۰ ۸۰ ۱‏D 3A 74 E8 CD 87 13 26‏. پس ‏28=0x1D2^8 = \mathtt{0x1D}‏ است، یعنی همان سرریز اول. برای ۰‏x53‏ × ۰‏xCA‏ داریم ‏log[0x53]‏ = ۲۰۶ و ‏log[0xCA]‏ = ۷۳. جمع آن‌ها ۲۷۹ است و چون از ۲۵۴ بزرگ‌تر است، ۲۷۹ − ۲۵۵ = ۲۴ و ‏exp[24]‏ = ۰‏x8F‏. وارون ۰‏x02‏ برابر ‏exp[255‏ − ۱] = ۰‏x8E‏ است. آزمون: ۰‏x8E‏ × ۲ = ۰‏x11C‏ و ۰‏x11C‏ ⊕ ۰‏x11D‏ = ۰‏x01‏.

این روش برای هر بایت دو دسترسی تصادفی به جدول و یک شاخه دارد و در حلقه‌ی داغ کند است. برای همین ‏ISA-L‏ جدول دیگری می‌سازد که با دستورهای ‏SIMD‏ سازگار است.

شکل ۲۷-۵: جدول ۳۲ بایتی که gf_vect_mul_init_base برای ضریب c=0x02 می‌سازد. ۱۶ خانه‌ی اول حاصل‌ضرب c در نیم‌بایت پایین (۰ تا F ) هستند و ۱۶ خانه‌ی دوم حاصل‌ضرب c در نیم‌بایت بالا (۰ x00 تا ۰ xF0 ). ضرب هر بایت با دو جست‌وجو و یک XOR انجام می‌شود، چون ضرب در GF (۲⁸) نسبت به XOR خطی است: c ·( hi ⊕ lo ) = c · hi ⊕ c · lo . خانه‌های زرد همان دو خانه‌ای‌اند که برای بایت ۰ xB7 خوانده می‌شوند
شکل ۲۷-۵: جدول ۳۲ بایتی که ‏gf_vect_mul_init_base‏ برای ضریب ‏c=0x02‏ می‌سازد. ۱۶ خانه‌ی اول حاصل‌ضرب ‏c‏ در نیم‌بایت پایین (۰ تا ‏F‏) هستند و ۱۶ خانه‌ی دوم حاصل‌ضرب ‏c‏ در نیم‌بایت بالا (۰‏x00‏ تا ۰‏xF0‏). ضرب هر بایت با دو جست‌وجو و یک ‏XOR‏ انجام می‌شود، چون ضرب در ‏GF‏(۲⁸) نسبت به ‏XOR‏ خطی است: ‏c‏·(‏hi‏ ⊕ ‏lo‏) = ‏c‏·‏hi‏ ⊕ ‏c‏·‏lo‏. خانه‌های زرد همان دو خانه‌ای‌اند که برای بایت ۰‏xB7‏ خوانده می‌شوند

دلیل درستی ترفند این است که هر بایت ‏d‏ برابر (‏d AND 0xF0‏) ⊕ (‏d AND 0x0F‏) است و ضرب در ‏c‏ روی ⊕ پخش می‌شود. پس به جای جدول ۲۵۶ خانه‌ای برای هر ‏c‏، دو جدول ۱۶ خانه‌ای کافی است. ۱۶ خانه دقیقاً اندازه‌ی یک ثبات ‏XMM‏ است و دستور ‏PSHUFB‏ با همین اندازه کار می‌کند: هر بایت از ثبات دوم را شاخصی در ثبات اول می‌گیرد.

∑ الگوریتم: ضرب برداری با جدول نیم‌بایت (‏split-table‏)

منبع در کد: isa-l/erasure_code/ec_base.c :: gf_vect_mul_init_base، ec_init_tables_base و نسخه‌های اسمبلی gf_*vect_dot_prod_avx2.asm

پیچیدگی: ساخت جدول ‏O‏(۳۲) برای هر ضریب، یعنی ۳۲·‏k‏·‏m‏ بایت برای کل ماتریس. کدگذاری ‏O‏(k·‏m‏·‏L/W‏) دستور برای ‏L‏ بایت با پهنای ثبات ‏W‏ (۱۶/۳۲/۶۴)

text
pseudocode
init(c): for n in 0..15: tbl[n] = c·n ; tbl[16+n] = c·(n≪4)
encode_row(row r, data D0..Dk-1, length L):
    for off in 0, W, 2W, … < L:                 # W = 32 در AVX2
        acc = 0
        for j in 0..k-1:
            v  = load(Dj + off)
            lo = v AND 0x0F ; hi = (v ≫ 4) AND 0x0F
            acc ^= PSHUFB(tbl[r][j].lo, lo) ^ PSHUFB(tbl[r][j].hi, hi)
        store(P_r + off, acc)

مثال عددی. برای ‏d‏ = ۰‏xB7‏ و ‏c‏ = ۲: نیم‌بایت پایین ۷ به ‏tbl[7]‏ = ۰‏x0E‏ می‌رسد و نیم‌بایت بالا ‏B‏ به ‏tbl[27]‏ = ۰‏x7D. 0x0E‏ ⊕ ۰‏x7D‏ = ۰‏x73‏، که با ۲·۰‏xB7‏ مستقیم (۰‏x16E‏ ⊕ ۰‏x11D‏ = ۰‏x73‏) یکی است. برای ‏k=4‏، ‏m=2‏، یعنی شش ضریب، ‏ec_init_tables 6‏ × ۳۲ = ۱۹۲ بایت جدول می‌سازد. ‏Ceph‏ در ‏ErasureCodeIsaDefault::prepare‏ آرایه‌ای به اندازه‌ی ‏k‏·(‏m+k‏)·۳۲ بایت می‌گیرد و جدول‌ها را در ‏ErasureCodeIsaTableCache‏ نگه می‌دارد تا برای هر (‏k, m‏) فقط یک‌بار ساخته شوند. جدول‌های ‏decode‏ هم در یک ‏LRU‏ به طول ‏decoding_tables_lru_length‏ نگه داشته می‌شوند، چون برای هر الگوی خرابی ماتریس متفاوتی لازم است.

شکل ۲۷-۶: مسیر ضرب ۳۲ بایت داده در ثابت c=2 با AVX2 . داده به دو نیم‌بایت شکسته می‌شود. هر نیم‌بایت با VPSHUFB از جدول ۱۶ خانه‌ای مربوطش جست‌وجو می‌شود و دو نتیجه XOR می‌شوند. ردیف پایین نشان می‌دهد این ضرب برای هر ستون داده تکرار می‌شود و در parity انباشته می‌شود. نسخه‌های gf_Nvect_dot_prod چند parity را در یک گذر روی داده می‌سازند تا داده فقط یک‌بار از حافظه خوانده شود
شکل ۲۷-۶: مسیر ضرب ۳۲ بایت داده در ثابت ‏c=2‏ با ‏AVX2‏. داده به دو نیم‌بایت شکسته می‌شود. هر نیم‌بایت با ‏VPSHUFB‏ از جدول ۱۶ خانه‌ای مربوطش جست‌وجو می‌شود و دو نتیجه ‏XOR‏ می‌شوند. ردیف پایین نشان می‌دهد این ضرب برای هر ستون داده تکرار می‌شود و در ‏parity‏ انباشته می‌شود. نسخه‌های ‏gf_Nvect_dot_prod‏ چند ‏parity‏ را در یک گذر روی داده می‌سازند تا داده فقط یک‌بار از حافظه خوانده شود
◆ عمیق‌تر

در پردازنده‌هایی که ‏GFNI‏ دارند، ‏ISA-L‏ نسخه‌های ‏*_gfni.asm‏ را انتخاب می‌کند. دستور ‏GF2P8AFFINEQB‏ ضرب در یک ثابت ‏GF(28)\mathrm{GF}(2^8)‏ را به شکل یک تبدیل ‏affine‏ روی هر بایت انجام می‌دهد و جدول نیم‌بایت را حذف می‌کند. انتخاب نسخه در زمان اجرا با ‏dispatcher‏ چندنسخه‌ای (‏ec_multibinary.asm‏) و بر اساس ‏CPUID‏ انجام می‌شود، نه در زمان کامپایل ‏Ceph‏.

۲۷-۹ ماتریس‌های تولید: ‏Vandermonde‏ در برابر ‏Cauchy‏#

پلاگین ‏isa‏ دو ماتریس می‌شناسد: ‏technique=reed_sol_van‏ (پیش‌فرض) که ‏gf_gen_rs_matrix‏ را صدا می‌زند و ‏technique=cauchy‏ که ‏gf_gen_cauchy1_matrix‏ را. هر دو ماتریس (‏k+m‏)×‏k‏ با یک ماتریس همانی ‏k‏×‏k‏ در بالا می‌سازند، پس کد سیستماتیک است.

∑ الگوریتم: تولید ماتریس کدگذاری ‏ISA-L‏

منبع در کد: isa-l/erasure_code/ec_base.c :: gf_gen_rs_matrix، gf_gen_cauchy1_matrix؛ src/erasure-code/isa/ErasureCodeIsa.cc :: ErasureCodeIsaDefault::parse، prepare

پیچیدگی: ‏O‏(k·‏m‏) ضرب ‏GF‏ برای هر دو

text
pseudocode
gf_gen_rs_matrix(a, rows=k+m, k):
    a = 0 ; for i < k: a[i][i] = 1
    gen = 1
    for i in k .. rows-1:
        p = 1
        for j < k: a[i][j] = p ; p = p·gen      # سطر i: توان‌های gen
        gen = gen·2
gf_gen_cauchy1_matrix(a, rows, k):
    همانی در بالا ; for i in k..rows-1, j < k: a[i][j] = gf_inv(i ⊕ j)

مثال عددی (‏k=4‏، ‏m=2‏). ‏Vandermonde‏: سطر ‏P‏ برابر [۱ ۱ ۱ ۱] است (‏gen‏ = ۱) و سطر ‏Q‏ برابر [۱ ۲ ۴ ۸] (‏gen‏ = ۲). پس ‏P‏ همان ‏XOR‏ ساده است. ‏Cauchy‏: سطر ۴ برابر [‏inv(4) inv(5) inv(6) inv‏(۷)] = [۴۷ ‏A7 7A BA]‏ و سطر ۵ برابر [‏inv(5) inv(4) inv(7) inv‏(۶)] = [‏A7 47 BA 7A]‏. برای داده‌ی ‏D‏ = [۳۷ ۵‏C A1 0F]‏، با ‏Vandermonde P‏ = ۰‏xC5‏ و ‏Q‏ = ۰‏x49‏، و با ‏Cauchy P‏ = ۰‏xD1‏ و ‏Q‏ = ۰‏x09‏.

شکل ۲۷-۷: دو ماتریس کدگذاری ISA-L برای k=4 ، m=2 و برچسب parity حاصل برای بایت نمونه‌ی D = [۰ x37 ، ۰ x5C ، ۰ xA1 ، ۰ x0F] . چهار سطر بالا همانی‌اند و داده را بی‌تغییر به shard های ۰ تا ۳ می‌برند. دو سطر پایین ضرایب parity هستند. در Vandermonde ، سطر P تماماً ۱ است، پس P همان XOR است و مسیر سریع xor_gen برای m=1 به‌کار می‌رود. در Cauchy همه‌ی ضرایب parity غیر بدیهی‌اند
شکل ۲۷-۷: دو ماتریس کدگذاری ‏ISA-L‏ برای ‏k=4‏، ‏m=2‏ و برچسب ‏parity‏ حاصل برای بایت نمونه‌ی ‏D‏ = [۰‏x37‏، ۰‏x5C‏، ۰‏xA1‏، ۰‏x0F]‏. چهار سطر بالا همانی‌اند و داده را بی‌تغییر به ‏shard‏های ۰ تا ۳ می‌برند. دو سطر پایین ضرایب ‏parity‏ هستند. در ‏Vandermonde‏، سطر ‏P‏ تماماً ۱ است، پس ‏P‏ همان ‏XOR‏ است و مسیر سریع ‏xor_gen‏ برای ‏m=1‏ به‌کار می‌رود. در ‏Cauchy‏ همه‌ی ضرایب ‏parity‏ غیر بدیهی‌اند

چرا ‏Ceph‏ محدودیت می‌گذارد. ماتریس ‏gf_gen_rs_matrix‏ یک ‏Vandermonde‏ «واقعی» نیست: سطرهای همانی روی آن گذاشته شده‌اند و ‏MDS‏ بودنش برای همه‌ی (‏k, m‏) تضمین نمی‌شود. خود ‏ISA-L‏ ابزار ‏gen_rs_matrix_limits.c‏ را برای یافتن حدها دارد. ‏Ceph‏ در ‏ErasureCodeIsaDefault::parse‏ این حدها را اعمال می‌کند: ‏k‏ ≤ ۳۲ و ‏m‏ ≤ ۳۲ (‏MAX_K‏ و ‏MAX_M‏). برای ‏Vandermonde‏ اگر ‏m‏ > ۴ باشد، یا ‏m‏ = ۴ و ‏k‏ > ۲۱، پیام هشدار می‌دهد و ‏technique‏ را خودکار به ‏cauchy‏ تغییر می‌دهد. یعنی پروفایل ذخیره‌شده عوض می‌شود. با شبیه‌سازی جداگانه (حذف همه‌ی ترکیب‌های ‏m‏ سطری و آزمون وارون‌پذیری ‏k‏×‏k‏ باقی‌مانده) برای ۴+۲، ۶+۳، ۸+۳، ۸+۴، ۱۰+۴ و ۱۲+۴ هیچ زیرماتریس منفردی پیدا نشد (مثلاً ۱۰۰۱ ترکیب برای ۱۰+۴)، که با این حدها سازگار است.

۲۷-۱۰ ‏decode‏ با وارون ماتریس: یک مثال کامل بایت‌به‌بایت#

∑ الگوریتم: ‏decode‏ با حذف گاوس-جردن در ‏GF‏(۲⁸)

منبع در کد: isa-l/erasure_code/ec_base.c :: gf_invert_matrix؛ src/erasure-code/isa/ErasureCodeIsa.cc :: ErasureCodeIsaDefault::isa_decode

پیچیدگی: وارون ‏O‏(k³) ضرب ‏GF‏ برای هر الگوی خرابی (کش در ‏LRU‏)، سپس ‏O‏(k·‏e‏·‏L‏) برای ‏e shard‏ گم‌شده

text
pseudocode
B = سطرهای ماتریس تولید برای k shard سالم انتخاب‌شده
Binv = gf_invert_matrix(B)            # گاوس-جردن: محور، ضرب سطر در gf_inv، حذف با ⊕
for each lost data shard i: c_i = سطر i از Binv
for each lost parity shard p: c_p = (سطر p از ماتریس تولید) × Binv
ec_init_tables(k, e, c, decode_tbls) ; ec_encode_data(L, k, e, decode_tbls, survivors, outputs)

مثال عددی. داده‌ی [۳۷ ۵‏C A1 0F]‏ و ‏parity‏های ‏Vandermonde P‏ = ‏C5‏ و ‏Q‏ = ۴۹ را داریم. ‏D1‏ و ‏D3‏ گم شده‌اند و ‏D0‏، ‏D2‏، ‏P‏ و ‏Q‏ باقی مانده‌اند. ابتدا سهم داده‌های معلوم را کم می‌کنیم:

  • ‏P'‏ = ‏P‏ ⊕ ‏D0‏ ⊕ ‏D2‏ = ‏C5‏ ⊕ ۳۷ ⊕ ‏A1‏ = ۰‏x53‏، که برابر ‏D1‏ ⊕ ‏D3‏ است.
  • ‏Q'‏ = ‏Q‏ ⊕ ‏D0‏ ⊕ ۴·‏D2‏ = ۴۹ ⊕ ۳۷ ⊕ ‏BE‏ = ۰‏xC0‏، که برابر ۲·‏D1‏ ⊕ ۸·‏D3‏ است.

با حذف ‏D1: Q'‏ ⊕ ۲·‏P'‏ = (۸ ⊕ ۲)·‏D3‏ = ۰‏x0A‏·‏D3‏. داریم ۲·‏P'‏ = ۰‏xA6‏ و ‏Q'‏ ⊕ ۰‏xA6‏ = ۰‏x66‏. پس ‏D3‏ = ۰‏x66‏ · ‏inv‏(۰x۰A) = ۰‏x66‏ · ۰‏xDD‏ = ۰‏x0F‏ و ‏D1‏ = ‏P'‏ ⊕ ‏D3‏ = ۰‏x5C‏. ماتریس وارونی که ‏gf_invert_matrix‏ برای سطرهای {۰, ۲, ‏P, Q}‏ می‌سازد، برای ‏D3‏ سطر [۷‏A F4 A7 DD]‏ و برای ‏D1‏ سطر [۷‏B F5 A6 DD]‏ دارد. ضرب آن‌ها در [۳۷ ‏A1 C5 49]‏ همین دو عدد را می‌دهد.

۲۷-۱۱ به‌روزرسانی ‏parity‏ با دلتا (‏Tentacle‏)#

در ‏EC‏ قدیمی، نوشتن جزئی کل ‏stripe‏ را می‌خواند (بخش ‏read-modify-write‏). مسیر ‏EC‏ بهینه در ‏Tentacle‏، برای پلاگین‌هایی که پرچم ‏FLAG_EC_PLUGIN_PARITY_DELTA_OPTIMIZATION‏ را اعلام می‌کنند، از خطی بودن کد استفاده می‌کند. تغییر یک ‏shard‏ داده فقط ‏parity‏ها را به اندازه‌ی ضریبِ همان ستون ضرب در دلتا تغییر می‌دهد.

∑ الگوریتم: به‌روزرسانی ‏parity‏ با دلتا

منبع در کد: src/erasure-code/isa/ErasureCodeIsa.cc :: ErasureCodeIsaDefault::encode_delta، apply_delta؛ isa-l :: ec_encode_data_update

پیچیدگی: ‏O‏(m·‏L‏) به جای ‏O‏(k·‏m‏·‏L‏)؛ خواندن ۱ + ‏m‏ بازه به جای ‏k‏ بازه

text
pseudocode
encode_delta(old, new):  Δ = old ⊕ new                    # isa_xor با دو بردار
apply_delta(in = {i: Δ_i, p: P_p}, out):
    for each data shard i با دلتا:
        for each parity p:
            if m == 1:  P_p ^= Δ_i                          # ستون همه 1
            else: ec_encode_data_update(L, k, 1, i, tbls + 32·k·(p−k), Δ_i, &P_p)
                  # P_p ^= a[p][i]·Δ_i

مثال عددی. ‏D1‏ از ۰‏x5C‏ به ۰‏xA5‏ تغییر می‌کند، پس Δ = ۰‏x5C‏ ⊕ ۰‏xA5‏ = ۰‏xF9‏. در ‏Vandermonde‏ ضرایب ستون ۱ برای ‏P‏ و ‏Q‏ برابر ۱ و ۲ هستند: ‏P_new‏ = ‏C5‏ ⊕ ‏F9‏ = ۰‏x3C‏ و ‏Q_new‏ = ۴۹ ⊕ ۲·‏F9‏ = ۴۹ ⊕ ‏EF‏ = ۰‏xA6‏. محاسبه‌ی کامل از صفر با داده‌ی جدید هم دقیقاً ۳‏C‏ و ‏A6‏ می‌دهد. در ‏Cauchy‏ ضرایب ستون ۱ برابر ‏A7‏ و ۴۷ هستند و نتیجه ۰‏x17‏ و ۰‏x70‏ می‌شود، که آن هم با محاسبه‌ی کامل یکی است. برای نوشتن ۴‏K‏ در ۸+۳، مسیر قدیمی ۸ خواندن ۴‏K‏ (یا کل ‏stripe‏) لازم داشت، اما مسیر دلتا ۱ + ۳ = ۴ خواندن و ۴ نوشتن دارد.

شکل ۲۷-۸: نوشتن ۴ K درون shard D1 در pool با k=4 ، m=2 و مسیر parity delta. primary فقط همان بازه را از D1 و دو parity می‌خواند. Δ را با encode_delta حساب می‌کند و با apply_delta ضریب ستون ۱ ضرب در Δ را به هر parity XOR می‌کند. سپس سه نوشتن را در یک تراکنش توزیع‌شده می‌فرستد. shard های D0 ، D2 و D3 درگیر نمی‌شوند
شکل ۲۷-۸: نوشتن ۴‏K‏ درون ‏shard D1‏ در ‏pool‏ با ‏k=4‏، ‏m=2‏ و مسیر ‏parity delta. primary‏ فقط همان بازه را از ‏D1‏ و دو ‏parity‏ می‌خواند. Δ را با ‏encode_delta‏ حساب می‌کند و با ‏apply_delta‏ ضریب ستون ۱ ضرب در Δ را به هر ‏parity XOR‏ می‌کند. سپس سه نوشتن را در یک تراکنش توزیع‌شده می‌فرستد. ‏shard‏های ‏D0‏، ‏D2‏ و ‏D3‏ درگیر نمی‌شوند
⚠ هشدار

دلتا فقط وقتی درست است که مقدار قدیمی خوانده‌شده دقیقاً همان مقداری باشد که ‏parity‏ فعلی با آن ساخته شده است. اگر ‏shard‏ ناسازگار باشد (مثلاً پس از خطای نیمه‌کاره)، دلتا خطا را منتشر می‌کند. به همین دلیل ‏scrub‏ عمیق در ‏pool‏های ‏EC‏ (فصل ۳۲) و ‏crc‏ هر ‏shard‏ (بخش ‏crc‏ در همین فصل) اهمیت بیشتری پیدا می‌کنند.

۲۷-۱۲ ‏LRC‏: از ‏k‏، ‏m‏ و ‏l‏ تا رشته‌ی لایه‌ها#

∑ الگوریتم: تولید ‏mapping‏ و ‏layers‏ در ‏LRC‏

منبع در کد: src/erasure-code/lrc/ErasureCodeLrc.cc :: ErasureCodeLrc::parse_kml

پیچیدگی: ‏O‏(k+m) برای ساخت رشته‌ها؛ ترمیم محلی ‏l‏ خواندن به جای ‏k‏

text
pseudocode
require (k+m) % l == 0 ; g = (k+m)/l ; require k % g == 0 and m % g == 0
mapping = repeat g: 'D'×(k/g) + '_'×(m/g) + '_'
global  = repeat g: 'D'×(k/g) + 'c'×(m/g) + '_'
local_i = for j < g: (i==j) ? 'D'×l + 'c' : '_'×(l+1)
if crush-locality: steps = [choose g type locality, chooseleaf (l+1) type failure-domain]

مثال عددی. با ‏k=4‏، ‏m=2‏ و ‏l=3: g‏ = ۶/۳ = ۲ و هر گروه ۲ داده، ۱ ‏parity‏ سراسری و ۱ ‏parity‏ محلی دارد. کل ‏chunk‏ها ۴ + ۲ + ۲ = ۸ است. نتیجه: ‏mapping‏ = ‏DD__DD__‏، لایه‌ی سراسری ‏DDc_DDc_‏ و لایه‌های محلی ‏DDDc____‏ و ‏____DDDc‏. سربار فضا ۸/۴ = ۲ برابر است (در برابر ۱.۵ برای ۴+۲ ساده). در مقابل، ترمیم یک ‏chunk‏ به جای ۴ خواندن فقط ۳ خواندن از همان گروه لازم دارد.

شکل ۲۷-۹: اینکه parse_kml سه پارامتر k=4 ، m=2 و l=3 را چگونه به رشته‌های mapping و layers تبدیل می‌کند. در mapping ، حرف D جای داده و خط زیر جای chunk کدشده است. در هر لایه، c یعنی chunk ی که آن لایه تولید می‌کند. لایه‌ی سراسری parity های ۲ و ۶ را با RS می‌سازد و هر لایه‌ی محلی یک parity XOR مانند برای سه chunk گروه خود. با crush-locality=rack ، قانون CRUSH دو rack و در هر rack چهار host انتخاب می‌کند تا ترمیم محلی از rack بیرون نرود
شکل ۲۷-۹: اینکه ‏parse_kml‏ سه پارامتر ‏k=4‏، ‏m=2‏ و ‏l=3‏ را چگونه به رشته‌های ‏mapping‏ و ‏layers‏ تبدیل می‌کند. در ‏mapping‏، حرف ‏D‏ جای داده و خط زیر جای ‏chunk‏ کدشده است. در هر لایه، ‏c‏ یعنی ‏chunk‏ی که آن لایه تولید می‌کند. لایه‌ی سراسری ‏parity‏های ۲ و ۶ را با ‏RS‏ می‌سازد و هر لایه‌ی محلی یک ‏parity XOR‏ مانند برای سه ‏chunk‏ گروه خود. با ‏crush-locality=rack‏، قانون ‏CRUSH‏ دو ‏rack‏ و در هر ‏rack‏ چهار ‏host‏ انتخاب می‌کند تا ترمیم محلی از ‏rack‏ بیرون نرود

۲۷-۱۳ ‏Clay‏: کدام ‏sub-chunk‏ها برای ترمیم خوانده می‌شوند#

∑ الگوریتم: انتخاب ‏sub-chunk‏های ترمیم در ‏Clay‏

منبع در کد: src/erasure-code/clay/ErasureCodeClay.cc :: ErasureCodeClay::parse، get_repair_subchunks، get_repair_sub_chunk_count

پیچیدگی: ‏O‏(qʸ) بازه برای گره گم‌شده در سطر ‏y‏؛ ترافیک ‏d‏·(‏sub_chunk_no/q) sub-chunk‏

text
pseudocode
q = d − k + 1 ; nu = (k+m) % q ? q − (k+m) % q : 0
t = (k+m+nu)/q ; sub_chunk_no = q^t
get_repair_subchunks(lost):
    y = lost / q ; x = lost % q
    seq = q^(t−1−y) ; num = q^y ; idx = x·seq
    repeat num times: emit [idx, idx+seq) ; idx += q·seq

مثال عددی. ‏k=4‏، ‏m=2‏، ‏d=5‏ ⇒ ‏q‏ = ۲، ‏nu‏ = ۰، ‏t‏ = ۳ و ‏sub_chunk_no‏ = ۸. برای گره ۰ (‏x=0‏، ‏y=0‏): ‏seq‏ = ۴ و ‏num‏ = ۱، پس ‏sub-chunk‏های ۰ تا ۳. برای گره ۲ (‏x=0‏، ‏y=1‏): ‏seq‏ = ۲ و ‏num‏ = ۲، پس {۰، ۱، ۴، ۵}. برای گره ۵ (‏x=1‏، ‏y=2‏): ‏seq‏ = ۱ و ‏num‏ = ۴، پس {۱، ۳، ۵، ۷}. هر یک از ۵ ‏helper‏ نصف ‏chunk‏ خود را می‌فرستد، یعنی ۲.۵ ‏chunk‏ در برابر ۴ ‏chunk‏ در ‏RS‏. یک ‏chunk‏ چهار مگابایتی با ۲.۵ × ۴ = ۱۰ ‏MiB‏ به جای ۱۶ ‏MiB‏ ترمیم می‌شود.

شکل ۲۷-۱۰: شبکه‌ی sub-chunk در Clay با k=4 ، m=2 و d=5 برای سه گره گم‌شده‌ی متفاوت. سطرها گره‌ها با مختصات ( x, y ) در شبکه‌ی q × t هستند و ستون‌ها شاخص sub-chunk . سطر قرمز گره گم‌شده است و خانه‌های آبی sub-chunk هایی‌اند که هر helper می‌فرستد. الگو از get_repair_subchunks می‌آید: هرچه y بزرگ‌تر باشد، بازه‌ها کوتاه‌تر و پراکنده‌ترند. پس خواندن روی دیسک تکه‌تکه‌تر می‌شود، و این هزینه‌ی پنهان Clay روی HDD است
شکل ۲۷-۱۰: شبکه‌ی ‏sub-chunk‏ در ‏Clay‏ با ‏k=4‏، ‏m=2‏ و ‏d=5‏ برای سه گره گم‌شده‌ی متفاوت. سطرها گره‌ها با مختصات (‏x, y‏) در شبکه‌ی ‏q‏×‏t‏ هستند و ستون‌ها شاخص ‏sub-chunk‏. سطر قرمز گره گم‌شده است و خانه‌های آبی ‏sub-chunk‏هایی‌اند که هر ‏helper‏ می‌فرستد. الگو از ‏get_repair_subchunks‏ می‌آید: هرچه ‏y‏ بزرگ‌تر باشد، بازه‌ها کوتاه‌تر و پراکنده‌ترند. پس خواندن روی دیسک تکه‌تکه‌تر می‌شود، و این هزینه‌ی پنهان ‏Clay‏ روی ‏HDD‏ است
💡 نکته

در ‏Clay‏، چون خواندن‌ها به بازه‌های کوچک (‏sub_chunk_no‏ برابر) شکسته می‌شوند، اندازه‌ی ‏stripe_unit‏ باید آن‌قدر بزرگ باشد که هر ‏sub-chunk‏ دست‌کم چند کیلوبایت شود. اگر ‏chunk‏ فقط ۴‏K‏ باشد و ‏sub_chunk_no‏ = ۸، هر ‏sub-chunk‏ فقط ۵۱۲ بایت می‌شود (اندازه‌ی واقعی ‏chunk‏ را ‏get_chunk_size‏ با هم‌ترازی ‏sub_chunk_no‏·‏k‏ گرد می‌کند).

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

  • پیش‌فرض پلاگین: در ‏Squid‏ ‏plugin=jerasure‏، و در ‏Tentacle‏ و ‏main‏ ‏plugin=isa‏ (هر دو با ‏technique=reed_sol_van k=2 m=2‏). این تغییر را در کد ‏global.yaml.in‏ هر دو شاخه بررسی کرده‌ایم.
  • ‏EC‏ بهینه: کد ‏main‏ کلاس ‏ECSwitch‏ را دارد که به‌عنوان «سوئیچ موقت» میان ‏backend‏ قدیمی (‏ECBackendL‏) و جدید (‏ECBackend‏) توصیف شده است. طبق توضیح خود کد، پس از اطمینان کامل، ‏backend‏ قدیمی حذف خواهد شد.
  • ‏EC‏ و ‏stretch cluster‏: کارهای پشتیبانی ‏EC‏ در ‏stretch mode‏ در شاخه‌ی ‏main‏ ادامه دارد. پیش از استفاده، یادداشت‌های انتشار نسخه‌ی هدف را بخوانید.

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

  1. ۱. ضرب ‏GF(28)\mathrm{GF}(2^8)‏ با ۰‏x11D‏ را در پایتون پیاده کنید و جدول ضرب‌های این فصل را بازتولید کنید. سپس ‏decode‏ با دو خرابی را برای الگوی دیگری (مثلاً ‏D0‏ و ‏P1‏) حل کنید.
  2. ۲. با ‏ceph_erasure_code_benchmark‏ (در بسته‌ی تست‌ها) سرعت ‏encode‏ پلاگین‌های ‏isa‏ و ‏jerasure‏ را برای ‏k=8‏ و ‏m=3‏ روی سرور خود مقایسه کنید.
  3. ۳. روی یک ‏pool‏ آزمایشی ‏EC‏ با و بدون ‏allow_ec_optimizations‏، ‏fio‏ با نوشتن تصادفی ۴‏K‏ اجرا و ‏IOPS‏ و تأخیر را مقایسه کنید.
  4. ۴. پهنای باند ترمیم ‏CLAY‏ را برای (‏k=6‏، ‏m=3‏، ‏d=8‏) محاسبه و با ‏RS‏ مقایسه کنید.

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

  1. ۱. چرا جمع و تفریق در ‏GF(28)\mathrm{GF}(2^8)‏ هر دو ‏XOR‏ هستند؟
  2. ۲. ۰‏x56‏ × ۰‏x04‏ در ‏GF(28)\mathrm{GF}(2^8)‏ با ۰‏x11D‏ چگونه محاسبه می‌شود؟
  3. ۳. کد ‏MDS‏ چیست و چرا ‏Vandermonde‏ ساده‌ی ‏ISA-L‏ همیشه ‏MDS‏ نیست؟
  4. ۴. ‏Ceph‏ در چه شرایطی خودکار به ‏cauchy‏ تغییر می‌دهد؟
  5. ۵. در مثال این فصل، ‏P0‏ و ‏P1‏ چگونه به دست آمدند؟
  6. ۶. چرا ضرایب ‏decode‏ کش می‌شوند؟
  7. ۷. ‏PSHUFB‏ چگونه ضرب در ثابت را سریع می‌کند؟
  8. ۸. ‏parity delta‏ چیست و چرا درست است؟
  9. ۹. ‏stripe_unit‏ پیش‌فرض در ‏pool‏ بهینه چقدر است و چرا؟
  10. ۱۰. پهنای باند ترمیم ‏CLAY‏ با ‏k=8‏، ‏m=4‏ و ‏d=11‏ چقدر است و هزینه‌ی آن چیست؟

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

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

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

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