Sudaryta paskirstytos sistemos konfigūracija

Norėčiau pasakyti vieną įdomų mechanizmą, kaip dirbti su paskirstytos sistemos konfigūracija. Konfigūracija pateikiama tiesiogiai kompiliuota kalba (Scala), naudojant saugius tipus. Šiame įraše pateikiamas tokios konfigūracijos pavyzdys ir aptariami įvairūs sukompiliuotos konfigūracijos diegimo į bendrą kūrimo procesą aspektai.

Sudaryta paskirstytos sistemos konfigūracija

(Anglų)

įvedimas

Patikimos paskirstytos sistemos sukūrimas reiškia, kad visi mazgai naudoja teisingą konfigūraciją, sinchronizuotą su kitais mazgais. DevOps technologijos (terraform, ansible ar kažkas panašaus) dažniausiai naudojamos automatiškai generuoti konfigūracijos failus (dažnai specifinius kiekvienam mazgui). Taip pat norėtume būti tikri, kad visi bendraujantys mazgai naudoja identiškus protokolus (įskaitant tą pačią versiją). Priešingu atveju mūsų paskirstytoje sistemoje bus įdiegtas nesuderinamumas. JVM pasaulyje viena iš šio reikalavimo pasekmių yra ta, kad visur turi būti naudojama ta pati bibliotekos, kurioje yra protokolo pranešimai, versija.

O kaip paskirstytos sistemos testavimas? Žinoma, manome, kad visi komponentai turi vienetų testus prieš pereinant prie integracijos testavimo. (Kad galėtume ekstrapoliuoti testo rezultatus į vykdymo laiką, taip pat turime pateikti identišką bibliotekų rinkinį testavimo etape ir vykdymo metu.)

Dirbant su integravimo testais, dažnai lengviau naudoti tą patį klasės kelią visuose mazguose. Viskas, ką turime padaryti, tai užtikrinti, kad vykdymo metu būtų naudojamas tas pats klasės kelias. (Nors visiškai įmanoma paleisti skirtingus mazgus su skirtingais klasės keliais, tai daro sudėtingesnę bendrą konfigūraciją ir sunkumų atliekant diegimo ir integravimo testus.) Šiame įraše darome prielaidą, kad visi mazgai naudos tą patį klasės kelią.

Konfigūracija vystosi kartu su programa. Versijas naudojame skirtingiems programos evoliucijos etapams nustatyti. Atrodo logiška nustatyti ir skirtingas konfigūracijų versijas. Ir įdėkite pačią konfigūraciją į versijos valdymo sistemą. Jei gamyboje yra tik viena konfigūracija, galime tiesiog naudoti versijos numerį. Jei naudosime daug gamybos egzempliorių, mums reikės kelių
konfigūracijos šakas ir papildomą etiketę prie versijos (pavyzdžiui, šakos pavadinimo). Tokiu būdu galime aiškiai nustatyti tikslią konfigūraciją. Kiekvienas konfigūracijos identifikatorius unikaliai atitinka tam tikrą paskirstytų mazgų, prievadų, išorinių išteklių ir bibliotekos versijų derinį. Šiame įraše darysime prielaidą, kad yra tik viena šaka ir konfigūraciją galime identifikuoti įprastu būdu, naudodami tris skaičius, atskirtus tašku (1.2.3).

Šiuolaikinėje aplinkoje konfigūracijos failai retai kuriami rankiniu būdu. Dažniau jie generuojami diegiant ir nebeliečiami (taigi nieko nesulaužyk). Kyla natūralus klausimas: kodėl mes vis dar naudojame teksto formatą konfigūracijai saugoti? Atrodo, kad tinkama alternatyva yra galimybė naudoti įprastą kodą konfigūravimui ir gauti naudos iš kompiliavimo laiko patikrų.

Šiame įraše išnagrinėsime idėją pateikti konfigūraciją sudarytame artefakte.

Sukompiliuota konfigūracija

