Kompiléiert verdeelt Systemkonfiguratioun

Ech wéilt Iech en interessante Mechanismus soen fir mat der Konfiguratioun vun engem verdeelte System ze schaffen. D'Konfiguratioun gëtt direkt an enger kompiléierter Sprooch (Scala) mat sécheren Typen vertrueden. Dëse Post bitt e Beispill vun esou enger Konfiguratioun an diskutéiert verschidden Aspekter vun der Ëmsetzung vun enger kompiléierter Konfiguratioun an de Gesamtentwécklungsprozess.

Kompiléiert verdeelt Systemkonfiguratioun

(Englesch)

Aféierung

Bauen vun engem zouverlässeg verdeelt System heescht datt all Wirbelen déi richteg Konfiguratioun benotzen, synchroniséiert mat anere Wirbelen. DevOps Technologien (terraform, ansible oder eppes wéi dat) ginn normalerweis benotzt fir automatesch Konfiguratiounsdateien ze generéieren (dacks spezifesch fir all Node). Mir wëllen och sécher sinn datt all kommunizéierend Noden identesch Protokoller benotzen (inklusiv déiselwecht Versioun). Soss gëtt Inkompatibilitéit an eisem verdeelte System agebaut. An der JVM Welt ass eng Konsequenz vun dëser Fuerderung datt déiselwecht Versioun vun der Bibliothéik mat de Protokollmeldungen iwwerall muss benotzt ginn.

Wat iwwer Testen vun engem verdeelt System? Natierlech huelen mir un datt all Komponenten Eenheetstester hunn ier mer op d'Integratiounstest weidergoen. (Fir datt mir d'Testresultater op d'Runtime extrapoléiere kënnen, musse mir och en identesche Set vu Bibliothéiken an der Teststadium an der Runtime ubidden.)

Wann Dir mat Integratiounstester schafft, ass et dacks méi einfach dee selwechte Klassewee iwwerall op all Noden ze benotzen. Alles wat mir maache mussen ass sécherzestellen datt dee selwechte Klassewee während der Runtime benotzt gëtt. (Obwuel et ganz méiglech ass verschidde Wirbelen mat verschiddene Klasseweeër ze lafen, füügt dëst Komplexitéit un d'Gesamtkonfiguratioun a Schwieregkeete mat Deployment- an Integratiounstester.) Fir d'Ziler vun dësem Post gi mir un datt all Wirbelen deeselwechte Klassepath benotzen.

D'Konfiguratioun evoluéiert mat der Applikatioun. Mir benotze Versioune fir verschidden Etappe vun der Programmevolutioun z'identifizéieren. Et schéngt logesch och verschidde Versioune vu Konfiguratiounen z'identifizéieren. A plazéiert d'Konfiguratioun selwer am Versiounskontrollsystem. Wann et nëmmen eng Konfiguratioun an der Produktioun ass, da kënne mir einfach d'Versiounsnummer benotzen. Wa mir vill Produktiounsinstanzen benotzen, da brauche mir e puer
Konfiguratiounszweige an en zousätzleche Label zousätzlech zu der Versioun (zum Beispill den Numm vun der Branche). Op dës Manéier kënne mir déi exakt Konfiguratioun kloer identifizéieren. All Konfiguratiounsidentifizéierer entsprécht eenzegaarteg enger spezifescher Kombinatioun vu verdeelte Wirbelen, Ports, externe Ressourcen a Bibliothéiksversioune. Fir den Zweck vun dësem Post wäerte mir ugeholl datt et nëmmen eng Branche gëtt a mir kënnen d'Konfiguratioun op déi üblech Manéier identifizéieren andeems Dir dräi Zuelen getrennt vun engem Punkt (1.2.3) benotzt.

A modernen Ëmfeld gi Konfiguratiounsdateien selten manuell erstallt. Méi dacks gi se wärend der Deployment generéiert a ginn net méi beréiert (sou datt brécht näischt). Eng natierlech Fro stellt sech: Firwat benotze mir nach ëmmer Textformat fir d'Konfiguratioun ze späicheren? Eng liewensfäeg Alternativ schéngt d'Fäegkeet ze sinn regelméisseg Code fir d'Konfiguratioun ze benotzen a vu Compile-Zäit Kontrollen ze profitéieren.

An dësem Post wäerte mir d'Iddi entdecken fir eng Konfiguratioun an engem kompiléierten Artefakt ze representéieren.

Kompiléiert Konfiguratioun

Dës Sektioun gëtt e Beispill vun enger statesch kompiléiert Configuratioun. Zwee einfach Servicer ginn ëmgesat - den Echo Service an den Echo Service Client. Baséierend op dësen zwee Servicer ginn zwou Systemoptiounen zesummegesat. An enger Optioun sinn béid Servicer um selwechten Node, an enger anerer Optioun - op verschiddene Wirbelen.

Typesch enthält e verdeelt System verschidde Wirbelen. Dir kënnt Noden identifizéieren mat Wäerter vun iergendenger Aart NodeId:

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

oder

case class NodeId(hostName: String)

oder souguer

object Singleton
type NodeId = Singleton.type

Node maachen verschidde Rollen, si lafen Servicer an TCP / HTTP Verbindunge kënnen tëscht hinnen etabléiert ginn.

Fir eng TCP Verbindung ze beschreiwen brauche mir op d'mannst eng Portnummer. Mir wëllen och de Protokoll reflektéieren deen op deem Hafen ënnerstëtzt gëtt fir sécherzestellen datt souwuel de Client wéi de Server deeselwechte Protokoll benotzen. Mir beschreiwen d'Verbindung mat der folgender Klass:

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

wou Port - just eng ganz Zuel Int déi d'Gamme vun akzeptable Wäerter uginn:

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

Raffinéiert Zorten

Gesinn Bibliothéik raffinéiert и mäi de Bericht. Kuerz gesot, d'Bibliothéik erlaabt Iech Aschränkungen un Typen ze addéieren déi an der Zesummesetzungszäit gepréift ginn. An dësem Fall sinn valabel Portnummer Wäerter 16-Bit ganz Zuelen. Fir eng kompiléiert Konfiguratioun ass d'Benotzung vun der raffinéierter Bibliothéik net obligatoresch, awer et verbessert d'Fäegkeet vum Compiler fir d'Konfiguratioun ze kontrolléieren.

Fir HTTP (REST) ​​Protokoller, zousätzlech zu der Portnummer, kënne mir och de Wee zum Service brauchen:

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

Phantom Typen

Fir de Protokoll an der Zäit ze kompiléieren, benotze mir en Typparameter deen net an der Klass benotzt gëtt. Dës Entscheedung ass wéinst der Tatsaach datt mir keng Protokollinstanz beim Runtime benotzen, awer mir wëllen datt de Compiler d'Protokollkompatibilitéit iwwerpréift. Andeems Dir de Protokoll spezifizéiert, kënne mir net en onpassend Service als Ofhängegkeet passéieren.

Ee vun de gemeinsame Protokoller ass de REST API mat Json Serialiséierung:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

wou RequestMessage - Ufro Typ, ResponseMessage - Äntwert Typ.
Natierlech kënne mir aner Protokollbeschreiwunge benotzen déi d'Genauegkeet vun der Beschreiwung ubidden, déi mir brauchen.

Fir Zwecker vun dësem Post benotze mir eng vereinfacht Versioun vum Protokoll:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Hei ass d'Ufro eng String, déi un d'URL bäigefüügt ass, an d'Äntwert ass de zréckginn String am Kierper vun der HTTP Äntwert.

D'Servicekonfiguratioun gëtt vum Servicenumm, Ports an Ofhängegkeeten beschriwwen. Dës Elementer kënnen a Scala op verschidde Manéiere vertruede ginn (zum Beispill, HList-s, algebraesch Datentypen). Fir den Zweck vun dësem Post benotze mir de Kuchmuster a representéieren Moduler benotzt trait'ov. (De Kuchmuster ass keen erfuerderlechen Element vun dëser Approche. Et ass einfach eng méiglech Ëmsetzung.)

Ofhängegkeeten tëscht Servicer kënnen als Methoden duergestallt ginn, déi Häfen zréckginn EndPointvun aneren Noden:

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

Fir en Echo Service ze kreéieren, braucht Dir nëmmen eng Portnummer an eng Indikatioun datt den Hafen den Echo Protokoll ënnerstëtzt. Mir kënnen net e spezifesche Port spezifizéieren, well ... Charaktere erlaben Iech Methoden ouni Ëmsetzung ze deklaréieren (abstrakt Methoden). An dësem Fall, wann Dir eng konkret Konfiguratioun erstellt, brauch de Compiler datt mir eng Implementatioun vun der abstrakter Method ubidden an eng Portnummer ubidden. Well mir d'Method ëmgesat hunn, wann Dir eng spezifesch Konfiguratioun erstellt, kënne mir net en aneren Hafen spezifizéieren. De Standardwäert gëtt benotzt.

An der Clientkonfiguratioun erkläre mir eng Ofhängegkeet vum Echo Service:

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

D'Ofhängegkeet ass vum selwechten Typ wéi den exportéierten Service echoService. Besonnesch am Echo Client brauche mir dee selwechte Protokoll. Dofir, wann Dir zwee Servicer verbënnt, kënne mir sécher sinn datt alles richteg funktionnéiert.

Ëmsetzung vun Servicer

Eng Funktioun ass erfuerderlech fir de Service ze starten an ze stoppen. (D'Kapazitéit fir e Service ze stoppen ass kritesch fir Testen.) Erëm ginn et e puer Méiglechkeeten fir esou eng Fonktioun ëmzesetzen (zum Beispill kënne mir Typ Klassen op Basis vun der Konfiguratiounstyp benotzen). Fir den Zweck vun dësem Post benotze mir de Kuchmuster. Mir wäerten de Service mat enger Klass vertrieden cats.Resource, well Dës Klass bitt scho Mëttele fir sécher d'Verëffentlechung vu Ressourcen am Fall vu Probleemer ze garantéieren. Fir eng Ressource ze kréien, musse mir Konfiguratioun an e fäerdege Runtime Kontext ubidden. D'Servicestartfunktioun kann esou ausgesinn:

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

wou

  • Config - Konfiguratiounstyp fir dëse Service
  • AddressResolver - e Runtime-Objet deen Iech erlaabt d'Adresse vun aneren Noden erauszefannen (kuckt hei ënnen)

an aner Zorte vun der Bibliothéik cats:

  • F[_] - Typ vun Effekt (am einfachsten Fall F[A] kéint just eng Funktioun sinn () => A. An dësem Post wäerte mir benotzen cats.IO.)
  • Reader[A,B] - méi oder manner synonym mat Funktioun A => B
  • cats.Resource - eng Ressource déi kritt a verëffentlecht ka ginn
  • Timer - Timer (erlaabt Iech fir eng Zäit ze schlofen an Zäitintervaller ze moossen)
  • ContextShift - analog ExecutionContext
  • Applicative - eng Effekttyp Klass déi Iech erlaabt individuell Effekter ze kombinéieren (bal eng Monad). A méi komplexen Uwendungen schéngt et besser ze benotzen Monad/ConcurrentEffect.

Mat dëser Funktioun Ënnerschrëft kënne mir verschidde Servicer implementéieren. Zum Beispill, e Service deen näischt mécht:

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

(Kuckt Quell, an deenen aner Servicer ëmgesat ginn - echo Service, echo Client
и Liewensdauer Controller.)

En Node ass en Objet deen e puer Servicer lancéiere kann (de Start vun enger Kette vu Ressourcen ass duerch de Cake Pattern gesuergt):

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

Notéiert w.e.g. datt mir déi exakt Aart vun der Konfiguratioun spezifizéieren déi fir dësen Node erfuerderlech ass. Wa mir vergiessen eng vun de Konfiguratiounstypen ze spezifizéieren, déi vun engem bestëmmte Service erfuerderlech sinn, gëtt et e Kompiléierungsfehler. Och wäerte mir net fäeg sinn en Node ze starten, ausser mir liwweren en Objet vum passenden Typ mat all néideg Donnéeën.

Host Numm Resolutioun

Fir mat engem Fernhost ze verbannen, brauche mir eng richteg IP Adress. Et ass méiglech datt d'Adress méi spéit bekannt gëtt wéi de Rescht vun der Konfiguratioun. Also brauche mir eng Funktioun déi den Node ID op eng Adress mapéiert:

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

Et gi verschidde Weeër fir dës Funktioun ëmzesetzen:

  1. Wann d'Adressen eis bekannt ginn virum Ofbau, da kënne mir Scala Code generéieren mat
    Adressen a lafen dann de Bau. Dëst wäert Tester kompiléieren a lafen.
    An dësem Fall gëtt d'Funktioun statesch bekannt a kann am Code als Mapping vertruede ginn Map[NodeId, NodeAddress].
  2. A verschiddene Fäll ass déi aktuell Adress eréischt bekannt nodeems den Node ugefaang huet.
    An dësem Fall kënne mir en "Entdeckungsservice" implementéieren, dee virun anere Wirbelen leeft an all Wirbelen registréiere sech mat dësem Service an froen d'Adressen vun aneren Wirbelen.
  3. Wa mir kënnen änneren /etc/hosts, da kënnt Dir virdefinéiert Hostnumm benotzen (wéi my-project-main-node и echo-backend) an einfach dës Nimm verbannen
    mat IP Adressen wärend der Deployment.

An dësem Post wäerte mir dës Fäll net méi am Detail betruechten. Fir eis
an engem Spillsaachen Beispill, all Noden hunn déi selwecht IP Adress - 127.0.0.1.

Als nächst betruechte mir zwou Méiglechkeeten fir e verdeelt System:

  1. All Servicer op engem Node setzen.
  2. A hosten den Echo Service an den Echo Client op verschidden Noden.

Configuratioun fir engem Node:

Single Node Configuratioun

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

Den Objet implementéiert d'Konfiguratioun vum Client a vum Server. Eng Zäit-ze-Liewe Configuratioun gëtt och benotzt sou datt nom Intervall lifetime de Programm ofschléissen. (Ctrl-C funktionnéiert och a befreit all Ressourcen korrekt.)

Dee selwechte Set vu Konfiguratiouns- an Implementéierungseigenschaften ka benotzt ginn fir e System ze kreéieren deen aus zwee separat Wirbelen:

Zwee Node Konfiguratioun

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

Wichteg! Notéiert wéi d'Servicer verbonne sinn. Mir spezifizéieren e Service deen vun engem Node implementéiert gëtt als Ëmsetzung vun der Ofhängegkeetsmethod vun engem aneren Node. Den Ofhängegkeetstyp gëtt vum Compiler gepréift, well enthält de Protokolltyp. Wann Dir leeft, enthält d'Ofhängegkeet déi richteg Zilnode ID. Dank dësem Schema spezifizéiere mir d'Portnummer genau eemol a si garantéiert ëmmer op de richtege Port ze referenzéieren.

Ëmsetzung vun zwee System Wirbelen

Fir dës Konfiguratioun benotze mir déiselwecht Serviceimplementatiounen ouni Ännerungen. Deen eenzegen Ënnerscheed ass datt mir elo zwee Objekter hunn déi verschidde Sets vu Servicer implementéieren:

  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
  }

Den éischte Node implementéiert de Server a brauch nëmmen Serverkonfiguratioun. Den zweeten Node implementéiert de Client a benotzt en aneren Deel vun der Konfiguratioun. Och béid Node brauche Liewensdauermanagement. De Server Node leeft onbestëmmt bis et gestoppt gëtt SIGTERM'om, an de Client Node endet no enger Zäit. Cm. Launcher App.

Allgemeng Entwécklung Prozess

Loosst eis kucken wéi dës Konfiguratiouns Approche de Gesamtentwécklungsprozess beaflosst.

D'Konfiguratioun gëtt zesumme mam Rescht vum Code kompiléiert an en Artefakt (.jar) gëtt generéiert. Et schéngt Sënn ze maachen d'Konfiguratioun an engem separaten Artefakt ze setzen. Dëst ass well mir verschidde Konfiguratioune kënne baséieren op deemselwechte Code. Erëm ass et méiglech Artefakte ze generéieren, déi zu verschiddene Konfiguratiounszweige entspriechen. Ofhängegkeete vu spezifesche Versioune vu Bibliothéike ginn zesumme mat der Konfiguratioun gespäichert, an dës Versioune gi fir ëmmer gespäichert wann mir décidéieren dës Versioun vun der Konfiguratioun z'installéieren.

All Konfiguratiounsännerung verwandelt sech an eng Codeännerung. An dofir, jidderee
d'Ännerung gëtt vum normale Qualitéitssécherungsprozess ofgedeckt:

Ticket am Käfer Tracker -> PR -> Iwwerpréiwung -> fusionéieren mat relevante Filialen ->
Integratioun -> Deployment

D'Haaptkonsequenze vun der Ëmsetzung vun enger kompiléierter Konfiguratioun sinn:

  1. D'Konfiguratioun wäert konsequent iwwer all Node vum verdeelte System sinn. Wéinst der Tatsaach, datt all Node déi selwecht Konfiguratioun vun enger eenzeger Quell kréien.

  2. Et ass problematesch d'Konfiguratioun an nëmmen engem vun de Wirbelen z'änneren. Dofir ass "Konfiguratiounsdrift" onwahrscheinlech.

  3. Et gëtt méi schwéier kleng Ännerungen un der Konfiguratioun ze maachen.

  4. Déi meescht Konfiguratiounsännerunge wäerten als Deel vum Gesamtentwécklungsprozess optrieden a ginn iwwerpréift.

Braucht ech e separaten Repository fir d'Produktiounskonfiguratioun ze späicheren? Dës Konfiguratioun kann Passwierder an aner sensibel Informatioun enthalen, op déi mir den Zougang limitéiere wëllen. Baséierend op dësem, schéngt et Sënn ze maachen déi lescht Konfiguratioun an engem separaten Repository ze späicheren. Dir kënnt d'Konfiguratioun an zwee Deeler opdeelen - een mat ëffentlech zougänglechen Konfiguratiounsastellungen an een mat limitéierten Astellungen. Dëst erlaabt déi meescht Entwéckler Zougang zu gemeinsamen Astellungen ze hunn. Dës Trennung ass einfach z'erreechen mat Zwëscheegenschaften déi Standardwäerter enthalen.

Méiglech Variatiounen

Loosst eis probéieren déi kompiléiert Konfiguratioun mat e puer allgemeng Alternativen ze vergläichen:

  1. Textdatei op der Zilmaschinn.
  2. Zentraliséierte Schlësselwäertgeschäft (etcd/zookeeper).
  3. Prozesskomponenten déi nei konfiguréiert / nei gestart kënne ginn ouni de Prozess nei ze starten.
  4. Späichert Konfiguratioun ausserhalb vun Artefakt a Versiounskontroll.

Textdateien bidden bedeitend Flexibilitéit a punkto kleng Ännerungen. De Systemadministrator kann sech op de Fernknot aloggen, Ännerunge fir déi entspriechend Dateien maachen an de Service nei starten. Fir grouss Systemer kann esou Flexibilitéit awer net wënschenswäert sinn. D'Ännerunge verloosse keng Spueren an anere Systemer. Keen iwwerpréift d'Ännerungen. Et ass schwéier ze bestëmmen, wien genee d'Ännerunge gemaach huet a fir wéi ee Grond. Ännerungen ginn net getest. Wann de System verdeelt ass, da kann den Administrateur vergiessen déi entspriechend Ännerung op anere Wirbelen ze maachen.

(Et sollt och bemierkt ginn datt d'Benotzung vun enger kompiléierter Konfiguratioun d'Méiglechkeet fir Textdateien an Zukunft net ze benotzen. Config, an Dir kënnt Textdateien benotzen. Et follegt direkt datt d'Komplexitéit vun engem System mat enger kompiléierter Konfiguratioun e bësse manner ass wéi d'Komplexitéit vun engem System mat Textdateien, well Textdateien erfuerderen zousätzleche Code.)

En zentraliséierte Schlësselwäertgeschäft ass e gudde Mechanismus fir Metaparameter vun enger verdeelerer Applikatioun ze verdeelen. Mir mussen entscheeden wat Konfiguratiounsparameter sinn a wat just Daten sinn. Loosst eis eng Funktioun hunn C => A => B, an d'Parameteren C selten Ännerungen, an daten A - dacks. An dësem Fall kënne mir dat soen C - Configuratioun Parameteren, an A - daten. Et schéngt datt d'Konfiguratiounsparameter vun den Daten ënnerscheeden an datt se normalerweis manner dacks änneren wéi Daten. Och Daten kommen normalerweis aus enger Quell (vum Benotzer), an Konfiguratiounsparameter vun engem aneren (vum Systemadministrator).

Wann selten Ännerunge vun Parameteren aktualiséiert musse ginn ouni de Programm nei ze starten, da kann dat dacks zu der Komplikatioun vum Programm féieren, well mir mussen iergendwéi Parameteren liwweren, späicheren, parséieren a kontrolléieren, a veraarbecht falsch Wäerter. Dofir, aus der Siicht vun der Komplexitéit vum Programm ze reduzéieren, mécht et Sënn fir d'Zuel vun de Parameteren ze reduzéieren, déi während der Operatioun vum Programm änneren (oder sou Parameteren guer net ënnerstëtzen).

Fir den Zweck vun dësem Post wäerte mir tëscht statesch an dynamesche Parameteren ënnerscheeden. Wann d'Logik vum Service verlaangt datt Parameteren während der Operatioun vum Programm geännert ginn, da wäerte mir esou Parameteren dynamesch nennen. Soss sinn d'Optiounen statesch a kënne mat der kompiléierter Konfiguratioun konfiguréiert ginn. Fir dynamesch Rekonfiguratioun brauche mir vläicht e Mechanismus fir Deeler vum Programm mat neie Parameteren ze starten, ähnlech wéi d'Betriebssystemprozesser nei gestart ginn. (Eiser Meenung no ass et unzeroden d'Rekonfiguratioun vun Echtzäit ze vermeiden, well dëst d'Komplexitéit vum System vergréissert. Wa méiglech, ass et besser d'Standard OS-Kapazitéite fir d'Prozesser nei ze starten.)

Ee wichtegen Aspekt vun der Benotzung vun der statesch Konfiguratioun, déi d'Leit mécht déi dynamesch Rekonfiguratioun ze betruechten, ass d'Zäit déi et dauert fir de System no engem Konfiguratiounsupdate (Downtime) nei ze starten. Tatsächlech, wa mir Ännerungen un der statesch Konfiguratioun musse maachen, musse mir de System nei starten fir datt déi nei Wäerter a Kraaft trieden. D'Downtime Problem variéiert an der Gravitéit fir verschidde Systemer. An e puer Fäll kënnt Dir e Restart plangen zu enger Zäit wou d'Laascht minimal ass. Wann Dir braucht kontinuéierlech Service ze bidden, Dir kënnt ëmsetzen AWS ELB Verbindung draining. Zur selwechter Zäit, wa mir de System nei starten, lancéiere mir eng parallel Instanz vun dësem System, schalt de Balancer op et a waart bis déi al Verbindungen fäerdeg sinn. Nodeems all al Verbindungen ofgeschloss sinn, schalten mir déi al Instanz vum System aus.

Loosst eis elo d'Thema vun der Späichere vun der Konfiguratioun bannen oder ausserhalb vum Artefakt betruechten. Wa mir d'Konfiguratioun an engem Artefakt späicheren, dann hu mir op d'mannst d'Méiglechkeet d'Korrektheet vun der Konfiguratioun während der Assemblée vum Artefakt z'iwwerpréiwen. Wann d'Konfiguratioun ausserhalb vum kontrolléierten Artefakt ass, ass et schwéier ze verfolgen wien Ännerungen an dëser Datei gemaach huet a firwat. Wéi wichteg ass et? An eiser Meenung no ass et fir vill Produktiounssystemer wichteg eng stabil a qualitativ héichwäerteg Konfiguratioun ze hunn.

D'Versioun vun engem Artefakt erlaabt Iech ze bestëmmen wéini et erstallt gouf, wéi eng Wäerter et enthält, wéi eng Funktiounen aktivéiert / behënnert sinn, a wien fir all Ännerung vun der Konfiguratioun verantwortlech ass. Natierlech erfuerdert d'Späichere vun der Konfiguratioun an engem Artefakt e bëssen Effort, also musst Dir eng informéiert Entscheedung treffen.

Pros a Réck

Ech géif gären op d'Virdeeler an Nodeeler vun der proposéierter Technologie ophalen.

Virdeeler

Drënner ass eng Lëscht vun den Haaptfeatures vun enger kompiléierter verdeelt Systemkonfiguratioun:

  1. Statesch Configuratioun kontrolléieren. Erlaabt Iech sécher ze sinn, datt
    d'Konfiguratioun ass richteg.
  2. Räich Configuratioun Sprooch. Typesch sinn aner Konfiguratiounsmethoden maximal op Stringvariabelen Ersatz limitéiert. Wann Dir Scala benotzt, sinn eng breet Palette vu Sproochefeatures verfügbar fir Är Konfiguratioun ze verbesseren. Zum Beispill kënne mir benotzen
    Charaktere fir Standardwäerter, benotzt Objekter fir Parameteren ze gruppéieren, kënne mir op Vals bezéien, déi nëmmen eemol deklaréiert ginn (DRY) am ëmfaassend Ëmfang. Dir kënnt all Klassen direkt an der Konfiguratioun instantiéieren (Seq, Map, Benotzerdefinéiert Klassen).
  3. DSL. Scala huet eng Rei vu Sproochefeatures, déi et méi einfach maachen en DSL ze kreéieren. Et ass méiglech vun dëse Funktiounen ze profitéieren an eng Konfiguratiounssprooch ëmzesetzen déi méi bequem ass fir d'Zilgrupp vun de Benotzer, sou datt d'Konfiguratioun op d'mannst liesbar ass vun Domainexperten. Spezialisten kënnen zum Beispill un der Konfiguratiounsrevisiounsprozess deelhuelen.
  4. Integritéit a Synchronitéit tëscht Noden. Ee vun de Virdeeler fir d'Konfiguratioun vun engem ganze verdeelte System op engem eenzege Punkt ze späicheren ass datt all Wäerter genau eemol deklaréiert ginn an dann erëmbenotzt ginn, wou se gebraucht ginn. D'Benotzung vu Phantomtypen fir Ports ze deklaréieren garantéiert datt Noden kompatibel Protokoller an all korrekt Systemkonfiguratiounen benotzen. Explizit obligatoresch Ofhängegkeeten tëscht Noden ze hunn garantéiert datt all Servicer verbonne sinn.
  5. Héich Qualitéit Ännerungen. Ännerungen an der Konfiguratioun mat engem gemeinsamen Entwécklungsprozess ze maachen mécht et méiglech och héich Qualitéitsnormen fir d'Konfiguratioun z'erreechen.
  6. Gläichzäiteg Configuratioun update. Automatesch Systeminstallatioun no Konfiguratiounsännerungen suergen datt all Noden aktualiséiert ginn.
  7. Vereinfachung vun der Applikatioun. D'Applikatioun brauch net Parsing, Konfiguratiounskontroll oder Handhabung vu falsche Wäerter. Dëst reduzéiert d'Komplexitéit vun der Applikatioun. (E puer vun der Konfiguratiounskomplexitéit, déi an eisem Beispill beobachtet gëtt, ass net en Attribut vun der kompiléierter Konfiguratioun, awer nëmmen eng bewosst Entscheedung gedriwwen duerch de Wonsch fir méi Typsécherheet ze bidden.) Et ass ganz einfach zréck an déi üblech Konfiguratioun zréckzekommen - implementéiert just déi fehlend Deeler. Dofir kënnt Dir zum Beispill mat enger kompiléierter Konfiguratioun ufänken, d'Ëmsetzung vun onnéidege Deeler ofzesetzen bis d'Zäit wou et wierklech gebraucht gëtt.
  8. Verifizéiert Konfiguratioun. Zënter Konfiguratiounsännerungen no dem gewéinleche Schicksal vun all aner Ännerunge verfollegen, ass den Output dee mir kréien en Artefakt mat enger eenzegaarteger Versioun. Dëst erlaabt eis, zum Beispill, op eng fréier Versioun vun der Konfiguratioun zréckzekommen wann néideg. Mir kënne souguer d'Konfiguratioun vun engem Joer benotzen an de System funktionnéiert genau d'selwecht. Eng stabil Konfiguratioun verbessert der Prévisibilitéit an Zouverlässegkeet vun engem verdeelt System. Well d'Konfiguratioun an der Kompiléierungsstadium fixéiert ass, ass et zimmlech schwéier et an der Produktioun ze gefälscht.
  9. Modularitéit. De proposéierte Kader ass modulär an d'Module kënnen op verschidde Weeër kombinéiert ginn fir verschidde Systemer ze kreéieren. Besonnesch kënnt Dir de System konfiguréieren fir op engem eenzegen Node an enger Ausféierung ze lafen, an op verschidde Wirbelen an engem aneren. Dir kënnt verschidde Konfiguratioune fir Produktiounsinstanzen vum System erstellen.
  10. Testen. Andeems Dir individuell Servicer mat Spottobjekter ersetzt, kënnt Dir verschidde Versioune vum System kréien, déi bequem sinn fir ze testen.
  11. Integratioun Testen. Eng eenzeg Konfiguratioun fir de ganze verdeelte System ze hunn mécht et méiglech all Komponenten an engem kontrolléierten Ëmfeld als Deel vun der Integratiounstest ze lafen. Et ass einfach ze emuléieren, zum Beispill, eng Situatioun wou e puer Noden zougänglech ginn.

Nodeeler an Aschränkungen

Kompiléiert Konfiguratioun ënnerscheet sech vun anere Konfiguratiouns Approche a ka fir e puer Uwendungen net gëeegent sinn. Drënner sinn e puer Nodeeler:

  1. Statesch Configuratioun. Heiansdo musst Dir d'Konfiguratioun an der Produktioun séier korrigéieren, all Schutzmechanismen ëmgoen. Mat dëser Approche kann et méi schwéier ginn. Op d'mannst wäert d'Kompilatioun an den automateschen Deployment nach ëmmer erfuerderlech sinn. Dëst ass souwuel eng nëtzlech Feature vun der Approche an en Nodeel an e puer Fäll.
  2. Konfiguratioun Generatioun. Am Fall wou d'Konfiguratiounsdatei vun engem automateschen Tool generéiert gëtt, kënnen zousätzlech Efforten erfuerderlech sinn fir de Build-Skript z'integréieren.
  3. Tools. De Moment sinn Utilities an Techniken, déi fir d'Konfiguratioun entwéckelt ginn, baséiert op Textdateien. Net all esou Utilities / Technike wäerten an enger kompiléierter Konfiguratioun verfügbar sinn.
  4. Eng Ännerung vun der Haltung ass néideg. Entwéckler an DevOps sinn un Textdateien gewinnt. Déi ganz Iddi fir eng Konfiguratioun ze kompiléieren kann e bëssen onerwaart an ongewéinlech sinn an Oflehnung verursaachen.
  5. En héichqualitativen Entwécklungsprozess ass erfuerderlech. Fir déi kompiléiert Konfiguratioun bequem ze benotzen, ass eng voll Automatiséierung vum Prozess vum Bau an der Uwendung vun der Applikatioun (CI / CD) néideg. Soss wäert et zimlech onbequem sinn.

Loosst eis och op eng Zuel vu Aschränkungen vum betruechte Beispill wunnen, déi net mat der Iddi vun enger kompiléierter Konfiguratioun verbonne sinn:

  1. Wa mir onnéideg Konfiguratiounsinformatioun ubidden, déi net vum Node benotzt gëtt, da wäert de Compiler eis net hëllefen déi fehlend Ëmsetzung z'entdecken. Dëse Problem kann geléist ginn andeems Dir de Kuchmuster opzeginn a méi steif Aarte benotzt, zum Beispill, HList oder algebraesch Daten Zorte (Fall Klassen) Configuratioun representéieren.
  2. Et gi Linnen an der Konfiguratiounsdatei déi net mat der Konfiguratioun selwer verbonne sinn: (package, import, Objekterklärungen; override deffir Parameteren déi Standardwäerter hunn). Dëst kann deelweis vermeit ginn wann Dir Ären eegene DSL implementéiert. Zousätzlech, aner Zorte vu Configuratioun (zum Beispill, XML) och bestëmmte Restriktiounen op der Datei Struktur.
  3. Fir Zwecker vun dësem Post betruechte mir keng dynamesch Rekonfiguratioun vun engem Cluster vun ähnlechen Noden.

Konklusioun

An dësem Post hu mir d'Iddi exploréiert fir d'Konfiguratioun am Quellcode ze representéieren mat de fortgeschratt Fäegkeeten vum Scala Typ System. Dës Approche kann a verschiddenen Uwendungen benotzt ginn als Ersatz fir traditionell Konfiguratiounsmethoden baséiert op XML oder Textdateien. Och wann eist Beispill an Scala implementéiert ass, kënnen déiselwecht Iddien op aner kompiléiert Sprooche transferéiert ginn (wéi Kotlin, C#, Swift, ...). Dir kënnt dës Approche an engem vun de folgende Projete probéieren, a wann et net funktionnéiert, gitt op d'Textdatei weider, füügt déi fehlend Deeler derbäi.

Natierlech erfuerdert eng kompiléiert Konfiguratioun en héichqualitativen Entwécklungsprozess. Am Géigesaz, héich Qualitéit an Zouverlässegkeet vun Konfiguratiounen assuréiert.

Déi betruecht Approche kann ausgebaut ginn:

  1. Dir kënnt Makroen benotze fir d'Kompiléierungszäitkontrolle auszeféieren.
  2. Dir kënnt en DSL implementéieren fir d'Konfiguratioun op eng Manéier ze presentéieren déi fir Endbenotzer zougänglech ass.
  3. Dir kënnt dynamesch Ressourceverwaltung mat automatescher Konfiguratiounsanpassung ëmsetzen. Zum Beispill, d'Zuel vun de Wirbelen an engem Cluster z'änneren erfuerdert datt (1) all Node eng liicht aner Konfiguratioun kritt; (2) de Clustermanager krut Informatioun iwwer nei Wirbelen.

Unerkennungen

Ech soen dem Andrei Saksonov, dem Pavel Popov an dem Anton Nekhaev Merci fir hir konstruktiv Kritik un den Entworfsartikel.

Source: will.com

Setzt e Commentaire