Kompajbilna konfiguracija distribuiranog sistema

U ovom postu želimo podijeliti zanimljiv način rješavanja konfiguracije distribuiranog sistema.
Konfiguracija je predstavljena direktno u Scala jeziku na bezbedan način. Detaljno je opisan primjer implementacije. Razmatraju se različiti aspekti prijedloga, uključujući utjecaj na cjelokupni razvojni proces.

Kompajbilna konfiguracija distribuiranog sistema

(na ruskom)

Uvod

Izgradnja robusnih distribuiranih sistema zahtijeva korištenje ispravne i koherentne konfiguracije na svim čvorovima. Tipično rješenje je korištenje tekstualnog opisa implementacije (terraform, ansible ili nešto slično) i automatski generiranih konfiguracijskih datoteka (često — namijenjenih za svaki čvor/ulogu). Također bismo željeli da koristimo iste protokole istih verzija na svakom komunikacionom čvoru (inače bismo iskusili probleme s nekompatibilnošću). U JVM svijetu to znači da bi barem biblioteka za razmjenu poruka trebala biti iste verzije na svim komunikacionim čvorovima.

Šta je sa testiranjem sistema? Naravno, trebali bismo imati jedinične testove za sve komponente prije nego što dođemo do integracijskih testova. Da bismo mogli ekstrapolirati rezultate testiranja na vrijeme izvođenja, trebali bismo se pobrinuti da verzije svih biblioteka budu identične iu vremenu izvođenja iu okruženju testiranja.

Kada izvodite integracijske testove, često je mnogo lakše imati istu stazu klase na svim čvorovima. Samo trebamo biti sigurni da se isti put do klase koristi pri implementaciji. (Moguće je koristiti različite staze klasa na različitim čvorovima, ali je teže predstaviti ovu konfiguraciju i pravilno je implementirati.) Dakle, da bismo stvari održali jednostavnim, razmotrit ćemo samo identične staze klasa na svim čvorovima.

Konfiguracija ima tendenciju da se razvija zajedno sa softverom. Obično koristimo verzije za identifikaciju različitih
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 će nam trebati posebna grana konfiguracije. Dakle, konfiguracije mogu biti označene granom i verzijom kako bi se jedinstveno identificirale različite konfiguracije. Svaka oznaka grane i verzija odgovaraju jednoj kombinaciji distribuiranih čvorova, portova, vanjskih resursa, verzija biblioteke staza klasa na svakom čvoru. Ovdje ćemo pokriti samo jednu granu i identificirati konfiguracije pomoću trokomponentne decimalne verzije (1.2.3), na isti način kao i ostali artefakti.

U modernim okruženjima konfiguracijski fajlovi se više ne mijenjaju ručno. Obično generišemo
konfiguracijske datoteke u vrijeme implementacije i nikada ih ne dirajte nakon toga. Dakle, moglo bi se zapitati zašto još uvijek koristimo tekstualni format za konfiguracijske datoteke? Izvodljiva opcija je da se konfiguracija smjesti unutar kompilacijske jedinice i da se iskoristi validacija konfiguracije u vrijeme kompajliranja.

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

Kompabilna konfiguracija

U ovom dijelu ćemo raspravljati o primjeru statičke konfiguracije. Konfiguriraju se i implementiraju dvije jednostavne usluge - echo servis i klijent echo servisa. Zatim se instanciraju dva različita distribuirana sistema sa oba servisa. Jedan je za konfiguraciju jednog čvora, a drugi za konfiguraciju dva čvora.

Tipičan distribuirani sistem sastoji se od nekoliko čvorova. Čvorovi se mogu identificirati pomoću nekog tipa:

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 biti u mogućnosti komunicirati s drugim čvorovima putem TCP/HTTP veza.

Za TCP vezu je potreban barem broj porta. Takođe želimo da budemo sigurni da klijent i server razgovaraju istim protokolom. Da 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 dozvoljenog raspona:

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

Rafinirani tipovi

vidjeti prefinjen biblioteka. Ukratko, omogućava dodavanje vremenskih ograničenja kompajliranja drugim tipovima. U ovom slučaju Int dozvoljeno je imati samo 16-bitne vrijednosti koje mogu predstavljati broj porta. Ne postoji zahtjev za korištenje ove biblioteke za ovaj pristup konfiguraciji. Čini se da se jako dobro uklapa.

Za HTTP (REST) ​​možda će nam trebati i putanja usluge:

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

Fantomski tip