Šiame skyriuje pateikiamas statinės sudarytos konfigūracijos pavyzdys. Įdiegtos dvi paprastos paslaugos – echo paslauga ir echo paslaugos klientas. Remiantis šiomis dviem paslaugomis, surenkamos dvi sistemos parinktys. Vienu variantu abi paslaugos yra tame pačiame mazge, kitu variantu – skirtinguose mazguose.

Paprastai paskirstytoje sistemoje yra keli mazgai. Galite nustatyti mazgus naudodami tam tikro tipo reikšmes NodeId:

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

arba

case class NodeId(hostName: String)

или даже

object Singleton
type NodeId = Singleton.type

Mazgai atlieka įvairius vaidmenis, vykdo paslaugas ir tarp jų galima užmegzti TCP/HTTP ryšius.

Norint apibūdinti TCP ryšį, mums reikia bent prievado numerio. Taip pat norėtume atspindėti tame prievade palaikomą protokolą, kad įsitikintume, jog klientas ir serveris naudoja tą patį protokolą. Mes apibūdinsime ryšį naudodami šią klasę:

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

kur Port - tik sveikasis skaičius Int nurodant priimtinų verčių diapazoną:

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

Rafinuoti tipai

Žiūrėti biblioteką rafinuotas и mano ataskaita. Trumpai tariant, biblioteka leidžia pridėti apribojimų tipams, kurie tikrinami kompiliavimo metu. Šiuo atveju galiojančios prievado numerio reikšmės yra 16 bitų sveikieji skaičiai. Sukompiliuotai konfigūracijai patobulintos bibliotekos naudojimas nėra privalomas, tačiau tai pagerina kompiliatoriaus galimybę patikrinti konfigūraciją.

HTTP (REST) ​​protokolams, be prievado numerio, mums taip pat gali prireikti kelio į paslaugą:

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

Fantomų tipai

Norėdami nustatyti protokolą kompiliavimo metu, naudojame tipo parametrą, kuris klasėje nenaudojamas. Tokį sprendimą nulėmė tai, kad vykdymo metu nenaudojame protokolo egzemplioriaus, tačiau norėtume, kad kompiliatorius patikrintų protokolo suderinamumą. Nurodę protokolą netinkamos paslaugos negalėsime perduoti kaip priklausomybės.

Vienas iš įprastų protokolų yra REST API su Json serializavimu:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

kur RequestMessage - prašymo tipas, ResponseMessage - atsakymo tipas.
Žinoma, galime naudoti kitus protokolų aprašymus, kurie suteikia mums reikalingą aprašymo tikslumą.

Šiame įraše naudosime supaprastintą protokolo versiją:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Čia užklausa yra eilutė, pridėta prie url, o atsakymas yra grąžinama eilutė HTTP atsakymo tekste.

Paslaugos konfigūracija apibūdinama paslaugos pavadinimu, prievadais ir priklausomybėmis. Šiuos elementus „Scala“ galima pavaizduoti keliais būdais (pvz., HList-s, algebrinių duomenų tipai). Šio įrašo tikslais naudosime torto šabloną ir pateiksime modulius trait'ov. (Torto šablonas nėra būtinas šio požiūrio elementas. Tai tiesiog vienas iš galimų įgyvendinimo variantų.)

Priklausomybės tarp paslaugų gali būti pavaizduotos kaip metodai, grąžinantys prievadus EndPointkitų mazgų:

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

Norint sukurti aido paslaugą, tereikia prievado numerio ir nuorodos, kad prievadas palaiko echo protokolą. Galime nenurodyti konkretaus prievado, nes... bruožai leidžia deklaruoti metodus be įgyvendinimo (abstraktūs metodai). Tokiu atveju, kurdamas konkrečią konfigūraciją, kompiliatorius pareikalautų, kad pateiktume abstrakčiojo metodo įgyvendinimą ir prievado numerį. Kadangi mes įdiegėme metodą, kurdami konkrečią konfigūraciją galime nenurodyti kito prievado. Bus naudojama numatytoji reikšmė.

