پنج دانشجو و سه فروشگاه با ارزش کلیدی توزیع کردند

یا اینکه چگونه یک کتابخانه C++ مشتری برای ZooKeeper، etcd و Consul KV نوشتیم

در دنیای سیستم های توزیع شده، تعدادی کار معمولی وجود دارد: ذخیره اطلاعات در مورد ترکیب خوشه، مدیریت پیکربندی گره ها، شناسایی گره های معیوب، انتخاب یک رهبر. و دیگران. برای حل این مشکلات، سیستم های توزیع شده ویژه ایجاد شده است - خدمات هماهنگی. اکنون ما به سه مورد از آنها علاقه مند خواهیم شد: ZooKeeper، etcd و Consul. از میان تمام قابلیت‌های غنی Consul، ما روی Consul KV تمرکز خواهیم کرد.

پنج دانشجو و سه فروشگاه با ارزش کلیدی توزیع کردند

در اصل، همه این سیستم‌ها انبارهای ارزش کلیدی قابل خطی‌سازی و تحمل‌پذیر خطا هستند. اگرچه مدل های داده آنها تفاوت های قابل توجهی دارند که در ادامه به آنها خواهیم پرداخت، اما آنها همان مشکلات عملی را حل می کنند. بدیهی است که هر برنامه ای که از سرویس هماهنگی استفاده می کند به یکی از آنها گره خورده است که ممکن است منجر به نیاز به پشتیبانی از چندین سیستم در یک مرکز داده شود که مشکلات یکسانی را برای برنامه های مختلف حل می کند.

ایده حل این مشکل از یک آژانس مشاوره استرالیایی سرچشمه گرفت و به عهده ما، تیم کوچکی از دانشجویان، قرار گرفت تا آن را اجرا کنیم، چیزی که من در مورد آن صحبت خواهم کرد.

ما موفق به ایجاد کتابخانه ای شدیم که یک رابط مشترک برای کار با ZooKeeper، etcd و Consul KV ارائه می دهد. این کتابخانه به زبان C++ نوشته شده است، اما برنامه‌هایی برای انتقال آن به زبان‌های دیگر وجود دارد.

مدل های داده

برای توسعه یک رابط مشترک برای سه سیستم مختلف، باید درک کنید که چه چیزی مشترک هستند و چگونه تفاوت دارند. بیایید آن را بفهمیم.

نگهبان باغ وحش

پنج دانشجو و سه فروشگاه با ارزش کلیدی توزیع کردند

کلیدها در یک درخت سازماندهی می شوند و گره نامیده می شوند. بر این اساس، برای یک گره می توانید لیستی از فرزندان آن را دریافت کنید. عملیات ایجاد znode (ایجاد) و تغییر یک مقدار (setData) از هم جدا می شوند: فقط کلیدهای موجود قابل خواندن و تغییر هستند. ساعت‌ها را می‌توان به عملیات بررسی وجود یک گره، خواندن یک مقدار و گرفتن فرزند متصل کرد. Watch یک ماشه یکبار مصرف است که با تغییر نسخه داده های مربوطه روی سرور فعال می شود. گره های زودگذر برای تشخیص خرابی ها استفاده می شوند. آنها به جلسه مشتری که آنها را ایجاد کرده است گره خورده اند. هنگامی که یک کلاینت جلسه ای را می بندد یا اطلاع ZooKeeper را از وجود آن متوقف می کند، این گره ها به طور خودکار حذف می شوند. تراکنش‌های ساده پشتیبانی می‌شوند - مجموعه‌ای از عملیات که اگر حداقل برای یکی از آنها امکان‌پذیر نباشد، همگی موفق می‌شوند یا شکست می‌خورند.

etcd

پنج دانشجو و سه فروشگاه با ارزش کلیدی توزیع کردند

