Konfiguracija porazdeljenega sistema, ki jo je mogoče prevesti

V tej objavi bi radi delili zanimiv način obravnavanja konfiguracije porazdeljenega sistema.
Konfiguracija je predstavljena neposredno v jeziku Scala na tipsko varen način. Primer izvedbe je podrobno opisan. Obravnavani so različni vidiki predloga, vključno z vplivom na celoten razvojni proces.

Konfiguracija porazdeljenega sistema, ki jo je mogoče prevesti

(v ruščini)

Predstavitev

Izgradnja robustnih porazdeljenih sistemov zahteva uporabo pravilne in skladne konfiguracije na vseh vozliščih. Tipična rešitev je uporaba besedilnega opisa uvajanja (terraform, ansible ali kaj podobnega) in samodejno ustvarjenih konfiguracijskih datotek (pogosto — namenjenih vsakemu vozlišču/vlogi). Prav tako bi želeli uporabiti iste protokole istih različic na vseh vozliščih, ki komunicirajo (sicer bi imeli težave z nezdružljivostjo). V svetu JVM to pomeni, da mora biti vsaj knjižnica za sporočanje enake različice na vseh vozliščih, ki komunicirajo.

Kaj pa testiranje sistema? Seveda bi morali imeti teste enot za vse komponente, preden pridemo do integracijskih testov. Da bi lahko ekstrapolirali rezultate testov v izvajalnem okolju, moramo zagotoviti, da so različice vseh knjižnic enake v izvajalnem in testnem okolju.

Pri izvajanju integracijskih testov je pogosto veliko lažje imeti isto razredno pot na vseh vozliščih. Zagotoviti moramo le, da se pri uvajanju uporablja ista razredna pot. (Na različnih vozliščih je mogoče uporabiti različne razredne poti, vendar je težje predstaviti to konfiguracijo in jo pravilno razmestiti.) Da bi stvari poenostavili, bomo upoštevali samo enake razredne poti na vseh vozliščih.

Konfiguracija se razvija skupaj s programsko opremo. Običajno uporabljamo različice za prepoznavanje različnih
stopnje razvoja programske opreme. Zdi se smiselno konfiguracijo zajeti pod upravljanjem različic in različne konfiguracije identificirati z nekaterimi oznakami. Če je v proizvodnji samo ena konfiguracija, lahko kot identifikator uporabimo eno različico. Včasih imamo lahko več produkcijskih okolij. In za vsako okolje bomo morda potrebovali ločeno vejo konfiguracije. Tako so lahko konfiguracije označene z vejo in različico za enolično prepoznavanje različnih konfiguracij. Vsaka oznaka veje in različica ustrezata eni sami kombinaciji porazdeljenih vozlišč, vrat, zunanjih virov, različic knjižnice poti razreda na vsakem vozlišču. Tukaj bomo pokrivali samo eno vejo in identificirali konfiguracije s trikomponentno decimalno različico (1.2.3), na enak način kot druge artefakte.

V sodobnih okoljih se konfiguracijske datoteke ne spreminjajo več ročno. Običajno ustvarjamo
konfiguracijske datoteke v času uvajanja in nikoli se jih ne dotikajte pozneje. Torej bi se lahko vprašali, zakaj še vedno uporabljamo besedilno obliko za konfiguracijske datoteke? Izvedljiva možnost je, da konfiguracijo postavite znotraj prevajalske enote in izkoristite validacijo konfiguracije med prevajanjem.

V tej objavi bomo preučili zamisel o ohranjanju konfiguracije v prevedenem artefaktu.

Konfiguracija, ki jo je mogoče prevesti

V tem razdelku bomo obravnavali primer statične konfiguracije. Dve enostavni storitvi - storitev echo in odjemalec storitve echo sta v fazi konfiguriranja in implementacije. Nato se ustvarita dva različna porazdeljena sistema z obema storitvama. Ena je za konfiguracijo enega vozlišča, druga pa za konfiguracijo dveh vozlišč.

Tipičen porazdeljen sistem je sestavljen iz nekaj vozlišč. Vozlišča je mogoče identificirati z uporabo neke vrste:

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

ali samo

case class NodeId(hostName: String)

ali celo

object Singleton
type NodeId = Singleton.type

Ta vozlišča opravljajo različne vloge, izvajajo nekatere storitve in morajo imeti možnost komuniciranja z drugimi vozlišči prek povezav TCP/HTTP.

