Jaotatud süsteemi kompileeritav konfiguratsioon

Selles postituses soovime jagada huvitavat viisi hajutatud süsteemi konfigureerimiseks.
Konfigureerimine on esitatud otse Scala keeles tüübisohkalikul viisil. Näidisrakendust kirjeldatakse üksikasjalikult. Arutatakse erinevaid ettepaneku aspekte, sealhulgas mõju üldisele arendusprotsessile.

Jaotatud süsteemi kompileeritav konfiguratsioon

(vene keeles)

Tutvustus

Tugevate hajutatud süsteemide ehitamine nõuab õige ja koherentse konfiguratsiooni kasutamist kõigis sõlmedes. Tüüpiline lahendus on kasutada tekstilist juurutusülevaadet (terraform, ansible või midagi sarnast) ja automaatselt genereeritud konfiguratsioonifaile (tihti - pühendatud iga sõlme/rolli jaoks). Soovime kasutada ka samade protokollide samu versioone igal suhtleva sõlme puhul (vastasel juhul kogeme ühilduvusprobleeme). JVM-i maailmas tähendab see, et vähemalt sõnumiteeter peaks olema kõigil suhtlevatel sõlmedel sama versiooni.

Kuidas testida süsteemi? Loomulikult peaksime enne integratsioonitestide tegemist olema iga komponendi jaoks olemas üksustestid. Et suudaksime testitulemusi õigeaegselt extrapoleerida, peame tagama, et kõigi teekide versioonid püsivad identsete nii töö- kui ka testimisvõrkudes.

Integratsioonitestide käitamisel on sageli palju lihtsam, kui kõikidel sõlmedel on sama classpath. Peame lihtsalt veenduma, et juurutamisel kasutatakse sama classpathi. (On võimalik kasutada eri classpathe erinevates sõlmedes, kuid selle konfiguratsiooni esitlemine ja õige juurutamine on keerulisem.) Seega, et asju lihtsamalt hoida, arvestame vaid identsete classpathide olemasolu kõigil sõlmedel.

Konfigureerimine kalduvusega arenema koos tarkvaraga. Tüüpiliselt kasutame versioone erinevate identifitseerimiseks.
tarkvara arengu etapid. Tundub mõistlik varjatud seadistuse katte all versioonihalduse alla ja tuvastada erinevad seadistused mõne sildi abil. Kui tootmises on ainult üks seadistus, võime kasutada ainukest versiooni identifikaatorina. Mõnikord võime omada mitmeid tootmisringlusi. Ja iga ringluse jaoks võime vajada eraldi seadistuse haru. Seetõttu võivad seadistusi märgistada haru ja versiooniga, et unikaalselt tuvastada erinevaid seadistusi. Iga haru silt ja versioon vastavad ühele kombineeritud jaotatud sõlmede, portide, väliste ressursside, klassitee teeki versioonide kombinatsioonile igas sõlmes. Siin katame ainult ühte haru ja tuvastame seadistused kolmekomponendilise kümnendversiooniga (1.2.3), samamoodi nagu teiste artefaktide puhul.

Kaasaegsetes keskkondades pole seadistusfailid enam käsitsi muudetud. Tüüpiliselt genereerime
seadistusfailid juurutamise ajal ja pole neid pärast seda kunagi puudutatud. Nii võib küsida, miks me ikka kasutame seadistusfailide tekstivormi? Elujõuline valik on paigutada seadistus kompilatsiooniüksusesse ja kasu saada kompileerimise ajal seadistamise valideerimisest.

Selles postituses uurime ideed hoida seadistus kompileeritud artefaktis.

Kompileeritav seadistus

Selles jaotises arutame staatilise seadistuse näidet. Kaks lihtsat teenust — echo teenus ja echo teenuse klient, on seadistatud ja rakendatud. Seejärel instanteeritakse kaks erinevat jaotatud süsteemi, mis mõlemad teenuseid sisaldavad. Üks on ühe sõlme seadistuse jaoks ja teine kahe sõlme seadistuse jaoks.

