Saamstelbare konfigurasie van 'n verspreide stelsel

In hierdie pos wil ons graag 'n interessante manier deel om die konfigurasie van 'n verspreide stelsel te hanteer.
Die konfigurasie word direk in Scala-taal op 'n tipe veilige manier voorgestel. 'n Voorbeeldimplementering word in besonderhede beskryf. Verskeie aspekte van die voorstel word bespreek, insluitend invloed op die algehele ontwikkelingsproses.

Saamstelbare konfigurasie van 'n verspreide stelsel

(in Russies)

Inleiding

Die bou van robuuste verspreide stelsels vereis die gebruik van korrekte en samehangende konfigurasie op alle nodusse. 'n Tipiese oplossing is om 'n tekstuele ontplooiingsbeskrywing (terraform, ansible of iets soortgelyks) en outomaties gegenereerde konfigurasielêers te gebruik (dikwels - toegewy vir elke nodus/rol). Ons sal ook dieselfde protokolle van dieselfde weergawes op elke kommunikerende nodusse wil gebruik (anders sal ons onversoenbaarheidskwessies ervaar). In die JVM-wêreld beteken dit dat ten minste die boodskapbiblioteek van dieselfde weergawe op alle kommunikerende nodusse moet wees.

Wat van die toets van die stelsel? Natuurlik moet ons eenheidstoetse vir alle komponente hê voordat ons by integrasietoetse kom. Om toetsresultate op looptyd te kan ekstrapoleer, moet ons seker maak dat die weergawes van alle biblioteke identies gehou word in beide looptyd en toetsomgewings.

Wanneer integrasietoetse uitgevoer word, is dit dikwels baie makliker om dieselfde klaspad op alle nodusse te hê. Ons moet net seker maak dat dieselfde klaspad tydens ontplooiing gebruik word. (Dit is moontlik om verskillende klaspaaie op verskillende nodusse te gebruik, maar dit is moeiliker om hierdie konfigurasie voor te stel en dit korrek te ontplooi.) So om dinge eenvoudig te hou, sal ons slegs identiese klaspaaie op alle nodusse oorweeg.

Konfigurasie is geneig om saam met die sagteware te ontwikkel. Ons gebruik gewoonlik weergawes om verskeie te identifiseer
stadiums van sagteware-evolusie. Dit lyk redelik om konfigurasie onder weergawebestuur te dek en verskillende konfigurasies met sommige etikette te identifiseer. As daar net een konfigurasie in produksie is, kan ons enkele weergawe as 'n identifiseerder gebruik. Soms kan ons verskeie produksie-omgewings hê. En vir elke omgewing het ons dalk 'n aparte vertakking van konfigurasie nodig. So konfigurasies kan gemerk word met tak en weergawe om verskillende konfigurasies uniek te identifiseer. Elke taketiket en weergawe stem ooreen met 'n enkele kombinasie van verspreide nodusse, poorte, eksterne hulpbronne, klaspad-biblioteekweergawes op elke nodus. Hier sal ons slegs die enkele tak dek en konfigurasies identifiseer deur 'n drie-komponent desimale weergawe (1.2.3), op dieselfde manier as ander artefakte.

In moderne omgewings word konfigurasielêers nie meer met die hand gewysig nie. Tipies genereer ons
config lêers tydens ontplooiing tyd en nooit aan hulle raak nie daarna. So mens kan vra hoekom gebruik ons ​​steeds teksformaat vir konfigurasielêers? 'n Lewensvatbare opsie is om die konfigurasie binne 'n samestellingseenheid te plaas en voordeel te trek uit samestelling-tyd-konfigurasievalidering.

In hierdie pos sal ons die idee ondersoek om die konfigurasie in die saamgestelde artefak te hou.

Saamstelbare konfigurasie

