Përpiluar konfigurimin e sistemit të shpërndarë

Do të doja t'ju tregoja një mekanizëm interesant për të punuar me konfigurimin e një sistemi të shpërndarë. Konfigurimi përfaqësohet drejtpërdrejt në një gjuhë të përpiluar (Scala) duke përdorur lloje të sigurta. Ky post ofron një shembull të një konfigurimi të tillë dhe diskuton aspekte të ndryshme të zbatimit të një konfigurimi të përpiluar në procesin e përgjithshëm të zhvillimit.

Përpiluar konfigurimin e sistemit të shpërndarë

(anglisht)

Paraqitje

Ndërtimi i një sistemi të besueshëm të shpërndarë do të thotë që të gjitha nyjet përdorin konfigurimin e duhur, të sinkronizuar me nyjet e tjera. Teknologjitë DevOps (terraform, ansible ose diçka e tillë) zakonisht përdoren për të gjeneruar automatikisht skedarë konfigurimi (shpesh specifikë për secilën nyje). Ne gjithashtu dëshirojmë të jemi të sigurt që të gjitha nyjet komunikuese përdorin protokolle identike (duke përfshirë të njëjtin version). Përndryshe, papajtueshmëria do të ndërtohet në sistemin tonë të shpërndarë. Në botën JVM, një pasojë e kësaj kërkese është se i njëjti version i bibliotekës që përmban mesazhet e protokollit duhet të përdoret kudo.

Po në lidhje me testimin e një sistemi të shpërndarë? Sigurisht, ne supozojmë se të gjithë komponentët kanë teste njësi përpara se të kalojmë në testimin e integrimit. (Në mënyrë që ne të ekstrapolojmë rezultatet e testit në kohën e ekzekutimit, duhet të ofrojmë gjithashtu një grup identik bibliotekash në fazën e testimit dhe në kohën e ekzekutimit.)

Kur punoni me testet e integrimit, shpesh është më e lehtë të përdoret e njëjta rrugë e klasës kudo në të gjitha nyjet. Gjithçka që duhet të bëjmë është të sigurohemi që e njëjta rrugë e klasës të përdoret në kohën e ekzekutimit. (Ndërsa është plotësisht e mundur të ekzekutohen nyje të ndryshme me shtigje të ndryshme klasash, kjo i shton kompleksitet konfigurimit të përgjithshëm dhe vështirësitë me testet e vendosjes dhe integrimit.) Për qëllimet e këtij postimi, ne po supozojmë se të gjitha nyjet do të përdorin të njëjtën rrugë të klasës.

Konfigurimi evoluon me aplikacionin. Ne përdorim versione për të identifikuar faza të ndryshme të evolucionit të programit. Duket logjike të identifikohen gjithashtu versione të ndryshme të konfigurimeve. Dhe vendoseni vetë konfigurimin në sistemin e kontrollit të versionit. Nëse ka vetëm një konfigurim në prodhim, atëherë thjesht mund të përdorim numrin e versionit. Nëse përdorim shumë instanca prodhimi, atëherë do të na duhen disa
degët e konfigurimit dhe një etiketë shtesë përveç versionit (për shembull, emri i degës). Në këtë mënyrë ne mund të identifikojmë qartë konfigurimin e saktë. Çdo identifikues i konfigurimit korrespondon në mënyrë unike me një kombinim specifik të nyjeve të shpërndara, porteve, burimeve të jashtme dhe versioneve të bibliotekës. Për qëllimet e këtij postimi, ne do të supozojmë se ka vetëm një degë dhe ne mund ta identifikojmë konfigurimin në mënyrën e zakonshme duke përdorur tre numra të ndarë me një pikë (1.2.3).

Në mjediset moderne, skedarët e konfigurimit rrallë krijohen manualisht. Më shpesh ato krijohen gjatë vendosjes dhe nuk preken më (në mënyrë që mos thyej asgjë). Shtrohet një pyetje e natyrshme: pse ende përdorim format teksti për të ruajtur konfigurimin? Një alternativë e mundshme duket të jetë aftësia për të përdorur kodin e rregullt për konfigurim dhe për të përfituar nga kontrollet në kohën e përpilimit.

Në këtë postim ne do të eksplorojmë idenë e përfaqësimit të një konfigurimi brenda një objekti të përpiluar.