Tüüpiline jaotatud süsteem koosneb mitmest sõlmest. Sõlmi võiks tuvastada mingi tüübi abil:

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

või lihtsalt

case class NodeId(hostName: String)

või isegi

object Singleton
type NodeId = Singleton.type

Need sõlmed täidavad erinevaid rolle, käitajad teenuseid ja peaksid olema võimelised ühendust võtma teiste sõlmedega TCP/HTTP ühenduste abil.

TCP-ühenduse jaoks on vajalik vähemalt üks pordinumber. Samuti tahame veenduda, et klient ja server suhtlevad sama protokolli kaudu. Ühenduse modelleerimiseks sõlmitame järgmise klassi:

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

where Sadama on lihtsalt Int lubatud vahemikus:

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

Täpsemad tüübid

Vaata refined raamatukogu. Lühidalt, see võimaldab lisada kompileerimise ajal piiranguid teistele tüüpidele. Käesoleval juhul Int on lubatud ainult 16-bitised väärtused, mis võivad esindada pordinumbrit. Seda raamatukogu ei ole nõutav selle konfiguratsiooni lähenemise jaoks. See lihtsalt sobib väga hästi.

HTTP (REST) puhul võib meil samuti olla teenuse rada:

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

Phantom-tüüp

Protokolli tuvastamiseks kompileerimise ajal kasutame Scala omadust, mis kuulutab tüüpide argumenti, Protokoll mida klassis ei kasutata. See on nn phantom-tüüp. Käitamise ajal vajame harva protokolli tuvastaja eksemplari, seetõttu me seda ei salvestata. Kompileerimise ajal annab see phantom-tüüp täiendava tüübi turvalisuse. Me ei saa edastada porti vale protokolliga.

Üks kõige laialdasemalt kasutatavaid protokolle on REST API koos Json-serialiseerimisega:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

where RequestMessage on sõnumite põhiliik, mida klient saab serverile saata ja ResponseMessage on serverist saadud vastus. Muidugi võime luua ka muid protokolli kirjeldusi, mis täpsustavad suhtlusprotokolli soovitud täpsusega.

Selle postituse eesmärkideks kasutame me lihtsamat protokolli versiooni:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Selles protokollis lisatakse päringu sõnum url-ile ja vastus saadetakse tavatekstina.

Teenuse konfiguratsioon võib olla kirjeldatud teenuse nime, portide kogumi ja mõne sõltuvuse kaudu. On mitmeid võimalikke viise, kuidas neid elemente Scalasse esindada (näiteks, HList, algebrailised andme tüübid). Selle postituse eesmärkideks kasutame me Cake Pattern'i ja esindame kombineeritavaid osi (mooduleid) kui traatide kaudu. (Cake Pattern ei ole selle kompileeritava konfiguratsiooni lähenemise nõue. See on lihtsalt üks võimalik rakendusideedest.)

Sõltuvusi saab esindada Cake Patterni kaudu teiste sõlmede lõpp-punktidena:

  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 teenusel on vajalik vaid konfigureeritud port. Ja me deklareerime, et see port toetab echo protokolli. Pange tähele, et me ei pea selles etapis täpsustama konkreetset porti, kuna tüüpide omadus lubab abstraktsete meetodite deklareerimist. Kui kasutame abstraktseid meetodeid, nõuab kompilaator nende rakendamist konfigureerimisinstrumendis. Siin oleme esitanud rakenduse (8081) ja seda kasutatakse vaikeväärtusena, kui me jätame selle konkreetsetes konfiguratsioonides vahele.

Saame deklareerida sõltuvuse echo teenuse kliendi konfiguratsioonis:

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

Sõltuvus on sama tüüpi kui echoService. Eriti nõuab see sama protokolli. Seega võime olla kindlad, et kui ühendame need kaks sõltuvust, töötavad need õigesti.

Teenuste rakendamine

