تطوير مشغل Kubernetes مع إطار المشغل

صورة


كما هو مذكور في مقال 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 .

في التعليقات ، سيكون من المثير للاهتمام معرفة خبرتك في تطوير المشغلين. وفي الجزء التالي سنتحدث عن اختبار المشغل.

Source: https://habr.com/ru/post/ar446648/


All Articles