Kompileeritud hajutatud süsteemi konfiguratsioon

Tahaksin teile öelda ühe huvitava mehhanismi hajutatud süsteemi konfiguratsiooniga töötamiseks. Konfiguratsioon on esitatud otse kompileeritud keeles (Scala), kasutades turvalisi tüüpe. See postitus on näide sellisest konfiguratsioonist ja käsitleb kompileeritud konfiguratsiooni üldises arendusprotsessis rakendamise erinevaid aspekte.

Kompileeritud hajutatud süsteemi konfiguratsioon

(Inglise)

Sissejuhatus

Usaldusväärse hajutatud süsteemi loomine tähendab, et kõik sõlmed kasutavad õiget konfiguratsiooni, mis on sünkroonitud teiste sõlmedega. DevOpsi tehnoloogiaid (terraform, ansible või midagi sellist) kasutatakse tavaliselt konfiguratsioonifailide (sageli iga sõlme jaoks spetsiifiliste) automaatseks genereerimiseks. Samuti tahaksime olla kindlad, et kõik suhtlevad sõlmed kasutavad identseid protokolle (sh sama versiooni). Vastasel juhul on meie hajutatud süsteemi sisse ehitatud ühildumatus. JVM-i maailmas on selle nõude üheks tagajärjeks see, et igal pool tuleb kasutada sama protokollisõnumeid sisaldava teegi versiooni.

Aga hajutatud süsteemi testimine? Muidugi eeldame, et enne integratsioonitestimise juurde liikumist on kõigil komponentidel ühikutestid. (Selleks, et saaksime testitulemusi käitusajale ekstrapoleerida, peame pakkuma ka testimisetapis ja käitusajal identsed teegid.)

Integratsioonitestidega töötades on sageli lihtsam kasutada sama klassiteed kõikjal kõigis sõlmedes. Peame vaid tagama, et käitusajal kasutatakse sama klassiteed. (Kuigi erinevate sõlmede käitamine erinevate klassiteedega on täiesti võimalik, muudab see üldise konfiguratsiooni keerukamaks ning juurutamise ja integreerimise testidega seotud raskused.) Selle postituse jaoks eeldame, et kõik sõlmed kasutavad sama klassiteed.

Konfiguratsioon areneb koos rakendusega. Kasutame versioone programmi arendamise erinevate etappide tuvastamiseks. Tundub loogiline tuvastada ka konfiguratsioonide erinevad versioonid. Ja asetage konfiguratsioon ise versioonikontrollisüsteemi. Kui tootmises on ainult üks konfiguratsioon, saame kasutada lihtsalt versiooninumbrit. Kui kasutame palju tootmiseksemplare, vajame mitut
konfiguratsiooni harud ja lisaks versioonile (näiteks haru nimi) täiendav silt. Nii saame täpselt kindlaks määrata täpse konfiguratsiooni. Iga konfiguratsiooniidentifikaator vastab ainulaadselt hajutatud sõlmede, portide, välisressursside ja teegi versioonide konkreetsele kombinatsioonile. Selle postituse jaoks eeldame, et on ainult üks haru ja saame konfiguratsiooni tuvastada tavapärasel viisil, kasutades kolme punktiga eraldatud numbrit (1.2.3).

Kaasaegsetes keskkondades luuakse konfiguratsioonifaile harva käsitsi. Sagedamini genereeritakse need juurutamise ajal ja neid enam ei puudutata (nii et ära lõhu midagi). Tekib loomulik küsimus: miks me ikkagi kasutame konfiguratsiooni salvestamiseks tekstivormingut? Elujõuline alternatiiv näib olevat võimalus kasutada konfigureerimiseks tavalist koodi ja saada kasu kompileerimisaja kontrollidest.

Selles postituses uurime ideed kujutada konfiguratsiooni koostatud artefakti sees.

Koostatud konfiguratsioon

Selles jaotises on näide staatilisest kompileeritud konfiguratsioonist. Rakendatud on kaks lihtsat teenust - kajateenus ja kajateenuse klient. Nende kahe teenuse põhjal on kokku pandud kaks süsteemivalikut. Ühe võimaluse korral asuvad mõlemad teenused samas sõlmes, teises variandis - erinevates sõlmedes.

Tavaliselt sisaldab hajutatud süsteem mitut sõlme. Sõlme saate tuvastada teatud tüüpi väärtuste abil NodeId:

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

või

