Konfigurimi i përpilueshëm i një sistemi të shpërndarë

Në këtë postim ne dëshirojmë të ndajmë një mënyrë interesante për t'u marrë me konfigurimin e një sistemi të shpërndarë.
Konfigurimi përfaqësohet drejtpërdrejt në gjuhën Scala në një mënyrë të sigurt. Një shembull i zbatimit përshkruhet në detaje. Diskutohen aspekte të ndryshme të propozimit, duke përfshirë ndikimin në procesin e përgjithshëm të zhvillimit.

Konfigurimi i përpilueshëm i një sistemi të shpërndarë

(në rusisht)

Prezantimi

Ndërtimi i sistemeve të fuqishme të shpërndara kërkon përdorimin e konfigurimit korrekt dhe koherent në të gjitha nyjet. Një zgjidhje tipike është përdorimi i një përshkrimi të vendosjes tekstuale (terraform, ansible ose diçka e ngjashme) dhe skedarët e konfigurimit të gjeneruar automatikisht (shpesh - të dedikuar për çdo nyje/rol). Ne gjithashtu do të dëshironim të përdornim të njëjtat protokolle të të njëjtave versione në çdo nyje komunikuese (përndryshe do të kishim probleme të papajtueshmërisë). Në botën JVM kjo do të thotë që të paktën biblioteka e mesazheve duhet të jetë e të njëjtit version në të gjitha nyjet komunikuese.

Po në lidhje me testimin e sistemit? Sigurisht, ne duhet të kemi teste njësi për të gjithë komponentët përpara se të vijmë në testet e integrimit. Për të qenë në gjendje të ekstrapolojmë rezultatet e testimit në kohën e ekzekutimit, duhet të sigurohemi që versionet e të gjitha bibliotekave të mbahen identike si në kohën e ekzekutimit ashtu edhe në mjediset e testimit.

Gjatë ekzekutimit të testeve të integrimit, shpesh është shumë më e lehtë të kesh të njëjtën rrugë të klasës në të gjitha nyjet. Thjesht duhet të sigurohemi që e njëjta rrugë e klasës të përdoret në vendosje. (Është e mundur të përdoren klasa të ndryshme në nyje të ndryshme, por është më e vështirë të përfaqësosh këtë konfigurim dhe ta vendosësh në mënyrë korrekte.) Pra, për t'i mbajtur gjërat të thjeshta, ne do të konsiderojmë vetëm shtigje identike të klasës në të gjitha nyjet.

Konfigurimi tenton të evoluojë së bashku me softuerin. Zakonisht përdorim versione për të identifikuar të ndryshme
fazat e evolucionit të softuerit. Duket e arsyeshme për të mbuluar konfigurimin nën menaxhimin e versionit dhe për të identifikuar konfigurime të ndryshme me disa etiketa. Nëse ka vetëm një konfigurim në prodhim, ne mund të përdorim një version të vetëm si identifikues. Ndonjëherë mund të kemi mjedise të shumta prodhimi. Dhe për çdo mjedis mund të na duhet një degë e veçantë konfigurimi. Pra, konfigurimet mund të etiketohen me degë dhe version për të identifikuar në mënyrë unike konfigurime të ndryshme. Çdo emërtim dhe version i degës korrespondon me një kombinim të vetëm të nyjeve të shpërndara, porteve, burimeve të jashtme, versioneve të bibliotekës së klasës në secilën nyje. Këtu do të mbulojmë vetëm degën e vetme dhe do të identifikojmë konfigurimet nga një version dhjetor me tre komponentë (1.2.3), në të njëjtën mënyrë si artefaktet e tjera.

Në mjediset moderne skedarët e konfigurimit nuk modifikohen më manualisht. Zakonisht ne gjenerojmë
skedarët e konfigurimit në kohën e vendosjes dhe kurrë mos i prek ato më pas. Pra, dikush mund të pyesë pse ne ende përdorim format teksti për skedarët e konfigurimit? Një opsion i mundshëm është vendosja e konfigurimit brenda një njësie përpilimi dhe përfitimi nga vlefshmëria e konfigurimit në kohën e përpilimit.

