🌐 در Cloudflare، سرویس‌های داخلی زیادی نیاز دارند که از بیش از 330 دیتاسنتر جهانی، یک حالت مشترک در کنترل‌پلین را بخوانند و تغییر دهند. این سرویس‌ها نیاز به تضمینی دارند که خواننده‌ها هرگز حالت ناسازگار نبینند و سیستم برای نوشتن در دسترس بماند حتی وقتی برخی دیتاسنترها یا لینک‌ها از کار می‌افتند. اما شبکه‌ی ما روی اینترنت اجرا می‌شود و اینترنت قابل پیش‌بینی نیست: سرورها و دیتاسنترها افت می‌کنند، صف‌ها پر می‌شوند، لینک‌ها و کابل‌ها قطع می‌شوند — و این شرایط اجرای یک دیتاست جهانی با قوام قوی (مثل تضمین اینکه تمام خواننده‌ها همه نوشته‌های قبلی را می‌بینند) را دشوار می‌کند.

یکی از راه‌های همگام‌سازی ایمن داده‌ها در شرایط شبکه‌ای نامناسب، استفاده از یک الگوریتم consensus است که به مجموعه‌ای از ماشین‌ها اجازه می‌دهد بر روی یک توالی مشترک از مقادیر توافق کنند (مثلاً عملیات put و get در یک key-value store)، مشروط بر اینکه اکثریت ماشین‌ها زنده و قابل ارتباط باشند. اما الگوریتم‌های متداولی مثل Raft در شبکه‌های گسترده (wide-area) مثل شبکه‌ی Cloudflare مشکل دارند، چون به وجود یک leader و تایم‌اوت‌ها وابسته‌اند: فقط leader می‌تواند نوشتن انجام دهد و اگر آن دچار خرابی یا افت ارتباط شود، سیستم تا زمانی که یک replica دیگر تایم‌اوت شود و leader جدید انتخاب گردد، در دسترس نخواهد بود. تنظیم این تایم‌اوت‌ها در شبکه‌هایی با تاخیر غیرقابل پیش‌بینی کار سختی است.

ما چندین حادثه ناشی از ناپایداری leader در سیستم‌های مبتنی بر consensus را تجربه کرده‌ایم. به همین دلیل تیم Research ما در سال گذشته سرویس توزیع‌شده‌ی جدیدی به نام Meerkat را ساخته که هسته‌اش یک الگوریتم consensus به نام QuePaxa است (منتشرشده در 2023 توسط پژوهشگران EPFL). تفاوت کلیدی QuePaxa با Raft این است که در QuePaxa همه‌ی replicaها در تمام زمان‌ها می‌توانند نوشتن انجام دهند و پیشرفت سیستم به‌خاطر تایم‌اوت متوقف نمی‌شود؛ همین باعث می‌شود QuePaxa برای شبکه‌ی توزیع‌شده و ناهمگن Cloudflare مناسب باشد. بر روی لاگ consensusِ Meerkat، اپلیکیشن‌هایی مثل یک transactional key-value store و یک سیستم leasing ساخته‌ایم. تا جای ممکن می‌دانیم این، اولین استقرار صنعتی QuePaxa در مقیاس جهانی خواهد بود.

باید توجه کرد که Meerkat فعلاً یک سرویس experimental است و همچنان در حال توسعه است. هدف اولیه مدیریت قطعات کوچک از حالت کنترل‌پلین (مثلاً اطلاعات leadership برای دیتابیس‌های تکثیرشده) است و فعلاً فقط داخلی نگه داشته خواهد شد. این نوشته معرفی Meerkat است و پایه‌ای برای مطالب بعدی مرتبط با Meerkat فراهم می‌کند.

چرا به یک دیتاست کنترل‌پلینِ جهانی نیاز داریم؟ سرویس‌های زیادی در Cloudflare از چند ماشین توزیع‌شده در سراسر جهان، داده‌های control-plane را می‌خوانند و می‌نویسند؛ داده‌هایی که برای عملکرد درست آن سرویس‌ها لازم‌اند. مثال‌ها: اطلاعات placement (مثلاً کجا یک instance از یک مدل AI ذخیره شده) و اطلاعات leadership (کدام ماشین مجاز به نوشتن در یک دیتابیس است). این داده‌ها باید هم قویاً consistent باشند و هم در مواجهه با نوع خاصی از خطاها در دسترس باقی بمانند.

