Saamgestelde verspreide stelselkonfigurasie

Ek wil jou graag een interessante meganisme vertel om met die konfigurasie van 'n verspreide stelsel te werk. Die konfigurasie word direk verteenwoordig in 'n saamgestelde taal (Scala) met behulp van veilige tipes. Hierdie pos verskaf 'n voorbeeld van so 'n konfigurasie en bespreek verskeie aspekte van die implementering van 'n saamgestelde konfigurasie in die algehele ontwikkelingsproses.

Saamgestelde verspreide stelselkonfigurasie

(Engels)

Inleiding

Die bou van 'n betroubare verspreide stelsel beteken dat alle nodusse die korrekte konfigurasie gebruik, gesinchroniseer met ander nodusse. DevOps-tegnologieë (terraform, ansible of so iets) word gewoonlik gebruik om konfigurasielêers outomaties te genereer (dikwels spesifiek vir elke nodus). Ons wil ook seker wees dat alle kommunikerende nodusse identiese protokolle gebruik (insluitend dieselfde weergawe). Andersins sal onversoenbaarheid in ons verspreide stelsel ingebou word. In die JVM-wêreld is een gevolg van hierdie vereiste dat dieselfde weergawe van die biblioteek wat die protokolboodskappe bevat, oral gebruik moet word.

Wat van die toets van 'n verspreide stelsel? Natuurlik neem ons aan dat alle komponente eenheidstoetse het voordat ons oorgaan na integrasietoetsing. (Om toetsresultate na looptyd te ekstrapoleer, moet ons ook 'n identiese stel biblioteke tydens die toetsstadium en tydens looptyd verskaf.)

Wanneer daar met integrasietoetse gewerk word, is dit dikwels makliker om dieselfde klaspad oral op alle nodusse te gebruik. Al wat ons hoef te doen is om te verseker dat dieselfde klaspad tydens looptyd gebruik word. (Terwyl dit heeltemal moontlik is om verskillende nodusse met verskillende klaspaaie te laat loop, voeg dit kompleksiteit by tot die algehele konfigurasie en probleme met ontplooiing en integrasietoetse.) Vir die doeleindes van hierdie pos, neem ons aan dat alle nodusse dieselfde klaspad sal gebruik.

Die konfigurasie ontwikkel met die toepassing. Ons gebruik weergawes om verskillende stadiums van programevolusie te identifiseer. Dit lyk logies om ook verskillende weergawes van konfigurasies te identifiseer. En plaas die konfigurasie self in die weergawebeheerstelsel. As daar net een konfigurasie in produksie is, kan ons eenvoudig die weergawenommer gebruik. As ons baie produksiegevalle gebruik, sal ons verskeie nodig hê
konfigurasie takke en 'n bykomende etiket bykomend tot die weergawe (byvoorbeeld, die naam van die tak). Op hierdie manier kan ons die presiese konfigurasie duidelik identifiseer. Elke konfigurasie-identifiseerder stem uniek ooreen met 'n spesifieke kombinasie van verspreide nodusse, poorte, eksterne hulpbronne en biblioteekweergawes. Vir die doeleindes van hierdie pos sal ons aanvaar dat daar net een tak is en ons kan die konfigurasie op die gewone manier identifiseer deur drie getalle geskei deur 'n punt (1.2.3) te gebruik.

In moderne omgewings word konfigurasielêers selde met die hand geskep. Hulle word meer dikwels tydens ontplooiing gegenereer en word nie meer aangeraak nie (sodat moet niks breek nie). 'n Natuurlike vraag ontstaan: hoekom gebruik ons ​​steeds teksformaat om konfigurasie te stoor? 'n Lewensvatbare alternatief blyk die vermoë te wees om gereelde kode vir konfigurasie te gebruik en voordeel te trek uit samestelling-tydkontroles.

In hierdie pos sal ons die idee ondersoek om 'n konfigurasie binne 'n saamgestelde artefak voor te stel.

Saamgestelde konfigurasie

Hierdie afdeling verskaf 'n voorbeeld van 'n statiese saamgestelde opset. Twee eenvoudige dienste word geïmplementeer - die eggo diens en die eggo diens kliënt. Op grond van hierdie twee dienste word twee stelselopsies saamgestel. In een opsie is beide dienste op dieselfde nodus geleë, in 'n ander opsie - op verskillende nodusse.

Tipies bevat 'n verspreide stelsel verskeie nodusse. U kan nodusse identifiseer deur waardes van een of ander soort te gebruik NodeId:

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

of

case class NodeId(hostName: String)

of selfs

object Singleton
type NodeId = Singleton.type

Nodusse verrig verskeie rolle, hulle bedryf dienste en TCP/HTTP-verbindings kan tussen hulle tot stand gebring word.

Om 'n TCP-verbinding te beskryf, benodig ons ten minste 'n poortnommer. Ons wil ook graag die protokol weerspieël wat op daardie poort ondersteun word om te verseker dat beide die kliënt en bediener dieselfde protokol gebruik. Ons sal die verbinding beskryf deur die volgende klas te gebruik:

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

waar Port - net 'n heelgetal Int wat die reeks aanvaarbare waardes aandui:

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

Verfynde tipes

Sien biblioteek verfynde и my die verslag. Kortom, die biblioteek laat jou toe om beperkings by te voeg tot tipes wat gekontroleer word tydens samestelling. In hierdie geval is geldige poortnommerwaardes 16-bis heelgetalle. Vir 'n saamgestelde konfigurasie is die gebruik van die verfynde biblioteek nie verpligtend nie, maar dit verbeter die samesteller se vermoë om die konfigurasie na te gaan.

Vir HTTP (REST) ​​protokolle, benewens die poortnommer, kan ons ook die pad na die diens benodig:

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

Fantoom tipes

Om die protokol tydens samestelling te identifiseer, gebruik ons ​​'n tipe parameter wat nie binne die klas gebruik word nie. Hierdie besluit is te wyte aan die feit dat ons nie 'n protokol-instansie tydens looptyd gebruik nie, maar ons wil graag hê dat die samesteller protokolversoenbaarheid nagaan. Deur die protokol te spesifiseer, sal ons nie 'n onvanpaste diens as 'n afhanklikheid kan deurgee nie.

Een van die algemene protokolle is die REST API met Json-serialisering:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

waar RequestMessage - tipe versoek, ResponseMessage - tipe reaksie.
Natuurlik kan ons ander protokolbeskrywings gebruik wat die akkuraatheid van beskrywing verskaf wat ons benodig.

Vir die doeleindes van hierdie pos sal ons 'n vereenvoudigde weergawe van die protokol gebruik:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Hier is die versoek 'n string wat by die url aangeheg is en die antwoord is die teruggekeerde string in die liggaam van die HTTP-antwoord.

Die dienskonfigurasie word beskryf deur die diensnaam, poorte en afhanklikhede. Hierdie elemente kan op verskeie maniere in Scala voorgestel word (bv. HList-s, algebraïese datatipes). Vir die doeleindes van hierdie pos sal ons die Koekpatroon gebruik en modules voorstel wat gebruik word trait'ov. (Die Koekpatroon is nie 'n vereiste element van hierdie benadering nie. Dit is bloot een moontlike implementering.)

Afhanklikhede tussen dienste kan voorgestel word as metodes wat poorte terugstuur EndPointse van ander nodusse:

  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 'n eggo-diens te skep, is al wat jy nodig het 'n poortnommer en 'n aanduiding dat die poort die eggo-protokol ondersteun. Ons spesifiseer dalk nie 'n spesifieke poort nie, want... eienskappe laat jou toe om metodes te verklaar sonder implementering (abstrakte metodes). In hierdie geval, wanneer 'n konkrete konfigurasie geskep word, sal die samesteller vereis dat ons 'n implementering van die abstrakte metode verskaf en 'n poortnommer verskaf. Aangesien ons die metode geïmplementeer het, mag ons nie 'n ander poort spesifiseer wanneer 'n spesifieke konfigurasie geskep word nie. Die verstekwaarde sal gebruik word.

In die kliëntkonfigurasie verklaar ons 'n afhanklikheid van die eggo-diens:

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

Die afhanklikheid is van dieselfde tipe as die uitgevoer diens echoService. Veral, in die eggo-kliënt benodig ons dieselfde protokol. Daarom, wanneer ons twee dienste verbind, kan ons seker wees dat alles reg sal werk.

Implementering van dienste

'n Funksie word vereis om die diens te begin en stop. (Die vermoë om 'n diens te stop is krities vir toetsing.) Weereens, daar is verskeie opsies vir die implementering van so 'n kenmerk (ons kan byvoorbeeld tipe klasse gebruik wat gebaseer is op die konfigurasie tipe). Vir die doeleindes van hierdie pos sal ons die Koekpatroon gebruik. Ons sal die diens verteenwoordig deur 'n klas te gebruik cats.Resource, omdat Hierdie klas bied reeds middele om die vrystelling van hulpbronne veilig te waarborg in geval van probleme. Om 'n hulpbron te kry, moet ons konfigurasie en 'n klaargemaakte looptydkonteks verskaf. Die diensopstartfunksie kan soos volg lyk:

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

waar

  • Config - konfigurasietipe vir hierdie diens
  • AddressResolver - 'n runtime-voorwerp waarmee u die adresse van ander nodusse kan uitvind (sien hieronder)

en ander tipes uit die biblioteek cats:

  • F[_] - tipe effek (in die eenvoudigste geval F[A] kan net 'n funksie wees () => A. In hierdie pos sal ons gebruik cats.IO.)
  • Reader[A,B] - min of meer sinoniem met funksie A => B
  • cats.Resource - 'n hulpbron wat verkry en vrygestel kan word
  • Timer - timer (laat jou toe om vir 'n rukkie aan die slaap te raak en tydintervalle te meet)
  • ContextShift - analoog ExecutionContext
  • Applicative — 'n effektipe klas waarmee u individuele effekte (amper 'n monade) kan kombineer. In meer komplekse toepassings lyk dit beter om te gebruik Monad/ConcurrentEffect.

Deur hierdie funksie handtekening te gebruik, kan ons verskeie dienste implementeer. Byvoorbeeld, 'n diens wat niks doen nie:

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

(Cm. bron, waarin ander dienste geïmplementeer word - eggo diens, eggo kliënt
и lewenslange beheerders.)

'n Nodus is 'n voorwerp wat verskeie dienste kan begin (die bekendstelling van 'n ketting hulpbronne word verseker deur die Koekpatroon):

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

Neem asseblief kennis dat ons die presiese tipe konfigurasie spesifiseer wat vir hierdie nodus benodig word. As ons vergeet om een ​​van die konfigurasietipes te spesifiseer wat deur 'n spesifieke diens vereis word, sal daar 'n samestellingsfout wees. Ons sal ook nie 'n nodus kan begin nie, tensy ons 'n voorwerp van die toepaslike tipe met al die nodige data verskaf.

Gasheernaam-resolusie

Om aan 'n afgeleë gasheer te koppel, benodig ons 'n regte IP-adres. Dit is moontlik dat die adres later bekend sal word as die res van die konfigurasie. Ons benodig dus 'n funksie wat die nodus-ID na 'n adres karteer:

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

Daar is verskeie maniere om hierdie funksie te implementeer:

  1. As die adresse aan ons bekend word voor ontplooiing, dan kan ons Scala-kode genereer met
    adresse en voer dan die bou uit. Dit sal toetse saamstel en laat loop.
    In hierdie geval sal die funksie staties bekend wees en kan dit in kode voorgestel word as 'n kartering Map[NodeId, NodeAddress].
  2. In sommige gevalle is die werklike adres eers bekend nadat die nodus begin het.
    In hierdie geval kan ons 'n "ontdekkingsdiens" implementeer wat voor ander nodusse loop en alle nodusse sal by hierdie diens registreer en die adresse van ander nodusse aanvra.
  3. As ons kan verander /etc/hosts, dan kan jy vooraf gedefinieerde gasheername gebruik (soos my-project-main-node и echo-backend) en koppel eenvoudig hierdie name
    met IP-adresse tydens ontplooiing.

In hierdie pos sal ons nie hierdie gevalle in meer besonderhede oorweeg nie. Vir ons
in 'n speelgoedvoorbeeld sal alle nodusse dieselfde IP-adres hê - 127.0.0.1.

Vervolgens oorweeg ons twee opsies vir 'n verspreide stelsel:

  1. Plaas alle dienste op een nodus.
  2. En huisves die eggo-diens en eggo-kliënt op verskillende nodusse.

Konfigurasie vir een nodus:

Enkelknoopkonfigurasie

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

Die objek implementeer die konfigurasie van beide die kliënt en bediener. 'n Tyd-tot-lewe-konfigurasie word ook gebruik sodat na die interval lifetime beëindig die program. (Ctrl-C werk ook en maak alle hulpbronne korrek vry.)

Dieselfde stel konfigurasie- en implementeringseienskappe kan gebruik word om 'n stelsel te skep wat bestaan ​​uit twee afsonderlike nodusse:

Twee nodus konfigurasie

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

Belangrik! Let op hoe die dienste gekoppel is. Ons spesifiseer 'n diens wat deur een nodus geïmplementeer word as 'n implementering van 'n ander nodus se afhanklikheidsmetode. Die afhanklikheidstipe word deur die samesteller nagegaan, want bevat die protokoltipe. Wanneer dit uitgevoer word, sal die afhanklikheid die korrekte teikennodus-ID bevat. Danksy hierdie skema spesifiseer ons die poortnommer presies een keer en word altyd gewaarborg om na die korrekte poort te verwys.

Implementering van twee stelsel nodusse

Vir hierdie konfigurasie gebruik ons ​​dieselfde diensimplementerings sonder veranderinge. Die enigste verskil is dat ons nou twee voorwerpe het wat verskillende stelle dienste implementeer:

  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
  }

Die eerste nodus implementeer die bediener en benodig slegs bedienerkonfigurasie. Die tweede nodus implementeer die kliënt en gebruik 'n ander deel van die konfigurasie. Beide nodusse benodig ook lewenslange bestuur. Die bedienernodus loop onbepaald totdat dit gestop word SIGTERM'om, en die kliëntnodus eindig na 'n geruime tyd. Cm. lanseerder-toepassing.

Algemene ontwikkelingsproses

Kom ons kyk hoe hierdie konfigurasiebenadering die algehele ontwikkelingsproses beïnvloed.

Die konfigurasie sal saam met die res van die kode saamgestel word en 'n artefak (.jar) sal gegenereer word. Dit blyk sinvol te wees om die konfigurasie in 'n aparte artefak te plaas. Dit is omdat ons verskeie konfigurasies kan hê gebaseer op dieselfde kode. Weereens, dit is moontlik om artefakte te genereer wat ooreenstem met verskillende konfigurasietakke. Afhanklikhede van spesifieke weergawes van biblioteke word saam met die opstelling gestoor, en hierdie weergawes word vir ewig gestoor wanneer ons besluit om daardie weergawe van die opstelling te ontplooi.

Enige konfigurasieverandering verander in 'n kodeverandering. En daarom, elkeen
die verandering sal deur die normale kwaliteitsversekeringsproses gedek word:

Kaartjie in die foutspoorder -> PR -> resensie -> voeg saam met relevante takke ->
integrasie -> ontplooiing

Die belangrikste gevolge van die implementering van 'n saamgestelde konfigurasie is:

  1. Die konfigurasie sal konsekwent wees oor alle nodusse van die verspreide stelsel. As gevolg van die feit dat alle nodusse dieselfde konfigurasie van 'n enkele bron ontvang.

  2. Dit is problematies om die konfigurasie in slegs een van die nodusse te verander. Daarom is "konfigurasieverskuiwing" onwaarskynlik.

  3. Dit word moeiliker om klein veranderinge aan die konfigurasie te maak.

  4. Die meeste konfigurasieveranderinge sal plaasvind as deel van die algehele ontwikkelingsproses en sal onderhewig wees aan hersiening.

Het ek 'n aparte bewaarplek nodig om die produksiekonfigurasie te stoor? Hierdie opstelling kan wagwoorde en ander sensitiewe inligting bevat waartoe ons toegang wil beperk. Op grond hiervan blyk dit sin te maak om die finale konfigurasie in 'n aparte bewaarplek te stoor. Jy kan die opstelling in twee dele verdeel—een wat publiek toeganklike konfigurasie-instellings bevat en een wat beperkte instellings bevat. Dit sal die meeste ontwikkelaars in staat stel om toegang tot algemene instellings te hê. Hierdie skeiding is maklik om te bereik met behulp van intermediêre eienskappe wat verstekwaardes bevat.

Moontlike variasies

Kom ons probeer om die saamgestelde konfigurasie met 'n paar algemene alternatiewe te vergelyk:

  1. Tekslêer op die teikenmasjien.
  2. Gesentraliseerde sleutel-waarde winkel (etcd/zookeeper).
  3. Proses komponente wat herkonfigureer/herbegin kan word sonder om die proses te herbegin.
  4. Berg konfigurasie buite artefak en weergawebeheer.

Tekslêers bied aansienlike buigsaamheid in terme van klein veranderinge. Die stelseladministrateur kan by die afgeleë nodus aanmeld, veranderinge aan die toepaslike lêers maak en die diens herbegin. Vir groot stelsels kan sulke buigsaamheid egter nie wenslik wees nie. Die veranderinge wat aangebring is, laat geen spore in ander stelsels nie. Niemand hersien die veranderinge nie. Dit is moeilik om te bepaal wie presies die veranderinge gemaak het en om watter rede. Veranderinge word nie getoets nie. As die stelsel versprei word, kan die administrateur vergeet om die ooreenstemmende verandering op ander nodusse aan te bring.

(Daar moet ook op gelet word dat die gebruik van 'n saamgestelde konfigurasie nie die moontlikheid sluit om tekslêers in die toekoms te gebruik nie. Dit sal genoeg wees om 'n ontleder en valideerder by te voeg wat dieselfde tipe as uitvoer produseer Config, en jy kan tekslêers gebruik. Dit volg onmiddellik dat die kompleksiteit van 'n stelsel met 'n saamgestelde konfigurasie ietwat minder is as die kompleksiteit van 'n stelsel wat tekslêers gebruik, omdat tekslêers vereis addisionele kode.)

'n Gesentraliseerde sleutelwaarde-winkel is 'n goeie meganisme om metaparameters van 'n verspreide toepassing te versprei. Ons moet besluit wat konfigurasieparameters is en wat net data is. Laat ons 'n funksie hê C => A => B, en die parameters C selde verander, en data A - dikwels. In hierdie geval kan ons dit sê C - konfigurasie parameters, en A - data. Dit blyk dat konfigurasieparameters van data verskil deurdat hulle oor die algemeen minder gereeld as data verander. Ook, data kom gewoonlik van een bron (van die gebruiker), en konfigurasie parameters van 'n ander (van die stelsel administrateur).

As parameters wat selde verander moet word opgedateer sonder om die program te herbegin, kan dit dikwels lei tot die komplikasie van die program, want ons sal op een of ander manier parameters moet lewer, stoor, ontleed en kontroleer en verkeerde waardes moet verwerk. Daarom, uit die oogpunt van die vermindering van die kompleksiteit van die program, is dit sinvol om die aantal parameters wat tydens die werking van die program kan verander te verminder (of glad nie sulke parameters ondersteun nie).

Vir die doeleindes van hierdie pos sal ons onderskei tussen statiese en dinamiese parameters. As die logika van die diens vereis dat parameters tydens die werking van die program verander word, sal ons sulke parameters dinamies noem. Andersins is die opsies staties en kan dit gekonfigureer word met behulp van die saamgestelde konfigurasie. Vir dinamiese herkonfigurasie het ons dalk 'n meganisme nodig om dele van die program met nuwe parameters te herbegin, soortgelyk aan hoe bedryfstelselprosesse herbegin word. (Na ons mening is dit raadsaam om intydse herkonfigurasie te vermy, aangesien dit die kompleksiteit van die stelsel verhoog. Indien moontlik, is dit beter om die standaard OS-vermoëns te gebruik om prosesse te herbegin.)

Een belangrike aspek van die gebruik van statiese konfigurasie wat mense dinamiese herkonfigurasie laat oorweeg, is die tyd wat dit neem vir die stelsel om te herlaai na 'n konfigurasie-opdatering (stilstand). Trouens, as ons veranderinge aan die statiese konfigurasie moet aanbring, sal ons die stelsel moet herbegin sodat die nuwe waardes in werking tree. Die stilstandprobleem verskil in erns vir verskillende stelsels. In sommige gevalle kan u 'n herlaai skeduleer op 'n tyd wanneer die vrag minimaal is. As jy deurlopende diens moet lewer, kan jy implementeer AWS ELB aansluiting dreineer. Terselfdertyd, wanneer ons die stelsel moet herlaai, begin ons 'n parallelle instansie van hierdie stelsel, skakel die balanseerder na dit en wag vir die ou verbindings om te voltooi. Nadat alle ou verbindings beëindig is, het ons die ou instansie van die stelsel afgesluit.

Kom ons kyk nou na die kwessie van die stoor van die konfigurasie binne of buite die artefak. As ons die konfigurasie binne 'n artefak stoor, dan het ons ten minste die geleentheid gehad om die korrektheid van die konfigurasie tydens die samestelling van die artefak te verifieer. As die konfigurasie buite die beheerde artefak is, is dit moeilik om op te spoor wie veranderinge aan hierdie lêer gemaak het en hoekom. Hoe belangrik is dit? Na ons mening is dit vir baie produksiestelsels belangrik om 'n stabiele en hoëgehalte-konfigurasie te hê.

Die weergawe van 'n artefak laat jou toe om te bepaal wanneer dit geskep is, watter waardes dit bevat, watter funksies geaktiveer/gedeaktiveer is, en wie verantwoordelik is vir enige verandering in die konfigurasie. Natuurlik verg dit moeite om die konfigurasie binne 'n artefak te stoor, so jy moet 'n ingeligte besluit neem.

Die voor- en nadele

Ek wil graag stilstaan ​​by die voor- en nadele van die voorgestelde tegnologie.

Voordele

Hieronder is 'n lys van die hoofkenmerke van 'n saamgestelde, verspreide stelselkonfigurasie:

  1. Statiese konfigurasiekontrole. Laat jou toe om seker te wees dat
    die konfigurasie is korrek.
  2. Ryk konfigurasietaal. Tipies is ander konfigurasiemetodes hoogstens beperk tot stringveranderlike vervanging. Wanneer jy Scala gebruik, is 'n wye verskeidenheid taalkenmerke beskikbaar om jou konfigurasie te verbeter. Ons kan byvoorbeeld gebruik
    eienskappe vir verstekwaardes, met behulp van voorwerpe om parameters te groepeer, kan ons verwys na vals wat slegs een keer verklaar is (DRY) in die omsluitende omvang. Jy kan enige klasse direk binne die konfigurasie instansieer (Seq, Map, pasgemaakte klasse).
  3. DSL. Scala het 'n aantal taalkenmerke wat dit makliker maak om 'n DSL te skep. Dit is moontlik om voordeel te trek uit hierdie kenmerke en 'n konfigurasietaal te implementeer wat geriefliker is vir die teikengroep gebruikers, sodat die konfigurasie ten minste leesbaar is deur domeinkundiges. Spesialiste kan byvoorbeeld aan die konfigurasiehersieningsproses deelneem.
  4. Integriteit en sinchronie tussen nodusse. Een van die voordele om die konfigurasie van 'n hele verspreide stelsel op 'n enkele punt te laat stoor, is dat alle waardes presies een keer verklaar word en dan hergebruik word waar dit ook al nodig is. Die gebruik van fantoomtipes om poorte te verklaar verseker dat nodusse versoenbare protokolle in alle korrekte stelselkonfigurasies gebruik. Om eksplisiete verpligte afhanklikhede tussen nodusse te hê, verseker dat alle dienste gekoppel is.
  5. Hoë kwaliteit veranderinge. Deur veranderinge aan die konfigurasie aan te bring deur gebruik te maak van 'n gemeenskaplike ontwikkelingsproses, maak dit moontlik om ook hoë kwaliteitstandaarde vir die konfigurasie te bereik.
  6. Gelyktydige konfigurasieopdatering. Outomatiese stelselontplooiing na konfigurasieveranderings verseker dat alle nodusse opgedateer word.
  7. Vereenvoudig die toepassing. Die toepassing het nie ontleding, konfigurasiekontrolering of hantering van verkeerde waardes nodig nie. Dit verminder die kompleksiteit van die toepassing. (Sommige van die konfigurasie-kompleksiteit wat in ons voorbeeld waargeneem word, is nie 'n kenmerk van die saamgestelde konfigurasie nie, maar slegs 'n bewuste besluit wat gedryf word deur die begeerte om groter tipe veiligheid te verskaf.) Dit is redelik maklik om terug te keer na die gewone konfigurasie - implementeer net die ontbrekende dele. Daarom kan u byvoorbeeld begin met 'n saamgestelde konfigurasie, wat die implementering van onnodige dele uitstel tot die tyd wanneer dit regtig nodig is.
  8. Geverifieerde konfigurasie. Aangesien konfigurasieveranderinge die gewone lot van enige ander veranderinge volg, is die uitset wat ons kry 'n artefak met 'n unieke weergawe. Dit stel ons byvoorbeeld in staat om terug te keer na 'n vorige weergawe van die konfigurasie indien nodig. Ons kan selfs die konfigurasie van 'n jaar gelede gebruik en die stelsel sal presies dieselfde werk. 'n Stabiele opset verbeter die voorspelbaarheid en betroubaarheid van 'n verspreide stelsel. Aangesien die konfigurasie in die samestellingstadium vasgestel is, is dit redelik moeilik om dit in produksie te vervals.
  9. Modulariteit. Die voorgestelde raamwerk is modulêr en die modules kan op verskillende maniere gekombineer word om verskillende stelsels te skep. In die besonder, kan jy die stelsel instel om te loop op 'n enkele nodus in een verpersoonliking, en op veelvuldige nodusse in 'n ander. U kan verskeie konfigurasies vir produksiegevalle van die stelsel skep.
  10. Toets. Deur individuele dienste met skynvoorwerpe te vervang, kan jy verskeie weergawes van die stelsel kry wat gerieflik is om te toets.
  11. Integrasie toets. Om 'n enkele konfigurasie vir die hele verspreide stelsel te hê, maak dit moontlik om alle komponente in 'n beheerde omgewing te laat loop as deel van integrasietoetsing. Dit is maklik om byvoorbeeld 'n situasie na te boots waar sommige nodusse toeganklik word.

Nadele en beperkings

Saamgestelde konfigurasie verskil van ander konfigurasiebenaderings en is dalk nie geskik vir sommige toepassings nie. Hieronder is 'n paar nadele:

  1. Statiese konfigurasie. Soms moet u die konfigurasie in produksie vinnig regstel, deur alle beskermingsmeganismes te omseil. Met hierdie benadering kan dit moeiliker wees. Ten minste sal samestelling en outomatiese ontplooiing steeds vereis word. Dit is beide 'n nuttige kenmerk van die benadering en 'n nadeel in sommige gevalle.
  2. Konfigurasie generering. In die geval dat die konfigurasielêer deur 'n outomatiese hulpmiddel gegenereer word, kan addisionele pogings nodig wees om die bouskrip te integreer.
  3. Gereedskap. Tans is nutsprogramme en tegnieke wat ontwerp is om met konfigurasie te werk, gebaseer op tekslêers. Nie alle sulke hulpmiddels/tegnieke sal in 'n saamgestelde opset beskikbaar wees nie.
  4. 'n Verandering in houdings word vereis. Ontwikkelaars en DevOps is gewoond aan tekslêers. Die idee van die samestelling van 'n konfigurasie kan ietwat onverwags en ongewoon wees en verwerping veroorsaak.
  5. 'n Ontwikkelingsproses van hoë gehalte word vereis. Om die saamgestelde konfigurasie gemaklik te kan gebruik, is volle outomatisering van die proses van bou en ontplooiing van die toepassing (CI/CD) nodig. Anders sal dit nogal ongerieflik wees.

Laat ons ook stilstaan ​​by 'n aantal beperkings van die oorwoë voorbeeld wat nie verband hou met die idee van 'n saamgestelde konfigurasie nie:

  1. As ons onnodige konfigurasie-inligting verskaf wat nie deur die nodus gebruik word nie, sal die samesteller ons nie help om die ontbrekende implementering op te spoor nie. Hierdie probleem kan opgelos word deur die koekpatroon te laat vaar en meer rigiede tipes te gebruik, byvoorbeeld, HList of algebraïese datatipes (gevalklasse) om konfigurasie voor te stel.
  2. Daar is reëls in die konfigurasielêer wat nie met die konfigurasie self verband hou nie: (package, import,voorwerpverklarings; override defse vir parameters wat verstekwaardes het). Dit kan gedeeltelik vermy word as jy jou eie DSL implementeer. Daarbenewens stel ander tipes konfigurasie (byvoorbeeld XML) ook sekere beperkings op die lêerstruktuur.
  3. Vir die doeleindes van hierdie pos oorweeg ons nie dinamiese herkonfigurasie van 'n groep soortgelyke nodusse nie.

Gevolgtrekking

In hierdie pos het ons die idee ondersoek om konfigurasie in bronkode voor te stel deur die gevorderde vermoëns van die Scala-tipe stelsel te gebruik. Hierdie benadering kan in verskeie toepassings gebruik word as 'n plaasvervanger vir tradisionele konfigurasiemetodes gebaseer op xml- of tekslêers. Alhoewel ons voorbeeld in Scala geïmplementeer is, kan dieselfde idees oorgedra word na ander saamgestelde tale (soos Kotlin, C#, Swift, ...). Jy kan hierdie benadering in een van die volgende projekte probeer, en as dit nie werk nie, gaan na die tekslêer en voeg die ontbrekende dele by.

Natuurlik vereis 'n saamgestelde opset 'n hoë kwaliteit ontwikkelingsproses. In ruil daarvoor word hoë kwaliteit en betroubaarheid van konfigurasies verseker.

Die beskoude benadering kan uitgebrei word:

  1. U kan makro's gebruik om samestelling-tydkontroles uit te voer.
  2. Jy kan 'n DSL implementeer om die konfigurasie aan te bied op 'n manier wat toeganklik is vir eindgebruikers.
  3. Jy kan dinamiese hulpbronbestuur implementeer met outomatiese konfigurasie-aanpassing. Byvoorbeeld, die verandering van die aantal nodusse in 'n kluster vereis dat (1) elke nodus 'n effens ander konfigurasie ontvang; (2) die groepbestuurder het inligting oor nuwe nodusse ontvang.

Erkennings

Ek wil graag Andrei Saksonov, Pavel Popov en Anton Nekhaev bedank vir hul konstruktiewe kritiek op die konsepartikel.

Bron: will.com

Voeg 'n opmerking