التكوين المترجم للنظام الموزع

في هذا المنشور ، نود أن نشارك طريقة ممتعة للتعامل مع تهيئة النظام الموزع.
يتم تمثيل التكوين مباشرة في لغة Scala بطريقة آمنة من النوع. تطبيق مثال موصوف في التفاصيل. وتناقش جوانب مختلفة من الاقتراح ، بما في ذلك التأثير على عملية التنمية الشاملة.


عملية إدارة التكوين الشاملة


( باللغة الروسية )


مقدمة


يتطلب بناء أنظمة قوية موزعة استخدام التكوين الصحيح والمتماسك على جميع العقد. الحل النموذجي هو استخدام وصف النشر النصي (terraform ، غير مرئية أو شيء على حد سواء) وملفات التكوين التي تم إنشاؤها تلقائيًا (غالبًا - مخصصة لكل عقدة / دور). نود أيضًا استخدام نفس البروتوكولات الخاصة بالإصدارات نفسها على كل عقد اتصال (وإلا فقد نواجه مشكلات عدم التوافق). في عالم JVM ، هذا يعني أنه على الأقل يجب أن تكون مكتبة الرسائل من نفس الإصدار على جميع العقد المتصلة.


ماذا عن اختبار النظام؟ بالطبع ، يجب أن يكون لدينا اختبارات وحدة لجميع المكونات قبل الوصول إلى اختبارات التكامل. لتكون قادرًا على استقراء نتائج الاختبار في وقت التشغيل ، يجب أن نتأكد من أن إصدارات جميع المكتبات تبقى متطابقة في بيئتي التشغيل والاختبار.


عند إجراء اختبارات التكامل ، غالبًا ما يكون من السهل جدًا امتلاك نفس المسار على كل العقد. نحتاج فقط إلى التأكد من استخدام نفس الفصل الدراسي عند النشر. (من الممكن استخدام مسارات مدرسية مختلفة على عقد مختلفة ، ولكن من الصعب تمثيل هذا التكوين ونشره بشكل صحيح.) ومن أجل الحفاظ على الأمور بسيطة ، سننظر فقط في مسارات مدرسية متطابقة على جميع العقد.


يميل التكوين إلى التطور مع البرنامج. نستخدم عادة الإصدارات لتحديد مختلف
مراحل تطور البرمجيات. يبدو من المعقول تغطية التكوين تحت إدارة الإصدار وتحديد التكوينات المختلفة مع بعض التسميات. إذا كان هناك تكوين واحد فقط في الإنتاج ، فقد نستخدم إصدار واحد كمعرف. في بعض الأحيان قد يكون لدينا بيئات إنتاج متعددة. ولكل بيئة قد نحتاج إلى فرع منفصل من التكوين. لذلك ، قد تتم تسمية التكوينات بفرع وإصدار لتحديد التكوينات المختلفة بشكل فريد. يتوافق كل تسمية فرع وإصدار مع مجموعة واحدة من العقد الموزعة ، والموانئ ، والموارد الخارجية ، وإصدارات مكتبة classpath على كل عقدة. سنقوم هنا فقط بتغطية الفرع الفردي وتحديد التكوينات بواسطة إصدار عشري مكون من ثلاثة (1.2.3) ، بنفس طريقة القطع الأثرية الأخرى.


في البيئات الحديثة لم يتم تعديل ملفات التكوين يدويًا بعد الآن. عادة نحن نولد
قم بتكوين الملفات في وقت النشر ولا تلمسها أبدًا بعد ذلك. لذلك يمكن للمرء أن يتساءل لماذا ما زلنا نستخدم تنسيق النص لملفات التكوين؟ يتمثل الخيار القابل للتطبيق في وضع التكوين داخل وحدة الترجمة والاستفادة من التحقق من صحة تكوين وقت الترجمة.


في هذا المنشور سنبحث فكرة الحفاظ على التكوين في قطعة أثرية مترجمة.


التكوين المترجم


سنناقش في هذا القسم مثالًا للتكوين الثابت. يتم الآن تكوين وتنفيذ خدمتين بسيطتين - خدمة الارتداد وعميل خدمة الارتداد. ثم يتم إنشاء مثيل لنظامين موزعين مختلفين مع كلتا الخدمتين. واحد لتكوين عقدة واحدة والآخر لتكوين عقدتين.