حالا دقیق‌تر می‌گوییم که چه نیازمندی‌هایی برای consistency و fault tolerance از یک سرویس consensus در Cloudflare انتظار داریم. برای مثال توضیح از یک key-value store استفاده می‌کنیم، اما اپلیکیشن‌های دیگری مثل distributed leases یا locks هم کاربرد دارند.

Consistency: سطح consistency یک سیستم توزیع‌شده توصیف می‌کند که هنگام دریافت هم‌زمان خوانش و نوشتن‌ها چه رفتارهای غیرمنتظره‌ای مجاز است. فرض کنید یک key-value که مقدار عددی x = 6 را در چند نود نگهداری می‌کند و دو نوشتن زیر ارسال می‌شوند (ممکن است با تاخیرهای مختلف به نودها برسند):

x = x + 1
x = x / 2

سطح consistency به شما می‌گوید که بعد از این نوشتن‌ها یک کلاینت ممکن است چه مقادیری از x ببیند. در سطوح ضعیف‌تر، نوشتن‌ها می‌توانند دوباره‌مرتب شوند. در سطوح قوی‌تر، نوشتن‌ها ترتیب‌شان حفظ می‌شود اما خوانش‌ها ممکن است قدیمی باشند. قوی‌ترین حالت این است که عملیات دقیقاً همان‌طور که در زمان واقعی رخ داده‌اند مرتب شوند؛ این خاصیت linearizability نامیده می‌شود. بسیاری از سرویس‌های Cloudflare به linearizability نیاز دارند چون این سطح از consistency برنامه‌نویس را از فکر کردن به رفتارهای عجیب سیستم‌های توزیع‌شده بی‌نیاز می‌کند و مثل حافظه محلی رفتار قابل پیش‌بینی فراهم می‌سازد. (پیشنهادی: Meerkat همچنین در آینده قابلیت serializability را برای KV ارائه خواهد داد و در پست بعدی دربارۀ آن توضیح می‌دهیم.)

Fault tolerance: سطح تحمل خطا توصیف می‌کند که سیستم در مقابل چه نوع خطاهایی می‌تواند به خواسته‌هایش عمل کند قبل از این که «فاجعه» رخ دهد — مثلاً نقض خواصی که سیستم برایش طراحی شده مثل اینکه دو خوانش متوالی بدون نوشتن میانی برای یک کلید نباید مقادیر متضاد برگردانند، یا اینکه سیستم برای نوشتن در دسترس بماند. خطاها شامل خرابی یا تأخیر شبکه، کرش ماشین‌ها، یا ری‌استارت‌ها هستند. معمولاً یک سیستم فقط دسته‌ای از خطاها را صراحتاً پوشش می‌دهد (نمی‌توان همه انواع خطا را پوشش داد).

خواسته‌های مشخص ما از نظر تحمل خطا به این شکل‌اند: اول، سیستم داده باید برای خواندن و نوشتن از سمت یک کلاینت در هر دیتاسنتری در دسترس بماند مادامی که: اکثریت ماشین‌های سیستم زنده و قادر به ارتباط با هم باشند. (به طور رسمی، ما f خطا را در سیستم 2f + 1 تحمل می‌کنیم.) و کلاینت بتواند با هر ماشینی تماس بگیرد که به اکثریت ماشین‌های زنده متصل است. این یعنی خرابی یک ماشین یا افت یک لینک شبکه نباید دسترسی را مختل کند — ویژگی‌ای که سیستم‌های مبتنی بر Raft به شکل مستقیم فراهم نمی‌کنند. دوم، سیستم تا زمانی که هیچ بازیگری رفتار مخرب (Byzantine) نداشته باشد و البته کد بدون باگ باشد، صحیح خواهد ماند؛ به عبارت دیگر، هیچ دو ماشینِ به‌روز نباید دربارۀ وضعیت جهان به توافق نرسیده باشند (مثلاً یکی بگوید key1=1 و دیگری بگوید key1=2). خلاصه اینکه سیستم باید در حضور کرش‌ها، ری‌استارت‌ها، و بروز یا افت شبکه و حتی خاموشی دیتاسنترها درست عمل کند — هرچند مانند Raft، ما با Byzantine faults سروکار نداریم.

