کوبرنتیز الانا به قدری گنده شده که معرفی نمیخواد دیگه. ولی خب برای یه توضیح کوتاه، کوبرنتیز یه container orchestratorه. نه اولینشونه و نه آخرینشون، ولی الان معروفترینشونه.
نمیخوام راجع به اینکه کوبرنتیز چیه صحبت کنم. میخوام یخرده راجع به تمایز جدیای که کوبرنتیز با نمونههای مشابه خودش داره صحبت کنم؛ یه تصمیم طراحی بزرگ و یه رویکرد متفاوت.
یه مسئلهی ساده بگم!:
یه چالشی که من بعضا دارم، جواب دادن به پیامهای تلگراممه. حالا توی کیسای عادی شاید خیلی چالش نباشه ولی خب مثلا غزال شاکی میشه اگه جوابشو تو یه تایم معقولی ندم :))).
حالا من رویکردم واسه اینکه بفهمم جوابی لازم هست بدم یا نه، میتونه این باشه که:
روش ۱: اگه نوتیفیکیشن اومد، برم و بهش جواب بدم که خب مشکل اینه که بعضی اوقات به دلایل مختلف نوتیفیکیشن نمیآد برام. (تو پاورسیوینگ مود باشم، اینترنت مشکل داشته باشه، ویپیان خراب شده باشه و...)
روش ۲: یه کار دیگهای که میتونم بکنم اینه که هر ۵ دیقه یه بار تلگرامم رو باز کنم و بذارم آپدیت شه که ببینم پیام جدید اومده یا نه. ولی حتی اینم مشکل داره. چون ممکنه دفعهی آخری که توی چت بودهم، به هر دلیلی، یه پیامی رو ندیده باشم یا یادم رفته باشه جواب بدم یا حتی تو ذهنم جواب داده باشم ولی ننوشته باشم! ( بله این اتفاق واقعا برای من میافته :))) )
روش ۳: کار آخری که میتونم انجام بدم و مطمئن باشم که دیگه حتما اگه پیامی هست جواب دادهم، اینه که هر ۲۰ دیقه یه بار چت رو باز کنم و پیامها رو بخونم و مطمئن بشم که همه رو جواب دادهم. منطقا هم چون کاره زمانبره، نمیتونم خیلی زود زود انجامش بدم.
به وضوح هر چقدر که پایین اومدیم، هزینهی کارمون بیشتر شد. ولی خب در عوض خیلی مطمئنتر عمل کردیم و تونستیم اتفاقات رو کمتر میس کنیم. یه نکتهی مهم دیگه هم اینه که توی هر کدوم از روشهای پایینتر، هنوز میشه رویکردهای بالاتر رو برای آپتیمایز بودن، داشت. یعنی چی؟ یعنی مثلا اگه من میخوام که ۲۰ دیقه یه بار چتو باز کنم و بخونم پیامها رو که مطمئن بشم همه رو جواب دادهم، اگه جایی نوتیفیکیشن اومد، خب میرم جواب میدم دیگه. واینمیستم ۲۰ دیقه بگذره. و مثلا هر ۵ دیقه یه بار هم فقط تلگرامو باز میکنم که آپدیت بشه اگه پیام جدیدی هست.
یخرده اگه بخوایم تکنیکالتر به کیسهامون نگاه بکنیم،
روش اولی Interruptه. یعنی من دارم کار خودمو میکنم، منتها از بیرون یه چیزی بهم push میشه و من بر اساس اون یه کاری انجام میدم.
روش دومی Pollingه. یعنی من هر چند وقت یه بار بررسی میکنم ببینم خبر جدیدی هست یا نه که بر اساسش یه کار انجام بدم.
هر دوتای این روشها، event triggeredن. یعنی من الان تو یه شرایطی هستم، و از بیرون یه event یا رخدادی رو مشاهده میکنم (حالا یا خودم میرم میپرسم، یا بهم میگن.) و بر اساسش یه اکشنی میزنم. و دقیقا نکتهای که داره اینه که شاید بهینه باشه، ولی همیشه شانس از دست دادن یه رخداد رو داریم. و زمانی که یه رخداد رو از دست دادیم، شاید دیگه نتونیم ببینیمش.
ولی رویکردی که توی سومی هست متفاوته. این مکانیزم رو بهش میگن Level triggered. یعنی دیگه اصلا چیزی به اسم event یا رخداد توش وجود نداره. چیزی که اهمیت داره stateه.
یعنی توی روش سوم، اصلا مهم نیست یه مسج کی اومده یا من نوتیفیکیشن گرفتهم یا قبلا خوندهم یا هر رخداد دیگهای. من الان مستقل از هر چیزی میدونم این پیامها وجود دارن و باید جوابشون هم وجود داشته باشه.
یعنی در واقع یک current state در حال حاضر وجود داره و من باید برسونمش به desired state. یعنی به یه شرایط دلخواه. و زمانی خوشحالم که current stateم همون desired stateم باشه.
این دوتا لفظ Level و Edge از طراحی سیستمهای دیجیتال میآن. اینکه کنترلر یه مدار، کارهایی که میخواد انجام بده رو روی عوض شدن مقدار یه سیگنال انجام میده یا روی یه مقدار خاصی از سیگنال. یعنی مثلا وقتی سیگنال x از ۰ میشه ۱ (edge) یا وقتی سیگنال ۱ه (level).
این رویکردیه که کوبرنتیز تو دیزاینش داره. یه جایی توش هست به اسم kube-apiserver که توش desired stateها نگهداری میشه. این استیتها توی یه سری منیفست نگهداری میشن. منیفست یه فایل با توصیفی از چیزیه که ما میخوایم. اینجا لازم به ذکره که API کوبرنتیز Declarativeه. یعنی تو چیزی که میخوای رو بهش میگی ولی بهش نمیگی چیکار بکن. اون خودش میدونه چیکار کنه که چیزی که میخوای رو بهت بده.
مثلا اون تو، یه منیفست هست که توش نوشته یه pod میخواد که یه کانتینرِ nginx روش باشه و ۲core سیپییو و ۴GB مموری داشته باشه. این منیفست وقتی ساخته میشه، به این معنی نیستش که چیزی که ما میخواستیم حاضره، به این معنیه که سفارش ثبت شده. هر موقع که هر اتفاقی بیفته، روی همون منیفست یه سری status ست میشه که بگه پادمون در چه فازیه الان.
به اپلیکیشنهایی که بر اساس منیفست (که desired stateه) و state فعلی، یه سری کارا انجام میدن که به شرایط دلخواه برسیم، میگیم «کنترلر». این کنترلرها یه فرایند کلی براشون تعریف میشه که وقتی یه منیفست رو بهشون میدی، بتونن شرایط فعلی رو به شرایط دلخواه برسونن. بعلاوه، هر چند وقت یه بار، حتی اگه هیچجای سیستم هیچ خبری هم نباشه، این فراینده واسه منیفستها اجرا میشه. در واقع دائما کنترلرها شرایط کلاستر رو با منیفستها سینک میکنن. به این عملیات سینک کردن تکرارشونده میگن Reconciliation. به کل این پترن طراحی هم میگن Controller Pattern.
البته برای اینکه لازم نباشه برای اعمال هر تغییری بر اساس منیفستها، به اندازهی یه interval صبر کنیم، کنترلرها علاوه بر اینکه هر چند وقت یه بار کل منیفستها رو reconcile میکنن، روی منیفستها watch میکنن و اگر تغییری کردن، reconcileشون رو در همون لحظه اجرا میکنن. مثل همون که ما میخوایم چت تلگرامو ۲۰ دیقه یه بار چک کنیم که همهی پیامها رو جواب داده باشیم، اما اگه حالا نوتیفیکیشن اومد، همون لحظه جواب بدیم که بهینهتر باشیم.
حالا دقیقا کجا این دیزاینه کمک میکنه؟
برای مثال اگه یه اتفاقی یه جایی افتاد و یه کنترلر بخاطر زمانبندی بد یا مشکل نتورک نتونست ببیندش (مثلا یه کانتینر exit کنه)، دفعهی بعدی میبیندش. اگه دفعهی بعدی هم نتونست، دفعهی بعدیش میبینه و به همین منوال.
یا مثلا اونجایی که این منیفستها ساخته شدهن و یهو وسط ساختن یه پاد، اون سرویسی که قراره بیاردش بالا یا مثلا نتورکشو ردیف کنه، میره پایین. وقتی که دوباره اومد بالا، دیگه اصلا براش مهم نیست کجای کار ول شده. وظیفهش اینه که الان پاد رو بیاره بالا؛ مستقل از اینکه state فعلی چیه، بفهمدش و بتونه state رو به طوری که میخوایم در بیاره.
یا برای مثال یه تغییر از بیرون اعمال میشه که یه چیزی رو خراب میکنه. وقتی کنترلر هر چند وقت یه بار میآد دوباره نگاه میکنه، مشکله رو درست میکنه.
جدای از اینا، این دیزاین نسبت به این که بشکونیمش به تیکههای کوچیک و یه حالت میکروسرویسی داشته باشیم، کاملا انعطافپذیره و علاوه بر این، همهی کنترلرها statelessن و همهی stateها جمع شدهن توی apiserver و هیچجای دیگه لازم نیست دیتایی نگه داشته بشه.
قطعا چنین طراحیای چالشای خودش رو هم داره. توی کنترلر، عملیاتهای reconciliation باید idempotent باشن. idempotency چیه؟ به این معنیه که یه کاریو اگه دوبار یا ده بار انجام بدی فرقی با اینکه یه بار انجام بدی نداشته باشه. برای مثال اگه یه اندپوینت برای آپدیت یه رکورد توی دیتابیس داشته باشیم، اگه با یه محتوا چند بار callش کنیم، در نهایت فرقی با اینکه یه بار callش کنیم نداره. یعنی در واقع اون عملیات idempotentه. ولی مثلا اگه یه اندپوینتی داشته باشیم که یه رکوردی رو یکی اضافه کنه(counter)، قطعا idempotent نیست.
حالا تو کنترلر چرا باید اینو داشته باشیم؟ چون بینهایت بار قراره یه منیفست reconcile بشه و بدیهتا اگه یه وقتی در شرایط درستیه، نباید شرایطش تغییر کنه.
یخرده بخوام در عمل توضیح بدم، توی کد کنترلر ما هیچوقت یه چیزیو نمیسازیم یا پاکش نمیکنیم. ما توی کد کنترلر وجود یا عدم وجودش رو سینک میکنیم. یعنی مثلا اگه میخوایم باشه، کاری که میکنیم اینه که چک میکنیم که هست یا نه. اگه نبود، میسازیمش. اگه بود چک میکنیم محتواش همونیه که ما میخوایم یا نه. اگه همون نبود، آپدیتش میکنیم. و اگه همون بود، رهاش میکنیم.
بخاطر اینه که میگن دیزاین کوبرنتیز جوریه که self-healingه. یعنی خودترمیمگره. اگه یه جایی خرابکاری بشه، بعدا بر میگرده درستش میکنه. هیچوقت هم نگران میس شدن یه event نیستیم دیگه. چون اصلا سیستم با event کار نمیکنه و هر آنچه که هست و اهمیت داره stateه.
در نهایت رویکردی که اینجا میبینیم تا حد خوبی با چیزی که برامون عادی باشه متفاوته. یه سری سختیهایی رو تحمیل میکنه به طراح و دولپر ولی در عین حال یه سیستمی رو در اختیارش قرار میده که میتونه نگرانیای کمتری از نظر reliability داشته باشه. چون یه سری چالشا با این دیزاین از بین میرن.
اگه دوست داشتین راجع به Controller Pattern عمیقتر بدونین و در عمل ببینید چه شکلیه، این پستها براتون جالب خواهند بود:
پانوشت: با تشکر از زید خوبم که در تمامی مراحل نوشتن این پست پشتیبان و یاور من بود =)))