Kliento konfigūracijoje deklaruojame priklausomybę nuo echo paslaugos:

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

Priklausomybė yra to paties tipo kaip ir eksportuota paslauga echoService. Visų pirma, echo kliente mums reikia to paties protokolo. Todėl sujungę dvi paslaugas galime būti tikri, kad viskas veiks tinkamai.

Paslaugų įgyvendinimas

Norint paleisti ir sustabdyti paslaugą, reikalinga funkcija. (Galimybė sustabdyti paslaugą yra labai svarbi atliekant testavimą.) Vėlgi, yra keletas tokios funkcijos įdiegimo variantų (pavyzdžiui, galime naudoti tipų klases pagal konfigūracijos tipą). Šio įrašo tikslais naudosime torto modelį. Atstovausime paslaugą naudodami klasę cats.Resource, nes Ši klasė jau suteikia priemones saugiai garantuoti išteklių atleidimą iškilus problemoms. Norėdami gauti išteklių, turime pateikti konfigūraciją ir paruoštą vykdymo laiko kontekstą. Paslaugos paleidimo funkcija gali atrodyti taip:

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

kur

  • Config — šios paslaugos konfigūracijos tipas
  • AddressResolver - vykdymo objektas, leidžiantis sužinoti kitų mazgų adresus (žr. toliau)

ir kitų tipų iš bibliotekos cats:

  • F[_] - efekto tipas (paprasčiausiu atveju F[A] gali būti tik funkcija () => A. Šiame įraše naudosime cats.IO.)
  • Reader[A,B] - daugiau ar mažiau funkcijos sinonimas A => B
  • cats.Resource - išteklius, kurį galima gauti ir išleisti
  • Timer — laikmatis (leidžia trumpam užmigti ir matuoti laiko intervalus)
  • ContextShift - analogas ExecutionContext
  • Applicative — efektų tipo klasė, leidžianti derinti atskirus efektus (beveik monada). Sudėtingesnėse programose atrodo geriau naudoti Monad/ConcurrentEffect.

Naudodami šį funkcijos parašą galime įdiegti keletą paslaugų. Pavyzdžiui, paslauga, kuri nieko nedaro:

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

(Cm. šaltinis, kurioje diegiamos kitos paslaugos - aido paslauga, echo klientas
и visą gyvenimą trunkantys valdikliai.)

Mazgas yra objektas, galintis paleisti kelias paslaugas (resursų grandinės paleidimą užtikrina torto šablonas):

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

Atkreipkite dėmesį, kad mes nurodome tikslų konfigūracijos tipą, kurio reikia šiam mazgui. Jei pamiršime nurodyti vieną iš konfigūracijos tipų, kurių reikia konkrečiai paslaugai, įvyks kompiliavimo klaida. Be to, negalėsime paleisti mazgo, jei nepateiksime kokio nors atitinkamo tipo objekto su visais reikalingais duomenimis.

Pagrindinio kompiuterio vardo raiška

Norėdami prisijungti prie nuotolinio kompiuterio, mums reikia tikro IP adreso. Gali būti, kad adresas taps žinomas vėliau nei likusi konfigūracija. Taigi mums reikia funkcijos, kuri susieja mazgo ID su adresu:

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

Yra keletas būdų, kaip įgyvendinti šią funkciją:

  1. Jei adresai mums tampa žinomi prieš įdiegiant, galime sugeneruoti Scala kodą su
    adresus ir paleiskite statybą. Tai sukompiliuos ir vykdys testus.
    Tokiu atveju funkcija bus žinoma statiškai ir gali būti pavaizduota kode kaip atvaizdavimas Map[NodeId, NodeAddress].
  2. Kai kuriais atvejais tikrasis adresas sužinomas tik pradėjus mazgui.
    Tokiu atveju galime įdiegti „atradimo paslaugą“, kuri veikia prieš kitus mazgus, o visi mazgai užsiregistruos šioje paslaugoje ir paprašys kitų mazgų adresų.
  3. Jei galime modifikuoti /etc/hosts, tada galite naudoti iš anksto nustatytus pagrindinio kompiuterio pavadinimus (pvz., my-project-main-node и echo-backend) ir tiesiog susiekite šiuos pavadinimus
    su IP adresais diegimo metu.