معرفی Meerkat: Meerkat یک سرویس consensus است که می‌خواهیم روی آن اپلیکیشن‌هایی با ویژگی‌های بالا (قابلیت تطابق قوی و تحمل خطا) بسازیم، مثل یک key-value store. ساختار کلی Meerkat را توضیح می‌دهیم و سپس نشان می‌دهیم چگونه انتخاب الگوریتم consensus به فراهم کردن این خواسته‌ها کمک می‌کند.

معماری کلی: توسعه‌دهندگان یک کلاستر از Meerkat replicas درخواست می‌کنند. هر replica به همه‌ی replicaهای دیگر متصل است و در الگوریتم consensus شرکت می‌کند و می‌تواند هم خوانش و هم نوشتن دریافت کند. توسعه‌دهنده می‌تواند تعیین کند کدام دیتاسنترها می‌توانند میزبان replicaها باشند و Meerkat آنها را خودکار قرار می‌دهد. برای تعامل، کلاینت‌های توسعه‌دهنده می‌توانند درخواست‌های اپلیکیشن-خاص را به هر replica در کلاستر بفرستند؛ یک replica ممکن است چند اپلیکیشن میزبانی کند، اما ساده‌ترین آنها یک key-value store است و بنابراین انواع ساده‌ی درخواست‌ها get یا put خواهند بود. replica به درخواست پاسخِ اپلیکیشن-خاص می‌دهد (مثلاً رکوردهای درخواست‌شده در get). توجه کنید که خوانش‌های KV (gets) تضمین می‌شوند که به‌روز باشند.

لاگ Meerkat: در سطح داخلی، replica درخواست‌های اپلیکیشن را (مثل get و put) به شکل رویدادهایی در یک log ترجمه می‌کند. آن replica هر رویداد لاگ را به همه‌ی replicaها توزیع می‌کند با استفاده از الگوریتم consensus به‌طوری که همه‌ی replicaها یک لاگ دقیقاً یکسان از رویدادها نگهداری کنند (در عمل ممکن است یک replica عقب بماند، اما هرگز نباید ورودی‌های متفاوت ثبت کند). محتوای این رویدادها برای هسته‌ی Meerkat اهمیتی ندارد؛ اپلیکیشن‌ها هستند که از محتوای رویدادها استفاده می‌کنند. هر replica میزبانی‌کننده‌ی چند اپلیکیشن Meerkat است که لاگ را می‌خوانند و از روی آن state می‌سازند. برای مثال، اپلیکیشن KV از روی رویدادهای لاگ یک store در حافظه می‌سازد: وقتی یک کلاینت نوشتن put k1 v1 می‌فرستد، replica گیرنده آن را در یک رویداد لاگ قرار می‌دهد و آن را به همه توزیع می‌کند. اگر شخص دیگری بعدها put k1 v11 را به replica دیگری بفرستد، آن هم توزیع می‌شود. چون همه‌ی replicaهای سالم همان لاگ را دارند، می‌توانند عملیاتی که در لاگ آمده را به ترتیب اجرا کنند و state یکسانی بسازند. نکته مهم: برای حفظ linearizability، درخواست‌های get نیز رویدادهای لاگ توزیع‌شده تولید می‌کنند.

ضمانت‌ها: Meerkat تضمین می‌کند که اگر یک کلاینت put k1 v1 را اجرا کند، یک کلاینت دوم بعداً put k1 v11 را اجرا کند و یک کلاینت سوم بعداً get k1 بگیرد (با یک خوانش سازگار)، همیشه مقدار v11 خوانده خواهد شد — حتی اگر هر درخواست به replicaهای مختلف و پراکنده در دنیا ارسال شده باشد. این همان linearizability است که برای برنامه‌نویسی مطمئن در سطح توزیع‌شده حیاتی‌ست.