Prevedena konfiguracija porazdeljenega sistema

Rad bi vam povedal en zanimiv mehanizem za delo s konfiguracijo porazdeljenega sistema. Konfiguracija je predstavljena neposredno v prevedenem jeziku (Scala) z uporabo varnih tipov. Ta objava ponuja primer takšne konfiguracije in razpravlja o različnih vidikih implementacije prevedene konfiguracije v celoten razvojni proces.

Prevedena konfiguracija porazdeljenega sistema

(angleščina)

Predstavitev

Izgradnja zanesljivega porazdeljenega sistema pomeni, da vsa vozlišča uporabljajo pravilno konfiguracijo, sinhronizirano z drugimi vozlišči. Tehnologije DevOps (terraform, ansible ali kaj podobnega) se običajno uporabljajo za samodejno ustvarjanje konfiguracijskih datotek (pogosto specifičnih za vsako vozlišče). Prav tako želimo biti prepričani, da vsa vozlišča, ki komunicirajo, uporabljajo enake protokole (vključno z isto različico). V nasprotnem primeru bo v naš porazdeljeni sistem vgrajena nekompatibilnost. V svetu JVM je ena od posledic te zahteve ta, da je treba povsod uporabljati isto različico knjižnice, ki vsebuje sporočila protokola.

Kaj pa testiranje porazdeljenega sistema? Seveda predpostavljamo, da imajo vse komponente enotne teste, preden preidemo na integracijsko testiranje. (Da lahko rezultate testov ekstrapoliramo v čas izvajanja, moramo zagotoviti enak nabor knjižnic na stopnji testiranja in v času izvajanja.)

Pri delu z integracijskimi testi je pogosto lažje uporabiti isto razredno pot povsod na vseh vozliščih. Vse, kar moramo storiti, je zagotoviti, da se med izvajanjem uporablja ista razredna pot. (Čeprav je povsem mogoče zagnati različna vozlišča z različnimi razrednimi potmi, to še dodatno zaplete celotno konfiguracijo in težave pri uvajanju in integracijskih preizkusih.) Za namene te objave predpostavljamo, da bodo vsa vozlišča uporabljala isto razredno pot.

Konfiguracija se razvija z aplikacijo. Različice uporabljamo za prepoznavanje različnih stopenj razvoja programa. Zdi se logično, da se identificirajo tudi različne različice konfiguracij. In postavite samo konfiguracijo v sistem za nadzor različic. Če je v proizvodnji samo ena konfiguracija, lahko preprosto uporabimo številko različice. Če uporabljamo veliko proizvodnih primerkov, jih bomo potrebovali več
konfiguracijske veje in dodatno oznako poleg različice (na primer ime veje). Tako lahko jasno prepoznamo natančno konfiguracijo. Vsak konfiguracijski identifikator edinstveno ustreza določeni kombinaciji porazdeljenih vozlišč, vrat, zunanjih virov in različic knjižnice. Za namene te objave bomo predvidevali, da obstaja samo ena veja in da lahko konfiguracijo identificiramo na običajen način z uporabo treh številk, ločenih s piko (1.2.3).

V sodobnih okoljih se konfiguracijske datoteke le redko ustvarijo ročno. Pogosteje se ustvarijo med uvajanjem in se jih ne dotikajo več (tako da ne zlomi ničesar). Postavlja se naravno vprašanje: zakaj še vedno uporabljamo besedilno obliko za shranjevanje konfiguracije? Izvedljiva alternativa se zdi zmožnost uporabe redne kode za konfiguracijo in izkoriščanje prednosti preverjanj med prevajanjem.

V tej objavi bomo raziskali idejo o predstavitvi konfiguracije znotraj prevedenega artefakta.

Prevedena konfiguracija

V tem razdelku je primer statične prevedene konfiguracije. Implementirani sta dve preprosti storitvi - storitev echo in odjemalec storitve echo. Na podlagi teh dveh storitev sta sestavljeni dve sistemski možnosti. V eni možnosti se obe storitvi nahajata na istem vozlišču, v drugi možnosti - na različnih vozliščih.

Običajno porazdeljeni sistem vsebuje več vozlišč. Vozlišča lahko identificirate z uporabo vrednosti neke vrste NodeId:

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

ali

case class NodeId(hostName: String)

ali celo

object Singleton
type NodeId = Singleton.type

Vozlišča opravljajo različne vloge, izvajajo storitve in med njimi je mogoče vzpostaviti povezave TCP/HTTP.

Za opis povezave TCP potrebujemo vsaj številko vrat. Prav tako želimo odražati protokol, ki je podprt na teh vratih, da zagotovimo, da odjemalec in strežnik uporabljata isti protokol. Povezavo bomo opisali z naslednjim razredom:

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

če Port - samo celo število Int ki označuje razpon sprejemljivih vrednosti:

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

Rafinirane vrste

Glej knjižnico rafinirano и moja poročilo. Skratka, knjižnica vam omogoča dodajanje omejitev tipom, ki se preverjajo med prevajanjem. V tem primeru so veljavne vrednosti številke vrat 16-bitna cela števila. Za prevedeno konfiguracijo uporaba izboljšane knjižnice ni obvezna, vendar izboljša zmožnost prevajalnika, da preveri konfiguracijo.

Za protokole HTTP (REST) ​​bomo poleg številke vrat morda potrebovali tudi pot do storitve:

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

Fantomske vrste

Za identifikacijo protokola v času prevajanja uporabimo parameter tipa, ki se ne uporablja v razredu. Ta odločitev je posledica dejstva, da v času izvajanja ne uporabljamo primerka protokola, vendar želimo, da prevajalnik preveri združljivost protokola. Z določitvijo protokola ne bomo mogli posredovati neustrezne storitve kot odvisnosti.

Eden od pogostih protokolov je REST API s serializacijo Json:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

če RequestMessage - vrsto zahteve, ResponseMessage — vrsto odziva.
Seveda lahko uporabimo druge opise protokolov, ki zagotavljajo natančnost opisa, ki jo zahtevamo.

Za namene te objave bomo uporabili poenostavljeno različico protokola:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Tukaj je zahteva niz, pripet url-ju, odgovor pa vrnjeni niz v telesu odgovora HTTP.

Konfiguracija storitve je opisana z imenom storitve, vrati in odvisnostmi. Te elemente je mogoče v Scali predstaviti na več načinov (npr. HList-s, algebraični podatkovni tipi). Za namene te objave bomo uporabili vzorec torte in predstavili module z uporabo trait'ov. (Vzorec torte ni obvezen element tega pristopa. Je le ena možna izvedba.)

Odvisnosti med storitvami je mogoče predstaviti kot metode, ki vračajo vrata EndPointdrugih 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)
  }

Če želite ustvariti storitev odmeva, potrebujete le številko vrat in navedbo, da vrata podpirajo protokol odmeva. Morda ne bomo določili določenih vrat, ker ... lastnosti vam omogočajo, da deklarirate metode brez implementacije (abstraktne metode). V tem primeru bi prevajalnik pri ustvarjanju konkretne konfiguracije zahteval, da zagotovimo izvedbo abstraktne metode in podamo številko vrat. Ker smo implementirali metodo, pri ustvarjanju določene konfiguracije morda ne bomo določili drugih vrat. Uporabljena bo privzeta vrednost.

V konfiguraciji odjemalca razglasimo odvisnost od storitve echo:

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

Odvisnost je iste vrste kot izvožena storitev echoService. Zlasti v odjemalcu echo potrebujemo isti protokol. Zato smo lahko pri povezovanju dveh storitev prepričani, da bo vse delovalo pravilno.

Izvajanje storitev

Za zagon in zaustavitev storitve je potrebna funkcija. (Zmožnost zaustavitve storitve je ključnega pomena za testiranje.) Ponovno obstaja več možnosti za implementacijo takšne funkcije (lahko bi na primer uporabili razrede tipov, ki temeljijo na tipu konfiguracije). Za namene te objave bomo uporabili vzorec torte. Storitev bomo predstavili z uporabo razreda cats.Resource, Ker Ta razred že zagotavlja sredstva za varno zagotavljanje sprostitve virov v primeru težav. Če želimo pridobiti vir, moramo zagotoviti konfiguracijo in že pripravljen kontekst izvajalnega okolja. Funkcija zagona storitve je lahko 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]
  }

če

  • Config — vrsta konfiguracije za to storitev
  • AddressResolver — izvajalni objekt, ki vam omogoča, da ugotovite naslove drugih vozlišč (glejte spodaj)

in druge vrste iz knjižnice cats:

  • F[_] — vrsta učinka (v najpreprostejšem primeru F[A] lahko samo funkcija () => A. V tej objavi bomo uporabili cats.IO.)
  • Reader[A,B] - bolj ali manj sinonim za funkcijo A => B
  • cats.Resource - vir, ki ga je mogoče pridobiti in sprostiti
  • Timer — časovnik (omogoča, da za nekaj časa zaspite in merite časovne intervale)
  • ContextShift - analogno ExecutionContext
  • Applicative — razred vrste učinka, ki omogoča kombiniranje posameznih učinkov (skoraj monada). V kompleksnejših aplikacijah se zdi bolje uporabiti Monad/ConcurrentEffect.

Z uporabo tega podpisa funkcije lahko implementiramo več 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](()))
  }

(Cm. vir, v katerem so implementirane druge storitve - echo storitev, odjemalec echo
и doživljenjski krmilniki.)

Vozlišče je objekt, ki lahko zažene več storitev (zagon verige virov zagotavlja 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 določamo točno vrsto konfiguracije, ki je potrebna za to vozlišče. Če pozabimo podati enega od tipov konfiguracije, ki jih zahteva določena storitev, bo prišlo do napake pri prevajanju. Prav tako ne bomo mogli zagnati vozlišča, če ne zagotovimo nekega objekta ustreznega tipa z vsemi potrebnimi podatki.

Razrešitev imena gostitelja

Za povezavo z oddaljenim gostiteljem potrebujemo pravi naslov IP. Možno je, da bo naslov znan pozneje kot ostala konfiguracija. Torej potrebujemo funkcijo, ki preslika ID vozlišča v naslov:

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

Obstaja več načinov za izvajanje te funkcije:

  1. Če nam naslovi postanejo znani pred uvedbo, lahko ustvarimo kodo Scala z
    naslove in nato zaženite gradnjo. To bo prevedlo in zagnalo teste.
    V tem primeru bo funkcija znana statično in jo je mogoče predstaviti v kodi kot preslikavo Map[NodeId, NodeAddress].
  2. V nekaterih primerih je dejanski naslov znan šele po zagonu vozlišča.
    V tem primeru lahko implementiramo "storitev odkrivanja", ki se izvaja pred drugimi vozlišči in vsa vozlišča se bodo registrirala pri tej storitvi in ​​zahtevala naslove drugih vozlišč.
  3. Če lahko spremenimo /etc/hosts, potem lahko uporabite vnaprej določena imena gostiteljev (npr my-project-main-node и echo-backend) in preprosto povežite ta imena
    z naslovi IP med uvajanjem.

V tej objavi teh primerov ne bomo podrobneje obravnavali. Za naš
v primeru igrače bodo imela vsa vozlišča enak naslov IP - 127.0.0.1.

Nato razmislimo o dveh možnostih za porazdeljeni sistem:

  1. Postavitev vseh storitev na eno vozlišče.
  2. In gostovanje storitve echo in odjemalca echo na različnih vozliščih.

Konfiguracija za eno vozlišče:

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

Objekt implementira konfiguracijo odjemalca in strežnika. Uporabljena je tudi konfiguracija časa do življenja, tako da po intervalu lifetime prekinite program. (Ctrl-C tudi deluje in pravilno sprosti vse vire.)

Isti nabor konfiguracijskih in implementacijskih lastnosti je mogoče uporabiti za ustvarjanje sistema, sestavljenega iz dva ločena vozlišča:

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

Pomembno! Upoštevajte, kako so storitve povezane. Določimo storitev, ki jo izvaja eno vozlišče, kot implementacijo metode odvisnosti drugega vozlišča. Tip odvisnosti preveri prevajalnik, ker vsebuje vrsto protokola. Ko se zažene, bo odvisnost vsebovala pravilen ID ciljnega vozlišča. Zahvaljujoč tej shemi natančno enkrat določimo številko vrat in vedno je zagotovljeno, da se nanašajo na pravilna vrata.

Izvedba dveh sistemskih vozlišč

Za to konfiguracijo uporabljamo iste izvedbe storitev brez sprememb. Edina razlika je v tem, da imamo zdaj dva objekta, ki izvajata različne nize 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 implementira strežnik in potrebuje samo konfiguracijo strežnika. Drugo vozlišče izvaja odjemalca in uporablja drug del konfiguracije. Prav tako obe vozlišči potrebujeta upravljanje celotne življenjske dobe. Strežniško vozlišče deluje neomejeno dolgo, dokler ni ustavljeno SIGTERM'om in odjemalsko vozlišče se čez nekaj časa prekine. Cm. zaganjalnik.

Splošni razvojni proces

Poglejmo, kako ta konfiguracijski pristop vpliva na celoten razvojni proces.

Konfiguracija bo prevedena skupaj s preostalo kodo in ustvarjen bo artefakt (.jar). Zdi se, da je smiselno postaviti konfiguracijo v ločen artefakt. To je zato, ker imamo lahko več konfiguracij, ki temeljijo na isti kodi. Spet je mogoče ustvariti artefakte, ki ustrezajo različnim konfiguracijskim vejam. Odvisnosti od določenih različic knjižnic se shranijo skupaj s konfiguracijo in te različice se shranijo za vedno, kadar koli se odločimo za uvedbo te različice konfiguracije.

Vsaka sprememba konfiguracije se spremeni v spremembo kode. In zato vsak
sprememba bo zajeta v običajnem postopku zagotavljanja kakovosti:

Vstopnica v sledilniku hroščev -> PR -> pregled -> združi z ustreznimi vejami ->
integracija -> uvajanje

Glavne posledice implementacije prevedene konfiguracije so:

  1. Konfiguracija bo skladna v vseh vozliščih porazdeljenega sistema. Zaradi dejstva, da vsa vozlišča prejmejo enako konfiguracijo iz enega vira.

  2. Težava je spremeniti konfiguracijo samo v enem od vozlišč. Zato je "konfiguracijski premik" malo verjeten.

  3. Manjše spremembe v konfiguraciji postanejo težje.

  4. Večina sprememb konfiguracije se bo zgodila kot del celotnega razvojnega procesa in bo predmet pregleda.

Ali potrebujem ločeno skladišče za shranjevanje produkcijske konfiguracije? Ta konfiguracija lahko vsebuje gesla in druge občutljive informacije, do katerih bi radi omejili dostop. Na podlagi tega se zdi smiselno končno konfiguracijo shraniti v ločeno skladišče. Konfiguracijo lahko razdelite na dva dela – enega, ki vsebuje javno dostopne konfiguracijske nastavitve, in enega, ki vsebuje omejene nastavitve. To bo večini razvijalcev omogočilo dostop do skupnih nastavitev. To ločitev je enostavno doseči z uporabo vmesnih lastnosti, ki vsebujejo privzete vrednosti.

Možne variacije

Poskusimo primerjati prevedeno konfiguracijo z nekaj pogostimi alternativami:

  1. Besedilna datoteka na ciljnem računalniku.
  2. Centralizirana shramba ključev in vrednosti (etcd/zookeeper).
  3. Komponente procesa, ki jih je mogoče znova konfigurirati/ponovno zagnati brez ponovnega zagona procesa.
  4. Shranjevanje konfiguracije zunaj nadzora artefaktov in različic.

Besedilne datoteke zagotavljajo veliko prilagodljivost v smislu majhnih sprememb. Sistemski skrbnik se lahko prijavi v oddaljeno vozlišče, spremeni ustrezne datoteke in znova zažene storitev. Za velike sisteme pa takšna prilagodljivost morda ni zaželena. Izvedene spremembe ne puščajo sledi v drugih sistemih. Nihče ne pregleda sprememb. Težko je ugotoviti, kdo točno je naredil spremembe in zakaj. Spremembe se ne testirajo. Če je sistem porazdeljen, lahko skrbnik pozabi narediti ustrezno spremembo na drugih vozliščih.

(Upoštevati je treba tudi, da uporaba prevedene konfiguracije ne zapre možnosti uporabe besedilnih datotek v prihodnosti. Dovolj bo, če dodate razčlenjevalnik in validator, ki ustvarita isto vrsto kot izhod Configin lahko uporabite besedilne datoteke. Iz tega takoj sledi, da je kompleksnost sistema s prevedeno konfiguracijo nekoliko manjša od kompleksnosti sistema, ki uporablja besedilne datoteke, ker besedilne datoteke zahtevajo dodatno kodo.)

Centralizirana shramba ključev in vrednosti je dober mehanizem za distribucijo meta parametrov porazdeljene aplikacije. Odločiti se moramo, kaj so konfiguracijski parametri in kaj le podatki. Naj imamo funkcijo C => A => B, in parametri C redko spreminja, in podatkov A - pogosto. V tem primeru lahko rečemo, da C - konfiguracijski parametri in A - podatki. Zdi se, da se konfiguracijski parametri od podatkov razlikujejo po tem, da se na splošno spreminjajo manj pogosto kot podatki. Tudi podatki običajno prihajajo iz enega vira (od uporabnika), konfiguracijski parametri pa iz drugega (od sistemskega administratorja).

Če je treba redko spreminjajoče se parametre posodobiti brez ponovnega zagona programa, potem lahko to pogosto privede do zapleta programa, saj bomo morali parametre nekako dostaviti, shraniti, razčleniti in preveriti ter obdelati nepravilne vrednosti. Zato je z vidika zmanjšanja kompleksnosti programa smiselno zmanjšati število parametrov, ki se lahko spreminjajo med delovanjem programa (ali pa takih parametrov sploh ne podpirati).

Za namene te objave bomo razlikovali med statičnimi in dinamičnimi parametri. Če logika storitve zahteva spreminjanje parametrov med delovanjem programa, potem bomo takšne parametre imenovali dinamični. V nasprotnem primeru so možnosti statične in jih je mogoče konfigurirati s prevedeno konfiguracijo. Za dinamično rekonfiguracijo bomo morda potrebovali mehanizem za ponovni zagon delov programa z novimi parametri, podobno kot se znova zaženejo procesi operacijskega sistema. (Po našem mnenju se je priporočljivo izogibati ponovni konfiguraciji v realnem času, saj to poveča kompleksnost sistema. Če je mogoče, je bolje uporabiti standardne zmogljivosti OS za ponovni zagon procesov.)

Eden od pomembnih vidikov uporabe statične konfiguracije, zaradi katerega ljudje razmišljajo o dinamični rekonfiguraciji, je čas, ki je potreben, da se sistem znova zažene po posodobitvi konfiguracije (nedelovanje). Pravzaprav, če moramo spremeniti statično konfiguracijo, bomo morali znova zagnati sistem, da bodo nove vrednosti začele veljati. Problem nedelovanja se razlikuje glede na resnost za različne sisteme. V nekaterih primerih lahko načrtujete ponovni zagon v času, ko je obremenitev minimalna. Če morate zagotoviti neprekinjeno storitev, jo lahko implementirate Izpraznitev povezave AWS ELB. Hkrati, ko moramo znova zagnati sistem, zaženemo vzporedno instanco tega sistema, preklopimo balanser nanjo in počakamo, da se stare povezave zaključijo. Po prekinitvi vseh starih povezav zaustavimo staro instanco sistema.

Oglejmo si zdaj vprašanje shranjevanja konfiguracije znotraj ali zunaj artefakta. Če shranimo konfiguracijo znotraj artefakta, potem smo vsaj imeli možnost preveriti pravilnost konfiguracije med sestavljanjem artefakta. Če je konfiguracija zunaj nadzorovanega artefakta, je težko slediti, kdo je spremenil to datoteko in zakaj. Kako pomemben je? Po našem mnenju je za številne proizvodne sisteme pomembna stabilna in kakovostna konfiguracija.

Različica artefakta vam omogoča, da ugotovite, kdaj je bil ustvarjen, katere vrednosti vsebuje, katere funkcije so omogočene/onemogočene in kdo je odgovoren za kakršne koli spremembe v konfiguraciji. Seveda shranjevanje konfiguracije znotraj artefakta zahteva nekaj truda, zato se morate odločiti na podlagi informacij.

Prednosti in slabosti

Rad bi se osredotočil na prednosti in slabosti predlagane tehnologije.

Prednosti

Spodaj je seznam glavnih funkcij konfiguracije prevedenega porazdeljenega sistema:

  1. Preverjanje statične konfiguracije. Omogoča, da se prepričate, da
    konfiguracija je pravilna.
  2. Bogat konfiguracijski jezik. Običajno so druge konfiguracijske metode omejene največ na zamenjavo spremenljivke niza. Pri uporabi Scala je na voljo širok nabor jezikovnih funkcij za izboljšanje vaše konfiguracije. Na primer lahko uporabimo
    lastnosti za privzete vrednosti, z uporabo objektov za združevanje parametrov, se lahko sklicujemo na vrednosti, ki so deklarirane samo enkrat (DRY) v obsegu, ki ga zajema. Poljubne razrede lahko ustvarite neposredno znotraj konfiguracije (Seq, Map, razredi po meri).
  3. DSL. Scala ima številne jezikovne funkcije, ki olajšajo ustvarjanje DSL. Možno je izkoristiti te funkcije in implementirati konfiguracijski jezik, ki je bolj primeren za ciljno skupino uporabnikov, tako da je konfiguracija berljiva vsaj strokovnjakom za področje. Strokovnjaki lahko na primer sodelujejo pri pregledu konfiguracije.
  4. Celovitost in sinhronost med vozlišči. Ena od prednosti konfiguracije celotnega porazdeljenega sistema, shranjene na eni točki, je ta, da so vse vrednosti deklarirane točno enkrat in nato ponovno uporabljene, kjer koli so potrebne. Uporaba tipov fantomov za deklariranje vrat zagotavlja, da vozlišča uporabljajo združljive protokole v vseh pravilnih konfiguracijah sistema. Izrecne obvezne odvisnosti med vozlišči zagotavljajo, da so vse storitve povezane.
  5. Visoko kakovostne spremembe. Spreminjanje konfiguracije z uporabo skupnega razvojnega procesa omogoča doseganje visokih standardov kakovosti tudi za konfiguracijo.
  6. Hkratna posodobitev konfiguracije. Samodejna uvedba sistema po spremembi konfiguracije zagotavlja, da so vsa vozlišča posodobljena.
  7. Poenostavitev aplikacije. Aplikacija ne potrebuje razčlenjevanja, preverjanja konfiguracije ali obravnavanja nepravilnih vrednosti. To zmanjša kompleksnost aplikacije. (Nekatere zapletenosti konfiguracije, opažene v našem primeru, niso atribut prevedene konfiguracije, temveč le zavestna odločitev, ki jo vodi želja po zagotavljanju večje varnosti tipov.) Povsem enostavno se je vrniti k običajni konfiguraciji - preprosto implementirajte manjkajoče deli. Zato lahko na primer začnete s prevedeno konfiguracijo in odložite implementacijo nepotrebnih delov do trenutka, ko je res potrebna.
  8. Preverjena konfiguracija. Ker spremembe konfiguracije sledijo običajni usodi vseh drugih sprememb, je rezultat, ki ga dobimo, artefakt z edinstveno različico. To nam omogoča, da se na primer po potrebi vrnemo na prejšnjo različico konfiguracije. Uporabimo lahko celo konfiguracijo izpred enega leta in sistem bo deloval popolnoma enako. Stabilna konfiguracija izboljša predvidljivost in zanesljivost porazdeljenega sistema. Ker je konfiguracija določena v fazi prevajanja, jo je v proizvodnji precej težko ponarediti.
  9. Modularnost. Predlagano ogrodje je modularno in module je mogoče kombinirati na različne načine za ustvarjanje različnih sistemov. Zlasti lahko konfigurirate sistem za delovanje na enem vozlišču v eni izvedbi in na več vozliščih v drugi. Ustvarite lahko več konfiguracij za proizvodne primerke sistema.
  10. Testiranje. Z zamenjavo posameznih storitev z lažnimi objekti lahko dobite več različic sistema, ki so primerne za testiranje.
  11. Integracijsko testiranje. Ena konfiguracija za celoten porazdeljeni sistem omogoča izvajanje vseh komponent v nadzorovanem okolju kot del testiranja integracije. Enostavno je na primer posnemati situacijo, ko nekatera vozlišča postanejo dostopna.

Slabosti in omejitve

Prevedena konfiguracija se razlikuje od drugih konfiguracijskih pristopov in morda ni primerna za nekatere aplikacije. Spodaj je navedenih nekaj slabosti:

  1. Statična konfiguracija. Včasih morate hitro popraviti konfiguracijo v proizvodnji, mimo vseh zaščitnih mehanizmov. S tem pristopom je lahko težje. Še vedno bosta potrebna vsaj prevajanje in samodejna namestitev. To je koristna lastnost pristopa in v nekaterih primerih slabost.
  2. Generiranje konfiguracije. Če je konfiguracijska datoteka ustvarjena s samodejnim orodjem, bodo morda potrebna dodatna prizadevanja za integracijo gradbenega skripta.
  3. Orodja. Trenutno pripomočki in tehnike, zasnovani za delo s konfiguracijo, temeljijo na besedilnih datotekah. Vsi taki pripomočki/tehnike ne bodo na voljo v prevedeni konfiguraciji.
  4. Potrebna je sprememba odnosa. Razvijalci in DevOps so navajeni besedilnih datotek. Sama ideja o sestavljanju konfiguracije je lahko nekoliko nepričakovana in nenavadna ter povzroči zavrnitev.
  5. Potreben je visokokakovosten razvojni proces. Za udobno uporabo prevedene konfiguracije je potrebna popolna avtomatizacija procesa gradnje in uvajanja aplikacije (CI/CD). V nasprotnem primeru bo precej neprijetno.

Oglejmo si tudi številne omejitve obravnavanega primera, ki niso povezane z idejo prevedene konfiguracije:

  1. Če zagotovimo nepotrebne informacije o konfiguraciji, ki jih vozlišče ne uporablja, nam prevajalnik ne bo pomagal odkriti manjkajoče izvedbe. To težavo je mogoče rešiti tako, da opustimo vzorec torte in uporabimo bolj toge vrste, na primer HList ali algebraične podatkovne tipe (razredi primerov), ki predstavljajo konfiguracijo.
  2. V konfiguracijski datoteki so vrstice, ki niso povezane s samo konfiguracijo: (package, import,izjave predmetov; override defza parametre, ki imajo privzete vrednosti). Temu se lahko delno izognete, če implementirate svoj DSL. Poleg tega tudi druge vrste konfiguracije (na primer XML) nalagajo določene omejitve glede strukture datoteke.
  3. Za namene te objave ne razmišljamo o dinamični rekonfiguraciji gruče podobnih vozlišč.

Zaključek

V tej objavi smo raziskali idejo o predstavitvi konfiguracije v izvorni kodi z uporabo naprednih zmogljivosti sistema tipa Scala. Ta pristop se lahko uporablja v različnih aplikacijah kot zamenjava za tradicionalne metode konfiguracije, ki temeljijo na xml ali besedilnih datotekah. Čeprav je naš primer implementiran v Scali, lahko iste ideje prenesemo v druge prevedene jezike (kot so Kotlin, C#, Swift, ...). Ta pristop lahko preizkusite v enem od naslednjih projektov in, če ne deluje, nadaljujte z besedilno datoteko in dodajte manjkajoče dele.

Seveda prevedena konfiguracija zahteva visokokakovosten razvojni proces. V zameno je zagotovljena visoka kakovost in zanesljivost konfiguracij.

Obravnavani pristop je mogoče razširiti:

  1. Za izvajanje preverjanj med prevajanjem lahko uporabite makre.
  2. DSL lahko implementirate za predstavitev konfiguracije na način, ki je dostopen končnim uporabnikom.
  3. Dinamično upravljanje virov lahko implementirate s samodejno prilagoditvijo konfiguracije. Na primer, spreminjanje števila vozlišč v gruči zahteva, da (1) vsako vozlišče prejme nekoliko drugačno konfiguracijo; (2) upravitelj gruče je prejel informacije o novih vozliščih.

Zahvala

Rad bi se zahvalil Andreju Saksonovu, Pavlu Popovu in Antonu Nekhaevu za njihovo konstruktivno kritiko osnutka člena.

Vir: www.habr.com

Dodaj komentar