Da bismo identifikovali protokol tokom kompilacije, koristimo Scala karakteristiku deklarisanja argumenta tipa Protocol koji se ne koristi u nastavi. To je tzv fantomski tip. Za vrijeme izvođenja rijetko nam je potrebna instanca identifikatora protokola, zato je ne pohranjujemo. Tokom kompilacije ovaj fantomski tip daje dodatnu sigurnost tipa. Ne možemo proći port s pogrešnim protokolom.

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

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

gdje RequestMessage je osnovni tip poruka koje klijent može poslati serveru i ResponseMessage je poruka odgovora sa servera. Naravno, možemo kreirati 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 protokolu poruka zahtjeva se dodaje URL-u, a poruka odgovora se vraća kao običan niz.

Konfiguracija usluge se može opisati imenom usluge, kolekcijom portova i nekim ovisnostima. Postoji nekoliko mogućih načina kako sve ove elemente predstaviti u Scali (na primjer, HList, algebarski tipovi podataka). Za potrebe ovog posta koristit ćemo Cake Pattern i predstavljati komade (module) koji se mogu kombinovati kao osobine. (Uzorak kolača nije uslov za ovaj pristup konfiguracije koji se može kompilirati. To je samo jedna moguća implementacija ideje.)

Zavisnosti se mogu predstaviti korištenjem uzorka kolača kao krajnjih tačaka 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)
  }

Echo servisu je potreban samo konfigurisan port. I izjavljujemo da ovaj port podržava eho protokol. Imajte na umu da u ovom trenutku ne moramo specificirati određeni port, jer osobina dozvoljava deklaracije apstraktnih metoda. Ako koristimo apstraktne metode, kompajler će zahtijevati implementaciju u instanci konfiguracije. Ovdje smo obezbijedili implementaciju (8081) i koristit će se kao zadana vrijednost ako je preskočimo u konkretnoj konfiguraciji.

Možemo deklarirati ovisnost u konfiguraciji klijenta usluge eho:

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

Zavisnost ima isti tip kao i echoService. Konkretno, zahtijeva isti protokol. Dakle, možemo biti sigurni da će, ako povežemo ove dvije zavisnosti, raditi ispravno.

Implementacija usluga

Servisu je potrebna funkcija za pokretanje i graciozno gašenje. (Mogućnost gašenja servisa je kritična za testiranje.) Opet postoji nekoliko opcija specificiranja takve funkcije za datu konfiguraciju (na primjer, mogli bismo koristiti klase tipa). Za ovaj post ćemo ponovo koristiti Cake Pattern. Možemo predstavljati uslugu koristeći cats.Resource koji već pruža zagrade i oslobađanje resursa. Da bismo nabavili resurs, trebali bismo obezbijediti konfiguraciju i neki kontekst 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 — tip konfiguracije koji je potreban za ovaj starter servisa
  • AddressResolver — runtime objekat koji ima mogućnost da dobije stvarne adrese drugih čvorova (nastavite čitati za detalje).

ostale vrste potiču iz cats:

  • F[_] — tip efekta (U najjednostavnijem slučaju F[A] mogao biti pravedan () => A. U ovom postu ćemo koristiti cats.IO.)
  • Reader[A,B] — je manje-više sinonim za funkciju A => B
  • cats.Resource — ima načina za sticanje i oslobađanje
  • Timer — omogućava spavanje/mjerenje vremena
  • ContextShift - analog od ExecutionContext
  • Applicative — omotač funkcija na snazi ​​(skoro monada) (mogli bismo ga eventualno zamijeniti nečim drugim)

Koristeći ovaj interfejs možemo implementirati nekoliko servisa. 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 implementaciju ostalih usluga — echo service,
echo client i doživotni kontroleri.)

Čvor je jedan objekat koji pokreće nekoliko usluga (pokretanje lanca resursa je omogućeno Cake Patternom):

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 specificiramo tačan tip konfiguracije koji je potreban ovom čvoru. Kompajler nam ne dozvoljava da izgradimo objekat (Cake) sa nedovoljnim tipom, jer svaka karakteristika usluge deklarira ograničenje na Config tip. Također nećemo moći pokrenuti čvor bez pružanja kompletne konfiguracije.

Rezolucija adrese čvora

