Sastavljena konfiguracija distribuiranog sustava

Želio bih vam reći jedan zanimljiv mehanizam za rad s konfiguracijom distribuiranog sustava. Konfiguracija je predstavljena izravno u prevedenom jeziku (Scala) korištenjem sigurnih tipova. Ovaj post daje primjer takve konfiguracije i raspravlja o različitim aspektima implementacije kompajlirane konfiguracije u cjelokupni proces razvoja.

Sastavljena konfiguracija distribuiranog sustava

(Engleski)

Uvod

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

Što je s testiranjem distribuiranog sustava? Naravno, pretpostavljamo da sve komponente imaju jedinične testove prije nego što prijeđemo na integracijsko testiranje. (Kako bismo mogli ekstrapolirati rezultate testa na vrijeme izvođenja, također moramo osigurati identičan skup biblioteka u fazi testiranja i za vrijeme izvođenja.)

Kada radite s integracijskim testovima, često je lakše koristiti isti classpath posvuda na svim čvorovima. Sve što trebamo učiniti je osigurati da se ista staza klase koristi za vrijeme izvođenja. (Iako je potpuno moguće pokretati različite čvorove s različitim stazama klasa, to 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 klasa.

Konfiguracija se razvija s aplikacijom. Koristimo verzije za prepoznavanje različitih faza evolucije programa. Čini se logičnim identificirati i različite verzije konfiguracija. I smjestite samu konfiguraciju u sustav kontrole verzija. Ako postoji samo jedna konfiguracija u proizvodnji, tada možemo jednostavno koristiti broj verzije. Ako koristimo mnogo proizvodnih instanci, trebat će nam nekoliko
konfiguracijske grane i dodatnu oznaku uz verziju (na primjer, naziv grane). Na taj način možemo jasno identificirati točnu konfiguraciju. Svaki identifikator konfiguracije jedinstveno odgovara specifičnoj kombinaciji distribuiranih čvorova, portova, vanjskih resursa i verzija knjižnice. Za potrebe ovog posta pretpostavit ćemo da postoji samo jedna grana i možemo identificirati konfiguraciju na uobičajeni način koristeći tri broja odvojena točkom (1.2.3).

U modernim okruženjima konfiguracijske datoteke rijetko se izrađuju ručno. Češće se generiraju tijekom postavljanja i više se ne dodiruju (tako da nemoj ništa slomiti). Postavlja se prirodno pitanje: zašto još uvijek koristimo tekstualni format za pohranu konfiguracije? Čini se da je održiva alternativa mogućnost korištenja uobičajenog koda za konfiguraciju i iskorištavanje prednosti provjera tijekom kompajliranja.

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

Sastavljena konfiguracija

Ovaj odjeljak daje primjer statičke kompajlirane konfiguracije. Implementirane su dvije jednostavne usluge - usluga echo i klijent usluge echo. Na temelju ove dvije usluge sastavljaju se dvije opcije sustava. U jednoj opciji, obje usluge nalaze se na istom čvoru, u drugoj opciji - na različitim čvorovima.

Distribuirani sustav obično sadrži nekoliko čvorova. Čvorove možete identificirati pomoću vrijednosti neke vrste 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 između njih se mogu uspostaviti TCP/HTTP veze.

Za opis TCP veze potreban nam je barem broj porta. Također bismo željeli prikazati protokol koji je podržan na tom priključku kako bismo osigurali da i klijent i poslužitelj koriste isti protokol. Opisat ćemo vezu pomoću sljedeće klase:

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

gdje Port - samo cijeli broj Int koji označava raspon prihvatljivih vrijednosti:

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

Pročišćene vrste

Vidi knjižnicu rafiniran и moj izvješće. Ukratko, biblioteka vam omogućuje dodavanje ograničenja tipovima koji se provjeravaju tijekom kompajliranja. U ovom slučaju, važeće vrijednosti broja porta su 16-bitni cijeli brojevi. Za prevedenu konfiguraciju, korištenje pročišćene biblioteke nije obavezno, ali poboljšava sposobnost prevoditelja da provjeri konfiguraciju.

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

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

Tipovi fantoma

Za identifikaciju protokola tijekom kompajliranja koristimo parametar tipa koji se ne koristi unutar klase. Ova odluka je zbog činjenice da ne koristimo instancu protokola tijekom izvođenja, ali bismo željeli da prevodilac provjeri kompatibilnost protokola. Određivanjem protokola nećemo moći proslijediti neodgovarajuću uslugu kao ovisnost.

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

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

gdje RequestMessage - vrstu zahtjeva, ResponseMessage — vrsta odgovora.
Naravno, možemo koristiti druge opise protokola koji pružaju točnost opisa koju zahtijevamo.

Za potrebe ovog posta koristit ćemo pojednostavljenu verziju protokola:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Ovdje je zahtjev niz pridodan url-u, a odgovor je vraćeni niz u tijelu HTTP odgovora.

Konfiguracija usluge opisana je nazivom usluge, priključcima 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 obrazac za kolače i predstavljati module pomoću njih trait'ov. (Uzorak kolača nije obavezan element ovog pristupa. To je jednostavno jedna moguća implementacija.)

Ovisnosti između usluga mogu se predstaviti kao metode koje vraćaju portove EndPointdrugih č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)
  }