case class NodeId(hostName: String)

или даже

object Singleton
type NodeId = Singleton.type

Sõlmed täidavad erinevaid rolle, käitavad teenuseid ja nende vahel saab luua TCP/HTTP-ühendusi.

TCP-ühenduse kirjeldamiseks vajame vähemalt pordi numbrit. Samuti soovime kajastada selles pordis toetatud protokolli, et tagada nii klient kui ka server sama protokolli kasutamine. Kirjeldame ühendust järgmise klassi abil:

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

kus Port - lihtsalt täisarv Int mis näitab vastuvõetavate väärtuste vahemikku:

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

Rafineeritud tüübid

Vaata raamatukogu puhastatud и minu aruanne. Lühidalt öeldes võimaldab teek lisada kompileerimise ajal kontrollitavatele tüüpidele piiranguid. Sel juhul on kehtivad pordinumbri väärtused 16-bitised täisarvud. Kompileeritud konfiguratsiooni puhul ei ole täiustatud teegi kasutamine kohustuslik, kuid see parandab kompilaatori võimet konfiguratsiooni kontrollida.

HTTP (REST) protokollide puhul võib lisaks pordi numbrile vaja minna ka teenuse teed:

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

Fantoomtüübid

Protokolli tuvastamiseks kompileerimise ajal kasutame tüübiparameetrit, mida klassis ei kasutata. See otsus tuleneb asjaolust, et me ei kasuta käitusajal protokolli eksemplari, kuid soovime, et kompilaator kontrolliks protokolli ühilduvust. Protokolli täpsustades ei saa me ebasobivat teenust sõltuvusena edasi anda.

Üks levinumaid protokolle on REST API koos Json-serialiseerimisega:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

kus RequestMessage — taotluse tüüp, ResponseMessage — vastuse tüüp.
Muidugi saame kasutada ka teisi protokollikirjeldusi, mis tagavad meile vajaliku kirjelduse täpsuse.

Selle postituse jaoks kasutame protokolli lihtsustatud versiooni:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Siin on päring string, mis on lisatud URL-ile ja vastus on HTTP vastuse põhiosas tagastatav string.

Teenuse konfiguratsiooni kirjeldavad teenuse nimi, pordid ja sõltuvused. Neid elemente saab Scalas esitada mitmel viisil (näiteks HList-s, algebralised andmetüübid). Selle postituse jaoks kasutame koogimustrit ja esindame mooduleid kasutades trait'ov. (Koogi muster ei ole selle lähenemisviisi nõutav element. See on lihtsalt üks võimalik rakendus.)

Teenuste vahelisi sõltuvusi saab kujutada meetoditena, mis tagastavad porte EndPointteiste sõlmede 'd:

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

Kajateenuse loomiseks vajate vaid pordi numbrit ja märge, et port toetab kajaprotokolli. Me ei pruugi konkreetset porti määrata, kuna... tunnused võimaldavad deklareerida meetodeid ilma rakendamiseta (abstraktsed meetodid). Sel juhul nõuab kompilaator konkreetse konfiguratsiooni loomisel, et esitaksime abstraktse meetodi teostuse ja esitaksime pordi numbri. Kuna oleme meetodi rakendanud, ei pruugi me konkreetse konfiguratsiooni loomisel teist porti määrata. Kasutatakse vaikeväärtust.

Kliendi konfiguratsioonis deklareerime sõltuvuse kajateenusest:

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

Sõltuvus on sama tüüpi kui eksporditud teenus echoService. Eelkõige vajame kajakliendis sama protokolli. Seetõttu võime kahe teenuse ühendamisel olla kindlad, et kõik töötab õigesti.

Teenuste rakendamine

Teenuse käivitamiseks ja peatamiseks on vaja funktsiooni. (Teenuse peatamise võimalus on testimisel kriitilise tähtsusega.) Jällegi on sellise funktsiooni rakendamiseks mitu võimalust (näiteks võiksime kasutada konfiguratsioonitüübi alusel tüübiklasse). Selle postituse jaoks kasutame koogi mustrit. Esindame teenust klassi abil cats.Resource, sest See klass pakub juba vahendeid ressursside ohutuks vabastamiseks probleemide korral. Ressursi saamiseks peame pakkuma konfiguratsiooni ja valmis käitusaja konteksti. Teenuse käivitamise funktsioon võib välja näha järgmine:

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

kus

  • Config — selle teenuse konfiguratsioonitüüp
  • AddressResolver - käitusaegne objekt, mis võimaldab teil teada saada teiste sõlmede aadresse (vt allpool)

ja muud tüübid raamatukogust cats:

  • F[_] — efekti tüüp (lihtsamal juhul F[A] võib olla lihtsalt funktsioon () => A. Selles postituses kasutame cats.IO.)
  • Reader[A,B] - funktsiooni enam-vähem sünonüüm A => B
  • cats.Resource - ressurss, mida saab hankida ja vabastada
  • Timer — taimer (võimaldab mõneks ajaks magama jääda ja ajavahemikke mõõta)
  • ContextShift - analoog ExecutionContext
  • Applicative — efektitüüpide klass, mis võimaldab kombineerida üksikuid efekte (peaaegu monaad). Keerulisemates rakendustes tundub parem kasutada Monad/ConcurrentEffect.

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

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

(cm. allikas, milles rakendatakse muid teenuseid - kajateenus, kaja klient
и eluaegsed kontrollerid.)

Sõlm on objekt, mis võib käivitada mitu teenust (ressursside ahela käivitamise tagab kooki muster):

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

Pange tähele, et me täpsustame selle sõlme jaoks vajalikku konfiguratsiooni täpset tüüpi. Kui unustame määrata ühe konkreetse teenuse jaoks nõutava konfiguratsioonitüübi, ilmneb kompileerimisviga. Samuti ei saa me sõlme käivitada, kui me ei anna mõnda sobivat tüüpi objekti kõigi vajalike andmetega.

Hosti nime eraldusvõime

Kaughostiga ühenduse loomiseks vajame tegelikku IP-aadressi. Võimalik, et aadress saab teada hiljem kui ülejäänud konfiguratsioon. Seega vajame funktsiooni, mis kaardistab sõlme ID aadressiga:

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

Selle funktsiooni rakendamiseks on mitu võimalust:

  1. Kui aadressid saavad meile teada enne juurutamist, saame Scala koodi genereerida
    aadressid ja seejärel käivitage ehitus. See kompileerib ja käivitab testid.
    Sel juhul tuntakse funktsiooni staatiliselt ja seda saab esitada koodis vastendusena Map[NodeId, NodeAddress].
  2. Mõnel juhul saab tegelik aadress teada alles pärast sõlme käivitumist.
    Sel juhul saame rakendada "tuvastusteenust", mis töötab enne teisi sõlme ja kõik sõlmed registreeruvad selle teenusega ja taotlevad teiste sõlmede aadresse.
  3. Kui saame muuta /etc/hosts, siis saate kasutada eelmääratletud hostinimesid (nt my-project-main-node и echo-backend) ja lihtsalt linkige need nimed
    IP-aadressidega juurutamise ajal.

Selles postituses me neid juhtumeid üksikasjalikumalt ei käsitle. Meie
mänguasja näites on kõigil sõlmedel sama IP-aadress - 127.0.0.1.

Järgmisena kaalume kahte hajutatud süsteemi võimalust:

  1. Kõigi teenuste paigutamine ühte sõlme.
  2. Ja kajateenuse ja kajakliendi hostimine erinevates sõlmedes.

Konfiguratsioon jaoks üks sõlm:

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

Objekt rakendab nii kliendi kui ka serveri konfiguratsiooni. Kasutatakse ka eluaja konfiguratsiooni, nii et pärast intervalli lifetime programm lõpetada. (Ctrl-C töötab ka ja vabastab kõik ressursid õigesti.)

Sama konfiguratsiooni- ja teostustunnuste komplekti saab kasutada süsteemi loomiseks, mis koosneb kaks eraldi sõlme:

Kahe sõlme konfiguratsioon

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

Tähtis! Pange tähele, kuidas teenused on seotud. Määrame ühe sõlme rakendatud teenuse teise sõlme sõltuvusmeetodi teostusena. Sõltuvustüüpi kontrollib kompilaator, sest sisaldab protokolli tüüpi. Käivitamisel sisaldab sõltuvus õiget sihtsõlme ID-d. Tänu sellele skeemile täpsustame pordi numbri täpselt ühe korra ja garanteerime, et viitame alati õigele pordile.

Kahe süsteemisõlme rakendamine