Da bismo uspostavili vezu potrebna nam je stvarna adresa domaćina za svaki čvor. Možda će biti poznato kasnije od ostalih dijelova konfiguracije. Dakle, potreban nam je način da obezbijedimo mapiranje između ID-a čvora i njegove stvarne adrese. Ovo mapiranje 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 za implementaciju takve funkcije.

  1. Ako znamo stvarne adrese prije implementacije, tokom instanciranja hostova čvorova, onda možemo generirati Scala kod sa stvarnim adresama i pokrenuti izgradnju nakon toga (koja vrši provjere vremena kompajliranja i zatim pokreće integracijski testni paket). U ovom slučaju naša funkcija mapiranja je statički poznata i može se pojednostaviti na nešto poput a Map[NodeId, NodeAddress].
  2. Ponekad stvarne adrese dobijemo tek kasnije kada je čvor stvarno pokrenut, ili nemamo adrese čvorova koji još nisu pokrenuti. U ovom slučaju možemo imati 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 zavisnosti.
  3. Ako možemo da izmenimo /etc/hosts, možemo koristiti unaprijed definirana imena hostova (npr my-project-main-node i echo-backend) i samo povežite ovo ime sa ip adresom u vrijeme implementacije.

U ovom postu ne obrađujemo ove slučajeve detaljnije. Zapravo u našem primjeru igračke svi čvorovi će imati istu IP adresu — 127.0.0.1.

U ovom postu ćemo razmotriti dva rasporeda distribuiranog sistema:

  1. Izgled jednog čvora, gdje su sve usluge smještene na jednom čvoru.
  2. Izgled 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 kreiramo jednu konfiguraciju koja proširuje konfiguraciju servera i klijenta. Također konfiguriramo kontroler životnog ciklusa koji će nakon toga normalno prekinuti klijenta i servera lifetime interval prolazi.

Isti skup implementacija i konfiguracija usluge može se koristiti za kreiranje rasporeda sistema sa dva odvojena čvora. Samo treba da stvaramo dvije odvojene konfiguracije čvora sa odgovarajućim uslugama:

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 zavisnost. Pominjemo pruženu uslugu drugog čvora kao zavisnost od trenutnog čvora. Tip zavisnosti je provjeren jer sadrži fantomski tip koji opisuje protokol. I u vrijeme izvođenja imat ćemo ispravan id čvora. Ovo je jedan od važnih aspekata predloženog pristupa konfiguraciji. Pruža nam mogućnost da samo jednom postavimo port i uvjerimo se da referenciramo ispravan port.

Implementacija dva čvora

Za ovu konfiguraciju koristimo potpuno iste implementacije usluga. Nema nikakvih promjena. Međutim, kreiramo 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 server i potrebna mu je samo konfiguracija na strani servera. Drugi čvor implementira klijenta i treba mu drugi dio konfiguracije. Oba čvora zahtijevaju neke doživotne specifikacije. Za potrebe ove poštanske usluge čvor će imati beskonačan životni vijek koji se može prekinuti korištenjem SIGTERM, dok će echo klijent prekinuti nakon konfiguriranog konačnog trajanja. Vidi starter aplikacija za detalje.

Cjelokupni razvojni proces

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

Konfiguracija kao kod će se kompajlirati i proizvodi artefakt. Čini se razumnim odvojiti konfiguracijski artefakt 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. Dakle, trebalo bi da bude pokriveno istim procesom osiguranja kvaliteta:

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

Postoje sljedeće posljedice pristupa:

  1. Konfiguracija je koherentna za određenu instancu sistema. Čini se da ne postoji način da se uspostavi pogrešna veza između čvorova.
  2. Nije lako promijeniti konfiguraciju samo u jednom čvoru. Čini se nerazumnim prijaviti se i promijeniti neke tekstualne datoteke. Dakle, odstupanje konfiguracije postaje manje moguće.
  3. Male promjene konfiguracije nije lako napraviti.
  4. Većina promjena konfiguracije će pratiti isti razvojni proces i proći će izvjestan pregled.

Da li nam je potrebno posebno spremište za konfiguraciju proizvodnje? Konfiguracija proizvodnje može sadržavati osjetljive informacije koje bismo željeli držati izvan dohvata mnogih ljudi. Stoga bi možda bilo vrijedno čuvati zasebno spremište s ograničenim pristupom koje će sadržavati konfiguraciju proizvodnje. Konfiguraciju možemo podijeliti na dva dijela - jedan koji sadrži najotvorenije parametre proizvodnje i onaj koji sadrži tajni dio konfiguracije. Ovo bi omogućilo pristup većini programera velikoj većini parametara dok bi se ograničio pristup zaista osjetljivim stvarima. To je lako postići korištenjem srednjih osobina sa zadanim vrijednostima parametara.

