Soovin rääkida ühest huvitavast mehhanismist jaotatud süsteemi konfiguratsiooniga töötamiseks. Konfiguratsioon on esitatud otse kompileeritavas keeles (Scala) kasutades ohutüüpe. Selles postituses on analüüsitud sellise konfiguratsiooni näidet ja kaalutud erinevaid aspekte kompileeritava konfiguratsiooni rakendamisest üldises arendusprotsessis.

()
Sissejuhatus
Usaldusväärse jaotatud süsteemi loomine eeldab, et kõikides sõlmedes kasutatakse õigesti konfigureeritud seadeid, mis on sünkroniseeritud teiste sõlmedega. Tüüpiliselt kasutatakse DevOpsi tehnoloogiaid (näiteks terraform, ansible või muud sarnased) konfiguratsioonifailide automaatseks genereerimiseks (tihti spetsiaalselt iga sõlme jaoks). Samuti tahame olla kindlad, et kõigis interakteeruvates sõlmedes kasutatakse identseid protokolle (sealhulgas sama versiooniga). Vastasel juhul tekib meie jaotatud süsteemis ühilduvuse probleem. JVM maailmas on sellise nõude tagajärjeks vajadus kasutada kõikjal sama versiooni teeki, mis sisaldab protokolli sõnumeid.
Kuidas on lood jaotatud süsteemi testimisega? Muidugi eeldame, et kõikide komponentide jaoks on ette nähtud unit-testid, enne kui liigume integreerimistestimise juurde. (Kuna me peame testimistulemusi runtime'ile projekteerima, peame samuti tagama, et testimise ja runtime'i etapis oleks identne teekide komplekt.)
Integratsioonitestide läbiviimisel on sageli lihtsam kasutada kõigil sõlmedel ühte classpathi. Me peame vaid tagama, et sama classpath oleks kasutusel ka runtime'is. (Kuigi on täiesti võimalik käivitada erinevaid sõlmi erinevate classpath'idega, toob see kaasa kogu konfigureerimise keerukuse ning raskused juurutamise ja integratsioonitestide läbiviimisel.) Selle postituse raames lähtume eeldusest, et kõikidel sõlmedel kasutatakse sama classpathi.
Konfiguratsioon areneb koos rakendusega. Erinevate tarkvaraversioonide määratlemiseks kasutame versioone. Tundub loogiline samuti tuvastada erinevaid konfiguratsiooniversioone. Ning ise konfiguratsioon tuleks paigutada versioonihaldussüsteemi. Kui produktsioonis on ainult üks konfiguratsioon, saame kasutada lihtsalt versiooninumbrit. Kui aga on kasutusel mitu produktsiooniversiooni, siis on vajalik mitmeid
konfiguratsiooni harud ja täiendav silt versiooni kõrval (nt haru nimi). Nii saame täpselt tuvastada konkreetse konfiguratsiooni. Iga konfiguratsiooni identifikaator vastab kindlasti teatud kombineeritud hajutatud sõlmedele, pordidele, välistablettide, teekide versioonidele. Selles postituses eeldame, et olemas on ainult üks haru, ja me saame tuvastada konfiguratsiooni tavapärasel viisil, kasutades kolme numbrit, mis on eraldatud punktiga (1.2.3).
Kaasaegsetes keskkondades luuakse konfiguratsioonifaile käsitsi üsna harva. Tihti genereeritakse need juurutamise käigus ja neid ei muudeta enam (et ). Tekib loogiline küsimus, miks me ikka veel kasutame konfiguratsiooni jaoks tekstivormingut? Täiesti elujõuline alternatiiv tundub olevat kasutada tavalist koodi konfiguratsiooniks ja saada kasu kompileerimise ajal tehtud kontrollidest.
Selles postituses uurimegi idee konfiguratsiooni esitamisest kompileeritud artefaktis.
Kompileeritav konfiguratsioon
Selles osas käsitletakse staatilise kompileeritava konfiguratsiooni näidet. Rakendatakse kahte lihtsat teenust — kaja teenus ja kaja teenuse klient. Nende kahe teenuse põhjal kogutakse kaks süsteemi varianti. Ühes variandis asuvad mõlemad teenused ühel sõlmel, teises variandis aga erinevatel sõlmedel.
Tavaliselt koosneb jaotatud süsteem mitmest sõlmest. Sõlmi saab tuvastada mõne tüüpi väärtuste abil. NodeId:
sealed trait NodeId
case object Backend extends NodeId
case object Frontend extends NodeIdvõi
case class NodeId(hostName: String)või isegi
object Singleton
type NodeId = Singleton.typeSõlmed täidavad erinevaid rolle, nendel töötavad teenused ja nende vahel võivad olla loodud TCP/HTTP sidemed.
TCP sideme kirjeldamiseks vajame vähemalt sadama numbrit. Soovime ka näidata protokolli, mida sellel sadamal toetatakse, et tagada, et nii klient kui ka server kasutavad sama protokolli. Kirjeldame ühendust järgmise klassiga:
case class TcpEndPoint[Protocol](node: NodeId, port: Port[Protocol])kus Sadama — lihtsalt täisarv Int näidatud lubatud väärtuste vahemikku:
type PortNumber = Refined[Int, Closed[_0, W.`65535`.T]]Täpsustatud tüübid
Vaata raamatukogu ja . Lühidalt, raamatukogu võimaldab lisada tüüpidele piirangud, mida kontrollitakse kompileerimise etapil. Antud juhul on lubatud sadama numbri väärtused 16-bitised täisarvud. Kompileeritava konfigureerimise puhul ei ole raamatukogu refined kasutamine kohustuslik, kuid see parandab kompilaatori võimet konfiguratsiooni kontrollida.
HTTP (REST) protokollide jaoks võib meil sadama numbri kõrval olla vajalik ka teenuse tee:
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 etapil kasutame tüüpi parameetrit, mida klassi sees ei kasutata. Selline lahendus on seotud sellega, et runtime’is me protokolli instantsi ei kasuta, kuid sooviksime, et kompilaator kontrolliks protokollide ühilduvust. Protokolli määratlemisega ei saa me edastada sobimatut teenust sõltuvusena.
Üks levinumaid protokolle on REST API koos Json-serialiseerimisega:
sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]kus RequestMessage — päringu tüüp, ResponseMessage — vastuse tüüp.
Muidugi, saab kasutada ka teisi protokollide kirjeldusi, mis tagavad vajalikku täpsust.
Käesoleva postituse eesmärkideks on meil lihtsustatud versioon protokollist:
sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]Siin esindab päring stringi, mis lisatakse url-ile, ja vastus on tekst, mis tagastatakse HTTP vastuse kehas.
Teenuse konfiguratsioon võetakse kokku teenuse nime, portide ja sõltuvuste kaudu. Nende elemente saab Scala-s esitada mitmeti (näiteks HList-de, algebraliste andmetüüpide kaudu). Käesoleva postituse jaoks kasutame Cake Pattern'i ja esindame mooduleid trait'de kaudu. (Cake Pattern ei ole kirjeldatud lähenemise kohustuslik element. See on lihtsalt üks võimalik rakendus.)
Teenuste vahelisi sõltuvusi saab esitada meetoditena, mis tagastavad portide EndPoint'ide teiste sõlmede kohta:
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 piisab ainult pordinumbri ja selle näitamisest, et see port toetab eho-protokolli. Me oleksime võinud ka konkreetset porti mitte näidata, kuna trait'id võimaldavad meetodeid deklareerida ilma implementatsioonita (abstraktsed meetodid). Sellisel juhul nõuaks kompilaator konkreetselt konfigureerimisel meilt abstraktse meetodi rakendamise kohta teabe esitamist ja pordinumbrit. Kuna me oleme meetodi rakendanud, võime konkreetse konfiguratsiooni loomisel mitte määrata teist porti. Selle asemel kasutatakse vaikeväärtust.
Kliendi konfiguratsioonis deklareerime sõltuvuse eho-teenusest:
trait EchoClientConfig[A] {
def testMessage: String = "test"
def pollInterval: FiniteDuration
def echoServiceDependency: HttpSimpleGetEndPoint[_, EchoProtocol[A]]
}Sõltuvusel on sama tüüp, mis eksporditud teenusel echoService. Ehkki eho-kliendis nõuame sama protokolli. Seetõttu saame kahe teenuse ühendamisel olla kindlad, et kõik töötab korrektselt.
Teenuste rakendamine
Teenuse käivitamiseks ja peatamiseks on vajalik funktsioon. (Teenuse peatamise võimalus on testimise jaoks kriitilise tähtsusega.) Jällegi on mitmeid võimalusi sellise funktsiooni elluviimiseks (näiteks võiksime kasutada konfiguratsiooni tüübi põhiseid klasside tüüpe). Käesoleva postituse raames kasutame Cake Pattern'it. Me esitleme teenust klassi cats.Resource, kuna see klass juba toetab turvalist ja garanteeritud ressursside vabastamist probleemide korral. Ressursi saamiseks peame pakkuma konfiguratsiooni ja valmis runtime-konteksti. Teenuse käivitamise funktsioon võiks 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— teenuse konfiguratsiooni tüüpAddressResolver— käituskeskkonna objekt, mis võimaldab teada saada teiste sõlmede aadresse (vt allpool)
ja teised tüübid raamatukogust cats:
F[_]— efekti tüüp (kõige lihtsamal juhulF[A]võib olla lihtsalt funktsioon() => A. Käesolevas postituses kasutamecats.IO.)Reader[A,B]— enam-vähem sünonüüm funktsioonileA => Bcats.Resource— ressurss, mida saab hankida ja vabastadaTimer— taimer (lubab magama jääda mõneks ajaks ja mõõta ajaintervvale)ContextShift— analoogExecutionContextApplicative— efekti tüüpide klass, mis võimaldab eraldi efekte (peaaegu monaad) ühendada. Tõenäoliselt on keerukamates rakendustes parem kasutadaMonad/ConcurrentEffect.
Selle funktsiooni signatuuri abil saame rakendada mitmeid teenuseid. Näiteks teenus, mis ei tee midagit:
trait ZeroServiceImpl[F[_]] extends ServiceImpl[F] {
type Config Resource.pure[F, Unit](()))
}(Vaata , kus on rakendatud teisi teenuseid — ,
ja .)
Sõlm on objekt, mis suudab käivitada mitu teenust (ressursside käivitamise ahel on tagatud Cake Pattern’i abil):
object SingleNodeImpl extends ZeroServiceImpl[IO]
with EchoServiceService
with EchoClientService
with FiniteDurationLifecycleServiceImpl
{
type Config = EchoConfig[String] with EchoClientConfig[String] with FiniteDurationLifecycleConfig
}Pange tähele, et näitame selle sõlme vajalikku täpset konfiguratsioonitüüpi. Kui me unustame märkida mõne konfiguratsioonitüübi, mida konkreetne teenus nõuab, tõuseb kompileerimisviga. Samuti ei saa me sõlme käivitada, kui me ei esita mõnda objekti, millel on sobiv tüüp koos kõigi vajalike andmetega.
Sõlmede nimede lahendamine
Kaugsõlmega ühendamiseks vajame reaalset IP-aadressi. On täiesti võimalik, et aadress saab teada hiljem kui muud konfiguratsiooni osad. Seetõttu vajame funktsiooni, mis kuvab sõlme ID aadressiks:
case class NodeAddress[NodeId](host: Uri.Host)
trait AddressResolver[F[_]] {
def resolve[NodeId](nodeId: NodeId): F[NodeAddress[NodeId]]
}Võib ettepanek teha mitmeid viise selle funktsiooni rakendamiseks:
- Kui aadressid saavad meile teada enne juurutamist, saame genereerida Scala-koodi koos
aadressidega ja seejärel käivitada kogumise. Sel juhul toimub kompileerimine ja testide läbiviimine.
Sellisel juhul on funktsioon staatiliselt teada ja seda saab koodis esitada kaardistamisenaMap[NodeId, NodeAddress]. - Mõnel juhul selgub kehtiv aadress alles pärast sõlme käivitamist.
Selle olukorra korral saame rakendada avastusteenust (discovery), mis käivitub enne teisi sõlmi ja kõik sõlmed registreeritakse selles teenuses ning küsivad teiste sõlmede aadresse. - Kui me saame modifitseerida
/etc/hosts, siis on võimalik kasutada eeldefineeritud hostinimesid (ntmy-project-main-nodejaecho-backend) ja lihtsalt siduda need nimed
IP-aadressidega juurutamise käigus.
Selles postituses ei käsitleme neid juhtumeid põhjalikumalt. Meie
mängu näites on kõigil sõlmedel üks IP-aadress — 127.0.0.1.
Edasi vaatame kahte jaotussüsteemi varianti:
- Kõik teenused on ühel sõlmel.
- Ja ekso teenuse ja ekso kliendi paigutamine erinevatele sõlmedele.
Konfiguratsioon ühele sõlmele :
Ühe sõlme konfiguratsioon
object SingleNodeConfig extends EchoConfig[String]
with EchoClientConfig[String] with FiniteDurationLifecycleConfig
{
case object Singleton // identifier of the single node
// configuration of server
type NodeId = Singleton.type
def nodeId = Singleton
/** Type safe service port specification. */
override def portNumber: PortNumber = 8088
// configuration of client
/** We'll use the service provided by the same host. */
def echoServiceDependency = echoService
override def testMessage: UrlPathElement = "hello"
def pollInterval: FiniteDuration = 1.second
// lifecycle controller configuration
def lifetime: FiniteDuration = 10500.milliseconds // additional 0.5 seconds so that there are 10 requests, not 9.
}Objekt ümbritseb nii kliendi kui ka serveri konfiguratsiooni. Samuti kasutatakse eluea konfiguratsiooni, et möödunud intervalli jooksul eluea programm lõpetada. (Ctrl-C töötab ka ja vabastab kõik ressursid õigesti.)
Sama hulk trait'e konfiguratsiooni ja rakendusi saab kasutada süsteemi loomiseks, mis koosneb :
Kaks sõlme konfiguratsioon
objekt NodeServerConfig ulatub EchoConfig[String] koos SigTermLifecycleConfig
{
tüüp NodeId = NodeIdImpl
def nodeId = NodeServer
override def portNumber: PortNumber = 8080
}
objekt NodeClientConfig ulatub EchoClientConfig[String] koos FiniteDurationLifecycleConfig
{
// NB! sõltuvuse määratlemine
def echoServiceDependency = NodeServerConfig.echoService
def pollInterval: FiniteDuration = 1.second
def lifetime: FiniteDuration = 10500.milliseconds // veel 0.5 sekundit, et saaks 10 päringut, mitte 9.
def testMessage: String = "dolly"
}Oluline! Pange tähele, kuidas teenuste sidumist teostatakse. Näitame teenust, mida rakendab üks sõlm, sõltuvuse meetodi rakendusel teise sõlme jaoks. Sõltuvuse tüüpi kontrollib kompilaator, kuna see sisaldab protokolli tüüpi. Käivitamisel sisaldab sõltuvus sihtsõlme õiget identifikaatorit. Sarnase skeemi korral anname pordinumbri täpselt ühe korra ja garanteerime, et viitame alati õigele pordile.
Süsteemi kahe sõlme rakendamine
Selle konfiguratsiooni jaoks kasutame samu teenuste rakendusi ilma muudatusteta. Ainuke 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
}Esimese sõlme ülesanne on rakendada serverit, mis vajab ainult serveri konfiguratsiooni. Teine sõlm rakendab klienti ja kasutab teist konfiguratsiooni osa. Mõlemad sõlmed vajavad ka eluea haldamist. Serveri sõlm töötab piiramatult kaua, kuni see peatatakse. SIGTERMja kliendipoolne sõlm lõpetatakse teatud aja pärast. Vt. .
Üldine arendusprotsess
Vaata, kuidas see lähenemine konfigureerimisele mõjutab üldist arendusprotsessi.
Konfigureerimine kompileeritakse koos ülejäänud koodiga ja genereeritakse artefakt (.jar). Ilmselt on mõistlik paigutada konfiguratsioon eraldi artefakti. See tuleneb sellest, et meil võib olla mitu konfiguratsiooni sama koodi põhjal. Taaskord, artefakte saab genereerida, mis vastavad erinevatele konfiguratsiooni harudele. Koos konfiguratsiooniga salvestatakse sõltuvused konkreetsete raamatukogude versioonidelt ja need versioonid jäävad püsima igaveseks, olenemata sellest, millal me otsustame selle konfiguratsiooni versiooni deploy’ida.
Iga konfiguratsiooni muudatus muutub koodi muudatuseks. Seega, iga selline
muudatus hõlmatakse tavapärases kvaliteedi tagamise protsessis:
Pilet veahalduris -> PR -> ülevaatus -> liitmine vastavate harudega ->
integratsioon -> deploy
Kompileeritava konfiguratsiooni rakendamise peamised tagajärjed:
Konfigureerimine kooskõlastatakse kõigis jaotatud süsteemi sõlmedes. Kuna kõik sõlmed saavad sama konfiguratsiooni ühest allikast.
Ühe sõlme konfigureerimine on keeruline. Seetõttu on „konfigureerimise desünkroniseerimine“ (configuration drift) ebatõenäoline.
Väikeste muudatuste tegemine konfiguratsioonis muutub keerulisemaks.
Enamik konfiguratsiooni muudatusi toimub üldise arendusprotsessi käigus ja neid vaadatakse üle.
Kas on vaja eraldi hoidlat production-konfiguratsiooni säilitamiseks? Sellises konfiguratsioonis võivad olla paroolid ja muu salajane teave, millele soovime piire seada. Seetõttu tundub mõistlik hoida lõppkonfiguratsioon eraldi hoidlas. Konfiguratsiooni saab jagada kaheks osaks — üks, mis sisaldab avalikke konfiguratsiooni parameetreid, ja teine, mis sisaldab piiratud juurdepääsuga parameetreid. See võimaldab enamikel arendajatest juurdepääsu avalikele parameetritele. Sellise jagamise saavutamine ei ole keeruline, kasutades vahepealseid trait'e, mis sisaldavad vaikeväärtusi.
Võimalikud variatsioonid
Proovime võrrelda kompileeritud konfiguratsiooni mõnede levinud alternatiividega:
- Tekstifail sihtmasinas.
- Keskne võtme-väärtuse ladustamine (
etcd/zookeeper). - Protsessi komponendid, mida saab konfigureerida / taaskäivitada ilma kogu protsessi taaskäivitamiseta.
- Konfiguratsiooni hoidmine väljaspool artefakti ja versioonihaldus.
Tekstifailid pakuvad suurel määral paindlikkust väikeste muudatuste läbiviimisel. Süsteemiadministraator saab siseneda kaugõlale, teha vastavatesse failidesse muudatused ja teenuse uuesti käivitada. Suurte süsteemide korral võib selline paindlikkus aga olla soovimatu. Teisesüsteemides ei jää muudatusest jälgi. Keegi ei kontrolli muudatusi. On raske kindlaks teha, kes täpselt muudatused tegi ja miks. Muudatusi ei testita. Kui süsteem on jaotatud, võib administraator unustada sarnased muudatused teistel sülemitel teha.
(Samuti tuleks märkida, et kompileeritava konfiguratsiooni rakendamine ei välista tulevikus tekstifailide kasutamise võimalust. Piisab, kui lisada parser ja valideerija, mis annab välja sama tüüpi Konfiguratsioon, ja saab kasutada tekstifaile. Siit tuleneb otseselt, et kompileeritava konfiguratsiooni süsteemi keerukus on mõnevõrra väiksem kui tekstifaile 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 jagamiseks. Peame kindlaks tegema, mis on konfiguratsiooniparameetrid ja mis on lihtsalt andmed. Olgu meil funktsioon C => A => B, kuigi parameetrid C harva muutuvad, ja andmed A — sageli. Sel juhul saame öelda, et C — on konfiguratsiooniparameetrid, ja A — on andmed. Tundub, et konfiguratsiooniparameetrid erinevad andmetest selle poolest, et need muutuvad üldiselt harvemini kui andmed. Samuti saavad andmed tavaliselt ühelt allikalt (kasutajalt), samas kui konfiguratsiooniparameetrid tulevad teisest (süsteemi administraatorilt).
Kui harva muutuvaid parameetreid tuleb uuendada ilma programmi taaskäivitamiseta, võib see sageli viia programmi keerukuse suurenemiseni. Peame leidma viisi parameetrite edastamiseks, nende talletamiseks, parsimiseks, kontrollimiseks ja valeväärtuste töötlemiseks. Seetõttu on programmi keerukuse vähendamise seisukohalt mõistlik vähendada selliste parameetrite arvu, mis võivad programmi töö käigus muutuda (või selliseid parameetreid üldse mitte toetada).
Selle postituse kontekstis eristame staatilisi ja dünaamilisi parameetreid. Kui teenuse tööloogika nõuab parameetrite muutmist programmi käivitamise käigus, nimetame neid parameetreid dünaamilisteks. Vastupidisel juhul on parameetrid staatilised ja neid saab konfigureerida kompileeritava konfiguratsiooni abil. Dünaamiliseks ümberkonfigureerimiseks võib meil olla vajalik programmiosade taaskäivitamise mehhanism uute parameetritega, sarnaselt operatsioonisüsteemi protsesside taaskäivitamisele. (Meie arvates on soovitatav vältida reaalajas ümberkonfigureerimist, kuna see suurendab süsteemi keerukust. Kui see on võimalik, on parem kasutada opsüsteemi standardseid võimalusi protsesside taaskäivitamiseks.)
Üks olulisemaid aspekte staatilise konfiguratsiooni kasutamisel, mis paneb inimesi kaaluma dünaamilist ümberkonfigureerimist, on süsteemi taaskäivitamiseks vajalik aeg pärast konfiguratsiooni värskendamist (seisakuaeg). Tõepoolest, kui peame tegema muudatusi staatilises konfiguratsioonis, peame süsteemi taaskäivitama, et uued väärtused hakkaksid kehtima. Seisakuaea probleem on erinevas süsteemide puhul erineva tõsidusega. Mõnel juhul saab taaskäivitamist planeerida ajale, mil koormus on minimaalne. Juhul, kui on vajalik tagada katkematu teenus, on võimalik rakendada . Sel juhul, kui peame süsteemi taaskäivitama, käivitame selle süsteemi paralleelse eksemplari, suuname koormustasakaalustaja sellele, ja ootame, kuni vanad ühendused on lõppenud. Pärast seda, kui kõik vanad ühendused on lõpetatud, lülitame välja vana süsteemi eksemplari.
Vaatame nüüd üle konfiguratsiooni hoidmise küsimuse artefakti sees või väljaspool seda. Kui me hoiame konfiguratsiooni artefakti sees, siis vähemalt saame artefakti koostamise ajal veenduda, et konfiguratsioon on õige. Kui konfiguratsioon asub kontrollitavast artefaktist väljaspool, on raske jälgida, kes ja miks on sellesse faili muudatusi teinud. Miks see oluline 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älja lülitatud, ning kes vastutab mistahes konfiguratsiooni muudatuste eest. Loomulikult nõuab konfiguratsiooni hoidmine artefakti sees teatud jõupingutusi, seega tuleb teha teadlik otsus.
Plussid ja miinused
Sooviksin peatuda pakutava tehnoloogia eeliste ja puuduste juures.
Eelised
Allpool on loetletud peamised võimalused kompilatsiooniks mõeldud jaotatud süsteemi konfiguratsioonide jaoks:
- Konfiguratsiooni staatiline kontroll. See tagab, et
konfiguratsioon on õige. - Rikka keele konfiguratsioon. Üldiselt on muud konfiguratsioonimeetodid piiratud vaid stringimuutujate asendamisega. Scala kasutamisel avanevad laiemad keelevõimalused, et täiustada konfiguratsiooni. Näiteks saame kasutada
trait'e vaikimisi väärtuste jaoks, gruppida parameetreid objektidega, ning viidata üheselt (DRY) deklareeritud val'idele kattevates ulatuses. Saame otse konfiguratsiooni sees instantsi teha mistahes klassidest (Seq,Map, kasutajate loodud klassid). - DSL. Scalal on mitmeid keelemöglichkeiten, mis muudavad DSL-i loomise lihtsamaks. Saame kasutada neid võimalusi ja arendada välja konfiguratsioonikeelt, mis oleks sihtrühmale kasutajatele mugavam, nii et konfiguratsioon oleks vähemalt valdkonna spetsialistide poolt loetav. Spetsialistid saavad näiteks osaleda konfiguratsiooni ülevaatusprotsessis.
- Rikkumatus ja sünkroonsus sõlmede vahel. Üheks eeliseks, et kogu hajutatud süsteemi konfigureerimine on talletatud ühes punktis, on see, et kõik väärtused kuulutatakse täpselt üks kord ja seejärel kasutatakse neid igal pool, kus neid vajatakse. Fantoomtüüpide kasutamine portide kuulutamiseks tagab, et kõik süsteemi õigetes konfiguratsioonides kasutavad sõlmed ühilduvaid protokolle. Selgete nõudlike sõltuvuste olemasolu sõlmede vahel tagab, et kõik teenused on omavahel seotud.
- Kõrge kvaliteet muudatuste tegemisel. Ühe ja sama arendusprotsessi kasutamine konfigureerimise muutmiseks tagab, et konfigureerimisele kehtivad kõrged kvaliteedistandardid.
- Konfiguratsiooni samal ajal uuendamine. Süsteemi automaatne juurutamine pärast konfigureerimise muudatusi tagab, et kõik sõlmed saavad värskendused.
- Rakenduse lihtsustamine. Rakendus ei vaja parsimist, konfiguratsiooni kontrollimist ega vigaste väärtuste töötlemist. Seeläbi väheneb rakenduse keerukus. (Mõningane konfiguratsiooni keerukus, mida meie näites täheldatakse, ei ole kompileeritava konfiguratsiooni omadus, vaid lihtsalt teadlik otsus, mis tuleneb soovist tagada suuremat tüübiturvalisust.) Tavalise konfiguratsiooni juurde tagasimine on piisavalt lihtne — tuleb lihtsalt teostada puuduolevad osad. Seetõttu on võimalik alustada näiteks kompileeritava konfiguratsiooniga, lükates üleliigsete osade rakendamise edasi aega, mil see tõesti vajalik on.
- Versioonitud konfiguratsioon. Kuna konfiguratsiooni muudatused järgivad tavalist trajektoori nagu kõik teised muudatused, saame väljundina ainulaadse versiooniga objekti. See võimaldab meil näiteks vajadusel naasta eelmise konfiguratsiooni juurde. Me võime isegi kasutada aasta tagust konfiguratsiooni ja süsteem töötab täpselt samamoodi. Stabiilne konfiguratsioon parandab jaotatud süsteemi prognoositavust ja usaldusväärsust. Kuna konfiguratsioon on fikseeritud kompileerimise etapis, on seda üsna raske tootmises valehäälestada.
- Moodulsus. Pakutud raamistik on moodulaarne ning mooduleid saab kombineerida erinevates variatsioonides, et luua erinevaid süsteeme. Eriti on võimalik ühe variandiga seadistada süsteem töötama ühel sõlmel ja teisega mitmel sõlmel. Saame luua mitu konfiguratsiooni tootmisversioonide jaoks.
- Testimine. Asendades üksikud teenused mock-objektidega, saame mitu versiooni süsteemist, mis on testimiseks mugavad.
- Integratsioonitestimine. Kogu jaotatud süsteemi ühtne konfigureerimine võimaldab kõiki komponente käitada kontrollitud keskkonnas integratsioonitestimise raames. Näiteks on lihtne emuleerida olukorda, kus mõned sõlmed muutuvad kergesti kättesaamatuteks.
Puudused ja piirangud
Kompileeritav konfiguratsioon erineb teistest konfigureerimise lähenemistest ja ei pruugi sobida mõnedele rakendustele. Allpool on mõned puudused:
- Staatiline konfiguratsioon. Mõnikord on vajalik kiiresti muuta konfiguratsiooni tootmises, mööda minnes kõigist kaitsemehhanismidest. Selle lähenemise raames võib see olla keerulisem. Igal juhul on vähemalt kompileerimine ja automaatne juurutamine siiski vajalikud. See on samas nii lähenemise kasulik omadus kui ka mõnel juhul puudus.
- Konfiguratsiooni genereerimine. Kui konfiguratsioonifail genereeritakse automaatse skripti poolt, võivad tekkida täiendavad pingutused skripti integreerimisel.
- Tööriistad. Praegu põhinevad konfigureerimisega seotud utiliidid ja meetodid tekstifailidel. Mitte kõik sellised utiliidid/meetodid ei pruugi olla saadaval kompileeritava konfiguratsiooni korral.
- Vajatakse arusaamade muutmist. Arendajad ja DevOps on harjunud tekstifailidega. Isegi idee kompileeritava konfiguratsiooni osas võib olla veidi ootamatu ja harjumatu ning tekitada vastumeelsust.
- Vajalik on kõrgekvaliteediline arendusprotsess. Mugavuse tagamiseks kompileeritava konfiguratsiooni kasutamisel on vajalik täielik automatiseerimine rakenduse koostamise ja juurutamise protsessis (CI/CD). Ilma selleta võib see olla üsna ebamugav.
Jätkame ka arutelu mitmete piirangute üle, mis ei ole seotud kompileeritava konfiguratsiooni ideega:
- Kui me anname lisainfot konfiguratsiooni kohta, mida sõlm ei kasuta, siis kompilaator ei aita meil tuvastada, et rakendust ei ole. Selle probleemi saab lahendada, kui loobuda Cake Pattern’ist ja kasutada rangemaid tüüpe, näiteks,
HListvõi algebralisi andmetüüpe (case class’id) konfiguratsiooni esitamiseks. - Konfiguratsioonifailis on read, mis ei kuulu otseselt konfiguratsiooni alla: (
package,import, objekti deklareerimised;override def‘id vaikimisi väärtustega parameetrite jaoks). Osaliselt on võimalik sellest vältida, kui rakendada oma DSL-i. Lisaks kehtivad teiste tüüpi konfiguratsioonide (näiteks XML) kohta ka teatud piirangud faili struktuurile. - Selles postituses ei käsitle me sarnaste sõlmede klastri dünaamilist rekonstruktsiooni.
Kokkuvõte
Selles postituses vaatlesime idee, kuidas esitada konfiguratsiooni lähtekoodis, kasutades Scala tüüpide süsteemi arenenud võimalusi. Sellist lähenemist saab kasutada erinevates rakendustes traditsiooniliste XML- või tekstifailide põhiste konfigureerimismeetodite asendamiseks. Ehkki meie näide on rakendatud Scalal, saab samu ideid üle kanda ka teistesse kompileeritavatesse keeltesse (nt Kotlin, C#, Swift jne). Seda lähenemist saab katsetada ühes järgmistest projektidest ja juhul, kui see ei sobi, saab tagasi minna tekstifailide juurde, lisades puuduvad detailid.
Muidugi nõuab koostatud konfiguratsioon kvaliteetset arendusprotsessi. Selle asemel tagatakse konfiguratsioonide kõrge kvaliteet ja usaldusväärsus.
Uuritud lähenemisviisi saab laiendada:
- Makse autoregistreerimist saab kasutada kompileerimise ajal kontrollimiseks.
- Saab luua DSL-i, et esitada konfiguratsiooni kasutajate jaoks arusaadaval kujul.
- Saab rakendada dünaamilist ressursside haldamist koos konfiguratsiooni automaatse kohandamisega. Näiteks kui klusteris suureneb sõlmede arv, peab (1) iga sõlm saama veidi erineva konfiguratsiooni; (2) klusterihaldur peab saama teavet uute sõlmede kohta.
Tänud
Sooviksin tänada Andreid Saksonovi, Pavel Popovit ja Anton Nehaevit artikli mustandi konstruktiivse kriitika eest.
Allikas: habr.com