Teenusel on vajalik funktsioon, et alustada ja lõpetada see korralikult. (Teenuse katkestamise võime on testimiseks kriitilise tähtsusega.) Jällegi on mitmeid võimalusi, kuidas sellise funktsiooni määrata antud konfiguratsiooni jaoks (näiteks võiksime kasutada tüübi klasse). Käesolevas postituses kasutame jälle Cake Patternit. Saame esindada teenust cats.Resource , mis juba pakub sulgemise ja ressursside vabastamise võimalust. Ressursi omandamiseks peame esitama konfigureerimise ja mõne tööaegse konteksti. Seega võib teenuse käivitamise funktsioon välja näha järgmiselt:

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

where

  • Konfiguratsioon — konfiguratsiooni tüüp, mis on vajalik selle teenuse käivitajale
  • AddressResolver — tööaegne objekt, mis suudab hankida teiste sõlmede reaalseid aadresse (loe edasi üksikasjade saamiseks).

teised tüübid tulevad cats:

  • F[_] — efekti tüüp (lihtsaimal juhul F[A] võib olla lihtsalt () => A. Käesolevas postituses kasutame cats.IO.)
  • Reader[A,B] — on enam-vähem sünonüüm funktsioonile A => B
  • cats.Resource — omab viise, kuidas omandada ja vabastada
  • Timer — võimaldab magada/ajastada aega
  • ContextShift — vaste ExecutionContext
  • Applicative — funktsioonide mähis effektis (peaaegu monaad) (me võime lõpuks asendada selle millegi muuga)

Selle liidese abil saame rakendada mitmeid teenuseid. Näiteks teenus, mis ei tee midagi:

  trait ZeroServiceImpl[F[_]] extends ServiceImpl[F] {
    type Config  Resource.pure[F, Unit](()))
  }

(Vaata Allika kood teiste teenuste rakendamiseks — echo teenus,
echo klient ja eluaegsed juhtimisseadmed.)

Sõlm on üksik objekt, mis käitab mitmeid teenuseid (ressursside ahelat võimaldab alustada Cake Pattern):

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

Pange tähele, et sõlmes määrame täpselt selle konfiguratsiooni tüübi, mida see sõlm vajab. Kompilaator ei lase meil objekti (Cake) luua ebapiisava tüübiga, kuna iga teenuse omadus deklareerib piirangu põhjal Konfiguratsioon tüübi. Samuti ei saa me sõlme käivitada, ilma et oleksime esitanud täieliku konfiguratsiooni.

Sõlme aadressi lahendamine

Ühenduse loomiseks vajame iga sõlme jaoks tõelist hosti aadressi. See võib olla teada hiljem kui teised konfiguratsiooni osad. Seega vajame viisi, kuidas anda kaardistamine sõlme id ja selle tegeliku aadressi vahel. See kaardistamine on funktsioon:

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

On mõned võimalikud viisid selle funktsiooni rakendamiseks.

  1. Kui me teame tegelikke aadresse enne juurutamist, sõlmede hostide loomise ajal, siis saame genereerida Scala koodi tegelike aadressidega ja seejärel käivitada ehituse (mis teostab kompileerimisaja kontrollid ja seejärel käivitab integratsioonikatsete komplekti). Sel juhul on meie kaardistamisfunktsioon teada staatiliselt ja saab lihtsustada näiteks midagi sellist nagu Map[NodeId, NodeAddress].
  2. Mõnikord saame tegelikke aadresse alles hiljem, kui sõlm on tegelikult käivitunud, või meil ei ole aadresse sõlmedelt, mis pole veel käivitatud. Sel juhul võiks meil olla avastamisteenus, mis käivitatakse enne kõiki teisi sõlmi, ja iga sõlm võiks reklaamida oma aadressi selles teenuses ning tellida sõltuvusi.
  3. Kui me saame muuta /etc/hosts, saame kasutada eeldefineeritud hosti nimesid (nagu my-project-main-node ja echo-backend) ja lihtsalt seondada see nimi IP aadressiga juurutamise ajal.

Selles postituses me ei käsitle neid juhtumeid süvitsi. Tegelikult meie katse näites on kõikide sõlmedega sama IP aadress — 127.0.0.1.