Konfigurimi i përpiluar

Ky seksion ofron një shembull të një konfigurimi të përpiluar statik. Janë implementuar dy shërbime të thjeshta - shërbimi echo dhe klienti i shërbimit echo. Bazuar në këto dy shërbime, mblidhen dy opsione të sistemit. Në një opsion, të dy shërbimet janë të vendosura në të njëjtën nyje, në një opsion tjetër - në nyje të ndryshme.

Zakonisht një sistem i shpërndarë përmban disa nyje. Ju mund të identifikoni nyjet duke përdorur vlera të një lloji NodeId:

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

ose

case class NodeId(hostName: String)

ose madje

object Singleton
type NodeId = Singleton.type

Nyjet kryejnë role të ndryshme, ato drejtojnë shërbime dhe lidhjet TCP/HTTP mund të krijohen ndërmjet tyre.

Për të përshkruar një lidhje TCP, na duhet të paktën një numër porti. Ne gjithashtu dëshirojmë të pasqyrojmë protokollin që mbështetet në atë portë për të siguruar që klienti dhe serveri përdorin të njëjtin protokoll. Ne do ta përshkruajmë lidhjen duke përdorur klasën e mëposhtme:

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

ku Port - vetëm një numër i plotë Int duke treguar gamën e vlerave të pranueshme:

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

Llojet e rafinuara

Shihni bibliotekën i rafinuar и im raportin. Shkurtimisht, biblioteka ju lejon të shtoni kufizime për llojet që kontrollohen në kohën e përpilimit. Në këtë rast, vlerat e vlefshme të numrit të portit janë numra të plotë 16-bit. Për një konfigurim të përpiluar, përdorimi i bibliotekës së rafinuar nuk është i detyrueshëm, por përmirëson aftësinë e përpiluesit për të kontrolluar konfigurimin.

Për protokollet HTTP (REST), përveç numrit të portit, mund të na duhet edhe shtegu i shërbimit:

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

Llojet e fantazmave

Për të identifikuar protokollin në kohën e kompilimit, ne përdorim një parametër tipi që nuk përdoret brenda klasës. Ky vendim është për faktin se ne nuk përdorim një shembull protokolli në kohën e ekzekutimit, por do të dëshironim që përpiluesi të kontrollonte përputhshmërinë e protokollit. Duke specifikuar protokollin, ne nuk do të mund të kalojmë një shërbim të papërshtatshëm si varësi.

Një nga protokollet e zakonshme është API REST me serializimin Json:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

ku RequestMessage - lloji i kërkesës, ResponseMessage - lloji i përgjigjes.
Sigurisht, ne mund të përdorim përshkrime të tjera të protokollit që ofrojnë saktësinë e përshkrimit që kërkojmë.

Për qëllimet e këtij postimi, ne do të përdorim një version të thjeshtuar të protokollit:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Këtu kërkesa është një varg i bashkëngjitur url-së dhe përgjigja është vargu i kthyer në trupin e përgjigjes HTTP.

Konfigurimi i shërbimit përshkruhet nga emri i shërbimit, portat dhe varësitë. Këta elementë mund të përfaqësohen në Scala në disa mënyra (për shembull, HList-s, llojet e të dhënave algjebrike). Për qëllimet e këtij postimi, ne do të përdorim modelin e tortës dhe do të përfaqësojmë modulet duke përdorur trait'ov. (Modeli i tortës nuk është një element i kërkuar i kësaj qasjeje. Është thjesht një zbatim i mundshëm.)

Varësitë ndërmjet shërbimeve mund të përfaqësohen si metoda që kthejnë portet EndPointtë nyjeve të 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)
  }

Për të krijuar një shërbim echo, gjithçka që ju nevojitet është një numër porti dhe një tregues që porti mbështet protokollin echo. Mund të mos specifikojmë një port specifik, sepse... tiparet ju lejojnë të deklaroni metoda pa zbatim (metoda abstrakte). Në këtë rast, kur krijoni një konfigurim konkret, përpiluesi do të kërkonte nga ne të ofrojmë një implementim të metodës abstrakte dhe të sigurojmë një numër porti. Meqenëse ne kemi zbatuar metodën, kur krijojmë një konfigurim specifik, mund të mos specifikojmë një port tjetër. Do të përdoret vlera e paracaktuar.