Šiame įraše šių atvejų plačiau nenagrinėsime. Mums
žaislo pavyzdyje visi mazgai turės tą patį IP adresą - 127.0.0.1.

Toliau svarstome dvi paskirstytos sistemos galimybes:

  1. Visų paslaugų išdėstymas viename mazge.
  2. Ir aido paslaugos bei aido kliento priegloba skirtinguose mazguose.

Konfigūracija skirta vienas mazgas:

Vieno mazgo konfigūracija

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

Objektas įgyvendina ir kliento, ir serverio konfigūraciją. Taip pat naudojama laiko iki gyvavimo konfigūracija, kad po intervalo lifetime nutraukti programą. (Ctrl-C taip pat veikia ir tinkamai atlaisvina visus išteklius.)

Tas pats konfigūracijos ir įgyvendinimo bruožų rinkinys gali būti naudojamas kuriant sistemą, kurią sudaro du atskiri mazgai:

Dviejų mazgų konfigūracija

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

Svarbu! Atkreipkite dėmesį, kaip paslaugos yra susietos. Vieno mazgo įdiegtą paslaugą nurodome kaip kito mazgo priklausomybės metodo įgyvendinimą. Priklausomybės tipą tikrina kompiliatorius, nes yra protokolo tipas. Kai vykdoma, priklausomybėje bus tinkamas tikslinio mazgo ID. Šios schemos dėka tiksliai vieną kartą nurodome prievado numerį ir visada garantuojame, kad nurodysime tinkamą prievadą.

Dviejų sistemos mazgų įgyvendinimas

Šiai konfigūracijai naudojame tuos pačius paslaugų diegimus be pakeitimų. Vienintelis skirtumas yra tas, kad dabar turime du objektus, kurie įgyvendina skirtingus paslaugų rinkinius:

  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
  }

Pirmasis mazgas įgyvendina serverį ir jam reikia tik serverio konfigūracijos. Antrasis mazgas įgyvendina klientą ir naudoja kitą konfigūracijos dalį. Be to, abu mazgai turi valdyti visą gyvenimą. Serverio mazgas veikia neribotą laiką, kol jis sustabdomas SIGTERM'om, ir kliento mazgas nutrūksta po kurio laiko. Cm. paleidimo programa.

Bendras kūrimo procesas

Pažiūrėkime, kaip šis konfigūravimo metodas veikia bendrą kūrimo procesą.

Konfigūracija bus sudaryta kartu su likusiu kodu ir bus sugeneruotas artefaktas (.jar). Atrodo, kad tikslinga konfigūraciją sudėti į atskirą artefaktą. Taip yra todėl, kad galime turėti kelias konfigūracijas pagal tą patį kodą. Vėlgi, galima generuoti artefaktus, atitinkančius skirtingas konfigūracijos šakas. Priklausomybės nuo konkrečių bibliotekų versijų išsaugomos kartu su konfigūracija ir šios versijos išsaugomos amžinai, kai tik nusprendžiame įdiegti tą konfigūracijos versiją.

Bet koks konfigūracijos pakeitimas virsta kodo pakeitimu. Ir todėl kiekvienas
pakeitimui bus taikomas įprastas kokybės užtikrinimo procesas:

Bilietas klaidų sekimo priemonėje -> PR -> peržiūra -> sujungti su atitinkamomis šakomis ->
integracija -> diegimas

Pagrindinės sukompiliuotos konfigūracijos įgyvendinimo pasekmės yra šios:

  1. Konfigūracija bus vienoda visuose paskirstytos sistemos mazguose. Dėl to, kad visi mazgai gauna tą pačią konfigūraciją iš vieno šaltinio.

  2. Sunku pakeisti konfigūraciją tik viename iš mazgų. Todėl „konfigūracijos nukrypimas“ mažai tikėtinas.

  3. Tampa sunkiau atlikti nedidelius konfigūracijos pakeitimus.

  4. Dauguma konfigūracijos pakeitimų įvyks kaip bendro kūrimo proceso dalis ir bus peržiūrimi.