يتكون النظام الموزع النموذجي من بضع عقد. يمكن تحديد العقد باستخدام نوع ما:


sealed trait NodeId case object Backend extends NodeId case object Frontend extends NodeId 

او فقط


 case class NodeId(hostName: String) 

او حتى


 object Singleton type NodeId = Singleton.type 

تؤدي هذه العقد أدوارًا متعددة وتدير بعض الخدمات ويجب أن تكون قادرة على الاتصال بالعقد الأخرى عن طريق اتصالات TCP / HTTP.


بالنسبة لاتصال TCP ، يلزم وجود رقم منفذ على الأقل. نريد أيضًا التأكد من أن العميل والخادم يتحدثان عن نفس البروتوكول. لنمذجة الاتصال بين العقد ، لنعلن الفئة التالية:


 case class TcpEndPoint[Protocol](node: NodeId, port: Port[Protocol]) 

حيث Port هو مجرد Int ضمن النطاق المسموح به:


 type PortNumber = Refined[Int, Closed[_0, W.`65535`.T]] 

أنواع المكررة

انظر المكتبة المكررة . باختصار ، يسمح بإضافة قيود وقت الترجمة إلى أنواع أخرى. في هذه الحالة ، يُسمح لـ Int فقط أن يكون لها قيم 16 بت يمكنها تمثيل رقم المنفذ. لا يوجد أي شرط لاستخدام هذه المكتبة لهذا النهج التكوين. يبدو فقط لتناسب بشكل جيد للغاية.


بالنسبة إلى HTTP (REST) ​​، قد نحتاج أيضًا إلى مسار الخدمة:


 type UrlPathPrefix = Refined[String, MatchesRegex[W.`"[a-zA-Z_0-9/]*"`.T]] case class PortWithPrefix[Protocol](portNumber: PortNumber, pathPrefix: UrlPathPrefix) 

نوع الوهمية

من أجل تحديد البروتوكول أثناء التحويل البرمجي ، نستخدم ميزة Scala في الإعلان عن Protocol وسيطة النوع غير المستخدم في الفصل الدراسي. إنه نوع شبح يسمى. في وقت التشغيل ، نادراً ما نحتاج إلى مثيل لمعرّف البروتوكول ، ولهذا السبب لا نقوم بتخزينه. أثناء التجميع ، يوفر هذا النوع الوهمي أمانًا إضافيًا من النوع. لا يمكننا تمرير منفذ مع بروتوكول غير صحيح.


أحد البروتوكولات الأكثر استخدامًا هو REST API مع تسلسل Json:


 sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage] 

حيث RequestMessage هو النوع الأساسي للرسائل التي يمكن للعميل إرسالها إلى الخادم و ResponseMessage هي رسالة الاستجابة من الخادم. بالطبع ، قد نقوم بإنشاء أوصاف بروتوكول أخرى تحدد بروتوكول الاتصال بالدقة المطلوبة.


لأغراض هذا المنشور ، سنستخدم إصدارًا أبسط من البروتوكول:


 sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage] 

في هذا البروتوكول ، يتم إلحاق رسالة طلب url وإرجاع رسالة الاستجابة كسلسلة عادية.


يمكن وصف تكوين الخدمة من خلال اسم الخدمة ومجموعة من المنافذ وبعض التبعيات. هناك بعض الطرق الممكنة لكيفية تمثيل كل هذه العناصر في Scala (على سبيل المثال ، HList ، وأنواع البيانات الجبرية). لأغراض هذا المنشور ، سوف نستخدم Cake Pattern ونمثل القطع المدمجة (الوحدات) كسمات. (لا يعد Cake Cake مطلبًا لنهج التكوين القابل للتجميع. إنه مجرد تنفيذ ممكن للفكرة.)


يمكن تمثيل التبعيات باستخدام Cake Pattern كنقاط نهاية للعقد الأخرى:


  type EchoProtocol[A] = SimpleHttpGetRest[A, A] trait EchoConfig[A] extends ServiceConfig { def portNumber: PortNumber = 8081 def echoPort: PortWithPrefix[EchoProtocol[A]] = PortWithPrefix[EchoProtocol[A]](portNumber, "echo") def echoService: HttpSimpleGetEndPoint[NodeId, EchoProtocol[A]] = providedSimpleService(echoPort) } 

