Prevedena konfiguracija distribuiranog sistema

Želio bih da vam ispričam jedan zanimljiv mehanizam za rad sa konfiguracijom distribuiranog sistema. Konfiguracija je predstavljena direktno u kompajliranom jeziku (Scala) koristeći sigurne tipove. Ovaj post daje primjer takve konfiguracije i raspravlja o različitim aspektima implementacije kompajlirane konfiguracije u cjelokupni proces razvoja.

Prevedena konfiguracija distribuiranog sistema

(engleski)

Uvod

Izgradnja pouzdanog distribuiranog sistema znači da svi čvorovi koriste ispravnu konfiguraciju, sinhronizovanu sa drugim čvorovima. DevOps tehnologije (terraform, ansible ili nešto slično) se obično koriste za automatsko generiranje konfiguracijskih datoteka (često specifične za svaki čvor). Također bismo željeli biti sigurni da svi komunikacioni čvorovi koriste identične protokole (uključujući istu verziju). U suprotnom, nekompatibilnost će biti ugrađena u naš distribuirani sistem. U svijetu JVM-a, jedna posljedica ovog zahtjeva je da se ista verzija biblioteke koja sadrži poruke protokola mora koristiti svuda.

Šta je sa testiranjem distribuiranog sistema? Naravno, pretpostavljamo da sve komponente imaju jedinične testove prije nego što pređemo na testiranje integracije. (Da bismo mogli ekstrapolirati rezultate testa na vrijeme izvođenja, također moramo obezbijediti identičan skup biblioteka u fazi testiranja iu vremenu izvođenja.)

Kada radite sa integracijskim testovima, često je lakše koristiti istu stazu klase svuda na svim čvorovima. Sve što treba da uradimo je da osiguramo da se ista staza klase koristi tokom vremena izvođenja. (Iako je potpuno moguće pokrenuti različite čvorove sa različitim stazama klasa, ovo dodaje složenost cjelokupnoj konfiguraciji i poteškoće s implementacijom i integracijskim testovima.) Za potrebe ovog posta, pretpostavljamo da će svi čvorovi koristiti istu stazu klase.

Konfiguracija se razvija sa aplikacijom. Koristimo verzije za identifikaciju različitih faza evolucije programa. Čini se logičnim identificirati i različite verzije konfiguracija. I postavite samu konfiguraciju u sistem kontrole verzija. Ako postoji samo jedna konfiguracija u proizvodnji, onda možemo jednostavno koristiti broj verzije. Ako koristimo mnogo proizvodnih instanci, onda će nam trebati nekoliko
konfiguracijske grane i dodatnu oznaku pored verzije (na primjer, naziv grane). Na taj način možemo jasno identificirati tačnu konfiguraciju. Svaki identifikator konfiguracije jedinstveno odgovara specifičnoj kombinaciji distribuiranih čvorova, portova, vanjskih resursa i verzija biblioteke. Za potrebe ovog posta pretpostavićemo da postoji samo jedna grana i možemo identifikovati konfiguraciju na uobičajeni način koristeći tri broja odvojena tačkom (1.2.3).

U modernim okruženjima, konfiguracijske datoteke se rijetko kreiraju ručno. Češće se generiraju tijekom implementacije i više se ne dodiruju (tako da nemoj ništa slomiti). Postavlja se prirodno pitanje: zašto još uvijek koristimo tekstualni format za pohranjivanje konfiguracije? Čini se da je održiva alternativa mogućnost korištenja redovnog koda za konfiguraciju i koristi od provjera vremena kompajliranja.

U ovom postu ćemo istražiti ideju predstavljanja konfiguracije unutar kompajliranog artefakta.

Prevedena konfiguracija

Ovaj odjeljak pruža primjer statičke kompajlirane konfiguracije. Implementirane su dvije jednostavne usluge - echo servis i echo servis klijent. Na osnovu ova dva servisa sklapaju se dvije sistemske opcije. U jednoj opciji, obje usluge se nalaze na istom čvoru, u drugoj opciji - na različitim čvorovima.

Obično distribuirani sistem sadrži nekoliko čvorova. Čvorove možete identificirati koristeći vrijednosti nekog tipa NodeId:

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