توسعه دهندگان این سیستم به وضوح از ZooKeeper الهام گرفتند و بنابراین همه چیز را متفاوت انجام دادند. هیچ سلسله مراتبی از کلیدها وجود ندارد، اما آنها یک مجموعه نظم واژگانی را تشکیل می دهند. شما می توانید تمام کلیدهای متعلق به یک محدوده خاص را دریافت یا حذف کنید. این ساختار ممکن است عجیب به نظر برسد، اما در واقع بسیار گویا است و یک دیدگاه سلسله مراتبی را می توان به راحتی از طریق آن تقلید کرد.

etcd یک عملیات مقایسه و تنظیم استاندارد ندارد، اما چیز بهتری دارد: تراکنش ها. البته، آنها در هر سه سیستم وجود دارند، اما تراکنش های etcd به ویژه خوب هستند. آنها از سه بلوک تشکیل شده اند: چک، موفقیت، شکست. بلوک اول شامل مجموعه ای از شرایط، دوم و سوم - عملیات است. معامله به صورت اتمی انجام می شود. اگر همه شرایط درست باشد، بلوک موفقیت اجرا می شود، در غیر این صورت بلوک شکست اجرا می شود. در API 3.3، بلوک های موفقیت و شکست می توانند شامل تراکنش های تو در تو باشند. یعنی امکان اجرای اتمی سازه های شرطی در سطح تودرتو دلخواه وجود دارد. می‌توانید در مورد اینکه چک‌ها و عملیات‌هایی که از آنها وجود دارد بیشتر بدانید مستندات.

ساعت‌ها در اینجا نیز وجود دارند، اگرچه کمی پیچیده‌تر هستند و قابل استفاده مجدد هستند. یعنی بعد از نصب یک ساعت روی یک محدوده کلید، تمام آپدیت های این محدوده را تا زمانی که ساعت را لغو نکنید، دریافت خواهید کرد و نه فقط اولی. در etcd، مشابه جلسات مشتری ZooKeeper، اجاره است.

کنسول K.V.

همچنین هیچ ساختار سلسله مراتبی دقیقی در اینجا وجود ندارد، اما کنسول می تواند ظاهری را ایجاد کند که وجود دارد: می توانید همه کلیدها را با پیشوند مشخص شده دریافت و حذف کنید، یعنی با "زیردرخت" کلید کار کنید. به چنین پرس و جوهایی بازگشتی می گویند. علاوه بر این، کنسول می تواند تنها کلیدهایی را انتخاب کند که شامل کاراکتر مشخص شده پس از پیشوند نیست، که مربوط به دریافت فوری "فرزندان" است. اما شایان ذکر است که این دقیقاً ظاهر یک ساختار سلسله مراتبی است: اگر والد آن وجود نداشته باشد، می توان یک کلید ایجاد کرد یا کلیدی را که دارای فرزندان است حذف کرد، در حالی که فرزندان همچنان در سیستم ذخیره می شوند.

پنج دانشجو و سه فروشگاه با ارزش کلیدی توزیع کردند
کنسول به جای ساعت، درخواست های HTTP را مسدود کرده است. در اصل، اینها فراخوانی های معمولی به روش خواندن داده ها هستند که همراه با سایر پارامترها، آخرین نسخه شناخته شده داده ها نشان داده شده است. اگر نسخه فعلی داده های مربوطه در سرور بزرگتر از مقدار مشخص شده باشد، پاسخ بلافاصله برگردانده می شود، در غیر این صورت - هنگامی که مقدار تغییر می کند. همچنین جلساتی وجود دارد که می توان آنها را در هر زمان به کلیدها متصل کرد. شایان ذکر است که برخلاف etcd و ZooKeeper که حذف جلسات منجر به حذف کلیدهای مرتبط می شود، حالتی وجود دارد که در آن جلسه به سادگی از آنها جدا می شود. در دسترس معاملات، بدون شعبه ولی با انواع چک.

همه اش را بگذار کنار هم