تحتاج خدمة الارتداد فقط إلى منفذ تم تكوينه. ونعلن أن هذا المنفذ يدعم بروتوكول الصدى. لاحظ أننا لسنا بحاجة إلى تحديد منفذ معين في هذه اللحظة ، لأن السمات تسمح بإعلانات الطرق المجردة. إذا استخدمنا أساليب مجردة ، سيتطلب برنامج التحويل البرمجي تطبيقًا في مثيل التكوين. هنا قدمنا ​​التنفيذ ( 8081 ) وسيتم استخدامه كقيمة افتراضية إذا تخطينا في تكوين محدد.


يمكننا إعلان تبعية في تكوين عميل خدمة الصدى:


  trait EchoClientConfig[A] { def testMessage: String = "test" def pollInterval: FiniteDuration def echoServiceDependency: HttpSimpleGetEndPoint[_, EchoProtocol[A]] } 

التبعية لها نفس النوع مثل echoService . على وجه الخصوص ، فإنه يتطلب نفس البروتوكول. وبالتالي ، يمكننا أن نتأكد من أننا إذا قمنا بتوصيل هاتين التبعيتين ، فسوف يعملان بشكل صحيح.


تنفيذ الخدمات

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


  type ResourceReader[F[_], Config, A] = Reader[Config, Resource[F, A]] trait ServiceImpl[F[_]] { type Config def resource( implicit resolver: AddressResolver[F], timer: Timer[F], contextShift: ContextShift[F], ec: ExecutionContext, applicative: Applicative[F] ): ResourceReader[F, Config, Unit] } 

حيث


  • Config - نوع التكوين المطلوب من قبل كاتب الخدمة هذا
  • AddressResolver - كائن وقت تشغيل لديه القدرة على الحصول على عناوين حقيقية للعقد الأخرى (تابع القراءة للحصول على التفاصيل).

الأنواع الأخرى تأتي من cats :


  • F[_] - نوع التأثير (في أبسط الحالات يمكن أن تكون F[A] فقط () => A في هذا cats.IO سنستخدم cats.IO )
  • Reader[A,B] - هو مرادف للدالة A => B بشكل أو بآخر
  • cats.Resource - لديه طرق لاكتساب وإطلاق سراح
  • Timer - يسمح للنوم / قياس الوقت
  • ContextShift - التناظرية من ExecutionContext
  • تطبيقي - مجموعة من الوظائف سارية (شبه أحادية) (قد نستبدلها في النهاية بشيء آخر)

باستخدام هذه الواجهة يمكننا تنفيذ بعض الخدمات. على سبيل المثال ، خدمة لا تفعل شيئًا:


  trait ZeroServiceImpl[F[_]] extends ServiceImpl[F] { type Config <: Any def resource(...): ResourceReader[F, Config, Unit] = Reader(_ => Resource.pure[F, Unit](())) } 

(انظر شفرة المصدر لتطبيقات الخدمات الأخرى - خدمة الصدى ،
صدى العميل والتحكم مدى الحياة .)


العقدة هي كائن واحد يقوم بتشغيل خدمات قليلة (يتم تمكين بدء سلسلة من الموارد بواسطة Cake Pattern):


 object SingleNodeImpl extends ZeroServiceImpl[IO] with EchoServiceService with EchoClientService with FiniteDurationLifecycleServiceImpl { type Config = EchoConfig[String] with EchoClientConfig[String] with FiniteDurationLifecycleConfig } 

لاحظ أننا في العقدة نحدد نوع التهيئة المطلوب بواسطة هذه العقدة. لن يسمح لنا برنامج التحويل البرمجي بإنشاء الكائن (الكعكة) بنوع غير كافٍ ، لأن كل سمة خدمة تعلن عن وجود قيود على نوع Config . كما لن نتمكن من بدء العقدة دون توفير التكوين الكامل.


تحليل عنوان العقدة

من أجل إنشاء اتصال ، نحتاج إلى عنوان مضيف حقيقي لكل عقدة. قد يكون معروفًا لاحقًا عن الأجزاء الأخرى من التكوين. وبالتالي ، نحن بحاجة إلى طريقة لتوفير تعيين بين معرف العقدة وعنوانه الفعلي. هذا التعيين هو وظيفة:


 case class NodeAddress[NodeId](host: Uri.Host) trait AddressResolver[F[_]] { def resolve[NodeId](nodeId: NodeId): F[NodeAddress[NodeId]] } 

