Kompileeritav konfiguratsioon jaotatud süsteemile

Selles postituses soovime jagada huvitavat viisi hajutatud süsteemi konfiguratsiooni haldamiseks.
Konfiguratsioon on esitatud otse Scala keeles tüübistatud ja turvalisel viisil. Näidise teostus on üksikasjalikult kirjeldatud. Arutatakse erinevaid ettepaneku aspekte, sealhulgas nende mõju üldisele arendusprotsessile.

Kompileeritav konfiguratsioon jaotatud süsteemile

(eesti keeles)

Sissejuhatus

Robustsete hajutatud süsteemide loomine nõuab kõigis sõlmedes õige ja koherentse konfiguratsiooni kasutamist. Tüüpiline lahendus on kasutada tekstilist juurutuskujulist kirjeldust (terraform, ansible või midagi sarnast) ja automaatselt genereeritud konfiguratsioonifaile (tavaliselt — iga sõlme/rolli jaoks eraldi). Soovime ka, et kõik suhtlevad sõlmed kasutaksid samu protokolle sama versiooniga (vastasel juhul seisame silmitsi ühilduvuse probleemidega). JVM maailmas tähendab see, et vähemalt sõnumi teegid peavad olema kõigil suhtlevatel sõlmedel sama versiooniga.

Kuidas on lood süsteemi testimisega? Mõistagi peaks meil olema üksustestid kõikide komponentide jaoks enne integreerimistestide tegemist. Selleks, et testitulemusi jooksvalt extrapoleerida, peame veenduma, et kõikide teekide versioonid püsivad mõlemas keskkonnas samad.

Integreerimisteste tehes on sageli palju lihtsam hoida kõigil sõlmedel sama klasifitseerimisrada. Peame lihtsalt veenduma, et juurutamisel kasutatakse sama klasifitseerimisrada. (On võimalik kasutada erinevaid klasifitseerimisradu erinevates sõlmedes, kuid selle konfiguratsiooni esitamine ja õige juurutamine on keerulisem.) Seega et asju lihtsana hoida, vaatleme ainult identseid klasifitseerimisradasid kõigil sõlmedel.

Konfiguratsioon kipub koos tarkvaraga arenema. Me kasutame tavaliselt versioone, et tuvastada erinevaid
tarkvara arengufaase. Tundub mõistlik katta konfiguratsioon versioonihaldusega ja tuvastada erinevat konfiguratsiooni mõne sildiga. Kui tootmises on vaid üks konfiguratsioon, võime kasutada ühte versiooni identifikaatorina. Mõnikord võib meil olla mitu tootmis keskkonda. Ja iga keskkonna jaoks võib vajada eraldi konfiguratsiooniharu. Seega võivad konfiguratsioonid olla märgitud haru ja versiooniga, et unikaalselt tuvastada erinevaid konfiguratsioone. Iga haru silt ja versioon vastavad ühele kombinatsioonile hajutatud sõlmedest, portidest, välistest ressurssidest, klasifitseerimisraamatukogude versioonidest igas sõlmes. Siin katame ainult ühe haru ja tuvastame konfiguratsioonid kolme komponentiga kümnendsüsteemi versioonina (1.2.3), nagu teiste artefaktide puhul.

Kaasaegsetes keskkondades ei muudetud konfiguratsioonifaile enam käsitsi. Tüüpiliselt genereerime
konfiguratsioonifaile juurutamise ajal ja ei puutu neisse hiljem. Seega võiks küsida, miks me ikka kasutame tekstivormingut konfiguratsioonifailide jaoks? Üks mõistlik võimalus on paigutada konfiguratsioon kompileeritavasse üksusesse ja kasu saada kompileerimise ajal konfiguratsiooni valideerimisest.

Selles postituses vaatleme ideed hoida konfiguratsioon kompileeritud artefaktis.

Kompileeritav konfiguratsioon

Selles jaotises arutame staatilise konfiguratsiooni näidet. Kaks lihtsat teenust — echo teenus ja echo teenuse klient on konfigureeritud ja teostatud. Seejärel luuakse kaks erinevat hajutatud süsteemi mõlema teenusega. Üks on ühe sõlme konfiguratsioon ja teine on kahe sõlme konfiguratsioon.

Tüüpiline hajutatud süsteem koosneb mitmest sõlmest. Sõlmed saab tuvastada mõne tüübi alusel:

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

või lihtsalt

case class NodeId(hostName: String)

või isegi

object Singleton
type NodeId = Singleton.type

Need sõlmed täidavad erinevaid rolle, käitavad teenuseid ja peavad suutma suhelda teiste sõlmedega TCP/HTTP ühenduste kaudu.

TCP ühenduse jaoks on vajalik vähemalt porti number. Samuti soovime veenduda, et klient ja server kasutavad sama protokolli. Ühenduse modelleerimiseks sõlmitame järgmise klassi:

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

where Port on lihtsalt Int lubatud vahemikus:

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

Refined tüübid

Vaata refined raamatukogus. Lühidalt, see võimaldab lisada kompileerimise ajal tingimusi teistele tüüpidele. Antud juhul Int on lubatud ainult 16-bitised väärtused, mis suudavad esindada porti numbrit. Selle konfiguratsioonimeetodi jaoks ei ole selle raamatukogu kasutamine kohustuslik. Tundub, et see sobib väga hästi.

HTTP (REST) jaoks võime vajada ka teenuse teed:

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

Phantom tüüp

Protokolli tuvastamiseks kompileerimise ajal kasutame Scala omadust, mis seisneb tüüpi argumentide deklareermises Protokoll , mida klassis ei kasutata. See on nii öeldud phantom tüüp. Käitamise ajal vajame harva protokolli identifikaatori eksemplari, seetõttu me seda ei salvestada. Kompileerimise ajal pakub see phantom tüüp täiendavat tüübi ohutust. Me ei saa edastada porti vale protokolliga.

Üks kõige laialdasemalt kasutatavaid protokolle on REST API koos Json seerimisega:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

where RequestMessage on sõnumite baas tüüp, mida klient saadab serverile ja ResponseMessage on serverilt saadud vastus. Muidugi, võime luua ka muid protokolli kirjeldusi, mis määratlevad suhtlusprotokolli soovitud täpsusega.

Käesoleva postituse jaoks kasutame lihtsamat versiooni protokollist:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Käesolevas protokollis lisatakse päringu sõnum URL-le ja vastussõnum tagastatakse tavalise stringina.

Teenuse konfiguratsioon võiks sisaldada teenuse nime, portide kogumit ja mõningaid sõltuvusi. Kõiki neid elemente saab Scala-s esindada mitmel erineval viisil (näiteks, HList, algebralised andmetüübid). Käesoleva postituse jaoks kasutame Cake Pattern'i ja esindame kombineeritavaid osi (mooduleid) kui rasvaseid. (Cake Pattern ei ole selle kompileeritava konfiguratsioonimeetodi nõue. See on lihtsalt üks võimalik rakendus mõttest.)

Sõltuvusi võiks esindada Cake Pattern'i abil kui teiste node'ide lõpp-punkte:

  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 teenus vajab ainult portaali seadistamist. Ja deklareerime, et see port toetab echo protokolli. Pange tähele, et me ei pea sel hetkel määrama konkreetset porti, kuna rasvased lubavad abstraktsete meetodite deklareerimist. Kui kasutame abstrakte meetodeid, nõuab kompilaator rakendust konfiguratsiooni eksemplaris. Siin oleme pakkunud rakenduse (8081) ja seda kasutatakse vaikimisi väärtusena, kui me jätame selle konkreetse konfiguratsiooni jätmiseks vahele.

Saame deklareerida sõltuvuse echo teenuse kliendi konfiguratsioonis:

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

Sõltuvusel on sama tüüp, mis echoService. Eelkõige nõuab see sama protokolli. Seega saame olla kindlad, et kui ühendame need kaks sõltuvust, töötavad nad õigesti.

Teenuste rakendamine

Teenus vajab funktsiooni, mis võimaldab selle käivitamist ja sujuvat sulgemist. (Teenuse sulgemise võimalus on testimise mõttes kriitilise tähtsusega.) Uuesti on mõned võimalused, kuidas sellist funktsiooni konkreetse konfiguratsiooni jaoks määrata (näiteks saame kasutada tüpiklasse). Selle postituse jaoks kasutame jälle Cake Patterni. Saame teenust esindada, kasutades cats.Resource , mis juba pakub raamimist ja ressursi vabastamist. Ressursi saamiseks peaksime esitama konfiguratsiooni ja teatud käituskonteksti. Seega võiks teenuse käivitamise funktsioon välja näha järgmiselt:

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

where

  • Konfiguratsioon — konfiguratsiooni tüüp, mis on vajalik selle teenuse käivitamiseks
  • AddressResolver — käituskonteksti objekt, mis suudab hankida teiste sõlmede tegelikke aadresse (lue edasi üksikasjade jaoks).

teised tüübid pärinevad cats:

  • F[_] — efekti tüüp (lihtsaimatel juhtudel F[A] võiks olla lihtsalt () => A. Selles postituses kasutame cats.IO.)
  • Reader[A,B] — on enam-vähem sünonüüm funktsioonile A => B
  • cats.Resource — omab võimalusi hankida ja vabastada
  • Timer — võimaldab magada/mõõta aega
  • ContextShift — analoog ExecutionContext
  • Applicative — funktsioonide pakett efektis (peaaegu monad) (me võime lõpuks asendada selle millegi muu vastu)

Kasutades seda liidest, saame rakendada mõned teenused. Näiteks teenus, mis ei tee midagi:

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

(Vaata Allika kood muude teenuste rakenduste jaoks — echo teenus,
echo klient ja eluea kontrollijad.)

Sõlm on üksik objekt, mis käitab mitmeid teenuseid (ressursside ahela käivitamine on võimaldatud Cake Patterni 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 sõlmes määrame täpselt selle konfiguratsiooni tüübi, mis on selle sõlme jaoks vajalik. Kompilatsioon ei lase meil objekti (Cake) ehitada ebapiisava tüübiga, sest iga teenuse trait deklareerib sõltuvuse Konfiguratsioon tüübile. Samuti ei saa me sõlme käivitada ilma täieliku konfiguratsioonita.

Sõlme aadressi lahendamine

Konnekti loomiseks vajame tõelist hosti aadressi iga sõlme jaoks. See võib olla tuntud hiljem kui teised konfiguratsiooni osad. Seega vajame viisi, kuidas esitada kaardistamine sõlme ID ja selle tegeliku aadressi vahel. See kaardistamine on funktsioon:

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

On mõned võimalikud viisid sellise funktsiooni rakendamiseks.

  1. Kui teame tegelikke aadresse enne juurutamist, sõlmede hostide loomise ajal, saame genereerida Scala koodi tegelike aadressidega ja käivitada ehituse hiljem (mis teeb kompileerimisetapi kontrollid ja seejärel käivitab integreerimistestide komplekti). Sel juhul on meie kaardistamisfunktsioon staatiliselt tuntud ja seda saab lihtsustada midagi sellist nagu Map[NodeId, NodeAddress].
  2. Mõnikord saame tegelikke aadresse ainult hiljem, kui sõlm on tegelikult käivitatud, või meil ei ole aadresse sõlmedele, mis pole veel käivitatud. Sel juhul võime kasutada avastamisteenust, mis käivitatakse enne kõiki teisi sõlmi, ja iga sõlm võib selle teenuse kaudu reklaamida oma aadressi ja tellida sõltuvusi.
  3. Kui saame muuta /etc/hosts, saame kasutada eelnevalt määratud hostinimesid (nagu my-project-main-node ja echo-backend) ja seostada selle nime IP-aadressiga juurutamise ajal.

Selles postituses me ei käsitle neid juhtumeid põhjalikumalt. Tegelikult meie mängu näites on kõigil sõlmedel sama IP-aadress — 127.0.0.1.

Selles postituses keskendume kahele jaotatud süsteemi paigutusele:

  1. Ühe sõlme paigutus, kus kõik teenused on paigutatud ühele sõlmele.
  2. Kahesõlmelise paigutus, kus teenus ja klient on erinevates sõlmedes.

Ühe sõlme paigutuse konfiguratsioon on järgmine:

Ühe sõlme konfiguratsioon

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

Siin loome ühe konfiguratsiooni, mis laiendab nii serveri kui ka kliendi konfiguratsiooni. Samuti konfigureerime elutsükli juhik, mis lõpetab tavaliselt kliendi ja serveri pärast elutsüklit intervalli möödumist.

Sama teenuste rakenduste ja konfiguratsioonide komplekti saab kasutada süsteemi paigutuse loomiseks kahe eraldi sõlmega. Peame lihtsalt looma kaks eraldi sõlme konfi sobivate teenustega:

Kahe sõlme konfiguratsioon

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

Vaata, kuidas me määratleme sõltuvuse. Mainime teise sõlme pakutavat teenust kui praeguse sõlme sõltuvust. Sõltuvuse tüüp kontrollitakse, kuna see sisaldab fantoom tüüpi, mis kirjeldab protokolli. Ja tööajal on meid õige sõlme id. See on üks olulisi aspekte pakutud konfiguratsiooni lähenemisest. See annab meile võimaluse määrata port ainult kord ja veenduda, et me viitame õigele pordile.

Kahe sõlme rakendus

Selle konfiguratsiooni jaoks kasutame täpselt samu teenuste rakendusi. Ükski muudatus pole vajalik. Kuid loome kaks erinevat sõlme rakendust, mis sisaldavad erinevat komplekti teenuseid:

  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 serveri ja vajab ainult serveripoolset konfiguratsiooni. Teine sõlm rakendab kliendi ja vajab teist osade konfiguratsiooni. Mõlemad sõlmed vajavad mingisugust elutsükli määratlemist. Selle postituse eesmärgil on teenuse sõlmel piiramatu eluiga, mille saab lõpetada kasutades SIGTERM, samas kui echo klient lõpetab pärast määratud lõpliku kestuse möödumist. Vaata algus rakendust täiendavate detailide saamiseks.

Kogu arendamisprotsess

Vaadake, kuidas see lähenemine muudab meie ülesannet konfiguratsiooniga töötamises.

Konfiguratsioonikood kompileeritakse ja toodab artefakti. Tundub mõistlik eraldada konfiguratsiooni artefakt teistest koodiartefaktidest. Tihti võib meil olla palju konfiguratsioone sama koodibaasi peal. Ja muidugi, meil võivad olla mitmed versioonid erinevatest konfiguratsiooniharu harudest. Konfiguratsioonis saame valida teatud versioonid teekidest ja see jääb püsima igal korral, kui me seda konfiguratsiooni kasutame.

Konfiguratsiooni muutus muutub koodimuutuseks. Seega peaks selle katma sama kvaliteedi tagamise protsess:

Pilet -> PR -> ülevaatus -> sulandumine -> pidev integreerimine -> pidev juurutamine

Selle lähenemise järgsetel on järgmised tagajärjed:

  1. Konfiguratsioon on koherentne konkreetse süsteemi instantsi jaoks. Tundub, et ei ole võimalust, et sõlmede vahel oleks vale ühendus.
  2. Konfiguratsiooni muutmine ainult ühes sõlmes ei ole lihtne. Tundub mõistlikum mitte sisse logida ja muuta mõnda tekstifaili. Seega muutub konfiguratsiooni drift vähem tõenäoliseks.
  3. Väikeste konfiguratsioonimuudatuste tegemine ei ole kerge.
  4. Enamik konfiguratsioonimuudatusi järgib sama arendusprotsessi ja see läbib mingisuguse ülevaatuse.

Kas meil on vaja eraldi репозиторий tootmis konfiguratsiooni jaoks? Tootmis konfiguratsioon võib sisaldada tundlikku teavet, mille me soovime paljude inimeste eest kaitsta. Seega võib olla mõistlik hoida eraldi репозиторий piiratud ligipääsuga, mis sisaldab tootmis konfiguratsiooni. Saame jagada konfiguratsiooni kaheks osaks — üks, mis sisaldab enamasti avatud parameetreid tootmisest, ja teine, mis sisaldab konfiguratsiooni salajast osa. See võimaldaks juurdepääsu enamikule arendajatest suurte parameetrite hulka, samas piirates pääsu tõeliselt tundlikele asjadele. Seda on lihtne saavutada, kasutades vahepealseid omadusi vaikeparameetrite väärtustega.

Variatsioonid

Vaatame pakkumise plusse ja miinuseid võrreldes teiste konfiguratsioonihalduse tehnikatega.

Esiteks loetleme mõned alternatiivid erinevate aspektide jaoks pakutud konfiguratsioonide käsitlemise viisidest:

  1. Tekstifail sihtmasinal.
  2. Keskne võtme-väärtuse salvestus (nt etcd/zookeeper).
  3. Alamhargnendid, mida saaks ümber seadistada/taaskäivitada ilma protsessi taaskäivitamiseta.
  4. Konfiguratsioon väljaspool artefakti ja versioonihaldust.

Tekstifail annab teatud paindlikkuse ad-hoc paranduste osas. Süsteemi administraator saab sisse logida sihtnodele, teha muudatusi ja lihtsalt teenuse taaskäivitada. See ei pruugi suurtes süsteemides hästi toimida. Muudatuste jälgi ei jää. Muudatust ei ole üle vaadanud keegi teine. Võib olla keeruline välja selgitada, mis muudatuse põhjustas. Seda ei ole testitud. Jaotatud süsteemi vaatenurgast võib administraator lihtsalt unustada uuendada konfiguratsiooni ühes teises sõlmes.

(Muide, kui lõpuks tuleb vajadus alustada tekstikonfiguratsioonifailide kasutamist, peame lihtsalt lisama parseri + valideerija, mis võiks toota sama Konfiguratsioon tüüp ja sellest piisab alustada tekstikonfiguratsioonide kasutamist. See näitab ka, et kompileerimise ajal konfiguratsiooni keerukus on natuke väiksem kui tekstipõhiste konfiguratsioonide keerukus, kuna tekstipõhises versioonis vajame mõningaid lisakoode.)

Keskne võtme-väärtuse salvestus on hea mehhanism rakenduse meta parameetrite jagamiseks. Siin peame mõtlema sellele, mida peame konfiguratsiooniväärtusteks ja mis on lihtsalt andmed. Antud funktsiooni C => A => B nimetame me harva muutuvaid väärtusi C «konfiguratsiooniks», samas kui sageli muutuvaid andmeid A — lihtsalt sisendandmed. Konfiguratsioon peaks olema funktsioonile ette antud varem kui andmed. A. Selle idee tõttu võime öelda, et oodatav muudatuste sagedus on see, mis võiks eristada konfigureerimisandmeid tavalisest andmest. Samuti tuleb andmed tavaliselt ühest allikast (kasutaja) ja konfiguratsioon tuleb erinevast allikast (administraator). Parameetritega tegelemine, mida saab muuta pärast protsessi algatamist, suurendab rakenduse keerukust. Selliste parameetrite korral peame käsitlema nende edastamismehhanismi, analüüsi ja valideerimist, samuti valeväärtuste töötlemist. Seetõttu, et vähendada programmi keerukust, oleks parem vähendada jooksuaegade muutmise parameetrite arvu (või isegi need täielikult kõrvaldada).

Selle postituse vaatenurgast peaksime eristama staatilisi ja dünaamilisi parameetreid. Kui teenuse loogika nõuab mõnede parameetrite harva muutmist jooksuajal, siis võime neid nimetada dünaamilisteks parameetriteks. Vastasel juhul on need staatilised ja neid võib konfigureerida pakutud lähenemisviisi abil. Dünaamiliseks ümberkonfigureerimiseks võivad olla vajalikud teised lähenemisviisid. Näiteks võiks süsteemi osade uuesti käivitamine toimuda uute konfiguratsiooniparametritega sarnaselt jaotatud süsteemi eraldi protsesside käivitamisele.
(Minu arust on parem vältida jooksuaegset ümberkonfigureerimist, sest see suurendab süsteemi keerukust.
Võib-olla oleks otsem loota lihtsalt operatsioonisüsteemi toetusele protsesside taaskäivitamiseks. Kuigi see ei pruugi alati võimalik olla.)

Üks oluline aspekt staatilise konfiguratsiooni kasutamisel, mis mõnikord paneb inimesi kaaluma dünaamilist konfiguratsiooni (ilma muude põhjusteta), on teenuse seiskamine konfiguratsiooni uuendamise ajal. Tõepoolest, kui peame tegema muudatusi staatilises konfiguratsioonis, peab süsteem taaskäivitama, et uued väärtused saaksid kehtima hakkata. Seiskamise nõudmised varieeruvad erinevates süsteemides, seega ei pruugi see olla nii kriitiline. Kui see on kriitiline, siis peame ette planeerima kõik süsteemi taaskäivitused. Näiteks võiksime rakendada AWS ELB ühenduse tühjendamist.. Sel juhul, kui peame süsteemi taaskäivitama, käivitame paralleelselt uue süsteemi instantsi, seejärel lülitame ELB sellele, samal ajal kui lubame vanal süsteemil lõpetada olemasolevate ühenduste teenindamise.

Kuidas on asjad, kui hoida konfiguratsiooni versioonitud artefaktide sees või väljaspool? Konfiguratsiooni hoidmine artefakti sees tähendab enamasti seda, et see konfiguratsioon on läbinud sama kvaliteedi tagamise protsessi, mis muud artefaktid. Seega võib olla kindel, et konfiguratsioon on hea kvaliteediga ja usaldusväärne. Vastupidiselt tähendab konfiguratsiooni hoidmine eraldi failis seda, et pole jälgi, kes ja miks selle faili muudatusi tegi. Kas see on oluline? Usume, et enamik produktsioonisüsteeme eelistab stabiilset ja kõrge kvaliteediga konfiguratsiooni.

Artefakti versioon võimaldab välja selgitada, millal see loodi, millised väärtused sellele kuuluvad, millised omadused on lubatud/keelatud, kes oli iga konfiguratsiooni muudatuse eest vastutav. See võib nõuda teatavaid pingutusi, et hoida konfiguratsioon artefakti sees, ja see on kujundusvalik.

Plussid ja miinused

Siin tahame esile tuua mõned eelised ning arutada mõningaid puudusi pakutud lähenemise kohta.

Eelised

Kompileeritava konfiguratsiooni omadused täielikus jaotatud süsteemis:

  1. Konfiguratsiooni staatiline kontroll. See annab kõrge taseme kindlust, et konfiguratsioon on õige antud tüübi piirangute korral.
  2. Rikas konfiguratsioonikeel. Tüüpiliselt on teised konfiguratsioonilähenemised piiratud maksimaalselt muutuja asendamisega.
    Scala kasutades saab kasutada laia valikut keele omadusi, et konfiguratsiooni paremini teha. Näiteks võime kasutada traate, et määrata vaikeseaded, objekte erinevate ulatuste seadmiseks, saame viidata vals, mis on defineeritud vaid korra välimises ulatuses (DRY). On võimalik kasutada literaalseid järjestusi või teatud klasside eksemplare (Seq, Map, jne.).
  3. DSL. Scala pakub DSL kirjutajatele head tuge. Need funktsioonid võimaldavad luua konfiguratsioonikeele, mis on kasutajale mugavam ja sõbralikum, et lõplik konfiguratsioon oleks vähemalt domeeni kasutajate jaoks loetav.
  4. Integriteet ja ühtsus sõlmede vahel. Üks eelis, mis tuleneb kogu hajutatud süsteemi konfiguratsiooni paiknemisest ühes kohas, on see, et kõik väärtused on rangelt defineeritud vaid korra ja seejärel kasutatakse neid kõikides kohtades, kus me neid vajame. Ka tüübisertifikaadiga portide deklareerimine tagab, et kõikides õigetes konfiguratsioonides räägivad süsteemi sõlmed sama keelt. Sõlmede vahel on selged sõltuvused, mis muudavad teenuste unustamise keeruliseks.
  5. Muutuste kõrge kvaliteet. Ükskõik milline konfiguratsiooni muutuste läbinemine tavalise PR-protsessi kaudu seab kõrged kvaliteedistandardid ka konfiguratsioonile.
  6. Korraga tehtavad konfiguratsioonimuudatused. Iga kord, kui me konfiguratsioonis muudatusi teeme, tagab automaatne kasutuselevõtt, et kõik sõlmed saavad värskendatud.
  7. Rakenduse lihtsustumine. Rakendus ei pea parsimise ja valideerimise ning vale konfiguratsiooniväärtuste käsitlemisega tegelema. See lihtsustab kogu rakendust. (Mõningane keerukuse suurenemine on ise konfiguratsioonis, kuid see on teadlik vahetus ohutuse kasuks.) Tavalise konfiguratsiooni juurde naasmine on üsna lihtne — lihtsalt lisage puuduvad osad. Kompileeritud konfiguratsiooniga alustamine on lihtsam ja täiendavate osade rakendamist saab edasi lükata.
  8. Versioonitud konfiguratsioon. Kuna konfiguratsioonimuudatused järgivad sama arendusprotsessi, saame tulemusena unikaalse versiooniga artefakti. See võimaldab meil vajadusel konfiguratsiooni tagasi lülitada. Me võime isegi seadistada konfiguratsiooni, mida kasutati aasta tagasi, ja see töötab täpselt samamoodi. Stabiilne konfiguratsioon parandab hajutatud süsteemi ennustatavust ja usaldusväärsust. Konfiguratsioon fikseeritakse kompileerimise ajal ja ei saa tootmissüsteemis kergesti rikutud.
  9. Modulaarsus. Pakutud raamistik on modulaarne ja mooduleid saaks erinevates kombinatsioonides ühendada, et
    toetada erinevaid konfiguratsioone (seadeid/paigutusi). Eriti on võimalik omada väikese mahuga ühe sõlme paigutust ja suure mahuga mitme sõlme seadet. Mõistlik on omada mitmeid tootmispaigutusi.
  10. Testimine. Testimise eesmärgil võib implementida vale teenuse ja kasutada seda tüübisertifikaadi kaudu sõltuvusena. Erinevaid testimispaigutusi, milles mitmed osad on asendatud valeversioonidega, võiks samal ajal säilitada.
  11. Integratsiooni testimine. Mõnikord on hajutatud süsteemides integratsioonitestide läbiviimine keeruline. Kirjeldatud tüübist ohutu konfiguratsiooni lähenemise kasutamisel kogu hajutatud süsteemi jaoks saame käivitada kõik hajutatud osad ühel serveril kontrollitaval viisil. Olukorra simuleerimine on lihtne,
    kui üks teenustest ei ole saadaval.

Puudused

Kompileeritud konfiguratsiooni lähenemine erineb "tavalisest" konfiguratsioonist ja see ei pruugi sobida kõikidele vajadustele. Siin on mõned kompileeritud konfiguratsiooni puudused:

  1. Statiline konfiguratsioon. See ei pruugi sobida kõikidele rakendustele. Mõningatel juhtudel on vaja tootmises kiiresti konfiguratsiooni parandada, mööda minnes kõigist ohutusmeetmetest. See lähenemine muudab selle keeruliseks. Muudatuste tegemisel konfiguratsioonis on vajalik kompileerimine ja uuesti kasutuselevõtt. See on nii omadus kui ka koormus.
  2. Konfiguratsiooni genereerimine. Kui konfiguratsioon genereeritakse mõne automatiseerimise tööriista abil, nõuab see hilisema kompileerimist (mis võib omakorda ebaõnnestuda). See võib nõuda täiendavaid pingutusi, et integreerida see lisasamm koostöösüsteemi.
  3. Tööriistad. Praegu on kasutusel palju tööriistu, mis toetuvad tekstipõhistele konfiguratsioonidele. Mõned neist
    ei pruugi olla rakendatavad, kui konfiguratsioon on kompileeritud.
  4. Mõtteviisi muutus on vajalik. Arendajad ja DevOps on tuttavad tekstipõhiste konfiguratsioonifailidega. Konfiguratsiooni kompileerimise idee võib neile tunduda kummaline.
  5. Enne kompileeritava konfiguratsiooni tutvustamist on vajalik kõrgekvaliteediline tarkvaraarenduse protsess.

Rakendatud näite mõned piirangud on:

  1. Kui me anname lisakonfiguratsiooni, mida sõlmiselt sõlmitud rakendamine ei nõua, ei aita kompilaator meil tuvastada puuduvat rakendust. Seda võiks lahendada, kasutades HList või ADT-sid (koosolekuklassid) sõlme konfiguratsiooni jaoks, mitte traate ja Cake Pattern'i.
  2. Peame pakkuma konfiguratsioonifailis natuke boilerplate'i: (pakett, import, objekt deklaratsioonid;
    ülekirjutamise funktsioonparametrite jaoks, millel on vaikimisi väärtused). Seda võiks osaliselt lahendada DSL-i kasutamisega.
  3. Selles postituses me ei katseta sarnaste sõlmede klastrite dünaamilist ümberkonfigureerimist.

Kokkuvõte

Selles postituses oleme arutanud mõtet esindada konfiguratsiooni otse allika koodis tüübiohutuslikul viisil. Lähenemist võiks kasutada paljudes rakendustes xml- ja muude tekstipõhiste konfiguratsioonide asendamiseks. Kuigi meie näide on rakendatud Scala keeles, võiks selle tõlkida ka teistesse kompileeritavatesse keeltesse (nagu Kotlin, C#, Swift jne). Keegi võiks seda lähenemist proovida uues projektis ja juhul, kui see ei sobi, vahetada tagasi vanamoodsama viisi juurde.

Küsimus on selles, et kompileeritav konfiguratsioon nõuab kõrgekvaliteedilist arendusprotsessi. Vastutasuks lubab see pakkuda sama kõrgekvaliteedilist usaldusväärset konfiguratsiooni.

Seda lähenemist võiks laiendada mitmeti:

  1. Keegi võiks kasutada makrosid konfiguratsiooni valideerimiseks ja kompilatsioonivigade korral, kui tekivad mingid äriloogika piirangute rikkumised.
  2. DSL-i võiks rakendada, et esindada konfiguratsiooni domäänikasutajasõbralikul viisil.
  3. Dünaamiline ressursihaldus automaatsete konfiguratsiooni kohandustega. Näiteks, kui me kohandame klastrisõlmede arvu, soovime, et (1) sõlmed saaksid veidi muudetud konfiguratsiooni; (2) klastrihaldur saaks uute sõlmede teavet.

Aitäh

Soovin tänada Andrey Saksonovit, Pavel Popovit, Anton Nehaevi inspiratsiooniandva tagasiside eest selle postituse mustandi kohta, mis aitas seda selgemaks muuta.

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