Varijacije

Pogledajmo prednosti i nedostatke predloženog pristupa u poređenju sa drugim tehnikama upravljanja konfiguracijom.

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

  1. Tekstualni fajl na ciljnoj mašini.
  2. Centralizirana pohrana ključ/vrijednost (npr etcd/zookeeper).
  3. Komponente potprocesa koje se mogu rekonfigurirati/ponovno pokrenuti bez ponovnog pokretanja procesa.
  4. Konfiguracija izvan kontrole artefakata i verzija.

Tekstualni fajl daje određenu fleksibilnost u smislu ad-hoc popravki. Administrator sistema se može prijaviti na ciljni čvor, izvršiti promjenu i jednostavno ponovo pokrenuti uslugu. Ovo možda neće biti dobro za veće sisteme. Iza promene ne ostaju nikakvi tragovi. Promjenu ne pregleda drugi par očiju. Možda će biti teško otkriti šta je uzrokovalo promjenu. Nije testirano. Iz perspektive distribuiranog sistema, administrator može jednostavno zaboraviti ažurirati konfiguraciju u jednom od drugih čvorova.

(Btw, ako na kraju bude potrebno da počnemo koristiti tekstualne konfiguracijske datoteke, morat ćemo dodati samo parser + validator koji bi mogao proizvesti isto Config upišite i to bi bilo dovoljno da počnete koristiti tekstualne konfiguracije. Ovo također pokazuje da je složenost konfiguracije u vrijeme kompajliranja malo manja od složenosti konfiguracija zasnovanih na tekstu, jer nam je u verziji zasnovanoj na tekstu potreban dodatni kod.)

Centralizirana pohrana ključ/vrijednost je dobar mehanizam za distribuciju meta parametara aplikacije. Ovdje trebamo razmisliti o tome što smatramo konfiguracijskim vrijednostima, a što samo podacima. Zadata funkcija C => A => B obično nazivamo vrijednostima koje se rijetko mijenjaju C "konfiguracija", a često mijenjani podaci A - samo unesite podatke. Konfiguraciju treba dati funkciji prije podataka A. S obzirom na ovu ideju možemo reći da je očekivana učestalost promjena ono što bi se moglo koristiti za razlikovanje konfiguracijskih podataka od samo podataka. Takođe podaci obično dolaze iz jednog izvora (korisnika), a konfiguracija dolazi iz drugog izvora (administratora). Bavljenje parametrima koji se mogu promijeniti nakon procesa inicijalizacije dovodi do povećanja složenosti aplikacije. Za takve parametre moraćemo da rukujemo njihovim mehanizmom isporuke, raščlanjivanjem i validacijom, rukovanjem netačnim vrednostima. Stoga, da bismo smanjili složenost programa, bolje bi bilo da smanjimo broj parametara koji se mogu promijeniti tokom izvršavanja (ili ih čak potpuno eliminirati).

Iz perspektive ovog posta trebalo bi napraviti razliku između statičkih i dinamičkih parametara. Ako servisna logika zahtijeva rijetku promjenu nekih parametara u vrijeme izvođenja, onda ih možemo nazvati dinamičkim parametrima. Inače su statične i mogu se konfigurirati korištenjem predloženog pristupa. Za dinamičku rekonfiguraciju mogu biti potrebni drugi pristupi. Na primjer, dijelovi sistema mogu se ponovo pokrenuti s novim konfiguracijskim parametrima na sličan način kao ponovno pokretanje odvojenih procesa distribuiranog sistema.
(Moje skromno mišljenje je izbjegavanje rekonfiguracije vremena izvršavanja jer to povećava složenost sistema.
Možda bi bilo jednostavnije osloniti se na OS podršku za ponovno pokretanje procesa. Mada, to možda nije uvek moguće.)

Jedan važan aspekt upotrebe statičke konfiguracije koji ponekad navodi ljude da razmisle o dinamičkoj konfiguraciji (bez drugih razloga) je prekid servisa tokom ažuriranja konfiguracije. Zaista, ako moramo da unesemo promene u statičku konfiguraciju, moramo ponovo pokrenuti sistem kako bi nove vrednosti postale efektivne. Zahtjevi za vrijeme zastoja se razlikuju za različite sisteme, tako da to možda nije toliko kritično. Ako je kritično, onda moramo unaprijed planirati bilo kakvo ponovno pokretanje sistema. Na primjer, mogli bismo implementirati Pražnjenje AWS ELB veze. U ovom scenariju kad god trebamo ponovo pokrenuti sistem, paralelno pokrećemo novu instancu sistema, a zatim prebacimo ELB na nju, dok starom sistemu dopuštamo da završi servisiranje postojećih veza.

