Kompileeritav konfiguratsioon jaotatud sĂŒsteemis

Sooviksin rÀÀkida ĂŒhest huvitavast mehhanismist, mis puudutab hajutatud sĂŒsteemi konfiguratsiooni. Konfiguratsioon on otse esitatud kompileeritavas keeles (Scala) kasutades turvalisi tĂŒĂŒpe. Selles postituses analĂŒĂŒsime sellise konfiguratsiooni nĂ€idet ja kĂ€sitleme erinevaid aspekte kompileeritava konfiguratsiooni integreerimisest arenduse ĂŒldprotsessi.

Kompileeritav konfiguratsioon jaotatud sĂŒsteemis

(english)

Sissejuhatus

UsaldusvÀÀrse hajutatud sĂŒsteemi ĂŒlesehitamine eeldab, et kĂ”igil sĂ”lmedel on Ă”ige konfiguratsioon, mis on sĂŒnkroniseeritud teiste sĂ”lmedega. T tavaliselt kasutatakse DevOps tehnoloogiaid (terraform, ansible vĂ”i midagi sarnast) konfiguratsioonifailide automaatseks genereerimiseks (tihti oma iga sĂ”lme jaoks). Soovime samuti olla kindlad, et kĂ”ikides suhtlevates sĂ”lmedes kasutatakse samu protokolle (sh sama versiooni). Vastasel juhul vĂ”ib meie hajutatud sĂŒsteemis ilmneda ĂŒhilduvuse probleem. JVM maailmas on ĂŒks sellise nĂ”ude tagajĂ€rgi vajadus kasutada igal pool sama versiooni teeki, mis sisaldab protokolli sĂ”numeid.

Kuidas on lood hajutatud sĂŒsteemi testimisega? Muidugi eeldame, et kĂ”igi komponentide jaoks on ette nĂ€htudĂŒksus-testid enne, kui liigume integreerimistestimise juurde. (Kuna me soovime testimistulemusi eksitada runtime'ile, peame tagama samuti identse teegikomplekti nii testimise faasis kui ka runtime'is.)

Integreerimistestide puhul on sageli lihtsam kasutada igal sĂ”lmel ĂŒhtset classpath'i. Me peame tagama, et sama classpath'i kasutatakse ka runtime'is. (Kuigi on tĂ€iesti vĂ”imalik kĂ€ivitada erinevaid sĂ”lmi erinevate classpath'idega, toob see kaasa kogu konfiguratsiooni keerukuse ning raskused juurutamise ja integreerimistestide puhul.) Selle postituse raames eeldame, et kĂ”igil sĂ”lmedel kasutatakse sama classpath'i.

Konfiguratsioon areneb koos rakendusega. Erinevate arendusetappide tuvastamiseks kasutame versioone. On loogiline, et tuvastame ka erinevad konfiguratsioonid. Isegi konfiguratsioon tuleks panna versioonihaldussĂŒsteemi. Kui tootmises on ainult ĂŒks konfiguratsioon, saame kasutada lihtsalt versiooninumbrit. Kui aga on mitmeid tootmisexemplare, peame vajama mitut
konfiguratsiooniharu ja lisamĂ€rki versioonist (nĂ€iteks haru nimi). Nii saame selgelt tuvastada tĂ€pse konfiguratsiooni. Iga konfiguratsiooni identifikaator vastab ĂŒhemĂ”tteliselt teatud jaotatud sĂ”lmede, portide, vĂ€liste ressursside ja teegiversioonide kombinatsioonile. Selles postituses lĂ€htume eeldusest, et on olemas ainult ĂŒks haru, ja saame konfiguratsiooni tuvastada tavapĂ€rasel viisil, kasutades kolme numbrit, mis on eraldatud punktiga (1.2.3).

Kaasaegsetes keskkondades luuakse konfiguratsioonifailid harva kĂ€sitsi. Enamasti genereeritakse need deployimiseks ja neid ei muudetaks enam (et midagi ei rikuks). Tekkib loogiline kĂŒsimus, miks me ikka veel kasutame konfiguratsiooni salvestamiseks tekstivormingut? ElujĂ”uliseks alternatiiviks nĂ€ib olevat vĂ”imalus kasutada tavalist koodi konfiguratsiooniks ja saada kasu kontrollidest kompileerimise ajal.

Selles postituses uurimegi idee, et esitada konfiguratsioon kompileeritud artefakti sees.

Kompileeritav konfiguratsioon

Selles osas on toodud nĂ€ide staatilisest kompileeritavast konfiguratsioonist. Rakendatakse kahe lihtsa teenuse — eho teenuse ja eho teenuse kliendi — nĂ€idet. Nende kahe teenuse pĂ”hjal kogume kaks varianti sĂŒsteemist. Ühes variandis asuvad mĂ”lemad teenused ĂŒhes sĂ”lmes, teises variandis aga erinevates sĂ”lmedes.