Në konfigurimin e klientit ne deklarojmë një varësi nga shërbimi echo:

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

Varësia është e të njëjtit lloj si shërbimi i eksportuar echoService. Në veçanti, në klientin echo ne kërkojmë të njëjtin protokoll. Prandaj, kur lidhni dy shërbime, mund të jemi të sigurt se gjithçka do të funksionojë siç duhet.

Zbatimi i shërbimeve

Kërkohet një funksion për të nisur dhe ndaluar shërbimin. (Aftësia për të ndaluar shërbimin është kritike për testimin.) Përsëri, ka disa opsione për zbatimin e një veçorie të tillë (për shembull, ne mund të përdorim klasat e tipit bazuar në llojin e konfigurimit). Për qëllimet e këtij postimi ne do të përdorim modelin e tortës. Ne do të përfaqësojmë shërbimin duke përdorur një klasë cats.Resource, sepse Kjo klasë tashmë ofron mjete për të garantuar lirimin e sigurt të burimeve në rast të problemeve. Për të marrë një burim, ne duhet të sigurojmë konfigurimin dhe një kontekst të gatshëm të ekzekutimit. Funksioni i nisjes së shërbimit mund të duket si ky:

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

ku

  • Config — lloji i konfigurimit për këtë shërbim
  • AddressResolver - një objekt kohëzgjatjeje që ju lejon të gjeni adresat e nyjeve të tjera (shih më poshtë)

dhe lloje të tjera nga biblioteka cats:

  • F[_] - lloji i efektit (në rastin më të thjeshtë F[A] mund të jetë thjesht një funksion () => A. Në këtë postim do të përdorim cats.IO.)
  • Reader[A,B] - pak a shumë sinonim i funksionit A => B
  • cats.Resource - një burim që mund të merret dhe lëshohet
  • Timer — timer (ju lejon të bini në gjumë për një kohë dhe të matni intervalet kohore)
  • ContextShift - analog ExecutionContext
  • Applicative — një klasë e llojit të efektit që ju lejon të kombinoni efekte individuale (pothuajse një monadë). Në aplikacione më komplekse duket më mirë të përdoret Monad/ConcurrentEffect.

Duke përdorur këtë nënshkrim funksioni, ne 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 <: Any
    def resource(...): ResourceReader[F, Config, Unit] =
      Reader(_ => Resource.pure[F, Unit](()))
  }

(Cm. burim, në të cilën zbatohen shërbime të tjera - shërbim jehonë, klient jehonë
и kontrollorët gjatë gjithë jetës.)

Një nyje është një objekt që mund të nisë disa shërbime (hapja e një zinxhiri burimesh sigurohet nga Modeli i tortës):

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

Ju lutemi vini re se ne po specifikojmë llojin e saktë të konfigurimit që kërkohet për këtë nyje. Nëse harrojmë të specifikojmë një nga llojet e konfigurimit të kërkuar nga një shërbim i caktuar, do të ketë një gabim përpilimi. Gjithashtu, ne nuk do të jemi në gjendje të nisim një nyje nëse nuk ofrojmë një objekt të llojit të duhur me të gjitha të dhënat e nevojshme.

Rezolucioni i emrit të hostit

Për t'u lidhur me një host të largët, na duhet një adresë IP e vërtetë. Është e mundur që adresa të bëhet e njohur më vonë se pjesa tjetër e konfigurimit. Pra, ne kemi nevojë për një funksion që harton ID-në e nyjes në një adresë:

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

Ka disa mënyra për të zbatuar këtë funksion:

  1. Nëse adresat na bëhen të njohura përpara vendosjes, atëherë mund të gjenerojmë kodin Scala me
    adresat dhe më pas ekzekutoni ndërtimin. Kjo do të përpilojë dhe ekzekutojë teste.
    Në këtë rast, funksioni do të njihet në mënyrë statike dhe mund të përfaqësohet në kod si një hartë Map[NodeId, NodeAddress].
  2. Në disa raste, adresa aktuale njihet vetëm pasi të ketë filluar nyja.
    Në këtë rast, ne mund të implementojmë një "shërbim zbulimi" që funksionon përpara nyjeve të tjera dhe të gjitha nyjet do të regjistrohen në këtë shërbim dhe do të kërkojnë adresat e nyjeve të tjera.
  3. Nëse mund të modifikojmë /etc/hosts, atëherë mund të përdorni emra të paracaktuar të hosteve (si p.sh my-project-main-node и echo-backend) dhe thjesht lidhni këta emra
    me adresat IP gjatë vendosjes.