Ar man reikia atskiros saugyklos gamybos konfigūracijai saugoti? Šioje konfigūracijoje gali būti slaptažodžių ir kitos neskelbtinos informacijos, prie kurios norėtume apriboti prieigą. Remiantis tuo, atrodo, kad tikslinga galutinę konfigūraciją saugoti atskiroje saugykloje. Galite padalyti konfigūraciją į dvi dalis – vienoje yra viešai pasiekiami konfigūracijos nustatymai, o kitoje – riboti parametrai. Tai leis daugumai kūrėjų turėti prieigą prie bendrų nustatymų. Šį atskyrimą lengva pasiekti naudojant tarpinius bruožus su numatytosiomis reikšmėmis.

Galimi variantai

Pabandykime palyginti sudarytą konfigūraciją su kai kuriomis įprastomis alternatyvomis:

  1. Teksto failas tiksliniame kompiuteryje.
  2. Centralizuota raktų vertės saugykla (etcd/zookeeper).
  3. Proceso komponentai, kuriuos galima iš naujo sukonfigūruoti / paleisti iš naujo nepaleidžiant proceso iš naujo.
  4. Konfigūracijos saugojimas už artefaktų ir versijų valdymo ribų.

Tekstiniai failai suteikia daug lankstumo atliekant nedidelius pakeitimus. Sistemos administratorius gali prisijungti prie nuotolinio mazgo, atlikti atitinkamų failų pakeitimus ir iš naujo paleisti paslaugą. Tačiau didelėms sistemoms toks lankstumas gali būti nepageidautinas. Atlikti pakeitimai nepalieka pėdsakų kitose sistemose. Pakeitimų niekas neperžiūri. Sunku nustatyti, kas tiksliai atliko pakeitimus ir dėl kokios priežasties. Pakeitimai nėra tikrinami. Jei sistema paskirstyta, administratorius gali pamiršti atlikti atitinkamus pakeitimus kituose mazguose.

(Taip pat reikėtų atkreipti dėmesį į tai, kad naudojant sukompiliuotą konfigūraciją, nebus panaikinta galimybė ateityje naudoti tekstinius failus. Pakaks pridėti analizatorių ir tikrintuvą, kuris sukuria tokio paties tipo kaip išvestis Configir galite naudoti tekstinius failus. Iš karto matyti, kad sistemos su sudaryta konfigūracija sudėtingumas yra šiek tiek mažesnis nei sistemos, naudojančios tekstinius failus, sudėtingumas, nes tekstiniams failams reikia papildomo kodo.)

Centralizuota raktų verčių saugykla yra geras paskirstytos programos metaparametrų paskirstymo mechanizmas. Turime nuspręsti, kas yra konfigūracijos parametrai, o kas – tik duomenys. Leiskite mums atlikti funkciją C => A => B, ir parametrus C retai keičiasi, ir duomenis A - dažnai. Šiuo atveju galime pasakyti C - konfigūracijos parametrai ir A - duomenys. Atrodo, kad konfigūracijos parametrai skiriasi nuo duomenų tuo, kad paprastai keičiasi rečiau nei duomenys. Be to, duomenys dažniausiai gaunami iš vieno šaltinio (iš vartotojo), o konfigūracijos parametrai iš kito (iš sistemos administratoriaus).

Jei retai besikeičiančius parametrus reikia atnaujinti nepaleidžiant programos iš naujo, tai dažnai gali sukelti programos komplikacijų, nes mums reikės kažkaip pateikti parametrus, saugoti, analizuoti ir patikrinti bei apdoroti neteisingas reikšmes. Todėl programos sudėtingumo mažinimo požiūriu prasminga sumažinti parametrų, kurie gali keistis programos veikimo metu (arba iš viso nepalaikyti tokių parametrų), skaičių.