ZooKeeper دقیق ترین مدل داده را دارد. پرس و جوهای محدوده بیانی موجود در etcd را نمی توان به طور موثر در ZooKeeper یا Consul تقلید کرد. در تلاش برای ترکیب بهترین ها از همه سرویس ها، به یک رابط تقریباً معادل رابط ZooKeeper با استثناهای مهم زیر رسیدیم:

  • توالی، ظرف و گره های TTL پشتیبانی نشده
  • ACL ها پشتیبانی نمی شوند
  • متد set در صورت نبود کلید ایجاد می کند (در ZK setData در این مورد یک خطا را برمی گرداند)
  • متدهای set و cas از هم جدا هستند (در ZK اساساً یکسان هستند)
  • روش erase یک گره را به همراه زیردرخت آن حذف می کند (در ZK delete اگر گره دارای فرزند باشد، خطا را برمی گرداند)
  • برای هر کلید فقط یک نسخه وجود دارد - نسخه ارزش (در ZK سه تا از آنها وجود دارد)

رد گره های متوالی به این دلیل است که etcd و Consul پشتیبانی داخلی برای آنها ندارند و می توانند به راحتی توسط کاربر در بالای رابط کتابخانه حاصل پیاده سازی شوند.

اجرای رفتار مشابه ZooKeeper در هنگام حذف یک راس مستلزم حفظ یک شمارنده فرزند جداگانه برای هر کلید در etcd و Consul است. از آنجایی که سعی کردیم از ذخیره اطلاعات متا اجتناب کنیم، تصمیم گرفته شد که کل زیردرخت حذف شود.

ظرافت های اجرا

بیایید نگاهی دقیق تر به برخی از جنبه های پیاده سازی رابط کتابخانه در سیستم های مختلف بیندازیم.

سلسله مراتب در etcd

حفظ یک دیدگاه سلسله مراتبی در etcd یکی از جالب ترین کارها بود. جستجوهای محدوده بازیابی لیستی از کلیدها با پیشوند مشخص را آسان می کند. به عنوان مثال، اگر به هر چیزی که با آن شروع می شود نیاز دارید "/foo"، شما یک محدوده می خواهید ["/foo", "/fop"). اما این کل زیردرخت کلید را برمی گرداند، که اگر درخت فرعی بزرگ باشد ممکن است قابل قبول نباشد. در ابتدا قصد داشتیم از مکانیزم ترجمه کلیدی استفاده کنیم، در zetcd پیاده سازی شده است. این شامل اضافه کردن یک بایت در ابتدای کلید، برابر با عمق گره در درخت است. بگذارید برای شما مثالی بزنم.

"/foo" -> "u01/foo"
"/foo/bar" -> "u02/foo/bar"

سپس همه فرزندان فوری را از کلید دریافت کنید "/foo" با درخواست محدوده امکان پذیر است ["u02/foo/", "u02/foo0"). بله، در ASCII "0" درست بعد می ایستد "/".

اما چگونه می توان حذف یک راس را در این مورد پیاده سازی کرد؟ به نظر می رسد که شما باید تمام محدوده های نوع را حذف کنید ["uXX/foo/", "uXX/foo0") برای XX از 01 تا FF. و بعد با هم برخورد کردیم محدودیت تعداد عملیات در یک معامله

در نتیجه، یک سیستم تبدیل کلید ساده اختراع شد، که امکان اجرای موثر هم حذف یک کلید و هم به دست آوردن لیستی از کودکان را فراهم کرد. کافی است قبل از آخرین توکن یک کاراکتر خاص اضافه کنید. مثلا:

"/very" -> "/u00very"
"/very/long" -> "/very/u00long"
"/very/long/path" -> "/very/long/u00path"

سپس کلید را پاک کنید "/very" به حذف تبدیل می شود "/u00very" و محدوده ["/very/", "/very0")، و گرفتن همه کودکان - در یک درخواست برای کلید از محدوده ["/very/u00", "/very/u01").

حذف کلید در ZooKeeper

