
كما هو مذكور في مقال Radar Technology ، فإن Lamoda تتحرك بنشاط نحو بنية الخدمات الميكروية. يتم حزم معظم خدماتنا باستخدام هيلم ونشرها على Kubernetes. هذا النهج يلبي احتياجاتنا بالكامل في 99 ٪ من الحالات. يبقى 1٪ عندما لا تكون وظيفة Kubernetes القياسية كافية ، على سبيل المثال ، عندما تحتاج إلى تكوين تحديث للنسخ الاحتياطي أو الخدمة لحدث معين. لحل هذه المشكلة ، نستخدم نمط المشغل. في هذه السلسلة من المقالات ، سوف أتحدث أنا - غريغوري ميخالكين ، مطور فريق البحث والتطوير في لامودا - عن الدروس التي تعلمتها من تجربتي في تطوير مشغلي طائرات K8 باستخدام إطار عمل المشغل .
ما هو المشغل؟
تتمثل إحدى طرق توسيع وظيفة Kubernetes في إنشاء وحدات التحكم الخاصة بك. التجريدات الرئيسية في Kubernetes هي الأشياء وأجهزة التحكم. تصف الكائنات الحالة المطلوبة للكتلة. على سبيل المثال ، يصف Pod ما هي الحاويات التي يجب بدءها ومعلمات بدء التشغيل ، ويخبر كائن ReplicaSet عدد النسخ المتماثلة لهذا Pod التي يجب تشغيلها. تتحكم وحدات التحكم في حالة الكتلة بناءً على وصف الكائنات ، في الحالة الموضحة أعلاه ، ستدعم وحدة التحكم في النسخ المتماثل عدد النسخ المتماثلة لـ Pod المحددة في ReplicaSet. بمساعدة وحدات التحكم الجديدة ، يمكنك تنفيذ منطق إضافي ، مثل إرسال إشعارات للأحداث أو التعافي من الفشل أو إدارة موارد الجهة الخارجية .
المشغل هو تطبيق kubernetes يتضمن واحد أو أكثر من وحدات التحكم التي تخدم مورد طرف ثالث. تم اختراع هذا المفهوم من قبل فريق CoreOS في عام 2016 ، ومؤخراً ، تزايدت شعبية المشغلين بسرعة. يمكنك محاولة العثور على المشغل المطلوب في القائمة على kubedex ، (أكثر من 100 مشغل متاح للجمهور مدرجة هنا) ، وكذلك على OperatorHub . هناك 3 أدوات شائعة لتطوير المشغل: Kubebuilder و Operator SDK و Metacontroller . في Lamoda نستخدم Operator SDK ، لذلك سنتحدث عنه لاحقًا.
مشغل SDK

يعد المشغل SDK جزءًا من إطار عمل المشغل ، والذي يتضمن جزأين مهمين: مدير دورة حياة المشغل وقياس المشغل.
- يعد المشغل SDK عبارة عن غلاف لبرنامج وقت تشغيل وحدة التحكم ، وهي مكتبة شائعة لتطوير وحدات التحكم (والتي بدورها عبارة عن غلاف لبرنامج العميل ) ، وهو عبارة عن مولد رمز + إطار عمل لكتابة اختبارات E2E.
- مشغل دورة حياة المشغل - إطار عمل لإدارة المشغلين الحاليين ؛ يحل المواقف عندما يدخل المشغل وضع الزومبي أو يتم طرح إصدار جديد.
- قياس المشغل - كما يوحي الاسم ، فإنه يجمع البيانات عن عمل المشغل ، ويمكنه أيضًا إنشاء تقارير بناءً عليها.
إنشاء مشروع جديد
مثال على ذلك هو المشغل الذي يراقب ملفًا باستخدام التكوينات في المستودع ، وعند إعادة التحديث ، يقوم بإعادة تشغيل نشر الخدمة بتكوينات جديدة. رمز عينة كامل متاح هنا .