Selle konfiguratsiooni jaoks kasutame samu teenuserakendusi ilma muudatusteta. Ainus erinevus on see, et meil on nüüd kaks objekti, mis rakendavad erinevaid teenusekomplekte:

  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 serverit ja vajab ainult serveri konfigureerimist. Teine sõlm rakendab klienti ja kasutab konfiguratsiooni teistsugust osa. Mõlemad sõlmed vajavad ka eluaegset haldust. Serverisõlm töötab määramata ajaks, kuni see peatatakse SIGTERM'om, ja kliendisõlm mõne aja pärast lõpetab töö. cm. käivitaja rakendus.

Üldine arendusprotsess

Vaatame, kuidas see konfiguratsiooni lähenemisviis üldist arendusprotsessi mõjutab.

Konfiguratsioon kompileeritakse koos ülejäänud koodiga ja luuakse artefakt (.jar). Tundub, et on mõttekas panna konfiguratsioon eraldi artefakti. Seda seetõttu, et sama koodi alusel võib meil olla mitu konfiguratsiooni. Jällegi on võimalik genereerida erinevatele konfiguratsiooniharudele vastavaid artefakte. Sõltuvused teekide konkreetsetest versioonidest salvestatakse koos konfiguratsiooniga ja need versioonid salvestatakse igaveseks, kui otsustame selle konfiguratsiooni versiooni juurutada.

Iga konfiguratsioonimuudatus muutub koodimuudatuseks. Ja seetõttu igaüks
muudatus kaetakse tavalise kvaliteedi tagamise protsessiga:

Pilet veajälgijas -> PR -> ülevaade -> ühenda asjakohaste harudega ->
integreerimine -> juurutamine

Kompileeritud konfiguratsiooni rakendamise peamised tagajärjed on järgmised:

  1. Konfiguratsioon on hajutatud süsteemi kõigis sõlmedes ühtlane. Tulenevalt asjaolust, et kõik sõlmed saavad ühest allikast sama konfiguratsiooni.

  2. Konfiguratsiooni muutmine ainult ühes sõlmes on problemaatiline. Seetõttu on "konfiguratsiooni triivimine" ebatõenäoline.

  3. Konfiguratsioonis väikeste muudatuste tegemine muutub keerulisemaks.

  4. Enamik konfiguratsioonimuudatusi tehakse osana üldisest arendusprotsessist ja need vaadatakse üle.

Kas mul on tootmiskonfiguratsiooni salvestamiseks vaja eraldi hoidlat? See konfiguratsioon võib sisaldada paroole ja muud tundlikku teavet, millele soovime juurdepääsu piirata. Selle põhjal tundub olevat mõttekas salvestada lõplik konfiguratsioon eraldi hoidlasse. Saate konfiguratsiooni jagada kaheks osaks – üks sisaldab avalikult juurdepääsetavaid konfiguratsiooniseadeid ja teine ​​​​piiratud seadeid. See võimaldab enamikul arendajatel juurdepääsu tavaseadetele. Seda eraldamist on lihtne saavutada, kasutades vaikeväärtusi sisaldavaid vahepealseid tunnuseid.

Võimalikud variatsioonid

Proovime võrrelda koostatud konfiguratsiooni mõne levinud alternatiiviga:

  1. Tekstifail sihtmasinas.
  2. Tsentraliseeritud võtmeväärtuste pood (etcd/zookeeper).
  3. Protsessi komponendid, mida saab ilma protsessi taaskäivitamata ümber konfigureerida/taaskäivitada.
  4. Konfiguratsiooni salvestamine väljaspool artefakti ja versioonikontrolli.

Tekstifailid pakuvad väikeste muudatuste tegemisel märkimisväärset paindlikkust. Süsteemiadministraator saab kaugsõlme sisse logida, teha vastavates failides muudatusi ja teenuse taaskäivitada. Suurte süsteemide puhul ei pruugi selline paindlikkus aga soovitav olla. Tehtud muudatused ei jäta teistesse süsteemidesse jälgi. Keegi ei vaata muudatusi üle. Kes ja mis põhjusel muudatused täpselt tegi, on raske kindlaks teha. Muudatusi ei testita. Kui süsteem on hajutatud, võib administraator unustada teistes sõlmedes vastava muudatuse teha.

(Samuti tuleb märkida, et kompileeritud konfiguratsiooni kasutamine ei sulge tulevikus tekstifailide kasutamise võimalust. Piisab parseri ja validaatori lisamisest, mis toodab väljundina sama tüüpi Configja saate kasutada tekstifaile. Sellest järeldub kohe, et kompileeritud konfiguratsiooniga süsteemi keerukus on mõnevõrra väiksem kui tekstifaile kasutava süsteemi keerukus, sest tekstifailid nõuavad lisakoodi.)