Tavaliselt sisaldab jaotatud sĂŒsteem mitmeid sĂ”lmi. SĂ”lme saab tuvastada mĂ”ne tĂŒĂŒpi vÀÀrtuste abil NodeId:

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

vÔi

case class NodeId(hostName: String)

vÔi isegi

object Singleton
type NodeId = Singleton.type

SÔlmed tÀidavad erinevaid rolle, neis on teenuseid ja nende vahel vÔivad olla TCP/HTTP suhted.

TCP ĂŒhenduse kirjeldamiseks on meil vĂ€hemalt vajalik port. Samuti soovime peegeldada protokolli, mis on sellel pordil toetatud, et tagada, et nii klient kui server kasutavad sama protokolli. Kirjeldame ĂŒhendust jĂ€rgmise klassi abil:

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

kus Port — lihtsalt tĂ€isarv Int kivide lubatud vÀÀrtuste vahemiku nĂ€itamisega:

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

TĂ€psustatud tĂŒĂŒbid

Vaata raamatukogu refined ja minu ettekanne. LĂŒhidalt, raamatukogu vĂ”imaldab tĂŒĂŒpidele lisada piiranguid, mida kontrollitakse kompileerimise ajal. Antud juhul on lubatud pordi numbriteks 16-bitised tĂ€isarvud. Raamatukogu refined kasutamine kompileeritava konfiguratsiooni jaoks ei ole kohustuslik, kuid see parandab kompilaatori kontrollimisvĂ”imet.

HTTP (REST) protokollide jaoks vÔib pordi numbrist lisaks vaja minna ka teenuse teed:

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

FantoomtĂŒĂŒbid

Protokolli tuvastamiseks kompileerimise ajal kasutame tĂŒĂŒpparameetrit, mida klassis ei kasutata. See lahendus tuleneb sellest, et runtime'is me protokolli eksemplari ei kasuta, kuid soovime, et kompilaator kontrolliks protokollide ĂŒhilduvust. Protokolli mÀÀratlemise tĂ”ttu ei saa me ebamugavat teenust sĂ”ltuvusena edastada.

Üks levinumaid protokolle on REST API JSON-seerialiseerimisega:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

kus RequestMessage — pĂ€ringu tĂŒĂŒp, ResponseMessage — vastuse tĂŒĂŒp.
Loomulikult vÔib kasutada ka muid protokolli kirjeldusi, mis pakuvad vajalikku tÀpsust.

Selle postituse eesmÀrkide saavutamiseks kasutame lihtsustatud versiooni protokollist:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Siin on pÀring string, mis lisatakse url-ile, ja vastus on string, mis tagastatakse HTTP vastuse kehas.

Teenuse konfiguratsioon kirjeldatakse teenuse nime, portide ja sĂ”ltuvustega. Neid elemente saab esitada Scala-s mitmel viisil (nt. HList-de, algebrailiste andmetĂŒĂŒpide kaudu). Selle postituse eesmĂ€rkide saavutamiseks kasutame Cake Pattern'i ja esindame mooduleid kaudu trait‘de. (Cake Pattern ei ole kirjeldatud lĂ€henemise kohustuslik element. See on lihtsalt ĂŒks vĂ”imalikest teostustest.)

Teenuste vahelisi sĂ”ltuvusi saab esitada meetodite kujul, mis tagastavad portid EndPoint‘de teiste sĂ”lmede:

  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 teenuse loomiseks on piisav ainult porti number ja mÀÀramine, et see port toetab echo protokolli. Me vÔiksime ka mitte mÀÀrata konkreetset porti, kuna trait'id vÔimaldavad deklaratsiooni ilma rakenduseta (abstraktsed meetodid). Sellisel juhul nÔuaks konkreetse konfiguratsiooni loomine kompilaatorilt, et me esitaksime abstraktsiooni rakenduse ja porti numbri. Kuna me oleme meetodi rakendanud, ei pea me konkreetse konfiguratsiooni loomisel mÀÀrama teist porti. Kasutusele vÔetakse vaikevÀÀrtus.

Kliendi konfiguratsioonis kuulutame vÀlja sÔltuvuse echo teenusest:

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

SĂ”ltuvus on sama tĂŒĂŒpi nagu eksponeeritud teenus echoService. Ehkki echo kliendis nĂ”uame sama protokolli. Seega, kui kaks teenust omavahel ĂŒhendada, saame olla kindlad, et kĂ”ik töötab Ă”igesti.

Teenuste rakendamine

