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

( الإنجليزية )
مقدمة
يعني إنشاء نظام موزع موثوق أن جميع العقد تستخدم التكوين الصحيح ، متزامن مع عقد أخرى. عادةً ما تستخدم تقنيات DevOps (terraform أو ansible أو شيء من هذا القبيل) لإنشاء ملفات التكوين تلقائيًا (غالبًا ما تكون خاصة بكل عقدة). نود أيضًا التأكد من أن جميع العقد المتفاعلة تستخدم بروتوكولات متطابقة (بما في ذلك نفس الإصدار). خلاف ذلك ، سيتم تضمين عدم التوافق في نظامنا الموزع. في عالم JVM ، إحدى نتائج هذا المطلب هي الحاجة إلى استخدام نفس الإصدار من مكتبة تحتوي على رسائل بروتوكول في كل مكان.
ماذا عن اختبار النظام الموزع؟ بالطبع ، نحن نفترض أن اختبارات الوحدة يتم توفيرها لجميع المكونات قبل الانتقال إلى اختبار التكامل. (لكي نتمكن من استقراء نتائج الاختبار إلى وقت التشغيل ، يجب علينا أيضًا توفير مجموعة متطابقة من المكتبات في مرحلة الاختبار وفي وقت التشغيل.)
عند العمل في اختبارات التكامل ، غالبًا ما يكون من السهل في كل مكان استخدام classpath واحد على جميع العقد. سيكون علينا فقط التأكد من أن نفس classpath متورط في وقت التشغيل. (على الرغم من أنه من الممكن تمامًا تشغيل عقد مختلفة باستخدام مسارات مدرسية مختلفة ، فإن هذا يؤدي إلى تعقيد التكوين بأكمله وصعوبات في اختبارات النشر والتكامل.) كجزء من هذا المنشور ، نفترض أنه سيتم استخدام نفس المسار على كل العقد.
التكوين يتطور مع التطبيق. لتحديد المراحل المختلفة لتطور البرامج ، نستخدم الإصدارات. يبدو من المنطقي أيضًا تحديد إصدارات مختلفة من التكوينات. ويجب وضع التكوين نفسه في نظام التحكم في الإصدار. إذا كان هناك تكوين واحد فقط في الإنتاج ، فيمكننا فقط استخدام رقم الإصدار. إذا تم استخدام العديد من مثيلات الإنتاج ، فإننا نحتاج إلى العديد من الأمثلة
فروع التكوين وتسمية إضافية بالإضافة إلى الإصدار (على سبيل المثال ، اسم الفرع). وبالتالي ، يمكننا تحديد التكوين الدقيق بشكل فريد. يتوافق كل معرف تكوين بشكل فريد مع مجموعة معينة من العقد الموزعة والمنافذ والموارد الخارجية وإصدارات المكتبة. في إطار هذا المنشور ، سننتقل من حقيقة أن هناك فرعًا واحدًا فقط ، ويمكننا تحديد التهيئة بالطريقة المعتادة باستخدام ثلاثة أرقام مفصولة بنقطة (1.2.3).
في البيئات الحديثة ، يتم إنشاء ملفات التكوين يدويًا بشكل نادر جدًا. في كثير من الأحيان يتم إنشاؤها أثناء النشر ولم يتم لمسها (حتى لا تكسر أي شيء ). يطرح سؤال منطقي ، لماذا لا نزال نستخدم تنسيق نصي لتخزين التكوين؟ البديل القابل للتطبيق تمامًا هو القدرة على استخدام التعليمات البرمجية العادية للتكوين والحصول على فوائد من الاختبارات في وقت الترجمة.
في هذا المنشور ، نستكشف فكرة تمثيل التكوين داخل قطعة أثرية مترجمة.
التكوين المترجمة
يصف هذا القسم مثالًا على التكوين المترجم الثابت. يتم تنفيذ خدمتين بسيطتين - خدمة الصدى وخدمة صدى العميل. بناءً على هاتين الخدمتين ، يتم تجميع نسختين من النظام. في أحد النماذج ، توجد كلتا الخدمتين على نفس العقدة ، في تجسيد آخر ، على عقد مختلفة.
عادةً ما يحتوي النظام الموزع على عدة عقد. يمكن تحديد العقد باستخدام قيم من نوع NodeId
:
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
مجرد عدد صحيح مع مجموعة من القيم الصالحة:
type PortNumber = Refined[Int, Closed[_0, W.`65535`.T]]
أنواع المكررةانظر المكتبة المكررة وتقريري. باختصار ، تتيح لك المكتبة إضافة قيود يتم فحصها في وقت التحويل البرمجي للأنواع. في هذه الحالة ، تكون قيم رقم المنفذ الصالحة عبارة عن أرقام عدد صحيح 16 بت. بالنسبة للتكوين المترجم ، يعد استخدام المكتبة المكررة أمرًا اختياريًا ، ولكنه يمكن أن يحسن قدرة برنامج التحويل البرمجي على التحقق من التكوين.
بالنسبة لبروتوكولات HTTP (REST) ، بالإضافة إلى رقم المنفذ ، قد نحتاج أيضًا إلى مسار للخدمة:
type UrlPathPrefix = Refined[String, MatchesRegex[W.`"[a-zA-Z_0-9/]*"`.T]] case class PortWithPrefix[Protocol](portNumber: PortNumber, pathPrefix: UrlPathPrefix)
أنواع الوهميةلتحديد البروتوكول في مرحلة الترجمة ، نستخدم معلمة كتابة غير مستخدمة داخل الفصل. يرجع هذا القرار إلى حقيقة أننا في وقت التشغيل لا نستخدم مثيل بروتوكول ، لكننا نود من المترجم التحقق من توافق البروتوكول. بفضل البروتوكول ، لن نتمكن من نقل الخدمة غير المناسبة كتبعية.
بروتوكول مشترك واحد هو REST API مع Json التسلسل:
sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]
حيث RequestMessage
هو نوع الطلب ، ResponseMessage
هو نوع الاستجابة.
بالطبع ، يمكنك استخدام أوصاف البروتوكول الأخرى التي توفر الدقة التي نطلبها.
لأغراض هذا المنشور ، سوف نستخدم نسخة مبسطة من البروتوكول:
sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]
هنا ، الطلب عبارة عن سلسلة تمت إضافتها إلى عنوان url ، والاستجابة هي السلسلة التي تم إرجاعها في نص استجابة HTTP.
يتم وصف تكوين الخدمة من خلال اسم الخدمة والمنافذ والتبعيات. يمكن تمثيل هذه العناصر في Scala بعدة طرق (على سبيل المثال ، HList
، وأنواع البيانات الجبرية). لأغراض هذا المنشور ، سوف نستخدم نموذج الكيك ونمثل الوحدات التي تستخدم trait
. (نموذج الكعكة ليس عنصرًا ضروريًا للنهج الموصوف. إنه مجرد تطبيق ممكن.)
يمكن تمثيل التبعيات بين الخدمات EndPoint
لإرجاع منافذ EndPoint
بالعقد الأخرى:
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) }
لإنشاء خدمة صدى ، يكفي فقط رقم المنفذ وإشارة إلى أن هذا المنفذ يدعم بروتوكول الارتداد. لم نتمكن من الإشارة إلى منفذ معين ، لأن تسمح لك السمات بتعريف الطرق دون التنفيذ (الأساليب المجردة). في هذه الحالة ، عند إنشاء تكوين محدد ، سيتطلب منا المحول البرمجي توفير تنفيذ طريقة مجردة وتوفير رقم منفذ. بما أننا طبقنا هذه الطريقة ، عند إنشاء تكوين محدد ، لا يمكننا تحديد منفذ آخر. سيتم استخدام القيمة الافتراضية.
في تكوين العميل ، نعلن التبعية على خدمة الصدى:
trait EchoClientConfig[A] { def testMessage: String = "test" def pollInterval: FiniteDuration def echoServiceDependency: HttpSimpleGetEndPoint[_, EchoProtocol[A]] }
التبعية من نفس النوع مثل خدمة تصدير echoService
. على وجه الخصوص ، في عميل الصدى نطلب نفس البروتوكول. لذلك ، عند توصيل الخدمتين ، يمكننا التأكد من أن كل شيء سوف يعمل بشكل صحيح.
تنفيذ الخدمةلبدء وإيقاف الخدمة ، مطلوب وظيفة. (تعد القدرة على إيقاف الخدمة أمرًا ضروريًا للاختبار.) مرة أخرى ، هناك عدة خيارات لتنفيذ هذه الوظيفة (على سبيل المثال ، يمكننا استخدام فئات الكتابة على أساس نوع التكوين). لأغراض هذا المنشور ، سنستخدم نموذج الكيك. cats.Resource
الخدمة باستخدام cats.Resource
class ، لأن في هذه الفئة ، يتم توفير وسائل الإفراج الآمن المضمون عن الموارد في حالة حدوث مشاكل. للحصول على المورد ، نحتاج إلى توفير تكوين وسياق وقت تشغيل جاهز. قد تكون وظيفة بدء الخدمة بالشكل التالي:
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
Applicative
- فئة نوع التأثير التي تسمح لك بدمج التأثيرات الفردية (أحادية اللون تقريبًا). في التطبيقات الأكثر تعقيدًا ، يبدو من الأفضل استخدام Monad
/ ConcurrentEffect
.
باستخدام هذا التوقيع الوظيفي ، يمكننا تنفيذ العديد من الخدمات. على سبيل المثال ، خدمة لا تفعل شيئًا:
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 }
يرجى ملاحظة أننا نشير إلى نوع التكوين الصحيح المطلوب لهذه العقدة. إذا نسينا تحديد أحد أنواع التكوين المطلوبة بواسطة خدمة منفصلة ، فسيحدث خطأ في التحويل البرمجي. أيضًا ، لن نتمكن من بدء العقدة إذا لم نوفر بعض الكائنات من النوع المناسب بكل البيانات اللازمة.
اسم المضيف القرارللاتصال بمضيف بعيد ، نحتاج إلى عنوان IP حقيقي. من الممكن أن يصبح العنوان معروفًا بعد بقية التكوين. لذلك ، نحتاج إلى وظيفة تقوم بتعيين معرف العقدة إلى العنوان:
case class NodeAddress[NodeId](host: Uri.Host) trait AddressResolver[F[_]] { def resolve[NodeId](nodeId: NodeId): F[NodeAddress[NodeId]] }
يمكنك تقديم عدة طرق لتنفيذ هذه الوظيفة:
- إذا أصبحت العناوين معروفة لنا قبل النشر ، فيمكننا إنشاء رمز Scala باستخدام
عناوين ثم ابدأ التجميع. هذا سوف يجمع ويدير الاختبارات.
في هذه الحالة ، ستُعرف الوظيفة بشكل ثابت ويمكن تمثيلها في الكود كعرض الخريطة Map[NodeId, NodeAddress]
. - في بعض الحالات ، يصبح عنوان صالح معروفًا فقط بعد بدء العقدة.
في هذه الحالة ، يمكننا تطبيق "خدمة اكتشاف" (اكتشاف) ، تعمل قبل العقد الأخرى وستسجل جميع العقد في هذه الخدمة وتطلب عناوين العقد الأخرى. - إذا استطعنا تعديل
/etc/hosts
، فبإمكاننا استخدام أسماء المضيف المعرفة مسبقًا (مثل my-project-main-node
و echo-backend
) وربط هذه الأسماء فقط
مع عناوين IP أثناء النشر.
في إطار هذا المنشور ، لن ننظر في هذه الحالات بمزيد من التفصيل. لدينا
في مثال لعبة ، سيكون لكل العقد عنوان IP واحد - 127.0.0.1
.
بعد ذلك ، ننظر في خيارين لنظام موزع:
- وضع جميع الخدمات على عقدة واحدة.
- ووضع خدمة الصدى وعميل الصدى على عقد مختلفة.
التكوين لعقدة واحدة :
تكوين عقدة واحدة 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 override def portNumber: PortNumber = 8088
ينفذ الكائن تكوين كل من العميل والخادم. يتم استخدام تكوين العمر أيضًا من أجل إنهاء البرنامج بعد lifetime
. (تعمل Ctrl-C أيضًا وتحرر جميع الموارد بشكل صحيح.)
يمكن استخدام نفس مجموعة سمات التكوين والتطبيقات لإنشاء نظام يتكون من عقدتين منفصلتين :
التكوين لاثنين من العقد 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 {
! المهم لاحظ كيف يتم تنفيذ ربط الخدمة. نحن نشير إلى أن الخدمة التي تنفذها إحدى العقدة هي تنفيذ طريقة التبعية لعقدة أخرى. يتم التحقق من نوع التبعية من قبل المترجم ، لأن يحتوي على نوع البروتوكول. عند بدء التشغيل ، ستتضمن التبعية المعرف الصحيح للعقدة الهدف. بفضل هذا المخطط ، نشير إلى رقم المنفذ بالضبط مرة واحدة ويضمن دائمًا الرجوع إلى المنفذ الصحيح.
تنفيذ عقدتين للنظامبالنسبة لهذا التكوين ، نستخدم تطبيق الخدمة نفسه بدون تغييرات. الفرق الوحيد هو أنه لدينا الآن كائنين يقومان بتطبيق مجموعات مختلفة من الخدمات:
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
، وتنتهي عقدة العميل بعد مرور بعض الوقت. انظر تطبيق الإطلاق .
عملية التنمية العامة
دعونا نرى كيف يؤثر نهج التكوين هذا على عملية التطوير الشاملة.
سيتم تجميع التكوين مع باقي الكود وسيتم إنشاء قطعة أثرية (. jar). على ما يبدو ، فمن المنطقي وضع التكوين في قطعة أثرية منفصلة. ويرجع ذلك إلى حقيقة أننا يمكن أن يكون لدينا العديد من التكوينات على أساس نفس الرمز. مرة أخرى ، يمكنك إنشاء القطع الأثرية التي تتوافق مع فروع التكوين المختلفة. إلى جانب التكوين ، يتم الحفاظ على التبعيات على إصدارات محددة من المكتبات والحفاظ على هذه الإصدارات إلى الأبد ، كلما قررنا نشر هذا الإصدار من التكوين.
أي تغيير التكوين يتحول إلى تغيير الرمز. وبالتالي ، كل هذا
سيتم تغطية التغيير من خلال عملية ضمان الجودة المعتادة:
تذكرة في bugtracker -> PR -> مراجعة -> دمج مع الفروع المقابلة ->
التكامل -> النشر
النتائج الرئيسية لتطبيق تكوين مترجمة:
سيتم تنسيق التكوين على جميع عقد النظام الموزع. نظرًا لحقيقة أن جميع العقد تتلقى نفس التكوين من مصدر واحد.
من الصعب تغيير التكوين في واحدة فقط من العقد. لذلك ، "انجراف التكوين" غير محتمل.
يصبح من الصعب إجراء تغييرات التكوين الصغيرة.
ستحدث معظم تغييرات التكوين كجزء من عملية التطوير الشاملة وسيتم مراجعتها.
هل أحتاج إلى مستودع منفصل لتخزين تكوين الإنتاج؟ قد يحتوي هذا التكوين على كلمات مرور ومعلومات سرية أخرى ، والتي نود الوصول إليها. بناءً على ذلك ، يبدو من المنطقي تخزين التكوين النهائي في مستودع منفصل. يمكنك تقسيم التكوين إلى قسمين - أحدهما يحتوي على إعدادات التكوين العامة والآخر يحتوي على إعدادات الوصول المقيد. سيسمح ذلك لمعظم المطورين بالوصول إلى المعلمات الشائعة. من السهل تحقيق هذا الفصل باستخدام السمات المتوسطة التي تحتوي على القيم الافتراضية.
الاختلافات المحتملة
دعنا نحاول مقارنة التكوين المترجم ببعض البدائل الشائعة:
- ملف نصي على الجهاز الهدف.
- تخزين مركزي ذو قيمة
etcd
( etcd
/ zookeeper
). - مكونات العملية التي يمكن إعادة تكوينها / إعادة تشغيلها دون إعادة تشغيل العملية.
- تخزين التكوين خارج قطعة أثرية والتحكم في الإصدار.
توفر الملفات النصية مرونة كبيرة من حيث التغييرات الصغيرة. يمكن لمسؤول النظام الانتقال إلى العقدة البعيدة وإجراء تغييرات على الملفات المقابلة وإعادة تشغيل الخدمة. بالنسبة للأنظمة الكبيرة ، قد تكون هذه المرونة غير مرغوب فيها. من التغييرات التي أدخلت لا توجد آثار في النظم الأخرى. لا أحد يستعرض التغييرات. من الصعب تحديد من قام بالتغييرات ولأي سبب. لا يتم اختبار التغييرات. إذا تم توزيع النظام ، فقد ينسى المسؤول إجراء التغيير المقابل على العقد الأخرى.
(تجدر الإشارة أيضًا إلى أن استخدام التكوين المترجم لا يمنع إمكانية استخدام الملفات النصية في المستقبل. سيكون كافياً لإضافة محلل ومدقق يقدم نفس نوع Config
كإخراج ، ويمكنك استخدام ملفات نصية. يتبع ذلك على الفور تعقيد النظام مع التكوين المترجم إلى حد ما أقل من تعقيد نظام يستخدم ملفات نصية ، لأن الملفات النصية تتطلب كودًا إضافيًا.)
التخزين المركزي ذو القيمة المركزية هو آلية جيدة لتوزيع المعلمات الوصفية للتطبيق الموزع. يجب أن نقرر ما هي معلمات التكوين وما هي البيانات فقط. لنفترض أن لدينا وظيفة C => A => B
، مع نادراً ما تتغير المعلمات C
، والبيانات A
غالبًا. في هذه الحالة ، يمكننا القول أن C
هي معلمات التكوين ، و A
هي البيانات. يبدو أن معلمات التكوين تختلف عن البيانات في أنها تتغير بشكل أقل تواتراً بشكل عام من البيانات. أيضًا ، تأتي البيانات عادةً من مصدر واحد (من المستخدم) ، ومعلمات التكوين من مصدر آخر (من مسؤول النظام).
إذا كانت هناك حاجة إلى تحديث المعلمات النادرة المتغيرة دون إعادة تشغيل البرنامج ، فقد يؤدي ذلك غالبًا إلى تعقيد البرنامج ، لأننا سنحتاج إلى تسليم المعلمات وتخزينها وتحليلها والتحقق منها ومعالجتها بطريقة غير صحيحة بطريقة ما. لذلك ، من وجهة نظر الحد من تعقيد البرنامج ، فمن المنطقي تقليل عدد المعلمات التي يمكن أن تتغير أثناء البرنامج (أو لا تدعم هذه المعلمات على الإطلاق).
من وجهة نظر هذا المنشور ، سنميز بين المعلمات الثابتة والديناميكية. إذا كان منطق الخدمة يتطلب تغيير المعلمات أثناء البرنامج ، فسوف نسمي هذه المعلمات ديناميكية. وإلا ، فإن المعلمات ثابتة ويمكن تهيئتها باستخدام تكوين مترجم. لإعادة التكوين الديناميكي ، قد نحتاج إلى آلية لإعادة تشغيل أجزاء من البرنامج بمعلمات جديدة ، على غرار كيفية إعادة تشغيل عمليات نظام التشغيل. (في رأينا ، من المستحسن تجنب إعادة التكوين في الوقت الفعلي ، مع زيادة تعقيد النظام. إذا كان ذلك ممكنًا ، فمن الأفضل استخدام إمكانات نظام التشغيل القياسية لإعادة تشغيل العمليات.)
يتمثل أحد الجوانب الهامة لاستخدام التكوين الثابت الذي يفرض على الأشخاص في التفكير في إعادة التكوين الديناميكي في الوقت الذي يستغرقه النظام لإعادة التشغيل بعد تحديث التكوين (تعطل). في الواقع ، إذا كنا بحاجة إلى إجراء تغييرات على التكوين الثابت ، فسيتعين علينا إعادة تشغيل النظام لتصبح القيم الجديدة نافذة المفعول. مشكلة التوقف مع خطورة مختلفة لأنظمة مختلفة. في بعض الحالات ، يمكنك جدولة إعادة تشغيل في وقت يكون الحمل فيه ضئيلًا. إذا كنت ترغب في تقديم خدمة مستمرة ، يمكنك تنفيذ "اتصالات الصرف" (استنزاف اتصال AWS ELB) . في الوقت نفسه ، عندما نحتاج إلى إعادة تشغيل النظام ، نطلق مثيلًا موازٍ لهذا النظام ، ونحول الموازن إليه ، وننتظر حتى تكتمل الاتصالات القديمة. بعد الانتهاء من جميع الاتصالات القديمة ، نقوم بإيقاف تشغيل مثيل النظام القديم.
دعونا الآن ننظر في مسألة تخزين التكوين داخل أو خارج قطعة أثرية. إذا قمنا بتخزين التكوين داخل قطعة أثرية ، فسنحصل على الأقل على فرصة أثناء تجميع القطعة للتأكد من صحة التكوين. إذا كان التكوين خارج قطعة أثرية مسيطر عليها ، فمن الصعب تتبع من ولماذا أجريت تغييرات على هذا الملف. ما مدى أهمية هذا؟ في رأينا ، من المهم بالنسبة للعديد من أنظمة الإنتاج تكوين مستقر وعالي الجودة.
يتيح لك إصدار الأداة تحديد وقت إنشائه ، والقيم التي يحتوي عليها ، والوظائف التي يتم تمكينها / تعطيلها ، والمسؤول عن أي تغيير في التكوين. بالطبع ، يتطلب تخزين التكوين داخل الأداة بعض الجهد ، لذلك تحتاج إلى اتخاذ قرار مستنير.
إيجابيات وسلبيات
أود أن أتطرق إلى إيجابيات وسلبيات التكنولوجيا المقترحة.
الفوائد
فيما يلي قائمة بالميزات الرئيسية لتكوين النظام الموزع المترجم:
- ثابت التكوين الاختيار. يسمح لك أن تكون متأكدا من ذلك
التكوين صحيح. - . . Scala , . ,
trait' , , val', (DRY) . ( Seq
, Map
, ). - DSL. Scala , DSL. , , , . , , .
- . , , , , . , . , .
- . , , .
- . , .
- . , . . ( , , , , -.) — . , , , , .
- . , . , , . . . , production'.
- . , . , , — . production- .
- الاختبار. mock-, , .
- . . , , , .
. :
- . production', . . . .
- . , , .
- . , , . / .
- . DevOps . .
- . (CI/CD). .
, :
- , , . , Cake Pattern' , ,
HList
(case class') . - , : (
package
, import
, ; override def
' , ). , DSL. , (, XML), . - .
استنتاج
Scala. xml- . , Scala, ( Kotlin, C#, Swift, ...). , , , , , .
, . .
:
- .
- DSL .
- . , , (1) ; (2) .
, .