Tsentraliseeritud võtmeväärtuste hoidla on hea mehhanism hajutatud rakenduse metaparameetrite levitamiseks. Peame otsustama, mis on konfiguratsiooniparameetrid ja mis on lihtsalt andmed. Olgu meil funktsioon C => A => Bja parameetrid C harva muutub ja andmeid A - sageli. Sel juhul võime seda öelda C - konfiguratsiooniparameetrid ja A - andmed. Näib, et konfiguratsiooniparameetrid erinevad andmetest selle poolest, et üldiselt muutuvad need harvemini kui andmed. Samuti pärinevad andmed tavaliselt ühest allikast (kasutajalt) ja konfiguratsiooniparameetrid teisest (süsteemiadministraatorilt).

Kui harva muutuvaid parameetreid on vaja värskendada ilma programmi taaskäivitamata, võib see sageli põhjustada programmi tüsistusi, kuna peame parameetrid kuidagi edastama, salvestama, sõeluma ja kontrollima ning valed väärtused töötlema. Seetõttu on programmi keerukuse vähendamise seisukohalt mõttekas vähendada parameetrite arvu, mis võivad programmi töö käigus muutuda (või neid parameetreid üldse mitte toetada).

Selle postituse jaoks eristame staatilisi ja dünaamilisi parameetreid. Kui teenuse loogika nõuab programmi töötamise ajal parameetrite muutmist, siis nimetame selliseid parameetreid dünaamilisteks. Vastasel juhul on valikud staatilised ja neid saab konfigureerida koostatud konfiguratsiooni abil. Dünaamilise ümberseadistamise jaoks võib meil vaja minna mehhanismi programmi osade taaskäivitamiseks uute parameetritega, sarnaselt operatsioonisüsteemi protsesside taaskäivitamisele. (Meie arvates on soovitatav vältida reaalajas ümberseadistamist, kuna see muudab süsteemi keerukamaks. Võimalusel on parem kasutada protsesside taaskäivitamiseks OS-i standardseid võimalusi.)

Üks staatilise konfiguratsiooni kasutamise oluline aspekt, mis paneb inimesi dünaamilist ümberseadistamist kaaluma, on aeg, mis kulub süsteemi taaskäivitamiseks pärast konfiguratsioonivärskendust (seisakuaeg). Tegelikult, kui meil on vaja staatilises konfiguratsioonis muudatusi teha, peame uute väärtuste jõustumiseks süsteemi taaskäivitama. Seisakuprobleemide raskusaste on erinevate süsteemide puhul erinev. Mõnel juhul saate ajastada taaskäivituse ajal, mil koormus on minimaalne. Kui teil on vaja pakkuda pidevat teenust, saate seda rakendada AWS ELB ühenduse tühjendamine. Samal ajal, kui meil on vaja süsteemi taaskäivitada, käivitame selle süsteemi paralleelse eksemplari, lülitame sellele tasakaalustaja ja ootame vanade ühenduste lõpuleviimist. Pärast kõigi vanade ühenduste katkemist sulgeme süsteemi vana eksemplari.

Vaatleme nüüd konfiguratsiooni salvestamise küsimust artefakti sees või väljaspool. Kui salvestame konfiguratsiooni artefakti sees, siis oli meil vähemalt võimalus artefakti kokkupanemise käigus konfiguratsiooni õigsust kontrollida. Kui konfiguratsioon on väljaspool kontrollitud artefakti, on raske jälgida, kes ja miks selles failis muudatusi tegi. Kui oluline see on? Meie arvates on paljude tootmissüsteemide jaoks oluline stabiilne ja kvaliteetne konfiguratsioon.

Artefakti versioon võimaldab teil määrata, millal see loodi, milliseid väärtusi see sisaldab, millised funktsioonid on lubatud/keelatud ja kes vastutab konfiguratsiooni muudatuste eest. Loomulikult nõuab konfiguratsiooni artefakti salvestamine teatud pingutusi, seega peate tegema teadliku otsuse.

Plussid ja miinused

Tahaksin peatuda pakutud tehnoloogia plussidel ja miinustel.

Eelised