Za povezavo TCP je potrebna vsaj številka vrat. Prav tako želimo zagotoviti, da odjemalec in strežnik uporabljata isti protokol. Da bi modelirali povezavo med vozlišči, deklarirajmo naslednji razred:

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

Kje Port je samo an Int v dovoljenem območju:

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

Rafinirane vrste

Poglej rafinirano knjižnica. Skratka, omogoča dodajanje časovnih omejitev prevajanja drugim vrstam. V tem primeru Int sme imeti samo 16-bitne vrednosti, ki lahko predstavljajo številko vrat. Za ta konfiguracijski pristop ni treba uporabljati te knjižnice. Zdi se, da se zelo dobro prilega.

Za HTTP (REST) ​​​​morda potrebujemo tudi pot do storitve:

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

Tip fantoma

Za identifikacijo protokola med prevajanjem uporabljamo funkcijo Scala za deklaracijo tipa argumenta Protocol ki se ne uporablja v razredu. To je tako imenovani fantomski tip. Med izvajanjem redko potrebujemo primerek identifikatorja protokola, zato ga ne shranjujemo. Med prevajanjem ta fantomski tip zagotavlja dodatno varnost tipa. Ne moremo posredovati vrat z nepravilnim protokolom.

Eden najpogosteje uporabljenih protokolov je REST API s serializacijo Json:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

Kje RequestMessage je osnovna vrsta sporočil, ki jih lahko odjemalec pošlje strežniku in ResponseMessage je odgovorno sporočilo strežnika. Seveda lahko ustvarimo druge opise protokolov, ki določajo komunikacijski protokol z želeno natančnostjo.

Za namene te objave bomo uporabili enostavnejšo različico protokola:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

V tem protokolu je sporočilo zahteve pripeto url-ju in odgovorno sporočilo je vrnjeno kot navaden niz.

Konfiguracijo storitve bi lahko opisali z imenom storitve, zbirko vrat in nekaterimi odvisnostmi. Obstaja nekaj možnih načinov, kako predstaviti vse te elemente v Scali (npr. HList, algebrski tipi podatkov). Za namene te objave bomo uporabili vzorec torte in predstavili združljive kose (module) kot lastnosti. (Vzorec torte ni pogoj za ta konfiguracijski pristop, ki ga je mogoče prevesti. Je le ena možna izvedba ideje.)

Odvisnosti bi lahko predstavili z uporabo vzorca torte kot končne točke drugih vozlišč:

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

Storitev Echo potrebuje samo konfiguracijo vrat. In izjavljamo, da ta vrata podpirajo protokol echo. Upoštevajte, da nam trenutno ni treba določiti določenih vrat, ker lastnosti omogočajo deklaracije abstraktnih metod. Če uporabljamo abstraktne metode, bo prevajalnik zahteval implementacijo v primerku konfiguracije. Tukaj smo zagotovili izvedbo (8081) in bo uporabljena kot privzeta vrednost, če jo preskočimo v konkretni konfiguraciji.

V konfiguraciji odjemalca storitve echo lahko deklariramo odvisnost:

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

Odvisnost ima isto vrsto kot echoService. Zlasti zahteva enak protokol. Zato smo lahko prepričani, da bosta ti dve odvisnosti delovali pravilno, če povežemo.

Izvedba storitev

Storitev potrebuje funkcijo za zagon in elegantno zaustavitev. (Zmožnost zaustavitve storitve je ključnega pomena za testiranje.) Spet obstaja nekaj možnosti za določitev takšne funkcije za dano konfiguracijo (na primer, lahko uporabimo razrede tipov). Za to objavo bomo ponovno uporabili vzorec torte. Storitev lahko predstavljamo z uporabo cats.Resource ki že zagotavlja oklepanje in sprostitev virov. Če želimo pridobiti vir, moramo zagotoviti konfiguracijo in nekaj konteksta izvajalnega časa. Torej je lahko funkcija zagona storitve videti takole:

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

Kje

  • Config — vrsta konfiguracije, ki jo zahteva ta zaganjalnik storitve
  • AddressResolver — objekt izvajalnega okolja, ki lahko pridobi dejanske naslove drugih vozlišč (nadaljujte z branjem za podrobnosti).