Šta je sa zadržavanjem konfiguracije unutar verzionisanog artefakta ili izvan? Održavanje konfiguracije unutar artefakta znači u većini slučajeva da je ova konfiguracija prošla isti proces osiguranja kvaliteta kao i drugi artefakti. Dakle, neko može biti siguran da je konfiguracija dobrog kvaliteta i pouzdana. Naprotiv, konfiguracija u zasebnoj datoteci znači da nema tragova ko je i zašto izvršio promjene u tom fajlu. Je li ovo važno? Vjerujemo da je za većinu proizvodnih sistema bolje imati stabilnu i kvalitetnu konfiguraciju.

Verzija artefakta omogućava da se sazna kada je kreiran, koje vrijednosti sadrži, koje funkcije su omogućene/onemogućene, ko je odgovoran za svaku promjenu u konfiguraciji. Možda će biti potrebno malo truda da se konfiguracija zadrži unutar artefakta i to je izbor dizajna.

Za i protiv

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

prednosti

Karakteristike kompilibilne konfiguracije kompletnog distribuiranog sistema:

  1. Statička provjera konfiguracije. Ovo daje visok nivo pouzdanosti, da je konfiguracija ispravna s obzirom na ograničenja tipa.
  2. Bogat jezik konfiguracije. Obično su drugi pristupi konfiguraciji ograničeni na najviše varijabilnu zamjenu.
    Koristeći Scala može se koristiti širok raspon jezičnih karakteristika kako bi se konfiguracija poboljšala. Na primjer, možemo koristiti osobine za pružanje zadanih vrijednosti, objekte za postavljanje različitog opsega, na koje se možemo pozivati vals definiran samo jednom u vanjskom opsegu (DRY). Moguće je koristiti literalne sekvence ili instance određenih klasa (Seq, Map, Itd).
  3. DSL. Scala ima pristojnu podršku za DSL pisce. Ove karakteristike se mogu koristiti za uspostavljanje jezika konfiguracije koji je pogodniji i prilagođeniji krajnjem korisniku, tako da konačna konfiguracija bude barem čitljiva od strane korisnika domene.
  4. Integritet i koherentnost među čvorovima. Jedna od prednosti posedovanja konfiguracije za ceo distribuirani sistem na jednom mestu je ta što su sve vrednosti definisane striktno jednom, a zatim se ponovo koriste na svim mestima gde su nam potrebne. Također unesite deklaracije sigurnog porta osiguravaju da će u svim mogućim ispravnim konfiguracijama čvorovi sistema govoriti isti jezik. Postoje eksplicitne zavisnosti između čvorova zbog čega je teško zaboraviti pružiti neke usluge.
  5. Visok kvalitet promjena. Ukupni pristup prolaska promjena konfiguracije kroz normalan PR proces uspostavlja visoke standarde kvaliteta 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 aplikacije. Aplikacija ne mora analizirati i validirati konfiguraciju i rukovati netočnim vrijednostima konfiguracije. Ovo pojednostavljuje cjelokupnu primjenu. (Neki porast složenosti je u samoj konfiguraciji, ali to je svjesni kompromis ka sigurnosti.) Prilično je jednostavno vratiti se na uobičajenu konfiguraciju — samo dodajte dijelove koji nedostaju. Lakše je započeti s kompajliranom konfiguracijom i odgoditi implementaciju dodatnih dijelova za neka kasnija vremena.
  8. Versionirana konfiguracija. Zbog činjenice da promjene konfiguracije prate isti razvojni proces, kao rezultat dobijamo artefakt sa jedinstvenom verzijom. Omogućava nam da vratimo konfiguraciju ako je potrebno. Možemo čak primijeniti konfiguraciju koja je korištena prije godinu dana i ona će raditi na potpuno isti način. Stabilna konfiguracija poboljšava predvidljivost i pouzdanost distribuiranog sistema. Konfiguracija je fiksirana u vrijeme kompajliranja i ne može se lako mijenjati u proizvodnom sistemu.
  9. Modularnost. Predloženi okvir je modularan i moduli se mogu kombinovati na različite načine
    podržavaju različite konfiguracije (postavke/izgledi). Konkretno, moguće je imati mali raspored jednog čvora i postavku za više čvorova velikih razmjera. Razumno je imati više rasporeda proizvodnje.
  10. Testiranje. Za potrebe testiranja može se implementirati lažni servis i koristiti ga kao zavisnost na bezbedan način. Nekoliko različitih rasporeda testiranja s različitim dijelovima zamijenjenim simulacijama moglo bi se održavati istovremeno.
  11. Integracijsko testiranje. Ponekad je u distribuiranim sistemima teško pokrenuti integracijske testove. Koristeći opisani pristup bezbednoj konfiguraciji kompletnog distribuiranog sistema, možemo pokrenuti sve distribuirane delove na jednom serveru na kontrolisan način. Lako je oponašati situaciju
    kada jedna od usluga postane nedostupna.

nedostaci

Pristup kompajlirane konfiguracije razlikuje se od “normalne” konfiguracije i možda neće odgovarati svim potrebama. Evo nekih nedostataka kompajlirane konfiguracije:

  1. Statička konfiguracija. Možda nije pogodan za sve aplikacije. U nekim slučajevima postoji potreba za brzim fiksiranjem konfiguracije u proizvodnji zaobilazeći sve sigurnosne mjere. Ovaj pristup otežava. Kompilacija i ponovno raspoređivanje su potrebni nakon bilo kakve promjene u konfiguraciji. Ovo je i karakteristika i teret.
  2. Generisanje konfiguracije. Kada konfiguraciju generiše neki alat za automatizaciju, ovaj pristup zahtijeva naknadnu kompilaciju (koja zauzvrat može propasti). Možda će biti potrebni dodatni napori da se ovaj dodatni korak integriše u sistem izgradnje.
  3. Instrumenti. Danas je u upotrebi mnogo alata koji se oslanjaju na konfiguracije zasnovane na tekstu. Neki od njih
    neće biti primjenjivo kada se konfiguracija kompajlira.
  4. Potrebna je promjena u načinu razmišljanja. Programeri i DevOps su upoznati s tekstualnim konfiguracijskim datotekama. Ideja kompajliranja konfiguracije mogla bi im se činiti čudnom.
  5. Prije uvođenja kompajbilne konfiguracije potreban je proces razvoja softvera visokog kvaliteta.

Postoje neka ograničenja implementiranog primjera:

  1. Ako obezbedimo dodatnu konfiguraciju koju implementacija čvora ne zahteva, kompajler nam neće pomoći da otkrijemo odsustvo implementacije. Ovo bi se moglo riješiti korištenjem HList ili ADT (klase slučajeva) za konfiguraciju čvora umjesto osobina i uzorka kolača.
  2. Moramo da obezbedimo neki šablon u konfiguracionom fajlu: (package, import, object deklaracije;
    override def's za parametre koji imaju zadane vrijednosti). Ovo se može djelomično riješiti korištenjem DSL-a.
  3. U ovom postu ne pokrivamo dinamičku rekonfiguraciju klastera sličnih čvorova.

zaključak

U ovom postu smo raspravljali o ideji predstavljanja konfiguracije direktno u izvornom kodu na 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, mogao bi se prevesti i na druge kompilativne jezike (kao što su Kotlin, C#, Swift, itd.). Ovaj pristup bi se mogao isprobati u novom projektu i, u slučaju da se ne uklapa, preći na starinski način.

Naravno, kompajbilna konfiguracija zahtijeva visokokvalitetan razvojni proces. Zauzvrat obećava da će pružiti jednako kvalitetnu robusnu konfiguraciju.

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

  1. Moglo bi se koristiti makronaredbe za provjeru valjanosti konfiguracije i neuspjeh u vrijeme kompajliranja u slučaju neuspjeha bilo kakvih ograničenja poslovne logike.
  2. DSL bi se mogao implementirati da predstavlja konfiguraciju na način prilagođen korisniku domene.
  3. Dinamičko upravljanje resursima s automatskim prilagodbama konfiguracije. Na primjer, kada prilagodimo broj čvorova klastera, možda bismo željeli (1) da čvorovi dobiju malo izmijenjenu konfiguraciju; (2) upravitelj klastera za primanje informacija o novim čvorovima.

hvala

Želeo bih da se zahvalim Andreju Saksonovu, Pavelu Popovu, Antonu Nehajevu što su dali inspirativne povratne informacije o nacrtu ovog posta koje su mi pomogle da to bude jasnije.

izvor: www.habr.com

Kupite pouzdan hosting za sajtove sa DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan web hosting sa DDoS zaštitom, VPS VDS servere | ProHoster