ili

case class NodeId(hostName: String)

ili čak

object Singleton
type NodeId = Singleton.type

Čvorovi obavljaju različite uloge, pokreću usluge i TCP/HTTP veze se mogu uspostaviti između njih.

Za opis TCP veze potreban nam je barem broj porta. Također bismo željeli prikazati protokol koji je podržan na tom portu kako bismo osigurali da i klijent i server koriste isti protokol. Povezanost ćemo opisati pomoću sljedeće klase:

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

gdje Port - samo cijeli broj Int označavajući raspon prihvatljivih vrijednosti:

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

Rafinirani tipovi

Vidi biblioteku prefinjen и moj izveštaj. Ukratko, biblioteka vam omogućava da dodate ograničenja tipovima koji se provjeravaju u vrijeme kompajliranja. U ovom slučaju, važeće vrijednosti broja porta su 16-bitni cijeli brojevi. Za kompajliranu konfiguraciju, korištenje rafinirane biblioteke nije obavezno, ali poboljšava sposobnost kompajlera da provjeri konfiguraciju.

Za HTTP (REST) ​​protokole, osim broja porta, možda će nam trebati i putanja do usluge:

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

Fantomski tipovi

Da bismo identificirali protokol u vrijeme kompajliranja, koristimo parametar tipa koji se ne koristi unutar klase. Ova odluka je zbog činjenice da ne koristimo instancu protokola u vrijeme izvođenja, ali bismo željeli da kompajler provjeri kompatibilnost protokola. Određivanjem protokola nećemo moći proći neodgovarajuću uslugu kao zavisnost.

Jedan od uobičajenih protokola je REST API sa Json serijalizacijom:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

gdje RequestMessage - vrstu zahtjeva, ResponseMessage — tip odgovora.
Naravno, možemo koristiti druge opise protokola koji obezbjeđuju tačnost opisa koja nam je potrebna.

Za potrebe ovog posta, koristićemo pojednostavljenu verziju protokola:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Ovdje je zahtjev string dodat URL-u, a odgovor je vraćeni niz u tijelu HTTP odgovora.

Konfiguracija usluge je opisana imenom usluge, portovima i ovisnostima. Ovi elementi mogu biti predstavljeni u Scali na nekoliko načina (npr. HList-s, algebarski tipovi podataka). Za potrebe ovog posta, koristit ćemo Cake Pattern i predstavljati module koristeći trait'ov. (Uzorak kolača nije obavezan element ovog pristupa. To je jednostavno jedna moguća implementacija.)

Zavisnosti između usluga mogu se predstaviti kao metode koje vraćaju portove EndPoint's 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)
  }

Da biste kreirali echo uslugu, sve što vam treba je broj porta i indikacija da port podržava echo protokol. Možda nećemo navesti određeni port, jer... osobine vam omogućavaju da deklarirate metode bez implementacije (apstraktne metode). U ovom slučaju, prilikom kreiranja konkretne konfiguracije, kompajler bi od nas tražio da obezbedimo implementaciju apstraktne metode i da obezbedimo broj porta. Pošto smo implementirali metodu, prilikom kreiranja specifične konfiguracije, možda nećemo navesti drugi port. Koristit će se zadana vrijednost.

U konfiguraciji klijenta deklariramo ovisnost o echo servisu:

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

Ovisnost je istog tipa kao i izvezena usluga echoService. Konkretno, u echo klijentu nam je potreban isti protokol. Stoga, kada povežete dvije usluge, možemo biti sigurni da će sve raditi kako treba.

Implementacija usluga

Potrebna je funkcija za pokretanje i zaustavljanje usluge. (Mogućnost zaustavljanja usluge je kritična za testiranje.) Opet, postoji nekoliko opcija za implementaciju takve funkcije (na primjer, mogli bismo koristiti klase tipa na osnovu tipa konfiguracije). Za potrebe ovog posta ćemo koristiti Cake Pattern. Mi ćemo predstavljati uslugu koristeći klasu cats.Resource, jer Ova klasa već pruža sredstva za sigurno jamčenje oslobađanja resursa u slučaju problema. Da bismo dobili resurs, moramo obezbijediti konfiguraciju i gotov kontekst izvođenja. 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 za ovu uslugu
  • AddressResolver — runtime objekat koji vam omogućava da saznate adrese drugih čvorova (pogledajte ispod)