druge vrste izvirajo iz cats:

  • F[_] — vrsta učinka (v najpreprostejšem primeru F[A] lahko samo () => A. V tej objavi bomo uporabili cats.IO.)
  • Reader[A,B] — je bolj ali manj sinonim za funkcijo A => B
  • cats.Resource — ima načine za pridobitev in sprostitev
  • Timer — omogoča spanje/merjenje časa
  • ContextShift - analog od ExecutionContext
  • Applicative — ovoj delujočih funkcij (skoraj monada) (lahko ga sčasoma zamenjamo s čim drugim)

Z uporabo tega vmesnika lahko implementiramo nekaj storitev. Na primer, storitev, ki ne naredi ničesar:

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

(Glej Izvorna koda za druge izvedbe storitev — echo storitev,
odjemalec echo in doživljenjski krmilniki.)

Vozlišče je en sam objekt, ki izvaja nekaj storitev (zagon verige virov omogoča Cake Pattern):

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

Upoštevajte, da v vozlišču določimo točno vrsto konfiguracije, ki jo potrebuje to vozlišče. Prevajalnik nam ne dovoli, da zgradimo objekt (Cake) z nezadostnim tipom, ker vsaka lastnost storitve deklarira omejitev na Config vrsta. Prav tako ne bomo mogli zagnati vozlišča brez popolne konfiguracije.

Ločljivost naslova vozlišča

Za vzpostavitev povezave potrebujemo pravi gostiteljski naslov za vsako vozlišče. Morda bo znan pozneje kot drugi deli konfiguracije. Zato potrebujemo način za zagotavljanje preslikave med ID-jem vozlišča in njegovim dejanskim naslovom. Ta preslikava je funkcija:

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

Obstaja nekaj možnih načinov za izvedbo takšne funkcije.

  1. Če poznamo dejanske naslove pred uvedbo, med instanciacijo gostiteljev vozlišč, potem lahko ustvarimo kodo Scala z dejanskimi naslovi in ​​zaženemo gradnjo pozneje (ki izvaja preverjanja časa prevajanja in nato zažene zbirko integracijskih testov). V tem primeru je naša funkcija preslikave znana statično in jo je mogoče poenostaviti na nekaj podobnega a Map[NodeId, NodeAddress].
  2. Včasih dejanske naslove pridobimo šele pozneje, ko je vozlišče dejansko zagnano, ali pa nimamo naslovov vozlišč, ki še niso bila zagnana. V tem primeru imamo morda storitev odkrivanja, ki se zažene pred vsemi drugimi vozlišči, in vsako vozlišče lahko objavi svoj naslov v tej storitvi in ​​se naroči na odvisnosti.
  3. Če lahko spremenimo /etc/hosts, lahko uporabimo vnaprej določena imena gostiteljev (npr my-project-main-node in echo-backend) in samo povežite to ime z naslovom ip ob času uvajanja.

V tej objavi teh primerov ne pokrivamo podrobneje. Pravzaprav bodo imela v našem primeru igrače vsa vozlišča enak naslov IP — 127.0.0.1.

V tej objavi bomo obravnavali dve postavitvi porazdeljenega sistema:

  1. Postavitev z enim vozliščem, kjer so vse storitve nameščene na enem vozlišču.
  2. Postavitev dveh vozlišč, kjer sta storitev in odjemalec na različnih vozliščih.

Konfiguracija za a eno vozlišče postavitev je naslednja:

Konfiguracija enega vozlišč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.
}

Tukaj ustvarimo eno samo konfiguracijo, ki razširja konfiguracijo strežnika in odjemalca. Prav tako konfiguriramo krmilnik življenjskega cikla, ki bo po tem običajno prekinil odjemalca in strežnik lifetime intervalni prehodi.

Isti niz implementacij in konfiguracij storitev je mogoče uporabiti za ustvarjanje postavitve sistema z dvema ločenima vozliščema. Le ustvarjati moramo dve ločeni konfiguraciji vozlišča z ustreznimi storitvami:

Konfiguracija dveh vozlišč

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

Oglejte si, kako določimo odvisnost. Storitev, ki jo nudi drugo vozlišče, omenjamo kot odvisnost od trenutnega vozlišča. Tip odvisnosti je preverjen, ker vsebuje tip fantoma, ki opisuje protokol. In med izvajanjem bomo imeli pravilen ID vozlišča. To je eden od pomembnih vidikov predlaganega konfiguracijskega pristopa. Zagotavlja nam možnost, da vrata nastavimo samo enkrat in se prepričamo, da se sklicujemo na pravilna vrata.