إنشاء مشروع مع مشغل جديد:
operator-sdk new config-monitor
سينشئ مُنشئ الكود رمزًا للمشغل الذي يعمل في مساحة الاسم المخصصة. هذا النهج أفضل من منح حق الوصول إلى المجموعة بأكملها ، لأنه في حالة حدوث أخطاء ، سيتم عزل المشاكل داخل نفس مساحة الاسم. يمكن إنشاء عامل التشغيل على cluster-wide
عن طريق إضافة --cluster-scoped
. ستكون الدلائل التالية موجودة داخل المشروع الذي تم إنشاؤه:
- cmd - يحتوي على
main package
، حيث Manager
تهيئة Manager
وإطلاقها ؛ - نشر - يحتوي على إعلانات للمشغل و CRD والأشياء الضرورية لإعداد مشغل RBAC ؛
- pkg - هنا سيكون رمزنا الرئيسي للكائنات الجديدة وأجهزة التحكم.
يوجد ملف cmd/manager/main.go
واحد فقط في cmd/manager/main.go
.
مقتطف الشفرة // Become the leader before proceeding err = leader.Become(ctx, "config-monitor-lock") if err != nil { log.Error(err, "") os.Exit(1) } // Create a new Cmd to provide shared dependencies and start components mgr, err := manager.New(cfg, manager.Options{ Namespace: namespace, MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort), }) ... // Setup Scheme for all resources if err := apis.AddToScheme(mgr.GetScheme()); err != nil { log.Error(err, "") os.Exit(1) } // Setup all Controllers if err := controller.AddToManager(mgr); err != nil { log.Error(err, "") os.Exit(1) } ... // Start the Cmd if err := mgr.Start(signals.SetupSignalHandler()); err != nil { log.Error(err, "Manager exited non-zero") os.Exit(1) }
في السطر الأول: err = leader.Become(ctx, "config-monitor-lock")
- يتم تحديد زعيم. في معظم السيناريوهات ، يلزم وجود مثيل نشط واحد فقط لبيان على مساحة الاسم / الكتلة. افتراضيًا ، يستخدم المشغل SDK استراتيجية Leader for life - أول مثيل للمشغل سيبقى هو الرائد حتى يتم إزالته من المجموعة.
بعد تعيين مثيل عامل التشغيل هذا كقائد ، تتم تهيئة Manager
جديد - mgr, err := manager.New(...)
. تشمل مسؤولياته:
err := apis.AddToScheme(mgr.GetScheme())
- تسجيل أنظمة الموارد الجديدة ؛err := controller.AddToManager(mgr)
- تسجيل وحدات التحكم ؛err := mgr.Start(signals.SetupSignalHandler())
- إطلاق والتحكم في وحدات التحكم.
في الوقت الحالي ، ليس لدينا موارد جديدة ولا وحدات تحكم للتسجيل. يمكنك إضافة مورد جديد باستخدام الأمر:
operator-sdk add api --api-version=services.example.com/v1alpha1 --kind=MonitoredService
سيضيف هذا الأمر تعريف مخطط مورد MonitoredService
إلى دليل pkg/apis
، بالإضافة إلى yaml مع تعريف CRD
في deploy/crds
. من بين جميع الملفات التي تم إنشاؤها ، يجب تغيير تعريف المخطط يدويًا فقط في monitoredservice_types.go
. يحدد نوع MonitoredServiceSpec
الحالة المطلوبة للمورد: ما يحدده المستخدم في yaml مع تعريف المورد. في سياق المشغل الخاص بنا ، يحدد حقل Size
العدد المطلوب ConfigRepo
المتماثلة ، يشير ConfigRepo
إلى المكان الذي يمكن سحب التكوينات الحالية منه. يحدد MonitoredServiceStatus
الحالة المرصودة للمورد ، على سبيل المثال ، فإنه يخزن أسماء القرون التي تنتمي إلى هذا المورد spec
الحالية.
بعد تحرير المخطط ، تحتاج إلى تشغيل الأمر:
operator-sdk generate k8s
سيتم تحديث تعريف CRD
في deploy/crds
.
الآن لنقم بإنشاء الجزء الرئيسي من المشغل لدينا ، وحدة التحكم:
operator-sdk add controller --api-version=services.example.com/v1alpha1 --kind=Monitor
monitor_controller.go
ملف monitor_controller.go
في دليل pkg/controller
، الذي نضيف إليه المنطق الذي نحتاج إليه.
تطوير المراقب المالي
وحدة التحكم هي وحدة العمل الرئيسية للمشغل. في حالتنا ، هناك جهازي تحكم:
- مراقب المراقب تراقب تغييرات تكوين الخدمة.
- تقوم وحدة التحكم بالترقية بتحديث الخدمة والحفاظ عليها في الحالة المطلوبة.
وحدة التحكم في جوهرها عبارة عن حلقة تحكم ، وتقوم بمراقبة قائمة الانتظار بالأحداث التي اشتركت فيها ومعالجتها:

يتم إنشاء وحدة تحكم جديدة وتسجيلها بواسطة المدير في طريقة add
:
c, err := controller.New("monitor-controller", mgr, controller.Options{Reconciler: r})
باستخدام طريقة Watch
، نشترك في الأحداث المتعلقة بإنشاء مورد جديد أو تحديث Spec
لمورد MonitoredService
موجود:
err = c.Watch(&source.Kind{Type: &servicesv1alpha1.MonitoredService{}}, &handler.EnqueueRequestForObject{}, common.CreateOrUpdateSpecPredicate)
يمكن تكوين نوع الحدث باستخدام src
والمعلمات predicates
. src
يقبل الكائنات من النوع Source
.
Informer
- يستقصي بشكل دوري apiserver
للأحداث التي apiserver
مع عامل التصفية ، إذا كان هناك مثل هذا الحدث ، يضعه في قائمة انتظار وحدة التحكم. في controller-runtime
يعد هذا برنامج التفاف على SharedIndexInformer
من client-go
.- تعد
Kind
أيضًا SharedIndexInformer
على SharedIndexInformer
، ولكنها ، على عكس Informer
، تقوم بإنشاء مثيل مخبر بشكل مستقل استنادًا إلى المعلمات التي تم تمريرها (مخطط المورد المراقبة). Channel
- تقبل chan event.GenericEvent
كمعلمة ، يتم وضع الأحداث القادمة من خلاله في قائمة انتظار وحدة التحكم.
تتوقع تتوقع الكائنات التي تلبي واجهة Predicate
. في الواقع ، يعد هذا مرشحًا إضافيًا للأحداث ، على سبيل المثال ، عند تصفية UpdateEvent
يمكنك أن ترى بالضبط التغييرات التي تم إجراؤها في spec
المورد.
عندما يصل حدث ما ، يقبل EventHandler
ذلك - الوسيطة الثانية لطريقة Watch
- التي تلتف الحدث بتنسيق الطلب الذي يتوقعه Reconciler
:
EnqueueRequestForObject
- بإنشاء طلب باسم ومساحة اسم الكائن الذي تسبب في حدوث الحدث ؛EnqueueRequestForOwner
- بإنشاء طلب مع بيانات أصل الكائن. يعد ذلك ضروريًا ، على سبيل المثال ، إذا كان Pod
تم حذف Pod
التحكم في الموارد ، وتحتاج إلى البدء في استبدالها ؛- يأخذ
EnqueueRequestsFromMapFunc
- كمعلمة ، وظيفة map
التي تتلقى حدثًا (ملفوفًا في MapObject
) وتُرجع قائمة بالطلبات. مثال على ذلك عندما تكون هناك حاجة إلى هذا المعالج - هناك جهاز توقيت ، لكل علامة تحتاج إلى سحب تكوينات جديدة لجميع الخدمات المتاحة.
يتم وضع الطلبات في قائمة انتظار جهاز التحكم ، وسحب أحد العمال (بشكل افتراضي وحدة التحكم واحد) الحدث خارج قائمة الانتظار وتمريره إلى Reconciler
.
يقوم Reconciler بتنفيذ طريقة واحدة فقط - Reconcile
، والتي تحتوي على المنطق الأساسي لمعالجة الأحداث:
طريقة التوفيق func (r *ReconcileMonitor) Reconcile(request reconcile.Request) (reconcile.Result, error) { reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) reqLogger.Info("Checking updates in repo for MonitoredService") // fetch the Monitor instance instance := &servicesv1alpha1.MonitoredService{} err := r.client.Get(context.Background(), request.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue return reconcile.Result{}, nil } // Error reading the object - requeue the request. return reconcile.Result{}, err } // check if service's config was updated // if it was, send event to upgrade controller if podSpec, ok := r.isServiceConfigUpdated(instance); ok { // Update instance Spec instance.Status.PodSpec = *podSpec instance.Status.ConfigChanged = true err = r.client.Status().Update(context.Background(), instance) if err != nil { reqLogger.Error(err, "Failed to update service status", "Service.Namespace", instance.Namespace, "Service.Name", instance.Name) return reconcile.Result{}, err } r.eventsChan <- event.GenericEvent{Meta: &servicesv1alpha1.MonitoredService{}, Object: instance} } return reconcile.Result{}, nil }
تقبل الطريقة كائن Request
بحقل NamespacedName
، يمكن من خلاله سحب المورد من ذاكرة التخزين المؤقت: r.client.Get(context.TODO(), request.NamespacedName, instance)
. في المثال ، يتم تقديم طلب إلى الملف مع تكوين الخدمة المشار إليه بواسطة حقل ConfigRepo
في spec
المورد. إذا تم تحديث التكوين ، يتم إنشاء حدث جديد من نوع GenericEvent
وإرساله إلى القناة التي يستمع إليها جهاز التحكم في Upgrade
.
بعد معالجة الطلب ، ترجع Reconcile
كائنًا من النوع Result
and error
. إذا كان الحقل " Result
هو Requeue: true
أو error != nil
، فسوف تُرجع وحدة التحكم الطلب إلى قائمة الانتظار مرة أخرى باستخدام طريقة queue.AddRateLimited
. سيتم إرجاع الطلب إلى قائمة الانتظار مع تأخير ، والذي يتم تحديده بواسطة RateLimiter
. بشكل افتراضي ، ItemExponentialFailureRateLimiter
استخدام ItemExponentialFailureRateLimiter
، مما يزيد من وقت التأخير أضعافا مضاعفة مع زيادة في عدد "إرجاع" الطلب. إذا لم Requeue
تعيين حقل Requeue
، ولم يحدث أي خطأ أثناء معالجة الطلب ، Queue.Forget
وحدة التحكم Queue.Forget
، مما يؤدي إلى إزالة الطلب من ذاكرة التخزين المؤقت RateLimiter
(وبالتالي إعادة تعيين عدد المرتجعات). في نهاية معالجة الطلب ، تقوم وحدة التحكم بإزالته من قائمة الانتظار باستخدام الأسلوب Queue.Done
.
إطلاق المشغل
تم وصف مكونات المشغل أعلاه ، وظل سؤال واحد: كيف نبدأ تشغيله. تحتاج أولاً إلى التأكد من تثبيت جميع الموارد اللازمة (للاختبار المحلي ، أوصي بإعداد minikube ):
# Setup Service Account kubectl create -f deploy/service_account.yaml # Setup RBAC kubectl create -f deploy/role.yaml kubectl create -f deploy/role_binding.yaml # Setup the CRD kubectl create -f deploy/crds/services_v1alpha1_monitoredservice_crd.yaml # Setup custom resource kubectl create -f deploy/crds/services_v1alpha1_monitoredservice_cr.yaml
بمجرد استيفاء الشروط المسبقة ، هناك طريقتان سهلتان لتشغيل العبارة للاختبار. الأسهل هو تشغيله خارج الكتلة باستخدام الأمر:
operator-sdk up local --namespace=default
الطريقة الثانية هي نشر عامل التشغيل في الكتلة. تحتاج أولاً إلى إنشاء صورة Docker مع المشغل:
operator-sdk build config-monitor-operator:latest
في ملف REPLACE_IMAGE
deploy/operator.yaml
، REPLACE_IMAGE
بـ config-monitor-operator:latest
:
sed -i "" 's|REPLACE_IMAGE|config-monitor-operator:latest|g' deploy/operator.yaml
إنشاء نشر مع بيان:
kubectl create -f deploy/operator.yaml
الآن في قائمة Pod
على المجموعة يجب أن تظهر Pod
مع خدمة اختبار ، وفي الحالة الثانية - واحدة أخرى مع مشغل.
بدلا من الاستنتاج أو أفضل الممارسات
تتمثل المشكلات الرئيسية لتطوير المشغل في الوقت الحالي في التوثيق الضعيف للأدوات والافتقار إلى أفضل الممارسات المعمول بها. عندما يبدأ مطور جديد في تطوير مشغل ، لا يوجد لديه مكان يبحث فيه عملياً عن أمثلة لتنفيذ متطلبات معينة ، لذلك لا مفر من الأخطاء. فيما يلي بعض الدروس التي تعلمناها من أخطائنا:
- إذا كان هناك تطبيقان مرتبطان ، فيجب عليك تجنب الرغبة في دمجهما مع مشغل واحد. خلاف ذلك ، يتم انتهاك مبدأ خدمات اقتران فضفاضة.
- عليك أن تتذكر الفصل بين المخاوف: يجب ألا تحاول تنفيذ كل المنطق في وحدة تحكم واحدة. على سبيل المثال ، يجدر نشر وظائف مراقبة التكوينات وإنشاء / تحديث مورد.
- يجب تجنب حظر المكالمات في طريقة
Reconcile
. على سبيل المثال ، يمكنك سحب التكوينات من مصدر خارجي ، ولكن إذا كانت العملية أطول ، فقم بإنشاء مجموعة goroutine لذلك ، وأرسل الطلب مرة أخرى إلى قائمة الانتظار ، مع الإشارة إلى الاستجابة Requeue: true
.
في التعليقات ، سيكون من المثير للاهتمام معرفة خبرتك في تطوير المشغلين. وفي الجزء التالي سنتحدث عن اختبار المشغل.