i druge vrste iz biblioteke cats:

  • F[_] — vrsta efekta (u najjednostavnijem slučaju F[A] može biti samo funkcija () => A. U ovom postu ćemo koristiti cats.IO.)
  • Reader[A,B] - manje-više sinonim za funkciju A => B
  • cats.Resource - resurs koji se može nabaviti i osloboditi
  • Timer — tajmer (omogućava vam da zaspite neko vrijeme i mjerite vremenske intervale)
  • ContextShift - analogni ExecutionContext
  • Applicative — klasa tipa efekta koja vam omogućava da kombinujete pojedinačne efekte (skoro monada). U složenijim aplikacijama čini se da je bolje koristiti Monad/ConcurrentEffect.

Koristeći ovaj potpis funkcije 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](()))
  }

(Cm. izvor, u kojem se implementiraju i druge usluge - echo service, echo client
и doživotni kontroleri.)

Čvor je objekat koji može pokrenuti nekoliko servisa (pokretanje lanca resursa osigurava 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 navodimo tačan tip konfiguracije koji je potreban za ovaj čvor. Ako zaboravimo navesti jedan od tipova konfiguracije koje zahtijeva određeni servis, doći će do greške pri kompilaciji. Također, nećemo moći pokrenuti čvor osim ako ne obezbijedimo neki objekat odgovarajućeg tipa sa svim potrebnim podacima.

Rezolucija imena hosta

Da bismo se povezali na udaljeni host, potrebna nam je prava IP adresa. Moguće je da će adresa postati poznata kasnije od ostatka konfiguracije. Dakle, potrebna nam je funkcija koja mapira ID čvora na adresu:

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

Postoji nekoliko načina za implementaciju ove funkcije:

  1. Ako nam adrese postanu poznate prije implementacije, onda možemo generirati Scala kod pomoću
    adrese, a zatim pokrenite build. Ovo će kompajlirati i pokrenuti testove.
    U ovom slučaju, funkcija će biti poznata statički i može se predstaviti u kodu kao mapiranje Map[NodeId, NodeAddress].
  2. U nekim slučajevima, stvarna adresa je poznata tek nakon što se čvor pokrene.
    U ovom slučaju, možemo implementirati “uslugu otkrivanja” koja se pokreće prije drugih čvorova i svi čvorovi će se registrovati na ovom servisu i tražiti adrese drugih čvorova.
  3. Ako možemo da izmenimo /etc/hosts, tada možete koristiti unaprijed definirana imena hostova (npr my-project-main-node и echo-backend) i jednostavno povežite ova imena
    sa IP adresama tokom implementacije.

U ovom postu nećemo detaljnije razmatrati ove slučajeve. Za naš
u primjeru igračke, svi čvorovi će imati istu IP adresu - 127.0.0.1.

Zatim razmatramo dvije opcije za distribuirani sistem:

  1. Postavljanje svih servisa na jedan čvor.
  2. I hosting echo usluge i echo klijenta na različitim čvorovima.

Konfiguracija za jedan čvor:

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

Objekt implementira konfiguraciju i klijenta i servera. Konfiguracija vremena za život se također koristi tako da nakon intervala lifetime prekinuti program. (Ctrl-C također radi i oslobađa sve resurse ispravno.)

Isti skup osobina konfiguracije i implementacije može se koristiti za kreiranje sistema koji se sastoji od dva odvojena čvora:

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

Bitan! Obratite pažnju na to kako su usluge povezane. Mi specificiramo uslugu koju implementira jedan čvor kao implementaciju metode zavisnosti drugog čvora. Tip zavisnosti provjerava kompajler, jer sadrži tip protokola. Kada se pokrene, zavisnost će sadržavati ispravan ID ciljnog čvora. Zahvaljujući ovoj šemi, navedemo broj porta tačno jednom i uvijek je zajamčeno da se odnosi na ispravan port.

Implementacija dva sistemska čvora

Za ovu konfiguraciju koristimo iste implementacije usluge bez promjena. Jedina razlika je u tome što sada imamo dva objekta koji implementiraju 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 servera. Drugi čvor implementira klijenta i koristi drugačiji dio konfiguracije. Također, oba čvora trebaju upravljanje životnim vijekom. Čvor servera radi neograničeno dok se ne zaustavi SIGTERM'om, a klijentski čvor se prekida nakon nekog vremena. Cm. aplikacija za pokretanje.

Opšti razvojni proces

Pogledajmo kako ovaj pristup konfiguraciji utječe na cjelokupni proces razvoja.

Konfiguracija će biti kompajlirana zajedno s ostatkom koda i generirat će se artefakt (.jar). Čini se da ima smisla staviti konfiguraciju u poseban artefakt. To je zato što možemo imati više konfiguracija zasnovanih na istom kodu. Opet, moguće je generirati artefakte koji odgovaraju različitim granama konfiguracije. Ovisnosti o određenim verzijama biblioteka spremaju se zajedno s konfiguracijom, a ove verzije se čuvaju zauvijek kad god odlučimo implementirati tu verziju konfiguracije.

Svaka promjena konfiguracije pretvara se u promjenu koda. I stoga, svaki
promjena će biti pokrivena normalnim procesom osiguranja kvaliteta:

Ulaznica u alatu za praćenje grešaka -> PR -> recenzija -> spajanje s relevantnim granama ->
integracija -> implementacija

Glavne posljedice implementacije kompajlirane konfiguracije su:

  1. Konfiguracija će biti konzistentna na svim čvorovima distribuiranog sistema. Zbog činjenice da svi čvorovi primaju istu konfiguraciju iz jednog izvora.

  2. Problematično je promijeniti konfiguraciju samo u jednom od čvorova. Stoga je malo vjerovatno „pomakanje konfiguracije“.

  3. Postaje teže napraviti male promjene u konfiguraciji.

  4. Većina promjena konfiguracije dogodit će se kao dio cjelokupnog procesa razvoja i bit će predmet revizije.

Trebam li zasebno spremište za pohranjivanje proizvodne konfiguracije? Ova konfiguracija može sadržavati lozinke i druge osjetljive informacije kojima bismo željeli ograničiti pristup. Na osnovu ovoga, čini se da ima smisla pohraniti konačnu konfiguraciju u zasebno spremište. Možete podijeliti konfiguraciju na dva dijela – jedan koji sadrži javno dostupne konfiguracijske postavke i jedan koji sadrži ograničena podešavanja. Ovo će većini programera omogućiti pristup uobičajenim postavkama. Ovo razdvajanje je lako postići korištenjem srednjih osobina koje sadrže zadane vrijednosti.

Moguće varijacije

Pokušajmo uporediti kompajliranu konfiguraciju s nekim uobičajenim alternativama:

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

Tekstualne datoteke pružaju značajnu fleksibilnost u smislu malih izmjena. Administrator sistema može se prijaviti na udaljeni čvor, napraviti promjene u odgovarajućim datotekama i ponovo pokrenuti uslugu. Za velike sisteme, međutim, takva fleksibilnost možda nije poželjna. Učinjene promjene ne ostavljaju tragove u drugim sistemima. Niko ne revidira promjene. Teško je utvrditi ko je tačno izvršio promene i iz kog razloga. Promjene se ne testiraju. Ako je sistem distribuiran, tada administrator može zaboraviti izvršiti odgovarajuću promjenu na drugim čvorovima.

(Također treba napomenuti da korištenje kompajlirane konfiguracije ne zatvara mogućnost korištenja tekstualnih datoteka u budućnosti. Biće dovoljno dodati parser i validator koji proizvodi isti tip kao izlaz Config, a možete koristiti tekstualne datoteke. Odmah slijedi da je složenost sistema sa kompajliranom konfiguracijom nešto manja od složenosti sistema koji koristi tekstualne datoteke, jer tekstualne datoteke zahtijevaju dodatni kod.)

Centralizovano skladište ključ/vrijednost je dobar mehanizam za distribuciju meta parametara distribuirane aplikacije. Moramo da odlučimo šta su konfiguracioni parametri, a šta samo podaci. Hajde da imamo funkciju C => A => B, i parametri C rijetko promjene i podaci A - često. U ovom slučaju to možemo reći C - konfiguracijskih parametara, i A - podaci. Čini se da se konfiguracijski parametri razlikuju od podataka po tome što se općenito mijenjaju rjeđe od podataka. Takođe, podaci obično dolaze iz jednog izvora (od korisnika), a konfiguracioni parametri iz drugog (od administratora sistema).

Ako je potrebno ažurirati parametre koji se rijetko mijenjaju bez ponovnog pokretanja programa, onda to često može dovesti do komplikacije programa, jer ćemo morati nekako isporučiti parametre, pohraniti, raščlaniti i provjeriti i obraditi pogrešne vrijednosti. Stoga, sa stanovišta smanjenja složenosti programa, ima smisla smanjiti broj parametara koji se mogu promijeniti tokom rada programa (ili ne podržavaju takve parametre uopće).

Za potrebe ovog posta, napravićemo razliku između statičkih i dinamičkih parametara. Ako logika usluge zahtijeva promjenu parametara tokom rada programa, tada ćemo takve parametre nazvati dinamičkim. Inače su opcije statične i mogu se konfigurirati pomoću kompajlirane konfiguracije. Za dinamičku rekonfiguraciju, možda će nam trebati mehanizam za ponovno pokretanje dijelova programa s novim parametrima, slično kako se procesi operativnog sistema ponovo pokreću. (Po našem mišljenju, preporučljivo je izbjegavati rekonfiguraciju u realnom vremenu, jer to povećava složenost sistema. Ako je moguće, bolje je koristiti standardne mogućnosti OS-a za ponovno pokretanje procesa.)

Jedan važan aspekt upotrebe statičke konfiguracije koji tjera ljude da razmisle o dinamičkoj rekonfiguraciji je vrijeme koje je potrebno da se sistem ponovo pokrene nakon ažuriranja konfiguracije (zastoja). U stvari, ako trebamo unijeti promjene u statičku konfiguraciju, morat ćemo ponovo pokrenuti sistem da bi nove vrijednosti stupile na snagu. Problem zastoja varira po ozbiljnosti za različite sisteme. U nekim slučajevima možete zakazati ponovno pokretanje u vrijeme kada je opterećenje minimalno. Ako trebate pružati kontinuiranu uslugu, možete implementirati Pražnjenje AWS ELB veze. U isto vrijeme, kada trebamo ponovo pokrenuti sistem, pokrećemo paralelnu instancu ovog sistema, prebacimo balanser na njega i čekamo da se stare veze završe. Nakon što su sve stare veze prekinute, ugasili smo staru instancu sistema.

Razmotrimo sada pitanje pohranjivanja konfiguracije unutar ili izvan artefakta. Ako pohranimo konfiguraciju unutar artefakta, onda smo barem imali priliku provjeriti ispravnost konfiguracije tokom sklapanja artefakta. Ako je konfiguracija izvan kontrolisanog artefakta, teško je pratiti ko je napravio promjene u ovoj datoteci i zašto. Koliko je to važno? Po našem mišljenju, za mnoge proizvodne sisteme važno je imati stabilnu i kvalitetnu konfiguraciju.

Verzija artefakta vam omogućava da odredite kada je kreiran, koje vrijednosti sadrži, koje funkcije su omogućene/onemogućene i tko je odgovoran za bilo kakvu promjenu u konfiguraciji. Naravno, pohranjivanje konfiguracije unutar artefakta zahtijeva određeni napor, tako da morate donijeti informiranu odluku.

Za i protiv

Želio bih se zadržati na prednostima i nedostacima predložene tehnologije.

Prednosti

Ispod je lista glavnih karakteristika kompajlirane konfiguracije distribuiranog sistema:

  1. Provjera statičke konfiguracije. Omogućava vam da budete sigurni u to
    konfiguracija je ispravna.
  2. Bogat konfiguracijski jezik. Tipično, druge metode konfiguracije su ograničene najviše na zamjenu varijabli niza. Kada koristite Scalu, dostupan je širok raspon jezičnih funkcija za poboljšanje vaše konfiguracije. Na primjer, možemo koristiti
    osobine za zadane vrijednosti, koristeći objekte za grupiranje parametara, možemo se pozvati na vrijednosti deklarirane samo jednom (DRY) u obuhvatnom opsegu. Možete instancirati bilo koju klasu direktno unutar konfiguracije (Seq, Map, prilagođene klase).
  3. DSL. Scala ima brojne jezičke karakteristike koje olakšavaju kreiranje DSL-a. Moguće je iskoristiti prednosti ovih mogućnosti i implementirati konfiguracijski jezik koji je pogodniji za ciljnu grupu korisnika, tako da konfiguracija bude barem čitljiva od strane stručnjaka domene. Stručnjaci mogu, na primjer, sudjelovati u procesu pregleda konfiguracije.
  4. Integritet i sinhronizacija između čvorova. Jedna od prednosti pohranjivanja konfiguracije cijelog distribuiranog sistema u jednu tačku je da se sve vrijednosti deklariraju tačno jednom i zatim ponovo koriste gdje god su potrebne. Korištenje fantomskih tipova za deklariranje portova osigurava da čvorovi koriste kompatibilne protokole u svim ispravnim sistemskim konfiguracijama. Eksplicitne obavezne zavisnosti između čvorova osiguravaju da su sve usluge povezane.
  5. Visokokvalitetne promjene. Izmjena konfiguracije korištenjem zajedničkog procesa razvoja omogućava postizanje visokih standarda kvaliteta i za konfiguraciju.
  6. Istovremeno ažuriranje konfiguracije. Automatsko postavljanje sistema nakon promjena konfiguracije osigurava da su svi čvorovi ažurirani.
  7. Pojednostavljivanje aplikacije. Aplikaciji nije potrebno raščlanjivanje, provjeru konfiguracije ili rukovanje netočnim vrijednostima. Ovo smanjuje složenost aplikacije. (Neki od složenosti konfiguracije uočene u našem primjeru nisu atribut kompajlirane konfiguracije, već samo svjesna odluka vođena željom da se obezbijedi veća sigurnost tipa.) Prilično je lako vratiti se na uobičajenu konfiguraciju - samo implementirajte nedostajuće dijelovi. Stoga, možete, na primjer, početi s kompajliranom konfiguracijom, odgađajući implementaciju nepotrebnih dijelova do trenutka kada je to zaista potrebno.
  8. Verificirana konfiguracija. Budući da promjene konfiguracije slijede uobičajenu sudbinu svih drugih promjena, izlaz koji dobijamo je artefakt s jedinstvenom verzijom. Ovo nam omogućava, na primjer, da se vratimo na prethodnu verziju konfiguracije ako je potrebno. Možemo čak koristiti i konfiguraciju od prije godinu dana i sistem će raditi potpuno isto. Stabilna konfiguracija poboljšava predvidljivost i pouzdanost distribuiranog sistema. Budući da je konfiguracija fiksirana u fazi kompilacije, prilično ju je teško lažirati u proizvodnji.
  9. Modularnost. Predloženi okvir je modularan i moduli se mogu kombinovati na različite načine za kreiranje različitih sistema. Konkretno, možete konfigurirati sistem da radi na jednom čvoru u jednoj izvedbi, a na više čvorova u drugoj. Možete kreirati nekoliko konfiguracija za proizvodne instance sistema.
  10. Testiranje. Zamjenom pojedinačnih servisa lažnim objektima možete dobiti nekoliko verzija sistema koje su pogodne za testiranje.
  11. Integracijsko testiranje. Posjedovanje jedinstvene konfiguracije za cijeli distribuirani sistem omogućava pokretanje svih komponenti u kontroliranom okruženju kao dio testiranja integracije. Lako je emulirati, na primjer, situaciju u kojoj neki čvorovi postaju dostupni.

Nedostaci i ograničenja

Prevedena konfiguracija se razlikuje od drugih pristupa konfiguraciji i možda neće biti prikladna za neke aplikacije. Ispod su neki nedostaci:

  1. Statička konfiguracija. Ponekad morate brzo ispraviti konfiguraciju u proizvodnji, zaobilazeći sve zaštitne mehanizme. Sa ovim pristupom može biti teže. U najmanju ruku, kompilacija i automatska implementacija će i dalje biti potrebni. Ovo je i korisna karakteristika pristupa i mana u nekim slučajevima.
  2. Generisanje konfiguracije. U slučaju da je konfiguracijski fajl generiran automatskim alatom, možda će biti potrebni dodatni napori da se integrira skripta za izgradnju.
  3. Alati. Trenutno su uslužni programi i tehnike dizajnirani za rad sa konfiguracijom zasnovani na tekstualnim datotekama. Neće svi takvi uslužni programi/tehnike biti dostupni u kompajliranoj konfiguraciji.
  4. Potrebna je promjena u stavovima. Programeri i DevOps navikli su na tekstualne datoteke. Sama ideja sastavljanja konfiguracije može biti pomalo neočekivana i neobična i uzrokovati odbijanje.
  5. Potreban je visokokvalitetan razvojni proces. Za udobno korištenje kompajlirane konfiguracije neophodna je potpuna automatizacija procesa izgradnje i postavljanja aplikacije (CI/CD). U suprotnom će biti prilično nezgodno.

Zadržimo se i na brojnim ograničenjima razmatranog primjera koja nisu povezana s idejom kompajlirane konfiguracije:

  1. Ako pružimo nepotrebne informacije o konfiguraciji koje čvor ne koristi, onda nam kompajler neće pomoći da otkrijemo implementaciju koja nedostaje. Ovaj problem se može riješiti napuštanjem uzorka kolača i korištenjem čvršćih tipova, na primjer, HList ili algebarski tipovi podataka (klase slučajeva) za predstavljanje konfiguracije.
  2. U konfiguracijskoj datoteci postoje linije koje nisu povezane sa samom konfiguracijom: (package, import,deklaracije objekata; override def's za parametre koji imaju zadane vrijednosti). Ovo se može djelomično izbjeći ako implementirate vlastiti DSL. Osim toga, druge vrste konfiguracije (na primjer, XML) također nameću određena ograničenja na strukturu datoteke.
  3. Za potrebe ovog posta, ne razmatramo dinamičku rekonfiguraciju klastera sličnih čvorova.

zaključak

U ovom postu smo istražili ideju predstavljanja konfiguracije u izvornom kodu koristeći napredne mogućnosti sistema tipa Scala. Ovaj pristup se može koristiti u raznim aplikacijama kao zamjena za tradicionalne metode konfiguracije zasnovane na xml ili tekstualnim datotekama. Iako je naš primjer implementiran u Scali, iste ideje se mogu prenijeti na druge kompajlirane jezike (kao što su Kotlin, C#, Swift,...). Ovaj pristup možete isprobati u jednom od sljedećih projekata i, ako ne uspije, prijeđite na tekstualnu datoteku, dodajući dijelove koji nedostaju.

Naravno, kompajlirana konfiguracija zahtijeva visokokvalitetan razvojni proces. Zauzvrat je osiguran visok kvalitet i pouzdanost konfiguracija.

Razmatrani pristup se može proširiti:

  1. Možete koristiti makroe za obavljanje provjera vremena kompajliranja.
  2. Možete implementirati DSL da predstavite konfiguraciju na način koji je dostupan krajnjim korisnicima.
  3. Možete implementirati dinamičko upravljanje resursima sa automatskim podešavanjem konfiguracije. Na primjer, promjena broja čvorova u klasteru zahtijeva da (1) svaki čvor dobije malo drugačiju konfiguraciju; (2) menadžer klastera je primio informacije o novim čvorovima.

Zahvalnice

Želeo bih da se zahvalim Andreju Saksonovu, Pavelu Popovu i Antonu Nehajevu na njihovoj konstruktivnoj kritici nacrta članka.

izvor: www.habr.com

Dodajte komentar