پیکربندی قابل کامپایل یک سیستم توزیع شده

در این پست می خواهیم روش جالبی برای مقابله با پیکربندی یک سیستم توزیع شده به اشتراک بگذاریم.
پیکربندی مستقیماً در زبان اسکالا به شیوه ای امن نمایش داده می شود. اجرای نمونه با جزئیات شرح داده شده است. جنبه های مختلف این پیشنهاد، از جمله تأثیر بر روند کلی توسعه مورد بحث قرار می گیرد.

پیکربندی قابل کامپایل یک سیستم توزیع شده

(در روسی)

معرفی

ساختن سیستم های توزیع شده قوی مستلزم استفاده از پیکربندی صحیح و منسجم در همه گره ها است. یک راه حل معمولی استفاده از شرح استقرار متنی (terraform، ansible یا چیزی مشابه) و فایل های پیکربندی خودکار تولید شده (اغلب - اختصاص داده شده برای هر گره/نقش) است. همچنین می‌خواهیم از پروتکل‌های مشابه نسخه‌های مشابه در هر گره ارتباطی استفاده کنیم (در غیر این صورت مشکلات ناسازگاری را تجربه خواهیم کرد). در دنیای JVM این بدان معناست که حداقل کتابخانه پیام‌رسانی باید در تمام گره‌های ارتباطی از یک نسخه باشد.

در مورد تست سیستم چطور؟ البته قبل از آمدن به تست های ادغام باید تست های واحد برای همه اجزا داشته باشیم. برای اینکه بتوانیم نتایج آزمایش را در زمان اجرا برون یابی کنیم، باید مطمئن شویم که نسخه های تمام کتابخانه ها در هر دو محیط اجرا و آزمایش یکسان نگه داشته می شوند.

هنگام اجرای تست های ادغام، اغلب ساده تر است که مسیر کلاسی یکسانی در همه گره ها داشته باشید. فقط باید مطمئن شویم که از همان classpath در استقرار استفاده می شود. (این امکان وجود دارد که از کلاس‌های مختلف در گره‌های مختلف استفاده کنیم، اما نمایش این پیکربندی و استقرار صحیح آن دشوارتر است.) بنابراین برای ساده نگه داشتن موارد، ما فقط مسیرهای کلاسی یکسان را در همه گره‌ها در نظر می‌گیریم.

پیکربندی همراه با نرم افزار به تکامل می رسد. ما معمولا از نسخه ها برای شناسایی انواع مختلف استفاده می کنیم
مراحل تکامل نرم افزار به نظر منطقی است که پیکربندی را تحت مدیریت نسخه پوشش دهیم و پیکربندی های مختلف را با برخی برچسب ها شناسایی کنیم. اگر فقط یک پیکربندی در تولید وجود داشته باشد، ممکن است از نسخه تک به عنوان شناسه استفاده کنیم. گاهی ممکن است چندین محیط تولید داشته باشیم. و برای هر محیط ممکن است به یک شاخه پیکربندی جداگانه نیاز داشته باشیم. بنابراین پیکربندی ها ممکن است با شاخه و نسخه برچسب گذاری شوند تا پیکربندی های مختلف را به طور منحصر به فرد شناسایی کنند. هر برچسب شاخه و نسخه مربوط به یک ترکیب واحد از گره های توزیع شده، پورت ها، منابع خارجی، نسخه های کتابخانه 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، انواع داده های جبری). برای اهداف این پست ما از الگوی کیک استفاده می کنیم و قطعات (ماژول ها) قابل ترکیب را به عنوان ویژگی نشان می دهیم. (الگوی کیک برای این رویکرد پیکربندی قابل کامپایل الزامی نیست. این فقط یک پیاده سازی ممکن از ایده است.)

وابستگی ها را می توان با استفاده از الگوی کیک به عنوان نقطه پایانی گره های دیگر نشان داد:

  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)
  }

