Kompajbilna konfiguracija distribuiranog sustava

U ovom postu želimo podijeliti zanimljiv način rješavanja konfiguracije distribuiranog sustava.
Konfiguracija je predstavljena izravno u Scala jeziku na tipski siguran način. Primjer implementacije je detaljno opisan. Razmatraju se različiti aspekti prijedloga, uključujući utjecaj na cjelokupni proces razvoja.

Kompajbilna konfiguracija distribuiranog sustava

(na ruskom)

Uvod

Izgradnja robusnih distribuiranih sustava zahtijeva upotrebu ispravne i koherentne konfiguracije na svim čvorovima. Tipično rješenje je korištenje tekstualnog opisa implementacije (teraforma, ansible ili nešto slično) i automatski generiranih konfiguracijskih datoteka (često — posvećenih svakom čvoru/ulozi). Također bismo željeli koristiti iste protokole istih verzija na svim komunikacijskim čvorovima (inače bismo imali problema s nekompatibilnošću). U svijetu JVM-a to znači da bi barem biblioteka za razmjenu poruka trebala biti iste verzije na svim komunikacijskim čvorovima.

Što je s testiranjem sustava? Naravno, trebali bismo imati jedinične testove za sve komponente prije nego što dođemo do integracijskih testova. Da bismo mogli ekstrapolirati rezultate testa na vrijeme izvođenja, trebali bismo osigurati da su verzije svih biblioteka identične iu okruženju izvođenja i testiranju.

Kada se izvode integracijski testovi, često je puno lakše imati isti put klase na svim čvorovima. Samo trebamo osigurati da se ista staza klase koristi pri implementaciji. (Moguće je koristiti različite staze klasa na različitim čvorovima, ali je teže predstaviti ovu konfiguraciju i ispravno je implementirati.) Dakle, kako bismo stvari održali jednostavnima, razmotrit ćemo samo identične staze klasa na svim čvorovima.

Konfiguracija ima tendenciju da se razvija zajedno sa softverom. Obično koristimo verzije da identificiramo različite
faze evolucije softvera. Čini se razumnim pokriti konfiguraciju pod upravljanjem verzijama i identificirati različite konfiguracije s nekim oznakama. Ako postoji samo jedna konfiguracija u proizvodnji, možemo koristiti jednu verziju kao identifikator. Ponekad možemo imati više proizvodnih okruženja. A za svako okruženje možda ćemo trebati zasebnu granu konfiguracije. Dakle, konfiguracije mogu biti označene granom i verzijom za jedinstvenu identifikaciju različitih konfiguracija. Svaka oznaka grane i verzija odgovara jednoj kombinaciji distribuiranih čvorova, portova, vanjskih resursa, verzija biblioteke staze klasa na svakom čvoru. Ovdje ćemo pokriti samo jednu granu i identificirati konfiguracije trokomponentnom decimalnom verzijom (1.2.3), na isti način kao i drugi artefakti.

U modernim okruženjima konfiguracijske datoteke više se ne mijenjaju ručno. Obično stvaramo
konfiguracijske datoteke u vrijeme postavljanja i nikad ih ne diraj poslije. Moglo bi se postaviti pitanje zašto još uvijek koristimo tekstualni format za konfiguracijske datoteke? Održiva opcija je smjestiti konfiguraciju unutar jedinice kompilacije i iskoristiti provjeru valjanosti konfiguracije tijekom kompilacije.

U ovom ćemo postu ispitati ideju zadržavanja konfiguracije u kompiliranom artefaktu.

Kompajbilna konfiguracija

U ovom odjeljku raspravljat ćemo o primjeru statičke konfiguracije. Dvije jednostavne usluge - echo usluga i klijent echo usluge su u konfiguraciji i implementaciji. Zatim se instanciraju dva različita distribuirana sustava s obje usluge. Jedan je za konfiguraciju s jednim čvorom, a drugi za konfiguraciju s dva čvora.

Tipičan distribuirani sustav sastoji se od nekoliko čvorova. Čvorovi se mogu identificirati pomoću neke vrste:

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

ili samo

case class NodeId(hostName: String)

ili čak

object Singleton
type NodeId = Singleton.type