Izvedba dveh vozlišč

Za to konfiguracijo uporabljamo popolnoma enake izvedbe storitev. Brez sprememb. Vendar ustvarimo dve različni izvedbi vozlišča, ki vsebujeta različen nabor storitev:

  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
  }

Prvo vozlišče izvaja strežnik in potrebuje samo konfiguracijo na strani strežnika. Drugo vozlišče izvaja odjemalca in potrebuje še en del konfiguracije. Obe vozlišči zahtevata določeno življenjsko dobo. Za namene tega poštnega vozlišča bo imelo neskončno življenjsko dobo, ki jo je mogoče prekiniti z uporabo SIGTERM, medtem ko se bo odjemalec echo končal po konfiguriranem končnem trajanju. Glej začetna aplikacija za podrobnosti.

Celoten razvojni proces

Poglejmo, kako ta pristop spremeni način dela s konfiguracijo.

Konfiguracija kot koda bo prevedena in ustvarila artefakt. Zdi se smiselno ločiti artefakte konfiguracije od drugih artefaktov kode. Pogosto imamo lahko množico konfiguracij na isti osnovi kode. In seveda lahko imamo več različic različnih konfiguracijskih vej. V konfiguraciji lahko izberemo določene različice knjižnic in to bo ostalo nespremenjeno, kadar koli uvedemo to konfiguracijo.

Sprememba konfiguracije postane sprememba kode. Zato mora biti zajet v istem postopku zagotavljanja kakovosti:

Vstopnica -> PR -> pregled -> spajanje -> neprekinjena integracija -> neprekinjeno uvajanje

Obstajajo naslednje posledice pristopa:

  1. Konfiguracija je skladna za določen sistemski primerek. Zdi se, da med vozlišči ni mogoče vzpostaviti napačne povezave.
  2. Ni enostavno spremeniti konfiguracije samo v enem vozlišču. Zdi se nesmiselno prijaviti se in spremeniti nekatere besedilne datoteke. Tako postane odmik konfiguracije manj možen.
  3. Majhnih konfiguracijskih sprememb ni lahko izvesti.
  4. Večina sprememb konfiguracije bo sledila istemu razvojnemu procesu in bo prestala nekaj pregledov.

Ali potrebujemo ločen repozitorij za produkcijsko konfiguracijo? Produkcijska konfiguracija lahko vsebuje občutljive podatke, ki bi jih želeli hraniti izven dosega mnogih ljudi. Zato bi bilo morda vredno obdržati ločen repozitorij z omejenim dostopom, ki bo vseboval produkcijsko konfiguracijo. Konfiguracijo lahko razdelimo na dva dela - enega, ki vsebuje najbolj odprte parametre proizvodnje, in enega, ki vsebuje tajni del konfiguracije. To bi večini razvijalcev omogočilo dostop do velike večine parametrov, hkrati pa omejilo dostop do res občutljivih stvari. To je enostavno doseči z uporabo vmesnih lastnosti s privzetimi vrednostmi parametrov.

Variacije

Oglejmo si prednosti in slabosti predlaganega pristopa v primerjavi z drugimi tehnikami upravljanja konfiguracije.

Najprej bomo našteli nekaj alternativ za različne vidike predlaganega načina obravnavanja konfiguracije:

  1. Besedilna datoteka na ciljnem računalniku.
  2. Centralizirano shranjevanje ključev in vrednosti (npr etcd/zookeeper).
  3. Komponente podprocesa, ki jih je mogoče znova konfigurirati/ponovno zagnati brez ponovnega zagona procesa.
  4. Konfiguracija zunaj artefakta in nadzora različic.

Besedilna datoteka daje nekaj prilagodljivosti v smislu ad hoc popravkov. Skrbnik sistema se lahko prijavi v ciljno vozlišče, naredi spremembo in preprosto znova zažene storitev. To morda ne bo tako dobro za večje sisteme. Za spremembo ne ostanejo sledi. Spremembe ne pregleda drug par oči. Morda bo težko ugotoviti, kaj je povzročilo spremembo. Ni testirano. Z vidika porazdeljenega sistema lahko skrbnik preprosto pozabi posodobiti konfiguracijo v enem od drugih vozlišč.