Në këtë postim do të shqyrtojmë idenë e mbajtjes së konfigurimit në objektin e përpiluar.

Konfigurimi i përpilueshëm

Në këtë seksion do të diskutojmë një shembull të konfigurimit statik. Dy shërbime të thjeshta - shërbimi echo dhe klienti i shërbimit echo janë duke u konfiguruar dhe implementuar. Pastaj instantohen dy sisteme të ndryshme të shpërndara me të dyja shërbimet. Njëra është për konfigurimin e një nyje të vetme dhe një tjetër për konfigurimin e dy nyjeve.

Një sistem tipik i shpërndarë përbëhet nga disa nyje. Nyjet mund të identifikohen duke përdorur disa lloje:

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

ose vetëm

case class NodeId(hostName: String)

apo edhe

object Singleton
type NodeId = Singleton.type

Këto nyje kryejnë role të ndryshme, ekzekutojnë disa shërbime dhe duhet të jenë në gjendje të komunikojnë me nyjet e tjera me anë të lidhjeve TCP/HTTP.

Për lidhjen TCP kërkohet të paktën një numër porti. Ne gjithashtu duam të sigurohemi që klienti dhe serveri po flasin të njëjtin protokoll. Për të modeluar një lidhje midis nyjeve, le të deklarojmë klasën e mëposhtme:

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

ku Port është vetëm një Int brenda kufijve të lejuar:

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

Llojet e rafinuara

Shiko i rafinuar librari. Me pak fjalë, ai lejon shtimin e kufizimeve kohore të përpilimit në lloje të tjera. Në këtë rast Int lejohet të ketë vetëm vlera 16-bitësh që mund të përfaqësojnë numrin e portit. Nuk ka asnjë kërkesë për të përdorur këtë bibliotekë për këtë qasje konfigurimi. Thjesht duket se përshtatet shumë mirë.

Për HTTP (REST) ​​mund të na duhet gjithashtu një shteg i shërbimit:

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

Lloji fantazmë

Për të identifikuar protokollin gjatë përpilimit ne përdorim veçorinë Scala të deklarimit të argumentit të tipit Protocol që nuk përdoret në klasë. Është i ashtuquajturi lloj fantazmë. Në kohën e ekzekutimit, ne rrallë kemi nevojë për një shembull të identifikuesit të protokollit, prandaj nuk e ruajmë atë. Gjatë përpilimit, ky tip fantazmë jep siguri shtesë të tipit. Nuk mund të kalojmë portin me protokoll të pasaktë.

Një nga protokollet më të përdorura është REST API me serializimin Json:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

ku RequestMessage është lloji bazë i mesazheve që klienti mund t'i dërgojë serverit 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 i shtohet url-së dhe mesazhi i përgjigjes kthehet si varg i thjeshtë.

Një konfigurim shërbimi mund të përshkruhet me emrin e 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, 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ë pjesë (module) të kombinueshme si tipare. (Modeli i tortës nuk është një kërkesë për këtë qasje të konfigurimit të përpilueshëm. Është vetëm një zbatim i mundshëm i idesë.)

Varësitë mund të përfaqësohen duke përdorur modelin e tortës si pika fundore të 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)
  }

Shërbimi Echo kërkon vetëm një port të konfiguruar. Dhe ne deklarojmë 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 deklarimet e metodave abstrakte. Nëse përdorim metoda abstrakte, kompajleri do të kërkojë një zbatim në një shembull konfigurimi. Këtu kemi ofruar zbatimin (8081) dhe do të përdoret si vlera e paracaktuar nëse e kapërcejmë në një konfigurim konkret.

Ne mund të deklarojmë 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 lloj si ajo echoService. Në veçanti, ai kërkon të njëjtin protokoll. Prandaj, mund të jemi të sigurt se nëse i lidhim këto dy varësi, ato do të funksionojnë siç duhet.

Zbatimi i shërbimeve

Një shërbim ka nevojë për një funksion për të nisur dhe për t'u mbyllur me hijeshi. (Aftësia për të mbyllur një shërbim është kritike për testimin.) Përsëri ka disa opsione për të specifikuar një funksion të tillë për një konfigurim të caktuar (për shembull, ne mund të përdorim klasat e tipit). Për këtë postim do të përdorim sërish modelin e tortës. Ne mund të përfaqësojmë një shërbim duke përdorur cats.Resource i cili tashmë ofron kllapa dhe lirim të burimeve. Për të marrë një burim, ne duhet të sigurojmë një konfigurim dhe një kontekst të kohës së funksionimit. Pra, funksioni i fillimit të 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 që kërkohet nga ky startues shërbimi
  • AddressResolver — një objekt ekzekutimi që ka aftësinë për të marrë adresa reale të nyjeve të tjera (vazhdoni të lexoni për detaje).

nga vijnë llojet e tjera cats:

  • F[_] — lloji i efektit (Në rastin më të thjeshtë F[A] mund të jetë vetëm () => A. Në këtë postim do të përdorim cats.IO.)
  • Reader[A,B] - është pak a shumë një sinonim për një funksion A => B
  • cats.Resource - ka mënyra për të fituar dhe liruar
  • Timer — lejon gjumin/matjen e kohës
  • ContextShift - analog i ExecutionContext
  • Applicative — mbështjellësi i funksioneve në fuqi (pothuajse një monadë) (mund ta zëvendësojmë përfundimisht me diçka tjetër)

Duke përdorur këtë ndërfaqe, 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](()))
  }

(Shih Source code për zbatimin e shërbimeve të tjera - shërbim jehonë,
klient jehonë kontrollorët gjatë gjithë jetës.)

Një nyje është një objekt i vetëm që ekzekuton disa shërbime (fillimi i një zinxhiri burimesh mundësohet 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
}

Vini re se në nyjë ne specifikojmë llojin e saktë të konfigurimit që i nevojitet kësaj nyje. Përpiluesi nuk na lejon të ndërtojmë objektin (Cake) me tip të pamjaftueshëm, sepse çdo tipar shërbimi deklaron një kufizim në Config lloji. Gjithashtu ne nuk do të jemi në gjendje të nisim nyjen pa ofruar konfigurim të plotë.

Rezolucioni i adresës së nyjeve

Për të krijuar një lidhje, ne kemi nevojë për një adresë të vërtetë të hostit për secilën nyje. Mund të njihet më vonë se pjesët e tjera të konfigurimit. Prandaj, ne kemi nevojë për një mënyrë për të ofruar një hartë midis ID-së së nyjës dhe adresës së saj aktuale. Ky 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ë zbatuar një funksion të tillë.

  1. Nëse i dimë adresat aktuale përpara vendosjes, gjatë instancimit të hosteve të nyjeve, atëherë mund të gjenerojmë kodin Scala me adresat aktuale dhe të ekzekutojmë ndërtimin më pas (i cili kryen kontrollet e kohës së kompilimit dhe më pas ekzekuton paketën e testimit të integrimit). Në këtë rast, funksioni ynë i hartës njihet në mënyrë statike dhe mund të thjeshtohet në diçka si a Map[NodeId, NodeAddress].
  2. Ndonjëherë ne marrim adresat aktuale vetëm në një pikë të mëvonshme kur nyja është nisur në të vërtetë, ose nuk kemi adresa të nyjeve që nuk janë nisur ende. Në këtë rast ne mund të kemi një shërbim zbulimi që niset përpara të gjitha nyjeve të tjera dhe secila nyje mund të reklamojë adresën e saj në atë shërbim dhe të regjistrohet në varësi.
  3. Nëse mund të modifikojmë /etc/hosts, ne mund të përdorim emra të paracaktuar të hosteve (si my-project-main-node echo-backend) dhe thjesht lidhni këtë emër me adresën IP në kohën e vendosjes.

Në këtë postim ne nuk i mbulojmë këto raste në më shumë detaje. Në fakt në shembullin tonë të lodrës të gjitha nyjet do të kenë të njëjtën adresë IP - 127.0.0.1.

Në këtë postim do të shqyrtojmë dy paraqitje të sistemit të shpërndarë:

  1. Paraqitja e një nyjeje, ku të gjitha shërbimet vendosen në një nyje të vetme.
  2. Paraqitja me dy nyje, ku shërbimi dhe klienti janë në nyje të ndryshme.

Konfigurimi për një nyje e vetme faqosja është si më poshtë:

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

Këtu ne krijojmë një konfigurim të vetëm që zgjeron konfigurimin e serverit dhe klientit. Gjithashtu ne konfigurojmë një kontrollues të ciklit jetësor që normalisht do të përfundojë klientin dhe serverin më pas lifetime kalon intervali.

I njëjti grup i implementimeve dhe konfigurimeve të shërbimit mund të përdoret për të krijuar paraqitjen e një sistemi me dy nyje të veçanta. Ne vetëm duhet të krijojmë dy konfigurime të veçanta të nyjeve me shërbimet e duhura:

Konfigurimi i dy nyjeve

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

Shihni se si e specifikojmë varësinë. Ne përmendim shërbimin e ofruar të nyjes tjetër si një varësi e nyjes aktuale. Lloji i varësisë kontrollohet sepse përmban tip fantazmë që përshkruan protokollin. Dhe në kohën e ekzekutimit do të kemi ID-në e saktë të nyjes. Ky është një nga aspektet e rëndësishme të qasjes së propozuar të konfigurimit. Na siguron mundësinë për të vendosur portin vetëm një herë dhe të sigurohemi që po i referohemi portit të saktë.

Zbatimi i dy nyjeve

Për këtë konfigurim ne përdorim saktësisht të njëjtat zbatime të shërbimeve. Asnjë ndryshim fare. Sidoqoftë, ne krijojmë dy implementime të ndryshme të nyjeve që përmbajnë 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 anës së serverit. Nyja e dytë zbaton klientin dhe ka nevojë për një pjesë tjetër të konfigurimit. Të dy nyjet kërkojnë disa specifikime të jetës. Për qëllimet e kësaj nyje shërbimi postar do të ketë jetëgjatësi të pafundme që mund të ndërpritet duke përdorur SIGTERM, ndërsa klienti echo do të përfundojë pas kohëzgjatjes së fundme të konfiguruar. Shihni aplikimi fillestar për detaje.

Procesi i përgjithshëm i zhvillimit

Le të shohim se si kjo qasje ndryshon mënyrën se si ne punojmë me konfigurimin.

Konfigurimi si kod do të përpilohet dhe do të prodhojë një objekt. Duket e arsyeshme që të ndahet objekti i konfigurimit nga artefaktet e tjera të kodit. Shpesh mund të kemi një mori 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 konstante sa herë që vendosim këtë konfigurim.

Një ndryshim i konfigurimit bëhet ndryshim i kodit. Pra, duhet të mbulohet nga i njëjti proces i sigurimit të cilësisë:

Bileta -> PR -> rishikim -> bashkim -> integrim i vazhdueshëm -> vendosje e vazhdueshme

Ka pasojat e mëposhtme të qasjes:

  1. Konfigurimi është koherent për një shembull të caktuar të sistemit. Duket se nuk ka asnjë mënyrë që të ketë lidhje të gabuar midis nyjeve.
  2. Nuk është e lehtë të ndryshosh konfigurimin vetëm në një nyje. Duket e paarsyeshme të identifikohesh dhe të ndryshosh disa skedarë teksti. Kështu që zhvendosja e konfigurimit bëhet më pak e mundur.
  3. Ndryshimet e vogla të konfigurimit nuk janë të lehta për t'u bërë.
  4. Shumica e ndryshimeve të konfigurimit do të ndjekin të njëjtin proces zhvillimi dhe do të kalojë një rishikim.

A kemi nevojë për një depo të veçantë për konfigurimin e prodhimit? Konfigurimi i prodhimit mund të përmbajë informacione të ndjeshme që ne dëshirojmë t'i mbajmë jashtë mundësive të shumë njerëzve. Kështu që mund të ia vlen të mbash një depo të veçantë me akses të kufizuar që do të përmbajë konfigurimin e prodhimit. Mund ta ndajmë konfigurimin në dy pjesë - një që përmban parametrat më të hapur të prodhimit dhe një që përmban pjesën sekrete të konfigurimit. Kjo do të mundësonte aksesin e shumicës së zhvilluesve në shumicën dërrmuese të parametrave duke kufizuar qasjen në gjëra vërtet të ndjeshme. Është e lehtë ta realizosh këtë duke përdorur tipare të ndërmjetme me vlera të paracaktuara të parametrave.

Variacionet

Le të shohim të mirat dhe të këqijat e qasjes së propozuar në krahasim me teknikat e tjera të menaxhimit të konfigurimit.

Para së gjithash, ne do të rendisim disa alternativa ndaj aspekteve të ndryshme të mënyrës së propozuar të trajtimit të konfigurimit:

  1. Skedar teksti në makinën e synuar.
  2. Ruajtja e centralizuar e vlerës së çelësit (si p.sh etcd/zookeeper).
  3. Komponentët e nënprocesit që mund të rikonfigurohen/rifillohen pa rifillimin e procesit.
  4. Konfigurimi jashtë artifaktit dhe kontrollit të versionit.

Skedari i tekstit jep njëfarë fleksibiliteti për sa i përket rregullimeve ad-hoc. Administratori i një sistemi mund të identifikohet në nyjen e synuar, të bëjë një ndryshim dhe thjesht të rifillojë shërbimin. Kjo mund të mos jetë aq e mirë për sistemet më të mëdha. Pas ndryshimit nuk ka mbetur asnjë gjurmë. Ndryshimi nuk shqyrtohet nga një palë tjetër sy. Mund të jetë e vështirë të zbulohet se çfarë e ka shkaktuar ndryshimin. Nuk është testuar. Nga këndvështrimi i sistemit të shpërndarë, një administrator thjesht mund të harrojë të përditësojë konfigurimin në një nga nyjet e tjera.

(Btw, nëse përfundimisht do të ketë nevojë për të filluar përdorimin e skedarëve të konfigurimit të tekstit, do të na duhet vetëm të shtojmë analizues + vërtetues që mund të prodhojë të njëjtën gjë Config shkruani dhe kjo do të mjaftonte për të filluar përdorimin e konfigurimeve të tekstit. Kjo tregon gjithashtu se kompleksiteti i konfigurimit të kohës së përpilimit është pak më i vogël se kompleksiteti i konfigurimeve të bazuara në tekst, sepse në versionin e bazuar në tekst na duhet një kod shtesë.)

Ruajtja e centralizuar e vlerës së çelësit është një mekanizëm i mirë për shpërndarjen e parametrave meta të aplikacionit. Këtu duhet të mendojmë se çfarë konsiderojmë si vlera konfigurimi dhe çfarë janë vetëm të dhëna. Jepet një funksion C => A => B ne zakonisht i quajmë vlera që ndryshojnë rrallë C "konfigurim", ndërsa të dhënat ndryshohen shpesh A - vetëm futni të dhëna. Konfigurimi duhet t'i sigurohet funksionit më herët se të dhënat A. Duke pasur parasysh këtë ide, mund të themi se është frekuenca e pritshme e ndryshimeve ajo që mund të përdoret për të dalluar të dhënat e konfigurimit nga të dhënat e thjeshta. Gjithashtu të dhënat zakonisht vijnë nga një burim (përdorues) dhe konfigurimi vjen nga një burim tjetër (admin). Ballafaqimi me parametrat që mund të ndryshohen pas procesit të inicializimit çon në një rritje të kompleksitetit të aplikacionit. Për parametra të tillë ne do të duhet të trajtojmë mekanizmin e shpërndarjes së tyre, analizimin dhe vlefshmërinë, trajtimin e vlerave të pasakta. Prandaj, për të reduktuar kompleksitetin e programit, do të ishte më mirë të zvogëlojmë numrin e parametrave që mund të ndryshojnë në kohën e ekzekutimit (ose edhe t'i eliminojmë ato fare).

Nga këndvështrimi i këtij postimi, ne duhet të bëjmë një dallim midis parametrave statikë dhe dinamikë. Nëse logjika e shërbimit kërkon ndryshim të rrallë të disa parametrave në kohën e ekzekutimit, atëherë mund t'i quajmë ato parametra dinamikë. Përndryshe ato janë statike 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ë sistemit mund të rifillojnë me parametrat e rinj të konfigurimit në një mënyrë të ngjashme me rinisjen e proceseve të veçanta të një sistemi të shpërndarë.
(Mendimi im modest është të shmangni rikonfigurimin e kohës së funksionimit sepse rrit kompleksitetin e sistemit.
Mund të jetë më e thjeshtë të mbështeteni vetëm në mbështetjen e OS për rinisjen e proceseve. Megjithatë, mund të mos jetë gjithmonë e mundur.)

Një aspekt i rëndësishëm i përdorimit të konfigurimit statik që ndonjëherë i bën njerëzit të konsiderojnë konfigurimin dinamik (pa arsye të tjera) është koha e ndërprerjes së shërbimit gjatë përditësimit të konfigurimit. Në të vërtetë, nëse duhet të bëjmë ndryshime në konfigurimin statik, duhet të rifillojmë sistemin në mënyrë që vlerat e reja të bëhen efektive. Kërkesat për kohën e ndërprerjes ndryshojnë për sisteme të ndryshme, kështu që mund të mos jetë aq kritike. Nëse është kritike, atëherë duhet të planifikojmë përpara për çdo rinisje të sistemit. Për shembull, ne mund të zbatonim Kullimi i lidhjes AWS ELB. Në këtë skenar, sa herë që na duhet të rinisim sistemin, ne nisim paralelisht një instancë të re të sistemit, më pas kalojmë ELB në të, duke e lënë sistemin e vjetër të përfundojë servisimin e lidhjeve ekzistuese.

Po në lidhje me mbajtjen e konfigurimit brenda objektit të versionuar ose jashtë? Mbajtja e konfigurimit brenda një objekti do të thotë në shumicën e rasteve që ky konfigurim ka kaluar të njëjtin proces të sigurimit të cilësisë si artefaktet e tjera. Pra, dikush mund të jetë i sigurt se konfigurimi është i cilësisë së mirë dhe i besueshëm. Përkundrazi, konfigurimi në një skedar të veçantë do të thotë se nuk ka gjurmë se kush dhe pse ka bërë ndryshime në atë skedar. A është kjo e rëndësishme? Ne besojmë se për shumicën e sistemeve të prodhimit është më mirë të kemi konfigurim të qëndrueshëm dhe me cilësi të lartë.

Versioni i artefaktit ju lejon të zbuloni se kur është krijuar, çfarë vlerash përmban, cilat veçori janë aktivizuar/çaktivizuar, kush ishte përgjegjës për kryerjen e çdo ndryshimi në konfigurim. Mund të kërkojë disa përpjekje për të mbajtur konfigurimin brenda një objekti dhe është një zgjedhje dizajni për t'u bërë.

Pro dhe kundër

Këtu dëshirojmë të theksojmë disa avantazhe dhe të diskutojmë disa disavantazhe të qasjes së propozuar.

Përparësitë

Karakteristikat e konfigurimit të përpilueshëm të një sistemi të plotë të shpërndarë:

  1. Kontroll statik i konfigurimit. Kjo jep një nivel të lartë besimi, se konfigurimi është i saktë duke pasur parasysh kufizimet e tipit.
  2. Gjuha e pasur e konfigurimit. Në mënyrë tipike, qasjet e tjera të konfigurimit janë të kufizuara më së shumti në zëvendësimin e variablave.
    Duke përdorur Scala, mund të përdorni një gamë të gjerë karakteristikash gjuhësore për ta bërë konfigurimin më të mirë. Për shembull, ne mund të përdorim tipare për të siguruar vlerat e paracaktuara, objekte për të vendosur shtrirje të ndryshme, mund t'i referohemi vals përcaktohet vetëm një herë në shtrirjen e jashtme (DRY). Është e mundur të përdoren sekuenca fjalë për fjalë, ose shembuj të klasave të caktuara (Seq, Map, Etj).
  3. DSL. Scala ka mbështetje të mirë për shkrimtarët DSL. Dikush mund t'i përdorë këto veçori për të krijuar një gjuhë konfigurimi që është më e përshtatshme dhe më 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 domenit.
  4. Integriteti dhe koherenca nëpër nyje. Një nga përfitimet e konfigurimit për të gjithë sistemin e shpërndarë në një vend është se të gjitha vlerat përcaktohen rreptësisht një herë dhe më pas ripërdoren në të gjitha vendet ku na duhen. Gjithashtu shkruani deklaratat e portave të sigurta sigurojnë që në të gjitha konfigurimet e mundshme të sakta, nyjet e sistemit do të flasin të njëjtën gjuhë. Ekzistojnë varësi të qarta midis nyjeve, gjë që e bën të vështirë harrimin e ofrimit të disa shërbimeve.
  5. Cilësi e lartë e ndryshimeve. Qasja e përgjithshme e kalimit të konfigurimit ndryshon përmes procesit normal të PR vendos standarde të larta të cilësisë edhe në konfigurim.
  6. Ndryshime të njëkohshme të konfigurimit. Sa herë që bëjmë ndonjë ndryshim në konfigurim, vendosja automatike siguron që të gjitha nyjet të përditësohen.
  7. Thjeshtimi i aplikimit. Aplikacioni nuk ka nevojë të analizojë dhe vërtetojë konfigurimin dhe të trajtojë vlerat e gabuara të konfigurimit. Kjo thjeshton aplikimin e përgjithshëm. (Disa rritje e kompleksitetit është në vetë konfigurimin, por është një shkëmbim i ndërgjegjshëm drejt sigurisë.) Është shumë e thjeshtë të ktheheni në konfigurimin e zakonshëm - thjesht shtoni pjesët që mungojnë. Është më e lehtë të filloni me konfigurimin e përpiluar dhe të shtyni zbatimin e pjesëve shtesë për disa kohë të mëvonshme.
  8. Konfigurimi i versionit. Për shkak të faktit se ndryshimet e konfigurimit ndjekin të njëjtin proces zhvillimi, si rezultat marrim një artefakt me version unik. Kjo na lejon të kthejmë konfigurimin përsëri nëse është e nevojshme. Ne madje mund të vendosim një konfigurim që është përdorur një vit më parë dhe do të funksionojë saktësisht në të njëjtën mënyrë. Konfigurimi i qëndrueshëm përmirëson parashikueshmërinë dhe besueshmërinë e sistemit të shpërndarë. Konfigurimi fiksohet në kohën e përpilimit dhe nuk mund të ngatërrohet lehtësisht në një sistem prodhimi.
  9. Modulariteti. Korniza e propozuar është modulare dhe modulet mund të kombinohen në mënyra të ndryshme
    mbështesin konfigurime të ndryshme (konfigurime / paraqitje). Në veçanti, është e mundur që të ketë një plan urbanistik me një nyje në shkallë të vogël dhe një cilësim shumë nyjesh në shkallë të gjerë. Është e arsyeshme që të ketë paraqitje të shumta prodhimi.
  10. Duke testuar. Për qëllime testimi, dikush mund të zbatojë një shërbim tallës dhe ta përdorë atë si një varësi në një mënyrë të sigurt. Disa paraqitje të ndryshme testimi me pjesë të ndryshme të zëvendësuara nga tallje mund të mbahen njëkohësisht.
  11. Testimi i integrimit. Ndonjëherë në sistemet e shpërndara është e vështirë të ekzekutohen testet e integrimit. Duke përdorur qasjen e përshkruar për të shtypur konfigurimin e sigurt të sistemit të plotë 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 i padisponueshëm.

Disavantazhet

Qasja e konfigurimit të përpiluar është e ndryshme nga konfigurimi "normal" dhe mund të mos i përshtatet të gjitha nevojave. Këtu janë disa nga disavantazhet e konfigurimit të përpiluar:

  1. Konfigurimi statik. Mund të mos jetë i përshtatshëm për të gjitha aplikacionet. Në disa raste ka nevojë për rregullim të shpejtë të konfigurimit në prodhim duke anashkaluar të gjitha masat e sigurisë. Kjo qasje e bën më të vështirë. Përpilimi dhe rivendosja kërkohet pasi të keni bërë ndonjë ndryshim në konfigurim. Kjo është edhe veçori edhe barra.
  2. Gjenerimi i konfigurimit. Kur konfigurimi gjenerohet nga ndonjë mjet automatizimi, kjo qasje kërkon përpilim të mëvonshëm (i cili nga ana tjetër mund të dështojë). Mund të kërkojë përpjekje shtesë për të integruar këtë hap shtesë në sistemin e ndërtimit.
  3. Instrumentet. Ka shumë mjete në përdorim sot që mbështeten në konfigurime të bazuara në tekst. Disa prej tyre
    nuk do të zbatohet kur të kompilohet konfigurimi.
  4. Nevojitet një ndryshim në mentalitet. Zhvilluesit dhe DevOps janë të njohur me skedarët e konfigurimit të tekstit. Ideja e përpilimit të konfigurimit mund t'u duket e çuditshme.
  5. Përpara prezantimit të konfigurimit të përpilueshëm kërkohet një proces i zhvillimit të softuerit me cilësi të lartë.

Ka disa kufizime të shembullit të zbatuar:

  1. Nëse ofrojmë konfigurim shtesë që nuk kërkohet nga zbatimi i nyjës, përpiluesi nuk do të na ndihmojë të zbulojmë zbatimin që mungon. Kjo mund të adresohet duke përdorur HList ose ADT (klasa rasti) për konfigurimin e nyjeve në vend të tipareve dhe modelit të tortës.
  2. Ne duhet të ofrojmë një pllakë boiler në skedarin e konfigurimit: (package, import, object deklaratat;
    override def's për parametrat që kanë vlera të paracaktuara). Kjo mund të adresohet pjesërisht duke përdorur një DSL.
  3. Në këtë postim ne nuk mbulojmë rikonfigurimin dinamik të grupimeve të nyjeve të ngjashme.

Përfundim

Në këtë postim ne kemi diskutuar idenë e përfaqësimit të konfigurimit direkt në kodin burimor në një mënyrë të sigurt. Qasja mund të përdoret në shumë aplikacione si një zëvendësim i konfigurimeve xml dhe të tjera të bazuara në tekst. Pavarësisht se shembulli ynë është zbatuar në Scala, ai mund të përkthehet edhe në gjuhë të tjera të përpilueshme (si Kotlin, C#, Swift, etj.). Dikush mund ta provojë këtë qasje në një projekt të ri dhe, në rast se nuk përshtatet mirë, të kalojë në mënyrën e modës së vjetër.

Sigurisht, konfigurimi i përpilueshëm kërkon një proces zhvillimi me cilësi të lartë. Në këmbim, ai premton të sigurojë konfigurim të fortë po aq të lartë.

Kjo qasje mund të zgjerohet në mënyra të ndryshme:

  1. Dikush mund të përdorë makro për të kryer vërtetimin e konfigurimit dhe të dështojë në kohën e përpilimit në rast të dështimeve të kufizimeve të logjikës së biznesit.
  2. Një DSL mund të zbatohet për të përfaqësuar konfigurimin në një mënyrë miqësore për përdoruesit.
  3. Menaxhimi dinamik i burimeve me rregullime automatike të konfigurimit. Për shembull, kur rregullojmë numrin e nyjeve të grupimit, mund të dëshirojmë (1) nyjet të marrin një konfigurim pak të modifikuar; (2) menaxheri i grupit për të marrë informacione të reja për nyjet.

Faleminderit

Unë do të doja të falënderoja Andrey Saksonov, Pavel Popov, Anton Nehaev për dhënien e komenteve frymëzuese për draftin e këtij postimi që më ndihmoi ta bëj më të qartë.

Burimi: www.habr.com