Në këtë postim dëshirojmë të ndajmë një mënyrë interesante për të trajtuar konfigurimin e një sistemi të shpërndarë.
Konfigurimi paraqitet drejtpërdrejt në gjuhën Scala në një mënyrë të sigurt për tipet. Një zbatim shembull është përshkruar në detaje. Aspektet e ndryshme të propozimit diskutohet, përfshirë ndikimin në procesin e zhvillimit në tërësi.

()
Hyrje
Ndërtimi i sistemeve të shpërndara të besueshme kërkon përdorimin e një konfigurimi korrekt dhe koherent në të gjitha nodet. Një zgjidhje tipike është përdorimi i një përshkrimi tekstual të implementimit (terraform, ansible ose diçka të ngjashme) dhe skedarëve të konfigurimit të gjeneruara automatikisht (zakonisht — të dedikuar për secilën nodë/rol). Ne gjithashtu do të dëshironim të përdornim të njëjtat protokolle të të njëjtave versione në çdo nodë komunikimi (ndryshe do të përjetojmë probleme të papajtueshmërisë). Në botën JVM, kjo do të thotë se të paktën biblioteka e mesazheve duhet të jetë e të njëjtit version në të gjitha nodet komunikimi.
Çfarë ndodh me testimin e sistemit? Sigurisht, duhet të kemi teste unike për të gjitha komponentët përpara se të kalojmë në testet e integrimit. Që të kemi mundësinë ta ekstrapolojmë rezultatin e testeve në kohën e ekzekutimit, duhet të sigurohemi që versionet e të gjitha bibliotekave mbahen identike në të dy ambientet e ekzekutimit dhe testeve.
Kur ekzekutojmë testet e integrimit, shpesh është shumë më e lehtë të kemi të njëjtin classpath në të gjitha nodet. Ne thjesht duhet të sigurohemi që po përdoret i njëjti classpath në implementim. (Është e mundur të përdoren classpath të ndryshme në nodet e ndryshme, por është më e vështirë të përfaqësohet kjo konfigurim dhe ta implementojmë atë saktë.) Pra, për të mbajtur gjërat të thjeshta, do të konsiderojmë vetëm classpath identike në të gjitha nodet.
Konfigurimi ka prirjen të evoluojë së bashku me softuerin. Ne zakonisht përdorim versione për të identifikuar fazat e ndryshme të evolucionit të softuerit.
Duket e arsyeshme të mbulojmë konfigurimin nën menaxhimin e versioneve dhe të identifikojmë konfigurime të ndryshme me disa etiketat. Nëse ka vetëm një konfigurim në prodhim, mund të përdorim një version të vetëm si identifikues. Ndonjëherë mund të kemi mjedise të shumëfishta prodhimi. Dhe për secilin ambient mund të na nevojitet një degë e veçantë e konfigurimit. Pra, konfigurimet mund të etiketohen me degë dhe version për të identifikuar në mënyrë unike konfigurime të ndryshme. Çdo etiketë dege dhe version korrespondon me një kombinim të vetëm të nodave të shpërndara, porteve, burimeve të jashtme dhe versioneve të bibliotekës classpath në çdo nod. Këtu do të mbulojmë vetëm degën e vetme dhe do të identifikojmë konfigurimet me një version tre komponentësh në decimal (1.2.3), në të njëjtën mënyrë si artefaktet e tjera.
Në ambientet moderne, skedarët e konfigurimit nuk modifikohen më manualisht. Zakonisht ne gjenerojmë
skedarë konfigurimi në kohën e implementimit dhe pas kësaj. Pra, dikush mund të pyesë pse ne akoma përdorim formatin tekstual për skedarët e konfigurimit? Një opsion i vlefshëm është të vendosim konfigurimin brenda një njësie kompilimi dhe të përfitojmë nga validimi i konfigurimit në kohën e kompilimit.
Në këtë postim do të shqyrtojmë idenë e mbajtjes së konfigurimit në artefaktin e kompiluar.
Konfigurimi i kompilueshëm
Në këtë seksion do të diskutojmë një shembull të konfigurimit static. Dy shërbime të thjeshta — shërbimi echo dhe klienti i shërbimit echo po konfigurohen dhe zbatohen. Pastaj, dy sisteme të ndryshme të shpërndara me të dy shërbimet instancohen. Njëri është për një konfigurim me një nod dhe tjetri për një konfigurim me dy nod.
Një sistem tipik i shpërndarë përbëhet nga disa nod. Nodet mund të identifikohen duke përdorur një lloj:
sealed trait NodeId
case object Backend extends NodeId
case object Frontend extends NodeIdose thjesht
case class NodeId(hostName: String)ose madje
object Singleton
type NodeId = Singleton.typeKëto nodo kryejnë role të ndryshme, operejnë disa shërbime dhe duhet të jenë në gjendje të komunikojnë me nodet e tjera përmes lidhjeve TCP/HTTP.
Për lidhjen TCP të paktën një numër porte është i nevojshëm. Ne gjithashtu duam të sigurohemi që klienti dhe serveri po flasin të njëjtin protokoll. Për të modeluar një lidhje midis nodave le të shpallim klasën e mëposhtme:
kasa klasa TcpEndPoint[Protocol](node: NodeId, port: Port[Protocol])ku Port është thjesht një Int brenda gamës së lejuar:
type PortNumber = Refined[Int, Closed[_0, W.`65535`.T]]Tipet e rafinuara
Shiko bibliotekë. Në përmbledhje, ajo lejon të shtoni kufizime në kohën e kompilimit për tipe të tjera. Në këtë rast Int lejohet vetëm të ketë vlera 16-bit që mund të përfaqësojnë numrin e portit. Nuk ka ndonjë kërkesë për të përdorur këtë bibliotekë për këtë qasje konfigurimi. Ajo thjesht duket se përshtatet shumë mirë.
Për HTTP (REST) mund të kemi gjithashtu një rrugë të shërbimit:
type UrlPathPrefix = Refined[String, MatchesRegex[W.`"[a-zA-Z_0-9\/]*"`.T]]
kasa klasa PortWithPrefix[Protocol](portNumber: PortNumber, pathPrefix: UrlPathPrefix)Lloji fantazmë
Për të identifikuar protokollin gjatë kompilimit përdorim tiparin e Scala për shpalljen e argumentit të tipit Protokolli që nuk përdoret në klasë. Ai quhet lloj fantazmë. Në kohën e ekzekutimit shpesh ne nuk kemi nevojë për një instancë të identifikuesit të protokollit, kjo është arsyeja pse nuk e ruajmë. Gjatë kompilimit ky lloj fantazmë ofron siguri shtesë të tipit. Ne nuk mund të kalojmë portin me protokollin e gabuar.
Një nga protokollet më të përdorura është REST API me serializim Json:
sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]ku RequestMessage është tipi bazë i mesazheve që klienti mund të dërgojë në server dhe ResponseMessage është mesazhi i përgjigjes nga serveri. Sigurisht, ne mund të krijojmë përshkrime të tjera të protokollit që specifikojnë protokollin e komunikimit me saktësinë e dëshiruar.
Për qëllimet e këtij postimi ne do të përdorim një version më të thjeshtë të protokollit:
sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]Në këtë protokoll, mesazhi i kërkesës bashkangjitet në url dhe mesazhi i përgjigjes kthehet si varg i thjeshtë.
Një konfigurim shërbimi mund të përshkruhet nga emri i shërbimit, një koleksion portesh dhe disa varësi. Ka disa mënyra të mundshme se si të përfaqësohen të gjithë këta elementë në Scala (për shembull, HList, tipe të dhënash algebraike). Për qëllimet e këtij postimi ne do të përdorim Cake Pattern dhe do të përfaqësojmë pjesët e kombinuara (module) si tipare. (Cake Pattern nuk është një kërkesë për këtë qasje konfigurimi që mund të kompiloni. Ajo është vetëm një implementim i mundshëm i idesë.)
Varësitë mund të përfaqësohen duke përdorur Cake Pattern si skaje të node tjera:
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)
}Shërbimi Echo vetëm ka nevojë për një port të konfiguruar. Dhe ne shpallim se ky port mbështet protokollin echo. Vini re se ne nuk kemi nevojë të specifikojmë një port të veçantë në këtë moment, sepse tipari lejon shpalljen e metodave abstrakte. Nëse përdorim metoda abstrakte, kompajleri do të kërkojë një implementim në një instancë konfigurimi. Këtu kemi ofruar implementimin (8081) dhe ai do të përdoret si vlera e paracaktuar nëse e kalojmë atë në një konfigurim konkret.
Ne mund të shpallim një varësi në konfigurimin e klientit të shërbimit echo:
trait EchoClientConfig[A] {
def testMessage: String = "test"
def pollInterval: FiniteDuration
def echoServiceDependency: HttpSimpleGetEndPoint[_, EchoProtocol[A]]
}Varësia ka të njëjtin tip si shërbimi echo. Në veçanti, ajo kërkon të njëjtin protokoll. Prandaj, ne mund të jemi të sigurt se nëse lidhen këto dy varësi ato do të funksionojnë siç duhet.
Implementimi i shërbimeve
Një shërbim ka nevojë për një funksion për t'u nisur dhe mbyllur në mënyrë të qetë. (Aftësia për të mbyllur një shërbim është kritike për testimin.) Sërish, ka disa opsione për specifikimin e një funksioni të tillë për një konfigurim të dhënë (për shembull, mund të përdorim klasa tipi). Për këtë post ne përsëri do të përdorim Cake Pattern. Mund të përfaqësojmë një shërbim duke përdorur cats.Resource i cili ofron tashmë bllokim dhe çlirimin e burimeve. Për të marrë një burim, ne duhet të ofrojmë një konfigurim dhe disa kontekste në ekzekutim. Pra, funksioni fillues i shërbimit mund të duket si:
tipi 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]
}ku
Konfigurimi— lloji i konfigurimit që kërkohet nga ky fillues shërbimiAddressResolver— një objekt në ekzekutim që ka aftësinë të fitojë adresat reale të nyjeve të tjera (vijoni të lexoni për detaje).
të tjerat lloje vijnë nga cats:
F[_]— lloji i efektit (në rastin më të thjeshtëF[A]mund të jetë thjesht() => A. Në këtë postim ne do të përdorimcats.IO.)Reader[A,B]— është më shumë ose më pak një sinonim për një funksionA => Bcats.Resource— ka mënyra për të fituar dhe për të çliruarTimer— lejon të bëjmë gjumë / matësi i kohësContextShift— analog iExecutionContextApplicative— mbështjellës i funksioneve në efekt (pothuajse një monadë) (ne mund ta zëvendësojmë atë me diçka tjetër më vonë)
Duke përdorur këtë ndërfaqe mund të implementojmë disa shërbime. Për shembull, një shërbim që nuk bën asgjë:
trait ZeroServiceImpl[F[_]] extends ServiceImpl[F] {
type Config Resource.pure[F, Unit](()))
}(Shih për implementimet e tjera të shërbimeve — ,
dhe .)
Një nyje është një objekt i vetëm që ekzekuton disa shërbime (fillimi i një zinxhiri burimesh është i mundësuar nga Cake Pattern):
objekti SingleNodeImpl extend ZeroServiceImpl[IO]
me EchoServiceService
me EchoClientService
me FiniteDurationLifecycleServiceImpl
{
type Config = EchoConfig[String] me EchoClientConfig[String] me FiniteDurationLifecycleConfig
}Kujdes se në nyje specifikojmë llojin e saktë të konfigurimit që nevojitet nga kjo nyje. Kompileri nuk do të na lejojë të ndërtojmë objektin (Cake) me lloj të pamjaftueshëm, sepse çdo trait shërbimi shpall një kufizim në Konfigurimi llojin. Gjithashtu nuk do të jemi në gjendje të fillojmë nyjen pa ofruar një konfigurim të plotë.
Zgjidhja e adresës së nyjës
Për të krijuar një lidhje na nevojitet një adresë reale për çdo nyje. Ajo mund të jetë e njohur më vonë se pjesët e tjera të konfigurimit. Prandaj, na nevojitet një mënyrë për të ofruar një hartë midis ID të nyjës dhe adresës së saj aktuale. Kjo hartë është një funksion:
case class NodeAddress[NodeId](host: Uri.Host)
trait AddressResolver[F[_]] {
def resolve[NodeId](nodeId: NodeId): F[NodeAddress[NodeId]]
}Ka disa mënyra të mundshme për të implementuar një funksion të tillë.
- Nëse e dimë adresat reale para vendosjes, gjatë instancimit të hosteve të nyjave, atëherë mund të gjenerojmë kod Scala me adresat reale dhe të ekzekutojmë ndërtimin më pas (i cili kryen kontrollet në kohën e kompilimit dhe më pas ekzekuton paketën e testeve të integrimit). Në këtë rast, funksioni ynë i hartës është i njohur statikisht dhe mund të thjeshtohet në diçka si një
Map[NodeId, NodeAddress]. - Ndonjëherë ne marrim adresat reale vetëm në një moment të mëvonshëm kur nyja është në të vërtetë e nisur, ose nuk e kemi adresën e nyjeve që nuk janë nisur ende. Në këtë rast, mund të kemi një shërbim zbulimi që fillohet para të gjitha nyjeve të tjera dhe çdo nyje mund të reklamojë adresën e saj në atë shërbim dhe të abonoheni për varësitë.
- Nëse mund të modifikojmë
/etc/hosts, mund të përdorim emra hostesh të paracaktuar (sinyja-kryesore-miadheecho-backend) dhe thjesht të asociojmë emrin me adresën IP në kohën e vendosjes.
Në këtë postim nuk i mbulon këto raste më në detaje. Në fakt, në shembujin tonë, të gjitha nyjat do të kenë të njëjtën adresë IP — 127.0.0.1.
Në këtë postim do të shqyrtojmë dy skema të sistemeve të shpërndara:
- Skema me një nyje, ku të gjitha shërbimet janë vendosur në nyjen e vetme.
- Skema me dy nyje, ku shërbimi dhe klienti janë në nyje të ndryshme.
Konfigurimi për një skemë është si më poshtë:
Konfigurim të një node të vetëm
object SingleNodeConfig extends EchoConfig[String]
with EchoClientConfig[String] with FiniteDurationLifecycleConfig
{
case object Singleton \/\/ identifikuesi i nodit të vetëm
\/\/ konfigurimi i serverit
type NodeId = Singleton.type
def nodeId = Singleton
\/** Specifikimi i portit të shërbimit të sigurt. *\/
override def portNumber: PortNumber = 8088
\/\/ konfigurimi i klientit
\/** Ne do të përdorim shërbimin që ofrohet nga hosti të njëjtë. *\/
def echoServiceDependency = echoService
override def testMessage: UrlPathElement = "hello"
def pollInterval: FiniteDuration = 1.second
\/\/ konfigurimi i kontrolluesit të ciklit të jetës
def lifetime: FiniteDuration = 10500.milliseconds \/\/ 0.5 sekonda të tjera në mënyrë që të ketë 10 kërkesa, jo 9.
}Këtu krijojmë një konfigurim të vetëm që zgjerohet si nga konfigurimi i serverit ashtu edhe nga ai i klientit. Gjithashtu ne konfigurojmë një kontrollues të ciklit të jetës që do të përfundojë normalisht klientin dhe serverin pas jetës kalon intervali.
I njëjti grup i implementimeve dhe konfigurimeve të shërbimeve mund të përdoret për të krijuar një strukturë të sistemit me dy node të ndara. Ne vetëm duhet të krijojmë me shërbimet përkatëse:
Konfigurimi i dy nodave
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! specifikimi i varësisë
def echoServiceDependency = NodeServerConfig.echoService
def pollInterval: FiniteDuration = 1.second
def lifetime: FiniteDuration = 10500.milliseconds \/\/ 0.5 sekonda të tjera në mënyrë që të ketë 10 kërkesa, jo 9.
def testMessage: String = "dolly"
}Shihni si specifikojmë varësinë. Ne përmendim shërbimin e ofruar nga nodi tjetër si një varësi të nodit aktual. Lloji i varësisë kontrollohet sepse përmban një tip fantazmë që përshkruan protokollin. Dhe në kohën e ekzekutimit do të kemi ID-në e duhur të nodit. Ky është një nga aspektet e rëndësishme të qasjes së propozuar të konfigurimit. Ai na ofron mundësinë për të vendosur portin vetëm një herë dhe sigurohemi që po referohemi portit të duhur.
Implementimi i dy nodave
Për këtë konfigurim përdorim pikërisht të njëjtat implementime të shërbimeve. Asnjë ndryshim fare. Megjithatë, ne krijojmë dy implementime të ndryshme të nodave që përmbajnë një grup të ndryshëm shërbimesh:
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
}Nodi i parë implementon serverin dhe i nevojitet vetëm konfigurimi anësore të serverit. Nodi i dytë implementon klientin dhe i duhet një pjesë tjetër e konfigurimit. Të dy nodet kërkojnë disa specifikime të jetës. Për qëllimet e këtij postimi, nodi shërbim do të ketë një jetë të pafund që mund të mbyllet duke SIGTERM, ndërsa klienti i echo do të përfundojë pas kohës së caktuar të jetës. Shihni për detaje.
Procesi i përgjithshëm i zhvillimit
Le të shohim se si kjo qasje ndryshon mënyrën se si punojmë me konfigurimin.
Konfigurimi si kod do të përbëhet dhe do të prodhojë një artikull. Dukej e arsyeshme të ndahet artikulli i konfigurimit nga artikujt e tjerë të kodit. Shpesh ne mund të kemi një shumëllojshmëri konfigurimesh në të njëjtën bazë kodi. Dhe sigurisht, ne mund të kemi versione të shumta të degëve të ndryshme të konfigurimit. Në një konfigurim ne mund të zgjedhim versione të veçanta të bibliotekave dhe kjo do të mbetet e pandryshuar sa herë që ne deployme këtë konfigurim.
Një ndryshim në konfigurim bëhet ndryshim në kod. Prandaj duhet të mbështetet nga e njëjta proces sigurie të cilësisë:
Bileta -> PR -> rishikim -> bashko -> integrimi i vazhdueshëm -> shpërndarja e vazhdueshme
Pasojat e këtij qasje janë si më poshtë:
- Konfigurimi është koherent për një instancë të caktuar të sistemit. Duke u dukur, nuk ka mundësi të ketë lidhje të gabuara mes nyjave.
- Nuk është e lehtë të ndryshosh konfigurimin në një nyje. Duket e arsyeshme të hysh dhe të ndryshosh disa skedarë tekstualë. Kështu, drifting i konfigurimit bëhet më pak i mundur.
- Ndryshimet e vogla në konfigurim nuk janë lehtë të realizueshme.
- Shumica e ndryshimeve të konfigurimit do të ndjekin të njëjtin proces zhvillimi dhe do të kalojnë nëpër disa rishikime.
A na nevojitet një repository i veçantë për konfigurimin në prodhim? Konfigurimi në prodhim mund të përmbajë informata të ndjeshme që do të donim t'i mbajtshim jashtë arritjes së shumë njerëzve. Prandaj, do të kishte vlerë të mbajmë një repository të veçantë me akses të kufizuar që do të përmbante konfigurimin e prodhimit. Mund të ndajmë konfigurimin në dy pjesë - një që përmban shumicën e parametrave të hapur të prodhimit dhe një që përmban pjesën e fshehtë të konfigurimit. Kjo do të mundësonte akses për shumicën e zhvilluesve në shumicën e parametrave, ndërsa do të kufizonte aksesin për gjërat vërtet të ndjeshme. Është e lehtë ta realizosh këtë duke përdorur karakteristika të ndërmjetme me vlera parametrike të paracaktuara.
Variacione
Le të shohim përparësitë dhe disavantazhet e qasjes së propozuar në krahasim me teknikat e tjera të menaxhimit të konfigurimit.
Së pari, do të listojmë disa alternativa për aspektet e ndryshme të mënyrës së propozuar për të trajtuar konfigurimin:
- Skedari tekstual në makinën e synuar.
- Ruajtje të centralizuar të çelësave dhe vlerave (si
etcd/zookeeper). - Komponentë nënprocesi që mund të rikonfigurohen/riprikohen pa i restartuar proceset.
- Konfigurimi jashtë artefakteve dhe versioneve të kontrollit.
Skedari tekstual ofron disa fleksibilitete në terma të rregullimeve adhoc. Një administrator sistemi mund të hyjë në nyjën e synuar, të bëjë një ndryshim dhe thjesht të restartohet shërbimi. Kjo mund të mos jetë aq e mirë për sistemet më të mëdha. Nuk mbeten gjurmë pas ndryshimit. Ndryshimi nuk rishikohet nga një palë tjetër sytë. Mund të jetë e vështirë të zbulohet çfarë e ka shkaktuar ndryshimin. Nuk është testuar. Nga perspektiva e sistemit të shpërndarë, një administrator mund thjesht të harrojë të përditësojë konfigurimin në një nga nyjat e tjera.
(Për më tepër, nëse përfundimisht do të ketë nevojë të fillojmë të përdorim skedarë konfigurimi tekstualë, ne do të duhet vetëm të shtojmë parser + validator që do të mundësonte të prodhonte të njëjtin Konfigurimi lidhje dhe që do të ishte e mjaftueshme për të filluar përdorimin e skedarëve tekstualë të konfigurimit. Kjo gjithashtu tregon se kompleksiteti i konfigurimit në kohë kompile është pak më i vogël se kompleksiteti i konfigurimeve të bazuara në tekst, sepse në versionin e bazuar në tekst na nevojitet ndonjë kod shtesë.)
Ruajtja të centralizuar të çelësave dhe vlerave është një mekanizëm i mirë për shpërndarjen e parametrave meta të aplikacionit. Këtu duhet të mendojmë se çfarë e konsiderojmë si vlera konfigurimi dhe çfarë është thjesht të dhëna. Duke marrë një funksion C => A => B ne zakonisht i quajmë vlera që ndryshojnë rrallë C «konfigurim», ndërsa të dhënat që ndryshojnë shpesh A — thjesht të dhëna hyrëse. Konfigurimi duhet të ofrohet funksionit përpara se të dhënat A. Duke marrë këtë ide, mund të themi se është frekuenca e pritur e ndryshimeve që mund të përdoret për të dalluar të dhënat e konfigurimit nga thjesht të dhënat. Gjithashtu, të dhënat zakonisht vijnë nga një burim (përdorues) dhe konfigurimi vjen nga një burim tjetër (administrator). Trajtimi i parametrave që mund të ndryshojnë pas fillimit të procesit çon në një rritje të kompleksitetit të aplikacionit. Për këta parametra do të duhet të menaxhojmë mekanizmin e dorëzimit të tyre, analizimin dhe validimin, trajtimin e vlerave të gabuara. Prandaj, për të reduktuar kompleksitetin e programit, është më mirë të reduktojmë numrin e parametrave që mund të ndryshojnë gjatë ekzekutimit (ose madje t'i eleminojmë ato krejt).
Nga perspectiva e këtij postimi, ne duhet të bëjmë një dallim midis parametrave statikë dhe dinamikë. Nëse logjika e shërbimit kërkon ndryshime të rralla të disa parametrave gjatë ekzekutimit, atëherë mund t'i quajmë parametrat dinamikë. Përndryshe, ata janë statikë dhe mund të konfigurohen duke përdorur qasjen e propozuar. Për rikonfigurimin dinamik, mund të nevojiten qasje të tjera. Për shembull, pjesët e sistemit mund të ribëhen me parametrat e reja të konfigurimit në një mënyrë të ngjashme me ribërjen e proceseve të veçanta të një sistemi të shpërndarë.
(Mendimi im modest është të shmangim rikonfigurimin gjatë ekzekutimit sepse e rrit kompleksitetin e sistemit.
Mund të jetë më e drejtpërdrejtë të mbështetemi thjesht në mbështetje të OS për shkarkimin e proceseve. Megjithatë, kjo mund të mos jetë gjithmonë e mundur.)
Një aspekt i rëndësishëm i përdorimit të konfigurimit statik që ndonjëherë e bën njerëzit të mendojnë për konfigurimin dinamik (pa arsye të tjera) është koha e ndalesës së shërbimit gjatë azhurnimit të konfigurimit. Në të vërtetë, nëse na nevojiten ndryshime në konfigurimin statik, ne duhet të ribëjmë sistemin që të rendit vlerat e reja të bëhen efektive. Kërkesat për ndalesë ndryshojnë për sisteme të ndryshme, kështu që mund të mos jetë kritike. Nëse është kritike, atëherë duhet të planifikojmë përpara çdo ribërje të sistemit. Për shembull, mund të zbatojmë Në këtë skenar, sa herë që na nevojitet të ribëjmë sistemin, ne fillojmë një instancë të re të sistemit në mënyrë paralele, pastaj kalojmë ELB te ajo, ndërsa lejojmë sistemin e vjetër të përfundojë shërbimin e lidhjeve ekzistuese.
Çfarë është më mirë, të mbani konfigurimin brenda një artefakti të versionuar apo jashtë? Mbajtja e konfigurimit brenda një artefakti do të thotë se në shumicën e rasteve ky konfigurim ka kaluar të njëjtin proces të sigurimit të cilësisë si artefaktet e tjera. Kështu që mund të jem i sigurt se konfigurimi është i mirë dhe i besueshëm. Përkundrazi, konfigurimi në një skedar të veçantë do të thotë se nuk ka gjurmë se kush dhe përse bëri ndryshime në atë skedë. A është kjo e rëndësishme? Ne besojmë se për shumicën e sistemeve të prodhimit është më mirë të kemi një konfigurim të qëndrueshëm dhe me cilësi të lartë.
Versioni i artefaktit lejon të mesosh kur është krijuar, cilat vlera përmban, cilat tipare janë aktivizuar / çaktivizuar, kush ishte përgjegjës për çdo ndryshim në konfigurim. Mund të kërkojë disa përpjekje për të mbajtur konfigurimin brenda një artefakti dhe është një zgjedhje dizajni për ta bërë atë.
Përshtatjet dhe disfavors
Këtu dëshirojmë të theksojmë disa avantazhe dhe të diskutojmë disa disavantazhe të qasjes së propozuar.
Avantazhet
Karakteristikat e konfigurimit të kompilueshëm të një sistemi të plotë të shpërndarë:
- Kontrolli statik i konfigurimit. Kjo jep një nivel të lartë besimi, që konfigurimi është i saktë duke marrë parasysh kufizimet e tipit.
- Gjuha e pasur e konfigurimit. Në mënyrë tipike, qasjet e tjera të konfigurimit janë të kufizuara në maksimum në zëvendësimin e variablit.
Duke përdorur Scala, një mund të përdorë një gamë të gjerë veçorash të gjuhës për ta bërë konfigurimin më të mirë. Për shembull, ne mund të përdorim tipare për të ofruar vlera të paracaktuara, objekte për të vendosur përmasa të ndryshme, mund të referohemi nëvaltë përcaktuar vetëm një herë në shkallën e jashtme (DRY). Është e mundur të përdoren sekuenca literal, ose instance të klasave të caktuara (Sekuencë,Hartë, etj.). - DSL. Scala ka mbështetje të denjë për shkruesit e DSL. Një mund të përdorë këto veçori për të krijuar një gjuhë konfigurimi që është më e përshtatshme dhe miqësore për përdoruesit, në mënyrë që konfigurimi përfundimtar të jetë të paktën i lexueshëm nga përdoruesit e domainit.
- Integriteti dhe koherenca ndërmjet nyjeve. Një nga përfitimet e mbajtjes së konfigurimit për të gjithë sistemin e shpërndarë në një vend është se të gjitha vlerat janë të përcaktuara saktësisht një herë dhe pastaj rishfrytëzohen në të gjitha vendet ku na nevojiten. Gjithashtu, shpalljet e porteve të sigurta të llojit sigurojnë që në të gjitha konfigurimet e sakta të mundshme, nyjet e sistemit do të flasin të njëjtën gjuhë. Ka varësi eksplicite midis nyjeve që e bëjnë të vështirë të harrosh të ofrosh disa shërbime.
- Cilësi e lartë e ndryshimeve. Qasja e përgjithshme e kalimit të ndryshimeve në konfigurim përmes procesit normal të PR vendos standarde të larta të cilësisë edhe në konfigurim.
- Ndryshime të menjëhershme në konfigurim. Kurdo që ne bëjmë ndonjë ndryshim në konfigurim, implementimi automatik siguron që të gjitha nyjat të përditësohen.
- Thjeshtësimi i aplikacionit. Aplikacioni nuk ka nevojë të analizojë dhe verifikojë konfigurimin dhe të trajtojë vlerat e pasakta të konfigurimit. Kjo e thjeshton aplikacionin e përgjithshëm. (Disa ngecja të kompleksitetit janë në konfigurim vetë, por është një kompromis i vetëdijshëm drejt sigurisë.) Është mjaft e thjeshtë të kthehemi në konfigurimin e zakonshëm — thjesht shtoni copët e humbura. Është më e lehtë të filloni me konfigurimin e përpiluar dhe të shtyni implementimin e pjesëve të tjera për në një kohë më vonë.
- Konfigurimi i versionuar. Për shkak të faktit se ndryshimet në konfigurim ndjekin të njëjtin proces zhvillimi, si rezultat ne marrim një artefakt me version unik. Kjo na lejon të kthejmë konfigurimin nëse nevojitet. Ne madje mund të implementojmë një konfigurim që është përdorur një vit më parë dhe ai do të funksionojë saktësisht njësoj. Konfigurimi stabil përmirëson parashikueshmërinë dhe besueshmërinë e sistemit të shpërndarë. Konfigurimi është i ngurtësuar në kohën e përpilimit dhe nuk mund të manipulohet lehtësisht në një sistem prodhimi.
- Modulariteti. Korniza e propozuar është modulare dhe modulet mund të kombinohen në mënyra të ndryshme për
të mbështetur konfigurime të ndryshme (ndarje/format). Në veçanti, është e mundur të kemi një skemë të vogël me një nyjë dhe një vendosje të madhe me shumë nyje. Është e arsyeshme të kemi disa skema prodhimi. - Testimi. Për qëllime testimi, dikush mund të implementojë një shërbim fallco dhe ta përdorë atë si një varësi në një mënyrë të sigurt për tipin. Disa forma të ndryshme testimi me pjesë të ndryshme të zëvendësuara me fallco mund të mbahen njëkohësisht.
- Testimi i integrimit. Ndonjëherë në sistemet e shpërndara është e vështirë të realizohen teste integrimi. Duke përdorur qasjen e përshkruar për konfigurim të sigurt për tipin e sistemit të shpërndarë, ne mund të ekzekutojmë të gjitha pjesët e shpërndara në një server të vetëm në një mënyrë të kontrollueshme. Është e lehtë të imitosh situatën
kur një nga shërbimet bëhet e padisponueshme.
Dobitë
Qasja e përpilimit të konfigurimit është ndryshe nga konfigurimi «normal» dhe mund të mos përshtatet me të gjitha nevojat. Ja disa nga disavantazhet e konfigurimit të përpiluar:
- Konfigurim statik. Mund të mos jetë i përshtatshëm për të gjitha aplikacionet. Në disa raste ka nevojë për të rregulluar shpejt konfigurimin në prodhim duke anashkaluar të gjitha masat e sigurisë. Kjo qasje e bën më të vështirë. Përpilimi dhe ripërpilimi janë të nevojshme pas çdo ndryshimi në konfigurim. Kjo është si veçori ashtu edhe ngarkesë.
- Generimi i konfigurimit. Kur konfigurimi krijohet nga ndonjë mjet automatizimi, kjo qasje kërkon përpilimin e mëtejshëm (i cili gjithashtu mund të dështojë). Mund të kërkojë përpjekje të tjera për të integruar këtë hap shtesë në sistemin e ndërtimit.
- Instruments. Ka mjaft mjete që përdoren sot që mbështeten në konfigurime të bazuara në tekst. Disa prej tyre
nuk do të jenë të aplikueshëm kur konfigurimi është i përpiluar. - Një ndryshim në mendësi është i nevojshëm. Zhvilluesit dhe DevOps janë të njohur me skedat e konfigurimit të tekstit. Ideja e përpilimit të konfigurimit mund të duket e çuditshme për ta.
- Para se të prezantohet konfigurimi i përpilueshëm, kërkohet një proces zhvillimi softuerik me cilësi të lartë.
Ka disa kufizime të shembullit të implementuar:
- Nëse ne ofrojmë konfigurim shtesë që nuk kërkohet nga implementimi i nyjës, kompajleri nuk do të ndihmojë që të zbulojmë implementimin e munguar. Kjo mund të trajtohet duke përdorur
HListose ADT (klasa rastesh) për konfigurimin e nyjës në vend të tipave dhe Cake Pattern. - Duhet të ofrojmë disi boilerplate në skedën e konfigurimit: (
paketë,import,objektdeklaratat;
të anuloj def‘s për parametrat që kanë vlera standarde). Kjo mund të trajtohet pjesërisht duke përdorur një DSL. - Në këtë postim ne nuk mbulojmë ri-konfigurimin dinamik të grupeve të nyjave të ngjashme.
Përfundim
Në këtë postim kemi diskutuar idenë e paraqitjes së konfigurimit drejtpërdrejt në kodin burim në një mënyrë të sigurt për tipin. Qasja mund të përdoret në shumë aplikacione si një zëvendësim për konfigurimet në formatin xml dhe të tjera të bazuara në tekst. Pavarësisht se shembulli ynë është implementuar në Scala, ai gjithashtu mund të përkthehet në gjuhë të tjera kompiluese (si Kotlin, C#, Swift, etj.). Një mundësi është ta provoni këtë qasje në një projekt të ri dhe, nëse nuk funksionon mirë, të ktheheni në mënyrën e vjetër.
Sigurisht, konfigurimi i kompilueshëm kërkon një proces zhvillimi të cilësisë së lartë. Në këmbim, ai premton të ofrojë konfigurim të fortë me cilësi të njëjtë të lartë.
Kjo qasje mund të shtrihet në disa mënyra:
- Një mund të përdorë makro për të realizuar validimin e konfigurimit dhe të dështojë në kohën e kompilimit në rast të ndonjë dështimi të kufizimeve të logjikës së biznesit.
- Një DSL mund të implementohet për të paraqitur konfigurimin në një mënyrë miqësore për përdoruesin e domain-it.
- Menaxhimi dinamik i burimeve me rregullime automatikë të konfigurimit. Për shembull, kur rregullojmë numrin e nyjeve të grumbullit, mund të duam (1) që nyjet të marrin një konfigurim të modifikuar pak; (2) që menaxheri i grumbullit të marrë informacionin e ri për nyjet.
Faleminderit
Dua t'i them faleminderit Andrey Saksonov, Pavel Popov, Anton Nehaev për dhënien e feedback-ut inspirues mbi draftin e këtij postimi që më ndihmoi ta bëj më të qartë.
Burimi: habr.com