Šio įrašo tikslais skirsime statinius ir dinaminius parametrus. Jei paslaugos logika reikalauja keisti parametrus programos veikimo metu, tai tokius parametrus vadinsime dinaminiais. Kitu atveju parinktys yra statinės ir jas galima konfigūruoti naudojant sudarytą konfigūraciją. Dinaminiam perkonfigūravimui mums gali prireikti mechanizmo, leidžiančio iš naujo paleisti programos dalis su naujais parametrais, panašiai kaip operacinės sistemos procesai paleidžiami iš naujo. (Mūsų nuomone, patartina vengti perkonfigūravimo realiuoju laiku, nes tai padidina sistemos sudėtingumą. Jei įmanoma, procesams iš naujo paleisti, jei įmanoma, naudoti standartines OS galimybes.)

Vienas svarbus statinės konfigūracijos naudojimo aspektas, dėl kurio žmonės galvoja apie dinaminę konfigūraciją, yra laikas, per kurį sistema paleidžiama iš naujo po konfigūracijos atnaujinimo (prastova). Tiesą sakant, jei mums reikia pakeisti statinę konfigūraciją, turėsime iš naujo paleisti sistemą, kad naujos reikšmės įsigaliotų. Skirtingose ​​sistemose prastovos problemos sunkumas skiriasi. Kai kuriais atvejais galite suplanuoti perkrovimą tuo metu, kai apkrova yra minimali. Jei reikia teikti nuolatines paslaugas, galite įgyvendinti AWS ELB jungties nutekėjimas. Tuo pačiu metu, kai reikia iš naujo paleisti sistemą, paleidžiame lygiagrečią šios sistemos egzempliorių, įjungiame balansavimo priemonę ir laukiame, kol baigsis senieji ryšiai. Nutraukus visus senus ryšius, išjungiame senąjį sistemos egzempliorių.

Dabar apsvarstykime konfigūracijos saugojimo artefakto viduje ar išorėje klausimą. Jei konfigūraciją saugome artefakte, tai bent jau turėjome galimybę patikrinti konfigūracijos teisingumą surenkant artefaktą. Jei konfigūracija nepatenka į valdomą artefaktą, sunku atsekti, kas ir kodėl atliko šio failo pakeitimus. Kiek tai svarbu? Mūsų nuomone, daugeliui gamybos sistemų svarbu turėti stabilią ir kokybišką konfigūraciją.

Artefakto versija leidžia nustatyti, kada jis buvo sukurtas, kokios reikšmės jame yra, kokios funkcijos įjungtos / išjungtos ir kas atsakingas už bet kokius konfigūracijos pakeitimus. Žinoma, norint išsaugoti konfigūraciją artefakte, reikia šiek tiek pastangų, todėl turite priimti pagrįstą sprendimą.

Privalumai ir trūkumai

Norėčiau trumpai aptarti siūlomos technologijos privalumus ir trūkumus.

privalumai