سرویس اکو فقط به یک پورت پیکربندی شده نیاز دارد. و ما اعلام می کنیم که این پورت از پروتکل echo پشتیبانی می کند. توجه داشته باشید که در حال حاضر نیازی به تعیین پورت خاصی نداریم، زیرا trait's امکان اعلان متدهای انتزاعی را می دهد. اگر از روش‌های انتزاعی استفاده کنیم، کامپایلر به یک پیاده‌سازی در یک نمونه پیکربندی نیاز دارد. در اینجا ما پیاده سازی را ارائه کرده ایم (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.)
  • Reader[A,B] - کم و بیش مترادف یک تابع است A => B
  • cats.Resource - راه هایی برای به دست آوردن و انتشار دارد
  • Timer - امکان خواب / اندازه گیری زمان را فراهم می کند
  • ContextShift - آنالوگ از ExecutionContext
  • Applicative - بسته بندی توابع در حال اجرا (تقریبا یک موناد) (در نهایت ممکن است آن را با چیز دیگری جایگزین کنیم)

با استفاده از این رابط می توانیم چند سرویس را پیاده سازی کنیم. به عنوان مثال، سرویسی که هیچ کاری انجام نمی دهد:

  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
}

توجه داشته باشید که در گره نوع دقیق پیکربندی مورد نیاز این گره را مشخص می کنیم. کامپایلر به ما اجازه نمی دهد که شی (Cake) را با نوع ناکافی بسازیم، زیرا هر صفت سرویس یک محدودیت را در Config نوع همچنین بدون ارائه پیکربندی کامل نمی‌توانیم گره را راه‌اندازی کنیم.

وضوح آدرس گره

برای ایجاد یک اتصال، ما به یک آدرس میزبان واقعی برای هر گره نیاز داریم. ممکن است دیرتر از سایر بخش‌های پیکربندی شناخته شود. از این رو، ما به راهی برای ارائه یک نقشه بین شناسه گره و آدرس واقعی آن نیاز داریم. این نگاشت یک تابع است:

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

چند راه ممکن برای اجرای چنین عملکردی وجود دارد.

  1. اگر آدرس‌های واقعی را قبل از استقرار، در طول نمونه‌سازی میزبان‌های گره بدانیم، می‌توانیم کد Scala را با آدرس‌های واقعی تولید کنیم و سپس ساخت را اجرا کنیم (که بررسی‌های زمان کامپایل را انجام می‌دهد و سپس مجموعه تست یکپارچه‌سازی را اجرا می‌کند). در این مورد تابع نگاشت ما به صورت ایستا شناخته شده است و می توان آن را به چیزی شبیه a ساده کرد Map[NodeId, NodeAddress].
  2. گاهی اوقات آدرس های واقعی را فقط در زمان دیگری که گره واقعاً شروع شده است به دست می آوریم، یا آدرس گره هایی را نداریم که هنوز راه اندازی نشده اند. در این مورد ممکن است یک سرویس کشف داشته باشیم که قبل از همه گره‌های دیگر شروع شده باشد و هر گره ممکن است آدرس خود را در آن سرویس تبلیغ کند و در وابستگی‌ها مشترک شود.
  3. اگر بتوانیم اصلاح کنیم /etc/hosts، می توانیم از نام های میزبان از پیش تعریف شده استفاده کنیم (مانند my-project-main-node و echo-backend) و فقط این نام را با آدرس IP در زمان استقرار مرتبط کنید.

در این پست به جزئیات بیشتر این موارد نمی پردازیم. در واقع در مثال اسباب بازی ما، همه گره ها آدرس IP یکسانی خواهند داشت - 127.0.0.1.

در این پست ما دو طرح بندی سیستم توزیع شده را در نظر خواهیم گرفت:

  1. طرح بندی تک گره، که در آن تمام سرویس ها بر روی یک گره قرار می گیرند.
  2. طرح بندی دو گره، که در آن سرویس و مشتری روی گره های مختلف قرار دارند.

پیکربندی برای a تک گره چیدمان به شرح زیر است:

پیکربندی تک گره

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 پس از مدت زمان محدود پیکربندی شده خاتمه می یابد. را ببینید برنامه شروع برای جزئیات بیشتر.

فرآیند کلی توسعه

بیایید ببینیم چگونه این رویکرد نحوه کار ما با پیکربندی را تغییر می دهد.

پیکربندی به عنوان کد کامپایل می شود و یک مصنوع تولید می کند. به نظر منطقی است که آرتیفکت پیکربندی را از سایر مصنوعات کد جدا کنیم. اغلب می‌توانیم تعداد زیادی از تنظیمات را بر روی یک پایه کد داشته باشیم. و البته، ما می توانیم چندین نسخه از شاخه های پیکربندی مختلف داشته باشیم. در یک پیکربندی، می‌توانیم نسخه‌های خاصی از کتابخانه‌ها را انتخاب کنیم و هر زمان که این پیکربندی را اجرا کنیم، این ثابت باقی می‌ماند.

تغییر پیکربندی به تغییر کد تبدیل می شود. بنابراین باید توسط همان فرآیند تضمین کیفیت پوشش داده شود:

بلیط -> روابط عمومی -> بررسی -> ادغام -> ادغام مداوم -> استقرار مداوم

این رویکرد پیامدهای زیر دارد:

  1. پیکربندی برای نمونه یک سیستم خاص منسجم است. به نظر می رسد هیچ راهی برای ارتباط نادرست بین گره ها وجود ندارد.
  2. تغییر پیکربندی فقط در یک گره آسان نیست. ورود به سیستم و تغییر برخی فایل های متنی غیر منطقی به نظر می رسد. بنابراین رانش پیکربندی کمتر ممکن می شود.
  3. ایجاد تغییرات کوچک در پیکربندی آسان نیست.
  4. بسیاری از تغییرات پیکربندی از همان فرآیند توسعه پیروی می کنند و بررسی هایی را پشت سر می گذارند.

آیا برای پیکربندی تولید به یک مخزن جداگانه نیاز داریم؟ پیکربندی تولید ممکن است حاوی اطلاعات حساسی باشد که مایلیم آنها را از دسترس بسیاری از افراد دور نگه داریم. بنابراین ممکن است ارزش نگهداری یک مخزن جداگانه با دسترسی محدود را داشته باشد که حاوی پیکربندی تولید باشد. ممکن است پیکربندی را به دو قسمت تقسیم کنیم - یکی که شامل بازترین پارامترهای تولید است و دیگری که حاوی بخش مخفی پیکربندی است. این امکان دسترسی بیشتر توسعه دهندگان را به اکثر پارامترها فراهم می کند و در عین حال دسترسی به چیزهای واقعا حساس را محدود می کند. انجام این کار با استفاده از صفات میانی با مقادیر پارامترهای پیش فرض آسان است.

تغییرات

بیایید مزایا و معایب رویکرد پیشنهادی را در مقایسه با سایر تکنیک‌های مدیریت پیکربندی ببینیم.

اول از همه، ما چند گزینه جایگزین برای جنبه های مختلف روش پیشنهادی برای مقابله با پیکربندی فهرست می کنیم:

  1. فایل متنی روی دستگاه مورد نظر
  2. ذخیره سازی متمرکز کلید-مقدار (مانند etcd/zookeeper).
  3. اجزای فرآیند فرعی که می‌توانند بدون راه‌اندازی مجدد فرآیند پیکربندی/راه‌اندازی مجدد شوند.
  4. پیکربندی خارج از مصنوعات و کنترل نسخه.