Teenuse kĂ€ivitamiseks ja peatamiseks on vajalik funktsioon. (Teenuse peatamise vĂ”imalus on testimise jaoks kriitilise tĂ€htsusega.) JĂ€lle on mitu vĂ”imalust sellise funktsiooni rakendamiseks (nĂ€iteks vĂ”iksime kasutada konfiguratsioonitĂŒĂŒpi pĂ”hiseid klasside tĂŒĂŒpe). KĂ€esoleva postituse jaoks kasutame Cake Pattern'it. Me esindame teenust klassi kaudu cats.Resource, kuna see klass sisaldab juba vahendeid ressursside turvaliseks ja garanteerituks vabastamiseks probleemide korral. Ressursi saamiseks peame esitama konfiguratsiooni ja valmis runtime konteksti. Teenuse kĂ€ivitamise funktsioon vĂ”ib olla jĂ€rgmine:

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

kus

  • Konfiguratsioon — konfiguratsiooni tĂŒĂŒp selle teenuse jaoks
  • AddressResolver — kĂ€itamise objekt, mis vĂ”imaldab teada teiste sĂ”lmede aadresse (vt allpool)

ja ĂŒlejÀÀnud tĂŒĂŒbid raamatukogust cats:

  • F[_] — efekti tĂŒĂŒp (lihtsaimatel juhtudel F[A] vĂ”ib olla lihtsalt funktsioon () => A. KĂ€esolevas postituses kasutame cats.IO.)
  • Reader[A,B] — rohkem-vĂ”i vĂ€hem funktsiooni sĂŒnonĂŒĂŒm A => B
  • cats.Resource — ressurss, mida saab hankida ja vabastada
  • Timer — taimer (vĂ”imaldab teatud ajaks uinuda ja mÔÔta ajavahemikke)
  • ContextShift — analoog ExecutionContext
  • Applicative — efekti tĂŒĂŒbi klass, mis vĂ”imaldab eraldi efekte ĂŒhendada (peaaegu monaad). TĂ”enĂ€oliselt on keerukamates rakendustes parem kasutada Monad/ConcurrentEffect.

Kasutades seda funktsiooni signatuuri saame rakendada mitmeid teenuseid. NĂ€iteks teenus, mis ei tee midagi:

  trait ZeroServiceImpl[F[_]] extends ServiceImpl[F] {
    type Config  Resource.pure[F, Unit](()))
  }

(Vt. allikas kood, kus on rakendatud teisi teenuseid — kaja teenus, kaja klient
ja eluea kontrollerid.)