Në këtë postim ne nuk do t'i shqyrtojmë më në detaje këto raste. Për tonë
në një shembull lodër, të gjitha nyjet do të kenë të njëjtën adresë IP - 127.0.0.1.

Më pas, ne konsiderojmë dy opsione për një sistem të shpërndarë:

  1. Vendosja e të gjitha shërbimeve në një nyje.
  2. Dhe pritja e shërbimit echo dhe klientit echo në nyje të ndryshme.

Konfigurimi për një nyje:

Konfigurimi i një nyje të vetme

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

Objekti zbaton konfigurimin si të klientit ashtu edhe të serverit. Përdoret gjithashtu një konfigurim kohë-to-live në mënyrë që pas intervalit lifetime përfundojnë programin. (Ctrl-C gjithashtu funksionon dhe çliron të gjitha burimet në mënyrë korrekte.)

I njëjti grup tiparesh konfigurimi dhe zbatimi mund të përdoret për të krijuar një sistem të përbërë nga dy nyje të veçanta:

Konfigurimi me dy nyje

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

E rëndësishme! Vini re se si janë të lidhura shërbimet. Ne specifikojmë një shërbim të zbatuar nga një nyje si një zbatim i metodës së varësisë së një nyje tjetër. Lloji i varësisë kontrollohet nga përpiluesi, sepse përmban llojin e protokollit. Kur ekzekutohet, varësia do të përmbajë ID-në e saktë të nyjës së synuar. Falë kësaj skeme, ne specifikojmë saktësisht një herë numrin e portit dhe gjithmonë garantohet t'i referohemi portit të duhur.

Implementimi i dy nyjeve të sistemit

Për këtë konfigurim, ne përdorim të njëjtat zbatime të shërbimit pa ndryshime. Dallimi i vetëm është se ne tani kemi dy objekte që zbatojnë grupe të ndryshme 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
  }

Nyja e parë zbaton serverin dhe ka nevojë vetëm për konfigurimin e serverit. Nyja e dytë implementon klientin dhe përdor një pjesë të ndryshme të konfigurimit. Gjithashtu të dy nyjet kanë nevojë për menaxhim gjatë gjithë jetës. Nyja e serverit funksionon pafundësisht derisa të ndalet SIGTERM'om, dhe nyja e klientit përfundon pas njëfarë kohe. Cm. aplikacioni lëshues.

Procesi i përgjithshëm i zhvillimit

Le të shohim se si kjo qasje e konfigurimit ndikon në procesin e përgjithshëm të zhvillimit.

Konfigurimi do të përpilohet së bashku me pjesën tjetër të kodit dhe do të gjenerohet një objekt (.jar). Duket se ka kuptim të vendosësh konfigurimin në një objekt të veçantë. Kjo është për shkak se ne mund të kemi konfigurime të shumta bazuar në të njëjtin kod. Përsëri, është e mundur të gjenerohen objekte që korrespondojnë me degë të ndryshme konfigurimi. Varësitë nga versionet specifike të bibliotekave ruhen së bashku me konfigurimin dhe këto versione ruhen përgjithmonë sa herë që vendosim të vendosim atë version të konfigurimit.

Çdo ndryshim i konfigurimit kthehet në një ndryshim kodi. Dhe për këtë arsye, secili
ndryshimi do të mbulohet nga procesi normal i sigurimit të cilësisë:

Bileta në gjurmuesin e gabimeve -> PR -> rishikim -> bashkohu me degët përkatëse ->
integrim -> vendosje

Pasojat kryesore të zbatimit të një konfigurimi të përpiluar janë:

  1. Konfigurimi do të jetë i qëndrueshëm në të gjitha nyjet e sistemit të shpërndarë. Për shkak të faktit se të gjitha nyjet marrin të njëjtin konfigurim nga një burim i vetëm.

  2. Është problematike të ndryshosh konfigurimin vetëm në një nga nyjet. Prandaj, "zhvendosja e konfigurimit" nuk ka gjasa.

  3. Bëhet më e vështirë të bësh ndryshime të vogla në konfigurim.

  4. Shumica e ndryshimeve të konfigurimit do të ndodhin si pjesë e procesit të përgjithshëm të zhvillimit dhe do t'i nënshtrohen rishikimit.

A kam nevojë për një depo të veçantë për të ruajtur konfigurimin e prodhimit? Ky konfigurim mund të përmbajë fjalëkalime dhe informacione të tjera të ndjeshme në të cilat ne dëshirojmë të kufizojmë aksesin. Bazuar në këtë, duket se ka kuptim të ruhet konfigurimi përfundimtar në një depo të veçantë. Mund ta ndani konfigurimin në dy pjesë - njëra që përmban cilësime konfigurimi të aksesueshme nga publiku dhe tjetra që përmban cilësime të kufizuara. Kjo do të lejojë që shumica e zhvilluesve të kenë akses në cilësimet e zakonshme. Kjo ndarje është e lehtë për t'u arritur duke përdorur tipare të ndërmjetme që përmbajnë vlera të paracaktuara.

Ndryshimet e mundshme

Le të përpiqemi të krahasojmë konfigurimin e përpiluar me disa alternativa të zakonshme:

  1. Skedar teksti në makinën e synuar.
  2. Dyqani i centralizuar me vlerë kyçe (etcd/zookeeper).
  3. Përpunoni komponentët që mund të rikonfigurohen/rifillohen pa rifilluar procesin.
  4. Ruajtja e konfigurimit jashtë kontrollit të objektit dhe versionit.

Skedarët e tekstit ofrojnë fleksibilitet të konsiderueshëm përsa i përket ndryshimeve të vogla. Administratori i sistemit mund të hyjë në nyjen e largët, të bëjë ndryshime në skedarët e duhur dhe të rifillojë shërbimin. Megjithatë, për sistemet e mëdha, një fleksibilitet i tillë mund të mos jetë i dëshirueshëm. Ndryshimet e bëra nuk lënë gjurmë në sisteme të tjera. Askush nuk i shqyrton ndryshimet. Është e vështirë të përcaktohet se kush i bëri saktësisht ndryshimet dhe për çfarë arsye. Ndryshimet nuk testohen. Nëse sistemi shpërndahet, atëherë administratori mund të harrojë të bëjë ndryshimin përkatës në nyjet e tjera.

(Duhet të theksohet gjithashtu se përdorimi i një konfigurimi të përpiluar nuk mbyll mundësinë e përdorimit të skedarëve tekst në të ardhmen. Do të mjaftojë të shtoni një analizues dhe vleftësues që prodhon të njëjtin lloj si rezultati Config, dhe mund të përdorni skedarë teksti. Menjëherë rrjedh se kompleksiteti i një sistemi me një konfigurim të përpiluar është disi më i vogël se kompleksiteti i një sistemi që përdor skedarë teksti, sepse skedarët e tekstit kërkojnë kod shtesë.)

Një dyqan i centralizuar me vlerë kyçe është një mekanizëm i mirë për shpërndarjen e meta parametrave të një aplikacioni të shpërndarë. Ne duhet të vendosim se cilat janë parametrat e konfigurimit dhe cilat janë vetëm të dhënat. Le të kemi një funksion C => A => B, dhe parametrat C ndryshon rrallë, dhe të dhënat A - shpesh. Në këtë rast mund të themi se C - parametrat e konfigurimit dhe A - të dhëna. Duket se parametrat e konfigurimit ndryshojnë nga të dhënat në atë që në përgjithësi ndryshojnë më rrallë se të dhënat. Gjithashtu, të dhënat zakonisht vijnë nga një burim (nga përdoruesi), dhe parametrat e konfigurimit nga një tjetër (nga administratori i sistemit).

Nëse parametrat me ndryshim të rrallë duhet të përditësohen pa rifilluar programin, atëherë kjo shpesh mund të çojë në ndërlikimin e programit, sepse do të na duhet të dorëzojmë disi parametrat, të ruajmë, analizojmë dhe kontrollojmë dhe përpunojmë vlera të pasakta. Prandaj, nga pikëpamja e zvogëlimit të kompleksitetit të programit, ka kuptim të zvogëlohet numri i parametrave që mund të ndryshojnë gjatë funksionimit të programit (ose të mos mbështesin fare parametra të tillë).

Për qëllimet e këtij postimi, ne do të bëjmë dallimin midis parametrave statikë dhe dinamikë. Nëse logjika e shërbimit kërkon ndryshimin e parametrave gjatë funksionimit të programit, atëherë parametra të tillë do t'i quajmë dinamikë. Përndryshe opsionet janë statike dhe mund të konfigurohen duke përdorur konfigurimin e përpiluar. Për rikonfigurim dinamik, mund të na duhet një mekanizëm për të rifilluar pjesë të programit me parametra të rinj, të ngjashëm me mënyrën se si rifillojnë proceset e sistemit operativ. (Sipas mendimit tonë, këshillohet të shmangni rikonfigurimin në kohë reale, pasi kjo rrit kompleksitetin e sistemit. Nëse është e mundur, është më mirë të përdorni aftësitë standarde të OS për rinisjen e proceseve.)

Një aspekt i rëndësishëm i përdorimit të konfigurimit statik që i bën njerëzit të marrin në konsideratë rikonfigurimin dinamik është koha që duhet që sistemi të rindizet pas një përditësimi të konfigurimit (koha joproduktive). Në fakt, nëse duhet të bëjmë ndryshime në konfigurimin statik, do të duhet të rinisim sistemin që vlerat e reja të hyjnë në fuqi. Problemi i kohës së ndërprerjes ndryshon në ashpërsi për sisteme të ndryshme. Në disa raste, mund të planifikoni një rindezje në një kohë kur ngarkesa është minimale. Nëse keni nevojë të ofroni shërbim të vazhdueshëm, mund ta zbatoni Kullimi i lidhjes AWS ELB. Në të njëjtën kohë, kur duhet të rindizni sistemin, ne nisim një shembull paralel të këtij sistemi, kalojmë balancuesin në të dhe presim që lidhjet e vjetra të përfundojnë. Pasi të kenë përfunduar të gjitha lidhjet e vjetra, ne mbyllim shembullin e vjetër të sistemit.

Le të shqyrtojmë tani çështjen e ruajtjes së konfigurimit brenda ose jashtë objektit. Nëse e ruajmë konfigurimin brenda një objekti, atëherë të paktën kemi pasur mundësinë të verifikojmë korrektësinë e konfigurimit gjatë montimit të artefaktit. Nëse konfigurimi është jashtë objektit të kontrolluar, është e vështirë të gjurmosh se kush bëri ndryshime në këtë skedar dhe pse. Sa e rëndësishme është? Sipas mendimit tonë, për shumë sisteme prodhimi është e rëndësishme që të ketë një konfigurim të qëndrueshëm dhe me cilësi të lartë.

Versioni i një objekti ju lejon të përcaktoni se kur është krijuar, çfarë vlerash përmban, cilat funksione janë aktivizuar/çaktivizuar dhe kush është përgjegjës për çdo ndryshim në konfigurim. Sigurisht, ruajtja e konfigurimit brenda një objekti kërkon disa përpjekje, kështu që ju duhet të merrni një vendim të informuar.

Mirat dhe të këqijat

Unë do të doja të ndalem në të mirat dhe të këqijat e teknologjisë së propozuar.

Avantazhet

Më poshtë është një listë e veçorive kryesore të një konfigurimi të përpiluar të sistemit të shpërndarë:

  1. Kontrolli i konfigurimit statik. Ju lejon të jeni të sigurt për këtë
    konfigurimi është i saktë.
  2. Gjuhë e pasur e konfigurimit. Në mënyrë tipike, metodat e tjera të konfigurimit janë të kufizuara në zëvendësimin e variablave të vargut. Kur përdorni Scala, disponohen një gamë e gjerë veçorish gjuhësore për të përmirësuar konfigurimin tuaj. Për shembull mund të përdorim
    tipare për vlerat e paracaktuara, duke përdorur objekte për të grupuar parametrat, ne mund t'i referohemi valeve të deklaruara vetëm një herë (DRY) në shtrirjen mbyllëse. Ju mund të instantoni çdo klasë direkt brenda konfigurimit (Seq, Map, klasa me porosi).
  3. DSL. Scala ka një sërë veçorish gjuhësore që e bëjnë më të lehtë krijimin e një DSL. Është e mundur të përfitoni nga këto veçori dhe të zbatoni një gjuhë konfigurimi që është më e përshtatshme për grupin e synuar të përdoruesve, në mënyrë që konfigurimi të jetë të paktën i lexueshëm nga ekspertët e domenit. Specialistët mund, për shembull, të marrin pjesë në procesin e rishikimit të konfigurimit.
  4. Integriteti dhe sinkronia midis nyjeve. Një nga avantazhet e konfigurimit të një sistemi të tërë të shpërndarë të ruajtur në një pikë të vetme është se të gjitha vlerat deklarohen saktësisht një herë dhe më pas ripërdoren kudo që nevojiten. Përdorimi i llojeve fantazmë për të deklaruar portet siguron që nyjet të përdorin protokolle të pajtueshme në të gjitha konfigurimet e duhura të sistemit. Duke pasur varësi të qarta të detyrueshme midis nyjeve siguron që të gjitha shërbimet janë të lidhura.
  5. Ndryshime me cilësi të lartë. Bërja e ndryshimeve në konfigurim duke përdorur një proces të përbashkët zhvillimi bën të mundur arritjen e standardeve të cilësisë së lartë edhe për konfigurimin.
  6. Përditësim i njëkohshëm i konfigurimit. Vendosja automatike e sistemit pas ndryshimeve të konfigurimit siguron që të gjitha nyjet të përditësohen.
  7. Thjeshtimi i aplikacionit. Aplikacioni nuk ka nevojë për analizë, kontroll konfigurimi ose trajtim të vlerave të pasakta. Kjo zvogëlon kompleksitetin e aplikacionit. (Disa nga kompleksiteti i konfigurimit të vërejtur në shembullin tonë nuk është një atribut i konfigurimit të përpiluar, por vetëm një vendim i vetëdijshëm i nxitur nga dëshira për të ofruar siguri më të madhe të tipit.) Është mjaft e lehtë të ktheheni në konfigurimin e zakonshëm - thjesht zbatoni atë që mungon pjesët. Prandaj, për shembull, mund të filloni me një konfigurim të përpiluar, duke shtyrë zbatimin e pjesëve të panevojshme deri në kohën kur është vërtet e nevojshme.
  8. Konfigurimi i verifikuar. Meqenëse ndryshimet e konfigurimit ndjekin fatin e zakonshëm të çdo ndryshimi tjetër, rezultati që marrim është një objekt me një version unik. Kjo na lejon, për shembull, të kthehemi në një version të mëparshëm të konfigurimit nëse është e nevojshme. Mund të përdorim edhe konfigurimin e një viti më parë dhe sistemi do të funksionojë saktësisht njësoj. Një konfigurim i qëndrueshëm përmirëson parashikueshmërinë dhe besueshmërinë e një sistemi të shpërndarë. Meqenëse konfigurimi është fiksuar në fazën e përpilimit, është mjaft e vështirë ta falsifikosh atë në prodhim.
  9. Modulariteti. Korniza e propozuar është modulare dhe modulet mund të kombinohen në mënyra të ndryshme për të krijuar sisteme të ndryshme. Në veçanti, mund ta konfiguroni sistemin që të funksionojë në një nyje të vetme në një mishërim dhe në nyje të shumta në një tjetër. Ju mund të krijoni disa konfigurime për instancat e prodhimit të sistemit.
  10. Duke testuar. Duke zëvendësuar shërbimet individuale me objekte tallje, mund të merrni disa versione të sistemit që janë të përshtatshëm për testim.
  11. Testimi i integrimit. Pasja e një konfigurimi të vetëm për të gjithë sistemin e shpërndarë bën të mundur ekzekutimin e të gjithë komponentëve në një mjedis të kontrolluar si pjesë e testimit të integrimit. Është e lehtë të imitohet, për shembull, një situatë ku disa nyje bëhen të aksesueshme.

Disavantazhet dhe kufizimet