فایل متنی از نظر اصلاحات موقتی انعطاف پذیری می دهد. مدیر یک سیستم می تواند به گره مورد نظر وارد شود، تغییری ایجاد کند و به سادگی سرویس را راه اندازی مجدد کند. این ممکن است برای سیستم های بزرگتر خوب نباشد. هیچ اثری از این تغییر باقی نمانده است. این تغییر توسط یک جفت چشم دیگر بررسی نمی شود. شاید سخت باشد که بفهمیم چه چیزی باعث این تغییر شده است. تست نشده است. از دیدگاه سیستم توزیع شده، یک مدیر می تواند به سادگی فراموش کند که پیکربندی را در یکی از گره های دیگر به روز کند.

(Btw، اگر در نهایت نیاز به شروع استفاده از فایل های پیکربندی متنی وجود داشته باشد، ما فقط باید تجزیه کننده + اعتبارسنجی را اضافه کنیم که می تواند همان را ایجاد کند. Config تایپ کنید و برای شروع استفاده از تنظیمات متن کافی است. این همچنین نشان می دهد که پیچیدگی پیکربندی زمان کامپایل کمی کمتر از پیچیدگی پیکربندی های مبتنی بر متن است، زیرا در نسخه مبتنی بر متن به کد اضافی نیاز داریم.)

ذخیره سازی متمرکز کلید-مقدار مکانیزم خوبی برای توزیع متا پارامترهای برنامه است. در اینجا باید به این فکر کنیم که چه چیزی را به عنوان مقادیر پیکربندی در نظر می گیریم و چه چیزی فقط داده است. یک تابع داده شده است C => A => B ما معمولاً مقادیر کم تغییر را می نامیم C "پیکربندی"، در حالی که داده ها اغلب تغییر می کردند A - فقط داده ها را وارد کنید. پیکربندی باید زودتر از داده به تابع ارائه شود A. با توجه به این ایده، می توان گفت که فرکانس مورد انتظار تغییرات است که می تواند برای تشخیص داده های پیکربندی از داده ها استفاده شود. همچنین داده ها معمولاً از یک منبع (کاربر) و پیکربندی از منبع دیگری (مدیر) می آید. برخورد با پارامترهایی که می توانند پس از فرآیند اولیه سازی تغییر کنند، منجر به افزایش پیچیدگی برنامه می شود. برای چنین پارامترهایی، ما باید مکانیسم تحویل آنها، تجزیه و اعتبارسنجی، مدیریت مقادیر نادرست را مدیریت کنیم. بنابراین، به منظور کاهش پیچیدگی برنامه، بهتر است تعداد پارامترهایی را که می‌توانند در زمان اجرا تغییر کنند (یا حتی آنها را به طور کامل حذف کنیم) کاهش دهیم.

از دیدگاه این پست ما باید بین پارامترهای استاتیک و پویا تمایز قائل شویم. اگر منطق سرویس نیاز به تغییر نادر برخی از پارامترها در زمان اجرا داشته باشد، ممکن است آنها را پارامترهای پویا بنامیم. در غیر این صورت، آنها ثابت هستند و می توانند با استفاده از روش پیشنهادی پیکربندی شوند. برای پیکربندی مجدد پویا ممکن است به روش های دیگری نیاز باشد. برای مثال، ممکن است بخش‌هایی از سیستم با پارامترهای پیکربندی جدید به روشی مشابه راه‌اندازی مجدد فرآیندهای جداگانه یک سیستم توزیع‌شده، مجدداً راه‌اندازی شوند.
(نظر فروتنانه من این است که از پیکربندی مجدد زمان اجرا خودداری کنید زیرا پیچیدگی سیستم را افزایش می دهد.
ممکن است ساده تر باشد که فقط به پشتیبانی سیستم عامل برای راه اندازی مجدد فرآیندها تکیه کنید. اگرچه، ممکن است همیشه امکان پذیر نباشد.)