SÔlm on objekt, mis suudab kÀivitada mitmeid teenuseid (ressursside kÀivitamine tagatakse Cake Pattern'i kaudu):

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

Pange tĂ€hele, et me mĂ€rgime tĂ€pset konfiguratsiooni tĂŒĂŒpi, mis on selle sĂ”lme jaoks vajalik. Kui unustame mÀÀrata mĂ”ne konfiguratsiooni tĂŒĂŒbi, mis on vajalik eraldi teenuse jaoks, siis tekib kompileerimise viga. Samuti ei saa me sĂ”lme kĂ€ivitada, kui ei esita mĂ”nd objekti, mis omab sobivat tĂŒĂŒpi koos kĂ”igi vajalike andmetega.

sÔlmede nimed

KaugsĂ”lmest ĂŒhenduse loomiseks on meil vajalik pĂ€ris IP-aadress. On tĂ€iesti vĂ”imalik, et aadress muutub tuntuks hiljem kui ĂŒlejÀÀnud konfiguratsiooni osad. SeetĂ”ttu on meil vajalik funktsioon, mis seob sĂ”lme identifikaatori aadressiga:

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

VÔime pakkuda mitmeid viise sellise funktsiooni rakendamiseks:

  1. Kui aadressid saavad meile tuntuks enne juurutamist, siis saame genereerida Scala-koodi koos
    aadressidega ja seejÀrel kÀivitada kogumise. Sellega toimub kompileerimine ja testide lÀbiviimine.
    Sellisel juhul on funktsioon staatiliselt teada ja seda saab esitada koodis kaardistamisena Map[NodeId, NodeAddress].
  2. MÔnel juhul muutub kehtiv aadress tuntuks alles pÀrast sÔlme kÀivitamist.
    Sellisel juhul saame rakendada «avastamisteenust» (discovery), mis kĂ€ivitatakse enne teisi sĂ”lmi, ja kĂ”ik sĂ”lmed registreeritakse selles teenuses ning kĂŒsivad teiste sĂ”lmede aadresse.
  3. Kui suudame muuta /etc/hosts, siis saab kasutada eelmÀÀratud hostide nimesid (nÀiteks my-project-main-node ja echo-backend) ja lihtsalt siduda need nimed
    IP-aadressidega juurutamise kÀigus.

KÀesolevas postituses ei kÀsitle me neid juhtumeid lÀhemalt. Meie
mĂ€nguasja nĂ€ite puhul on kĂ”ik sĂ”lmedel ĂŒks IP-aadress — 127.0.0.1.

Vaatame kahte jaotatud sĂŒsteemi varianti:

  1. KĂ”ik teenused paiknevad ĂŒhel sĂ”lmel.
  2. Ja kaja teenus ja kaja klient asuvad erinevates sÔlmedes.

Konfiguratsioon ĂŒhele sĂ”lmele konfiguratsioon ĂŒhele sĂ”lmele:

Konfiguratsioon ĂŒhele sĂ”lmele

object SingleNodeConfig extends EchoConfig[String] 
  with EchoClientConfig[String] with FiniteDurationLifecycleConfig
{
  case object Singleton \/\/ ainus sÔlm 
  \/\/ serveri konfiguratsioon
  type NodeId = Singleton.type
  def nodeId = Singleton

  \/** Type safe teenuse porte mÀÀratlemine. *\/ 
  override def portNumber: PortNumber = 8088

  \/\/ kliendi konfiguratsioon

  \/** Kasutame teenust, mis on sama hosti poolt. *\/ 
  def echoServiceDependency = echoService

  override def testMessage: UrlPathElement = "hello"

  def pollInterval: FiniteDuration = 1.second

  \/\/ elutsĂŒkli juhtimise konfiguratsioon
  def lifetime: FiniteDuration = 10500.milliseconds \/\/ lisaks 0.5 sekundit, et saaksime 10 pÀringut, mitte 9.
}

Objekt rakendab nii kliendi kui ka serveri konfiguratsiooni. Samuti kasutatakse eluea konfiguratsiooni, et mĂ”ne aja möödudes elutsĂŒklit programm lĂ”petada. (Ctrl-C töötab samuti ja vabastab Ă”igesti kĂ”ik ressursid.)

Sama konfiguratsiooni ja implementatsioonide komplekti saab kasutada sĂŒsteemi, mis koosneb kahest eraldi sĂ”lmest:

Konfiguratsioon kahest sÔlmest

  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! sÔltuvuse mÀÀratlemine
    def echoServiceDependency = NodeServerConfig.echoService

    def pollInterval: FiniteDuration = 1.second

    def lifetime: FiniteDuration = 10500.milliseconds \/\/ lisaks 0.5 sekundit, et saaksime 10 pÀringut, mitte 9.

    def testMessage: String = "dolly"
  }

Oluline! Pange tĂ€hele, kuidas teenuste sidumine toimub. NĂ€itame teenust, mille teostab ĂŒks sĂ”lm, kui teise sĂ”lme sĂ”ltuvuse rakendamise meetodit. SĂ”ltuvuse tĂŒĂŒp kontrollitakse kompilatsiooni kĂ€igus, kuna see sisaldab protokolli tĂŒĂŒpi. KĂ€ivitamisel sisaldab sĂ”ltuvus Ă”iget sihtsĂ”lme identifikaatorit. Selle skeemi abil mÀÀrame sadama numbri ainult ĂŒks kord ja viitame alati Ă”igetele sadamatele.

Kaks sĂ”lme sĂŒsteemi rakendamine

Selle konfiguratsiooni jaoks kasutame samu teenuste rakendusi muutumatul kujul. Ainus erinevus on see, et nĂŒĂŒd on meil kaks objekti, mis rakendavad erinevaid teenuste komplekte:

  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
  }

Esimene sÔlm rakendab serverit ja vajab ainult serveri konfiguratsiooni. Teine sÔlm rakendab klienti ja kasutab teist konfiguratsiooni osa. Samuti vajavad mÔlemad sÔlmed eluea haldamist. ServerisÔlm töötab piiramatu aja jooksul, kuni see peatatakse SIGTERMja kliendisÔlm lÔpetatakse teatud aja pÀrast. Vaata. rakenduse kÀivitamine.

Üldine arendusprotsess

Vaatame, kuidas see konfiguratsiooni lĂ€henemine mĂ”jutab ĂŒldist arendusprotsessi.

Konfiguratsioon kompileeritakse koos muu koodiga ja genereeritakse artefakt (.jar). Ilmselt on mÔistlik paigutada konfiguratsioon eraldi artefakti. See tuleneb sellest, et meil vÔib olla palju konfiguratsioone sama koodi pÔhjal. Taaskord on vÔimalik genereerida artefakte, mis vastavad erinevatele konfiguratsiooni harudele. Koos konfiguratsiooniga sÀilivad sÔltuvused konkreetsete teekide versioonide osas ja need versioonid sÀilitatakse igavesti, kui otsustame selle konfiguratsiooni versiooni juurutada.

Iga konfiguratsiooni muutmine muutub koodi muutmiseks. Seega on iga selline
muutus hÔlmatud tavapÀrase kvaliteediprotsessiga:

PÀÀsme viga jĂ€lgimisse -> PR -> ĂŒlevaatus -> liitmine vastavate harudega ->
integreerimine -> juurutamine

Peamised tagajÀrjed kompileeritava konfiguratsiooni rakendamisel:

  1. Konfiguratsioon kooskĂ”lastatakse kĂ”igis hajutatud sĂŒsteemi sĂ”lmedes. Kuna kĂ”ik sĂ”lmed saavad sama konfiguratsiooni ĂŒhelt allikalt.

  2. Konfiguratsiooni muutmine ainult ĂŒhes sĂ”lmes on keeruline. SeetĂ”ttu on "konfiguratsiooni desĂŒnkroniseerimine" (configuration drift) ebatĂ”enĂ€oline.

  3. VĂ€ikeste muudatuste tegemine konfiguratsioonis muutub keerulisemaks.

  4. Enamik konfiguratsioonimuudatusi toimub osana ĂŒldisest arendusprotsessist ja lĂ€bib ĂŒlevaatuse.

Kas on vaja eraldi hoidlat tootmisreakonfiguratsiooni salvestamiseks? Sellises konfiguratsioonis vĂ”ivad olla paroolid ja muu salajane teave, millele soovime juurdepÀÀsu piirata. Seega tundub mĂ”istlik hoida lĂ”plik konfiguratsioon eraldi hoidlas. Konfiguratsiooni vĂ”ib jagada kaheks osaks - ĂŒhe, mis sisaldab avalikke konfiguratsiooni parameetreid, ja teise, mis sisaldab piiratud juurdepÀÀsuga parameetreid. See vĂ”imaldab enamikul arendajatest pÀÀseda juurde avalikele parameetritele. Sellist jagamist on lihtne saavutada vahetreitide kasutamisega, mis sisaldavad vaikimisi vÀÀrtusi.

VÔimalikud variatsioonid

Proovime vÔrrelda kompileeritud konfiguratsiooni mÔne levinud alternatiiviga:

  1. Tekstifail sihtmasinas.
  2. Keskne vÔtme-vÀÀrtuse ladu (etcd/zookeeper).
  3. Protsessi komponendid, mida saab konfigureerida / taaskÀivitada ilma protsessi taaskÀivitamiseta.
  4. Konfiguratsiooni hoidmine vÀljaspool artefakti ja versioonihaldust.

Tekstifailid pakuvad olulist paindlikkust vĂ€ikeste muudatuste tegemisel. SĂŒsteemiadministraator saab kaugelt sĂ”lmedesse sisse logida, teha vastavatesse failidesse muudatused ja teenuse taaskĂ€ivitada. Suurte sĂŒsteemide puhul vĂ”ib selline paindlikkus siiski olla ebasoovitav. Tehtud muudatustest ei jÀÀ jĂ€lgi teistes sĂŒsteemides. Keegi ei tee muudatuste ĂŒlevaatust. Raske on kindlaks teha, kes ja miks muudatusi tegi. Muudatusi ei testita. Kui sĂŒsteem on hajutatud, vĂ”ib administraator unustada teha vastava muudatuse teistes sĂ”lmedes.

(Samuti tuleks mĂ€rkida, et kompileeritava konfiguriatsiooni kasutamine ei vĂ€lista tulevikus tekstifailide kasutamist. Piisab, kui lisada parser ja valideerija, mis annavad tulemusena sama tĂŒĂŒpi Konfiguratsioon, ja saab kasutada tekstifaile. Siit jĂ€reldub, et kompileeritava konfiguriatsiooni sĂŒsteemi keerukus on mĂ”nevĂ”rra vĂ€iksem kui tekstifailide kasutava sĂŒsteemi keerukus, kuna tekstifailide jaoks on vajalik tĂ€iendav kood.)

Keskne vĂ”tme-vÀÀrtuse hoidla on hea mehhanism jaotatud rakenduse meta-parameetrite edastamiseks. Peame kindlaks tegema, mis on konfigureerimisparameetrid ja mis on lihtsalt andmed. Oletame, et meil on funktsioon C => A => B, ning parameetrid C muutuvad harva, samas kui andmed A — muutuvad sageli. Sellisel juhul saame öelda, et C — on konfigureerimisparameetrid, ja A — on andmed. Tundub, et konfigureerimisparameetrid erinevad andmetest selle poolest, et nad muutuvad ĂŒldjuhul harvem kui andmed. Samuti tulevad andmed tavaliselt ĂŒhest allikast (kasutajalt), samas kui konfigureerimisparameetrid - teisest (sĂŒsteemi administraatorilt).

Kui harva muutuvaid parameetreid tuleb vÀrskendada ilma programmi taaskÀivitamiseta, siis vÔib see sageli viia programmi keerukuse suurenemiseni, kuna me peame kuidagi edastama parameetreid, neid hoidma, parsima ja kontrollima, töötlema valevÀÀrtusi. SeetÔttu on programmi keerukuse vÀhendamise seisukohalt mÔistlik vÀhendada parameetrite arvu, mis vÔivad programmi töö kÀigus muutuda (vÔi isegi mitte neid parameetreid toetada).

Antud postituse kontekstis eristame me staatilisi ja dĂŒnaamilisi parameetreid. Kui teenuse tööloogika nĂ”uab parameetrite muutmist programmi töö kĂ€igus, nimetame selliseid parameetreid dĂŒnaamilisteks. Vastupidisel juhul on parameetrid staatilised ja neid saab seadistada kompileeritava konfiguratsiooni abil. DĂŒnaamiliseks rekontekureerimiseks vĂ”ib meil olla vajalik mehhanism programmiosade taaskĂ€ivimiseks koos uute parameetritega, sarnaselt sellele, kuidas operatsioonisĂŒsteemi protsesse taaskĂ€ivitada. (Meie arvates on soovitatav vĂ€ltida reaalajas rekontekureerimist, kuna see suurendab sĂŒsteemi keerukust. Vajadusel on parem kasutada operatsioonisĂŒsteemi standardfunktsioone protsesside taaskĂ€ivitamiseks.)

Üks tĂ€htsamaid aspekte staatilise konfiguratsiooni kasutamisel, mis paneb inimesi kaaluma dĂŒnaamilist rekontekureerimist, on aeg, mis sĂŒsteemil on vajalik konfiguratsiooni uuendamisel taaskĂ€ivitamiseks (downtime). TĂ”epoolest, kui peame tegema muudatusi staatilises konfiguratsioonis, peame sĂŒsteemi taaskĂ€ivitama, et uued vÀÀrtused jĂ”ustuksid. Downtime'i probleem on erinevates sĂŒsteemides erineva teravusega. MĂ”nedes juhtumites on vĂ”imalik taaskĂ€ivitamine planeerida ajale, mil koormus on minimaalne. Kui on vaja tagada pidev teenus, saab rakendada â€žĂŒhenduste tĂŒhjendamine“ (AWS ELB connection draining). Sel juhul, kui peame sĂŒsteemi taaskĂ€ivitama, kĂ€ivitame parallelse selle sĂŒsteemi eksemplari, lĂŒlitame koormuse tasakaalustaja sellele ja ootame, kuni kĂ”ik vanad ĂŒhendused lĂ”petatakse. PĂ€rast seda, kui kĂ”ik vanad ĂŒhendused on lĂ”petatud, lĂŒlitame vĂ€lja vana sĂŒsteemi eksemplari.

Vaatame nĂŒĂŒd kĂŒsimust konfiguratsiooni salvestamisest kas artefakti sees vĂ”i vĂ€ljas. Kui me salvestame konfiguratsiooni artefakti sisse, on meil vĂ€hemalt vĂ”imalus veenduda artefakti koostamise ajal konfiguratsiooni Ă”igsuses. Kui konfiguratsioon asub kontrollitavast artefaktist vĂ€ljaspool, on raske jĂ€lgida, kes ja miks tegi selle faili muutused. Kui oluline see on? Meie arvates on paljude tootmisĂŒsteemide jaoks oluline omada stabiilset ja kvaliteetset konfiguratsiooni.

Artefakti versioon vĂ”imaldab mÀÀrata, millal see loodi, milliseid vÀÀrtusi see sisaldab, millised funktsioonid on sisse vĂ”i vĂ€lja lĂŒlitatud ning kes on vastutav mis tahes muutuste eest konfiguratsioonis. Loomulikult nĂ”uab konfiguratsiooni sĂ€ilitamine artefakti sees teatud pingutusi, seega tuleb teha teadlik otsus.

Plussid ja miinused

Sooviksin peatuda pakutava tehnoloogia plussidel ja miinustel.

Eelised

Allpool on toodud peamised vĂ”imalused kompileeritud konfiguratsiooniga hajutatud sĂŒsteemi jaoks:

  1. Konfiguratsiooni staatiline kontroll. See tagab, et
    konfiguratsioon on korrektne.
  2. Rikka konfiguratsioonikeele kasutamine. tavaliselt on muud seadistamisviisid piiratud maksimaalselt stringimuutujatega. Scala kasutamisel on saadaval lai valik keele vÔimalusi, et parandada konfiguratsiooni. NÀiteks saame kasutada
    trait'e vaikese vÀÀrtuste jaoks, gruppeerida parameetreid objektide kaudu, viidata korduvatele val'idele, mis on mÀÀratletud ĂŒhes kohas (DRY) ulatuses. Saame otse konfiguratsioonis instantsi luua igasuguseid klasse (Seq, Map, kasutajate klassid).
  3. DSL. Scalas on mitu keelelist vĂ”imalust, mis lihtsustavad DSL-i loomist. Saame neid vĂ”imalusi kasutada ja rakendada konfiguratsiooni keelt, mis oleks sihtrĂŒhmale mugavam, et konfiguratsioon oleks vĂ€hemalt valdkonna spetsialistide arusaadav. Spetsialistid saavad nĂ€iteks osaleda konfiguratsiooni ĂŒlevaatamise protsessis.
  4. Terviklikkus ja sĂŒnkroonsus sĂ”lmede vahel. Üks eelis, et terve hajutatud sĂŒsteemi konfiguratsioon on hoitud ĂŒhes kohas, on see, et kĂ”ik vÀÀrtused kuulutatakse vĂ€lja ainult ĂŒks kord ja neid kasutatakse seejĂ€rel igal pool, kus neid vajatakse. FantoomtĂŒĂŒpide kasutamine sadamate kuulutamiseks tagab, et kĂ”ik Ă”iges sĂŒsteemi konfiguratsioonis kasutatavad sĂ”lmed kasutavad ĂŒhilduvaid protokolle. Selgete nĂ”utavate sĂ”ltuvuste olemasolu sĂ”lmede vahel tagab, et kĂ”ik teenused on omavahel ĂŒhendatud.
  5. KĂ”rge kvaliteedi muutused. Konfiguratsiooni muutmine ĂŒldise arendusprotsessi kaudu vĂ”imaldab saavutada kĂ”rgemaid kvaliteedistandardeid ka konfiguratsiooni jaoks.
  6. Samaaegne konfiguratsiooni vĂ€rskendamine. SĂŒsteemi automaatne levitamine pĂ€rast konfiguratsiooni muudatuste tegemist tagab, et kĂ”ik sĂ”lmed uuendatakse.
  7. Rakenduse lihtsustamine. Rakendus ei vaja analĂŒĂŒsi, konfiguratsiooni kontrolle ega vigaste vÀÀrtuste töötlemist. SeelĂ€bi vĂ€heneb rakenduse keerukus. (MĂ”ningane konfiguratsiooni keerukus, mis meie nĂ€ites ilmneb, ei ole kompileeritava konfiguratsiooni omadus, vaid teadlik valik, mis tuleneb soovist tagada suurem tĂŒĂŒbisĂ”bralikkus.) Tavalise konfiguratsiooni juurde on ĂŒsna lihtne tagasi minna — lihtsalt rakendage puuduolevad osad. Seega vĂ”ite alustada nĂ€iteks kompileeritava konfiguratsiooniga, lĂŒkates ĂŒleliigsete osade teostamise edasi ajaks, mil seda tĂ”eliselt vajate.
  8. Versiooniga konfiguratsioon. Kuna konfiguratsiooni muutused jĂ€rgivad tavalist saatusese muudatuste, saame lĂ”puks ainulaadse versiooniga artefakti. See vĂ”imaldab meil nĂ€iteks naasta eelmise konfiguratsiooni versiooni juurde, kui vajadus tekib. Saame isegi kasutada aasta tagasi loodud konfiguratsiooni ning sĂŒsteem töötab tĂ€pselt sama moodi. Stabiilne konfiguratsioon parandab jaotatud sĂŒsteemi ettearvamatust ja usaldusvÀÀrsust. Kuna konfiguratsioon on fikseeritud kompileerimise etapis, on seda ĂŒsna keeruline tootmises valeandmetega segada.
  9. Modulaarsus. Pakutav raamistik on modulaarne ning mooduleid saab erinevates kombinatsioonides ĂŒhendada erinevate sĂŒsteemide saamiseks. Eriti on vĂ”imalik ĂŒhes variandis konfigureerida sĂŒsteemi töötama ĂŒhel sĂ”lmel, teises aga mitmel sĂ”lmel. Saate luua mitu konfiguratsiooni tootmiseksemplaride jaoks.
  10. Testimine. Asendades ĂŒksikud teenused makettide objektidega, on vĂ”imalik saada mitu sĂŒsteemi versiooni, mis on testimiseks mugavad.
  11. Integreerimise testimine. Ühe konfiguratsiooni olemasolu kogu jaotatud sĂŒsteemis tagab kĂ”ikide koostisosade kĂ€ivitamise vĂ”imaluse kontrollitud keskkonnas integreerimise testimise raames. NĂ€iteks on lihtne emuleerida olukorda, kus mĂ”ned sĂ”lmed muutuvad kĂ€ttesaamatuks.

Puudused ja piirangud

Kompileeritav konfiguratsioon erineb teistest konfiguratsioonimeetoditest ja ei pruugi mÔnede rakenduste jaoks sobida. Siin on mÔned puudused:

  1. Statiline konfiguratsioon. MÔnikord on vaja kiiresti parandada konfiguratsiooni tootmises, mööda minnes kÔigist kaitsemehhanismidest. Selle lÀhenemise raames vÔib see olla keerulisem. Igatahes on kompileerimine ja automaatne juurutamine ikkagi vajalikud. See on samas nii kasulik omadus lÀhenemise puhul kui ka mÔnes olukorras puudus.
  2. Konfiguratsiooni genereerimine. Kui konfiguratsioonifail genereeritakse automaatse tööriistaga, vÔib tekkida vajadus tÀiendavate pingutuste jÀrele skripti integreerimisel.
  3. Tööriistakomplekt. KÀesoleval hetkel pÔhinevad konfiguratsiooni haldamiseks mÔeldud tööriistad ja meetodid tekstifailidel. KÔik sellised tööriistad/meetodid ei pruugi olla kergesti kÀttesaadavad kompileeritava konfiguratsiooni korral.
  4. Vajadus muuta hoiakuid. Arendajad ja DevOps on harjunud tekstifailidega. Ideed konfiguratsiooni kompileerimisest vĂ”ib pidada mĂ”nevĂ”rra ĂŒllatavaks ja ebatavaliseks ning see vĂ”ib tekitada vastumeelsust.
  5. Kvaliteetne arendusprotsess on vajalik. Mugavaks kompileeritava konfiguratsiooni kasutamiseks on vajalik tÀielik automatiseerimine rakenduse ehitus- ja juurutamisprotsessis (CI/CD). Muud tingimused vÔivad olla ebamugavad.

Vaadakem ka uuringu nÀite mÔningaid piiranguid, mis ei ole seotud kompileeritava konfiguratsiooni ideega:

  1. Kui pakume liigset konfiguratsiooni teavet, mis ei ole sĂ”lme poolt kasutatav, ei aita kompilaator meil rakenduse puudumist avastada. Selle probleemi saab lahendada, loobudes Cake Pattern'ist ja kasutades rangemaid tĂŒĂŒpe, nĂ€iteks HList vĂ”i algebralisi andmetĂŒĂŒpe (case class'id) konfiguratsiooni esitamiseks.
  2. Konfiguratsioonifailis on read, mis ei kuulu otse konfiguratsiooni alla: (pakett, import, objektide deklaratsioonid; ĂŒlekirjutamise funktsioonMĂ”ned mÀÀrangud, millel on vaikimisi vÀÀrtused). Osaliselt on vĂ”imalik seda vĂ€ltida, kui rakendada oma DSL-i. Samuti seavad muud konfiguratsiooni tĂŒĂŒbid (nĂ€iteks XML) failistruktuurile teatud piirangud.
  3. Selles postituses ei kĂ€sitle me sarnaste sĂ”lmede klastrite dĂŒnaamilist rekonstrueerimist.

KokkuvÔte

Selles postituses vaatlesime ideed esitada konfiguratsiooni lĂ€htekoodis, kasutades Scala tĂŒĂŒpsĂŒsteemi arenenud vĂ”imalusi. Sellist lĂ€henemist saab kasutada erinevates rakendustes traditsiooniliste XML- vĂ”i tekstifailide pĂ”histe konfiguratsioonimeetodite asendamiseks. Kuigi meie nĂ€ide on rakendatud Scala-s, saab samu ideid rakendada ka teistes kompileeritavates keeltes (nt Kotlin, C#, Swift, jne). Seda lĂ€henemist saab katsetada mĂ”nes jĂ€rgmistest projektidest ja juhul, kui see ei sobi, saab naasta tekstifailide juurde, lisades puuduolevad detailid.

Muidugi nÔuab kompileeritav konfiguratsioon kvaliteetset arendusprotsessi. Vastutasuks tagatakse kÔrge kvaliteet ja usaldusvÀÀrsus.

KÀsitletud lÀhenemist saab laiendada:

  1. Saab kasutada makrosid kontrollide teostamiseks kompileerimise ajal.
  2. Saab rakendada DSL-i, et esitada konfiguratsiooni lÔppkasutajatele arusaadavas vormis.
  3. Saab rakendada dĂŒnaamilist ressursside haldust, mis automaatselt kohandab konfiguratsiooni. NĂ€iteks, kui klastris sĂ”lmede arv muutub, on vajalik, et (1) iga sĂ”lm saab veidi erineva konfiguratsiooni; (2) klastrihaldur saaks teavet uute sĂ”lmede kohta.

TĂ€nud

Soovin tÀnada Andrei Saksonovit, Pavel Popovit ja Anton Nehaevit konstruktiivse kriitika eest artikli mustandi kohta.

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster