Կազմված բաշխված համակարգի կոնֆիգուրացիա

Ես կցանկանայի ձեզ պատմել բաշխված համակարգի կոնֆիգուրացիայի հետ աշխատելու մեկ հետաքրքիր մեխանիզմ: Կազմաձևը ներկայացված է ուղղակիորեն կազմված լեզվով (Scala)՝ օգտագործելով անվտանգ տեսակներ: Այս գրառումը ներկայացնում է նման կոնֆիգուրացիայի օրինակ և քննարկում է կազմված կազմաձևման ընդհանուր զարգացման գործընթացում ներդրման տարբեր ասպեկտներ:

Կազմված բաշխված համակարգի կոնֆիգուրացիա

(Անգլերեն)

Ներածություն

Հուսալի բաշխված համակարգ կառուցելը նշանակում է, որ բոլոր հանգույցներն օգտագործում են ճիշտ կոնֆիգուրացիա՝ համաժամանակացված այլ հանգույցների հետ: DevOps տեխնոլոգիաները (terraform, ansible կամ նման բան) սովորաբար օգտագործվում են ավտոմատ կերպով կազմաձևման ֆայլեր ստեղծելու համար (հաճախ հատուկ յուրաքանչյուր հանգույցի համար): Մենք նաև կցանկանայինք վստահ լինել, որ բոլոր հաղորդակցվող հանգույցներն օգտագործում են միանման արձանագրություններ (ներառյալ նույն տարբերակը): Հակառակ դեպքում անհամատեղելիությունը կներկառուցվի մեր բաշխված համակարգում: JVM աշխարհում այս պահանջի հետևանքներից մեկն այն է, որ գրադարանի նույն տարբերակը, որը պարունակում է արձանագրային հաղորդագրություններ, պետք է օգտագործվի ամենուր:

Ինչ վերաբերում է բաշխված համակարգի փորձարկմանը: Իհարկե, մենք ենթադրում ենք, որ բոլոր բաղադրիչներն ունեն միավորի թեստեր, նախքան ինտեգրման թեստավորման անցնելը: (Որպեսզի մենք կարողանանք փորձարկման արդյունքները արտանետել գործարկման ժամանակի վրա, մենք պետք է նաև տրամադրենք գրադարանների նույնական շարք թեստավորման փուլում և գործարկման ժամանակ:)

Ինտեգրման թեստերի հետ աշխատելիս հաճախ ավելի հեշտ է օգտագործել նույն դասընթացը ամենուր բոլոր հանգույցներում: Մեզ մնում է միայն ապահովել, որ գործարկման ժամանակ օգտագործվի նույն դասընթացը: (Չնայած դա լիովին հնարավոր է գործարկել տարբեր հանգույցներ տարբեր դասընթացուղիներով, դա բարդացնում է ընդհանուր կազմաձևը և դժվարություններ տեղակայման և ինտեգրման թեստերի հետ:) Այս գրառման նպատակների համար մենք ենթադրում ենք, որ բոլոր հանգույցները կօգտագործեն նույն դասընթացը:

Կազմաձևը զարգանում է հավելվածի հետ միասին: Մենք օգտագործում ենք տարբերակներ՝ ծրագրի էվոլյուցիայի տարբեր փուլերը բացահայտելու համար: Թվում է, թե տրամաբանական է նաև նույնականացնել կոնֆիգուրացիաների տարբեր տարբերակները: Եվ տեղադրեք կոնֆիգուրացիան ինքնին տարբերակի կառավարման համակարգում: Եթե ​​արտադրության մեջ կա միայն մեկ կոնֆիգուրացիա, ապա մենք կարող ենք պարզապես օգտագործել տարբերակի համարը: Եթե ​​մենք օգտագործում ենք բազմաթիվ արտադրական օրինակներ, ապա մեզ մի քանիսը պետք կգան
կոնֆիգուրացիայի ճյուղեր և տարբերակից բացի լրացուցիչ պիտակ (օրինակ՝ մասնաճյուղի անվանումը): Այս կերպ մենք կարող ենք հստակորեն բացահայտել ճշգրիտ կոնֆիգուրացիան: Յուրաքանչյուր կոնֆիգուրացիայի նույնացուցիչ եզակիորեն համապատասխանում է բաշխված հանգույցների, նավահանգիստների, արտաքին ռեսուրսների և գրադարանի տարբերակների հատուկ համակցությանը: Այս գրառման նպատակների համար մենք կենթադրենք, որ կա միայն մեկ ճյուղ, և մենք կարող ենք նույնականացնել կոնֆիգուրացիան սովորական ձևով՝ օգտագործելով երեք թվեր, որոնք բաժանված են կետով (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 - ընդամենը մի ամբողջ թիվ Int նշելով ընդունելի արժեքների միջակայքը.

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-ներ, հանրահաշվական տվյալների տեսակներ): Այս գրառման նպատակների համար մենք կօգտագործենք Cake Pattern-ը և կներկայացնենք մոդուլներ՝ օգտագործելով trait«օվ. (Տորթի ձևանմուշը այս մոտեցման պարտադիր տարր չէ: Դա պարզապես հնարավոր իրականացումներից մեկն է:)

Ծառայությունների միջև կախվածությունը կարող է ներկայացվել որպես նավահանգիստներ վերադարձնող մեթոդներ 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)
  }

Էխո ծառայություն ստեղծելու համար ձեզ հարկավոր է ընդամենը մի պորտի համար և ցուցում, որ նավահանգիստն աջակցում է echo արձանագրությանը: Մենք կարող ենք չնշել կոնկրետ նավահանգիստ, քանի որ... հատկանիշները թույլ են տալիս հայտարարել մեթոդներ առանց իրականացման (վերացական մեթոդներ): Այս դեպքում, կոնկրետ կոնֆիգուրացիա ստեղծելիս, կոմպիլյատորը մեզանից կպահանջի ապահովել վերացական մեթոդի իրականացում և տրամադրել պորտի համար: Քանի որ մենք կիրառել ենք մեթոդը, հատուկ կոնֆիգուրացիա ստեղծելիս մենք կարող ենք չնշել այլ պորտ: Կօգտագործվի լռելյայն արժեքը:

Հաճախորդի կազմաձևում մենք հայտարարում ենք կախվածություն echo ծառայությունից.

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

Կախվածությունը նույն տեսակի է, ինչ արտահանվող ծառայությունը echoService. Մասնավորապես, echo հաճախորդում մենք պահանջում ենք նույն արձանագրությունը: Ուստի երկու ծառայություններ միացնելիս կարող ենք վստահ լինել, որ ամեն ինչ ճիշտ կաշխատի։

Ծառայությունների իրականացում

Ծառայությունը սկսելու և դադարեցնելու համար անհրաժեշտ է գործառույթ: (Ծառայությունը դադարեցնելու ունակությունը չափազանց կարևոր է փորձարկման համար:) Կրկին, կան մի քանի տարբերակներ նման գործառույթի իրականացման համար (օրինակ, մենք կարող ենք օգտագործել տիպի դասեր՝ հիմնված կազմաձևման տեսակի վրա): Այս գրառման նպատակների համար մենք կօգտագործենք տորթի նախշը: Մենք կներկայացնենք ծառայությունը՝ օգտագործելով դասը 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 — էֆեկտի տիպի դաս, որը թույլ է տալիս համատեղել առանձին էֆեկտներ (գրեթե մոնադ): Ավելի բարդ ծրագրերում, թվում է, ավելի լավ է օգտագործել Monad/ConcurrentEffect.

Օգտագործելով այս ֆունկցիայի ստորագրությունը, մենք կարող ենք իրականացնել մի քանի ծառայություններ: Օրինակ, ծառայություն, որը ոչինչ չի անում.

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

(Սմ. աղբյուրը, որում իրականացվում են այլ ծառայություններ՝ արձագանքների ծառայություն, echo հաճախորդ
и ցմահ կարգավորիչներ.)

Հանգույցը օբյեկտ է, որը կարող է գործարկել մի քանի ծառայություններ (ռեսուրսների շղթայի գործարկումն ապահովված է Cake Pattern-ով).

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

Խնդրում ենք նկատի ունենալ, որ մենք նշում ենք կոնֆիգուրացիայի ճշգրիտ տեսակը, որն անհրաժեշտ է այս հանգույցի համար: Եթե ​​մոռանանք նշել որոշակի ծառայության կողմից պահանջվող կոնֆիգուրացիայի տեսակներից մեկը, կառաջանա կոմպիլյացիայի սխալ: Բացի այդ, մենք չենք կարողանա սկսել հանգույց, եթե չտրամադրենք համապատասխան տեսակի օբյեկտ բոլոր անհրաժեշտ տվյալներով:

Հյուրընկալողի անվան լուծում

Հեռավոր հոսթին միանալու համար մեզ անհրաժեշտ է իրական IP հասցե: Հնարավոր է, որ հասցեն հայտնի դառնա ավելի ուշ, քան մնացած կոնֆիգուրացիան: Այսպիսով, մեզ անհրաժեշտ է գործառույթ, որը քարտեզագրում է հանգույցի ID-ն հասցեով.

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

Այս գործառույթն իրականացնելու մի քանի եղանակ կա.

  1. Եթե ​​հասցեները մեզ հայտնի են դառնում նախքան տեղակայումը, ապա մենք կարող ենք ստեղծել Scala կոդը
    հասցեները և այնուհետև գործարկել build-ը: Սա կկազմի և կգործարկի թեստերը:
    Այս դեպքում ֆունկցիան ստատիկորեն հայտնի կլինի և կոդով կարող է ներկայացվել որպես քարտեզագրում Map[NodeId, NodeAddress].
  2. Որոշ դեպքերում իրական հասցեն հայտնի է միայն հանգույցի մեկնարկից հետո:
    Այս դեպքում մենք կարող ենք իրականացնել «բացահայտման ծառայություն», որն աշխատում է այլ հանգույցներից առաջ, և բոլոր հանգույցները կգրանցվեն այս ծառայության մեջ և կպահանջեն այլ հանգույցների հասցեները:
  3. Եթե ​​մենք կարողանանք փոփոխել /etc/hosts, ապա կարող եք օգտագործել նախապես սահմանված հյուրընկալող անունները (ինչպես my-project-main-node и echo-backend) և ուղղակի կապեք այս անունները
    տեղակայման ժամանակ IP հասցեներով:

Այս գրառման մեջ մենք ավելի մանրամասն չենք քննարկի այս դեպքերը: Մեր .... համար
խաղալիքի օրինակում բոլոր հանգույցները կունենան նույն IP հասցեն. 127.0.0.1.

Հաջորդը, մենք դիտարկում ենք բաշխված համակարգի երկու տարբերակ.

  1. Բոլոր ծառայությունների տեղադրումը մեկ հանգույցի վրա:
  2. Եվ echo ծառայության և echo հաճախորդի հոսթինգը տարբեր հանգույցների վրա:

Կոնֆիգուրացիա համար մեկ հանգույց:

Մեկ հանգույցի կոնֆիգուրացիա

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 դադարեցնել ծրագիրը. (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
  {
    // 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"
  }

Կարևոր. Ուշադրություն դարձրեք, թե ինչպես են կապվում ծառայությունները: Մենք նշում ենք մի հանգույցի կողմից իրականացվող ծառայություն՝ որպես մեկ այլ հանգույցի կախվածության մեթոդի իրականացում։ Կախվածության տեսակը ստուգվում է կոմպիլյատորի կողմից, քանի որ պարունակում է արձանագրության տեսակը. Երբ գործարկվում է, կախվածությունը կպարունակի ճիշտ թիրախային հանգույցի ID-ն: Այս սխեմայի շնորհիվ մենք ճշգրիտ մեկ անգամ նշում ենք պորտի համարը և միշտ երաշխավորված ենք ճիշտ պորտին հղում կատարելու համար:

Երկու համակարգային հանգույցների իրականացում

Այս կազմաձևման համար մենք օգտագործում ենք նույն ծառայության իրականացումները՝ առանց փոփոխությունների: Միակ տարբերությունն այն է, որ մենք այժմ ունենք երկու օբյեկտ, որոնք իրականացնում են ծառայությունների տարբեր հավաքածուներ.

  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'om, և հաճախորդի հանգույցն ավարտվում է որոշ ժամանակ անց: Սմ. գործարկիչ հավելված.

Ընդհանուր զարգացման գործընթաց

Տեսնենք, թե ինչպես է այս կոնֆիգուրացիայի մոտեցումն ազդում զարգացման ընդհանուր գործընթացի վրա:

Կազմաձևը կկազմվի մնացած կոդի հետ միասին և կստեղծվի արտեֆակտ (.jar): Կարծես իմաստ ունի կոնֆիգուրացիան դնել առանձին արտեֆակտի մեջ: Դա պայմանավորված է նրանով, որ մենք կարող ենք ունենալ մի քանի կոնֆիգուրացիաներ՝ հիմնված նույն կոդի վրա: Կրկին, հնարավոր է ստեղծել տարբեր կոնֆիգուրացիայի ճյուղերին համապատասխան արտեֆակտներ: Գրադարանների որոշակի տարբերակներից կախվածությունը պահպանվում է կազմաձևման հետ մեկտեղ, և այս տարբերակները պահվում են ընդմիշտ, երբ մենք որոշենք տեղակայել կազմաձևման այդ տարբերակը:

Ցանկացած կոնֆիգուրացիայի փոփոխություն վերածվում է կոդի փոփոխության: Եվ հետևաբար, յուրաքանչյուրը
փոփոխությունը ծածկվելու է որակի ապահովման նորմալ գործընթացով.

Տոմս սխալների հետագծում -> PR -> վերանայում -> միաձուլվել համապատասխան մասնաճյուղերի հետ ->
ինտեգրում -> տեղակայում

Կազմված կոնֆիգուրացիայի իրականացման հիմնական հետևանքները հետևյալն են.

  1. Կազմաձևը հետևողական կլինի բաշխված համակարգի բոլոր հանգույցներում: Շնորհիվ այն բանի, որ բոլոր հանգույցները ստանում են նույն կոնֆիգուրացիան մեկ աղբյուրից:

  2. Խնդրահարույց է կոնֆիգուրացիան փոխել հանգույցներից միայն մեկում։ Հետևաբար, «կոնֆիգուրացիայի շեղումը» քիչ հավանական է:

  3. Կազմաձևում փոքր փոփոխություններ կատարելն ավելի դժվար է դառնում:

  4. Կազմաձևման փոփոխությունների մեծ մասը տեղի կունենա որպես ընդհանուր զարգացման գործընթացի մաս և ենթակա կլինի վերանայման:

Արդյո՞ք ինձ անհրաժեշտ է առանձին պահեստ՝ արտադրության կոնֆիգուրացիան պահելու համար: Այս կոնֆիգուրացիան կարող է պարունակել գաղտնաբառեր և այլ զգայուն տեղեկություններ, որոնց մուտքը մենք կցանկանայինք սահմանափակել: Ելնելով դրանից՝ թվում է, թե իմաստ ունի պահպանել վերջնական կոնֆիգուրացիան առանձին պահեստում: Դուք կարող եք կոնֆիգուրացիան բաժանել երկու մասի. մեկը պարունակում է հանրությանը հասանելի կազմաձևման կարգավորումներ, իսկ մյուսը պարունակում է սահմանափակ կարգավորումներ: Սա ծրագրավորողների մեծամասնությանը թույլ կտա մուտք գործել ընդհանուր կարգավորումներ: Այս տարանջատումը հեշտ է հասնել՝ օգտագործելով լռելյայն արժեքներ պարունակող միջանկյալ հատկանիշներ:

Հնարավոր տատանումներ

Փորձենք համեմատել կազմված կոնֆիգուրացիան որոշ ընդհանուր այլընտրանքների հետ.

  1. Տեքստային ֆայլ թիրախային մեքենայի վրա:
  2. Կենտրոնացված բանալի-արժեքի խանութ (etcd/zookeeper).
  3. Գործընթացի բաղադրիչներ, որոնք կարող են վերակազմավորվել/վերագործարկվել առանց գործընթացը վերագործարկելու:
  4. Կազմաձևերի պահպանում արտեֆակտի և տարբերակի վերահսկողությունից դուրս:

Տեքստային ֆայլերը զգալի ճկունություն են ապահովում փոքր փոփոխությունների առումով: Համակարգի ադմինիստրատորը կարող է մուտք գործել հեռավոր հանգույց, փոփոխություններ կատարել համապատասխան ֆայլերում և վերագործարկել ծառայությունը: Խոշոր համակարգերի համար, սակայն, նման ճկունությունը չի կարող ցանկալի լինել: Կատարված փոփոխությունները այլ համակարգերում հետք չեն թողնում։ Ոչ ոք չի վերանայում փոփոխությունները։ Դժվար է որոշել, թե կոնկրետ ով և ինչ պատճառով է կատարել փոփոխությունները։ Փոփոխությունները չեն փորձարկվում: Եթե ​​համակարգը բաշխված է, ապա ադմինիստրատորը կարող է մոռանալ կատարել համապատասխան փոփոխություն այլ հանգույցների վրա։

(Հարկ է նաև նշել, որ կոմպիլացված կոնֆիգուրացիայի օգտագործումը չի փակում ապագայում տեքստային ֆայլերի օգտագործման հնարավորությունը: Բավական կլինի ավելացնել վերլուծիչ և վավերացնող, որը արտադրում է նույն տիպը, ինչ ելքը: Config, և կարող եք օգտագործել տեքստային ֆայլեր: Անմիջապես հետևում է, որ կազմված կազմաձևով համակարգի բարդությունը փոքր-ինչ ավելի քիչ է, քան տեքստային ֆայլեր օգտագործող համակարգի բարդությունը, քանի որ տեքստային ֆայլերը պահանջում են լրացուցիչ կոդ:)

Կենտրոնացված բանալի-արժեքի խանութը լավ մեխանիզմ է բաշխված հավելվածի մետա պարամետրերը բաշխելու համար: Մենք պետք է որոշենք, թե որոնք են կազմաձևման պարամետրերը և որոնք են պարզապես տվյալները: Եկեք գործառույթ ունենանք C => A => B, և պարամետրերը C հազվադեպ է փոխվում, և տվյալները A - հաճախ. Այս դեպքում կարելի է ասել, որ C - կազմաձևման պարամետրեր և A - տվյալներ. Ըստ երևույթին, կազմաձևման պարամետրերը տարբերվում են տվյալներից նրանով, որ դրանք սովորաբար ավելի քիչ են փոխվում, քան տվյալները: Բացի այդ, տվյալները սովորաբար ստացվում են մեկ աղբյուրից (օգտագործողի կողմից), իսկ կազմաձևման պարամետրերը մեկ այլ աղբյուրից (համակարգի ադմինիստրատորից):

Եթե ​​հազվադեպ փոփոխվող պարամետրերը պետք է թարմացվեն առանց ծրագիրը վերագործարկելու, ապա դա հաճախ կարող է հանգեցնել ծրագրի բարդացման, քանի որ մեզ անհրաժեշտ կլինի ինչ-որ կերպ մատուցել պարամետրերը, պահել, վերլուծել և ստուգել և մշակել սխալ արժեքներ: Հետևաբար, ծրագրի բարդությունը նվազեցնելու տեսակետից իմաստ ունի նվազեցնել այն պարամետրերի քանակը, որոնք կարող են փոխվել ծրագրի շահագործման ընթացքում (կամ ընդհանրապես չաջակցել նման պարամետրերին):

Այս գրառման նպատակների համար մենք կտարբերակենք ստատիկ և դինամիկ պարամետրերը: Եթե ​​ծառայության տրամաբանությունը պահանջում է ծրագրի գործարկման ընթացքում պարամետրերի փոփոխություն, ապա այդպիսի պարամետրերը կանվանենք դինամիկ։ Հակառակ դեպքում ընտրանքները ստատիկ են և կարող են կազմաձևվել՝ օգտագործելով կազմված կոնֆիգուրացիան: Դինամիկ վերակազմավորման համար մեզ կարող է անհրաժեշտ լինել ծրագրի մասերը նոր պարամետրերով վերագործարկելու մեխանիզմ, որը նման է օպերացիոն համակարգի գործընթացների վերագործարկմանը: (Մեր կարծիքով, նպատակահարմար է խուսափել իրական ժամանակի վերակազմավորումից, քանի որ դա մեծացնում է համակարգի բարդությունը: Հնարավորության դեպքում ավելի լավ է օգտագործել ստանդարտ ՕՀ հնարավորությունները գործընթացները վերագործարկելու համար):

Ստատիկ կոնֆիգուրացիայի օգտագործման կարևոր ասպեկտներից մեկը, որը մարդկանց ստիպում է դիտարկել դինամիկ վերակազմավորումը, այն ժամանակն է, որը տևում է, որպեսզի համակարգը վերագործարկվի կազմաձևման թարմացումից հետո (անդադար): Փաստորեն, եթե մեզ անհրաժեշտ լինի փոփոխություններ կատարել ստատիկ կազմաձևում, մենք ստիպված կլինենք վերագործարկել համակարգը, որպեսզի նոր արժեքներն ուժի մեջ մտնեն: Դադարեցման խնդիրը տարբեր համակարգերի համար տարբերվում է ծանրությունից: Որոշ դեպքերում, դուք կարող եք պլանավորել վերաբեռնում այն ​​ժամանակ, երբ բեռը նվազագույն է: Եթե ​​Ձեզ անհրաժեշտ է շարունակական ծառայություն մատուցել, կարող եք իրականացնել AWS ELB կապի արտահոսք. Միևնույն ժամանակ, երբ մենք պետք է վերագործարկենք համակարգը, մենք գործարկում ենք այս համակարգի զուգահեռ օրինակը, միացնում ենք հավասարակշռիչը դրա վրա և սպասում, մինչև հին կապերը ավարտվեն: Բոլոր հին կապերն ավարտվելուց հետո մենք անջատում ենք համակարգի հին օրինակը:

Այժմ դիտարկենք կոնֆիգուրացիան արտեֆակտի ներսում կամ դրսում պահելու հարցը: Եթե ​​մենք պահպանում ենք կոնֆիգուրացիան արտեֆակտի ներսում, ապա գոնե մենք հնարավորություն ունեինք ստուգել կոնֆիգուրացիայի ճիշտությունը արտեֆակտի հավաքման ժամանակ: Եթե ​​կոնֆիգուրացիան դուրս է վերահսկվող արտեֆակտից, դժվար է հետևել, թե ով և ինչու է փոփոխություններ կատարել այս ֆայլում: Որքանո՞վ է դա կարևոր: Մեր կարծիքով, շատ արտադրական համակարգերի համար կարևոր է ունենալ կայուն և որակյալ կոնֆիգուրացիա:

Արտեֆակտի տարբերակը թույլ է տալիս որոշել, թե երբ է այն ստեղծվել, ինչ արժեքներ է պարունակում, ինչ գործառույթներ են միացված/անջատված, և ով է պատասխանատու կոնֆիգուրացիայի ցանկացած փոփոխության համար: Իհարկե, կոնֆիգուրացիան արտեֆակտի ներսում պահելը որոշակի ջանք է պահանջում, այնպես որ դուք պետք է տեղեկացված որոշում կայացնեք:

Ընդդիմության եւ հակառակը

Ես կցանկանայի կանգ առնել առաջարկվող տեխնոլոգիայի դրական և բացասական կողմերի վրա:

Առավելությունները

Ստորև բերված է կազմված բաշխված համակարգի կազմաձևման հիմնական հատկանիշների ցանկը.

  1. Ստատիկ կոնֆիգուրացիայի ստուգում: Թույլ է տալիս վստահ լինել դրանում
    կոնֆիգուրացիան ճիշտ է:
  2. Հարուստ կազմաձևման լեզու: Սովորաբար, այլ կազմաձևման մեթոդները սահմանափակվում են առավելագույնը լարային փոփոխականի փոխարինմամբ: Scala-ն օգտագործելիս հասանելի են լեզվական գործառույթների լայն շրջանակ՝ ձեր կազմաձևումը բարելավելու համար: Օրինակ, մենք կարող ենք օգտագործել
    լռելյայն արժեքների գծերը, օգտագործելով օբյեկտները պարամետրերը խմբավորելու համար, մենք կարող ենք հղում անել միայն մեկ անգամ (DRY) հայտարարված val-ներին կցվող տիրույթում: Դուք կարող եք ակնարկել ցանկացած դաս անմիջապես կոնֆիգուրացիայի ներսում (Seq, Map, մաքսային դասեր)։
  3. DSL. Scala-ն ունի մի շարք լեզվական առանձնահատկություններ, որոնք հեշտացնում են DSL-ի ստեղծումը: Հնարավոր է օգտվել այս հնարավորություններից և կիրառել կոնֆիգուրացիայի լեզու, որն ավելի հարմար է օգտատերերի թիրախային խմբի համար, որպեսզի կոնֆիգուրացիան գոնե ընթեռնելի լինի տիրույթի փորձագետների կողմից: Մասնագետները կարող են, օրինակ, մասնակցել կոնֆիգուրացիայի վերանայման գործընթացին:
  4. Ամբողջականություն և սինխրոնիա հանգույցների միջև: Ամբողջ բաշխված համակարգի կոնֆիգուրացիան մեկ կետում պահելու առավելություններից մեկն այն է, որ բոլոր արժեքները հայտարարվում են ուղիղ մեկ անգամ, այնուհետև նորից օգտագործվում են այնտեղ, որտեղ անհրաժեշտ է: Ֆանտոմային տեսակների օգտագործումը նավահանգիստները հայտարարելու համար ապահովում է, որ հանգույցները օգտագործում են համատեղելի արձանագրություններ բոլոր ճիշտ համակարգի կոնֆիգուրացիաներում: Հանգույցների միջև հստակ պարտադիր կախվածություն ունենալը ապահովում է, որ բոլոր ծառայությունները միացված են:
  5. Բարձր որակի փոփոխություններ։ Կազմաձևում փոփոխություններ կատարելը, օգտագործելով մշակման ընդհանուր գործընթաց, հնարավորություն է տալիս հասնել նաև կազմաձևման բարձր որակի չափանիշներին:
  6. Միաժամանակյա կոնֆիգուրացիայի թարմացում: Համակարգի ավտոմատ տեղակայումը կազմաձևման փոփոխություններից հետո ապահովում է, որ բոլոր հանգույցները թարմացվում են:
  7. Հավելվածի պարզեցում. Հավելվածը վերլուծության, կազմաձևման ստուգման կամ սխալ արժեքների մշակման կարիք չունի: Սա նվազեցնում է հավելվածի բարդությունը: (Մեր օրինակում նկատված կազմաձևման որոշ բարդություններ կազմված կոնֆիգուրացիայի հատկանիշ չէ, այլ միայն գիտակցված որոշում, որը պայմանավորված է ավելի մեծ տիպի անվտանգություն ապահովելու ցանկությամբ:) Բավականին հեշտ է վերադառնալ սովորական կազմաձևին. պարզապես կատարեք բացակայողը: մասեր. Հետևաբար, դուք կարող եք, օրինակ, սկսել կազմված կոնֆիգուրացիայից՝ հետաձգելով ավելորդ մասերի իրականացումը մինչև այն պահը, երբ դա իսկապես անհրաժեշտ է:
  8. Ստուգված կոնֆիգուրացիա: Քանի որ կազմաձևման փոփոխությունները հետևում են ցանկացած այլ փոփոխությունների սովորական ճակատագրին, ստացված արդյունքը եզակի տարբերակով արտեֆակտ է: Սա մեզ թույլ է տալիս, օրինակ, անհրաժեշտության դեպքում վերադառնալ կոնֆիգուրացիայի նախորդ տարբերակին: Մենք նույնիսկ կարող ենք օգտագործել մեկ տարի առաջվա կոնֆիգուրացիան, և համակարգը կաշխատի նույն կերպ: Կայուն կոնֆիգուրացիան բարելավում է բաշխված համակարգի կանխատեսելիությունն ու հուսալիությունը: Քանի որ կոնֆիգուրացիան ամրագրված է կազմման փուլում, բավականին դժվար է այն կեղծել արտադրության մեջ:
  9. Մոդուլյարություն. Առաջարկվող շրջանակը մոդուլային է, և մոդուլները կարող են համակցվել տարբեր ձևերով՝ տարբեր համակարգեր ստեղծելու համար: Մասնավորապես, դուք կարող եք կարգավորել համակարգը այնպես, որ գործարկվի մեկ հանգույցի վրա մեկ մարմնավորման դեպքում, և մի քանի հանգույցների վրա՝ մեկ այլ մարմնում: Դուք կարող եք ստեղծել մի քանի կոնֆիգուրացիաներ համակարգի արտադրության օրինակների համար:
  10. Փորձարկում. Անհատական ​​ծառայությունները կեղծ օբյեկտներով փոխարինելով՝ կարող եք ձեռք բերել համակարգի մի քանի տարբերակներ, որոնք հարմար են թեստավորման համար։
  11. Ինտեգրման փորձարկում. Ամբողջ բաշխված համակարգի համար մեկ կոնֆիգուրացիա ունենալը հնարավորություն է տալիս բոլոր բաղադրիչները գործարկել վերահսկվող միջավայրում՝ որպես ինտեգրման փորձարկման մաս: Հեշտ է ընդօրինակել, օրինակ, մի իրավիճակ, երբ որոշ հանգույցներ հասանելի են դառնում:

Թերություններ և սահմանափակումներ

Կազմված կոնֆիգուրացիան տարբերվում է կազմաձևման այլ մոտեցումներից և կարող է հարմար չլինել որոշ հավելվածների համար: Ստորև բերված են որոշ թերություններ.

  1. Ստատիկ կոնֆիգուրացիա: Երբեմն անհրաժեշտ է արագ շտկել կոնֆիգուրացիան արտադրության մեջ՝ շրջանցելով բոլոր պաշտպանիչ մեխանիզմները: Այս մոտեցմամբ դա կարող է ավելի դժվար լինել: Առնվազն, դեռևս կպահանջվի հավաքում և ավտոմատ տեղակայում: Սա և՛ մոտեցման օգտակար հատկանիշն է, և՛ որոշ դեպքերում թերություն:
  2. Կազմաձևման ձևավորում: Այն դեպքում, երբ կազմաձևման ֆայլը ստեղծվում է ավտոմատ գործիքի միջոցով, լրացուցիչ ջանքեր կարող են պահանջվել կառուցման սցենարը ինտեգրելու համար:
  3. Գործիքներ. Ներկայումս կոմունալ ծառայություններն ու տեխնիկան, որոնք նախատեսված են կոնֆիգուրացիայի հետ աշխատելու համար, հիմնված են տեքստային ֆայլերի վրա: Ոչ բոլոր նման կոմունալ ծառայությունները/տեխնիկան հասանելի կլինի կազմված կազմաձևում:
  4. Պահանջվում է վերաբերմունքի փոփոխություն. Մշակողները և DevOps-ը սովոր են տեքստային ֆայլերին: Կազմաձևման գաղափարը կարող է որոշ չափով անսպասելի և անսովոր լինել և մերժման պատճառ դառնալ:
  5. Պահանջվում է բարձրորակ զարգացման գործընթաց: Կազմված կոնֆիգուրացիան հարմարավետորեն օգտագործելու համար անհրաժեշտ է հավելվածի կառուցման և տեղակայման գործընթացի ամբողջական ավտոմատացում (CI/CD): Հակառակ դեպքում դա բավականին անհարմար կլինի։

Եկեք խոսենք նաև դիտարկված օրինակի մի շարք սահմանափակումների վրա, որոնք կապված չեն կազմված կազմաձևման գաղափարի հետ.

  1. Եթե ​​մենք տրամադրենք անհարկի կոնֆիգուրացիայի տեղեկատվություն, որը չի օգտագործվում հանգույցի կողմից, ապա կոմպիլյատորը չի օգնի մեզ հայտնաբերել բացակայող իրականացումը: Այս խնդիրը կարող է լուծվել՝ հրաժարվելով Cake Pattern-ից և օգտագործելով ավելի կոշտ տեսակներ, օրինակ. HList կամ հանրահաշվական տվյալների տեսակները (case classes)՝ կոնֆիգուրացիան ներկայացնելու համար:
  2. Կազմաձևման ֆայլում կան տողեր, որոնք կապված չեն հենց կազմաձևման հետ.package, import,օբյեկտների հայտարարագրեր; override def-ը այն պարամետրերի համար, որոնք ունեն լռելյայն արժեքներ): Սա կարելի է մասամբ խուսափել, եթե դուք ներդրում եք ձեր սեփական DSL-ը: Բացի այդ, այլ տեսակի կոնֆիգուրացիաներ (օրինակ՝ XML) նույնպես որոշակի սահմանափակումներ են դնում ֆայլի կառուցվածքի վրա։
  3. Այս գրառման նպատակների համար մենք չենք դիտարկում նմանատիպ հանգույցների կլաստերի դինամիկ վերակազմավորումը:

Ամփոփում

Այս գրառման մեջ մենք ուսումնասիրեցինք սկզբնական կոդով կազմաձևումը ներկայացնելու գաղափարը՝ օգտագործելով Scala տիպի համակարգի առաջադեմ հնարավորությունները: Այս մոտեցումը կարող է օգտագործվել տարբեր հավելվածներում՝ որպես xml կամ տեքստային ֆայլերի վրա հիմնված ավանդական կազմաձևման մեթոդների փոխարինում: Չնայած մեր օրինակն իրականացվում է Scala-ում, նույն գաղափարները կարող են փոխանցվել այլ կազմված լեզուների (օրինակ՝ Kotlin, C#, Swift, ...): Դուք կարող եք փորձել այս մոտեցումը հետևյալ նախագծերից մեկում և, եթե այն չի աշխատում, անցեք տեքստային ֆայլին՝ ավելացնելով բաց թողնված մասերը։

Բնականաբար, կազմված կոնֆիգուրացիան պահանջում է բարձրորակ մշակման գործընթաց: Դրա դիմաց ապահովվում է կոնֆիգուրացիաների բարձր որակ և հուսալիություն:

Դիտարկվող մոտեցումը կարող է ընդլայնվել.

  1. Կազմելու ժամանակի ստուգումներ կատարելու համար կարող եք օգտագործել մակրոները:
  2. Դուք կարող եք իրականացնել DSL՝ կոնֆիգուրացիան վերջնական օգտագործողների համար հասանելի ձևով ներկայացնելու համար:
  3. Դուք կարող եք իրականացնել ռեսուրսների դինամիկ կառավարում ավտոմատ կազմաձևման ճշգրտմամբ: Օրինակ, կլաստերի մեջ հանգույցների քանակի փոփոխությունը պահանջում է, որ (1) յուրաքանչյուր հանգույց ստանա մի փոքր այլ կոնֆիգուրացիա; (2) կլաստերի կառավարիչը տեղեկատվություն է ստացել նոր հանգույցների մասին:

Շնորհակալագրեր

Ցանկանում եմ շնորհակալություն հայտնել Անդրեյ Սաքսոնովին, Պավել Պոպովին և Անտոն Նեխաևին հոդվածի նախագծի վերաբերյալ կառուցողական քննադատության համար։

Source: www.habr.com

Добавить комментарий