یکی از جنبه‌های مهم استفاده از پیکربندی استاتیک که گاهی باعث می‌شود افراد پیکربندی پویا (بدون دلایل دیگر) را در نظر بگیرند، خرابی سرویس در طول به‌روزرسانی پیکربندی است. در واقع، اگر باید تغییراتی در پیکربندی استاتیک ایجاد کنیم، باید سیستم را مجددا راه اندازی کنیم تا مقادیر جدید موثر واقع شوند. الزامات خرابی برای سیستم های مختلف متفاوت است، بنابراین ممکن است آنقدر مهم نباشد. اگر حیاتی است، باید از قبل برای راه اندازی مجدد سیستم برنامه ریزی کنیم. به عنوان مثال، ما می توانیم پیاده سازی کنیم تخلیه اتصال AWS ELB. در این سناریو هر زمان که نیاز به راه اندازی مجدد سیستم داشته باشیم، نمونه جدیدی از سیستم را به صورت موازی راه اندازی می کنیم، سپس ELB را به آن تغییر می دهیم، در حالی که به سیستم قدیمی اجازه می دهیم سرویس اتصالات موجود را تکمیل کند.

در مورد حفظ پیکربندی در مصنوع نسخه‌سازی شده یا خارج از آن چطور؟ حفظ پیکربندی در داخل یک مصنوع به این معنی است که در اکثر موارد این پیکربندی همانند سایر مصنوعات فرآیند تضمین کیفیت را پشت سر گذاشته است. بنابراین می توان مطمئن بود که پیکربندی با کیفیت و قابل اعتماد است. برعکس پیکربندی در یک فایل جداگانه به این معنی است که هیچ اثری از اینکه چه کسی و چرا در آن فایل تغییراتی ایجاد کرده است وجود ندارد. آیا این مهم است؟ ما معتقدیم که برای اکثر سیستم های تولیدی بهتر است پیکربندی پایدار و با کیفیت بالا داشته باشد.

نسخه مصنوع به شما امکان می دهد بفهمید چه زمانی ایجاد شده است، چه مقادیری دارد، چه ویژگی هایی فعال/غیرفعال شده است، چه کسی مسئول ایجاد هر تغییر در پیکربندی بوده است. ممکن است برای حفظ پیکربندی در داخل یک مصنوع به تلاشی نیاز داشته باشد و این یک انتخاب طراحی است.

جوانب مثبت منفی

در اینجا می خواهیم به برخی از مزایا و برخی از معایب رویکرد پیشنهادی اشاره کنیم.

مزایای

ویژگی های پیکربندی قابل کامپایل یک سیستم کامل توزیع شده:

  1. بررسی استاتیک پیکربندی این به سطح بالایی از اطمینان می دهد که پیکربندی با توجه به محدودیت های نوع درست است.
  2. زبان غنی از پیکربندی. به طور معمول سایر رویکردهای پیکربندی حداکثر به جایگزینی متغیر محدود می شوند.
    با استفاده از Scala می توان از طیف گسترده ای از ویژگی های زبان برای بهبود پیکربندی استفاده کرد. به عنوان مثال، ما می توانیم از ویژگی ها برای ارائه مقادیر پیش فرض، اشیاء برای تنظیم دامنه های مختلف استفاده کنیم، می توانیم به vals فقط یک بار در محدوده بیرونی (DRY) تعریف شده است. امکان استفاده از دنباله های تحت اللفظی، یا نمونه هایی از کلاس های خاص (Seq, Map، و غیره).
  3. DSL. اسکالا پشتیبانی مناسبی از نویسندگان DSL دارد. می‌توان از این ویژگی‌ها برای ایجاد یک زبان پیکربندی که راحت‌تر و کاربر پسندتر است استفاده کرد، به طوری که پیکربندی نهایی حداقل برای کاربران دامنه قابل خواندن باشد.
  4. یکپارچگی و انسجام در سراسر گره ها. یکی از مزایای داشتن پیکربندی برای کل سیستم توزیع شده در یک مکان این است که همه مقادیر به طور دقیق یک بار تعریف می شوند و سپس در همه مکان هایی که به آنها نیاز داریم دوباره استفاده می شوند. همچنین اعلان‌های پورت امن را تایپ کنید و اطمینان حاصل کنید که در تمام تنظیمات صحیح ممکن، گره‌های سیستم به یک زبان صحبت می‌کنند. وابستگی های صریحی بین گره ها وجود دارد که فراموش کردن ارائه برخی خدمات را سخت می کند.
  5. کیفیت بالای تغییرات رویکرد کلی تغییر پیکربندی از طریق فرآیند عادی روابط عمومی، استانداردهای بالایی از کیفیت را نیز در پیکربندی ایجاد می‌کند.
  6. تغییرات پیکربندی همزمان هر زمان که ما تغییری در پیکربندی ایجاد کنیم، استقرار خودکار اطمینان حاصل می کند که همه گره ها به روز می شوند.
  7. ساده سازی برنامه برنامه نیازی به تجزیه و تأیید پیکربندی و مدیریت مقادیر پیکربندی نادرست ندارد. این برنامه کلی را ساده می کند. (برخی افزایش پیچیدگی در خود پیکربندی است، اما این یک مبادله آگاهانه به سمت ایمنی است.) بازگشت به پیکربندی معمولی بسیار ساده است - فقط قطعات گم شده را اضافه کنید. شروع کار با پیکربندی کامپایل‌شده و موکول کردن اجرای قطعات اضافی به زمان‌های بعدی آسان‌تر است.
  8. پیکربندی نسخه شده با توجه به این واقعیت که تغییرات پیکربندی روند توسعه یکسانی را دنبال می کند، در نتیجه یک مصنوع با نسخه منحصر به فرد دریافت می کنیم. این به ما امکان می دهد در صورت نیاز پیکربندی را به عقب برگردانیم. ما حتی می‌توانیم یک پیکربندی را که یک سال پیش استفاده شده است، اجرا کنیم و دقیقاً به همان روش کار خواهد کرد. پیکربندی پایدار قابلیت پیش بینی و قابلیت اطمینان سیستم توزیع شده را بهبود می بخشد. پیکربندی در زمان کامپایل ثابت است و نمی توان به راحتی در یک سیستم تولید دستکاری کرد.
  9. مدولار بودن. چارچوب پیشنهادی ماژولار است و ماژول ها را می توان به روش های مختلفی ترکیب کرد
    پشتیبانی از پیکربندی های مختلف (تنظیمات / طرح بندی). به طور خاص، امکان داشتن یک طرح بندی تک گره در مقیاس کوچک و یک تنظیم چند گره در مقیاس بزرگ وجود دارد. منطقی است که چندین طرح تولید داشته باشید.
  10. آزمایش کردن. برای مقاصد آزمایشی می‌توان یک سرویس ساختگی را پیاده‌سازی کرد و از آن به‌عنوان یک وابستگی به روشی مطمئن استفاده کرد. چند طرح آزمایشی مختلف با قطعات مختلف جایگزین شده با ماک ها می توانند به طور همزمان حفظ شوند.
  11. تست یکپارچه سازی گاهی اوقات در سیستم های توزیع شده اجرای تست های یکپارچه سازی دشوار است. با استفاده از روش توصیف شده برای تایپ پیکربندی ایمن سیستم توزیع شده کامل، می توانیم تمام قطعات توزیع شده را روی یک سرور واحد به روشی قابل کنترل اجرا کنیم. تقلید از موقعیت آسان است
    هنگامی که یکی از خدمات در دسترس نیست.

معایب

رویکرد پیکربندی کامپایل شده با پیکربندی "عادی" متفاوت است و ممکن است با همه نیازها مطابقت نداشته باشد. در اینجا برخی از معایب پیکربندی کامپایل شده آورده شده است:

  1. پیکربندی استاتیک ممکن است برای همه برنامه ها مناسب نباشد. در برخی موارد نیاز به تعمیر سریع پیکربندی در تولید با دور زدن تمام اقدامات ایمنی وجود دارد. این رویکرد کار را دشوارتر می کند. پس از ایجاد هرگونه تغییر در پیکربندی، کامپایل و جابجایی مجدد مورد نیاز است. این هم ویژگی است و هم بار.
  2. تولید پیکربندی هنگامی که پیکربندی توسط برخی ابزارهای اتوماسیون ایجاد می شود، این رویکرد نیاز به کامپایل بعدی دارد (که ممکن است به نوبه خود با شکست مواجه شود). ممکن است به تلاش بیشتری برای ادغام این مرحله اضافی در سیستم ساخت نیاز داشته باشد.
  3. ابزار. امروزه ابزارهای زیادی در حال استفاده هستند که به تنظیمات مبتنی بر متن متکی هستند. برخی از آنها
    هنگامی که پیکربندی کامپایل می شود، قابل اجرا نخواهد بود.
  4. تغییر در طرز فکر مورد نیاز است. توسعه دهندگان و DevOps با فایل های پیکربندی متن آشنا هستند. ایده کامپایل پیکربندی ممکن است برای آنها عجیب به نظر برسد.
  5. قبل از معرفی پیکربندی قابل کامپایل، یک فرآیند توسعه نرم افزار با کیفیت بالا مورد نیاز است.

نمونه پیاده سازی شده محدودیت هایی دارد:

  1. اگر پیکربندی اضافی را ارائه دهیم که توسط اجرای گره درخواست نمی شود، کامپایلر به ما کمک نمی کند تا پیاده سازی غایب را تشخیص دهیم. این را می توان با استفاده از HList یا ADT ها (کلاس های موردی) برای پیکربندی گره به جای صفات و الگوی کیک.
  2. ما باید در فایل پیکربندی مقداری دیگ بخار ارائه کنیم: (package, import, object اعلامیه ها؛
    override defبرای پارامترهایی که مقادیر پیش فرض دارند). این ممکن است تا حدی با استفاده از یک DSL برطرف شود.
  3. در این پست، پیکربندی مجدد پویا خوشه‌های گره‌های مشابه را پوشش نمی‌دهیم.

نتیجه

در این پست، ایده نمایش پیکربندی به طور مستقیم در کد منبع به روشی امن مورد بحث قرار گرفته است. این رویکرد می تواند در بسیاری از برنامه ها به عنوان جایگزینی برای xml و سایر تنظیمات مبتنی بر متن استفاده شود. علیرغم اینکه مثال ما در اسکالا پیاده سازی شده است، می توان آن را به سایر زبان های قابل کامپایل (مانند کاتلین، سی شارپ، سوئیفت و غیره) نیز ترجمه کرد. می‌توان این رویکرد را در پروژه‌ای جدید امتحان کرد و در صورت عدم تناسب، به روش قدیمی روی آورد.

البته، پیکربندی قابل کامپایل نیاز به فرآیند توسعه با کیفیت بالا دارد. در عوض، نوید ارائه پیکربندی قوی با کیفیت بالا را می دهد.

این رویکرد را می توان به طرق مختلف گسترش داد:

  1. می‌توان از ماکروها برای انجام اعتبارسنجی پیکربندی استفاده کرد و در صورت شکست محدودیت‌های منطقی تجاری، در زمان کامپایل با شکست مواجه شد.
  2. یک DSL می تواند پیاده سازی شود تا پیکربندی را به شیوه ای کاربرپسند دامنه نشان دهد.
  3. مدیریت منابع پویا با تنظیمات پیکربندی خودکار. به عنوان مثال، هنگامی که تعداد گره های خوشه ای را تنظیم می کنیم، ممکن است بخواهیم (1) گره ها پیکربندی کمی تغییر یافته را به دست آورند. (2) مدیر خوشه برای دریافت اطلاعات گره های جدید.

با تشکر

می‌خواهم از آندری ساکسونوف، پاول پوپوف، آنتون نهایف برای بازخورد الهام‌بخش پیش‌نویس این پست تشکر کنم که به من کمک کرد آن را واضح‌تر کنم.

منبع: www.habr.com