Konfigurimi i përpiluar ndryshon nga qasjet e tjera të konfigurimit dhe mund të mos jetë i përshtatshëm për disa aplikacione. Më poshtë janë disa disavantazhe:

  1. Konfigurimi statik. Ndonjëherë ju duhet të korrigjoni shpejt konfigurimin në prodhim, duke anashkaluar të gjithë mekanizmat mbrojtës. Me këtë qasje mund të jetë më e vështirë. Së paku, do të kërkohet ende përpilimi dhe vendosja automatike. Kjo është një veçori e dobishme e qasjes dhe një disavantazh në disa raste.
  2. Gjenerimi i konfigurimit. Në rast se skedari i konfigurimit gjenerohet nga një mjet automatik, mund të kërkohen përpjekje shtesë për të integruar skriptin e ndërtimit.
  3. Mjetet. Aktualisht, shërbimet dhe teknikat e krijuara për të punuar me konfigurimin bazohen në skedarë teksti. Jo të gjitha shërbimet/teknikat e tilla do të jenë të disponueshme në një konfigurim të përpiluar.
  4. Kërkohet një ndryshim në qëndrime. Zhvilluesit dhe DevOps janë mësuar me skedarët e tekstit. Vetë ideja e përpilimit të një konfigurimi mund të jetë disi e papritur dhe e pazakontë dhe të shkaktojë refuzim.
  5. Kërkohet një proces zhvillimi me cilësi të lartë. Për të përdorur me lehtësi konfigurimin e përpiluar, është i nevojshëm automatizimi i plotë i procesit të ndërtimit dhe vendosjes së aplikacionit (CI/CD). Përndryshe do të jetë mjaft e papërshtatshme.

Le të ndalemi gjithashtu në një numër kufizimesh të shembullit të konsideruar që nuk lidhen me idenë e një konfigurimi të përpiluar:

  1. Nëse japim informacion të panevojshëm konfigurimi që nuk përdoret nga nyja, atëherë përpiluesi nuk do të na ndihmojë të zbulojmë zbatimin që mungon. Ky problem mund të zgjidhet duke braktisur modelin e tortës dhe duke përdorur lloje më të ngurtë, për shembull, HList ose llojet e të dhënave algjebrike (klasat e rastit) për të përfaqësuar konfigurimin.
  2. Ka rreshta në skedarin e konfigurimit që nuk kanë lidhje me vetë konfigurimin: (package, import,deklarimet e objekteve; override def's për parametrat që kanë vlera të paracaktuara). Kjo mund të shmanget pjesërisht nëse zbatoni DSL-në tuaj. Përveç kësaj, llojet e tjera të konfigurimit (për shembull, XML) gjithashtu vendosin kufizime të caktuara në strukturën e skedarit.
  3. Për qëllimet e këtij postimi, ne nuk po shqyrtojmë rikonfigurimin dinamik të një grupi nyjesh të ngjashme.

Përfundim

Në këtë postim, ne eksploruam idenë e përfaqësimit të konfigurimit në kodin burimor duke përdorur aftësitë e avancuara të sistemit të tipit Scala. Kjo qasje mund të përdoret në aplikacione të ndryshme si një zëvendësim për metodat tradicionale të konfigurimit të bazuara në skedarë xml ose tekst. Edhe pse shembulli ynë zbatohet në Scala, të njëjtat ide mund të transferohen në gjuhë të tjera të përpiluara (të tilla si Kotlin, C#, Swift, ...). Mund ta provoni këtë qasje në një nga projektet e mëposhtme dhe, nëse nuk funksionon, kaloni te skedari i tekstit, duke shtuar pjesët që mungojnë.

Natyrisht, një konfigurim i përpiluar kërkon një proces zhvillimi me cilësi të lartë. Në këmbim, sigurohet cilësi dhe besueshmëri e lartë e konfigurimeve.

Qasja e konsideruar mund të zgjerohet:

  1. Ju mund të përdorni makro për të kryer kontrolle në kohën e përpilimit.
  2. Ju mund të zbatoni një DSL për të paraqitur konfigurimin në një mënyrë që të jetë e arritshme për përdoruesit fundorë.
  3. Ju mund të zbatoni menaxhimin dinamik të burimeve me rregullimin automatik të konfigurimit. Për shembull, ndryshimi i numrit të nyjeve në një grup kërkon që (1) çdo nyje të marrë një konfigurim paksa të ndryshëm; (2) menaxheri i grupit mori informacion rreth nyjeve të reja.

Mirënjohje

Unë do të doja të falënderoja Andrei Saksonov, Pavel Popov dhe Anton Nekhaev për kritikat e tyre konstruktive ndaj draft artikullit.

Burimi: www.habr.com

Shto një koment