Kompilearre ferspraat systeem konfiguraasje

Ik soe graach fertelle jo ien nijsgjirrich meganisme foar wurkjen mei de konfiguraasje fan in ferspraat systeem. De konfiguraasje wurdt fertsjintwurdige direkt yn in kompilearre taal (Scala) mei help fan feilige typen. Dizze post jout in foarbyld fan sa'n konfiguraasje en besprekt ferskate aspekten fan it útfieren fan in kompilearre konfiguraasje yn it algemiene ûntwikkelingsproses.

Kompilearre ferspraat systeem konfiguraasje

(Ingelsk)

Ynlieding

It bouwen fan in betrouber ferspraat systeem betsjut dat alle knopen de juste konfiguraasje brûke, syngronisearre mei oare knopen. DevOps-technologyen (terraform, ansible of sa) wurde normaal brûkt om automatysk konfiguraasjebestannen te generearjen (faak spesifyk foar elke knooppunt). Wy wolle ek der wis fan wêze dat alle kommunisearjende knopen identike protokollen brûke (ynklusyf deselde ferzje). Oars sil ynkompatibiliteit wurde ynboud yn ús ferspraat systeem. Yn 'e JVM-wrâld is ien gefolch fan dizze eask dat deselde ferzje fan' e bibleteek mei de protokolberjochten oeral brûkt wurde moat.

Hoe sit it mei it testen fan in ferspraat systeem? Fansels geane wy ​​derfan út dat alle komponinten ienheidstests hawwe foardat wy oergean nei yntegraasjetesten. (Om ús testresultaten te ekstrapolearjen nei runtime, moatte wy ek in identike set fan bibleteken leverje yn it teststadium en by runtime.)

By it wurkjen mei yntegraasjetests is it faak makliker om itselde klassepaad oeral op alle knopen te brûken. Alles wat wy hoege te dwaan is te soargjen dat itselde klassepaad wurdt brûkt by runtime. (Hoewol't it folslein mooglik is om ferskate knooppunten út te fieren mei ferskate klassepaden, foegje dit kompleksiteit ta oan 'e algemiene konfiguraasje en swierrichheden mei ynset- en yntegraasjetests.) Foar de doelen fan dizze post geane wy ​​derfan út dat alle knooppunten itselde klassepaad brûke.

De konfiguraasje evoluearret mei de applikaasje. Wy brûke ferzjes om ferskate stadia fan programma-evolúsje te identifisearjen. It liket logysk om ek ferskate ferzjes fan konfiguraasjes te identifisearjen. En pleats de konfiguraasje sels yn it ferzjekontrôlesysteem. As d'r mar ien konfiguraasje yn produksje is, dan kinne wy ​​gewoan it ferzjenûmer brûke. As wy in protte produksje-eksimplaren brûke, dan sille wy ferskate nedich wêze
konfiguraasjetûken en in ekstra label neist de ferzje (bygelyks de namme fan 'e branch). Op dizze manier kinne wy ​​de krekte konfiguraasje dúdlik identifisearje. Elke konfiguraasje-identifikaasje komt unyk oerien mei in spesifike kombinaasje fan ferdielde knopen, havens, eksterne boarnen en biblioteekferzjes. Foar de doelen fan dizze post sille wy oannimme dat d'r mar ien branch is en wy kinne de konfiguraasje op 'e gewoane manier identifisearje mei trije nûmers skieden troch in punt (1.2.3).

Yn moderne omjouwings wurde konfiguraasjebestannen selden mei de hân makke. Se wurde faker generearre tidens ynset en wurde net mear oanrekke (sadat net brekke neat). In natuerlike fraach ûntstiet: wêrom brûke wy noch tekstformaat om konfiguraasje op te slaan? In libbensfetber alternatyf liket de mooglikheid te wêzen om reguliere koade te brûken foar konfiguraasje en profitearje fan kontrôles foar kompilaasje.

Yn dizze post sille wy it idee ferkenne om in konfiguraasje yn in kompilearre artefakt te fertsjintwurdigjen.

Kompilearre konfiguraasje

Dizze seksje jout in foarbyld fan in statyske kompilearre konfiguraasje. Twa ienfâldige tsjinsten wurde ymplementearre - de echo-tsjinst en de echo-tsjinstkliïnt. Op grûn fan dizze twa tsjinsten wurde twa systeemopsjes gearstald. Yn ien opsje lizze beide tsjinsten op deselde knooppunt, yn in oare opsje - op ferskate knooppunten.

Typysk befettet in ferdield systeem ferskate knopen. Jo kinne knooppunten identifisearje mei wearden fan wat soarte NodeId:

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

of

case class NodeId(hostName: String)

of sels

object Singleton
type NodeId = Singleton.type

Knooppunten fiere ferskate rollen, se rinne tsjinsten en TCP / HTTP-ferbiningen kinne wurde oprjochte tusken har.

Om in TCP-ferbining te beskriuwen hawwe wy op syn minst in poartenûmer nedich. Wy wolle ek it protokol reflektearje dat wurdt stipe op dy poarte om te soargjen dat sawol de client as de server itselde protokol brûke. Wy sille de ferbining beskriuwe mei de folgjende klasse:

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

wêr Port - gewoan in hiel getal Int oanjout fan it berik fan akseptabele wearden:

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

Ferfine soarten

Sjoch bibleteek raffinearre и my melde. Koartsein, de bibleteek lit jo beheiningen tafoegje oan typen dy't wurde kontrolearre op it kompilearjen. Yn dit gefal binne jildige poartenûmerwearden 16-bit heule getallen. Foar in kompilearre konfiguraasje is it brûken fan de ferfine bibleteek net ferplichte, mar it ferbetteret de gearstaller syn fermogen om te kontrolearjen de konfiguraasje.

Foar HTTP (REST) ​​protokollen, neist it poartenûmer, kinne wy ​​​​ek it paad nei de tsjinst nedich hawwe:

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

Fantomtypen

Om it protokol te identifisearjen op kompilaasjetiid, brûke wy in typeparameter dy't net brûkt wurdt binnen de klasse. Dit beslút komt troch it feit dat wy gjin protokoleksimplaar brûke by runtime, mar wy wolle graach dat de kompilator protokolkompatibiliteit kontrolearret. Troch it protokol oan te jaan, sille wy in ûngepaste tsjinst net as ôfhinklikens kinne trochjaan.

Ien fan 'e mienskiplike protokollen is de REST API mei Json-serialisaasje:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

wêr RequestMessage - type oanfraach, ResponseMessage - antwurd type.
Fansels kinne wy ​​​​brûke oare protokol beskriuwings dy't jouwe de krektens fan beskriuwing wy easkje.

Foar de doelen fan dizze post sille wy in ferienfâldige ferzje fan it protokol brûke:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Hjir is it fersyk in tekenrige taheakke oan de url en it antwurd is de weromkommende tekenrige yn it lichem fan it HTTP-antwurd.

De tsjinstkonfiguraasje wurdt beskreaun troch de tsjinstnamme, havens en ôfhinklikens. Dizze eleminten kinne wurde fertsjintwurdige yn Scala op ferskate manieren (bygelyks, HList-s, algebrayske gegevenstypen). Foar de doelen fan dizze post sille wy it Cake Pattern brûke en modules brûke dy't brûke trait'ov. (It Cake Pattern is gjin fereaske elemint fan dizze oanpak. It is gewoan ien mooglike ymplemintaasje.)

Ofhinklikens tusken tsjinsten kinne wurde fertsjintwurdige as metoaden dy't havens werombringe EndPoint's fan oare knopen:

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

Om in echo-tsjinst te meitsjen, is alles wat jo nedich binne in poartenûmer en in yndikaasje dat de poarte it echo-protokol stipet. Wy kinne miskien gjin spesifike poarte opjaan, om't ... eigenskippen tastean jo te ferklearjen metoaden sûnder ymplemintaasje (abstrakte metoaden). Yn dit gefal, by it meitsjen fan in konkrete konfiguraasje, soe de kompilator ús fereaskje om in ymplemintaasje fan 'e abstrakte metoade te leverjen en in poartenûmer te leverjen. Sûnt wy hawwe ymplementearre de metoade, by it meitsjen fan in spesifike konfiguraasje, wy meie net opjaan in oare haven. De standertwearde sil brûkt wurde.

Yn 'e kliïntkonfiguraasje ferklearje wy in ôfhinklikens fan' e echo-tsjinst:

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

De ôfhinklikens is fan itselde type as de eksportearre tsjinst echoService. Benammen yn 'e echo-client hawwe wy itselde protokol nedich. Dêrom, by it ferbinen fan twa tsjinsten, kinne wy ​​der wis fan wêze dat alles goed wurket.

Útfiering fan tsjinsten

In funksje is nedich om de tsjinst te begjinnen en te stopjen. (De mooglikheid om de tsjinst te stopjen is kritysk foar testen.) Nochris binne d'r ferskate opsjes foar it útfieren fan sa'n funksje (wy kinne bygelyks typeklassen brûke op basis fan it konfiguraasjetype). Foar de doelen fan dizze post sille wy it Cake Pattern brûke. Wy sille fertsjintwurdigje de tsjinst mei help fan in klasse cats.Resource, omdat Dizze klasse leveret al middels om de frijlitting fan boarnen feilich te garandearjen yn gefal fan problemen. Om in boarne te krijen, moatte wy konfiguraasje en in klearmakke runtime-kontekst leverje. De tsjinst opstartfunksje kin der sa útsjen:

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

wêr

  • Config - konfiguraasjetype foar dizze tsjinst
  • AddressResolver - in runtime-objekt wêrmei jo de adressen fan oare knooppunten kinne fine (sjoch hjirûnder)

en oare soarten út 'e biblioteek cats:

  • F[_] - type effekt (yn it ienfâldichste gefal F[A] koe gewoan in funksje wêze () => A. Yn dizze post sille wy brûke cats.IO.)
  • Reader[A,B] - min of mear synonym mei funksje A => B
  • cats.Resource - in boarne dy't kin wurde krigen en frijlitten
  • Timer - timer (kinne jo in skoft yn sliep falle en tiidintervallen mjitte)
  • ContextShift - analog ExecutionContext
  • Applicative - in effekttypeklasse wêrmei jo yndividuele effekten kinne kombinearje (hast in monade). Yn mear komplekse applikaasjes liket it better te brûken Monad/ConcurrentEffect.

Mei dizze funksje-hantekening kinne wy ​​ferskate tsjinsten ymplementearje. Bygelyks, in tsjinst dy't neat docht:

  trait ZeroServiceImpl[F[_]] extends ServiceImpl[F] {
    type Config <: Any
    def resource(...): ResourceReader[F, Config, Unit] =
      Reader(_ => Resource.pure[F, Unit](()))
  }

(Sjoch boarne, wêryn oare tsjinsten wurde ymplementearre - echo tsjinst, echo client
и libbenslange controllers.)

In knooppunt is in objekt dat ferskate tsjinsten kin lansearje (de lansearring fan in keatling fan boarnen wurdt garandearre troch it Cake Pattern):

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

Tink derom dat wy it krekte type konfiguraasje spesifisearje dy't nedich is foar dit knooppunt. As wy ferjitte ien fan 'e konfiguraasjetypen oan te jaan dy't nedich binne troch in bepaalde tsjinst, sil d'r in kompilaasjeflater wêze. Ek sille wy gjin knooppunt kinne begjinne, útsein as wy in objekt fan it passende type leverje mei alle nedige gegevens.

Host Namme Resolúsje

Om te ferbinen mei in host op ôfstân, hawwe wy in echt IP-adres nedich. It is mooglik dat it adres letter bekend wurdt as de rest fan de konfiguraasje. Dat wy hawwe in funksje nedich dy't de knooppunt-ID yn kaart bringt nei in adres:

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

D'r binne ferskate manieren om dizze funksje út te fieren:

  1. As de adressen bekend wurde oan ús foar ynset, dan kinne wy ​​generearje Scala koade mei
    adressen en dan de build útfiere. Dit sil tests kompilearje en útfiere.
    Yn dit gefal sil de funksje statysk bekend wêze en kin wurde fertsjintwurdige yn koade as in mapping Map[NodeId, NodeAddress].
  2. Yn guon gefallen is it eigentlike adres pas bekend nei't it knooppunt begon is.
    Yn dit gefal kinne wy ​​​​in "ûntdekkingstsjinst" ymplementearje dy't foar oare knopen rint en alle knopen sille registrearje mei dizze tsjinst en freegje de adressen fan oare knopen.
  3. As wy kinne feroarje /etc/hosts, dan kinne jo foarôf definieare hostnammen brûke (lykas my-project-main-node и echo-backend) en dizze nammen gewoan keppelje
    mei IP-adressen by ynset.

Yn dit post sille wy dizze gefallen net yn mear detail beskôgje. Foar ús
yn in boartersguodfoarbyld sille alle knooppunten itselde IP-adres hawwe - 127.0.0.1.

Dêrnei beskôgje wy twa opsjes foar in ferdield systeem:

  1. It pleatsen fan alle tsjinsten op ien knooppunt.
  2. En hosting fan de echo-tsjinst en echo-kliïnt op ferskate knopen.

Konfiguraasje foar ien knoop:

Single node konfiguraasje

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

It objekt ymplementearret de konfiguraasje fan sawol de client as de tsjinner. In tiid-to-live konfiguraasje wurdt ek brûkt sadat nei it ynterval lifetime beëinigje it programma. (Ctrl-C wurket ek en befrijt alle boarnen goed.)

Deselde set fan konfiguraasje en ymplemintaasje eigenskippen kin brûkt wurde foar it meitsjen fan in systeem besteande út twa aparte knopen:

Twa node konfiguraasje

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

Belangryk! Notysje hoe't de tsjinsten keppele binne. Wy spesifisearje in tsjinst útfierd troch ien knooppunt as in ymplemintaasje fan in oare knooppunt syn ôfhinklikheid metoade. De ôfhinklikens type wurdt kontrolearre troch de kompilator, omdat befettet it protokoltype. By it útfieren sil de ôfhinklikens de juste doelknooppunt-ID befetsje. Mei tank oan dit skema spesifisearje wy it poartenûmer krekt ien kear en wurde altyd garandearre om te ferwizen nei de juste poarte.

Implementaasje fan twa systeemknooppunten

Foar dizze konfiguraasje brûke wy deselde tsjinstimplementaasjes sûnder feroaringen. It ienige ferskil is dat wy no twa objekten hawwe dy't ferskate sets tsjinsten ymplementearje:

  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
  }

It earste knooppunt ymplementearret de tsjinner en hat allinich tsjinnerkonfiguraasje nedich. It twadde knooppunt ymplementearret de kliïnt en brûkt in oar diel fan 'e konfiguraasje. Ek beide knooppunten hawwe lifetime management nedich. De tsjinner node rint foar ûnbepaalde tiid oant it wurdt stoppe SIGTERM'om, en it clientknooppunt einiget nei in skoftke. Cm. launcher app.

Algemiene ûntwikkeling proses

Litte wy sjen hoe't dizze konfiguraasjebenadering it algemiene ûntwikkelingsproses beynfloedet.

De konfiguraasje wurdt gearstald tegearre mei de rest fan de koade en in artefakt (.jar) wurdt oanmakke. It liket sin te meitsjen om de konfiguraasje yn in apart artefakt te setten. Dit is om't wy meardere konfiguraasjes hawwe kinne basearre op deselde koade. Nochris is it mooglik om artefakten te generearjen dy't oerienkomme mei ferskate konfiguraasjetûken. Ofhinklikheden fan spesifike ferzjes fan bibleteken wurde bewarre tegearre mei de konfiguraasje, en dizze ferzjes wurde bewarre foar altyd as wy beslute om dizze ferzje fan 'e konfiguraasje yn te setten.

Elke konfiguraasjeferoaring feroaret yn in koadeferoaring. En dêrom, elk
de wiziging sil wurde dekt troch it normale proses foar kwaliteitsfersekering:

Ticket yn 'e bug tracker -> PR -> resinsje -> gearfoegje mei relevante tûken ->
yntegraasje -> ynset

De wichtichste gefolgen fan it útfieren fan in kompilearre konfiguraasje binne:

  1. De konfiguraasje sil konsekwint wêze oer alle knooppunten fan it ferspraat systeem. Fanwegen it feit dat alle knooppunten deselde konfiguraasje krije fan ien boarne.

  2. It is problematysk om de konfiguraasje te feroarjen yn mar ien fan 'e knopen. Dêrom is "konfiguraasjedrift" net wierskynlik.

  3. It wurdt dreger om lytse feroarings oan 'e konfiguraasje te meitsjen.

  4. De measte konfiguraasjewizigingen sille plakfine as ûnderdiel fan it algemiene ûntwikkelingsproses en sille ûnderwurpen wêze oan hifking.

Haw ik in apart repository nedich om de produksjekonfiguraasje op te slaan? Dizze konfiguraasje kin wachtwurden en oare gefoelige ynformaasje befetsje dêr't wy de tagong ta beheine wolle. Op grûn dêrfan liket it sin te meitsjen om de definitive konfiguraasje op te slaan yn in apart repository. Jo kinne de konfiguraasje splitse yn twa dielen - ien mei iepenbier tagonklike konfiguraasjeynstellingen en ien mei beheinde ynstellings. Dit sil de measte ûntwikkelders tagong krije ta mienskiplike ynstellingen. Dizze skieding is maklik te berikken mei help fan tuskenlizzende eigenskippen mei standertwearden.

Mooglike fariaasjes

Litte wy besykje de kompilearre konfiguraasje te fergelykjen mei guon gewoane alternativen:

  1. Teksttriem op 'e doelmasine.
  2. Sintrale kaai-wearde winkel (etcd/zookeeper).
  3. Ferwurkje komponinten dy't opnij konfigureare / opnij starte kinne sûnder it proses opnij te begjinnen.
  4. Konfiguraasje opslaan bûten artefakt en ferzjekontrôle.

Tekstbestannen jouwe wichtige fleksibiliteit yn termen fan lytse feroarings. De systeembehearder kin ynlogge op it knooppunt op ôfstân, wizigingen oanmeitsje oan de passende bestannen en de tsjinst opnij starte. Foar grutte systemen kin sokke fleksibiliteit lykwols net winsklik wêze. De feroarings dy't makke binne litte gjin spoaren yn oare systemen. Nimmen beoardielet de feroaringen. It is dreech om te bepalen wa't de wizigingen krekt makke hat en om hokker reden. Feroarings wurde net hifke. As it systeem is ferspraat, dan kin de behearder ferjitte om de korrespondearjende feroaring op oare knopen te meitsjen.

(It moat ek opmurken wurde dat it brûken fan in kompilearre konfiguraasje de mooglikheid fan it brûken fan tekstbestannen yn 'e takomst net slút. It sil genôch wêze om in parser en validator ta te foegjen dy't itselde type produsearret as útfier Config, en jo kinne tekstbestannen brûke. It folget fuortendaliks dat de kompleksiteit fan in systeem mei in gearstalde konfiguraasje wat minder is as de kompleksiteit fan in systeem dat tekstbestannen brûkt, om't tekstbestannen fereaskje ekstra koade.)

In sintralisearre kaai-wearde winkel is in goed meganisme foar it fersprieden fan meta parameters fan in ferspraat applikaasje. Wy moatte beslute wat konfiguraasjeparameters binne en wat gewoan gegevens binne. Lit ús in funksje hawwe C => A => B, en de parameters C komselden feroarings, en gegevens A - faak. Yn dit gefal kinne wy ​​sizze dat C - konfiguraasje parameters, en A - data. It docht bliken dat konfiguraasjeparameters ferskille fan gegevens trochdat se oer it generaal minder faak feroarje as gegevens. Ek, gegevens meastal komme út ien boarne (fan de brûker), en konfiguraasje parameters fan in oare (fan de systeembehearder).

As selden feroarjende parameters moatte wurde bywurke sûnder it programma opnij te starten, dan kin dit faaks liede ta de komplikaasje fan it programma, om't wy op ien of oare manier parameters moatte leverje, opslaan, parse en kontrolearje, en ferkearde wearden ferwurkje. Dêrom, út it eachpunt fan it ferminderjen fan de kompleksiteit fan it programma, is it logysk om te ferminderjen it oantal parameters dy't kinne feroarje tidens programma operaasje (of net stypje sokke parameters op alle).

Foar de doelen fan dit post sille wy ûnderskiede tusken statyske en dynamyske parameters. As de logika fan 'e tsjinst fereasket it feroarjen fan parameters tidens de wurking fan it programma, dan sille wy sokke parameters dynamysk neame. Oars binne de opsjes statysk en kinne wurde konfigureare mei de kompilearre konfiguraasje. Foar dynamyske rekonfiguraasje kinne wy ​​​​in meganisme nedich wêze om dielen fan it programma opnij te begjinnen mei nije parameters, fergelykber mei hoe't bestjoeringssysteemprosessen opnij starte. (Yn ús miening is it oan te rieden om real-time rekonfiguraasje te foarkommen, om't dit de kompleksiteit fan it systeem fergruttet. As it mooglik is, is it better om de standert OS-mooglikheden te brûken foar it opnij starte fan prosessen.)

Ien wichtich aspekt fan it brûken fan statyske konfiguraasje wêrtroch minsken dynamyske rekonfiguraasje beskôgje, is de tiid dy't it duorret foar it systeem om te herstarten nei in konfiguraasjefernijing (downtime). Yn feite, as wy wizigingen moatte meitsje oan 'e statyske konfiguraasje, sille wy it systeem opnij moatte starte foar de nije wearden om effekt te nimmen. It probleem fan downtime ferskilt yn earnst foar ferskate systemen. Yn guon gefallen kinne jo in herstart planne op in tiid dat de lading minimaal is. As jo ​​moatte leverje kontinu tsjinst, kinne jo útfiere AWS ELB ferbining ôfwettering. Tagelyk, as wy it systeem opnij moatte opstarte, starte wy in parallele eksimplaar fan dit systeem, skeakelje de balancer nei it, en wachtsje oant de âlde ferbiningen foltôgje. Nei't alle âlde ferbinings binne beëinige, slute wy it âlde eksimplaar fan it systeem ôf.

Lit ús no beskôgje it probleem fan it opslaan fan de konfiguraasje binnen of bûten it artefakt. As wy de konfiguraasje yn in artefakt opslaan, dan hienen wy teminsten de kâns om de krektens fan 'e konfiguraasje te kontrolearjen tidens de gearstalling fan it artefakt. As de konfiguraasje bûten it kontroleare artefakt is, is it lestich om te folgjen wa't wizigingen yn dizze triem makke hat en wêrom. Hoe wichtich is it? Yn ús miening is it foar in protte produksjesystemen wichtich om in stabile en heechweardige konfiguraasje te hawwen.

De ferzje fan in artefakt lit jo bepale wannear't it is makke, hokker wearden it befettet, hokker funksjes binne yn-/útskeakele, en wa is ferantwurdlik foar elke feroaring yn 'e konfiguraasje. Fansels fereasket it bewarjen fan de konfiguraasje binnen in artefakt wat ynspannings, dus jo moatte in ynformearre beslút nimme.

Pros en Cons

Ik soe graach wenje op 'e foar- en neidielen fan' e foarstelde technology.

foardielen

Hjirûnder is in list mei de haadfunksjes fan in kompilearre ferdielde systeemkonfiguraasje:

  1. Statyske konfiguraasje kontrôle. Stelt jo der wis fan dat
    de konfiguraasje is korrekt.
  2. Ryk konfiguraasje taal. Typysk binne oare konfiguraasjemetoaden beheind ta string fariabele ferfanging op syn meast. By it brûken fan Scala is in breed skala oan taalfunksjes beskikber om jo konfiguraasje te ferbetterjen. Wy kinne bygelyks brûke
    eigenskippen foar standert wearden, mei help fan objekten te groep parameters, kinne wy ​​ferwize nei vals ferklearre mar ien kear (DRY) yn de omlizzende omfang. Jo kinne alle klassen direkt yn 'e konfiguraasje ynstantiearje (Seq, Map, oanpaste klassen).
  3. DSL. Scala hat in oantal taalfunksjes dy't it makliker meitsje om in DSL te meitsjen. It is mooglik om te profitearjen fan dizze funksjes en in konfiguraasjetaal te realisearjen dy't handiger is foar de doelgroep brûkers, sadat de konfiguraasje op syn minst lêsber is troch domeineksperts. Spesjalisten kinne bygelyks meidwaan oan it proses foar konfiguraasjebeoardieling.
  4. Yntegriteit en syngronisaasje tusken knopen. Ien fan 'e foardielen fan it hawwen fan de konfiguraasje fan in folslein ferspraat systeem op ien punt opslein is dat alle wearden krekt ien kear wurde ferklearre en dan opnij brûkt wêr't se nedich binne. It brûken fan fantomtypen om havens te ferklearjen soarget derfoar dat nodes kompatibele protokollen brûke yn alle juste systeemkonfiguraasjes. It hawwen fan eksplisite ferplichte ôfhinklikens tusken knopen soarget derfoar dat alle tsjinsten ferbûn binne.
  5. Feroarings fan hege kwaliteit. It meitsjen fan wizigingen yn 'e konfiguraasje mei in mienskiplik ûntwikkelingsproses makket it mooglik om ek hege kwaliteitsnormen foar de konfiguraasje te berikken.
  6. Simultane konfiguraasje update. Automatyske systeem ynset nei konfiguraasje feroarings soargje derfoar dat alle knopen wurde bywurke.
  7. Simplifying de applikaasje. De applikaasje hat gjin parsearjen, konfiguraasjekontrôle of it behanneljen fan ferkearde wearden nedich. Dit ferleget de kompleksiteit fan 'e applikaasje. (Guon fan 'e konfiguraasje kompleksiteit waarnommen yn ús foarbyld is net in attribút fan' e gearstalde konfiguraasje, mar allinnich in bewuste beslút dreaun troch de winsk om te foarsjen grutter type feiligens.) It is frij maklik om werom te gean nei de gewoane konfiguraasje - krekt útfiere de ûntbrekkende dielen. Dêrom kinne jo, bygelyks, begjinne mei in kompilearre konfiguraasje, útstelle de ymplemintaasje fan ûnnedige dielen oant it momint dat it echt nedich is.
  8. Ferifiearre konfiguraasje. Sûnt konfiguraasjewizigingen folgje it gewoane lot fan alle oare wizigingen, is de útfier dy't wy krije in artefakt mei in unike ferzje. Hjirmei kinne wy ​​bygelyks weromgean nei in eardere ferzje fan 'e konfiguraasje as it nedich is. Wy kinne sels de konfiguraasje fan in jier lyn brûke en it systeem sil krekt itselde wurkje. In stabile konfiguraasje ferbetteret de foarsisberens en betrouberens fan in ferspraat systeem. Sûnt de konfiguraasje is fêst yn 'e kompilaasjestadium, is it frij lestich om it yn produksje te fake.
  9. Modulariteit. It foarstelde ramt is modulêr en de modules kinne op ferskate manieren wurde kombinearre om ferskate systemen te meitsjen. Benammen kinne jo it systeem ynstelle om te rinnen op ien knooppunt yn ien belichaming, en op meardere knooppunten yn in oar. Jo kinne ferskate konfiguraasjes meitsje foar produksje-eksimplaren fan it systeem.
  10. Testen. Troch yndividuele tsjinsten te ferfangen mei mock-objekten kinne jo ferskate ferzjes fan it systeem krije dy't handich binne foar testen.
  11. Yntegraasje testen. It hawwen fan in inkele konfiguraasje foar it hiele ferspraat systeem makket it mooglik om te rinnen alle komponinten yn in kontrolearre omjouwing as ûnderdiel fan yntegraasje testen. It is maklik om bygelyks in situaasje te emulearjen wêr't guon knopen tagonklik wurde.

Neidielen en beheinings

Kompilearre konfiguraasje ferskilt fan oare konfiguraasjebenaderingen en is miskien net geskikt foar guon applikaasjes. Hjirûnder binne wat neidielen:

  1. Statyske konfiguraasje. Soms moatte jo de konfiguraasje yn 'e produksje fluch korrigearje, troch alle beskermjende meganismen te omgean. Mei dizze oanpak kin it dreger wêze. Op syn minst sil kompilaasje en automatyske ynset noch nedich wêze. Dit is sawol in nuttich skaaimerk fan 'e oanpak as in neidiel yn guon gefallen.
  2. Konfiguraasje generaasje. Yn it gefal dat it konfiguraasjetriem wurdt oanmakke troch in automatysk ark, kinne ekstra ynspannings nedich wêze om it bouskript te yntegrearjen.
  3. Tools. Op it stuit binne nutsbedriuwen en techniken ûntworpen om te wurkjen mei konfiguraasje basearre op tekstbestannen. Net al sokke nutsbedriuwen / techniken sille beskikber wêze yn in kompilearre konfiguraasje.
  4. In feroaring yn hâlding is nedich. Untwikkelders en DevOps binne wend oan tekstbestannen. It idee fan it kompilearjen fan in konfiguraasje kin wat ûnferwachts en ûngewoan wêze en feroarsaakje ôfwizing.
  5. In ûntwikkelingsproses fan hege kwaliteit is fereaske. Om de gearstalde konfiguraasje noflik te brûken, is folsleine automatisearring fan it proses fan it bouwen en ynsetten fan 'e applikaasje (CI / CD) nedich. Oars sil it frij ûngemaklik wêze.

Lit ús ek stilstean by in oantal beheiningen fan it beskôge foarbyld dy't net relatearje oan it idee fan in kompilearre konfiguraasje:

  1. As wy jouwe ûnnedige konfiguraasje ynformaasje dy't net brûkt wurdt troch de knooppunt, dan sil de kompilator net helpe ús detect de ûntbrekkende ymplemintaasje. Dit probleem kin wurde oplost troch it ferlitten fan it Cake Pattern en it brûken fan mear rigide soarten, bygelyks, HList of algebraic gegevens typen (case klassen) te fertsjintwurdigjen konfiguraasje.
  2. D'r binne rigels yn it konfiguraasjetriem dy't net relatearre binne oan de konfiguraasje sels: (package, import,objektferklearrings; override def's foar parameters dy't standertwearden hawwe). Dit kin foar in part foarkommen wurde as jo jo eigen DSL implementearje. Dêrneist oare soarten konfiguraasje (bygelyks XML) ek oplizze bepaalde beheinings op de triem struktuer.
  3. Foar de doelen fan dizze post beskôgje wy gjin dynamyske rekonfiguraasje fan in kluster fan ferlykbere knopen.

konklúzje

Yn dit post hawwe wy it idee ûndersocht om konfiguraasje yn boarnekoade te fertsjintwurdigjen mei de avansearre mooglikheden fan it Scala-type systeem. Dizze oanpak kin brûkt wurde yn ferskate applikaasjes as ferfanging foar tradisjonele konfiguraasjemetoaden basearre op xml- as tekstbestannen. Ek al is ús foarbyld ymplementearre yn Scala, deselde ideeën kinne wurde oerdroegen oan oare kompilearre talen (lykas Kotlin, C#, Swift, ...). Jo kinne dizze oanpak besykje yn ien fan 'e folgjende projekten, en, as it net wurket, gean troch nei it tekstbestân, en foegje de ûntbrekkende dielen ta.

Natuerlik fereasket in kompilearre konfiguraasje in ûntwikkelingsproses fan hege kwaliteit. Yn ruil, hege kwaliteit en betrouberens fan konfiguraasjes wurdt garandearre.

De beskôge oanpak kin útwreide wurde:

  1. Jo kinne makro's brûke om kompilaasjetiidkontrôles út te fieren.
  2. Jo kinne in DSL ymplementearje om de konfiguraasje te presintearjen op in manier dy't tagonklik is foar ein brûkers.
  3. Jo kinne dynamysk boarnebehear ymplementearje mei automatyske konfiguraasjeoanpassing. Bygelyks, it feroarjen fan it oantal knopen yn in kluster fereasket dat (1) elke node in wat oare konfiguraasje krijt; (2) de klusterbehearder krige ynformaasje oer nije knopen.

Erkennings

Ik wol Andrei Saksonov, Pavel Popov en Anton Nekhaev betankje foar har konstruktive krityk op it konseptartikel.

Boarne: www.habr.com

Add a comment