Za izradu usluge echo sve što trebate je broj priključka i naznaka da priključak podržava protokol echo. Možda nećemo navesti određeni priključak jer... osobine vam omogućuju da deklarirate metode bez implementacije (apstraktne metode). U ovom slučaju, prilikom stvaranja konkretne konfiguracije, prevodilac bi od nas zahtijevao implementaciju apstraktne metode i broj porta. Budući da smo implementirali metodu, prilikom izrade određene konfiguracije možda nećemo navesti drugi port. Koristit će se zadana vrijednost.

U konfiguraciji klijenta deklariramo ovisnost o usluzi echo:

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

Ovisnost je iste vrste kao i izvezena usluga echoService. Konkretno, u echo klijentu zahtijevamo isti protokol. Stoga, kada povezujemo dvije usluge, možemo biti sigurni da će sve raditi ispravno.

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 značajke (na primjer, mogli bismo koristiti klase tipa na temelju tipa konfiguracije). Za potrebe ovog posta koristit ćemo obrazac za kolače. Uslugu ćemo predstaviti pomoću klase cats.Resource, jer Ova klasa već nudi sredstva za sigurno jamčenje oslobađanja resursa u slučaju problema. Da bismo dobili resurs, moramo osigurati konfiguraciju i gotov kontekst vremena 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 — vrsta konfiguracije za ovu uslugu
  • AddressResolver — runtime objekt koji vam omogućuje da saznate adrese drugih čvorova (vidi dolje)

i druge vrste iz knjižnice cats:

  • F[_] — vrsta učinka (u najjednostavnijem slučaju F[A] može biti samo funkcija () => A. U ovom ćemo postu koristiti cats.IO.)
  • Reader[A,B] - više ili manje sinonim za funkciju A => B
  • cats.Resource - resurs koji se može dobiti i osloboditi
  • Timer — mjerač vremena (omogućuje vam da nakratko zaspite i mjerite vremenske intervale)
  • ContextShift - analogni ExecutionContext
  • Applicative — klasa tipa efekta koja vam omogućuje kombiniranje pojedinačnih efekata (gotovo monada). U složenijim aplikacijama čini se da je bolje koristiti Monad/ConcurrentEffect.

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

(Cm. izvor, u kojem su implementirane druge usluge - usluga odjeka, echo klijent
и doživotni regulatori.)

Čvor je objekt koji može pokrenuti nekoliko usluga (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 točnu vrstu konfiguracije koja je potrebna za ovaj čvor. Ako zaboravimo navesti jedan od tipova konfiguracije koje zahtijeva određena usluga, doći će do pogreške kompilacije. Također, nećemo moći pokrenuti čvor ako ne osiguramo neki objekt odgovarajućeg tipa sa svim potrebnim podacima.

Razrješenje imena računala

Za povezivanje s udaljenim hostom potrebna nam je prava IP adresa. Moguće je da će adresa postati poznata kasnije od ostatka konfiguracije. Dakle, trebamo funkciju koja preslikava 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, tada možemo generirati Scala kod s
    adrese i zatim pokrenite izgradnju. Ovo će kompilirati i pokrenuti testove.
    U ovom slučaju, funkcija će biti statički poznata i može se predstaviti u kodu kao preslikavanje Map[NodeId, NodeAddress].
  2. U nekim slučajevima, stvarna adresa je poznata tek nakon pokretanja čvora.
    U ovom slučaju, možemo implementirati "uslugu otkrivanja" koja se pokreće prije drugih čvorova i svi će se čvorovi registrirati s ovom uslugom i tražiti adrese drugih čvorova.
  3. Ako možemo modificirati /etc/hosts, tada možete koristiti unaprijed definirana imena hostova (kao my-project-main-node и echo-backend) i jednostavno povežite ta imena
    s IP adresama tijekom postavljanja.

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

Zatim ćemo razmotriti dvije opcije za distribuirani sustav:

  1. Postavljanje svih usluga 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 poslužitelja. Također se koristi konfiguracija vremena života tako da nakon intervala lifetime prekinuti program. (Ctrl-C također radi i oslobađa sve resurse ispravno.)

Isti skup značajki konfiguracije i implementacije može se koristiti za stvaranje sustava 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"
  }

Važno! Primijetite kako su usluge povezane. Specificiramo uslugu koju implementira jedan čvor kao implementaciju metode ovisnosti drugog čvora. Tip ovisnosti provjerava kompajler, jer sadrži tip protokola. Kada se pokrene, ovisnost će sadržavati točan ID ciljnog čvora. Zahvaljujući ovoj shemi, broj porta navodimo točno jednom i uvijek se jamči da se odnosi na točan port.

Implementacija dva čvora sustava

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 poslužitelj i treba samo konfiguraciju poslužitelja. Drugi čvor implementira klijenta i koristi drugačiji dio konfiguracije. Također oba čvora trebaju doživotno upravljanje. Čvor poslužitelja radi neograničeno dok se ne zaustavi SIGTERM'om, a čvor klijenta završava nakon nekog vremena. Cm. aplikacija za pokretanje.

Opći proces razvoja

Pogledajmo kako ovaj konfiguracijski pristup utječe na ukupni proces razvoja.

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

Svaka promjena konfiguracije pretvara se u promjenu koda. I stoga, svaki
promjena će biti obuhvaćena uobičajenim procesom osiguranja kvalitete:

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

Glavne posljedice implementacije kompajlirane konfiguracije su:

  1. Konfiguracija će biti dosljedna u svim čvorovima distribuiranog sustava. Zbog činjenice da svi čvorovi primaju istu konfiguraciju iz jednog izvora.

  2. Problematično je promijeniti konfiguraciju samo u jednom od čvorova. Stoga je "konfiguracijski pomak" malo vjerojatan.

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

  4. Većina promjena konfiguracije dogodit će se kao dio cjelokupnog razvojnog procesa i bit će podložne reviziji.

Trebam li zasebno spremište za pohranu proizvodne konfiguracije? Ova konfiguracija može sadržavati lozinke i druge osjetljive informacije kojima želimo ograničiti pristup. Na temelju toga, čini se da ima smisla pohraniti konačnu konfiguraciju u zasebno spremište. Konfiguraciju možete podijeliti u dva dijela — jedan koji sadrži javno dostupne konfiguracijske postavke i jedan koji sadrži ograničene postavke. To će većini programera omogućiti pristup zajedničkim postavkama. Ovo odvajanje je lako postići korištenjem srednjih obilježja koja sadrže zadane vrijednosti.

Moguće varijacije

Pokušajmo usporediti kompiliranu konfiguraciju s nekim uobičajenim alternativama:

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

Tekstualne datoteke pružaju značajnu fleksibilnost u pogledu malih promjena. Administrator sustava može se prijaviti na udaljeni čvor, unijeti izmjene u odgovarajuće datoteke i ponovno pokrenuti uslugu. Za velike sustave, međutim, takva fleksibilnost možda nije poželjna. Napravljene promjene ne ostavljaju tragove u drugim sustavima. Nitko ne pregledava promjene. Teško je utvrditi tko je točno napravio promjene i iz kojeg razloga. Promjene se ne testiraju. Ako je sustav distribuiran, tada administrator može zaboraviti napraviti 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. Bit ć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 sustava s kompajliranom konfiguracijom nešto manja od složenosti sustava koji koristi tekstualne datoteke, jer tekstualne datoteke zahtijevaju dodatni kod.)

Centralizirana pohrana ključeva i vrijednosti dobar je mehanizam za distribuciju meta parametara distribuirane aplikacije. Moramo odlučiti što su konfiguracijski parametri, a što samo podaci. Neka nam bude funkcija C => A => B, i parametri C rijetko mijenja, a podaci A - često. U ovom slučaju to možemo reći C - konfiguracijski parametri, i A - podaci. Čini se da se konfiguracijski parametri razlikuju od podataka po tome što se općenito mijenjaju rjeđe od podataka. Također, podaci najčešće dolaze iz jednog izvora (od korisnika), a konfiguracijski parametri iz drugog (od administratora sustava).

Ako parametre koji se rijetko mijenjaju treba ažurirati bez ponovnog pokretanja programa, to često može dovesti do kompliciranja programa, jer ćemo morati nekako isporučiti parametre, pohraniti, analizirati i provjeriti te obraditi netočne vrijednosti. Stoga, sa stajališta smanjenja složenosti programa, ima smisla smanjiti broj parametara koji se mogu mijenjati tijekom rada programa (ili uopće ne podržavati takve parametre).

Za potrebe ovog posta, razlikovat ćemo statičke i dinamičke parametre. Ako logika usluge zahtijeva promjenu parametara tijekom 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 ćemo trebati mehanizam za ponovno pokretanje dijelova programa s novim parametrima, slično kao što se procesi operacijskog sustava ponovno pokreću. (Prema našem mišljenju, preporučljivo je izbjegavati rekonfiguraciju u stvarnom vremenu, budući da to povećava složenost sustava. Ako je moguće, bolje je koristiti standardne OS mogućnosti za ponovno pokretanje procesa.)

Jedan važan aspekt korištenja statičke konfiguracije zbog kojeg ljudi razmišljaju o dinamičkoj rekonfiguraciji je vrijeme koje je potrebno da se sustav ponovno pokrene nakon ažuriranja konfiguracije (prekid rada). Zapravo, ako trebamo promijeniti statičku konfiguraciju, morat ćemo ponovno pokrenuti sustav kako bi nove vrijednosti stupile na snagu. Problem prekida rada razlikuje se u ozbiljnosti za različite sustave. U nekim slučajevima možete zakazati ponovno pokretanje u vrijeme kada je opterećenje minimalno. Ako trebate pružiti kontinuiranu uslugu, možete implementirati Pražnjenje veze AWS ELB. U isto vrijeme, kada trebamo ponovno pokrenuti sustav, pokrećemo paralelnu instancu ovog sustava, prebacujemo balanser na nju i čekamo da se stare veze završe. Nakon što su sve stare veze prekinute, gasimo staru instancu sustava.

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

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

Pro i kontra

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

Prednosti

Dolje je popis glavnih značajki kompajlirane konfiguracije distribuiranog sustava:

  1. Provjera statičke konfiguracije. Omogućuje vam da budete sigurni da
    konfiguracija je ispravna.
  2. Bogat konfiguracijski jezik. Tipično, druge konfiguracijske metode ograničene su najviše na zamjenu varijabli niza. Kada koristite Scala, dostupan je širok raspon jezičnih značajki za poboljšanje vaše konfiguracije. Na primjer, možemo koristiti
    značajke za zadane vrijednosti, koristeći objekte za grupiranje parametara, možemo se pozvati na valove deklarirane samo jednom (DRY) u priloženom opsegu. Možete instancirati bilo koju klasu izravno unutar konfiguracije (Seq, Map, prilagođene klase).
  3. DSL. Scala ima niz jezičnih značajki koje olakšavaju stvaranje DSL-a. Moguće je iskoristiti prednosti ovih značajki i implementirati konfiguracijski jezik koji je prikladniji za ciljnu skupinu korisnika, tako da konfiguraciju barem mogu pročitati stručnjaci za domenu. Stručnjaci mogu, primjerice, sudjelovati u procesu pregleda konfiguracije.
  4. Integritet i sinkronija između čvorova. Jedna od prednosti pohranjivanja konfiguracije cijelog distribuiranog sustava u jednoj točki je da se sve vrijednosti deklariraju točno jednom i zatim ponovno koriste gdje god su potrebne. Korištenje tipova fantoma za deklariranje portova osigurava da čvorovi koriste kompatibilne protokole u svim ispravnim konfiguracijama sustava. Postojanje eksplicitnih obveznih ovisnosti između čvorova osigurava da su sve usluge povezane.
  5. Promjene visoke kvalitete. Izrada promjena u konfiguraciji korištenjem zajedničkog razvojnog procesa također omogućuje postizanje visokih standarda kvalitete za konfiguraciju.
  6. Istodobno ažuriranje konfiguracije. Automatsko postavljanje sustava nakon promjena konfiguracije osigurava da su svi čvorovi ažurirani.
  7. Pojednostavljivanje aplikacije. Aplikaciji nije potrebno analiziranje, provjera konfiguracije ili rukovanje netočnim vrijednostima. Time se smanjuje složenost aplikacije. (Neke složenosti konfiguracije opažene u našem primjeru nisu atribut kompajlirane konfiguracije, već samo svjesna odluka vođena željom da se pruži veća sigurnost tipa.) Vrlo je lako vratiti se na uobičajenu konfiguraciju - samo implementirajte nedostajuće dijelovi. Stoga možete, na primjer, započeti s kompajliranom konfiguracijom, odgađajući implementaciju nepotrebnih dijelova do trenutka kada su stvarno potrebni.
  8. Verificirana konfiguracija. Budući da promjene konfiguracije slijede uobičajenu sudbinu svih drugih promjena, rezultat koji dobivamo je artefakt s jedinstvenom verzijom. To nam omogućuje, na primjer, povratak na prethodnu verziju konfiguracije ako je potrebno. Možemo čak koristiti konfiguraciju od prije godinu dana i sustav će raditi potpuno isto. Stabilna konfiguracija poboljšava predvidljivost i pouzdanost distribuiranog sustava. 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 kombinirati na različite načine za stvaranje različitih sustava. Konkretno, možete konfigurirati sustav da radi na jednom čvoru u jednoj izvedbi, i na više čvorova u drugoj. Možete kreirati nekoliko konfiguracija za proizvodne instance sustava.
  10. Testiranje. Zamjenom pojedinačnih usluga lažnim objektima možete dobiti nekoliko verzija sustava koje su prikladne za testiranje.
  11. Integracijsko testiranje. Posjedovanje jedinstvene konfiguracije za cijeli distribuirani sustav omogućuje 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 razlikuje se od drugih konfiguracijskih pristupa i možda neće biti prikladna za neke aplikacije. U nastavku su navedeni neki nedostaci:

  1. Statička konfiguracija. Ponekad morate brzo ispraviti konfiguraciju u proizvodnji, zaobilazeći sve zaštitne mehanizme. S ovim pristupom može biti teže. U najmanju ruku, i dalje će biti potrebna kompilacija i automatska implementacija. To je i korisna značajka pristupa i nedostatak u nekim slučajevima.
  2. Generiranje konfiguracije. U slučaju da konfiguracijsku datoteku generira automatski alat, možda će biti potrebni dodatni napori za integraciju skripte za izgradnju.
  3. Alati. Trenutno se uslužni programi i tehnike dizajnirane za rad s konfiguracijom temelje na tekstualnim datotekama. Neće svi takvi pomoćni programi/tehnike biti dostupni u kompajliranoj konfiguraciji.
  4. Potrebna je promjena stavova. Programeri i DevOps navikli su na tekstualne datoteke. Sama ideja sastavljanja konfiguracije može biti pomalo neočekivana i neuobičajena i izazvati odbijanje.
  5. Potreban je visokokvalitetan proces razvoja. Za udobno korištenje kompajlirane konfiguracije potrebna je potpuna automatizacija procesa izgradnje i postavljanja aplikacije (CI/CD). Inače će biti prilično nezgodno.

Zaustavimo 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, tada nam prevodilac neće pomoći u otkrivanju implementacije koja nedostaje. Ovaj se problem može riješiti napuštanjem Cake Pattern-a i korištenjem čvršćih tipova, na primjer, HList ili algebarski tipovi podataka (klase slučajeva) za predstavljanje konfiguracije.
  2. Postoje linije u konfiguracijskoj datoteci koje nisu povezane sa samom konfiguracijom: (package, import,deklaracije objekata; override defza 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 smo postu istražili ideju predstavljanja konfiguracije u izvornom kodu koristeći napredne mogućnosti sustava tipa Scala. Ovaj se pristup može koristiti u raznim aplikacijama kao zamjena za tradicionalne konfiguracijske metode temeljene 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, ...). Možete isprobati ovaj pristup u jednom od sljedećih projekata, a ako ne uspije, prijeđite na tekstualnu datoteku, dodajući dijelove koji nedostaju.

Naravno, kompilirana konfiguracija zahtijeva visokokvalitetni proces razvoja. Zauzvrat je osigurana visoka kvaliteta i pouzdanost konfiguracija.

Razmatrani pristup može se proširiti:

  1. Možete koristiti makronaredbe za izvođenje provjera tijekom 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 s automatskim podešavanjem konfiguracije. Na primjer, promjena broja čvorova u klasteru zahtijeva da (1) svaki čvor dobije malo drugačiju konfiguraciju; (2) upravitelj klastera primio je informacije o novim čvorovima.

Blagodarnosti

Želio bih zahvaliti Andreju Saksonovu, Pavelu Popovu i Antonu Nekhaevu na njihovoj konstruktivnoj kritici nacrta članka.

Izvor: www.habr.com

Dodajte komentar