Žemiau pateikiamas pagrindinių sudarytos paskirstytos sistemos konfigūracijos ypatybių sąrašas:

  1. Statinės konfigūracijos patikrinimas. Leidžia tuo įsitikinti
    konfigūracija teisinga.
  2. Turtinga konfigūracijos kalba. Paprastai kiti konfigūravimo metodai apsiriboja ne daugiau kaip eilutės kintamųjų pakeitimu. Kai naudojate „Scala“, galite naudoti daugybę kalbinių funkcijų, kad pagerintumėte konfigūraciją. Pavyzdžiui, galime naudoti
    Numatytųjų reikšmių bruožus, naudodami objektus parametrams sugrupuoti, įtraukiamojoje srityje galime nurodyti tik vieną kartą deklaruotas vertes (DRY). Galite sukurti bet kokias klases tiesiogiai konfigūracijos viduje (Seq, Map, pasirinktines klases).
  3. DSL. „Scala“ turi daugybę kalbos funkcijų, kurios palengvina DSL kūrimą. Galima pasinaudoti šiomis funkcijomis ir įdiegti tikslinei vartotojų grupei patogesnę konfigūravimo kalbą, kad konfigūraciją bent jau perskaitytų domeno ekspertai. Pavyzdžiui, specialistai gali dalyvauti konfigūracijos peržiūros procese.
  4. Vientisumas ir sinchroniškumas tarp mazgų. Vienas iš privalumų, kai visos paskirstytos sistemos konfigūracija saugoma viename taške, yra tai, kad visos reikšmės deklaruojamos tiksliai vieną kartą, o vėliau panaudojamos ten, kur jų reikia. Naudojant fantominius tipus prievadams deklaruoti, užtikrinama, kad mazgai naudotų suderinamus protokolus visose teisingose ​​sistemos konfigūracijose. Aiškios privalomos priklausomybės tarp mazgų užtikrina, kad visos paslaugos yra sujungtos.
  5. Aukštos kokybės pokyčiai. Konfigūracijos pakeitimai naudojant bendrą kūrimo procesą leidžia pasiekti aukštus kokybės standartus ir konfigūracijai.
  6. Konfigūracijos atnaujinimas vienu metu. Automatinis sistemos diegimas po konfigūracijos pakeitimų užtikrina, kad visi mazgai būtų atnaujinti.
  7. Paraiškos supaprastinimas. Programai nereikia analizuoti, tikrinti konfigūraciją ar tvarkyti neteisingas reikšmes. Tai sumažina programos sudėtingumą. (Kai kurios konfigūracijos sudėtingumas, pastebėtas mūsų pavyzdyje, nėra sudarytos konfigūracijos atributas, o tik sąmoningas sprendimas, nulemtas noro užtikrinti didesnį tipo saugumą.) Gana lengva grįžti prie įprastos konfigūracijos – tereikia įgyvendinti trūkstamą dalys. Todėl, pavyzdžiui, galite pradėti nuo sudarytos konfigūracijos, atidėdami nereikalingų dalių diegimą iki to laiko, kai to tikrai prireiks.
  8. Patvirtinta konfigūracija. Kadangi konfigūracijos pakeitimai atitinka įprastą bet kokių kitų pakeitimų likimą, gaunama išvestis yra artefaktas su unikalia versija. Tai leidžia, pavyzdžiui, prireikus grįžti prie ankstesnės konfigūracijos versijos. Galime naudoti net prieš metus buvusią konfigūraciją ir sistema veiks lygiai taip pat. Stabili konfigūracija pagerina paskirstytos sistemos nuspėjamumą ir patikimumą. Kadangi konfigūracija fiksuojama kompiliavimo etape, gana sunku ją suklastoti gamyboje.
  9. Moduliškumas. Siūloma sistema yra modulinė ir moduliai gali būti derinami įvairiais būdais kuriant skirtingas sistemas. Visų pirma, galite sukonfigūruoti sistemą, kad ji veiktų viename mazge viename įgyvendinimo variante, o kitame – keliuose mazguose. Galite sukurti kelias sistemos gamybos egzempliorių konfigūracijas.
  10. Testavimas. Atskiras paslaugas pakeitę netikrais objektais, galite gauti keletą patogių testavimui sistemos versijų.
  11. Integracijos testavimas. Turint vieną visos paskirstytos sistemos konfigūraciją, galima paleisti visus komponentus kontroliuojamoje aplinkoje kaip integracijos testavimo dalį. Nesunku imituoti, pavyzdžiui, situaciją, kai kai kurie mazgai tampa prieinami.

Trūkumai ir apribojimai