Allpool on loetelu koostatud hajutatud süsteemi konfiguratsiooni põhifunktsioonidest:

  1. Staatilise konfiguratsiooni kontroll. Võimaldab selles kindel olla
    konfiguratsioon on õige.
  2. Rikkalik konfiguratsioonikeel. Tavaliselt piirduvad muud konfiguratsioonimeetodid maksimaalselt stringi muutuja asendamisega. Scala kasutamisel on teie konfiguratsiooni täiustamiseks saadaval lai valik keelefunktsioone. Näiteks saame kasutada
    vaikeväärtuste tunnused, kasutades parameetrite rühmitamiseks objekte, saame hõlmavas ulatuses viidata ainult üks kord deklareeritud väärtustele (DRY). Saate luua mis tahes klassi otse konfiguratsioonis (Seq, Map, kohandatud klassid).
  3. DSL. Scalal on mitmeid keelefunktsioone, mis muudavad DSL-i loomise lihtsamaks. Neid funktsioone on võimalik ära kasutada ja rakendada kasutajate sihtrühmale mugavamat konfiguratsioonikeelt, et konfiguratsioon oleks vähemalt domeeniekspertidele loetav. Spetsialistid saavad osaleda näiteks konfiguratsiooni ülevaatuse protsessis.
  4. Sõlmede vaheline terviklikkus ja sünkroonsus. Kogu hajutatud süsteemi konfiguratsiooni ühes punktis salvestamise eeliseks on see, et kõik väärtused deklareeritakse täpselt üks kord ja seejärel kasutatakse neid uuesti, kus iganes neid vaja on. Fantoomtüüpide kasutamine portide deklareerimiseks tagab, et sõlmed kasutavad ühilduvaid protokolle kõigis õigetes süsteemikonfiguratsioonides. Sõlmede vahelised selgesõnalised kohustuslikud sõltuvused tagavad kõigi teenuste ühendamise.
  5. Kvaliteetsed muudatused. Ühise arendusprotsessi abil konfiguratsioonis muudatuste tegemine võimaldab saavutada ka konfiguratsiooni kõrged kvaliteedistandardid.
  6. Samaaegne konfiguratsiooni värskendamine. Süsteemi automaatne juurutamine pärast konfiguratsiooni muutmist tagab kõigi sõlmede värskendamise.
  7. Rakenduse lihtsustamine. Rakendus ei vaja sõelumist, konfiguratsiooni kontrollimist ega valede väärtuste käsitlemist. See vähendab rakenduse keerukust. (Osa meie näites täheldatud konfiguratsiooni keerukusest ei ole koostatud konfiguratsiooni atribuut, vaid teadlik otsus, mis on ajendatud soovist pakkuda suuremat tüüpi turvalisust.) Tavalise konfiguratsiooni juurde naasta on üsna lihtne – lihtsalt rakendage puuduv osad. Seetõttu võite alustada näiteks kompileeritud konfiguratsioonist, lükates mittevajalike osade rakendamise edasi hetkeni, mil seda tõesti vaja läheb.
  8. Verifitseeritud konfiguratsioon. Kuna konfiguratsioonimuudatused järgivad kõigi muude muudatuste tavapärast saatust, on väljundiks unikaalse versiooniga artefakt. See võimaldab meil näiteks vajadusel naasta konfiguratsiooni eelmise versiooni juurde. Saame kasutada isegi aasta tagust konfiguratsiooni ja süsteem töötab täpselt samamoodi. Stabiilne konfiguratsioon parandab hajutatud süsteemi prognoositavust ja töökindlust. Kuna konfiguratsioon fikseeritakse koostamise etapis, on seda tootmises üsna raske võltsida.
  9. Modulaarsus. Kavandatav raamistik on modulaarne ja mooduleid saab erinevatel viisidel kombineerida erinevate süsteemide loomiseks. Eelkõige saate süsteemi konfigureerida töötama ühes teostuses ühes sõlmes ja teises mitmes sõlmes. Süsteemi tootmiseksemplaride jaoks saate luua mitu konfiguratsiooni.
  10. Testimine. Asendades üksikud teenused näidisobjektidega, saate süsteemist mitu versiooni, mis on testimiseks mugavad.
  11. Integratsiooni testimine. Kui kogu hajutatud süsteemi jaoks on üks konfiguratsioon, on integratsioonitestimise osana võimalik käivitada kõiki komponente kontrollitud keskkonnas. Lihtne on jäljendada näiteks olukorda, kus mõned sõlmed muutuvad juurdepääsetavaks.

Puudused ja piirangud

Kompileeritud konfiguratsioon erineb teistest konfiguratsioonimeetoditest ja ei pruugi mõne rakenduse jaoks sobida. Allpool on mõned puudused:

  1. Staatiline konfiguratsioon. Mõnikord peate tootmises konfiguratsiooni kiiresti parandama, vältides kõiki kaitsemehhanisme. Selle lähenemisviisiga võib see olla keerulisem. Vähemalt on siiski vaja kompileerimist ja automaatset juurutamist. See on nii lähenemisviisi kasulik omadus kui ka mõnel juhul puudus.
  2. Konfiguratsiooni genereerimine. Kui konfiguratsioonifaili genereerib automaatne tööriist, võib ehitusskripti integreerimiseks olla vaja täiendavaid jõupingutusi.
  3. Tööriistad. Praegu põhinevad konfiguratsiooniga töötamiseks loodud utiliidid ja tehnikad tekstifailidel. Kõik sellised utiliidid/tehnikad pole kompileeritud konfiguratsioonis saadaval.
  4. Vaja on suhtumise muutust. Arendajad ja DevOpid on tekstifailidega harjunud. Konfiguratsiooni koostamise idee võib olla mõnevõrra ootamatu ja ebatavaline ning põhjustada tagasilükkamist.
  5. Vaja on kvaliteetset arendusprotsessi. Kompileeritud konfiguratsiooni mugavaks kasutamiseks on vajalik rakenduse (CI/CD) loomise ja juurutamise protsessi täielik automatiseerimine. Muidu on see üsna ebamugav.

Vaatleme ka vaadeldava näite mitmeid piiranguid, mis ei ole seotud kompileeritud konfiguratsiooni ideega:

  1. Kui anname tarbetut konfiguratsiooniteavet, mida sõlm ei kasuta, ei aita kompilaator meil puuduvat teostust tuvastada. Selle probleemi saab lahendada, kui loobuda koogimustrist ja kasutada jäigemaid tüüpe, näiteks HList või algebralised andmetüübid (juhtumiklassid) konfiguratsiooni esitamiseks.
  2. Konfiguratsioonifailis on ridu, mis ei ole konfiguratsiooni endaga seotud: (package, import,objektideklaratsioonid; override def's parameetrite jaoks, millel on vaikeväärtused). Seda saab osaliselt vältida, kui rakendate oma DSL-i. Lisaks seavad failistruktuurile teatud piirangud ka muud tüüpi konfiguratsioonid (näiteks XML).
  3. Selle postituse jaoks ei kaalu me sarnaste sõlmede klastri dünaamilist ümberkonfigureerimist.

Järeldus

Selles postituses uurisime ideed kujutada konfiguratsiooni lähtekoodis, kasutades Scala tüüpi süsteemi täiustatud võimalusi. Seda lähenemist saab kasutada erinevates rakendustes traditsiooniliste xml- või tekstifailidel põhinevate konfigureerimismeetodite asendajana. Kuigi meie näidet rakendatakse Scalas, saab samu ideid üle kanda ka teistesse kompileeritud keeltesse (nt Kotlin, C#, Swift jne). Saate seda lähenemist proovida ühes järgmistest projektidest ja kui see ei tööta, liikuge edasi tekstifaili juurde, lisades puuduvad osad.

Loomulikult nõuab kompileeritud konfiguratsioon kvaliteetset arendusprotsessi. Vastutasuks on tagatud konfiguratsioonide kõrge kvaliteet ja töökindlus.

Kaalutud lähenemisviisi saab laiendada:

  1. Kompileerimisaja kontrollimiseks saate kasutada makrosid.
  2. Saate rakendada DSL-i, et esitada konfiguratsioon lõppkasutajatele kättesaadaval viisil.
  3. Saate rakendada dünaamilist ressursside haldust automaatse konfiguratsiooni reguleerimisega. Näiteks sõlmede arvu muutmine klastris eeldab, et (1) iga sõlm saaks veidi erineva konfiguratsiooni; (2) klastrihaldur sai teavet uute sõlmede kohta.

Tänusõnad

Tänan Andrei Saksonovit, Pavel Popovit ja Anton Nekhaevit artikli kavandile suunatud konstruktiivse kriitika eest.

Allikas: www.habr.com

Lisa kommentaar