Cunfigurazione di u Sistema Distribuitu Cumpilatu

Vogliu dì à voi un mecanismu interessante per travaglià cù a cunfigurazione di un sistema distribuitu. A cunfigurazione hè rapprisintata direttamente in una lingua cumpilata (Scala) cù tippi sicuri. Questu post furnisce un esempiu di tale cunfigurazione è discute diversi aspetti di implementà una cunfigurazione compilata in u prucessu di sviluppu generale.

Cunfigurazione di u Sistema Distribuitu Cumpilatu

(Inglese)

Introduzione

Custruì un sistema distribuitu affidabile significa chì tutti i nodi utilizanu a cunfigurazione curretta, sincronizata cù altri nodi. Tecnulugie DevOps (terraform, ansible o qualcosa di simile) sò generalmente aduprate per generà automaticamente i schedarii di cunfigurazione (spessu specifichi per ogni node). Vuleriamu ancu esse sicuri chì tutti i nodi di cumunicazione utilizanu protokolli idèntici (cumpresa a stessa versione). Altrimenti, l'incompatibilità serà integrata in u nostru sistema distribuitu. In u mondu JVM, una cunsequenza di questu requisitu hè chì a stessa versione di a biblioteca chì cuntene i missaghji di protokollu deve esse usata in ogni locu.

Chì ci hè di pruvà un sistema distribuitu? Di sicuru, assumemu chì tutti i cumpunenti anu teste unità prima di passà à a prova di integrazione. (Per noi per estrapolà i risultati di teste à runtime, duvemu ancu furnisce un inseme identicu di biblioteche in a fase di prova è in runtime.)

Quandu travaglia cù testi di integrazione, hè spessu più faciule d'utilizà u stessu classpath in ogni locu in tutti i nodi. Tuttu ciò chì avemu da fà hè di assicurà chì u stessu classpath hè utilizatu in runtime. (Mentre hè interamente pussibule di eseguisce diversi nodi cù diversi classpaths, questu aghjunghje cumplessità à a cunfigurazione generale è difficultà cù e teste di implementazione è integrazione).

A cunfigurazione evoluzione cù l'applicazione. Utilizemu versioni per identificà e diverse tappe di l'evoluzione di u prugramma. Sembra logicu per identificà ancu diverse versioni di cunfigurazioni. E postu a cunfigurazione stessu in u sistema di cuntrollu di versione. Se ci hè solu una cunfigurazione in a pruduzzione, allora pudemu solu aduprà u numeru di versione. Sè avemu aduprà parechji casi pruduzzione, allura avemu bisognu di parechji
rami di cunfigurazione è una etichetta supplementu in più di a versione (per esempiu, u nome di u ramu). Questu modu pudemu identificà chjaramente a cunfigurazione esatta. Ogni identificatore di cunfigurazione currisponde unicu à una cumminazione specifica di nodi distribuiti, porti, risorse esterne è versioni di biblioteca. Per i scopi di questu post, assumeremu chì ci hè solu un ramu è pudemu identificà a cunfigurazione in u modu di solitu utilizendu trè numeri separati da un puntu (1.2.3).

In ambienti muderni, i schedarii di cunfigurazione raramente sò creati manualmente. Più spessu sò generati durante a implementazione è ùn sò più toccu (per quessa ùn rompe nunda). Ci hè una quistione naturale: perchè avemu sempre aduprà u furmatu di testu per almacenà a cunfigurazione? Una alternativa viable pare esse a capacità di utilizà codice regulare per a cunfigurazione è prufittà di cuntrolli di compilazione.

In questu post esploreremu l'idea di rapprisintà una cunfigurazione in un artefattu compilatu.

Cunfigurazione cumpilata

Questa sezione furnisce un esempiu di una cunfigurazione compilata statica. Dui servizii simplici sò implementati - u serviziu di eco è u cliente di serviziu di eco. Basatu nantu à sti dui servizii, duie opzioni di sistema sò assemblate. In una opzione, i dui servizii sò situati nantu à u stessu node, in una altra opzione - in diversi nodi.

Di genere, un sistema distribuitu cuntene parechji nodi. Pudete identificà i nodi usendu valori di qualchì tipu NodeId:

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

o

case class NodeId(hostName: String)

o ancu

object Singleton
type NodeId = Singleton.type

I nodi facenu diversi roli, eseguinu servizii è e cunnessione TCP / HTTP ponu esse stabiliti trà elli.