(Btw, če bo sčasoma treba začeti uporabljati besedilne konfiguracijske datoteke, bomo morali samo dodati razčlenjevalnik + validator, ki bi lahko ustvaril enako Config tip in to bi bilo dovolj za začetek uporabe besedilnih konfiguracij. To tudi kaže, da je zapletenost konfiguracije v času prevajanja nekoliko manjša od zapletenosti besedilnih konfiguracij, ker v besedilni različici potrebujemo nekaj dodatne kode.)

Centralizirano shranjevanje ključev in vrednosti je dober mehanizem za distribucijo metaparametrov aplikacije. Tukaj moramo razmisliti o tem, kaj štejemo za konfiguracijske vrednosti in kaj so samo podatki. Glede na funkcijo C => A => B običajno imenujemo redko spreminjajoče se vrednosti C "konfiguracijo", medtem ko pogosto spremenjene podatke A - samo vnesite podatke. Funkciji je treba konfiguracijo zagotoviti prej kot podatke A. Glede na to idejo lahko rečemo, da je pričakovana pogostost sprememb tisto, kar bi lahko uporabili za razlikovanje konfiguracijskih podatkov od samih podatkov. Tudi podatki običajno prihajajo iz enega vira (uporabnika), konfiguracija pa iz drugega vira (skrbnika). Ukvarjanje s parametri, ki jih je mogoče spremeniti po postopku inicializacije, povzroči povečanje kompleksnosti aplikacije. Za takšne parametre bomo morali obravnavati njihov mehanizem dostave, razčlenjevanje in preverjanje veljavnosti ter obravnavanje nepravilnih vrednosti. Da bi zmanjšali kompleksnost programa, je bolje, da zmanjšamo število parametrov, ki se lahko spreminjajo med izvajanjem (ali jih celo popolnoma odstranimo).

Z vidika te objave bi morali razlikovati med statičnimi in dinamičnimi parametri. Če storitvena logika zahteva redko spremembo nekaterih parametrov med izvajanjem, jih lahko imenujemo dinamični parametri. V nasprotnem primeru so statični in jih je mogoče konfigurirati s predlaganim pristopom. Za dinamično rekonfiguracijo bodo morda potrebni drugi pristopi. Na primer, dele sistema je mogoče znova zagnati z novimi konfiguracijskimi parametri na podoben način kot ponovni zagon ločenih procesov porazdeljenega sistema.
(Moje skromno mnenje je, da se izogibajte rekonfiguraciji izvajalnega časa, ker povečuje kompleksnost sistema.
Morda bi bilo bolj preprosto, če bi se zanašali samo na podporo OS za ponovni zagon procesov. Čeprav morda ne bo vedno mogoče.)

Eden od pomembnih vidikov uporabe statične konfiguracije, zaradi katerega ljudje včasih razmišljajo o dinamični konfiguraciji (brez drugih razlogov), je izpad storitve med posodobitvijo konfiguracije. Dejansko, če moramo spremeniti statično konfiguracijo, moramo znova zagnati sistem, da nove vrednosti začnejo veljati. Zahteve glede izpadov se razlikujejo za različne sisteme, zato morda niso tako kritične. Če je kritično, moramo vnaprej načrtovati morebitne ponovne zagone sistema. Na primer, lahko bi izvajali Izpraznitev povezave AWS ELB. V tem scenariju vsakič, ko moramo znova zagnati sistem, vzporedno zaženemo nov primerek sistema, nato preklopimo ELB nanj, medtem ko staremu sistemu pustimo, da dokonča servisiranje obstoječih povezav.

Kaj pa ohranjanje konfiguracije znotraj verzioniranega artefakta ali zunaj? Ohranjanje konfiguracije znotraj artefakta v večini primerov pomeni, da je ta konfiguracija prestala enak postopek zagotavljanja kakovosti kot drugi artefakti. Tako smo lahko prepričani, da je konfiguracija kakovostna in vredna zaupanja. Nasprotno, konfiguracija v ločeni datoteki pomeni, da ni sledi o tem, kdo in zakaj je naredil spremembe v tej datoteki. Je to pomembno? Verjamemo, da je za večino proizvodnih sistemov bolje imeti stabilno in kakovostno konfiguracijo.

