Kompilita Distribuita Sistema Agordo

Mi ŝatus diri al vi unu interesan mekanismon por labori kun la agordo de distribuita sistemo. La agordo estas reprezentita rekte en kompilita lingvo (Scala) uzante sekurajn tipojn. Ĉi tiu afiŝo provizas ekzemplon de tia agordo kaj diskutas diversajn aspektojn de efektivigado de kompilita agordo en la ĝeneralan evoluprocezon.

Kompilita Distribuita Sistema Agordo

(Angla)

Enkonduko

Konstrui fidindan distribuitan sistemon signifas, ke ĉiuj nodoj uzas la ĝustan agordon, sinkronigitan kun aliaj nodoj. DevOps-teknologioj (terraform, ansible aŭ io simila) estas kutime uzataj por aŭtomate generi agordajn dosierojn (ofte specifajn por ĉiu nodo). Ni ankaŭ ŝatus certigi, ke ĉiuj komunikaj nodoj uzas identajn protokolojn (inkluzive de la sama versio). Alie, nekongrueco estos konstruita en nian distribuitan sistemon. En la JVM-mondo, unu sekvo de ĉi tiu postulo estas ke la sama versio de la biblioteko enhavanta la protokolmesaĝojn devas esti uzata ĉie.

Kio pri testado de distribuita sistemo? Kompreneble, ni supozas, ke ĉiuj komponantoj havas unutestojn antaŭ ol ni transiras al integriĝa testado. (Por ke ni eksterpoli testrezultojn al rultempo, ni ankaŭ devas disponigi identan aron da bibliotekoj ĉe la testa stadio kaj ĉe rultempo.)

Kiam oni laboras kun integrigaj testoj, estas ofte pli facile uzi la saman klaspadon ĉie sur ĉiuj nodoj. Ĉio, kion ni devas fari, estas certigi, ke la sama klaspado estas uzata ĉe rultempo. (Kvankam estas tute eble ruli malsamajn nodojn kun malsamaj klasvojoj, tio ja aldonas kompleksecon al la ĝenerala agordo kaj malfacilaĵoj kun deplojo kaj integriĝotestoj.) Por la celoj de ĉi tiu afiŝo, ni supozas, ke ĉiuj nodoj uzos la saman klaspadon.

La agordo evoluas kun la aplikaĵo. Ni uzas versiojn por identigi malsamajn stadiojn de evoluado de la programo. Ŝajnas logike ankaŭ identigi malsamajn versiojn de agordoj. Kaj metu la agordon mem en la versio-kontrolsistemo. Se estas nur unu agordo en produktado, tiam ni povas simple uzi la version-numeron. Se ni uzas multajn produktadajn okazojn, tiam ni bezonos plurajn
agordaj branĉoj kaj plia etikedo krom la versio (ekzemple la nomo de la branĉo). Tiel ni povas klare identigi la ĝustan agordon. Ĉiu agorda identigilo unike respondas al specifa kombinaĵo de distribuitaj nodoj, havenoj, eksteraj resursoj kaj bibliotekversioj. Por la celoj de ĉi tiu afiŝo ni supozos, ke ekzistas nur unu branĉo kaj ni povas identigi la agordon en la kutima maniero uzante tri nombrojn apartigitajn per punkto (1.2.3).

En modernaj medioj, agordaj dosieroj malofte estas kreitaj permane. Pli ofte ili estas generitaj dum deplojo kaj ne plu estas tuŝitaj (tiel ke ne rompu ion ajn). Estiĝas natura demando: kial ni ankoraŭ uzas tekstoformaton por konservi agordon? Realigebla alternativo ŝajnas esti la kapablo uzi regulan kodon por agordo kaj profiti el kompiltempaj kontroloj.

En ĉi tiu afiŝo ni esploros la ideon reprezenti agordon ene de kompilita artefakto.

Kompilita agordo

Ĉi tiu sekcio provizas ekzemplon de senmova kompilita agordo. Du simplaj servoj estas efektivigitaj - la eĥservo kaj la eĥservo kliento. Surbaze de ĉi tiuj du servoj, du sistemaj elektoj estas kunvenitaj. En unu opcio, ambaŭ servoj situas sur la sama nodo, en alia opcio - sur malsamaj nodoj.

Tipe distribuita sistemo enhavas plurajn nodojn. Vi povas identigi nodojn uzante valorojn de iu tipo NodeId:

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

case class NodeId(hostName: String)

aŭ eĉ

object Singleton
type NodeId = Singleton.type

Nodoj plenumas diversajn rolojn, ili funkciigas servojn kaj TCP/HTTP-konektoj povas esti establitaj inter ili.

Por priskribi TCP-konekton ni bezonas almenaŭ pordan numeron. Ni ankaŭ ŝatus reflekti la protokolon kiu estas subtenata sur tiu haveno por certigi ke kaj la kliento kaj servilo uzas la saman protokolon. Ni priskribos la konekton uzante la sekvan klason:

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

kie Port - nur entjero Int indikante la gamon da akcepteblaj valoroj:

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

Rafinitaj tipoj

Vidu bibliotekon rafinita и mia raporto. Resume, la biblioteko permesas vin aldoni limojn al tipoj, kiuj estas kontrolitaj dum kompilo. En ĉi tiu kazo, validaj pordaj nombrovaloroj estas 16-bitaj entjeroj. Por kompilita agordo, uzi la rafinitan bibliotekon ne estas deviga, sed ĝi plibonigas la kapablon de la kompililo kontroli la agordon.

Por HTTP (REST) ​​​​protokoloj, krom la havena numero, ni eble ankaŭ bezonas la vojon al la servo:

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

Fantomaj tipoj

Por identigi la protokolon je kompilo, ni uzas tipparametron, kiu ne estas uzata ene de la klaso. Ĉi tiu decido ŝuldiĝas al la fakto, ke ni ne uzas protokolan petskribon ĉe rultempo, sed ni ŝatus, ke la kompililo kontrolu protokolkongruon. Specifante la protokolon, ni ne povos pasigi netaŭgan servon kiel dependecon.

Unu el la komunaj protokoloj estas la REST API kun seriigo Json:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

kie RequestMessage — petotipo, ResponseMessage — responda tipo.
Kompreneble, ni povas uzi aliajn protokolajn priskribojn, kiuj provizas la precizecon de priskribo, kiun ni postulas.

Por la celoj de ĉi tiu afiŝo, ni uzos simpligitan version de la protokolo:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Ĉi tie la peto estas ĉeno aldonita al la url kaj la respondo estas la resendita ĉeno en la korpo de la HTTP-respondo.

La servo-agordo estas priskribita per la servonomo, havenoj kaj dependecoj. Tiuj elementoj povas esti reprezentitaj en Scala laŭ pluraj manieroj (ekzemple, HList-s, algebraj datumtipoj). Por la celoj de ĉi tiu afiŝo, ni uzos la Kukan Ŝablonon kaj reprezentos modulojn uzante trait'ov. (La Kuka Ŝablono ne estas postulata elemento de ĉi tiu aliro. Ĝi estas simple unu ebla efektivigo.)

Dependecoj inter servoj povas esti reprezentitaj kiel metodoj kiuj resendas havenojn EndPoint'oj de aliaj nodoj:

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

Por krei eĥan servon, vi bezonas nur havenon kaj indikon, ke la haveno subtenas la eĥan protokolon. Ni eble ne specifos specifan havenon, ĉar... trajtoj permesas vin deklari metodojn sen efektivigo (abstraktaj metodoj). En ĉi tiu kazo, dum kreado de konkreta agordo, la kompililo postulus nin provizi efektivigon de la abstrakta metodo kaj havigi havenon nombro. Ĉar ni efektivigis la metodon, kreante specifan agordon, ni eble ne specifas malsaman havenon. La defaŭlta valoro estos uzata.