Selles postituses käsitleme kahte jaotatud süsteemi paigutust:

  1. Üksik sõlme paigutus, kus kõik teenused asuvad ühel sõlmel.
  2. Kaks sõlme paigutus, kus teenus ja klient asuvad erinevates sõlmedes.

Konfiguratsioon ühe sõlme paigutus on järgmine:

Ühe sõlme konfiguratsioon

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

Siin loome ühe konfiguratsiooni, mis laiendab nii serveri kui ka kliendi konfiguratsiooni. Samuti konfigureerime elutsükli kontrolleri, mis tavaliselt lõpetab kliendi ja serveri pärast eluea aja möödumist.

Sama teenuste rakenduste ja konfiguratsioonide komplekti abil saab luua süsteemi paigutuse kahe eraldi sõlmega. Peame lihtsalt looma kaks eraldi sõlme konfiguratsiooni sobivate teenustega:

Kaks sõlme konfiguratsiooni

  objekt NodeServerConfig ulatub EchoConfig[String] koos SigTermLifecycleConfig
  {
    tüüp NodeId = NodeIdImpl

    def nodeId = NodeServer

    override def portNumber: PortNumber = 8080
  }

  objekt NodeClientConfig ulatub EchoClientConfig[String] koos FiniteDurationLifecycleConfig
  {
    // NB! sõltuvuse määratlemine
    def echoServiceDependency = NodeServerConfig.echoService

    def pollInterval: FiniteDuration = 1.second

    def lifetime: FiniteDuration = 10500.milliseconds // veel 0.5 sekundit, et saaks 10 päringut, mitte 9.

    def testMessage: String = "dolly"
  }

Vaata, kuidas me määratleme sõltuvuse. Me mainime teise sõlme pakutavat teenust praeguse sõlme sõltuvusena. Sõltuvuse tüüp kontrollitakse, kuna see sisaldab phantomi tüüpi, mis kirjeldab protokolli. Ja tööajal saame õige sõlme ID. See on üks olulisemaid aspekte ettepanekud konfiguratsioonimeetodist. See annab meile võimaluse määrata port ainult üks kord ja veenduda, et viitame õigele portile.

Kaks sõlme rakendust

Selle konfiguratsiooni jaoks kasutame täpselt samu teenuste rakendusi. Ükski muudatus ei ole vajalik. Kuid loome kaks erinevat sõlme rakendust, mis sisaldavad erinevat teenuste komplekti:

  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
  }

Esimene sõlm rakendab serveri ja vajab ainult serveri kõrval konfiguratsiooni. Teine sõlm rakendab kliendi ja vajab teist osa konfiguratsioonist. Mõlemad sõlmed vajavad mõningaid eluea spetsifikatsioone. Käesoleva postituse jaoks on teenussõlm lõpmatu elueaga, mille saab lõpetada kasutades SIGTERM, samas kui echo klient lõpeb pärast konfigureeritud piiratud kestust. Vaata stardirakendust lisainformatsiooniks.

Kogu arenduse protsess

Vaata, kuidas see lähenemine muudab meie töö konfiguratsiooniga.

Konfiguratsioonikood kompileeritakse ja genereerib artefakti. On mõistlik eraldada konfiguratsiooni artefakt teistest koodiartefaktidest. Sageli võib meil olla palju konfiguratsioone sama koodibaasi peal. Ja muidugi võime omada mitmeid erinevaid versioone erinevatest konfiguratsiooniharudest. Konfiguratsioonis saame valida kindlad teegiversioonid, mis jäävad püsima, iga kord, kui me seda konfiguratsiooni kasutusele võtame.

Konfiguratsioonimuudatus muutub koodimuutuseks. Seetõttu peaks see olema kaetud sama kvaliteedikontrolli protsessiga:

Pilet -> PR -> ülevaatus -> liitmine -> pidev integratsioon -> pidev juurutamine

Selle lähenemisviisi järgmised tagajärjed on:

  1. Konfiguratsioon on konkreetse süsteemi näite jaoks koherentne. Tundub, et ei ole võimalik, et sõlmede vahel oleks vale seos.
  2. Ühe sõlme konfiguratsiooni muutmine ei ole lihtne. Tundub, et ei ole mõistlik sisse logida ja muuta mõnd tekstifaili. Seega muutub konfiguratsiooni kõrvalekalle vähem tõenäoliseks.
  3. Väikeste konfiguratsiooni muudatuste tegemine ei ole lihtne.
  4. Enamik konfiguratsioonimuudatusi järgib sama arendusprotsessi ning need läbivad ülevaatuse.

Kas vajame tootmiskonfiguratsiooniks eraldi hoidlat? Tootmiskonfiguratsioon võib sisaldada tundlikku teavet, mida soovime hoida paljude inimeste eest eemal. Seega tasub võib-olla hoida eraldi hoidlat piiratud juurdepääsuga, kus hoitakse tootmiskonfiguratsiooni. Me võime jagada konfiguratsiooni kaheks osaks — ühe, kus on enamiku tootmise avatud parameetritega ja teise, kus on konfiguratsiooni salajane osa. See võimaldaks enamikul arendajatest juurdepääsu enamikule parameetritest, samas kui tõeliselt tundlike asjade juurdepääs oleks piiratud. Seda on lihtne saavutada, kasutades vahepealseid atribuute vaikeparameetrite väärtustega.

Variatsioonid

Vaatame väljaettepaneku eeliseid ja puudusi võrreldes teiste konfiguratsioonihalduse tehnoloogiate abil.

Esiteks loetleme mõned alternatiivid mõnele ettepanekule seoses konfiguratsiooniga:

  1. Tekstifail sihtmasinas.
  2. Keskne võtme-väärtuse salvestus (nt etcd/zookeeper).
  3. Alamprotsessid, mida saaks ilma protsessi taaskäivitamiseta konfigureerida/taaskäivitada.
  4. Konfiguratsioon, mis on väljaspool artefakti ja versioonihaldust.

Tekstifail annab teatavat paindlikkust hädavajalike paranduste osas. Süsteemi administraator saab sisse logida sihtsõlmele, teha muudatusi ja lihtsalt teenuse taaskäivitada. See ei pruugi suuremate süsteemide puhul nii hea olla. Muudatusest ei jää mingeid jälgi. Muudatus ei ole teise isiku poolt läbi vaadatud. Võib olla raske välja selgitada, mis on muudatuse põhjustanud. Seda pole testitud. Jaotatud süsteemi perspektiivist võib administraator unustada määrata konfiguratsiooni mingis teises sõlmes.

(Muide, kui tulevikus tekib vajadus hakata kasutama tekstipõhiseid konfigureerimisfaile, peame lihtsalt lisama parseri + valideerija, mis suudab genereerida sama Konfiguratsioon tüüpi, ja see oleks piisav, et hakata kasutama tekstifaile. See näitab ka, et kompileerimise ajal konfiguratsiooni keerukus on natuke väiksem kui tekstipõhiste konfiguratsioonide keerukus, sest tekstiversiooni puhul vajame lisakoodi.)

Keskne võtme-väärtuse salvestus on hea mehhanism rakenduse meta parameetrite jagamiseks. Siin peame mõtlema, mida peame konfiguratsiooniväärtusteks ja mis on lihtsalt andmed. Antud funktsiooni C => A => B peame tavaliselt harva muudetavaid väärtusi C «konfiguratsiooniks», samas kui sagedamini muudetavad andmed A — lihtsalt sisendandmed. Konfiguratsioon peaks olema funktsioonile edastatud enne andmete edastamist. A. Selle idee põhjal võime öelda, et ootuspäraste muudatuste sagedus on see, mida saaks kasutada konfiguratsioonandmete eristamiseks lihtsalt andmetest. Samuti pärinevad andmed tavaliselt ühest allikast (kasutaja) ja konfiguratsioon tuleb teisest allikast (administraator). Parameetritega, mida saab muuta muutmisprotsessi algatamise järel, on seotud rakenduse keerukuse suurenemine. Selleks, et vähendada programmide keerukust, oleks parem vähendada jooksuaegade parameetrite arvu, mis võivad muutuda (või isegi täielikult kaotada).

Käesoleva postituse perspektiivist peaksime tegema vahet staatiliste ja dünaamiliste parameetrite vahel. Kui teenuse loogika nõuab haruldasi muudatusi mõnedes parameetrites jooksuajal, siis võime neid nimetada dünaamilisteks parameetriteks. Muul juhul on nad staatilised ja neid saab konfigureerida ettepanekudega. Dünaamiliseks ümberkonfigureerimiseks võivad olla vajalikud muud lähenemisviisid. Näiteks võib osa süsteemist taaskäivitada uute konfiguratsiooniparameetritega sarnaselt jagatud süsteemi eraldiseisvate protsesside taaskäivitamisele.
(Minu tagasihoidlik arvamus on, et tuleb vältida jooksuaegset ümberkonfigureerimist, kuna see suurendab süsteemi keerukust.
Oleneb, kas rebootimise toe usaldamine OP toele oleks lihtsam. Kuigi see ei pruugi alati olla võimalik.)

Üks oluline aspekt staatilise konfiguratsiooni kasutamisel, mis mõnikord ajendab inimesi kaaluma dünaamilist konfiguratsiooni (ilma muude põhjusteta), on teenuse seiskamine konfiguratsiooni värskendamise ajal. Tõepoolest, kui peame muutma staatilist konfiguratsiooni, peame süsteemi taaskäivitama, et uued väärtused jõustuksid. Puudumise nõuded varieeruvad erinevates süsteemides, seega ei pruugi see olla kriitiline. Kui see on kriitiline, peame kavandama ette igasugused süsteemi taaskäivitamised. Näiteks võiksime rakendada AWS ELB ühenduse äravool.. Sel juhul, kui peame süsteemi taaskäivitama, käivitame paralleelselt uue süsteemi eksemplari ja suuname ELB-i sellele, samas lastes vanal süsteemil lõpetada olemasolevate ühenduste teenindamine.

Kuidas on lood konfiguratsiooni säilitamisega versioonitud artefaktides või väljaspool neid? Konfiguratsiooni säilitamine artefaktis tähendab enamasti, et see konfigureerimine on läbinud sama kvaliteedi tagamise protsessi nagu teised artefaktid. Seetõttu võib olla kindel, et konfiguratsioon on hea kvaliteediga ja usaldusväärne. Vastupidiselt, konfiguratsiooni eraldi failis hoidmine tähendab, et pole jälgi sellest, kes ja miks selle faili muudatusi tegi. Kas see on oluline? Usume, et enamikus tootmis süsteemides on parem omada stabiilset ja kõrge kvaliteediga konfiguratsiooni.

Artefakti versioon võimaldab välja selgitada, millal see loodi, milliseid väärtusi see sisaldab, millised funktsioonid on lubatud/keelatud, kes vastutas konfiguratsiooni iga muudatuse tegemise eest. Konfiguratsiooni säilitamine artefaktis võib nõuda teatud pingutust ja see on disainivalik.

Plussid ja miinused

Siin soovime välja tuua mõned eelised ja arutada mõned puudused pakutud lähenemisviisi osas.

Eelised