Različica artefakta omogoča, da ugotovite, kdaj je bil ustvarjen, katere vrednosti vsebuje, katere funkcije so omogočene/onemogočene, kdo je bil odgovoren za vsako spremembo v konfiguraciji. Morda bo potrebno nekaj truda, da ohranite konfiguracijo znotraj artefakta, in to je odločitev glede oblikovanja.

Prednosti, slabosti

Tukaj bi radi izpostavili nekatere prednosti in razpravljali o nekaterih pomanjkljivostih predlaganega pristopa.

prednosti

Značilnosti konfiguracije celotnega porazdeljenega sistema, ki jo je mogoče prevesti:

  1. Statično preverjanje konfiguracije. To daje visoko stopnjo zaupanja, da je konfiguracija pravilna glede na omejitve vrste.
  2. Bogat jezik konfiguracije. Običajno so drugi konfiguracijski pristopi omejeni na največ spremenljivo zamenjavo.
    Z uporabo Scale lahko uporabite širok nabor jezikovnih funkcij za izboljšanje konfiguracije. Lastnosti lahko na primer uporabimo za zagotavljanje privzetih vrednosti, predmetov za nastavitev drugačnega obsega, na katere se lahko sklicujemo vals definiran samo enkrat v zunanjem obsegu (DRY). Možno je uporabiti dobesedna zaporedja ali primerke določenih razredov (Seq, Map, Itd.)
  3. DSL. Scala ima spodobno podporo za pisce DSL. Te funkcije lahko uporabite za vzpostavitev konfiguracijskega jezika, ki je bolj priročen in prijazen do končnega uporabnika, tako da je končna konfiguracija berljiva vsaj za uporabnike domene.
  4. Celovitost in skladnost med vozlišči. Ena od prednosti konfiguracije za celoten porazdeljeni sistem na enem mestu je, da so vse vrednosti definirane strogo enkrat in nato ponovno uporabljene na vseh mestih, kjer jih potrebujemo. Vnesite tudi deklaracije varnih vrat, ki zagotavljajo, da bodo v vseh možnih pravilnih konfiguracijah vozlišča sistema govorila isti jezik. Med vozlišči obstajajo eksplicitne odvisnosti, zaradi česar je težko pozabiti zagotoviti nekatere storitve.
  5. Visoka kakovost sprememb. Splošni pristop prenosa konfiguracijskih sprememb skozi običajni PR proces vzpostavlja visoke standarde kakovosti tudi v konfiguraciji.
  6. Hkratne spremembe konfiguracije. Kadar koli spremenimo konfiguracijo, samodejna uvedba zagotovi, da so vsa vozlišča posodobljena.
  7. Poenostavitev uporabe. Aplikaciji ni treba razčlenjevati in preverjati konfiguracije ter obravnavati nepravilnih vrednosti konfiguracije. To poenostavi celotno uporabo. (Nekaj ​​povečanja zapletenosti je v sami konfiguraciji, vendar je to zavesten kompromis v smeri varnosti.) Precej preprosto se je vrniti na običajno konfiguracijo - samo dodajte manjkajoče dele. Lažje je začeti s prevedeno konfiguracijo in preložiti implementacijo dodatnih delov na poznejši čas.
  8. Konfiguracija z različicami. Zaradi dejstva, da spremembe konfiguracije sledijo istemu razvojnemu procesu, kot rezultat dobimo artefakt z edinstveno različico. Omogoča nam, da po potrebi preklopimo konfiguracijo nazaj. Lahko celo uvedemo konfiguracijo, ki je bila uporabljena pred enim letom in bo delovala popolnoma enako. Stabilna konfiguracija izboljša predvidljivost in zanesljivost porazdeljenega sistema. Konfiguracija je določena v času prevajanja in je ni mogoče zlahka spremeniti v produkcijskem sistemu.
  9. Modularnost. Predlagano ogrodje je modularno in module je mogoče kombinirati na različne načine
    podpira različne konfiguracije (nastavitve/postavitve). Zlasti je mogoče imeti postavitev z enim vozliščem v majhnem obsegu in nastavitev z več vozlišči v velikem obsegu. Smiselno je imeti več proizvodnih postavitev.
  10. Testiranje. Za namene testiranja bi lahko implementirali lažno storitev in jo uporabili kot odvisnost na tipsko varen način. Hkrati je mogoče vzdrževati nekaj različnih postavitev testiranja z različnimi deli, nadomeščenimi z lažnimi.
  11. Testiranje integracije. Včasih je v porazdeljenih sistemih težko izvesti integracijske teste. Z opisanim pristopom za tipsko varno konfiguracijo celotnega porazdeljenega sistema lahko izvajamo vse porazdeljene dele na enem strežniku na nadzorovan način. Enostavno je posnemati situacijo
    ko ena od storitev postane nedosegljiva.

Slabosti

Pristop prevedene konfiguracije se razlikuje od "normalne" konfiguracije in morda ne bo ustrezal vsem potrebam. Tukaj je nekaj slabosti prevedene konfiguracije:

  1. Statična konfiguracija. Morda ni primeren za vse aplikacije. V nekaterih primerih je treba hitro popraviti konfiguracijo v proizvodnji, mimo vseh varnostnih ukrepov. Ta pristop otežuje. Prevajanje in ponovna razporeditev sta potrebni po kakršni koli spremembi konfiguracije. To je hkrati značilnost in breme.
  2. Generiranje konfiguracije. Ko konfiguracijo generira neko orodje za avtomatizacijo, ta pristop zahteva naknadno prevajanje (ki lahko posledično ne uspe). Za integracijo tega dodatnega koraka v gradbeni sistem boste morda potrebovali dodatne napore.
  3. instrumenti. Danes je v uporabi veliko orodij, ki temeljijo na besedilnih konfiguracijah. Nekateri od njih
    ne bo uporabna, ko bo konfiguracija prevedena.
  4. Potreben je premik v miselnosti. Razvijalci in DevOps poznajo besedilne konfiguracijske datoteke. Zamisel o sestavljanju konfiguracije se jim lahko zdi čudna.
  5. Pred uvedbo konfiguracije, ki jo je mogoče prevesti, je potreben visokokakovosten proces razvoja programske opreme.

Obstaja nekaj omejitev implementiranega primera:

  1. Če zagotovimo dodatno konfiguracijo, ki je ne zahteva implementacija vozlišča, nam prevajalnik ne bo pomagal zaznati odsotne implementacije. To bi lahko rešili z uporabo HList ali ADT (razredi primerov) za konfiguracijo vozlišča namesto lastnosti in vzorca torte.
  2. V konfiguracijski datoteki moramo dati nekaj predloge: (package, import, object izjave;
    override defza parametre, ki imajo privzete vrednosti). To je mogoče delno odpraviti z uporabo DSL.
  3. V tej objavi ne obravnavamo dinamične rekonfiguracije gruč podobnih vozlišč.

zaključek

V tej objavi smo razpravljali o ideji predstavitve konfiguracije neposredno v izvorni kodi na tipsko varen način. Pristop bi lahko uporabili v številnih aplikacijah kot zamenjavo za xml in druge besedilne konfiguracije. Kljub temu, da je bil naš primer implementiran v Scalo, ga je mogoče prevesti tudi v druge jezike, ki jih je mogoče prevesti (kot so Kotlin, C#, Swift itd.). Ta pristop bi lahko preizkusili v novem projektu in v primeru, da se ne ujema najbolje, prešli na staromoden način.

Seveda konfiguracija, ki jo je mogoče prevesti, zahteva visokokakovosten razvojni proces. V zameno obljublja enako kakovostno robustno konfiguracijo.

Ta pristop bi lahko razširili na različne načine:

  1. Lahko bi uporabili makre za izvedbo preverjanja veljavnosti konfiguracije in neuspeh v času prevajanja v primeru kakršnih koli napak v omejitvah poslovne logike.
  2. DSL bi lahko implementirali za predstavitev konfiguracije na domeni uporabniku prijazen način.
  3. Dinamično upravljanje virov s samodejnimi prilagoditvami konfiguracije. Na primer, ko prilagodimo število vozlišč gruče, bomo morda želeli (1) da vozlišča dobijo nekoliko spremenjeno konfiguracijo; (2) upravitelj gruče za prejemanje informacij o novih vozliščih.

Hvala

Rad bi se zahvalil Andreju Saksonovu, Pavlu Popovu in Antonu Nehaevu za navdihujoče povratne informacije o osnutku tega prispevka, ki so mi pomagali, da sem bil jasnejši.

Vir: www.habr.com