Scheduling یکی از اجزاء اصلی Control Plane کوبرنتیز به حساب میآید. عملیات اصلی که این بخش از کوبرنتیز انجام میدهد، انتساب پادها به نودها و بالانس کردن منابع مصرفی میان آنهاست.
اما این تمام ماجرا نیست چرا که Scheduling فرایند کاری نسبتا پیچیدهای داشته و نیاز است که به صورت کامل با ساز و کار آن آشنا شویم تا بتوانیم به خوبی آن را درک کنیم.
در این مطلب از وبلاگ همروش قصد داریم با Scheduling در کوبرنتیز آشنا شویم، دلیل وجود چنین ساز و کاری را متوجه شویم و همچنین فرایند کاری آن را یاد بگیریم.
آشنایی با Scheduling در کوبرنتیز
در مرحله بعدی، زمانی که یک پاد به یک نود اضافه میشود، Kubelet که در داخل Node قرار دارد، مشخصات و پاد مربوطه را برگشت داده و منابع و کانتینرهای لازم برای اجرا شدن آن را تخصیص میدهد.
بسیاری از افرادی که با کوبرنتیز کار میکنند، ممکن است هیچوقت لاگها و پیکربندیهای مربوط به Scheduler را مطالعه نکرده باشند چرا که این بخش به صورت پیشفرض به بهترین شکل کار کرده و امورات را به درستی پیش میبرد.
اما برای افرادی که به صورت تخصصی و حرفهای روی کوبرنتیز کار میکنند، یادگیری پیکربندیها و تنظیمات مربوط به Scheduler بسیار مهم بوده و سعی میکنند که به صورت کامل با ساختار آن آشنا شوند. در ادامه این مطلب با چگونگی کار Scheduler و مفاهیم تخصصی آن آشنا خواهیم شد.
چرا به یک Scheduler نیاز داریم؟
کشتیها و کانتینرهای بسیار زیادی در دنیا وجود دارند که صاحبان این کانتینرها دوست دارند در زمان حمل و نقل، آن را در داخل بهترین کشتی در دسترس قرار دهند.
در دنیای کوبرنتیز نیز تقریبا به همین شکل است: شما باید مطمئن شوید که پاد کوبرنتیزیتان روی یک Node مناسب قرار گرفته و به خوبی اجرا میشود.
Scheduler فرایند انتخاب یک کشتی درست برای کانتینرتان را به صورت خودکار انجام میدهد. این تکنولوژی خصوصیات و نیازمندیهای پاد مربوطه را در نظر گرفته و آن را روی قابل اعتمادترین و بهترین Node قرار میدهد تا به خوبی اجرا شود.
Scheduler در کوبرنتیز چگونه کار میکند؟
Scheduler یک واحد یکپارچه در کوبرنتیز به شمار میرود و از API Server که مسئولیت مدیریت کلاستر را برعهده دارد جداگانه عمل میکند. همانطور که تا به اینجا مطلب متوجه شدید، وظیفه اصلی Scheduler معرفی پادها به نودهای مناسب و بهینه است.
اما Scheduler در این فرایند کارهای دیگری را نیز انجام میدهد. برای مثال در همان فرایند انتخاب Node برای یک پاد، Scheduler وظیفه دارد تا منابع آن Node را بررسی کند تا متوجه شود که آیا این نود برای پاد مورد نظر مناسب است یا خیر! گاهی اوقات نیز برای بالا بردن «نرخ در دسترس بودن» یک پاد بین چندین Node توزیع میشود.
در تمامی این مراحل اگر Scheduler نتواند Node مناسبی را برای پاد پیدا کند (اغلب به دلیل محدودیتها و قواعد affinity وanti-affinity که در ادامه با آنها نیز آشنا خواهیم شد)، پاد مربوطه unscheduled و سرگردان خواهد بود. البته Scheduler در این فرایند همواره به کار خود ادامه داده و تا زمانی که نود مناسبی را پیدا نکند متوقف نخواهد شد.
برای اینکه به صورت کلی این فرایند را از طریق یک مثال توضیح دهیم میتوانیم چنین سناریویی را در نظر بگیریم:
تصور کنید که شما یک اپلیکیشن را توسعه دادهاید و برای مستقر کردن آن به یک گیگابایت حافظه اصلی (RAM) و دو هسته از CPU نیاز دارید. در این حالت پادی که برای اپلیکیشن ایجاد شده نیاز دارد که روی Nodeیی قرار بگیرد که این میزان از منابع را در اختیار داشته باشد.
Scheduler به نودهای مربوطه نگاه میکند و در صورتی که بتواند نود مناسبی را پیدا کند، پاد را روی آن قرار میدهد. بعد از انجام این کار Scheduler سراغ پادهای دیگر میرود و این فرایند را تا زمانی که پادی وجود داشته باشد انجام میدهد.
اما اگر بخواهیم به صورت عمیقتر با این فرایند آشنا شویم، نیاز است که با فرایند ۱۲ مرحلهای کار Scheduler در کوبرنتیز آشنا شویم. در این فرایند دو مرحله اصلی Scheduling Cycle و Binding Cycle انجام میشود که در تصویر زیر به صورت کامل نشان داده شده است. همچنین برای مطالعه بیشتر این موضوع میتوانید مستندات Scheduling Framework در وبسایت کوبرنتیز را مطالعه کنید.
منظور از Node Affinity چیست؟
حالتی را تصور کنید که در آن دوست دارید پادهایتان تنها روی نوعهای خاصی از Node قرار بگیرند. در این حالت چکاری را باید انجام داد و چگونه باید به Scheduler بگویید که چه پادی روی چه نوع نودی قرار بگیرد؟
برای حل این مشکل مفهوم Node Affinity در کوبرنتیز ایجاد شده است. در این فرایند مدیر کلاستر برای هر نود یکسری Label یا برچسب تعیین میکند، برای مثال: نود بزرگ، نود متوسط و نود کوچک.
در مرحله بعد زمانی که یک پاد را ایجاد میکنید، میتوانید نزدیکترین نوع نود به این پاد را از طریق همان برچسبها تعیین کنید. برای مثال پاد X برای نود متوسط مناسب است و با آن قرابت یا نزدیکی دارد. در قطعه کد زیر میتوانید ببینید که پاد مورد نظر برای Nodeهایی تعریف شده است که برچسب Medium دارند.
apiVersion: v1
kind: Pod
metadata:
name: medium-pod
spec:
containers:
- name: data-processor
image: data-processor
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: size
operator: In
values:
- Large
- Medium
به صورت کلی چهار نوع Node Affinity وجود دارد که در زیر این اسامی ذکر شده است:
- requiredDuringSchedulingIgnoredDuringExecution
- preferredDuringSchedulingIgnoredDuringExecution
- requiredDuringSchedulingRequriedDuringExecution
- preferredDuringSchedulingRequiredDuringExecution
بنابراین اگر قصد دارید فرایند قرارگیری پادها روی نوعهای خاصی از نود را کنترل کنید، میتوانید از Node Affinity استفاده کنید.
البته این نکته را به یاد داشته باشید که در تعریف کردن این موارد نیاز است که به خوبی و با آگاهی کامل عمل کنید چرا که ممکن است در فرایند Scheduling و پیدا کردن نود مناسب با مشکل روبهرو شوید.
آشنایی با فایل scheduler.go
scheduler.go فایل اصلی Scheduler در کوبرنتیز است که شامل ۹ هزار خط کد میشود. البته که ما قصد نداریم با تمام بخشهای این فایل آشنا شویم، اما سه بخش اساسی در فرایند انجام کارها وجود دارد که راجع به آنها صحبت خواهیم کرد.
۱- انتظار برای ایجاد پاد
از خط ۸۹۷۰ یک بخشی در فایل scheduler.go نوشته شده است که یک وظیفه ساده را برعهده دارد: انتظار برای ایجاد پاد. در واقع این بخش یکی از وظایف اصلی Scheduler را انجام میدهد که به صورت همیشگی و دائم منتظر ایجاد یک پاد جدید و کسب اطلاع از آن است.
// Run begins watching and scheduling. It waits for cache to be synced, then starts a goroutine and returns immediately.
func (sched *Scheduler) Run() {
if !sched.config.WaitForCacheSync() {
return
}
go wait.Until(sched.scheduleOne, 0, sched.config.StopEverything)
۲- قرار دادن پاد در صف
در خط ۷۳۶۰ یک تابع ایجاد شده است که وظیفه قرار دادن پاد در صف تخصیص به Node را برعهده دارد. همانطور که تا به اینجا فهمیدیم، بعد از ایجاد پاد، Scheduler آن را وارد یک صف میکند و تا زمانی که یک نود مناسب را برای آن پیدا نکند آن را از صف خارج نمیسازد.
func (f *ConfigFactory) getNextPod() *v1.Pod {
for {
pod := cache.Pop(f.podQueue).(*v1.Pod)
if f.ResponsibleForPod(pod) {
glog.V(4).Infof("About to try and schedule pod %v", pod.Name)
return pod
}
}
}
۳- مدیریت خطا
در فرایند کاری Scheduler حالتهای بسیار زیادی پیش خواهد آمد که منجر به ایجاد خطا یا Error میشود. برای این موضوع نیاز است که فرایند مدیریت خطا به صورت خودکار انجام شود. در زیر میتوانید شیوه مدیریت خطاها در Scheduler را مشاهده کنید. عمده این خطاها مربوط به موفقیت آمیز نبودن در فرایند تخصیص پاد به یک نود است.
// scheduled pod cache
podInformer.Informer().AddEventHandler(
cache.FilteringResourceEventHandler{
FilterFunc: func(obj interface{}) bool {
switch t := obj.(type) {
case *v1.Pod:
return assignedNonTerminatedPod(t)
default:
runtime.HandleError(fmt.Errorf("unable to handle object in %T: %T", c, obj))
return false
}
},
جمعبندی
درک وظایف و ساختار Scheduler در کوبرنتیز برای هر متخصصی در این حوزه بسیار مفید بوده و به آنها کمک میکند تا به بهترین شکل ممکن با ساختار کاری کوبرنتیز بهصورت عمیقتری آشنا شوند.