همانطور که قبلاً اشاره کردم، در ZooKeeper نمی توانید یک گره را اگر دارای فرزند باشد حذف کنید. می خواهیم کلید را به همراه زیردرخت حذف کنیم. باید چکار کنم؟ ما این کار را با خوش بینی انجام می دهیم. ابتدا به صورت بازگشتی زیر درخت را طی می کنیم و فرزندان هر رأس را با یک پرس و جو جداگانه بدست می آوریم. سپس یک تراکنش می سازیم که سعی می کند تمام گره های زیردرخت را به ترتیب صحیح حذف کند. البته ممکن است بین خواندن زیردرخت و حذف آن تغییراتی رخ دهد. در این صورت معامله با شکست مواجه می شود. علاوه بر این، درخت فرعی ممکن است در طول فرآیند خواندن تغییر کند. یک درخواست برای فرزندان گره بعدی ممکن است یک خطا را نشان دهد اگر مثلاً این گره قبلاً حذف شده باشد. در هر دو مورد، کل فرآیند را دوباره تکرار می کنیم.

این رویکرد حذف یک کلید را در صورتی که دارای فرزند باشد بسیار بی اثر می کند و حتی اگر برنامه به کار با زیر درخت ادامه دهد و کلیدها را حذف و ایجاد کند، بسیار بی اثر می شود. با این حال، این به ما اجازه داد تا از پیچیدگی اجرای روش‌های دیگر در etcd و کنسول اجتناب کنیم.

در ZooKeeper تنظیم شده است

در ZooKeeper متدهای جداگانه ای وجود دارد که با ساختار درختی کار می کند (ایجاد، حذف، getChildren) و با داده ها در گره ها (setData، getData) کار می کنند. علاوه بر این، همه متدها دارای پیش شرط های سختگیرانه هستند: اگر گره قبلاً ایجاد کرده باشد، خطا را برمی گرداند. ایجاد شده، حذف یا setData - اگر قبلا وجود نداشته باشد. ما به یک متد مجموعه نیاز داشتیم که بتوان آن را بدون فکر کردن به وجود کلید فراخوانی کرد.

یکی از گزینه ها، اتخاذ رویکردی خوش بینانه است، مانند حذف. بررسی کنید که آیا گره وجود دارد یا خیر. اگر وجود دارد، setData را فراخوانی کنید، در غیر این صورت ایجاد کنید. اگر روش آخر خطایی را نشان داد، آن را دوباره تکرار کنید. اولین چیزی که باید به آن توجه کرد این است که تست وجود بی معنی است. شما می توانید بلافاصله ایجاد را فراخوانی کنید. تکمیل موفقیت آمیز به این معنی است که گره وجود نداشته و ایجاد شده است. در غیر این صورت، create خطای مناسب را برمی گرداند و پس از آن باید setData را فراخوانی کنید. البته، بین تماس‌ها، یک راس می‌تواند توسط یک تماس رقیب حذف شود و setData نیز یک خطا برمی‌گرداند. در این مورد، می توانید همه چیز را دوباره انجام دهید، اما آیا ارزشش را دارد؟

اگر هر دو روش یک خطا را برگردانند، مطمئناً می دانیم که یک حذف رقابتی انجام شده است. بیایید تصور کنیم که این حذف پس از فراخوانی set رخ داده است. سپس هر معنایی که می خواهیم ایجاد کنیم، قبلاً پاک شده است. این بدان معنی است که ما می توانیم فرض کنیم که مجموعه با موفقیت اجرا شده است، حتی اگر در واقع چیزی نوشته نشده باشد.

جزئیات فنی بیشتر