Kompileeritava konfiguratsiooni omadused täielikus jaotatud süsteemis:

  1. Konfiguratsiooni staatiline kontroll. See annab kõrge usaldusväärsuse taseme, et konfiguratsioon on õige, arvestades tüüpi piiranguid.
  2. Rikas konfiguratsiooni keel. Tüüpiliselt on teised konfiguratsioonilähenemised piiratud maksimaalselt muutujate asendamisega.
    Scala abil saab kasutada laia valikut keele omadusi konfiguratsiooni parendamiseks. Näiteks saame kasutada jooni vaikimisi väärtuste seadmiseks, objekte erinevate ulatuste seadmiseks, saame viidata vals, mis on määratud ainult ühes välimises ulatuses (DRY). On võimalik kasutada kirjeldavaid järjestusi või teatud klasside instantsse (Seq, Map, jne).
  3. DSL. Scala pakub DSL kirjanikele head tuge. Nende funktsioonide abil saab luua konfigureerimiskeele, mis on mugavam ja kasutajasõbralikum, nii et lõplik konfigureerimine on vähemalt domeeni kasutajate poolt loetav.
  4. Integriteet ja kooskõla sõlmedes. Üks kasu, mille toob kaasa kogu jaotatud süsteemi konfiguratsiooni keskne koht, on see, et kõiki väärtusi määratakse täpselt üks kord ja seejärel kasutatakse neid igas kohas, kus neid vajame. Samuti tagavad tüübisüsteemi ohutud portide deklaratsioonid, et kõikide võimalike korrektsete konfiguratsioonide puhul räägivad süsteemi sõlmed sama keelt. Sõlmede vahel on selged sõltuvused, mis muudab teenuste pakkumise unustamise raske.
  5. Muudatuste kvaliteet. Üldine lähenemine konfigureerimismuudatuste edastamisel normaalse PR-protsessi kaudu kehtestab kõrged kvaliteedistandardid ka konfiguratsioonis.
  6. Korras samaaegsed konfigureerimismuudatused. Iga kord, kui teeme konfigureerimisse mingeid muudatusi, tagab automaatne juurutamine, et kõik sõlmed uuendatakse.
  7. Rakenduse lihtsustamine. Rakendus ei pea parsi ja valideerima konfiguratsiooni ning käsitlema vale konfiguratsiooniväärtusi. See lihtsustab kogu rakendust. (Mõningane keerukuse suurenemine on konfiguratsioonis endas, kuid see on teadlik tasakaalu leidmine ohutuse suunas.) Tavalisse konfiguratsiooni tagasitulek on üsna lihtne - lihtsalt lisage puuduolevad osad. Koostatud konfiguratsiooni alustamine on lihtsam ja täiendavate komponentide rakendamist saab edasi lükata hilisemaks ajaks.
  8. Versioonitud konfiguratsioon. Kuna konfiguratsiooni muudatused järgivad sama arendusprotsessi, saame tulemuseks ainulaadse versiooniga artefakti. See võimaldab meil vajadusel konfiguratsiooni tagasi vahetada. Me võime isegi juurutada konfiguratsiooni, mida kasutati aastat tagasi, ja see töötab täpselt samamoodi. Stabiilne konfiguratsioon parandab jaotatud süsteemi ennustatavust ja usaldusväärsust. Konfiguratsioon on fikseeritud kompileerimise ajal ja seda ei saa tootmissüsteemis kergesti muuta.
  9. Modulaarsus. Ettepanekud raamistik on modulaarsed ja mooduleid saab erinevates kombinatsioonides kasutada,
    et toetada erinevaid konfiguratsioone (seadeid/paigutusi). Eriti on võimalik luua väikese mastaabiga ühe sõlme paigutusi ja suurte mastaapidega mitme sõlme seadistusi. On mõistlik omada mitmeid tootmispaigutusi.
  10. Testimine. Testimise otstarbel võib keegi rakendada vale teenust ja kasutada seda sõltuvusena tüübimugavuse viisi. Samal ajal võiks hoida mitu erinevat testimispaigutust, kus erinevad osad on asendatud vale teenustega.
  11. Integreerimistestimine. Aeg-ajalt on jaotatud süsteemides integreerimistestide tegemine keeruline. Kasutades kirjeldatud lähenemist tüübimugavale kogu jaotatud süsteemile, saame kõik jaotatud osad ühel serveril kontrollitaval viisil käitada. Situation'i emuleerimine,
    kui üks teenustest muutub kättesaamatuks.

Puudused

Kompileeritud konfiguratsiooni lähenemine erineb "tavalisest" konfiguratsioonist ja see ei pruugi sobida kõikidele vajadustele. Siin on mõned kompileeritud konfiguratsiooni puudused:

  1. Staatiline konfiguratsioon. See ei pruugi sobida kõikidele rakendustele. Mõnedes olukordades on vaja kiiresti konfigureerida toodangus, möödudes kõigist turvameetmetest. See lähenemine muudab selle keerulisemaks. Konfiguratsiooni muutmiseks on vajalik kompileerimine ja uuesti juurutamine. See on nii omadus kui ka koormus.
  2. Konfiguratsiooni genereerimine. Kui konfiguratsioon genereeritakse mõne automatiseerimistööriista kaudu, nõuab see lähenemine järgmist koostamist (mis võib omakorda ebaõnnestuda). See võib nõuda täiendavat pingutust, et integreerida see lisastep viisBuildsüsteemi.
  3. Tööriistad. Täna on meil palju tööriistu, mis tuginevad tekstipõhistele konfiguratsioonidele. Mõned neist
    ei pruugi kehtida, kui konfiguratsioon on kompileeritud.
  4. Vajalik on mõtteviisi muutus. Arendajad ja DevOps on tuttavad tekstiliste konfiguratsioonifailidega. Konfiguratsiooni kompileerimise mõte võib neile tunduda kummaline.
  5. Enne kompileeritava konfiguratsiooni tutvustamist on vajalik kõrge kvaliteediga tarkvaraarenduse protsess.

Teatud näite rakendamise piirangud:

  1. Kui me pakume lisakonfiguratsiooni, mida sõlmpunkti rakendus ei nõua, ei aita kompilaator meil puuduvat rakendust tuvastada. Sellega saab tegeleda, kasutades HList või ADT-sid (jaotatav iseloom) sõlmpunkti konfiguratsioonis traitide ja Cake Pattern'i asemel.
  2. Peame konfiguratsiooni failis esitama mõningaid lõike: (package, import, object deklaratsioonid;
    override def's parameetrite jaoks, mis on vaikimisi väärtustega). Sellega saab osaliselt tegeleda DSL-i kasutamisega.
  3. Selles postituses ei käsitleta sarnaste sõlmpunktide klastrite dünaamilist reconfiguratsiooni.

Kokkuvõte

Selles postituses oleme arutanud ideed esindada konfiguratsiooni otse allika koodis tüübiturvalisel viisil. Seda lähenemist võiks kasutada paljudes rakendustes xml- ja muude tekstipõhiste konfiguratsioonide asendamiseks. Kuigi meie näide on rakendatud Scala-s, võiks seda ka tõlkida teistesse kompileeritavatesse keeltesse (nt Kotlin, C#, Swift jne). Seda lähenemist võiks proovida uues projektis ning vajadusel, kui see ei sobi, tagasi pöörduda vana meetodi juurde.

Kuid kompileeritav konfiguratsioon nõuab kõrge kvaliteediga arendusprotsessi. Vastutasuks lubab see pakkuda ühtlaselt kõrge kvaliteediga tugevat konfiguratsiooni.

Seda lähenemist võiks erineval viisil laiendada:

  1. Makrode kasutamine võib võimaldada konfiguratsiooni valideerimist ja kompileerimise ajal vigade saadet, kui esinevad äriloogika tingimuste ebaõnnestumised.
  2. Saab rakendada DSL-i, et esindada konfiguratsiooni kasutaja-domeeni sõbralikul viisil.
  3. Dünaamiline ressursihaldus automaatsete konfiguratsioonimuudatustega. Näiteks, kui me kohandame klastrite nodide arvu, võiksime soovida, et (1) nodid saaksid kergelt muudetud konfiguratsiooni; (2) klastrihaldur saaks uusi nodide andmeid.

Aitäh

Soovin tänada Andreid Saksonovi, Pavel Popovi ja Anton Nehaevi, kes andsid inspireerivat tagasisidet postituse mustandi kohta, mis aitas mul seda selgemaks muuta.

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster