Norėčiau pasakyti vieną įdomų mechanizmą, kaip dirbti su paskirstytos sistemos konfigūracija. Konfigūracija pateikiama tiesiogiai kompiliuota kalba (Scala), naudojant saugius tipus. Šiame įraše pateikiamas tokios konfigūracijos pavyzdys ir aptariami įvairūs sukompiliuotos konfigūracijos diegimo į bendrą kūrimo procesą aspektai.
(
įvedimas
Patikimos paskirstytos sistemos sukūrimas reiškia, kad visi mazgai naudoja teisingą konfigūraciją, sinchronizuotą su kitais mazgais. DevOps technologijos (terraform, ansible ar kažkas panašaus) dažniausiai naudojamos automatiškai generuoti konfigūracijos failus (dažnai specifinius kiekvienam mazgui). Taip pat norėtume būti tikri, kad visi bendraujantys mazgai naudoja identiškus protokolus (įskaitant tą pačią versiją). Priešingu atveju mūsų paskirstytoje sistemoje bus įdiegtas nesuderinamumas. JVM pasaulyje viena iš šio reikalavimo pasekmių yra ta, kad visur turi būti naudojama ta pati bibliotekos, kurioje yra protokolo pranešimai, versija.
O kaip paskirstytos sistemos testavimas? Žinoma, manome, kad visi komponentai turi vienetų testus prieš pereinant prie integracijos testavimo. (Kad galėtume ekstrapoliuoti testo rezultatus į vykdymo laiką, taip pat turime pateikti identišką bibliotekų rinkinį testavimo etape ir vykdymo metu.)
Dirbant su integravimo testais, dažnai lengviau naudoti tą patį klasės kelią visuose mazguose. Viskas, ką turime padaryti, tai užtikrinti, kad vykdymo metu būtų naudojamas tas pats klasės kelias. (Nors visiškai įmanoma paleisti skirtingus mazgus su skirtingais klasės keliais, tai daro sudėtingesnę bendrą konfigūraciją ir sunkumų atliekant diegimo ir integravimo testus.) Šiame įraše darome prielaidą, kad visi mazgai naudos tą patį klasės kelią.
Konfigūracija vystosi kartu su programa. Versijas naudojame skirtingiems programos evoliucijos etapams nustatyti. Atrodo logiška nustatyti ir skirtingas konfigūracijų versijas. Ir įdėkite pačią konfigūraciją į versijos valdymo sistemą. Jei gamyboje yra tik viena konfigūracija, galime tiesiog naudoti versijos numerį. Jei naudosime daug gamybos egzempliorių, mums reikės kelių
konfigūracijos šakas ir papildomą etiketę prie versijos (pavyzdžiui, šakos pavadinimo). Tokiu būdu galime aiškiai nustatyti tikslią konfigūraciją. Kiekvienas konfigūracijos identifikatorius unikaliai atitinka tam tikrą paskirstytų mazgų, prievadų, išorinių išteklių ir bibliotekos versijų derinį. Šiame įraše darysime prielaidą, kad yra tik viena šaka ir konfigūraciją galime identifikuoti įprastu būdu, naudodami tris skaičius, atskirtus tašku (1.2.3).
Šiuolaikinėje aplinkoje konfigūracijos failai retai kuriami rankiniu būdu. Dažniau jie generuojami diegiant ir nebeliečiami (taigi
Šiame įraše išnagrinėsime idėją pateikti konfigūraciją sudarytame artefakte.
Sukompiliuota konfigūracija
Šiame skyriuje pateikiamas statinės sudarytos konfigūracijos pavyzdys. Įdiegtos dvi paprastos paslaugos – echo paslauga ir echo paslaugos klientas. Remiantis šiomis dviem paslaugomis, surenkamos dvi sistemos parinktys. Vienu variantu abi paslaugos yra tame pačiame mazge, kitu variantu – skirtinguose mazguose.
Paprastai paskirstytoje sistemoje yra keli mazgai. Galite nustatyti mazgus naudodami tam tikro tipo reikšmes NodeId
:
sealed trait NodeId
case object Backend extends NodeId
case object Frontend extends NodeId
arba
case class NodeId(hostName: String)
или даже
object Singleton
type NodeId = Singleton.type
Mazgai atlieka įvairius vaidmenis, vykdo paslaugas ir tarp jų galima užmegzti TCP/HTTP ryšius.
Norint apibūdinti TCP ryšį, mums reikia bent prievado numerio. Taip pat norėtume atspindėti tame prievade palaikomą protokolą, kad įsitikintume, jog klientas ir serveris naudoja tą patį protokolą. Mes apibūdinsime ryšį naudodami šią klasę:
case class TcpEndPoint[Protocol](node: NodeId, port: Port[Protocol])
kur Port
- tik sveikasis skaičius Int
nurodant priimtinų verčių diapazoną:
type PortNumber = Refined[Int, Closed[_0, W.`65535`.T]]
Rafinuoti tipai
Žiūrėti biblioteką
HTTP (REST) protokolams, be prievado numerio, mums taip pat gali prireikti kelio į paslaugą:
type UrlPathPrefix = Refined[String, MatchesRegex[W.`"[a-zA-Z_0-9/]*"`.T]]
case class PortWithPrefix[Protocol](portNumber: PortNumber, pathPrefix: UrlPathPrefix)
Fantomų tipai
Norėdami nustatyti protokolą kompiliavimo metu, naudojame tipo parametrą, kuris klasėje nenaudojamas. Tokį sprendimą nulėmė tai, kad vykdymo metu nenaudojame protokolo egzemplioriaus, tačiau norėtume, kad kompiliatorius patikrintų protokolo suderinamumą. Nurodę protokolą netinkamos paslaugos negalėsime perduoti kaip priklausomybės.
Vienas iš įprastų protokolų yra REST API su Json serializavimu:
sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]
kur RequestMessage
- prašymo tipas, ResponseMessage
- atsakymo tipas.
Žinoma, galime naudoti kitus protokolų aprašymus, kurie suteikia mums reikalingą aprašymo tikslumą.
Šiame įraše naudosime supaprastintą protokolo versiją:
sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]
Čia užklausa yra eilutė, pridėta prie url, o atsakymas yra grąžinama eilutė HTTP atsakymo tekste.
Paslaugos konfigūracija apibūdinama paslaugos pavadinimu, prievadais ir priklausomybėmis. Šiuos elementus „Scala“ galima pavaizduoti keliais būdais (pvz., HList
-s, algebrinių duomenų tipai). Šio įrašo tikslais naudosime torto šabloną ir pateiksime modulius trait
'ov. (Torto šablonas nėra būtinas šio požiūrio elementas. Tai tiesiog vienas iš galimų įgyvendinimo variantų.)
Priklausomybės tarp paslaugų gali būti pavaizduotos kaip metodai, grąžinantys prievadus EndPoint
kitų mazgų:
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)
}
Norint sukurti aido paslaugą, tereikia prievado numerio ir nuorodos, kad prievadas palaiko echo protokolą. Galime nenurodyti konkretaus prievado, nes... bruožai leidžia deklaruoti metodus be įgyvendinimo (abstraktūs metodai). Tokiu atveju, kurdamas konkrečią konfigūraciją, kompiliatorius pareikalautų, kad pateiktume abstrakčiojo metodo įgyvendinimą ir prievado numerį. Kadangi mes įdiegėme metodą, kurdami konkrečią konfigūraciją galime nenurodyti kito prievado. Bus naudojama numatytoji reikšmė.
Kliento konfigūracijoje deklaruojame priklausomybę nuo echo paslaugos:
trait EchoClientConfig[A] {
def testMessage: String = "test"
def pollInterval: FiniteDuration
def echoServiceDependency: HttpSimpleGetEndPoint[_, EchoProtocol[A]]
}
Priklausomybė yra to paties tipo kaip ir eksportuota paslauga echoService
. Visų pirma, echo kliente mums reikia to paties protokolo. Todėl sujungę dvi paslaugas galime būti tikri, kad viskas veiks tinkamai.
Paslaugų įgyvendinimas
Norint paleisti ir sustabdyti paslaugą, reikalinga funkcija. (Galimybė sustabdyti paslaugą yra labai svarbi atliekant testavimą.) Vėlgi, yra keletas tokios funkcijos įdiegimo variantų (pavyzdžiui, galime naudoti tipų klases pagal konfigūracijos tipą). Šio įrašo tikslais naudosime torto modelį. Atstovausime paslaugą naudodami klasę cats.Resource
, nes Ši klasė jau suteikia priemones saugiai garantuoti išteklių atleidimą iškilus problemoms. Norėdami gauti išteklių, turime pateikti konfigūraciją ir paruoštą vykdymo laiko kontekstą. Paslaugos paleidimo funkcija gali atrodyti taip:
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]
}
kur
Config
— šios paslaugos konfigūracijos tipasAddressResolver
- vykdymo objektas, leidžiantis sužinoti kitų mazgų adresus (žr. toliau)
ir kitų tipų iš bibliotekos cats
:
F[_]
- efekto tipas (paprasčiausiu atvejuF[A]
gali būti tik funkcija() => A
. Šiame įraše naudosimecats.IO
.)Reader[A,B]
- daugiau ar mažiau funkcijos sinonimasA => B
cats.Resource
- išteklius, kurį galima gauti ir išleistiTimer
— laikmatis (leidžia trumpam užmigti ir matuoti laiko intervalus)ContextShift
- analogasExecutionContext
Applicative
— efektų tipo klasė, leidžianti derinti atskirus efektus (beveik monada). Sudėtingesnėse programose atrodo geriau naudotiMonad
/ConcurrentEffect
.
Naudodami šį funkcijos parašą galime įdiegti keletą paslaugų. Pavyzdžiui, paslauga, kuri nieko nedaro:
trait ZeroServiceImpl[F[_]] extends ServiceImpl[F] {
type Config <: Any
def resource(...): ResourceReader[F, Config, Unit] =
Reader(_ => Resource.pure[F, Unit](()))
}
(Cm.
и
Mazgas yra objektas, galintis paleisti kelias paslaugas (resursų grandinės paleidimą užtikrina torto šablonas):
object SingleNodeImpl extends ZeroServiceImpl[IO]
with EchoServiceService
with EchoClientService
with FiniteDurationLifecycleServiceImpl
{
type Config = EchoConfig[String] with EchoClientConfig[String] with FiniteDurationLifecycleConfig
}
Atkreipkite dėmesį, kad mes nurodome tikslų konfigūracijos tipą, kurio reikia šiam mazgui. Jei pamiršime nurodyti vieną iš konfigūracijos tipų, kurių reikia konkrečiai paslaugai, įvyks kompiliavimo klaida. Be to, negalėsime paleisti mazgo, jei nepateiksime kokio nors atitinkamo tipo objekto su visais reikalingais duomenimis.
Pagrindinio kompiuterio vardo raiška
Norėdami prisijungti prie nuotolinio kompiuterio, mums reikia tikro IP adreso. Gali būti, kad adresas taps žinomas vėliau nei likusi konfigūracija. Taigi mums reikia funkcijos, kuri susieja mazgo ID su adresu:
case class NodeAddress[NodeId](host: Uri.Host)
trait AddressResolver[F[_]] {
def resolve[NodeId](nodeId: NodeId): F[NodeAddress[NodeId]]
}
Yra keletas būdų, kaip įgyvendinti šią funkciją:
- Jei adresai mums tampa žinomi prieš įdiegiant, galime sugeneruoti Scala kodą su
adresus ir paleiskite statybą. Tai sukompiliuos ir vykdys testus.
Tokiu atveju funkcija bus žinoma statiškai ir gali būti pavaizduota kode kaip atvaizdavimasMap[NodeId, NodeAddress]
. - Kai kuriais atvejais tikrasis adresas sužinomas tik pradėjus mazgui.
Tokiu atveju galime įdiegti „atradimo paslaugą“, kuri veikia prieš kitus mazgus, o visi mazgai užsiregistruos šioje paslaugoje ir paprašys kitų mazgų adresų. - Jei galime modifikuoti
/etc/hosts
, tada galite naudoti iš anksto nustatytus pagrindinio kompiuterio pavadinimus (pvz.,my-project-main-node
иecho-backend
) ir tiesiog susiekite šiuos pavadinimus
su IP adresais diegimo metu.
Šiame įraše šių atvejų plačiau nenagrinėsime. Mums
žaislo pavyzdyje visi mazgai turės tą patį IP adresą - 127.0.0.1
.
Toliau svarstome dvi paskirstytos sistemos galimybes:
- Visų paslaugų išdėstymas viename mazge.
- Ir aido paslaugos bei aido kliento priegloba skirtinguose mazguose.
Konfigūracija skirta
Vieno mazgo konfigūracija
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.
}
Objektas įgyvendina ir kliento, ir serverio konfigūraciją. Taip pat naudojama laiko iki gyvavimo konfigūracija, kad po intervalo lifetime
nutraukti programą. (Ctrl-C taip pat veikia ir tinkamai atlaisvina visus išteklius.)
Tas pats konfigūracijos ir įgyvendinimo bruožų rinkinys gali būti naudojamas kuriant sistemą, kurią sudaro
Dviejų mazgų konfigūracija
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"
}
Svarbu! Atkreipkite dėmesį, kaip paslaugos yra susietos. Vieno mazgo įdiegtą paslaugą nurodome kaip kito mazgo priklausomybės metodo įgyvendinimą. Priklausomybės tipą tikrina kompiliatorius, nes yra protokolo tipas. Kai vykdoma, priklausomybėje bus tinkamas tikslinio mazgo ID. Šios schemos dėka tiksliai vieną kartą nurodome prievado numerį ir visada garantuojame, kad nurodysime tinkamą prievadą.
Dviejų sistemos mazgų įgyvendinimas
Šiai konfigūracijai naudojame tuos pačius paslaugų diegimus be pakeitimų. Vienintelis skirtumas yra tas, kad dabar turime du objektus, kurie įgyvendina skirtingus paslaugų rinkinius:
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
}
Pirmasis mazgas įgyvendina serverį ir jam reikia tik serverio konfigūracijos. Antrasis mazgas įgyvendina klientą ir naudoja kitą konfigūracijos dalį. Be to, abu mazgai turi valdyti visą gyvenimą. Serverio mazgas veikia neribotą laiką, kol jis sustabdomas SIGTERM
'om, ir kliento mazgas nutrūksta po kurio laiko. Cm.
Bendras kūrimo procesas
Pažiūrėkime, kaip šis konfigūravimo metodas veikia bendrą kūrimo procesą.
Konfigūracija bus sudaryta kartu su likusiu kodu ir bus sugeneruotas artefaktas (.jar). Atrodo, kad tikslinga konfigūraciją sudėti į atskirą artefaktą. Taip yra todėl, kad galime turėti kelias konfigūracijas pagal tą patį kodą. Vėlgi, galima generuoti artefaktus, atitinkančius skirtingas konfigūracijos šakas. Priklausomybės nuo konkrečių bibliotekų versijų išsaugomos kartu su konfigūracija ir šios versijos išsaugomos amžinai, kai tik nusprendžiame įdiegti tą konfigūracijos versiją.
Bet koks konfigūracijos pakeitimas virsta kodo pakeitimu. Ir todėl kiekvienas
pakeitimui bus taikomas įprastas kokybės užtikrinimo procesas:
Bilietas klaidų sekimo priemonėje -> PR -> peržiūra -> sujungti su atitinkamomis šakomis ->
integracija -> diegimas
Pagrindinės sukompiliuotos konfigūracijos įgyvendinimo pasekmės yra šios:
-
Konfigūracija bus vienoda visuose paskirstytos sistemos mazguose. Dėl to, kad visi mazgai gauna tą pačią konfigūraciją iš vieno šaltinio.
-
Sunku pakeisti konfigūraciją tik viename iš mazgų. Todėl „konfigūracijos nukrypimas“ mažai tikėtinas.
-
Tampa sunkiau atlikti nedidelius konfigūracijos pakeitimus.
-
Dauguma konfigūracijos pakeitimų įvyks kaip bendro kūrimo proceso dalis ir bus peržiūrimi.
Ar man reikia atskiros saugyklos gamybos konfigūracijai saugoti? Šioje konfigūracijoje gali būti slaptažodžių ir kitos neskelbtinos informacijos, prie kurios norėtume apriboti prieigą. Remiantis tuo, atrodo, kad tikslinga galutinę konfigūraciją saugoti atskiroje saugykloje. Galite padalyti konfigūraciją į dvi dalis – vienoje yra viešai pasiekiami konfigūracijos nustatymai, o kitoje – riboti parametrai. Tai leis daugumai kūrėjų turėti prieigą prie bendrų nustatymų. Šį atskyrimą lengva pasiekti naudojant tarpinius bruožus su numatytosiomis reikšmėmis.
Galimi variantai
Pabandykime palyginti sudarytą konfigūraciją su kai kuriomis įprastomis alternatyvomis:
- Teksto failas tiksliniame kompiuteryje.
- Centralizuota raktų vertės saugykla (
etcd
/zookeeper
). - Proceso komponentai, kuriuos galima iš naujo sukonfigūruoti / paleisti iš naujo nepaleidžiant proceso iš naujo.
- Konfigūracijos saugojimas už artefaktų ir versijų valdymo ribų.
Tekstiniai failai suteikia daug lankstumo atliekant nedidelius pakeitimus. Sistemos administratorius gali prisijungti prie nuotolinio mazgo, atlikti atitinkamų failų pakeitimus ir iš naujo paleisti paslaugą. Tačiau didelėms sistemoms toks lankstumas gali būti nepageidautinas. Atlikti pakeitimai nepalieka pėdsakų kitose sistemose. Pakeitimų niekas neperžiūri. Sunku nustatyti, kas tiksliai atliko pakeitimus ir dėl kokios priežasties. Pakeitimai nėra tikrinami. Jei sistema paskirstyta, administratorius gali pamiršti atlikti atitinkamus pakeitimus kituose mazguose.
(Taip pat reikėtų atkreipti dėmesį į tai, kad naudojant sukompiliuotą konfigūraciją, nebus panaikinta galimybė ateityje naudoti tekstinius failus. Pakaks pridėti analizatorių ir tikrintuvą, kuris sukuria tokio paties tipo kaip išvestis Config
ir galite naudoti tekstinius failus. Iš karto matyti, kad sistemos su sudaryta konfigūracija sudėtingumas yra šiek tiek mažesnis nei sistemos, naudojančios tekstinius failus, sudėtingumas, nes tekstiniams failams reikia papildomo kodo.)
Centralizuota raktų verčių saugykla yra geras paskirstytos programos metaparametrų paskirstymo mechanizmas. Turime nuspręsti, kas yra konfigūracijos parametrai, o kas – tik duomenys. Leiskite mums atlikti funkciją C => A => B
, ir parametrus C
retai keičiasi, ir duomenis A
- dažnai. Šiuo atveju galime pasakyti C
- konfigūracijos parametrai ir A
- duomenys. Atrodo, kad konfigūracijos parametrai skiriasi nuo duomenų tuo, kad paprastai keičiasi rečiau nei duomenys. Be to, duomenys dažniausiai gaunami iš vieno šaltinio (iš vartotojo), o konfigūracijos parametrai iš kito (iš sistemos administratoriaus).
Jei retai besikeičiančius parametrus reikia atnaujinti nepaleidžiant programos iš naujo, tai dažnai gali sukelti programos komplikacijų, nes mums reikės kažkaip pateikti parametrus, saugoti, analizuoti ir patikrinti bei apdoroti neteisingas reikšmes. Todėl programos sudėtingumo mažinimo požiūriu prasminga sumažinti parametrų, kurie gali keistis programos veikimo metu (arba iš viso nepalaikyti tokių parametrų), skaičių.
Šio įrašo tikslais skirsime statinius ir dinaminius parametrus. Jei paslaugos logika reikalauja keisti parametrus programos veikimo metu, tai tokius parametrus vadinsime dinaminiais. Kitu atveju parinktys yra statinės ir jas galima konfigūruoti naudojant sudarytą konfigūraciją. Dinaminiam perkonfigūravimui mums gali prireikti mechanizmo, leidžiančio iš naujo paleisti programos dalis su naujais parametrais, panašiai kaip operacinės sistemos procesai paleidžiami iš naujo. (Mūsų nuomone, patartina vengti perkonfigūravimo realiuoju laiku, nes tai padidina sistemos sudėtingumą. Jei įmanoma, procesams iš naujo paleisti, jei įmanoma, naudoti standartines OS galimybes.)
Vienas svarbus statinės konfigūracijos naudojimo aspektas, dėl kurio žmonės galvoja apie dinaminę konfigūraciją, yra laikas, per kurį sistema paleidžiama iš naujo po konfigūracijos atnaujinimo (prastova). Tiesą sakant, jei mums reikia pakeisti statinę konfigūraciją, turėsime iš naujo paleisti sistemą, kad naujos reikšmės įsigaliotų. Skirtingose sistemose prastovos problemos sunkumas skiriasi. Kai kuriais atvejais galite suplanuoti perkrovimą tuo metu, kai apkrova yra minimali. Jei reikia teikti nuolatines paslaugas, galite įgyvendinti
Dabar apsvarstykime konfigūracijos saugojimo artefakto viduje ar išorėje klausimą. Jei konfigūraciją saugome artefakte, tai bent jau turėjome galimybę patikrinti konfigūracijos teisingumą surenkant artefaktą. Jei konfigūracija nepatenka į valdomą artefaktą, sunku atsekti, kas ir kodėl atliko šio failo pakeitimus. Kiek tai svarbu? Mūsų nuomone, daugeliui gamybos sistemų svarbu turėti stabilią ir kokybišką konfigūraciją.
Artefakto versija leidžia nustatyti, kada jis buvo sukurtas, kokios reikšmės jame yra, kokios funkcijos įjungtos / išjungtos ir kas atsakingas už bet kokius konfigūracijos pakeitimus. Žinoma, norint išsaugoti konfigūraciją artefakte, reikia šiek tiek pastangų, todėl turite priimti pagrįstą sprendimą.
Privalumai ir trūkumai
Norėčiau trumpai aptarti siūlomos technologijos privalumus ir trūkumus.
privalumai
Žemiau pateikiamas pagrindinių sudarytos paskirstytos sistemos konfigūracijos ypatybių sąrašas:
- Statinės konfigūracijos patikrinimas. Leidžia tuo įsitikinti
konfigūracija teisinga. - Turtinga konfigūracijos kalba. Paprastai kiti konfigūravimo metodai apsiriboja ne daugiau kaip eilutės kintamųjų pakeitimu. Kai naudojate „Scala“, galite naudoti daugybę kalbinių funkcijų, kad pagerintumėte konfigūraciją. Pavyzdžiui, galime naudoti
Numatytųjų reikšmių bruožus, naudodami objektus parametrams sugrupuoti, įtraukiamojoje srityje galime nurodyti tik vieną kartą deklaruotas vertes (DRY). Galite sukurti bet kokias klases tiesiogiai konfigūracijos viduje (Seq
,Map
, pasirinktines klases). - DSL. „Scala“ turi daugybę kalbos funkcijų, kurios palengvina DSL kūrimą. Galima pasinaudoti šiomis funkcijomis ir įdiegti tikslinei vartotojų grupei patogesnę konfigūravimo kalbą, kad konfigūraciją bent jau perskaitytų domeno ekspertai. Pavyzdžiui, specialistai gali dalyvauti konfigūracijos peržiūros procese.
- Vientisumas ir sinchroniškumas tarp mazgų. Vienas iš privalumų, kai visos paskirstytos sistemos konfigūracija saugoma viename taške, yra tai, kad visos reikšmės deklaruojamos tiksliai vieną kartą, o vėliau panaudojamos ten, kur jų reikia. Naudojant fantominius tipus prievadams deklaruoti, užtikrinama, kad mazgai naudotų suderinamus protokolus visose teisingose sistemos konfigūracijose. Aiškios privalomos priklausomybės tarp mazgų užtikrina, kad visos paslaugos yra sujungtos.
- Aukštos kokybės pokyčiai. Konfigūracijos pakeitimai naudojant bendrą kūrimo procesą leidžia pasiekti aukštus kokybės standartus ir konfigūracijai.
- Konfigūracijos atnaujinimas vienu metu. Automatinis sistemos diegimas po konfigūracijos pakeitimų užtikrina, kad visi mazgai būtų atnaujinti.
- Paraiškos supaprastinimas. Programai nereikia analizuoti, tikrinti konfigūraciją ar tvarkyti neteisingas reikšmes. Tai sumažina programos sudėtingumą. (Kai kurios konfigūracijos sudėtingumas, pastebėtas mūsų pavyzdyje, nėra sudarytos konfigūracijos atributas, o tik sąmoningas sprendimas, nulemtas noro užtikrinti didesnį tipo saugumą.) Gana lengva grįžti prie įprastos konfigūracijos – tereikia įgyvendinti trūkstamą dalys. Todėl, pavyzdžiui, galite pradėti nuo sudarytos konfigūracijos, atidėdami nereikalingų dalių diegimą iki to laiko, kai to tikrai prireiks.
- Patvirtinta konfigūracija. Kadangi konfigūracijos pakeitimai atitinka įprastą bet kokių kitų pakeitimų likimą, gaunama išvestis yra artefaktas su unikalia versija. Tai leidžia, pavyzdžiui, prireikus grįžti prie ankstesnės konfigūracijos versijos. Galime naudoti net prieš metus buvusią konfigūraciją ir sistema veiks lygiai taip pat. Stabili konfigūracija pagerina paskirstytos sistemos nuspėjamumą ir patikimumą. Kadangi konfigūracija fiksuojama kompiliavimo etape, gana sunku ją suklastoti gamyboje.
- Moduliškumas. Siūloma sistema yra modulinė ir moduliai gali būti derinami įvairiais būdais kuriant skirtingas sistemas. Visų pirma, galite sukonfigūruoti sistemą, kad ji veiktų viename mazge viename įgyvendinimo variante, o kitame – keliuose mazguose. Galite sukurti kelias sistemos gamybos egzempliorių konfigūracijas.
- Testavimas. Atskiras paslaugas pakeitę netikrais objektais, galite gauti keletą patogių testavimui sistemos versijų.
- Integracijos testavimas. Turint vieną visos paskirstytos sistemos konfigūraciją, galima paleisti visus komponentus kontroliuojamoje aplinkoje kaip integracijos testavimo dalį. Nesunku imituoti, pavyzdžiui, situaciją, kai kai kurie mazgai tampa prieinami.
Trūkumai ir apribojimai
Sukompiliuota konfigūracija skiriasi nuo kitų konfigūravimo metodų ir gali būti netinkama kai kurioms programoms. Žemiau yra keletas trūkumų:
- Statinė konfigūracija. Kartais reikia greitai ištaisyti konfigūraciją gamyboje, apeinant visus apsauginius mechanizmus. Taikant šį metodą gali būti sunkiau. Bent jau vis tiek reikės kompiliavimo ir automatinio diegimo. Tai yra ir naudinga metodo savybė, ir kai kuriais atvejais trūkumas.
- Konfigūracijos generavimas. Jei konfigūracijos failą generuoja automatinis įrankis, gali prireikti papildomų pastangų norint integruoti kūrimo scenarijų.
- Įrankiai. Šiuo metu komunalinės paslaugos ir metodai, skirti dirbti su konfigūracija, yra pagrįsti tekstiniais failais. Ne visos tokios komunalinės paslaugos/technikos bus prieinamos sukompiliuotoje konfigūracijoje.
- Reikia keisti požiūrį. Kūrėjai ir „DevOps“ yra pripratę prie tekstinių failų. Pati konfigūracijos sudarymo idėja gali būti šiek tiek netikėta ir neįprasta ir sukelti atmetimą.
- Reikalingas aukštos kokybės kūrimo procesas. Norint patogiai naudoti sukompiliuotą konfigūraciją, būtina visiškai automatizuoti programos (CI/CD) kūrimo ir diegimo procesą. Priešingu atveju tai bus gana nepatogu.
Taip pat apsistokime ties keliais nagrinėjamo pavyzdžio apribojimais, nesusijusiais su sudarytos konfigūracijos idėja:
- Jei pateikiame nereikalingą konfigūracijos informaciją, kurios mazgas nenaudoja, kompiliatorius nepadės mums aptikti trūkstamo diegimo. Šią problemą galima išspręsti atsisakius tortų modelio ir naudojant griežtesnius tipus, pvz.
HList
arba algebriniai duomenų tipai (atvejų klasės), kad atvaizduotų konfigūraciją. - Konfigūracijos faile yra eilučių, nesusijusių su pačia konfigūracija: (
package
,import
,objektų deklaracijos;override def
's parametrams, kurių numatytosios reikšmės). To iš dalies galima išvengti, jei įdiegsite savo DSL. Be to, kitų tipų konfigūracija (pavyzdžiui, XML) taip pat nustato tam tikrus failo struktūros apribojimus. - Šio įrašo tikslais nesvarstome dinaminio panašių mazgų klasterio perkonfigūravimo.
išvada
Šiame įraše išnagrinėjome idėją pateikti konfigūraciją šaltinio kode naudojant išplėstines Scala tipo sistemos galimybes. Šis metodas gali būti naudojamas įvairiose programose kaip tradicinių konfigūravimo metodų, pagrįstų xml arba tekstiniais failais, pakaitalas. Nors mūsų pavyzdys įgyvendintas „Scala“, tos pačios idėjos gali būti perkeltos į kitas sudarytas kalbas (pvz., Kotlin, C#, Swift ir kt.). Galite išbandyti šį metodą viename iš toliau pateiktų projektų ir, jei jis neveikia, pereikite prie tekstinio failo, pridėdami trūkstamas dalis.
Žinoma, sukompiliuota konfigūracija reikalauja aukštos kokybės kūrimo proceso. Už tai užtikrinama aukšta konfigūracijų kokybė ir patikimumas.
Aptariamą metodą galima išplėsti:
- Galite naudoti makrokomandas kompiliavimo laiko patikrai atlikti.
- Galite įdiegti DSL, kad pateiktumėte konfigūraciją taip, kad ji būtų prieinama galutiniams vartotojams.
- Galite įdiegti dinaminį išteklių valdymą automatiniu konfigūracijos koregavimu. Pavyzdžiui, pakeitus mazgų skaičių klasteryje, reikia, kad (1) kiekvienas mazgas gautų šiek tiek skirtingą konfigūraciją; (2) klasterio valdytojas gavo informaciją apie naujus mazgus.
Padėkos
Norėčiau padėkoti Andrejui Saksonovui, Pavelui Popovui ir Antonui Nekhajevui už konstruktyvią straipsnio projekto kritiką.
Šaltinis: www.habr.com