Per discrive una cunnessione TCP avemu bisognu di almenu un numeru di portu. Vulemu ancu riflette u protocolu chì hè supportatu in quellu portu per assicurà chì u cliente è u servitore utilizanu u stessu protokollu. Descriveremu a cunnessione cù a seguente classa:

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

induve Port - solu un interu Int indicà a gamma di valori accettabili:

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

Tipi raffinati

Vede a biblioteca raffinati и mi rapportu. In corta, a biblioteca permette di aghjunghje limitazioni à i tipi chì sò verificati in tempu di compilazione. In questu casu, i valori validi di u numeru di portu sò interi 16-bit. Per una cunfigurazione compilata, l'usu di a biblioteca raffinata ùn hè micca ubligatoriu, ma migliurà a capacità di u compilatore per verificà a cunfigurazione.

Per i protocolli HTTP (REST), in più di u numeru di portu, pudemu ancu avè bisognu di a strada per u serviziu:

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

Tipi di fantasma

Per identificà u protokollu à u tempu di compilazione, usemu un paràmetru di tipu chì ùn hè micca usatu in a classe. Sta decisione hè duvuta à u fattu chì ùn avemu micca aduprà una istanza di protokollu in runtime, ma vulemu chì u compilatore verifica a cumpatibilità di u protokollu. Specificendu u protocolu, ùn pudemu micca passà un serviziu inappropriatu cum'è una dependenza.

Unu di i protokolli cumuni hè l'API REST cù serializazione Json:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

induve RequestMessage - tipu di dumanda, ResponseMessage - tipu di risposta.
Di sicuru, pudemu usà altre descrizzioni di protokollu chì furnisce l'accuratezza di descrizzione chì avemu bisognu.

Per i scopi di stu post, useremu una versione simplificata di u protocolu:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Quì a dumanda hè una stringa appiccicata à l'url è a risposta hè a stringa restituita in u corpu di a risposta HTTP.

A cunfigurazione di u serviziu hè descritta da u nome di u serviziu, i porti è e dipendenze. Questi elementi ponu esse rapprisintati in Scala in parechje manere (per esempiu, HList-s, tipi di dati algebrici). Per i scopi di stu post, useremu u Cake Pattern è rapprisentanu moduli chì utilizanu trait'ov. (U Cake Pattern ùn hè micca un elementu necessariu di questu approcciu. Hè solu una implementazione pussibule.)

Dipendenze trà i servizii ponu esse rapprisintati cum'è metudi chì tornanu i porti EndPoint's di altri nodi:

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

Per creà un serviziu di eco, tuttu ciò chì avete bisognu hè un numeru di portu è una indicazione chì u portu sustene u protocolu di eco. Ùn pudemu micca specificà un portu specificu, perchè ... e caratteristiche permettenu di dichjarà metudi senza implementazione (metudi astratti). In questu casu, quandu crea una cunfigurazione concreta, u compilatore ci vole à furnisce una implementazione di u metudu astrattu è furnisce un numeru di portu. Siccomu avemu implementatu u metudu, quandu crea una cunfigurazione specifica, ùn pudemu micca specificà un portu diversu. U valore predeterminatu serà utilizatu.

In a cunfigurazione di u cliente, dichjaremu una dipendenza da u serviziu di eco:

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

A dependenza hè di u listessu tipu cum'è u serviziu esportatu echoService. In particulare, in u cliente echo avemu bisognu di u listessu protokollu. Dunque, quandu cunnette dui servizii, pudemu esse sicuru chì tuttu funziona bè.

Implementazione di servizii

Una funzione hè necessaria per inizià è piantà u serviziu. (A capacità di piantà un serviziu hè critica per a prova.) In novu, ci sò parechje opzioni per implementà una tale funzione (per esempiu, pudemu usà classi di tipu basatu nantu à u tipu di cunfigurazione). Per i scopi di questu post useremu u Pattern di Cake. Rappresenteremu u serviziu cù una classa cats.Resource, perchè Sta classa dighjà furnisce i mezi per guarantisci in modu sicuru a liberazione di risorse in casu di prublemi. Per uttene una risorsa, avemu bisognu di furnisce a cunfigurazione è un cuntestu di runtime prontu. A funzione di startup di serviziu pò vede cusì:

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

induve

  • Config - tipu di cunfigurazione per stu serviziu
  • AddressResolver - un oggettu runtime chì vi permette di truvà l'indirizzi di altri nodi (vede sottu)

è altri tipi da a biblioteca cats:

  • F[_] - tipu d'effettu (in u casu più simplice F[A] puderia esse solu una funzione () => A. In questu post avemu aduprà cats.IO.)
  • Reader[A,B] - più o menu sinonimu di funzione A => B
  • cats.Resource - una risorsa chì pò esse acquistata è liberata
  • Timer - timer (permette di dorme per un tempu è misurà intervalli di tempu)
  • ContextShift - analogicu ExecutionContext
  • Applicative - una classa di tipu d'effettu chì permette di cumminà effetti individuali (quasi una monade). In l'applicazioni più cumplesse pare megliu à aduprà Monad/ConcurrentEffect.

Utilizendu sta firma di funzione pudemu implementà parechji servizii. Per esempiu, un serviziu chì ùn faci nunda:

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

(Cm. surghjente, in quale altri servizii sò implementati - serviziu di eco, cliente echo
и cuntrolli di vita.)

Un node hè un ughjettu chì pò lancià parechji servizii (u lanciamentu di una catena di risorse hè assicurata da u Cake Pattern):

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

Per piacè nutate chì avemu specificà u tipu esattu di cunfigurazione chì hè necessariu per stu node. Se scurdemu di specificà unu di i tipi di cunfigurazione richiesti da un serviziu particulari, ci sarà un errore di compilazione. Inoltre, ùn puderemu micca inizià un node, salvu ùn furniscemu qualchì ughjettu di u tipu appropritatu cù tutti i dati necessarii.

Risoluzione di u nome di host

Per cunnette à un host remoto, avemu bisognu di un veru indirizzu IP. Hè pussibule chì l'indirizzu diventerà cunnisciutu dopu à u restu di a cunfigurazione. Allora avemu bisognu di una funzione chì mape l'ID di nodu à un indirizzu:

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

Ci hè parechje manere di implementà sta funzione:

  1. Sè l 'indirizzi divintatu cunnisciuti à noi nanzu sparghjera, allura putemu generà codice Scala cù
    indirizzi è poi eseguite u build. Questu compilerà è eseguirà testi.
    In questu casu, a funzione serà cunnisciuta staticamente è pò esse rapprisintata in codice cum'è mapping Map[NodeId, NodeAddress].
  2. In certi casi, l'indirizzu propiu hè cunnisciutu solu dopu chì u node hà iniziatu.
    In questu casu, pudemu implementà un "serviziu di scuperta" chì corre prima di l'altri nodi è tutti i nodi si registranu cù stu serviziu è dumandà l'indirizzi di l'altri nodi.
  3. Se pudemu mudificà /etc/hosts, allora pudete aduprà nomi d'ospiti predefiniti (cum'è my-project-main-node и echo-backend) è simpricimenti ligà sti nomi
    cù l'indirizzi IP durante a implementazione.

In questu post, ùn cunsideremu micca questi casi in più detail. Per i nostri
in un esempiu di ghjoculi, tutti i nodi anu u listessu indirizzu IP - 127.0.0.1.

Dopu, cunsideremu duie opzioni per un sistema distribuitu:

  1. Pone tutti i servizii nantu à un node.
  2. È ospitu u serviziu di eco è u cliente di eco in diversi nodi.

Configurazione per un nodu:

Cunfigurazione unicu node

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

L'ughjettu implementa a cunfigurazione di u cliente è di u servitore. Una cunfigurazione time-to-live hè ancu utilizata in modu chì dopu l'intervallu lifetime finisce u prugramma. (Ctrl-C funziona ancu è libera tutte e risorse correttamente.)

U listessu settore di cunfigurazione è caratteristiche di implementazione pò esse utilizatu per creà un sistema cumpostu di dui nodi separati:

Cunfigurazione di dui nodi

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

Impurtante! Avvisu cumu i servizii sò ligati. Specificemu un serviziu implementatu da un node cum'è implementazione di u metudu di dependenza di un altru node. U tipu di dependenza hè verificatu da u compilatore, perchè cuntene u tipu di protocolu. Quandu si eseguisce, a dependenza cuntene l'ID di nodu di destinazione curretta. Grazie à stu schema, specifiemu u numeru di portu esattamente una volta è sò sempre garantiti per riferite à u portu currettu.

Implementazione di dui nodi di sistema

Per sta cunfigurazione, usemu i stessi implementazioni di serviziu senza cambiamenti. L'unica diferenza hè chì avemu avà dui oggetti chì implementanu diversi setti di servizii:

  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
  }

U primu node implementa u servitore è solu bisognu di cunfigurazione di u servitore. U sicondu node implementa u cliente è usa una parte sfarente di a cunfigurazione. Ancu i dui nodi necessitanu una gestione di a vita. U node di u servitore corre indefinitu finu à ch'ellu si ferma SIGTERM'om, è u nodu di u cliente finisce dopu qualchì tempu. Cm. app launcher.

U prucessu di sviluppu generale

Videmu cumu stu approcciu di cunfigurazione influenza u prucessu di sviluppu generale.

A cunfigurazione serà cumpilata cù u restu di u codice è un artefattu (.jar) serà generatu. Sembra sensu di mette a cunfigurazione in un artefattu separatu. Questu hè perchè pudemu avè parechje cunfigurazioni basate nantu à u listessu codice. In novu, hè pussibule generà artefatti chì currispondenu à e diverse rami di cunfigurazione. Dipendenze di versioni specifiche di biblioteche sò salvate cù a cunfigurazione, è queste versioni sò salvate per sempre ogni volta chì decidemu di implementà quella versione di a cunfigurazione.

Ogni cambiamentu di cunfigurazione si trasforma in un cambiamentu di codice. È dunque, ognunu
u cambiamentu serà cupartu da u prucessu normale di assicurazione di qualità:

Ticket in u bug tracker -> PR -> recensione -> fusione cù rami pertinenti ->
integrazione -> implementazione

I principali cunsiquenzi di implementà una cunfigurazione compilata sò:

  1. A cunfigurazione serà coherente in tutti i nodi di u sistema distribuitu. A causa di u fattu chì tutti i nodi ricevenu a listessa cunfigurazione da una sola fonte.

  2. Hè problematicu di cambià a cunfigurazione in solu unu di i nodi. Dunque, a "deriva di cunfigurazione" hè improbabile.

  3. Diventa più difficiuli di fà picculi cambiamenti à a cunfigurazione.

  4. A maiò parte di i cambiamenti di cunfigurazione saranu cum'è parte di u prucessu di sviluppu generale è seranu sottumessi à rivisione.

Aghju bisognu di un repositoriu separatu per almacenà a cunfigurazione di produzzione? Questa cunfigurazione pò cuntene password è altre informazioni sensibili à quale vulemu restringe l'accessu. Basatu annantu à questu, pare avè sensu per almacenà a cunfigurazione finale in un repositoriu separatu. Pudete sparte a cunfigurazione in duie parte - una chì cuntene paràmetri di cunfigurazione accessibile publicamente è una chì cuntene paràmetri ristretti. Questu permetterà à a maiò parte di i sviluppatori avè accessu à i paràmetri cumuni. Questa separazione hè faciule d'ottene cù tratti intermedi chì cuntenenu valori predeterminati.

Variazioni pussibuli

Pruvemu di paragunà a cunfigurazione compilata cù alcune alternative cumuni:

  1. File di testu nantu à a macchina di destinazione.
  2. Magazzinu centralizatu di valori chjave (etcd/zookeeper).
  3. Cumpunenti di prucessu chì ponu esse reconfigurati / riavviatu senza riavvia u prucessu.
  4. Memorizza a cunfigurazione fora di u cuntrollu di l'artefattu è di a versione.

I schedarii di testu furnisce una flessibilità significativa in termini di picculi cambiamenti. L'amministratore di u sistema pò accede à u node remotu, fà cambiamenti à i schedari appropritatu è riavvia u serviziu. Per i grandi sistemi, però, una tale flessibilità pò esse micca desiderata. I cambiamenti fatti ùn lascianu traccia in altri sistemi. Nimu riviseghja i cambiamenti. Hè difficiuli di determinà quale esattamente hà fattu i cambiamenti è per quale ragione. I cambiamenti ùn sò micca pruvati. Se u sistema hè distribuitu, allura l'amministratore pò scurdà di fà u cambiamentu currispundente in altri nodi.

(Deve esse ancu nutatu chì l'usu di una cunfigurazione compilata ùn chjude micca a pussibilità di utilizà schedarii di testu in u futuru. Bastarà aghjunghje un parser è validatore chì pruduce u listessu tipu di output. Config, è pudete aduprà schedarii di testu. Segue immediatamente chì a cumplessità di un sistema cù una cunfigurazione compilata hè un pocu menu di a cumplessità di un sistema chì usa i schedari di testu, perchè i schedarii di testu necessitanu un codice supplementu.)

Un magazzinu di chjave-valore centralizatu hè un bon mecanismu per a distribuzione di meta-parametri di una applicazione distribuita. Avemu bisognu di decide chì sò i paràmetri di cunfigurazione è ciò chì sò solu dati. Avemu una funzione C => A => B, è i paràmetri C raramenti cambiamenti, è dati A - spessu. In questu casu, pudemu dì chì C - paràmetri di cunfigurazione, è A - dati. Sembra chì i paràmetri di cunfigurazione differenu da e dati in quantu generalmente cambianu menu frequentemente cà i dati. Inoltre, i dati sò generalmente da una fonte (da l'utilizatore), è i paràmetri di cunfigurazione da un altru (da l'amministratore di u sistema).

Se i paràmetri cambianti raramenti devenu esse aghjurnati senza riavvià u prugramma, allora questu pò spessu porta à a cumplicazione di u prugramma, perchè avemu bisognu di qualchì manera di furnisce i parametri, almacenà, analizà è verificate, è processà i valori sbagliati. Per quessa, da u puntu di vista di riduzzione di a cumplessità di u prugramma, hè sensu per riduce u nùmeru di paràmetri chì ponu cambià durante l'operazione di u prugramma (o ùn sustene micca tali paràmetri).

Per i scopi di questu post, distingueremu trà parametri statichi è dinamichi. Se a logica di u serviziu hà bisognu di cambià i parametri durante l'operazione di u prugramma, allora chjamemu tali paràmetri dinamichi. Altrimenti l'opzioni sò statichi è ponu esse cunfigurati cù a cunfigurazione compilata. Per a ricunfigurazione dinamica, pudemu avè bisognu di un mecanismu per riavvia parti di u prugramma cù novi paràmetri, simili à cumu i prucessi di u sistema operatore sò riavviati. (In u nostru parè, hè cunsigliu per evità a reconfigurazione in tempu reale, postu chì questu aumenta a cumplessità di u sistema. Sè pussibule, hè megliu aduprà e capacità standard di u SO per riavvià i prucessi).

Un aspettu impurtante di l'usu di a cunfigurazione statica chì face chì a ghjente cunsiderà a ricunfigurazione dinamica hè u tempu chì ci vole à u sistema per riavvià dopu un aghjurnamentu di cunfigurazione (downtime). In fatti, s'ellu ci vole à fà cambiamenti à a cunfigurazione statica, avemu da ripiglià u sistema per chì i novi valori effettue. U prublema di downtime varieghja in gravità per diversi sistemi. In certi casi, pudete programà un reboot in un momentu quandu a carica hè minima. Sè avete bisognu di furnisce un serviziu cuntinuu, pudete implementà Scarico di cunnessione AWS ELB. À u listessu tempu, quandu avemu bisognu di reboot u sistema, lanciamu una istanza parallela di stu sistema, cambiate u balancer à questu, è aspittemu chì i vechji cunnessione per compie. Dopu chì tutte e vechji cunnessione anu terminatu, chjudemu l'antica istanza di u sistema.

Cunsideremu avà u prublema di almacenà a cunfigurazione in l'internu o fora di l'artefattu. Se guardemu a cunfigurazione in un artefattu, almenu avemu avutu l'uppurtunità di verificà a correttezza di a cunfigurazione durante l'assemblea di l'artefattu. Se a cunfigurazione hè fora di l'artefattu cuntrullatu, hè difficiule di seguità quale hà fattu cambiamenti à stu schedariu è perchè. Quantu hè impurtante? In u nostru parè, per parechji sistemi di produzzione hè impurtante per avè una cunfigurazione stabile è d'alta qualità.

A versione di un artefattu permette di determinà quandu hè statu creatu, quali valori cuntene, quali funzioni sò attivate / disattivate, è quale hè rispunsevule per ogni cambiamentu in a cunfigurazione. Di sicuru, almacenà a cunfigurazione in un artefattu richiede qualchì sforzu, cusì avete bisognu di piglià una decisione infurmata.

Pros and Cons

Mi piacerebbe aspittà nantu à i pro è i contra di a tecnulugia pruposta.

vantaghji

Quì sottu hè una lista di e caratteristiche principali di una cunfigurazione di sistema distribuitu compilatu:

  1. Verificazione di a cunfigurazione statica. Vi permette di esse sicuru chì
    a cunfigurazione hè curretta.
  2. Lingua di cunfigurazione ricca. Di genere, altri metudi di cunfigurazione sò limitati à a sustituzione di variabile di stringa à u più. Quandu si usa Scala, una larga gamma di funzioni di lingua sò dispunibili per migliurà a vostra cunfigurazione. Per esempiu, pudemu usà
    tratti per i valori predeterminati, usendu l'uggetti à i paràmetri di gruppu, pudemu riferite à i vals dichjarati una sola volta (DRY) in l'ambitu chjusu. Pudete instantiate qualsiasi classi direttamente in a cunfigurazione (Seq, Map, classi persunalizati).
  3. DSL. Scala hà una quantità di funzioni di lingua chì facenu più faciule per creà un DSL. Hè pussibule di prufittà di sti funziunalità è implementà una lingua di cunfigurazione chì hè più cunvene per u gruppu di destinazione d'utilizatori, per chì a cunfigurazione hè almenu leggibile da l'esperti di u duminiu. I specialisti ponu, per esempiu, participà à u prucessu di rivisione di cunfigurazione.
  4. Integrità è sincronia trà i nodi. Unu di i vantaghji di avè a cunfigurazione di un sistema distribuitu sanu guardatu in un puntu unicu hè chì tutti i valori sò dichjarati una sola volta è poi riutilizati induve sò necessarii. Utilizà i tipi fantasma per dichjarà porti assicura chì i nodi utilizanu protokolli cumpatibili in tutte e cunfigurazioni di sistema currette. Avè dipendenze obligatorie esplicite trà i nodi assicura chì tutti i servizii sò cunnessi.
  5. Cambiamenti di alta qualità. Fà cambiamenti à a cunfigurazione utilizendu un prucessu di sviluppu cumuni permette ancu di ottene standard di alta qualità per a cunfigurazione.
  6. Actualizazione di a cunfigurazione simultanea. A implementazione automatica di u sistema dopu i cambiamenti di cunfigurazione assicura chì tutti i nodi sò aghjurnati.
  7. Simplificà l'applicazione. L'applicazione ùn hà micca bisognu di analisi, verificazione di cunfigurazione o gestione di valori sbagliati. Questu reduce a cumplessità di l'applicazione. (Alcune di a cumplessità di cunfigurazione osservata in u nostru esempiu ùn hè micca un attributu di a cunfigurazione compilata, ma solu una decisione cuscente guidata da u desideriu di furnisce una più grande sicurezza di tipu). parti. Per quessa, pudete, per esempiu, principià cù una cunfigurazione compilata, deferring l'implementazione di parti innecessarii finu à u tempu quandu hè veramente necessariu.
  8. Cunfigurazione verificata. Siccomu i cambiamenti di cunfigurazione seguitanu u destinu abituale di qualsiasi altri cambiamenti, a pruduzzioni chì avemu ottene hè un artefattu cù una versione unica. Questu ci permette, per esempiu, di vultà à una versione precedente di a cunfigurazione se ne necessariu. Pudemu ancu aduprà a cunfigurazione da un annu fà è u sistema funziona esattamente u listessu. Una cunfigurazione stabile migliurà a prevedibilità è l'affidabilità di un sistema distribuitu. Siccomu a cunfigurazione hè fissata in u stadiu di compilazione, hè abbastanza difficiule di falsificà in a produzzione.
  9. Modularità. U quadru prupostu hè modulare è i moduli ponu esse cumminati in diverse manere per creà diversi sistemi. In particulare, pudete cunfigurà u sistema per eseguisce nantu à un unicu node in una incarnazione, è in parechji nodi in un altru. Pudete creà parechje cunfigurazioni per istanze di produzzione di u sistema.
  10. Testing. Per rimpiazzà i servizii individuali cù l'oggetti simulati, pudete uttene parechje versioni di u sistema chì sò cunvene per pruvà.
  11. Test d'integrazione. Avè una sola cunfigurazione per tuttu u sistema distribuitu permette di eseguisce tutti i cumpunenti in un ambiente cuntrullatu cum'è parte di teste di integrazione. Hè facilitu per emulà, per esempiu, una situazione induve certi nodi diventanu accessibili.

Svantaghji è limitazioni

A cunfigurazione compilata difiere da altri approcci di cunfigurazione è pò esse micca adattatu per alcune applicazioni. Quì sottu sò qualchi svantaghji:

  1. Cunfigurazione statica. Calchì volta ci vole à curregà rapidamente a cunfigurazione in a produzzione, sguassendu tutti i meccanismi protettivi. Cù stu approcciu pò esse più difficiule. À u minimu, a compilazione è a implementazione automatica serà sempre necessaria. Questu hè à tempu una funzione utile di l'approcciu è un svantaghju in certi casi.
  2. Generazione di cunfigurazione. In casu chì u schedariu di cunfigurazione hè generatu da un strumentu automaticu, sforzi supplementari ponu esse richiesti per integrà u script di creazione.
  3. Strumenti. Attualmente, l'utilità è e tecniche pensate per travaglià cù a cunfigurazione sò basati nantu à i schedarii di testu. Ùn sò micca tutti tali utilità / tecniche seranu dispunibili in una cunfigurazione compilata.
  4. Un cambiamentu d'attitudini hè necessariu. I sviluppatori è i DevOps sò abituati à i schedarii di testu. L'idea stessa di cumpilà una cunfigurazione pò esse un pocu inesperu è inusual è pruvucà u rifiutu.
  5. Un prucessu di sviluppu di alta qualità hè necessariu. Per utilizà comodamente a cunfigurazione compilata, l'automatizazione completa di u prucessu di custruisce è implementà l'applicazione (CI/CD) hè necessaria. Altrimenti, serà abbastanza inconveniente.

Fighjemu ancu nantu à una quantità di limitazioni di l'esempiu cunsideratu chì ùn sò micca ligati à l'idea di una cunfigurazione compilata:

  1. Se furnimu infurmazione di cunfigurazione innecessaria chì ùn hè micca utilizata da u node, allura u compilatore ùn ci aiuterà à detectà l'implementazione mancante. Stu prublema pò esse risolta abbandunendu u Pattern Cake è utilizendu tipi più rigidi, per esempiu, HList o tipi di dati algebrichi (classi di casu) per rapprisintà a cunfigurazione.
  2. Ci sò linii in u schedariu di cunfigurazione chì ùn sò micca ligati à a cunfigurazione stessu: (package, import, dichjarazioni d'ughjettu; override def's per i paràmetri chì anu valori predeterminati). Questu pò esse parzialmente evitata se implementate u vostru propiu DSL. Inoltre, altri tipi di cunfigurazione (per esempiu, XML) imponenu ancu certe restrizioni à a struttura di u schedariu.
  3. Per i scopi di questu post, ùn avemu micca cunsideratu a ricunfigurazione dinamica di un cluster di nodi simili.

cunchiusioni

In questu post, avemu esploratu l'idea di rapprisintà a cunfigurazione in u codice fonte utilizendu e capacità avanzate di u sistema di tipu Scala. Stu approcciu pò esse usatu in diverse applicazioni cum'è sustituzione per i metudi di cunfigurazione tradiziunali basati nantu à xml o schedarii di testu. Ancu s'è u nostru esempiu hè implementatu in Scala, e stesse idee ponu esse trasferite à altre lingue compilate (cum'è Kotlin, C#, Swift, ...). Pudete pruvà stu approcciu in unu di i seguenti prughjetti, è, s'ellu ùn viaghja micca, passà à u schedariu di testu, aghjunghjendu e parti mancanti.

Naturalmente, una cunfigurazione compilata richiede un prucessu di sviluppu di alta qualità. In ritornu, l'alta qualità è affidabilità di cunfigurazioni hè assicurata.

L'approcciu cunsideratu pò esse allargatu:

  1. Pudete utilizà macros per eseguisce cuntrolli in tempu di compilazione.
  2. Pudete implementà un DSL per presentà a cunfigurazione in una manera accessibile per l'utilizatori finali.
  3. Pudete implementà una gestione dinamica di risorse cù l'aghjustamentu automaticu di a cunfigurazione. Per esempiu, cambià u numeru di nodi in un cluster richiede chì (1) ogni node riceve una cunfigurazione ligeramente diversa; (2) u cluster manager hà ricevutu infurmazioni nantu à novi nodi.

Ringraziamenti

Vogliu ringrazià Andrei Saksonov, Pavel Popov è Anton Nekhaev per a so critica constructiva di u prugettu di l'articulu.

Source: www.habr.com

Add a comment