هناك بعض الطرق الممكنة لتنفيذ هذه الوظيفة.


  1. إذا علمنا بالعناوين الفعلية قبل النشر ، وأثناء إنشاء مضيف للعقدة ، فبإمكاننا إنشاء كود Scala باستخدام العناوين الفعلية وتشغيل البنية بعد ذلك (والتي تقوم بإجراء اختبارات وقت الترجمة ثم تقوم بتشغيل مجموعة اختبار التكامل). في هذه الحالة ، تُعرف وظيفة التعيين لدينا بشكل ثابت ويمكن تبسيطها إلى شيء مثل Map[NodeId, NodeAddress] .
  2. في بعض الأحيان ، نحصل على عناوين فعلية فقط عند نقطة لاحقة عندما تبدأ العقدة بالفعل ، أو ليس لدينا عناوين للعقد لم تبدأ بعد. في هذه الحالة ، قد يكون لدينا خدمة اكتشاف يتم تشغيلها قبل جميع العقد الأخرى وقد تعلن كل عقدة عن عنوانها في تلك الخدمة والاشتراك في التبعيات.
  3. إذا استطعنا تعديل /etc/hosts ، فيمكننا استخدام أسماء مضيف محددة مسبقًا (مثل my-project-main-node echo-backend ) وربط هذا الاسم بعنوان IP في وقت النشر.

في هذا المنشور لا نغطي هذه الحالات بمزيد من التفاصيل. في الواقع ، في مثال لعبتنا ، ستحصل جميع العقد على نفس عنوان IP - 127.0.0.1 .


سننظر في هذا المنشور في تخطيطين للنظام الموزعين:


  1. تخطيط عقدة واحدة ، حيث يتم وضع جميع الخدمات على عقدة واحدة.
  2. تخطيط العقدة اثنين ، حيث الخدمة والعميل على عقد مختلفة.

التكوين لتخطيط عقدة واحدة كما يلي:


تكوين عقدة واحدة
 object SingleNodeConfig extends EchoConfig[String] with EchoClientConfig[String] with FiniteDurationLifecycleConfig { case object Singleton // identifier of the single node // configuration of server type NodeId = Singleton.type def nodeId = Singleton /** Type safe service port specification. */ override def portNumber: PortNumber = 8088 // configuration of client /** We'll use the service provided by the same host. */ def echoServiceDependency = echoService override def testMessage: UrlPathElement = "hello" def pollInterval: FiniteDuration = 1.second // lifecycle controller configuration def lifetime: FiniteDuration = 10500.milliseconds // additional 0.5 seconds so that there are 10 requests, not 9. } 

هنا نقوم بإنشاء تكوين واحد يمتد تكوين الخادم والعميل. كما نقوم بتكوين وحدة تحكم دورة الحياة التي تنهي عادة العميل والخادم بعد مرور الفاصل الزمني lifetime .


يمكن استخدام نفس مجموعة من تطبيقات الخدمة والتكوينات لإنشاء تخطيط للنظام مع عقدتين منفصلتين. نحتاج فقط إلى إنشاء تكوينين منفصلين للعقدة مع الخدمات المناسبة:


تكوين العقدتين
  object NodeServerConfig extends EchoConfig[String] with SigTermLifecycleConfig { type NodeId = NodeIdImpl def nodeId = NodeServer override def portNumber: PortNumber = 8080 } object NodeClientConfig extends EchoClientConfig[String] with FiniteDurationLifecycleConfig { // NB! dependency specification def echoServiceDependency = NodeServerConfig.echoService def pollInterval: FiniteDuration = 1.second def lifetime: FiniteDuration = 10500.milliseconds // additional 0.5 seconds so that there are 10 request, not 9. def testMessage: String = "dolly" } 

انظر كيف نحدد التبعية. نذكر الخدمة المقدمة للعقدة الأخرى كتبعية للعقدة الحالية. يتم فحص نوع التبعية لأنه يحتوي على النوع الوهمي الذي يصف البروتوكول. وفي وقت التشغيل ، سيكون لدينا معرف العقدة الصحيح. هذا هو أحد الجوانب المهمة لنهج التكوين المقترح. يوفر لنا القدرة على تعيين منفذ مرة واحدة فقط والتأكد من أننا نشير إلى المنفذ الصحيح.


تنفيذ العقدتين

بالنسبة لهذا التكوين ، نستخدم بالضبط نفس تطبيقات الخدمات. لا توجد تغييرات على الإطلاق. ومع ذلك ، نقوم بإنشاء تطبيقين مختلفين للعقدة يحتويان على مجموعة مختلفة من الخدمات:


  object TwoJvmNodeServerImpl extends ZeroServiceImpl[IO] with EchoServiceService with SigIntLifecycleServiceImpl { type Config = EchoConfig[String] with SigTermLifecycleConfig } object TwoJvmNodeClientImpl extends ZeroServiceImpl[IO] with EchoClientService with FiniteDurationLifecycleServiceImpl { type Config = EchoClientConfig[String] with FiniteDurationLifecycleConfig } 

العقدة الأولى تنفذ الخادم وتحتاج فقط إلى تهيئة جانب الخادم. العقدة الثانية تنفذ العميل وتحتاج إلى جزء آخر من التكوين. كلا العقدتين تتطلب بعض مواصفات مدى الحياة. لأغراض عقد خدمة ما بعد الخدمة هذه ، سيكون لها عمر لانهائي يمكن إنهائه باستخدام SIGTERM ، في حين سينتهي عميل echo بعد انتهاء المدة المحددة. انظر تطبيق بداية للحصول على التفاصيل.


عملية التنمية الشاملة


دعونا نرى كيف يغير هذا النهج الطريقة التي نعمل بها مع التكوين.


سيتم تجميع التكوين كرمز وينتج قطعة أثرية. يبدو من المعقول فصل قطعة أثرية التكوين من القطع الأثرية رمز الأخرى. في كثير من الأحيان يمكن أن يكون لدينا العديد من التكوينات على نفس قاعدة الكود. وبالطبع ، يمكننا الحصول على إصدارات متعددة من فروع التكوين المختلفة. في التكوين ، يمكننا تحديد إصدارات معينة من المكتبات وسيظل هذا ثابتًا كلما نشرنا هذا التكوين.


تغيير التكوين يصبح تغيير الرمز. لذلك يجب تغطيتها بنفس عملية ضمان الجودة:


تذكرة -> PR -> مراجعة -> دمج -> تكامل مستمر -> نشر مستمر


هناك النتائج التالية لهذا النهج:


  1. التكوين متماسك لمثيل نظام معين. يبدو أنه لا توجد طريقة للاتصال غير صحيح بين العقد.


  2. ليس من السهل تغيير التكوين في عقدة واحدة فقط. يبدو من غير المعقول تسجيل الدخول وتغيير بعض الملفات النصية. لذلك يصبح الانجراف التكوين أقل ممكن.


  3. تغييرات التكوين الصغيرة ليست سهلة.


  4. ستتبع معظم تغييرات التكوين نفس عملية التطوير ، وستمر بعض المراجعة.



هل نحتاج إلى مستودع منفصل لتكوين الإنتاج؟ قد يحتوي تكوين الإنتاج على معلومات حساسة نود أن نبقيها في متناول العديد من الأشخاص. لذلك قد يستحق الاحتفاظ بمستودع منفصل مع وصول مقيد سيحتوي تكوين الإنتاج. قد نقوم بتقسيم التكوين إلى جزأين - أحدهما يحتوي على معظم معايير الإنتاج المفتوحة والآخر يحتوي على الجزء السري من التكوين. سيمكّن ذلك الوصول إلى معظم المطورين من الغالبية العظمى من المعلمات مع تقييد الوصول إلى الأشياء الحساسة حقًا. من السهل تحقيق ذلك باستخدام السمات المتوسطة بقيم المعلمة الافتراضية.


الاختلافات


دعونا نرى إيجابيات وسلبيات النهج المقترح مقارنة مع تقنيات إدارة التكوين الأخرى.


بادئ ذي بدء ، سنقوم بإدراج بعض البدائل للجوانب المختلفة للطريقة المقترحة للتعامل مع التكوين:


  1. ملف نصي على الجهاز الهدف.
  2. تخزين مركزي ذو قيمة رئيسية (مثل etcd / zookeeper ).
  3. مكونات العملية الفرعية التي يمكن إعادة تكوينها / إعادة تشغيلها دون إعادة تشغيل العملية.
  4. التكوين خارج قطعة أثرية والتحكم في الإصدار.

يمنح الملف النصي بعض المرونة فيما يتعلق بالإصلاحات المخصصة. يمكن لمسؤول النظام تسجيل الدخول إلى العقدة الهدف وإجراء تغيير وإعادة تشغيل الخدمة ببساطة. قد لا يكون هذا جيدًا للأنظمة الأكبر حجمًا. يتم ترك أي آثار وراء التغيير. لم تتم مراجعة التغيير من قبل زوج آخر من العيون. قد يكون من الصعب معرفة سبب التغيير. لم يتم اختباره. من منظور النظام الموزع ، يمكن للمسؤول ببساطة نسيان تحديث التكوين في إحدى العقد الأخرى.


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


التخزين المركزي ذي القيمة المركزية هو آلية جيدة لتوزيع معلمات تعريف التطبيق. نحن هنا بحاجة إلى التفكير فيما نعتبره قيم تكوين وما هي مجرد بيانات. بالنظر إلى دالة C => A => B فإننا ندعو عادةً إلى تغيير قيم C "التكوين" ، بينما يتم تغيير البيانات بشكل متكرر A - فقط إدخال البيانات. يجب توفير التكوين إلى الوظيفة الأقدم من البيانات A بالنظر إلى هذه الفكرة ، يمكننا القول إنه من المتوقع حدوث تغييرات في التغييرات التي يمكن استخدامها لتمييز بيانات التكوين عن البيانات فقط. كما تأتي البيانات عادة من مصدر واحد (مستخدم) والتكوين يأتي من مصدر مختلف (مسؤول). يؤدي التعامل مع المعلمات التي يمكن تغييرها بعد تهيئة العملية إلى زيادة تعقيد التطبيق. بالنسبة إلى هذه المعلمات ، سيتعين علينا التعامل مع آلية التسليم الخاصة بها والتحليل والتحقق من الصحة والتعامل مع القيم غير الصحيحة. وبالتالي ، من أجل تقليل تعقيد البرنامج ، من الأفضل أن نخفض عدد المعلمات التي يمكن أن تتغير في وقت التشغيل (أو حتى التخلص منها تمامًا).


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


يتمثل أحد الجوانب المهمة لاستخدام التكوين الثابت الذي يجعل الأشخاص يفكرون في التكوين الديناميكي (دون أسباب أخرى) في توقف الخدمة أثناء تحديث التكوين. في الواقع ، إذا كان يتعين علينا إجراء تغييرات على التكوين الثابت ، فعلينا إعادة تشغيل النظام حتى تصبح القيم الجديدة فعالة. تختلف متطلبات التوقف بالنسبة للأنظمة المختلفة ، لذلك قد لا يكون ذلك بالغ الأهمية. إذا كان الأمر بالغ الأهمية ، فعلينا أن نخطط للمستقبل لإعادة تشغيل أي نظام. على سبيل المثال ، يمكننا تنفيذ استنزاف اتصال AWS ELB . في هذا السيناريو ، عندما نحتاج إلى إعادة تشغيل النظام ، نبدأ بمثيل جديد للنظام بالتوازي ، ثم نحول ELB إليه ، بينما نسمح للنظام القديم بإكمال خدمة الاتصالات الحالية.


ماذا عن الحفاظ على التكوين داخل قطعة أثرية أو خارجها؟ يعني الحفاظ على التكوين داخل قطعة أثرية في معظم الحالات أن هذا التكوين اجتاز نفس عملية ضمان الجودة مثل القطع الفنية الأخرى. لذلك يمكن للمرء أن يكون متأكداً من أن التكوين من نوعية جيدة وجديرة بالثقة. على التكوين المعاكس في ملف منفصل يعني أنه لا توجد آثار لمن ولماذا أجريت تغييرات على هذا الملف. هل هذا مهم؟ نعتقد أنه بالنسبة لمعظم أنظمة الإنتاج ، من الأفضل أن يكون لديك تكوين مستقر وعالي الجودة.


يسمح إصدار الأداة بمعرفة وقت إنشائه ، وما القيم التي يحتوي عليها ، والميزات التي يتم تمكينها / تعطيلها ، والمسؤول عن إجراء كل تغيير في التكوين. قد يتطلب الأمر بعض الجهد للحفاظ على التكوين داخل قطعة أثرية ، وهو اختيار يصممه.


إيجابيات وسلبيات


هنا نود تسليط الضوء على بعض المزايا ومناقشة بعض عيوب النهج المقترح.


مزايا


ميزات التكوين المترجم لنظام توزيع كامل:


  1. تحقق ثابت من التكوين. هذا يعطي درجة عالية من الثقة ، أن التكوين صحيح قيود نوع معين.
  2. لغة غنية من التكوين. عادةً ما تقتصر طرق التكوين الأخرى على استبدال متغير على الأكثر.
    باستخدام Scala يمكن للمرء استخدام مجموعة واسعة من ميزات اللغة لجعل التكوين أفضل. على سبيل المثال ، يمكننا استخدام السمات لتوفير القيم الافتراضية ، والكائنات لتعيين نطاق مختلف ، ويمكننا الرجوع إلى val s المحدد مرة واحدة فقط في النطاق الخارجي (DRY). من الممكن استخدام التسلسل الحرفي ، أو مثيلات لفئات معينة ( Seq ، Map ، إلخ).
  3. DSL. Scala لديه دعم لائق لكتاب DSL. يمكن للمرء استخدام هذه الميزات لإنشاء لغة تكوين أكثر ملاءمةً للمستخدم النهائي ، بحيث يكون التكوين النهائي قابلاً للقراءة على الأقل من قِبل مستخدمي المجال.
  4. النزاهة والتماسك عبر العقد. أحد فوائد امتلاك التكوين للنظام الموزع بالكامل في مكان واحد هو أن جميع القيم يتم تعريفها بدقة مرة واحدة ثم يتم إعادة استخدامها في جميع الأماكن التي نحتاج إليها. اكتب أيضًا إقرارات المنافذ الآمنة التي تضمن أنه في جميع التكوينات الصحيحة الممكنة ، ستتحدث عقد النظام بنفس اللغة. هناك تبعية واضحة بين العقد مما يجعل من الصعب نسيان توفير بعض الخدمات.
  5. جودة عالية من التغييرات. النهج العام لتمرير تغييرات التكوين من خلال عملية العلاقات العامة العادية يحدد معايير عالية من الجودة أيضا في التكوين.
  6. تغييرات التكوين في وقت واحد. كلما أجرينا أي تغييرات في التكوين ، يضمن النشر التلقائي تحديث كل العقد.
  7. تبسيط التطبيق. لا يحتاج التطبيق إلى تحليل التكوين والتحقق من صحته ومعالجة قيم التكوين غير الصحيحة. هذا يبسط التطبيق الكلي. (توجد بعض التعقيدات في التهيئة نفسها ، لكنها مفاضلة واعية نحو الأمان.) من السهل جدًا العودة إلى التكوين العادي - فقط أضف القطع المفقودة. من الأسهل البدء في التكوين المترجم وتأجيل تنفيذ القطع الإضافية إلى بعض الأوقات اللاحقة.
  8. التكوين الإصدار. نظرًا لحقيقة أن تغييرات التكوين تتبع نفس عملية التطوير ، ونتيجة لذلك ، نحصل على قطعة أثرية بإصدار فريد. يسمح لنا بتبديل التكوين مرة أخرى إذا لزم الأمر. يمكننا حتى نشر التكوين الذي تم استخدامه منذ عام وسيعمل بنفس الطريقة تمامًا. يعمل التكوين المستقر على تحسين إمكانية التنبؤ وموثوقية النظام الموزع. يتم إصلاح التكوين في وقت الترجمة ولا يمكن العبث به بسهولة على نظام الإنتاج.
  9. نمطية. الإطار المقترح هو وحدات ويمكن دمج الوحدات بطرق مختلفة
    دعم تكوينات مختلفة (الاجهزة / تخطيطات). على وجه الخصوص ، من الممكن أن يكون هناك تخطيط لعقدة مفردة صغيرة الحجم وإعداد عقدة متعددة على نطاق واسع. من المعقول أن يكون لديك تخطيطات إنتاج متعددة.
  10. الاختبار. لأغراض الاختبار ، يمكن للمرء تنفيذ خدمة وهمية واستخدامها كاعتماد بطريقة آمنة على الكتابة. يمكن الحفاظ على تخطيطات اختبار مختلفة مع أجزاء مختلفة استبدالها mocks في وقت واحد.
  11. اختبار التكامل. في بعض الأحيان في الأنظمة الموزعة ، يصعب إجراء اختبارات التكامل. باستخدام الطريقة الموضحة لكتابة التكوين الآمن للنظام الموزع الكامل ، يمكننا تشغيل جميع الأجزاء الموزعة على خادم واحد بطريقة يمكن التحكم فيها. من السهل محاكاة الوضع
    عندما تصبح إحدى الخدمات غير متوفرة.

عيوب


يختلف أسلوب التكوين المترجم عن التكوين "العادي" وقد لا يناسب جميع الاحتياجات. فيما يلي بعض عيوب التكوين المترجم:


  1. التكوين ثابت. قد لا تكون مناسبة لجميع التطبيقات. في بعض الحالات ، هناك حاجة إلى تحديد التكوين بسرعة في الإنتاج وتجاوز جميع تدابير السلامة. هذا النهج يجعل الأمر أكثر صعوبة. التجميع وإعادة النشر مطلوبة بعد إجراء أي تغيير في التكوين. هذه هي الميزة والعبء.
  2. جيل التكوين. عندما يتم تكوين config بواسطة بعض أدوات التشغيل الآلي ، يتطلب هذا الأسلوب ترجمة لاحقة (والتي قد تفشل بدورها). قد يتطلب الأمر بذل جهد إضافي لدمج هذه الخطوة الإضافية في نظام الإنشاء.
  3. الصكوك. هناك الكثير من الأدوات المستخدمة اليوم والتي تعتمد على التكوينات النصية. بعض منهم
    لن يكون قابلاً للتطبيق عند تجميع التكوين.
  4. هناك حاجة إلى تحول في عقلية. المطورين و DevOps معتادون على ملفات تكوين النص. قد تبدو فكرة تجميع التكوين غريبة بالنسبة لهم.
  5. قبل تقديم التكوين المترجم ، يلزم إجراء عملية تطوير برامج عالية الجودة.

هناك بعض القيود على المثال المطبق:


  1. إذا قمنا بتوفير تهيئة إضافية لا يطلبها تطبيق العقدة ، فلن يساعدنا برنامج التحويل البرمجي في اكتشاف التطبيق الغائب. يمكن معالجة ذلك باستخدام HList أو ADTs (فئات الحالة) لتكوين العقدة بدلاً من السمات ونمط الكعكة.
  2. يتعين علينا توفير بعض ملفات التعريف في ملف التكوين: ( package ، import ، بيانات object ؛
    override def للمعلمات التي لها قيم افتراضية). قد تتم معالجة ذلك جزئيًا باستخدام DSL.
  3. في هذا المنشور ، لا نغطي إعادة التشكيل الديناميكي لمجموعات العقد المشابهة.

استنتاج


في هذا المنشور ناقشنا فكرة تمثيل التهيئة مباشرة في الكود المصدري بطريقة آمنة. يمكن استخدام النهج في العديد من التطبيقات كبديل للتكوينات المستندة إلى xml والتنسيقات النصية الأخرى. على الرغم من أن مثالنا قد تم تنفيذه في سكالا ، إلا أنه يمكن ترجمته إلى لغات أخرى قابلة للترجمة (مثل Kotlin ، C # ، Swift ، إلخ). يمكن للمرء أن يجرب هذا النهج في مشروع جديد ، وإذا لم يكن مناسبًا بشكل جيد ، فانتقل إلى الطريقة القديمة.


بالطبع ، التكوين المترجم يتطلب عملية تطوير عالية الجودة. في المقابل ، يعد بتوفير تكوين قوي عالي الجودة على قدم المساواة.


يمكن توسيع هذا النهج بطرق مختلفة:


  1. يمكن للمرء استخدام وحدات الماكرو لإجراء التحقق من صحة التكوين والفشل في وقت الترجمة في حالة حدوث أي فشل في قيود منطق العمل.
  2. يمكن تنفيذ DSL لتمثيل التهيئة بطريقة سهلة الاستخدام للمجال.
  3. إدارة ديناميكية للموارد مع تعديلات التكوين التلقائي. على سبيل المثال ، عندما نقوم بضبط عدد عقد الكتلة ، فقد نرغب في (1) الحصول على تكوين معدّل قليلاً ؛ (2) مدير الكتلة لتلقي معلومات العقد الجديدة.

شكر


أود أن أقول شكراً لأندري ساكسونوف ، بافيل بوبوف ، أنتون نيهايف لإبداء ملاحظات ملهمة حول مسودة هذا المقال الذي ساعدني في جعله أكثر وضوحًا.

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


All Articles