در این بخش از سیستم های توزیع شده فاصله می گیریم و در مورد کدنویسی صحبت می کنیم.
یکی از الزامات اصلی مشتری کراس پلتفرم بود: حداقل یکی از خدمات باید در لینوکس، MacOS و ویندوز پشتیبانی شود. در ابتدا، ما فقط برای لینوکس توسعه دادیم و بعداً آزمایش روی سیستم‌های دیگر را آغاز کردیم. این امر باعث ایجاد مشکلات زیادی شد که برای مدتی نحوه برخورد با آنها کاملاً نامشخص بود. در نتیجه، هر سه سرویس هماهنگی اکنون در لینوکس و MacOS پشتیبانی می شوند، در حالی که تنها Consul KV در ویندوز پشتیبانی می شود.

از همان ابتدا سعی کردیم از کتابخانه های آماده برای دسترسی به خدمات استفاده کنیم. در مورد ZooKeeper، انتخاب بر سر کار افتاد ZooKeeper C++، که در نهایت در ویندوز کامپایل نشد. با این حال، این تعجب آور نیست: کتابخانه فقط به صورت لینوکس قرار دارد. برای کنسول تنها گزینه این بود ppconsul. باید پشتیبانی به آن اضافه می شد جلسات и معاملات. برای etcd، یک کتابخانه کامل که آخرین نسخه پروتکل را پشتیبانی کند، یافت نشد، بنابراین ما به سادگی مشتری grpc تولید شده است.

با الهام از رابط ناهمزمان کتابخانه ZooKeeper C++، تصمیم گرفتیم یک رابط ناهمزمان را نیز پیاده سازی کنیم. ZooKeeper C++ از آينده/وعده اوليه براي اين كار استفاده مي كند. در STL، متأسفانه، آنها بسیار متواضعانه اجرا می شوند. مثلا خیر سپس روش، که تابع پاس شده را به هنگام در دسترس شدن به نتیجه آینده اعمال می کند. در مورد ما، چنین روشی برای تبدیل نتیجه به قالب کتابخانه ما ضروری است. برای غلبه بر این مشکل، ما مجبور شدیم مجموعه موضوعات ساده خود را پیاده سازی کنیم، زیرا به درخواست مشتری نمی توانیم از کتابخانه های شخص ثالث سنگین مانند Boost استفاده کنیم.

اجرای آن زمان ما به این صورت عمل می کند. هنگام فراخوانی، یک جفت وعده/آینده اضافی ایجاد می شود. آینده جدید برگردانده می‌شود و مورد ارسال شده به همراه تابع مربوطه و یک وعده اضافی در صف قرار می‌گیرد. یک رشته از استخر، چندین آتی را از صف انتخاب می کند و با استفاده از wait_for نظرسنجی می کند. هنگامی که یک نتیجه در دسترس می شود، تابع مربوطه فراخوانی می شود و مقدار بازگشتی آن به وعده داده می شود.

ما از همان Thread Pool برای اجرای پرس و جوها به etcd و Consul استفاده کردیم. این بدان معنی است که کتابخانه های زیربنایی می توانند توسط چندین رشته مختلف دسترسی داشته باشند. ppconsul ایمن نیست، بنابراین تماس با آن توسط قفل محافظت می شود.
شما می توانید با grpc از چندین رشته کار کنید، اما نکات ظریفی وجود دارد. در etcd ساعت ها از طریق جریان های grpc پیاده سازی می شوند. این ها کانال های دو طرفه برای پیام های نوع خاصی هستند. این کتابخانه یک رشته برای همه ساعت ها و یک رشته واحد ایجاد می کند که پیام های دریافتی را پردازش می کند. بنابراین grpc نوشتن موازی را برای استریم ممنوع می کند. این به این معنی است که هنگام تنظیم اولیه یا حذف یک ساعت، باید منتظر بمانید تا درخواست قبلی قبل از ارسال درخواست بعدی، ارسال کامل شود. ما برای همگام سازی استفاده می کنیم متغیرهای شرطی.

مجموع

خودت ببین: liboffkv.

تیم ما: رائد رومانوف, ایوان گلوشنکوف, دیمیتری کمالدینوف, ویکتور کراپیونسکی, ویتالی ایوانین.

منبع: www.habr.com

اضافه کردن نظر