En la klienta agordo ni deklaras dependecon de la eĥa servo:

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

La dependeco estas de la sama tipo kiel la eksportita servo echoService. Precipe, en la eĥa kliento ni postulas la saman protokolon. Tial, kiam vi konektas du servojn, ni povas esti certaj, ke ĉio funkcios ĝuste.

Efektivigo de servoj

Funkcio estas bezonata por komenci kaj haltigi la servon. (La kapablo ĉesigi la servon estas kritika por testado.) Denove, ekzistas pluraj ebloj por efektivigi tian funkcion (ekzemple, ni povus uzi tipklasojn bazitajn sur la agorda tipo). Por la celoj de ĉi tiu afiŝo ni uzos la Kukan Ŝablonon. Ni reprezentos la servon uzante klason cats.Resource, ĉar Ĉi tiu klaso jam provizas rimedojn por sekure garantii la liberigon de rimedoj en kazo de problemoj. Por akiri rimedon, ni devas provizi agordon kaj pretan rultempan kuntekston. La servo-komencfunkcio povas aspekti jene:

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

kie

  • Config — agorda tipo por ĉi tiu servo
  • AddressResolver — rultempa objekto, kiu ebligas al vi ekscii la adresojn de aliaj nodoj (vidu sube)

kaj aliaj tipoj el la biblioteko cats:

  • F[_] — tipo de efiko (en la plej simpla kazo F[A] povus esti nur funkcio () => A. En ĉi tiu afiŝo ni uzos cats.IO.)
  • Reader[A,B] - pli-malpli sinonimo de funkcio A => B
  • cats.Resource - rimedo kiu povas esti akirita kaj liberigita
  • Timer - tempigilo (ebligas al vi endormiĝi dum iom da tempo kaj mezuri tempintervalojn)
  • ContextShift - analoga ExecutionContext
  • Applicative — efikspeca klaso, kiu permesas vin kombini individuajn efikojn (preskaŭ monado). En pli kompleksaj aplikoj ŝajnas pli bone uzi Monad/ConcurrentEffect.

Uzante ĉi tiun funkcion subskribon ni povas efektivigi plurajn servojn. Ekzemple, servo kiu faras nenion:

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

(Cm. fonto, en kiu aliaj servoj estas efektivigitaj - eĥservo, eĥa kliento
и dumvivaj regiloj.)

Nodo estas objekto, kiu povas lanĉi plurajn servojn (la lanĉo de ĉeno de rimedoj estas certigita de la Kuka Ŝablono):

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

Bonvolu noti, ke ni specifas la ĝustan tipon de agordo, kiu estas postulata por ĉi tiu nodo. Se ni forgesas specifi unu el la agordaj tipoj postulataj de aparta servo, estos kompila eraro. Ankaŭ, ni ne povos komenci nodon krom se ni provizas iun objekton de la taŭga tipo kun ĉiuj necesaj datumoj.

Rezolucio pri Gastiganto

Por konektiĝi al fora gastiganto, ni bezonas realan IP-adreson. Eblas, ke la adreso fariĝos konata poste ol la resto de la agordo. Do ni bezonas funkcion, kiu mapas la nodan ID al adreso:

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

Estas pluraj manieroj efektivigi ĉi tiun funkcion:

  1. Se la adresoj iĝas konataj al ni antaŭ deplojo, tiam ni povas generi Scala-kodon per
    adresoj kaj poste rulu la konstruon. Ĉi tio kompilos kaj ruligos testojn.
    En ĉi tiu kazo, la funkcio estos konata statike kaj povas esti reprezentita en kodo kiel mapado Map[NodeId, NodeAddress].
  2. En kelkaj kazoj, la fakta adreso estas nur konata post kiam la nodo komenciĝis.
    En ĉi tiu kazo, ni povas efektivigi "malkovran servon" kiu funkcias antaŭ aliaj nodoj kaj ĉiuj nodoj registriĝos kun ĉi tiu servo kaj petos la adresojn de aliaj nodoj.
  3. Se ni povas modifi /etc/hosts, tiam vi povas uzi antaŭdifinitajn gastigajn nomojn (kiel my-project-main-node и echo-backend) kaj simple ligu ĉi tiujn nomojn
    kun IP-adresoj dum deplojo.

En ĉi tiu afiŝo ni ne konsideros ĉi tiujn kazojn pli detale. Por nia
en ekzemplo de ludilo, ĉiuj nodoj havos la saman IP-adreson - 127.0.0.1.

Poste, ni konsideras du eblojn por distribuita sistemo:

  1. Metante ĉiujn servojn sur unu nodo.
  2. Kaj gastigante la eĥan servon kaj eĥan klienton sur malsamaj nodoj.

Agordo por unu nodo:

Ununoda agordo

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

La objekto efektivigas la agordon de kaj la kliento kaj servilo. Tempo-porviva agordo ankaŭ estas uzata tiel ke post la intervalo lifetime ĉesigi la programon. (Ktrl-C ankaŭ funkcias kaj liberigas ĉiujn rimedojn ĝuste.)

La sama aro de agordaj kaj efektivigtrajtoj povas esti uzata por krei sistemon konsistantan el du apartaj nodoj:

Du noda agordo

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

Grave! Rimarku kiel la servoj estas ligitaj. Ni specifas servon efektivigitan de unu nodo kiel efektivigon de la dependeca metodo de alia nodo. La dependeca tipo estas kontrolita de la kompililo, ĉar enhavas la protokolan tipon. Kiam ĝi ruliĝas, la dependeco enhavos la ĝustan celnodan ID. Dank' al ĉi tiu skemo, ni precizigas la havenon-numeron precize unufoje kaj ĉiam garantias raporti al la ĝusta haveno.

Efektivigo de du sistemaj nodoj

Por ĉi tiu agordo, ni uzas la samajn servo-efektivigojn sen ŝanĝoj. La nura diferenco estas, ke ni nun havas du objektojn, kiuj efektivigas malsamajn arojn de servoj:

  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
  }

La unua nodo efektivigas la servilon kaj nur bezonas servilan agordon. La dua nodo efektivigas la klienton kaj uzas malsaman parton de la agordo. Ankaŭ ambaŭ nodoj bezonas dumvivan administradon. La servila nodo funkcias senfine ĝis ĝi estas haltigita SIGTERM'om, kaj la klienta nodo finiĝas post iom da tempo. Cm. lanĉila aplikaĵo.

Ĝenerala disvolva procezo

Ni vidu kiel ĉi tiu agorda aliro influas la ĝeneralan disvolvan procezon.

La agordo estos kompilita kune kun la resto de la kodo kaj artefakto (.jar) estos generita. Ŝajnas senco meti la agordon en apartan artefakton. Ĉi tio estas ĉar ni povas havi plurajn agordojn bazitajn sur la sama kodo. Denove, estas eble generi artefaktojn egalrilatantajn al malsamaj agordaj branĉoj. Dependecoj de specifaj versioj de bibliotekoj estas konservitaj kune kun la agordo, kaj ĉi tiuj versioj estas konservitaj por ĉiam kiam ni decidas deploji tiun version de la agordo.

Ajna agorda ŝanĝo fariĝas kodŝanĝo. Kaj sekve, ĉiu
la ŝanĝo estos kovrita per la normala kvalita certiga procezo:

Bileto en la cimspurilo -> PR -> revizio -> kunfandi kun koncernaj branĉoj ->
integriĝo -> deplojo

La ĉefaj konsekvencoj de efektivigado de kompilita agordo estas:

  1. La agordo estos konsekvenca tra ĉiuj nodoj de la distribuita sistemo. Pro la fakto, ke ĉiuj nodoj ricevas la saman agordon de ununura fonto.

  2. Estas probleme ŝanĝi la agordon en nur unu el la nodoj. Tial, "agorda drivo" estas neverŝajna.

  3. Fariĝas pli malfacile fari malgrandajn ŝanĝojn al la agordo.

  4. Plej multaj agordaj ŝanĝoj okazos kiel parto de la ĝenerala disvolva procezo kaj estos submetitaj al revizio.

Ĉu mi bezonas apartan deponejon por konservi la produktadkonfiguracion? Ĉi tiu agordo povas enhavi pasvortojn kaj aliajn sentemajn informojn al kiuj ni ŝatus limigi aliron. Surbaze de tio, ŝajnas havi sencon stoki la finan agordon en aparta deponejo. Vi povas dividi la agordon en du partojn—unu enhavante publike alireblajn agordojn kaj unu enhavanta limigitajn agordojn. Ĉi tio permesos al plej multaj programistoj havi aliron al oftaj agordoj. Ĉi tiu disiĝo estas facile atingi uzante mezajn trajtojn enhavantajn defaŭltajn valorojn.

Eblaj variadoj

Ni provu kompari la kompilitan agordon kun iuj komunaj alternativoj:

  1. Teksto dosiero sur la cela maŝino.
  2. Alcentrigita ŝlosilvalora vendejo (etcd/zookeeper).
  3. Procezkomponentoj kiuj povas esti reagorditaj/rekomencitaj sen rekomenci la procezon.
  4. Stokado de agordo ekster artefakto kaj versio-kontrolo.

Tekstaj dosieroj provizas gravan flekseblecon laŭ malgrandaj ŝanĝoj. La administranto de la sistemo povas ensaluti en la fora nodo, fari ŝanĝojn al la taŭgaj dosieroj kaj rekomenci la servon. Por grandaj sistemoj, tamen, tia fleksebleco eble ne estas dezirinda. La ŝanĝoj faritaj ne lasas spurojn en aliaj sistemoj. Neniu revizias la ŝanĝojn. Estas malfacile determini kiu precize faris la ŝanĝojn kaj pro kiu kialo. Ŝanĝoj ne estas testitaj. Se la sistemo estas distribuita, tiam la administranto eble forgesos fari la respondan ŝanĝon sur aliaj nodoj.

(Indas ankaŭ rimarki, ke uzi kompilitan agordon ne fermas la eblecon uzi tekstodosierojn estonte. Sufiĉos aldoni analizilon kaj validigilon, kiu produktas la saman tipon kiel eligo. Config, kaj vi povas uzi tekstajn dosierojn. Tuj sekvas, ke la komplekseco de sistemo kun kompilita agordo estas iom malpli ol la komplekseco de sistemo uzanta tekstajn dosierojn, ĉar tekstaj dosieroj postulas plian kodon.)

Alcentrigita ŝlosilvalora vendejo estas bona mekanismo por distribui meta-parametrojn de distribuita aplikaĵo. Ni devas decidi kio estas agordaj parametroj kaj kio estas nur datumoj. Ni havu funkcion C => A => B, kaj la parametroj C malofte ŝanĝoj, kaj datumoj A - ofte. En ĉi tiu kazo ni povas diri tion C - agordaj parametroj, kaj A - datumoj. Ŝajnas, ke agordaj parametroj diferencas de datumoj pro tio, ke ili ĝenerale ŝanĝiĝas malpli ofte ol datumoj. Ankaŭ datumoj kutime venas de unu fonto (de la uzanto), kaj agordaj parametroj de alia (de la sistemadministranto).

Se malofte ŝanĝiĝantaj parametroj devas esti ĝisdatigitaj sen rekomenco de la programo, tiam tio ofte povas konduki al la komplikaĵo de la programo, ĉar ni devos iel liveri parametrojn, stoki, analizi kaj kontroli, kaj prilabori malĝustajn valorojn. Tial, el la vidpunkto de reduktado de la komplekseco de la programo, havas sencon redukti la nombron da parametroj, kiuj povas ŝanĝiĝi dum programa funkciado (aŭ tute ne subtenas tiajn parametrojn).

Por la celoj de ĉi tiu afiŝo, ni diferencigos inter statikaj kaj dinamikaj parametroj. Se la logiko de la servo postulas ŝanĝi parametrojn dum la funkciado de la programo, tiam ni nomos tiajn parametrojn dinamikaj. Alie la opcioj estas senmovaj kaj povas esti agorditaj per la kompilita agordo. Por dinamika reagordo, ni eble bezonos mekanismon por rekomenci partojn de la programo kun novaj parametroj, simile al kiel operaciumaj procezoj estas rekomencitaj. (Laŭ nia opinio, estas konsilinde eviti realtempan reagordon, ĉar tio pliigas la kompleksecon de la sistemo. Se eble, estas pli bone uzi la normajn OS-kapablojn por rekomenci procezojn.)

Unu grava aspekto de uzado de senmova agordo, kiu igas homojn pripensi dinamikan reagordon, estas la tempo necesa por la sistemo rekomenci post agorda ĝisdatigo (malfunkcio). Fakte, se ni devas fari ŝanĝojn al la statika agordo, ni devos rekomenci la sistemon por ke la novaj valoroj efektiviĝu. La malfunkcioproblemo varias en severeco por malsamaj sistemoj. En iuj kazoj, vi povas plani rekomencon en tempo kiam la ŝarĝo estas minimuma. Se vi bezonas provizi kontinuan servon, vi povas efektivigi AWS ELB-konekto drenado. Samtempe, kiam ni bezonas rekomenci la sistemon, ni lanĉas paralelan ekzemplon de ĉi tiu sistemo, ŝanĝas la ekvilibron al ĝi kaj atendas ke la malnovaj konektoj finiĝos. Post kiam ĉiuj malnovaj konektoj finiĝis, ni fermis la malnovan petskribon de la sistemo.

Ni nun konsideru la aferon konservi la agordon ene aŭ ekster la artefakto. Se ni stokas la agordon ene de artefakto, tiam almenaŭ ni havis la ŝancon kontroli la ĝustecon de la agordo dum la muntado de la artefakto. Se la agordo estas ekster la kontrolita artefakto, estas malfacile spuri kiu faris ŝanĝojn al ĉi tiu dosiero kaj kial. Kiom gravas ĝi? Laŭ nia opinio, por multaj produktadsistemoj gravas havi stabilan kaj altkvalitan agordon.

La versio de artefakto permesas vin determini kiam ĝi estis kreita, kiajn valorojn ĝi enhavas, kiajn funkciojn estas ebligitaj/malŝaltitaj, kaj kiu respondecas pri ajna ŝanĝo en la agordo. Kompreneble, konservi la agordon ene de artefakto postulas iom da penado, do vi devas fari informitan decidon.

La avantaĝoj kaj kontraŭoj

Mi ŝatus deteni la avantaĝojn kaj malavantaĝojn de la proponita teknologio.

Profitoj

Malsupre estas listo de la ĉefaj trajtoj de kompilita distribuita sistema agordo:

  1. Kontrolo de senmova agordo. Permesas al vi esti certa pri tio
    la agordo estas ĝusta.
  2. Riĉa agorda lingvo. Tipe, aliaj agordaj metodoj estas limigitaj al ŝnuro-varia anstataŭigo maksimume. Kiam vi uzas Scala, ampleksa gamo de lingvaj funkcioj disponeblas por plibonigi vian agordon. Ekzemple ni povas uzi
    trajtoj por defaŭltaj valoroj, uzante objektojn por grupigi parametrojn, ni povas raporti al vals deklaritaj nur unufoje (SEKAJ) en la enferma amplekso. Vi povas krei iujn ajn klasojn rekte en la agordo (Seq, Map, kutimaj klasoj).
  3. DSL. Scala havas kelkajn lingvajn funkciojn, kiuj faciligas krei DSL. Eblas utiligi ĉi tiujn funkciojn kaj efektivigi agordan lingvon, kiu estas pli oportuna por la celgrupo de uzantoj, tiel ke la agordo estas almenaŭ legebla de domajnaj fakuloj. Specialistoj povas, ekzemple, partopreni en la agorda revizia procezo.
  4. Integreco kaj sinkroneco inter nodoj. Unu el la avantaĝoj de havi la agordon de tuta distribuita sistemo stokita ĉe unu punkto estas, ke ĉiuj valoroj estas deklaritaj ĝuste unufoje kaj poste reuzitaj kie ajn ili bezonas. Uzado de fantomaj tipoj por deklari havenojn certigas, ke nodoj uzas kongruajn protokolojn en ĉiuj ĝustaj sistemaj agordoj. Havi eksplicitajn devigajn dependecojn inter nodoj certigas ke ĉiuj servoj estas konektitaj.
  5. Altkvalitaj ŝanĝoj. Fari ŝanĝojn al la agordo uzante komunan evoluprocezon ebligas atingi altkvalitajn normojn ankaŭ por la agordo.
  6. Samtempa agorda ĝisdatigo. Aŭtomata sistema deplojo post agordaj ŝanĝoj certigas, ke ĉiuj nodoj estas ĝisdatigitaj.
  7. Simpligante la aplikon. La aplikaĵo ne bezonas analizon, konfiguracion kontroladon aŭ pritraktadon de malĝustaj valoroj. Ĉi tio reduktas la kompleksecon de la aplikaĵo. (Iu el la agorda komplekseco observita en nia ekzemplo ne estas atributo de la kompilita agordo, sed nur konscia decido pelita de la deziro provizi pli grandan tipan sekurecon.) Estas sufiĉe facile reveni al la kutima agordo - nur efektivigi la mankantan. partoj. Tial vi povas, ekzemple, komenci kun kompilita agordo, prokrastante la efektivigon de nenecesaj partoj ĝis la tempo, kiam ĝi vere bezonas.
  8. Verkita agordo. Ĉar agordaj ŝanĝoj sekvas la kutiman sorton de iuj aliaj ŝanĝoj, la eligo, kiun ni ricevas, estas artefakto kun unika versio. Ĉi tio ebligas al ni, ekzemple, reveni al antaŭa versio de la agordo se necese. Ni eĉ povas uzi la agordon de antaŭ unu jaro kaj la sistemo funkcios ĝuste same. Stabila agordo plibonigas la antaŭvideblecon kaj fidindecon de distribuita sistemo. Ĉar la agordo estas fiksita en la kompila stadio, estas sufiĉe malfacile falsi ĝin en produktado.
  9. Modulareco. La proponita kadro estas modula kaj la moduloj povas esti kombinitaj en malsamaj manieroj por krei malsamajn sistemojn. Aparte, vi povas agordi la sistemon por funkcii sur ununura nodo en unu enkorpiĝo, kaj sur pluraj nodoj en alia. Vi povas krei plurajn agordojn por produktadaj instancoj de la sistemo.
  10. Testado. Anstataŭigante individuajn servojn per imitaj objektoj, vi povas akiri plurajn versiojn de la sistemo, kiuj estas oportunaj por testado.
  11. Testo de integriĝo. Havi ununuran agordon por la tuta distribuita sistemo ebligas funkcii ĉiujn komponentojn en kontrolita medio kiel parto de integriĝtestado. Estas facile imiti, ekzemple, situacion kie iuj nodoj fariĝas alireblaj.

Malavantaĝoj kaj limigoj

Kompilita agordo diferencas de aliaj agordaj aliroj kaj eble ne taŭgas por iuj aplikoj. Malsupre estas kelkaj malavantaĝoj:

  1. Statika agordo. Kelkfoje vi devas rapide korekti la agordon en produktado, preterirante ĉiujn protektajn mekanismojn. Kun ĉi tiu aliro ĝi povas esti pli malfacila. Almenaŭ, kompilo kaj aŭtomata deplojo ankoraŭ estos postulataj. Ĉi tio estas kaj utila trajto de la aliro kaj malavantaĝo en iuj kazoj.
  2. Generacio de agordo. Se la agorda dosiero estas generita de aŭtomata ilo, aldonaj klopodoj povas esti necesaj por integri la konstruskripton.
  3. Iloj. Nuntempe, iloj kaj teknikoj destinitaj por labori kun agordo baziĝas sur tekstaj dosieroj. Ne ĉiuj tiaj iloj/teknikoj estos disponeblaj en kompilita agordo.
  4. Ŝanĝo en sintenoj estas postulata. Programistoj kaj DevOps estas kutimaj al tekstaj dosieroj. La ideo mem kompili agordon povas esti iom neatendita kaj nekutima kaj kaŭzi malakcepton.
  5. Altkvalita disvolva procezo estas postulata. Por komforte uzi la kompilitan agordon, estas necesa plena aŭtomatigo de la procezo de konstruado kaj deplojado de la aplikaĵo (CI/KD). Alie ĝi estos sufiĉe maloportuna.

Ni restu ankaŭ pri kelkaj limigoj de la pripensita ekzemplo, kiuj ne rilatas al la ideo de kompilita agordo:

  1. Se ni provizas nenecesajn agordajn informojn, kiuj ne estas uzataj de la nodo, tiam la kompililo ne helpos nin detekti la mankantan efektivigon. Ĉi tiu problemo povas esti solvita forlasante la Kuk-Ŝablon kaj uzante pli rigidajn tipojn, ekzemple, HList aŭ algebraj datumtipoj (kazklasoj) por reprezenti konfiguracion.
  2. Estas linioj en la agorda dosiero, kiuj ne rilatas al la agordo mem: (package, import,objektaj deklaroj; override def's por parametroj kiuj havas defaŭltajn valorojn). Ĉi tio povas esti parte evitita se vi efektivigas vian propran DSL. Krome, aliaj specoj de agordo (ekzemple, XML) ankaŭ trudas certajn restriktojn al la dosierstrukturo.
  3. Por la celoj de ĉi tiu afiŝo, ni ne pripensas dinamikan reagordon de aro de similaj nodoj.

konkludo

En ĉi tiu afiŝo, ni esploris la ideon reprezenti agordon en fontkodo uzante la altnivelajn kapablojn de la Scala-tipa sistemo. Ĉi tiu aliro povas esti uzata en diversaj aplikoj kiel anstataŭaĵo por tradiciaj agordaj metodoj bazitaj sur xml aŭ tekstaj dosieroj. Kvankam nia ekzemplo estas efektivigita en Scala, la samaj ideoj povas esti transdonitaj al aliaj kompilitaj lingvoj (kiel ekzemple Kotlin, C#, Swift, ...). Vi povas provi ĉi tiun aliron en unu el la sekvaj projektoj, kaj, se ĝi ne funkcias, transiru al la tekstdosiero, aldonante la mankantajn partojn.

Kompreneble, kompilita agordo postulas altkvalitan disvolvan procezon. Kontraŭe, alta kvalito kaj fidindeco de agordoj estas certigitaj.

La pripensita aliro povas esti vastigita:

  1. Vi povas uzi makroojn por fari kompiltempajn kontrolojn.
  2. Vi povas efektivigi DSL por prezenti la agordon en maniero alirebla por finaj uzantoj.
  3. Vi povas efektivigi dinamikan administradon de rimedoj kun aŭtomata agorda ĝustigo. Ekzemple, ŝanĝi la nombron da nodoj en areto postulas ke (1) ĉiu nodo ricevu iomete malsaman konfiguracion; (2) la clustermanaĝero ricevis informojn pri novaj nodoj.

Dankoj

Mi ŝatus danki al Andrej Saksonov, Pavel Popov kaj Anton Neĥaev pro ilia konstrua kritiko al la projekto de artikolo.

fonto: www.habr.com

Aldoni komenton