Ovi čvorovi obavljaju različite uloge, pokreću neke usluge i trebali bi moći komunicirati s drugim čvorovima putem TCP/HTTP veza.

Za TCP vezu potreban je barem broj porta. Također želimo biti sigurni da klijent i poslužitelj razgovaraju istim protokolom. Kako bismo modelirali vezu između čvorova, deklarirajmo sljedeću klasu:

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

gdje Port je samo Int unutar dopuštenog raspona:

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

Pročišćene vrste

Vidjeti rafiniran knjižnica. Ukratko, omogućuje dodavanje vremenskih ograničenja kompajliranja drugim vrstama. U ovom slučaju Int dopušteno je imati samo 16-bitne vrijednosti koje mogu predstavljati broj priključka. Ne postoji zahtjev za korištenje ove biblioteke za ovaj konfiguracijski pristup. Samo se čini da jako dobro pristaje.

Za HTTP (REST) ​​​​možda ćemo također trebati put usluge:

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

Tip fantoma

Kako bismo identificirali protokol tijekom kompilacije koristimo značajku Scala za deklariranje tipa argumenta Protocol koji se ne koristi u razredu. To je tzv fantomski tip. Tijekom izvođenja rijetko nam je potrebna instanca identifikatora protokola, zato ga ne pohranjujemo. Tijekom kompilacije ovaj fantomski tip daje dodatnu sigurnost tipa. Ne možemo proći port s netočnim protokolom.

Jedan od najčešće korištenih protokola je REST API s Json serijalizacijom:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

gdje RequestMessage je osnovni tip poruka koje klijent može poslati poslužitelju i ResponseMessage je odgovor poslužitelja. Naravno, možemo stvoriti druge opise protokola koji specificiraju komunikacijski protokol sa željenom preciznošću.

Za potrebe ovog posta koristit ćemo jednostavniju verziju protokola:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

U ovom se protokolu poruka zahtjeva dodaje url-u, a poruka odgovora vraća se kao običan niz.

Konfiguracija usluge može se opisati nazivom usluge, skupom portova i nekim ovisnostima. Postoji nekoliko mogućih načina kako predstaviti sve te elemente u Scali (na primjer, HList, algebarski tipovi podataka). Za potrebe ovog posta koristit ćemo obrazac kolača i predstaviti dijelove (module) koji se mogu kombinirati kao značajke. (Uzorak kolača nije preduvjet za ovaj konfiguracijski pristup koji se može kompilirati. To je samo jedna moguća implementacija ideje.)

Ovisnosti se mogu predstaviti pomoću uzorka torte kao krajnje točke drugih čvorova:

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

Usluga Echo treba samo konfigurirati priključak. I izjavljujemo da ovaj priključak podržava echo protokol. Imajte na umu da u ovom trenutku ne trebamo specificirati određeni port, jer značajke dopuštaju deklaracije apstraktnih metoda. Ako koristimo apstraktne metode, kompajler će zahtijevati implementaciju u instanci konfiguracije. Ovdje smo dali implementaciju (8081) i koristit će se kao zadana vrijednost ako je preskočimo u konkretnoj konfiguraciji.

Možemo deklarirati ovisnost u konfiguraciji klijenta usluge echo:

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

Ovisnost ima istu vrstu kao i echoService. Konkretno, zahtijeva isti protokol. Dakle, možemo biti sigurni da će, ako povežemo ove dvije ovisnosti, raditi ispravno.

Implementacija usluga

Usluga treba funkciju za pokretanje i elegantno isključivanje. (Mogućnost gašenja usluge je kritična za testiranje.) Opet postoji nekoliko opcija određivanja takve funkcije za danu konfiguraciju (na primjer, mogli bismo koristiti klase tipa). Za ovaj post ponovno ćemo koristiti obrazac za kolače. Možemo predstavljati uslugu korištenjem cats.Resource koji već osigurava postavljanje u zagrade i oslobađanje resursa. Kako bismo stekli resurs, trebali bismo osigurati konfiguraciju i neki kontekst vremena izvođenja. Dakle, funkcija pokretanja usluge može izgledati ovako:

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

gdje

  • Config — vrsta konfiguracije koju zahtijeva ovaj pokretač usluge
  • AddressResolver — runtime objekt koji ima mogućnost dobivanja stvarnih adresa drugih čvorova (nastavite čitati za detalje).

ostale vrste dolaze iz cats:

  • F[_] — vrsta učinka (U najjednostavnijem slučaju F[A] moglo biti samo () => A. U ovom postu ćemo koristiti cats.IO.)
  • Reader[A,B] — manje-više je sinonim za funkciju A => B
  • cats.Resource — ima načine za stjecanje i otpuštanje
  • Timer — omogućuje spavanje/mjerenje vremena
  • ContextShift - analog od ExecutionContext
  • Applicative — omotač funkcija na snazi ​​(skoro monada) (mogli bismo je eventualno zamijeniti nečim drugim)

Pomoću ovog sučelja možemo implementirati nekoliko usluga. Na primjer, usluga koja ne radi ništa:

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

(Vidi Izvorni kod za druge implementacije usluga — usluga odjeka,
echo klijent i doživotni regulatori.)

Čvor je jedan objekt koji pokreće nekoliko usluga (pokretanje lanca resursa omogućuje Cake Pattern):

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

Imajte na umu da u čvoru navodimo točnu vrstu konfiguracije koja je potrebna ovom čvoru. Kompajler nam ne dopušta da izgradimo objekt (Cake) s nedovoljnim tipom, jer svaka značajka usluge deklarira ograničenje na Config tip. Također nećemo moći pokrenuti čvor bez pružanja potpune konfiguracije.

Razlučivost adrese čvora

Kako bismo uspostavili vezu potrebna nam je prava host adresa za svaki čvor. Možda će biti poznato kasnije od ostalih dijelova konfiguracije. Stoga nam je potreban način za preslikavanje između ID-a čvora i njegove stvarne adrese. Ovo preslikavanje je funkcija:

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

Postoji nekoliko mogućih načina implementacije takve funkcije.

  1. Ako znamo stvarne adrese prije postavljanja, tijekom instanciranja hostova čvorova, tada možemo generirati Scala kod sa stvarnim adresama i pokrenuti izgradnju nakon toga (koja izvodi provjere vremena kompajliranja, a zatim pokreće integracijski testni paket). U ovom slučaju naša je funkcija preslikavanja statički poznata i može se pojednostaviti na nešto poput a Map[NodeId, NodeAddress].
  2. Ponekad dobivamo stvarne adrese tek kasnije kada je čvor zapravo pokrenut ili nemamo adrese čvorova koji još nisu pokrenuti. U ovom slučaju možda imamo uslugu otkrivanja koja se pokreće prije svih ostalih čvorova i svaki čvor može oglašavati svoju adresu u toj usluzi i pretplatiti se na ovisnosti.
  3. Ako možemo modificirati /etc/hosts, možemo koristiti unaprijed definirana imena hostova (kao my-project-main-node i echo-backend) i samo povežite ovo ime s IP adresom u vrijeme postavljanja.

U ovom postu ne pokrivamo te slučajeve detaljnije. Zapravo, u našem primjeru igračke svi će čvorovi imati istu IP adresu — 127.0.0.1.

U ovom ćemo postu razmotriti dva rasporeda distribuiranog sustava:

  1. Izgled s jednim čvorom, gdje su sve usluge smještene na jednom čvoru.
  2. Raspored dva čvora, gdje su usluga i klijent na različitim čvorovima.

Konfiguracija za a jedan čvor raspored je sljedeći:

Konfiguracija jednog čvora

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

Ovdje stvaramo jednu konfiguraciju koja proširuje konfiguraciju poslužitelja i klijenta. Također konfiguriramo kontroler životnog ciklusa koji će normalno prekinuti klijenta i poslužitelja nakon toga lifetime interval prolazi.

Isti skup implementacija i konfiguracija usluga može se koristiti za stvaranje izgleda sustava s dva odvojena čvora. Samo trebamo stvarati dvije odvojene konfiguracije čvora uz odgovarajuće usluge:

Konfiguracija dva čvora

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

Pogledajte kako specificiramo ovisnost. Spominjemo uslugu koju pruža drugi čvor kao ovisnost trenutnog čvora. Vrsta ovisnosti se provjerava jer sadrži tip fantoma koji opisuje protokol. I u vremenu izvođenja imat ćemo ispravan ID čvora. Ovo je jedan od važnih aspekata predloženog konfiguracijskog pristupa. Omogućuje nam da samo jednom postavimo port i provjerimo da li referiramo na točan port.

Implementacija dva čvora

Za ovu konfiguraciju koristimo potpuno iste implementacije usluga. Nema nikakvih promjena. Međutim, stvaramo dvije različite implementacije čvora koje sadrže različite skupove usluga:

  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
  }

Prvi čvor implementira poslužitelj i potrebna mu je samo konfiguracija na strani poslužitelja. Drugi čvor implementira klijenta i treba mu još jedan dio konfiguracije. Oba čvora zahtijevaju određene specifikacije životnog vijeka. Za potrebe ovog čvora poštanske usluge imat će beskonačni životni vijek koji se može prekinuti korištenjem SIGTERM, dok će echo klijent prekinuti nakon konfiguriranog konačnog trajanja. Vidite početna aplikacija za detalje.

Sveukupni proces razvoja

Pogledajmo kako ovaj pristup mijenja način na koji radimo s konfiguracijom.

Konfiguracija kao kod će se prevesti i proizvesti artefakt. Čini se razumnim odvojiti artefakt konfiguracije od ostalih artefakata koda. Često možemo imati mnoštvo konfiguracija na istoj bazi koda. I naravno, možemo imati više verzija različitih konfiguracijskih grana. U konfiguraciji možemo odabrati određene verzije biblioteka i to će ostati konstantno kad god implementiramo ovu konfiguraciju.

Promjena konfiguracije postaje promjena koda. Stoga bi trebao biti obuhvaćen istim postupkom osiguranja kvalitete:

Ulaznica -> PR -> pregled -> spajanje -> kontinuirana integracija -> kontinuirana implementacija

Postoje sljedeće posljedice pristupa:

  1. Konfiguracija je koherentna za određenu instancu sustava. Čini se da ne postoji način za netočnu vezu između čvorova.
  2. Nije lako promijeniti konfiguraciju samo u jednom čvoru. Čini se nerazumnim prijavljivati ​​se i mijenjati neke tekstualne datoteke. Tako pomicanje konfiguracije postaje manje moguće.
  3. Male promjene konfiguracije nije lako napraviti.
  4. Većina promjena konfiguracije pratit će isti proces razvoja i proći će određeni pregled.

Trebamo li zasebno spremište za konfiguraciju proizvodnje? Konfiguracija proizvodnje može sadržavati osjetljive informacije koje bismo željeli držati izvan dohvata mnogih ljudi. Stoga bi moglo biti vrijedno držati odvojeno spremište s ograničenim pristupom koje će sadržavati proizvodnu konfiguraciju. Konfiguraciju možemo podijeliti na dva dijela - jedan koji sadrži najotvorenije parametre proizvodnje i onaj koji sadrži tajni dio konfiguracije. To bi većini programera omogućilo pristup velikoj većini parametara dok bi se pristup doista osjetljivim stvarima ograničio. Lako je to postići korištenjem srednjih svojstava sa zadanim vrijednostima parametara.

Varijacije

Pogledajmo prednosti i mane predloženog pristupa u usporedbi s drugim tehnikama upravljanja konfiguracijom.

Prije svega, navest ćemo nekoliko alternativa za različite aspekte predloženog načina rješavanja konfiguracije:

  1. Tekstualna datoteka na ciljnom računalu.
  2. Centralizirana pohrana ključeva i vrijednosti (kao etcd/zookeeper).
  3. Komponente potprocesa koje se mogu ponovno konfigurirati/ponovo pokrenuti bez ponovnog pokretanja procesa.
  4. Konfiguracija izvan artefakta i kontrole verzija.

Tekstualna datoteka daje određenu fleksibilnost u smislu ad-hoc popravaka. Administrator sustava može se prijaviti na ciljni čvor, napraviti promjenu i jednostavno ponovno pokrenuti uslugu. Ovo možda nije tako dobro za veće sustave. Iza promjene ne ostaju nikakvi tragovi. Promjenu ne pregledava drugi par očiju. Možda će biti teško otkriti što je uzrokovalo promjenu. Nije ispitan. Iz perspektive distribuiranog sustava administrator može jednostavno zaboraviti ažurirati konfiguraciju u nekom od drugih čvorova.

(Btw, ako na kraju bude potrebe za korištenjem tekstualnih konfiguracijskih datoteka, morat ćemo samo dodati parser + validator koji bi mogao proizvesti isto Config tipa i to bi bilo dovoljno za početak korištenja tekstualnih konfiguracija. Ovo također pokazuje da je složenost konfiguracije tijekom kompajliranja malo manja od složenosti konfiguracija temeljenih na tekstu, jer nam je u verziji temeljenoj na tekstu potreban dodatni kod.)

Centralizirana pohrana ključ-vrijednosti dobar je mehanizam za distribuciju meta parametara aplikacije. Ovdje moramo razmisliti o tome što smatramo konfiguracijskim vrijednostima, a što samo podacima. S obzirom na funkciju C => A => B obično nazivamo rijetko promjenjivim vrijednostima C "konfiguracija", dok su često mijenjani podaci A - samo unesite podatke. Konfiguraciju treba dati funkciji prije podataka A. S obzirom na ovu ideju možemo reći da se očekivana učestalost promjena može koristiti za razlikovanje podataka o konfiguraciji od samih podataka. Podaci također obično dolaze iz jednog izvora (korisnik), a konfiguracija dolazi iz drugog izvora (administrator). Rad s parametrima koji se mogu mijenjati nakon procesa inicijalizacije dovodi do povećanja složenosti aplikacije. Za takve parametre morat ćemo rukovati njihovim mehanizmom isporuke, analiziranjem i provjerom valjanosti, rukovanjem netočnim vrijednostima. Stoga, kako bismo smanjili složenost programa, bilo bi bolje smanjiti broj parametara koji se mogu mijenjati tijekom izvođenja (ili ih čak potpuno eliminirati).

Iz perspektive ovog posta trebali bismo napraviti razliku između statičkih i dinamičkih parametara. Ako servisna logika zahtijeva rijetku promjenu nekih parametara tijekom izvođenja, tada ih možemo nazvati dinamičkim parametrima. Inače su statični i mogu se konfigurirati pomoću predloženog pristupa. Za dinamičku rekonfiguraciju možda će biti potrebni drugi pristupi. Na primjer, dijelovi sustava mogu se ponovno pokrenuti s novim konfiguracijskim parametrima na sličan način kao ponovno pokretanje odvojenih procesa distribuiranog sustava.
(Moje je skromno mišljenje da treba izbjegavati rekonfiguraciju vremena izvođenja jer ona povećava složenost sustava.
Moglo bi se jednostavnije osloniti samo na podršku OS-a za ponovno pokretanje procesa. No, možda neće uvijek biti moguće.)

Jedan važan aspekt korištenja statičke konfiguracije zbog kojeg ljudi ponekad razmišljaju o dinamičkoj konfiguraciji (bez drugih razloga) je prekid usluge tijekom ažuriranja konfiguracije. Doista, ako moramo promijeniti statičku konfiguraciju, moramo ponovno pokrenuti sustav kako bi nove vrijednosti postale učinkovite. Zahtjevi za vrijeme prekida rada razlikuju se za različite sustave, pa to možda i nije toliko kritično. Ako je kritično, onda moramo unaprijed planirati svako ponovno pokretanje sustava. Na primjer, mogli bismo implementirati Pražnjenje veze AWS ELB. U ovom scenariju kad god trebamo ponovno pokrenuti sustav, paralelno pokrećemo novu instancu sustava, zatim prebacujemo ELB na nju, dok puštamo starom sustavu da dovrši servisiranje postojećih veza.

Što je sa zadržavanjem konfiguracije unutar verzioniranog artefakta ili izvan njega? Održavanje konfiguracije unutar artefakta u većini slučajeva znači da je ta konfiguracija prošla isti proces osiguranja kvalitete kao i drugi artefakti. Tako bi netko mogao biti siguran da je konfiguracija kvalitetna i pouzdana. Naprotiv, konfiguracija u zasebnoj datoteci znači da nema tragova o tome tko je i zašto napravio izmjene u toj datoteci. Je li ovo važno? Vjerujemo da je za većinu proizvodnih sustava bolje imati stabilnu i kvalitetnu konfiguraciju.

Verzija artefakta omogućuje da saznate kada je stvoren, koje vrijednosti sadrži, koje su značajke omogućene/onemogućene, tko je odgovoran za svaku promjenu konfiguracije. Možda će biti potrebno malo truda da se konfiguracija zadrži unutar artefakta i to je dizajnerski izbor.

Za i protiv

Ovdje bismo željeli istaknuti neke prednosti i raspraviti neke nedostatke predloženog pristupa.

Prednosti

Značajke kompajbilne konfiguracije kompletnog distribuiranog sustava:

  1. Statička provjera konfiguracije. To daje visoku razinu povjerenja da je konfiguracija ispravna s obzirom na ograničenja tipa.
  2. Bogat jezik konfiguracije. Tipično su drugi konfiguracijski pristupi ograničeni na najviše varijabilnu zamjenu.
    Korištenjem Scale može se koristiti širok raspon jezičnih značajki kako bi se konfiguracija poboljšala. Na primjer, možemo koristiti značajke za pružanje zadanih vrijednosti, objekte za postavljanje različitih opsega, na koje se možemo pozivati valdefiniran samo jednom u vanjskom opsegu (DRY). Moguće je koristiti doslovne nizove ili instance određenih klasa (Seq, Map, Itd.).
  3. DSL. Scala ima pristojnu podršku za DSL pisce. Ove se značajke mogu koristiti za uspostavljanje konfiguracijskog jezika koji je praktičniji i lakši za krajnjeg korisnika, tako da konačnu konfiguraciju barem mogu čitati korisnici domene.
  4. Integritet i koherencija među čvorovima. Jedna od prednosti posjedovanja konfiguracije za cijeli distribuirani sustav na jednom mjestu je ta da su sve vrijednosti definirane striktno jednom i zatim se ponovno koriste na svim mjestima gdje su nam potrebne. Također tip deklaracija sigurnog priključka osigurava da u svim mogućim ispravnim konfiguracijama čvorovi sustava govore istim jezikom. Postoje eksplicitne ovisnosti između čvorova zbog čega je teško zaboraviti pružiti neke usluge.
  5. Visoka kvaliteta promjena. Cjelokupni pristup prolaska konfiguracijskih promjena kroz uobičajeni PR proces uspostavlja visoke standarde kvalitete iu konfiguraciji.
  6. Istovremene promjene konfiguracije. Kad god napravimo bilo kakve promjene u konfiguraciji, automatska implementacija osigurava da se svi čvorovi ažuriraju.
  7. Pojednostavljenje primjene. Aplikacija ne treba analizirati i provjeravati konfiguraciju i rukovati netočnim konfiguracijskim vrijednostima. Ovo pojednostavljuje cjelokupnu primjenu. (Neko povećanje složenosti je u samoj konfiguraciji, ali to je svjestan kompromis prema sigurnosti.) Prilično je jednostavno vratiti se na uobičajenu konfiguraciju - samo dodajte dijelove koji nedostaju. Lakše je započeti s kompiliranom konfiguracijom i odgoditi implementaciju dodatnih dijelova za neka kasnija vremena.
  8. Konfiguracija s verzijom. Zbog činjenice da promjene konfiguracije slijede isti proces razvoja, kao rezultat dobivamo artefakt s jedinstvenom verzijom. Omogućuje nam vraćanje konfiguracije prema potrebi. Možemo čak implementirati konfiguraciju koja je korištena prije godinu dana i radit će na potpuno isti način. Stabilna konfiguracija poboljšava predvidljivost i pouzdanost distribuiranog sustava. Konfiguracija je fiksirana u vrijeme kompajliranja i ne može se lako mijenjati u proizvodnom sustavu.
  9. Modularnost. Predloženi okvir je modularan i moduli se mogu kombinirati na različite načine
    podržavaju različite konfiguracije (postavke/izgledi). Konkretno, moguće je imati raspored s jednim čvorom u maloj mjeri i postavku s više čvorova u velikoj mjeri. Razumno je imati više produkcijskih izgleda.
  10. Testiranje. Za potrebe testiranja može se implementirati lažna usluga i koristiti je kao ovisnost na tipski siguran način. Nekoliko različitih izgleda testiranja s različitim dijelovima zamijenjenim mockovima može se održavati istovremeno.
  11. Integracijsko testiranje. Ponekad je u distribuiranim sustavima teško izvoditi integracijske testove. Koristeći opisani pristup sigurne konfiguracije tipa kompletnog distribuiranog sustava, možemo pokrenuti sve distribuirane dijelove na jednom poslužitelju na kontroliran način. Lako je oponašati situaciju
    kada jedna od usluga postane nedostupna.

Nedostaci

Pristup kompilirane konfiguracije razlikuje se od "normalne" konfiguracije i možda neće odgovarati svim potrebama. Ovdje su neki od nedostataka kompajlirane konfiguracije:

  1. Statička konfiguracija. Možda nije prikladan za sve primjene. U nekim slučajevima postoji potreba za brzim popravkom konfiguracije u proizvodnji zaobilazeći sve sigurnosne mjere. Ovaj pristup otežava. Kompilacija i ponovno postavljanje potrebni su nakon bilo kakve promjene u konfiguraciji. To je i značajka i teret.
  2. Generiranje konfiguracije. Kada konfiguraciju generira neki alat za automatizaciju, ovaj pristup zahtijeva naknadnu kompilaciju (koja bi zauzvrat mogla biti neuspješna). Možda će biti potreban dodatni napor da se ovaj dodatni korak integrira u sustav izgradnje.
  3. instrumenti. Danas se koristi mnogo alata koji se oslanjaju na tekstualne konfiguracije. Neki od njih
    neće biti primjenjivo kada se konfiguracija kompilira.
  4. Potreban je pomak u načinu razmišljanja. Programeri i DevOps upoznati su s tekstualnim konfiguracijskim datotekama. Ideja sastavljanja konfiguracije mogla bi im se učiniti čudnom.
  5. Prije uvođenja kompajbilne konfiguracije potreban je proces razvoja softvera visoke kvalitete.

Postoje neka ograničenja implementiranog primjera:

  1. Ako pružimo dodatnu konfiguraciju koju implementacija čvora ne zahtijeva, kompajler nam neće pomoći da otkrijemo odsutnu implementaciju. Ovo bi se moglo riješiti korištenjem HList ili ADT-ovi (klase slučajeva) za konfiguraciju čvora umjesto osobina i Cake Pattern-a.
  2. Moramo dati neki predložak u konfiguracijskoj datoteci: (package, import, object deklaracije;
    override defza parametre koji imaju zadane vrijednosti). Ovo se djelomično može riješiti pomoću DSL-a.
  3. U ovom postu ne pokrivamo dinamičku rekonfiguraciju klastera sličnih čvorova.

Zaključak

U ovom smo postu raspravljali o ideji predstavljanja konfiguracije izravno u izvornom kodu na tipski siguran način. Pristup bi se mogao koristiti u mnogim aplikacijama kao zamjena za xml i druge tekstualne konfiguracije. Unatoč tome što je naš primjer implementiran u Scali, također se može prevesti na druge jezike koji se mogu kompilirati (kao što su Kotlin, C#, Swift itd.). Moglo bi se isprobati ovaj pristup u novom projektu i, u slučaju da se ne uklapa, prebaciti se na starinski način.

Naravno, kompajbilna konfiguracija zahtijeva visokokvalitetni razvojni proces. Zauzvrat obećava pružanje jednako kvalitetne robusne konfiguracije.

Ovaj pristup se može proširiti na različite načine:

  1. Moglo bi se koristiti makronaredbe za izvođenje provjere valjanosti konfiguracije i neuspjeh u vrijeme kompajliranja u slučaju bilo kakvih kvarova ograničenja poslovne logike.
  2. DSL bi se mogao implementirati da predstavlja konfiguraciju na način koji je jednostavan za korištenje domene.
  3. Dinamičko upravljanje resursima s automatskim podešavanjem konfiguracije. Na primjer, kada prilagođavamo broj čvorova klastera možda bismo željeli (1) da čvorovi dobiju malo modificiranu konfiguraciju; (2) upravitelj klastera za primanje informacija o novim čvorovima.

Hvala

Želio bih zahvaliti Andreju Saksonovu, Pavelu Popovu, Antonu Nehaevu na davanju inspirativnih povratnih informacija o nacrtu ovog posta koji su mi pomogli da ga učinim jasnijim.

Izvor: www.habr.com