In hierdie afdeling sal ons 'n voorbeeld van statiese konfigurasie bespreek. Twee eenvoudige dienste - eggo diens en die kliënt van die eggo diens word gekonfigureer en geïmplementeer. Dan word twee verskillende verspreide stelsels met albei dienste geïnstansieer. Een is vir 'n enkele nodus konfigurasie en 'n ander een vir twee nodes konfigurasie.

'n Tipiese verspreide stelsel bestaan ​​uit 'n paar nodusse. Die nodusse kan geïdentifiseer word met behulp van een of ander tipe:

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

of net

case class NodeId(hostName: String)

of selfs

object Singleton
type NodeId = Singleton.type

Hierdie nodusse verrig verskeie rolle, bedryf sekere dienste en behoort deur middel van TCP/HTTP-verbindings met die ander nodusse te kan kommunikeer.

Vir TCP-verbinding word ten minste 'n poortnommer vereis. Ons wil ook seker maak dat kliënt en bediener dieselfde protokol praat. Om 'n verband tussen nodusse te modelleer, kom ons verklaar die volgende klas:

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

waar Port is net 'n Int binne die toegelate omvang:

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

Verfynde tipes

sien verfynde biblioteek. Kortom, dit laat toe om tydsbeperkings saam te stel by ander tipes. In hierdie geval Int word slegs toegelaat om 16-bis waardes te hê wat poortnommer kan verteenwoordig. Daar is geen vereiste om hierdie biblioteek vir hierdie konfigurasiebenadering te gebruik nie. Dit lyk net of dit baie goed pas.

Vir HTTP (REST) ​​sal ons dalk ook 'n pad van die diens nodig hê:

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

Fantoom tipe

Om protokol tydens samestelling te identifiseer, gebruik ons ​​die Scala-kenmerk om tipe argument te verklaar Protocol wat nie in die klas gebruik word nie. Dit is 'n sogenaamde spooktipe. Tydens looptyd het ons selde 'n voorbeeld van protokol-identifiseerder nodig, daarom stoor ons dit nie. Tydens samestelling bied hierdie fantoomtipe addisionele tipe veiligheid. Ons kan nie poort met verkeerde protokol slaag nie.

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

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

waar RequestMessage is die basis tipe boodskappe wat kliënt na bediener en ResponseMessage is die antwoordboodskap vanaf bediener. Natuurlik kan ons ander protokolbeskrywings skep wat die kommunikasieprotokol met die verlangde akkuraatheid spesifiseer.

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

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

In hierdie protokol word versoekboodskap by url aangeheg en antwoordboodskap word as gewone string teruggestuur.

'n Dienskonfigurasie kan beskryf word deur die diensnaam, 'n versameling poorte en sommige afhanklikhede. Daar is 'n paar moontlike maniere om al hierdie elemente in Scala voor te stel (byvoorbeeld, HList, algebraïese datatipes). Vir die doeleindes van hierdie pos sal ons Koekpatroon gebruik en kombineerbare stukke (modules) as eienskappe voorstel. (Koekpatroon is nie 'n vereiste vir hierdie saamstelbare konfigurasiebenadering nie. Dit is net een moontlike implementering van die idee.)

Afhanklikhede kan voorgestel word deur die Koekpatroon as eindpunte van ander nodusse te gebruik:

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

Echo-diens benodig slegs 'n poort wat gekonfigureer is. En ons verklaar dat hierdie poort eggo-protokol ondersteun. Let daarop dat ons nie op hierdie oomblik 'n spesifieke poort hoef te spesifiseer nie, want kenmerke laat abstrakte metodeverklarings toe. As ons abstrakte metodes gebruik, sal samesteller 'n implementering in 'n konfigurasie-instansie vereis. Hier het ons die implementering verskaf (8081) en dit sal as die verstekwaarde gebruik word as ons dit in 'n konkrete konfigurasie oorslaan.

Ons kan 'n afhanklikheid in die konfigurasie van die eggo-dienskliënt verklaar:

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

Afhanklikheid het dieselfde tipe as die echoService. Dit vereis veral dieselfde protokol. Daarom kan ons seker wees dat as ons hierdie twee afhanklikhede verbind hulle korrek sal werk.

Dienste implementering

'n Diens het 'n funksie nodig om te begin en grasieus af te sluit. (Die vermoë om 'n diens af te sluit is krities vir toetsing.) Weereens is daar 'n paar opsies om so 'n funksie vir 'n gegewe konfigurasie te spesifiseer (ons kan byvoorbeeld tipe klasse gebruik). Vir hierdie pos sal ons weer Koekpatroon gebruik. Ons kan 'n diens verteenwoordig deur gebruik te maak cats.Resource wat reeds bracketing en hulpbronvrystelling verskaf. Om 'n hulpbron te bekom, moet ons 'n konfigurasie en 'n mate van looptydkonteks verskaf. Die diensbeginfunksie kan dus 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 - tipe konfigurasie wat deur hierdie diensaansitter vereis word
  • AddressResolver - 'n looptydvoorwerp wat die vermoë het om regte adresse van ander nodusse te verkry (hou aan om te lees vir besonderhede).

die ander tipes kom vandaan cats:

  • F[_] - effek tipe (In die eenvoudigste geval F[A] kan net wees () => A. In hierdie pos sal ons gebruik cats.IO.)
  • Reader[A,B] — is min of meer 'n sinoniem vir 'n funksie A => B
  • cats.Resource - het maniere om te verkry en vry te stel
  • Timer — laat toe om te slaap/tyd te meet
  • ContextShift - analoog van ExecutionContext
  • Applicative - omhulsel van funksies in effek (byna 'n monade) (ons kan dit uiteindelik met iets anders vervang)

Deur hierdie koppelvlak te gebruik, kan ons 'n paar 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](()))
  }

(Sien Bronkode vir ander dienste implementerings — eggo diens,
eggo kliënt en lewenslange beheerders.)

'n Nodus is 'n enkele voorwerp wat 'n paar dienste bestuur (om 'n ketting hulpbronne te begin word deur Cake Pattern geaktiveer):

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

Let daarop dat ons in die nodus die presiese tipe konfigurasie spesifiseer wat deur hierdie nodus benodig word. Die samesteller sal ons nie toelaat om die voorwerp (Cake) met onvoldoende tipe te bou nie, want elke dienseienskap verklaar 'n beperking op die Config tipe. Ons sal ook nie nodus kan begin sonder om volledige konfigurasie te verskaf nie.

Node adres resolusie

Om 'n verbinding te vestig, benodig ons 'n regte gasheeradres vir elke nodus. Dit kan later as ander dele van die opstelling bekend wees. Daarom het ons 'n manier nodig om 'n kartering tussen nodus-ID en sy werklike adres te verskaf. Hierdie kartering is 'n funksie:

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

Daar is 'n paar moontlike maniere om so 'n funksie te implementeer.

  1. As ons werklike adresse ken voor ontplooiing, tydens instansiering van nodusgashere, kan ons Scala-kode genereer met die werklike adresse en die bou daarna uitvoer (wat samestellingstydkontroles uitvoer en dan integrasietoetssuite uitvoer). In hierdie geval is ons karteringsfunksie staties bekend en kan dit vereenvoudig word tot iets soos a Map[NodeId, NodeAddress].
  2. Soms kry ons werklike adresse eers op 'n later stadium wanneer die nodus werklik begin is, of ons het nie adresse van nodusse wat nog nie begin is nie. In hierdie geval kan ons 'n ontdekkingsdiens hê wat voor alle ander nodusse begin word en elke nodus kan sy adres in daardie diens adverteer en op afhanklikhede inteken.
  3. As ons kan verander /etc/hosts, kan ons vooraf gedefinieerde gasheername gebruik (soos my-project-main-node en echo-backend) en assosieer net hierdie naam met die IP-adres tydens ontplooiingstyd.

In hierdie pos dek ons ​​nie hierdie gevalle in meer besonderhede nie. Trouens, in ons speelgoedvoorbeeld sal alle nodusse dieselfde IP-adres hê - 127.0.0.1.

In hierdie pos sal ons twee verspreide stelseluitlegte oorweeg:

  1. Enkelnodusuitleg, waar alle dienste op die enkele nodus geplaas word.
  2. Twee nodusse uitleg, waar diens en kliënt op verskillende nodusse is.

Die konfigurasie vir 'n enkele nodus uitleg is soos volg:

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

Hier skep ons 'n enkele konfigurasie wat beide bediener- en kliëntkonfigurasie uitbrei. Ons konfigureer ook 'n lewensiklusbeheerder wat gewoonlik die kliënt en bediener daarna sal beëindig lifetime interval verby.

Dieselfde stel diensimplementerings en -konfigurasies kan gebruik word om 'n stelsel se uitleg met twee afsonderlike nodusse te skep. Ons moet net skep twee afsonderlike nodus konfigurasies met die toepaslike dienste:

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

Kyk hoe ons die afhanklikheid spesifiseer. Ons noem die ander nodus se gelewerde diens as 'n afhanklikheid van die huidige nodus. Die tipe afhanklikheid word nagegaan omdat dit fantoomtipe bevat wat protokol beskryf. En tydens looptyd sal ons die korrekte nodus-ID hê. Dit is een van die belangrike aspekte van die voorgestelde konfigurasiebenadering. Dit bied ons die vermoë om poort slegs een keer in te stel en seker te maak dat ons die korrekte poort verwys.

Twee nodusse implementering

Vir hierdie konfigurasie gebruik ons ​​presies dieselfde diensimplementasies. Geen veranderinge nie. Ons skep egter twee verskillende nodusimplementerings wat verskillende stel dienste bevat:

  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 bediener en dit benodig slegs bedienerkantkonfigurasie. Die tweede nodus implementeer kliënt en benodig 'n ander deel van konfigurasie. Beide nodusse vereis 'n mate van leeftydspesifikasie. Vir die doeleindes van hierdie posdiensnodus sal oneindige leeftyd hê wat beëindig kan word deur gebruik te maak SIGTERM, terwyl eggo-kliënt sal beëindig na die gekonfigureerde eindige duur. Sien die beginner aansoek vir meer besonderhede.

Algehele ontwikkelingsproses

Kom ons kyk hoe hierdie benadering die manier waarop ons met konfigurasie werk, verander.

Die konfigurasie as kode sal saamgestel word en produseer 'n artefak. Dit lyk redelik om konfigurasie-artefakte van ander kode-artefakte te skei. Dikwels kan ons 'n menigte konfigurasies op dieselfde kodebasis hê. En natuurlik kan ons verskeie weergawes van verskillende konfigurasietakke hê. In 'n konfigurasie kan ons spesifieke weergawes van biblioteke kies en dit sal konstant bly wanneer ons hierdie konfigurasie ontplooi.

'n Konfigurasieverandering word kodeverandering. Dit behoort dus deur dieselfde kwaliteitsversekeringsproses gedek te word:

Kaartjie -> PR -> hersiening -> saamsmelt -> deurlopende integrasie -> deurlopende ontplooiing

Daar is die volgende gevolge van die benadering:

  1. Die konfigurasie is koherent vir 'n spesifieke stelsel se instansie. Dit blyk dat daar geen manier is om verkeerde verbinding tussen nodusse te hê nie.
  2. Dit is nie maklik om konfigurasie net in een nodus te verander nie. Dit lyk onredelik om aan te meld en sommige tekslêers te verander. So konfigurasie dryf word minder moontlik.
  3. Klein konfigurasieveranderinge is nie maklik om te maak nie.
  4. Die meeste van die konfigurasieveranderinge sal dieselfde ontwikkelingsproses volg, en dit sal 'n mate van hersiening slaag.

Het ons 'n aparte bewaarplek nodig vir produksiekonfigurasie? Die produksiekonfigurasie bevat dalk sensitiewe inligting wat ons buite bereik van baie mense wil hou. Dit kan dus die moeite werd wees om 'n aparte bewaarplek met beperkte toegang te hou wat die produksiekonfigurasie sal bevat. Ons kan die konfigurasie in twee dele verdeel - een wat die mees oop produksieparameters bevat en een wat die geheime deel van konfigurasie bevat. Dit sal toegang tot die meeste van die ontwikkelaars moontlik maak tot die oorgrote meerderheid parameters, terwyl toegang tot werklik sensitiewe dinge beperk word. Dit is maklik om dit te bereik met behulp van intermediêre eienskappe met verstek parameterwaardes.

Variasies

Kom ons kyk na voor- en nadele van die voorgestelde benadering in vergelyking met die ander konfigurasiebestuurstegnieke.

Eerstens sal ons 'n paar alternatiewe lys vir die verskillende aspekte van die voorgestelde manier om met konfigurasie om te gaan:

  1. Tekslêer op die teikenmasjien.
  2. Gesentraliseerde sleutel-waarde berging (soos etcd/zookeeper).
  3. Subproseskomponente wat herkonfigureer/herbegin kan word sonder om proses te herbegin.
  4. Konfigurasie buite artefak en weergawebeheer.

Tekslêer gee 'n mate van buigsaamheid in terme van ad hoc-oplossings. 'n Stelsel se administrateur kan by die teikennodus aanmeld, 'n verandering aanbring en die diens eenvoudig herbegin. Dit is dalk nie so goed vir groter stelsels nie. Geen spore word agter die verandering gelaat nie. Die verandering word nie deur 'n ander paar oë hersien nie. Dit kan moeilik wees om uit te vind wat die verandering veroorsaak het. Dit is nie getoets nie. Vanuit die verspreide stelsel se perspektief kan 'n administrateur eenvoudig vergeet om die konfigurasie in een van die ander nodusse op te dateer.

(Btw, as dit uiteindelik nodig sal wees om tekskonfigurasielêers te begin gebruik, sal ons net ontleder + valideerder moet byvoeg wat dieselfde kan produseer Config tik en dit sal genoeg wees om tekskonfigurasies te begin gebruik. Dit wys ook dat die kompleksiteit van samestelling-tyd-konfigurasie 'n bietjie kleiner is as die kompleksiteit van teksgebaseerde konfigurasies, want in teksgebaseerde weergawe benodig ons 'n bietjie addisionele kode.)

Gesentraliseerde sleutelwaardeberging is 'n goeie meganisme vir die verspreiding van toepassingsmetaparameters. Hier moet ons dink oor wat ons as konfigurasiewaardes beskou en wat net data is. 'n funksie gegee C => A => B ons noem gewoonlik selde veranderende waardes C "konfigurasie", terwyl data gereeld verander word A - voer net data in. Konfigurasie moet vroeër as die data aan die funksie verskaf word A. Gegewe hierdie idee kan ons sê dat dit die verwagte frekwensie van veranderinge is wat gebruik kan word om konfigurasiedata van net data te onderskei. Ook data kom tipies van een bron (gebruiker) en konfigurasie kom van 'n ander bron (admin). Die hantering van parameters wat na die inisialiseringsproses verander kan word, lei tot 'n toename in toepassingskompleksiteit. Vir sulke parameters sal ons hul afleweringsmeganisme, ontleding en validering moet hanteer, en verkeerde waardes hanteer. Om programkompleksiteit te verminder, moet ons dus beter die aantal parameters wat tydens looptyd kan verander verminder (of selfs heeltemal uitskakel).

Vanuit die perspektief van hierdie pos moet ons 'n onderskeid tref tussen statiese en dinamiese parameters. As dienslogika seldsame verandering van sommige parameters tydens looptyd vereis, kan ons dit dinamiese parameters noem. Andersins is hulle staties en kan die voorgestelde benadering gekonfigureer word. Vir dinamiese herkonfigurasie kan ander benaderings nodig wees. Byvoorbeeld, dele van die stelsel kan herbegin word met die nuwe konfigurasieparameters op 'n soortgelyke manier as om afsonderlike prosesse van 'n verspreide stelsel te herbegin.
(My beskeie mening is om runtime-herkonfigurasie te vermy omdat dit die kompleksiteit van die stelsel verhoog.
Dit is dalk meer eenvoudig om net op OS-ondersteuning staat te maak om prosesse te herbegin. Alhoewel dit dalk nie altyd moontlik is nie.)

Een belangrike aspek van die gebruik van statiese konfigurasie wat mense soms dinamiese konfigurasie laat oorweeg (sonder ander redes), is diensstilstand tydens konfigurasieopdatering. Inderdaad, as ons veranderinge aan statiese konfigurasie moet aanbring, moet ons die stelsel herbegin sodat nuwe waardes effektief word. Die vereistes vir stilstand verskil vir verskillende stelsels, so dit is dalk nie so krities nie. As dit krities is, dan moet ons vooruit beplan vir enige stelsel herbegin. Ons kan byvoorbeeld implementeer AWS ELB aansluiting dreineer. In hierdie scenario wanneer ons ook al die stelsel moet herbegin, begin ons 'n nuwe instansie van die stelsel in parallel, skakel dan ELB daarheen oor, terwyl die ou stelsel die diens van bestaande verbindings laat voltooi.

Wat van die behoud van konfigurasie binne-in weergawe-artefak of buite? Om konfigurasie binne 'n artefak te hou, beteken in die meeste gevalle dat hierdie konfigurasie dieselfde kwaliteitsversekeringsproses as ander artefakte geslaag het. Mens kan dus seker wees dat die konfigurasie van goeie gehalte en betroubaar is. Inteendeel, konfigurasie in 'n aparte lêer beteken dat daar geen spore is van wie en hoekom veranderinge aan daardie lêer gemaak het nie. Is dit belangrik? Ons glo dat dit vir die meeste produksiestelsels beter is om stabiele en hoë kwaliteit konfigurasie te hê.

Weergawe van die artefak laat toe om uit te vind wanneer dit geskep is, watter waardes dit bevat, watter kenmerke geaktiveer/gedeaktiveer is, wie verantwoordelik was vir die maak van elke verandering in die konfigurasie. Dit sal dalk 'n bietjie moeite verg om konfigurasie binne 'n artefak te hou en dit is 'n ontwerpkeuse om te maak.

Voor-nadele

Hier wil ons 'n paar voordele uitlig en 'n paar nadele van die voorgestelde benadering bespreek.

voordele

Kenmerke van die saamstelbare konfigurasie van 'n volledige verspreide stelsel:

  1. Statiese kontrolering van konfigurasie. Dit gee 'n hoë vlak van vertroue, dat die konfigurasie korrek is gegewe tipe beperkings.
  2. Ryk taal van konfigurasie. Tipies is ander konfigurasiebenaderings beperk tot hoogstens veranderlike vervanging.
    Deur Scala te gebruik, kan 'n mens 'n wye reeks taalkenmerke gebruik om konfigurasie beter te maak. Ons kan byvoorbeeld eienskappe gebruik om verstekwaardes te verskaf, voorwerpe om verskillende omvang te stel, ons kan verwys na vals gedefinieer slegs een keer in die buitenste bestek (DROOG). Dit is moontlik om letterlike rye, of gevalle van sekere klasse (Seq, Map, Ens.)
  3. DSL. Scala het ordentlike ondersteuning vir DSL-skrywers. 'n Mens kan hierdie kenmerke gebruik om 'n konfigurasietaal te vestig wat geriefliker en eindgebruikervriendelik is, sodat die finale konfigurasie ten minste leesbaar is vir domeingebruikers.
  4. Integriteit en samehang oor nodusse heen. Een van die voordele om konfigurasie vir die hele verspreide stelsel op een plek te hê, is dat alle waardes streng een keer gedefinieer word en dan hergebruik word op alle plekke waar ons dit nodig het. Tik ook veilige poort verklarings verseker dat in alle moontlike korrekte konfigurasies die stelsel se nodusse dieselfde taal sal praat. Daar is eksplisiete afhanklikhede tussen nodusse wat dit moeilik maak om te vergeet om sekere dienste te verskaf.
  5. Hoë kwaliteit van veranderinge. Die algehele benadering om konfigurasieveranderinge deur normale PR-proses deur te gee, stel hoë standaarde van kwaliteit ook in konfigurasie.
  6. Gelyktydige konfigurasieveranderinge. Wanneer ons enige veranderinge in die konfigurasie maak, verseker outomatiese ontplooiing dat alle nodusse opgedateer word.
  7. Toepassingsvereenvoudiging. Die toepassing hoef nie konfigurasie te ontleed en te valideer en verkeerde konfigurasiewaardes te hanteer nie. Dit vereenvoudig die algehele toepassing. (Daar is 'n mate van kompleksiteitsverhoging in die konfigurasie self, maar dit is 'n doelbewuste afruil vir veiligheid.) Dit is redelik eenvoudig om terug te keer na gewone konfigurasie - voeg net die ontbrekende stukke by. Dit is makliker om met saamgestelde konfigurasie te begin en die implementering van bykomende stukke uit te stel na 'n paar later tye.
  8. Weergawe opstelling. As gevolg van die feit dat konfigurasieveranderinge dieselfde ontwikkelingsproses volg, kry ons gevolglik 'n artefak met 'n unieke weergawe. Dit stel ons in staat om konfigurasie terug te skakel indien nodig. Ons kan selfs 'n konfigurasie ontplooi wat 'n jaar gelede gebruik is en dit sal presies op dieselfde manier werk. Stabiele konfigurasie verbeter die voorspelbaarheid en betroubaarheid van die verspreide stelsel. Die konfigurasie is vasgestel tydens samestelling en kan nie maklik op 'n produksiestelsel gepeuter word nie.
  9. Modulariteit. Die voorgestelde raamwerk is modulêr en modules kan op verskeie maniere gekombineer word om
    ondersteun verskillende konfigurasies (opstellings/uitlegte). Dit is veral moontlik om 'n kleinskaalse enkelnodusuitleg en 'n grootskaalse multinodusinstelling te hê. Dit is redelik om verskeie produksie-uitlegte te hê.
  10. Toets. Vir toetsdoeleindes kan 'n mens 'n skyndiens implementeer en dit as 'n afhanklikheid op 'n tipe veilige manier gebruik. 'n Paar verskillende toetsuitlegte met verskeie onderdele wat deur spotters vervang is, kan gelyktydig gehandhaaf word.
  11. Integrasie toets. In verspreide stelsels is dit soms moeilik om integrasietoetse uit te voer. Deur die beskryf benadering te gebruik om veilige konfigurasie van die volledige verspreide stelsel te tik, kan ons alle verspreide dele op 'n enkele bediener op 'n beheerbare manier laat loop. Dit is maklik om die situasie na te boots
    wanneer een van die dienste onbeskikbaar raak.

Disadvantages

Die saamgestelde konfigurasiebenadering verskil van "normale" konfigurasie en dit pas dalk nie by alle behoeftes nie. Hier is 'n paar van die nadele van die saamgestelde konfigurasie:

  1. Statiese konfigurasie. Dit is dalk nie geskik vir alle toepassings nie. In sommige gevalle is dit nodig om die konfigurasie vinnig in produksie reg te stel en alle veiligheidsmaatreëls te omseil. Hierdie benadering maak dit moeiliker. Die samestelling en herontplooiing word vereis nadat enige verandering in konfigurasie gemaak is. Dit is beide die kenmerk en die las.
  2. Konfigurasie generering. Wanneer config deur een of ander outomatiseringsinstrument gegenereer word, vereis hierdie benadering daaropvolgende samestelling (wat op sy beurt kan misluk). Dit kan bykomende moeite verg om hierdie bykomende stap in die boustelsel te integreer.
  3. Instrumente. Daar is baie instrumente wat vandag gebruik word wat staatmaak op teksgebaseerde konfigurasies. Sommige van hulle
    sal nie van toepassing wees wanneer konfigurasie saamgestel is nie.
  4. ’n Verskuiwing in denkwyse is nodig. Ontwikkelaars en DevOps is vertroud met tekskonfigurasielêers. Die idee om konfigurasie saam te stel, kan vir hulle vreemd voorkom.
  5. Voordat saamstelbare konfigurasie bekendgestel word, word 'n hoë kwaliteit sagteware-ontwikkelingsproses vereis.

Daar is 'n paar beperkings van die geïmplementeerde voorbeeld:

  1. As ons ekstra konfigurasie verskaf wat nie deur die nodusimplementering vereis word nie, sal samesteller ons nie help om die afwesige implementering op te spoor nie. Dit kan aangespreek word deur gebruik te maak van HList of ADT's (gevalklasse) vir noduskonfigurasie in plaas van eienskappe en koekpatroon.
  2. Ons moet 'n paar boilerplate in config lêer verskaf: (package, import, object verklarings;
    override defse vir parameters wat verstekwaardes het). Dit kan gedeeltelik aangespreek word deur 'n DSL te gebruik.
  3. In hierdie pos dek ons ​​nie dinamiese herkonfigurasie van trosse van soortgelyke nodusse nie.

Gevolgtrekking

In hierdie pos het ons die idee bespreek om konfigurasie direk in die bronkode op 'n tipe veilige manier voor te stel. Die benadering kan in baie toepassings gebruik word as 'n vervanging vir xml- en ander teksgebaseerde konfigurasies. Ten spyte daarvan dat ons voorbeeld in Scala geïmplementeer is, kan dit ook vertaal word na ander saamstelbare tale (soos Kotlin, C#, Swift, ens.). Mens kan hierdie benadering in 'n nuwe projek probeer en, ingeval dit nie goed pas nie, oorskakel na die outydse manier.

Natuurlik vereis saamstelbare konfigurasie hoë kwaliteit ontwikkelingsproses. In ruil daarvoor beloof dit om robuuste konfigurasie van ewe hoë gehalte te verskaf.

Hierdie benadering kan op verskeie maniere uitgebrei word:

  1. 'n Mens kan makro's gebruik om konfigurasie-validering uit te voer en misluk tydens samestelling in die geval van enige besigheidslogika-beperkings mislukkings.
  2. 'n DSL kan geïmplementeer word om konfigurasie op 'n domeingebruikersvriendelike manier voor te stel.
  3. Dinamiese hulpbronbestuur met outomatiese konfigurasie-aanpassings. Byvoorbeeld, wanneer ons die aantal cluster nodusse aanpas, wil ons dalk (1) hê dat die nodusse effens gewysigde konfigurasie moet verkry; (2) groepbestuurder om nuwe nodusse-inligting te ontvang.

Dankie

Ek wil graag dankie sê aan Andrey Saksonov, Pavel Popov, Anton Nehaev vir die gee van inspirerende terugvoer oor die konsep van hierdie pos wat my gehelp het om dit duideliker te maak.

Bron: will.com