Sukompiliuota konfigūracija skiriasi nuo kitų konfigūravimo metodų ir gali būti netinkama kai kurioms programoms. Žemiau yra keletas trūkumų:

  1. Statinė konfigūracija. Kartais reikia greitai ištaisyti konfigūraciją gamyboje, apeinant visus apsauginius mechanizmus. Taikant šį metodą gali būti sunkiau. Bent jau vis tiek reikės kompiliavimo ir automatinio diegimo. Tai yra ir naudinga metodo savybė, ir kai kuriais atvejais trūkumas.
  2. Konfigūracijos generavimas. Jei konfigūracijos failą generuoja automatinis įrankis, gali prireikti papildomų pastangų norint integruoti kūrimo scenarijų.
  3. Įrankiai. Šiuo metu komunalinės paslaugos ir metodai, skirti dirbti su konfigūracija, yra pagrįsti tekstiniais failais. Ne visos tokios komunalinės paslaugos/technikos bus prieinamos sukompiliuotoje konfigūracijoje.
  4. Reikia keisti požiūrį. Kūrėjai ir „DevOps“ yra pripratę prie tekstinių failų. Pati konfigūracijos sudarymo idėja gali būti šiek tiek netikėta ir neįprasta ir sukelti atmetimą.
  5. Reikalingas aukštos kokybės kūrimo procesas. Norint patogiai naudoti sukompiliuotą konfigūraciją, būtina visiškai automatizuoti programos (CI/CD) kūrimo ir diegimo procesą. Priešingu atveju tai bus gana nepatogu.

Taip pat apsistokime ties keliais nagrinėjamo pavyzdžio apribojimais, nesusijusiais su sudarytos konfigūracijos idėja:

  1. Jei pateikiame nereikalingą konfigūracijos informaciją, kurios mazgas nenaudoja, kompiliatorius nepadės mums aptikti trūkstamo diegimo. Šią problemą galima išspręsti atsisakius tortų modelio ir naudojant griežtesnius tipus, pvz. HList arba algebriniai duomenų tipai (atvejų klasės), kad atvaizduotų konfigūraciją.
  2. Konfigūracijos faile yra eilučių, nesusijusių su pačia konfigūracija: (package, import,objektų deklaracijos; override def's parametrams, kurių numatytosios reikšmės). To iš dalies galima išvengti, jei įdiegsite savo DSL. Be to, kitų tipų konfigūracija (pavyzdžiui, XML) taip pat nustato tam tikrus failo struktūros apribojimus.
  3. Šio įrašo tikslais nesvarstome dinaminio panašių mazgų klasterio perkonfigūravimo.

išvada

Šiame įraše išnagrinėjome idėją pateikti konfigūraciją šaltinio kode naudojant išplėstines Scala tipo sistemos galimybes. Šis metodas gali būti naudojamas įvairiose programose kaip tradicinių konfigūravimo metodų, pagrįstų xml arba tekstiniais failais, pakaitalas. Nors mūsų pavyzdys įgyvendintas „Scala“, tos pačios idėjos gali būti perkeltos į kitas sudarytas kalbas (pvz., Kotlin, C#, Swift ir kt.). Galite išbandyti šį metodą viename iš toliau pateiktų projektų ir, jei jis neveikia, pereikite prie tekstinio failo, pridėdami trūkstamas dalis.

Žinoma, sukompiliuota konfigūracija reikalauja aukštos kokybės kūrimo proceso. Už tai užtikrinama aukšta konfigūracijų kokybė ir patikimumas.

Aptariamą metodą galima išplėsti:

  1. Galite naudoti makrokomandas kompiliavimo laiko patikrai atlikti.
  2. Galite įdiegti DSL, kad pateiktumėte konfigūraciją taip, kad ji būtų prieinama galutiniams vartotojams.
  3. Galite įdiegti dinaminį išteklių valdymą automatiniu konfigūracijos koregavimu. Pavyzdžiui, pakeitus mazgų skaičių klasteryje, reikia, kad (1) kiekvienas mazgas gautų šiek tiek skirtingą konfigūraciją; (2) klasterio valdytojas gavo informaciją apie naujus mazgus.

Padėkos

Norėčiau padėkoti Andrejui Saksonovui, Pavelui Popovui ir Antonui Nekhajevui už konstruktyvią straipsnio projekto kritiką.

Šaltinis: www.habr.com

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