Բաշխված համակարգի կոմպիլյատոր կոնֆիգուրացիա

Այս գրառման մեջ մենք կցանկանայինք կիսվել բաշխված համակարգի կազմաձևման հետ կապված հետաքրքիր ձևով:
Կազմաձևը ներկայացված է անմիջապես Scala լեզվով տիպի անվտանգ ձևով: Իրականացման օրինակը մանրամասն նկարագրված է: Քննարկվում են առաջարկի տարբեր ասպեկտներ, ներառյալ ազդեցությունը ընդհանուր զարգացման գործընթացի վրա:

Բաշխված համակարգի կոմպիլյատոր կոնֆիգուրացիա

(ռուսերեն լեզվով)

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

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

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

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

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

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

Այս գրառման մեջ մենք կքննարկենք կազմաձևումը կազմված արտեֆակտում պահելու գաղափարը:

Կազմվող կոնֆիգուրացիա

Այս բաժնում մենք կքննարկենք ստատիկ կոնֆիգուրացիայի օրինակ: Կազմաձևվում և ներդրվում են երկու պարզ ծառայություններ՝ echo ծառայությունը և echo ծառայության հաճախորդը: Այնուհետև գործարկվում են երկու տարբեր բաշխված համակարգեր երկու ծառայություններով: Մեկը նախատեսված է մեկ հանգույցի կոնֆիգուրացիայի համար, իսկ մյուսը՝ երկու հանգույցների կազմաձևման համար:

Տիպիկ բաշխված համակարգը բաղկացած է մի քանի հանգույցներից: Հանգույցները կարող են նույնականացվել՝ օգտագործելով որոշ տեսակներ.

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 Pattern-ը պարտադիր չէ այս կազմաձևման մոտեցման համար: Դա գաղափարի հնարավոր իրականացումն է միայն:)

Կախվածությունները կարող են ներկայացվել՝ օգտագործելով 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)
  }

Echo ծառայությանը միայն անհրաժեշտ է կարգավորել նավահանգիստը: Եվ մենք հայտարարում ենք, որ այս նավահանգիստն աջակցում է echo արձանագրությանը: Նկատի ունեցեք, որ այս պահին մենք կարիք չունենք որոշակի պորտ նշելու, քանի որ հատկանիշը թույլ է տալիս վերացական մեթոդների հայտարարություններ: Եթե ​​մենք օգտագործում ենք վերացական մեթոդներ, կոմպիլյատորը կպահանջի իրականացում կոնֆիգուրացիայի օրինակում: Այստեղ մենք տրամադրել ենք իրականացումը (8081) և այն կօգտագործվի որպես լռելյայն արժեք, եթե այն բաց թողնենք կոնկրետ կոնֆիգուրացիայի մեջ:

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

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

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

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

Ծառայությանը անհրաժեշտ է գործառույթ՝ գործարկելու և նրբորեն անջատելու համար: (Ծառայությունն անջատելու ունակությունը կարևոր է թեստավորման համար:) Կրկին կան մի քանի տարբերակներ տվյալ կոնֆիգուրացիայի համար նման գործառույթ նշելու համար (օրինակ, մենք կարող ենք օգտագործել տիպի դասեր): Այս գրառման համար մենք կրկին կօգտագործենք Cake Pattern-ը: Մենք կարող ենք ներկայացնել ծառայություն՝ օգտագործելով 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](()))
  }

(Տես Source կոդը այլ ծառայությունների իրականացման համար — արձագանքների ծառայություն,
echo հաճախորդ և ցմահ կարգավորիչներ.)

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

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

Նկատի ունեցեք, որ հանգույցում մենք նշում ենք կոնֆիգուրացիայի ճշգրիտ տեսակը, որն անհրաժեշտ է այս հանգույցին: Կոմպիլյատորը թույլ չի տա մեզ կառուցել օբյեկտը (Cake) անբավարար տիպով, քանի որ ծառայության յուրաքանչյուր հատկանիշ հայտարարում է սահմանափակումներ Config տիպ. Նաև մենք չենք կարողանա սկսել հանգույցը առանց ամբողջական կազմաձևման:

Հանգույցի հասցեի լուծում

Կապ հաստատելու համար մեզ անհրաժեշտ է իրական հյուրընկալող հասցե յուրաքանչյուր հանգույցի համար: Դա կարող է հայտնի լինել ավելի ուշ, քան կազմաձևման այլ մասերը: Հետևաբար, մեզ անհրաժեշտ է հանգույցի id-ի և դրա իրական հասցեի միջև քարտեզագրում տրամադրելու միջոց: Այս քարտեզագրումը գործառույթ է.

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

Նման գործառույթն իրականացնելու մի քանի հնարավոր եղանակներ կան:

  1. Եթե ​​մենք գիտենք իրական հասցեները նախքան տեղակայումը, հանգույցների հոսթինգի ինստանցիոնացման ժամանակ, ապա մենք կարող ենք գեներացնել Scala կոդը իրական հասցեներով և այնուհետև գործարկել build-ը (որը կատարում է կոմպիլյացիայի ժամանակի ստուգումներ և այնուհետև գործարկում է ինտեգրման թեստային փաթեթը): Այս դեպքում մեր քարտեզագրման ֆունկցիան ստատիկորեն հայտնի է և կարող է պարզեցվել a-ի նման 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"
  }

Տեսեք, թե ինչպես ենք մենք նշում կախվածությունը: Մենք նշում ենք մյուս հանգույցի մատուցած ծառայությունը որպես ընթացիկ հանգույցի կախվածություն: Կախվածության տեսակը ստուգված է, քանի որ այն պարունակում է ֆանտոմային տիպ, որը նկարագրում է արձանագրությունը: Եվ գործարկման ժամանակ մենք կունենանք ճիշտ հանգույցի 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, մինչդեռ echo հաճախորդը կդադարեցվի կազմաձևված վերջավոր տևողությունից հետո: Տեսեք մեկնարկային հավելված Մանրամասների համար դիմեք

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

Տեսնենք, թե ինչպես է այս մոտեցումը փոխում կոնֆիգուրացիայի հետ աշխատելու ձևը:

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

Կազմաձևի փոփոխությունը դառնում է կոդի փոփոխություն: Այսպիսով, այն պետք է ծածկվի որակի ապահովման նույն գործընթացով.

Տոմս -> PR -> վերանայում -> միաձուլում -> շարունակական ինտեգրում -> շարունակական տեղակայում

Մոտեցման հետևյալ հետևանքները կան.

  1. Կազմաձևը համահունչ է որոշակի համակարգի օրինակի համար: Թվում է, թե հանգույցների միջև սխալ կապ ունենալու հնարավորություն չկա։
  2. Հեշտ չէ փոխել կոնֆիգուրացիան միայն մեկ հանգույցում: Թվում է, թե անհիմն է մուտք գործել և փոխել որոշ տեքստային ֆայլեր: Այսպիսով, կոնֆիգուրացիայի շեղումը դառնում է ավելի քիչ հնարավոր:
  3. Փոքր կոնֆիգուրացիայի փոփոխությունները հեշտ չէ կատարել:
  4. Կազմաձևման փոփոխությունների մեծ մասը կհետևի նույն զարգացման գործընթացին, և այն կանցնի որոշակի վերանայում:

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

Տարբերակներ

Տեսնենք առաջարկվող մոտեցման դրական և բացասական կողմերը՝ համեմատած այլ կոնֆիգուրացիայի կառավարման տեխնիկայի հետ:

Նախ, մենք կթվարկենք մի քանի այլընտրանքներ կոնֆիգուրացիայի հետ կապված առաջարկվող եղանակի տարբեր ասպեկտների համար.

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

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

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

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

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

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

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

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

Կողմ և դեմ

Այստեղ մենք կցանկանայինք առանձնացնել որոշ առավելություններ և քննարկել առաջարկվող մոտեցման որոշ թերություններ:

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

Ամբողջական բաշխված համակարգի կոմպիլյացիաների կազմաձևման առանձնահատկությունները.

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

Թերությունները

Կազմված կազմաձևման մոտեցումը տարբերվում է «նորմալ» կոնֆիգուրացիայից և կարող է չհամապատասխանել բոլոր կարիքներին: Ահա կազմված կազմաձևի թերություններից մի քանիսը.

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

Իրականացված օրինակի որոշ սահմանափակումներ կան.

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

Եզրափակում

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

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

Այս մոտեցումը կարող է ընդլայնվել տարբեր ձևերով.

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

Շնորհակալություն

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

Source: www.habr.com