Hajautetun järjestelmän käännettävä kokoonpano

Tässä viestissä haluamme jakaa mielenkiintoisen tavan käsitellä hajautetun järjestelmän konfigurointia.
Kokoonpano esitetään suoraan Scala-kielellä tyyppiturvallisella tavalla. Esimerkki toteutuksesta on kuvattu yksityiskohtaisesti. Käsitellään ehdotuksen eri näkökohtia, mukaan lukien vaikutus koko kehitysprosessiin.

Hajautetun järjestelmän käännettävä kokoonpano

(на русском)

esittely

Kestävän hajautetun järjestelmän rakentaminen edellyttää oikean ja yhtenäisen konfiguroinnin käyttöä kaikissa solmuissa. Tyypillinen ratkaisu on käyttää tekstimuotoista käyttöönottokuvausta (terraform, ansible tai jotain vastaavaa) ja automaattisesti luotuja konfiguraatiotiedostoja (usein — jokaiselle solmulle/roolille omistettu). Haluaisimme myös käyttää samojen versioiden samoja protokollia jokaisessa kommunikaatiosolmussa (muuten kohdataan yhteensopivuusongelmia). JVM-maailmassa tämä tarkoittaa, että ainakin viestikirjaston tulee olla samaa versiota kaikissa kommunikoivissa solmuissa.

Entä järjestelmän testaus? Tietenkin meidän pitäisi tehdä yksikkötestit kaikille komponenteille ennen integrointitesteihin ryhtymistä. Jotta testitulokset voidaan ekstrapoloida ajonaikaisesti, meidän tulee varmistaa, että kaikkien kirjastojen versiot pidetään identtisinä sekä ajonaikaisissa että testausympäristöissä.

Integrointitestejä suoritettaessa on usein paljon helpompaa käyttää samaa luokkapolkua kaikissa solmuissa. Meidän on vain varmistettava, että käyttöönotossa käytetään samaa luokkapolkua. (On mahdollista käyttää eri luokkapolkuja eri solmuissa, mutta tätä kokoonpanoa on vaikeampi esittää ja ottaa se käyttöön oikein.) Joten asioiden yksinkertaistamiseksi harkitsemme vain identtisiä luokkapolkuja kaikissa solmuissa.

Konfiguraatiolla on taipumus kehittyä yhdessä ohjelmiston kanssa. Käytämme yleensä eri versioita tunnistaaksemme erilaisia
ohjelmistokehityksen vaiheet. Näyttää järkevältä kattaa konfiguraatiot versionhallinnan alla ja tunnistaa eri kokoonpanot joillakin tunnisteilla. Jos tuotannossa on vain yksi kokoonpano, voimme käyttää yhtä versiota tunnisteena. Joskus meillä voi olla useita tuotantoympäristöjä. Ja jokaista ympäristöä varten saatamme tarvita erillisen määrityshaaran. Joten kokoonpanot voidaan merkitä haaralla ja versiolla eri kokoonpanojen yksilöimiseksi. Jokainen haaranimike ja versio vastaavat yhtä yhdistelmää hajautetuista solmuista, porteista, ulkoisista resursseista ja luokkapolun kirjastoversioista kussakin solmussa. Tässä katetaan vain yksi haara ja tunnistetaan kokoonpanot kolmen komponentin desimaaliversiolla (1.2.3) samalla tavalla kuin muut artefaktit.

Nykyaikaisissa ympäristöissä asetustiedostoja ei enää muokata manuaalisesti. Yleensä tuotamme
konfiguraatiotiedostot käyttöönoton yhteydessä ja älä koske niihin jälkeenpäin. Joten voidaan kysyä, miksi käytämme edelleen tekstimuotoa asetustiedostoissa? Yksi toteuttamiskelpoinen vaihtoehto on sijoittaa kokoonpano käännösyksikön sisään ja hyötyä käännösaikaisesta konfiguraatiotarkistuksesta.

Tässä viestissä tarkastelemme ajatusta pitää kokoonpano kootussa artefaktissa.

Käännettävä kokoonpano

Tässä osiossa käsittelemme esimerkkiä staattisesta konfiguraatiosta. Kaksi yksinkertaista palvelua - kaikupalvelu ja kaikupalvelun asiakas - konfiguroidaan ja toteutetaan. Sitten luodaan kaksi erilaista hajautettua järjestelmää molemmilla palveluilla. Toinen on yhden solmun konfiguraatiolle ja toinen kahden solmun konfiguraatiolle.

Tyypillinen hajautettu järjestelmä koostuu muutamasta solmusta. Solmut voidaan tunnistaa käyttämällä jotakin tyyppiä:

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

tai vain

case class NodeId(hostName: String)

tai jopa

object Singleton
type NodeId = Singleton.type

Nämä solmut suorittavat erilaisia ​​rooleja, suorittavat joitakin palveluita ja niiden pitäisi pystyä kommunikoimaan muiden solmujen kanssa TCP/HTTP-yhteyksien avulla.

TCP-yhteyttä varten vaaditaan vähintään portin numero. Haluamme myös varmistaa, että asiakas ja palvelin puhuvat samaa protokollaa. Jotta mallinnetaan solmujen välinen yhteys, määritetään seuraava luokka:

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

jossa Port on vain Int sallitulla alueella:

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

Hienostuneita tyyppejä

nähdä puhdistettu kirjasto. Lyhyesti sanottuna sen avulla voidaan lisätä käännösaikarajoituksia muihin tyyppeihin. Tässä tapauksessa Int sallitaan vain 16-bittiset arvot, jotka voivat edustaa portin numeroa. Tätä kirjastoa ei tarvitse käyttää tässä konfigurointimenetelmässä. Se vain näyttää sopivan erittäin hyvin.

HTTP:tä (REST) ​​varten saatamme tarvita myös palvelun polun:

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

Phantom tyyppi

Protokollan tunnistamiseksi kääntämisen aikana käytämme Scala-ominaisuutta tyyppiargumentin ilmoittamiseen Protocol jota ei käytetä luokassa. Se on ns phantom tyyppi. Ajon aikana tarvitsemme harvoin protokollatunnisteen esiintymää, siksi emme tallenna sitä. Kokoamisen aikana tämä haamutyyppi antaa lisäturvallisuutta. Emme voi ohittaa porttia väärällä protokollalla.

Yksi laajimmin käytetyistä protokollista on REST API Json-serialisoinnilla:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

jossa RequestMessage on viestien perustyyppi, jonka asiakas voi lähettää palvelimelle ja ResponseMessage on vastausviesti palvelimelta. Voimme tietysti luoda muita protokollakuvauksia, jotka määrittelevät viestintäprotokollan halutulla tarkkuudella.

Tätä viestiä varten käytämme protokollan yksinkertaisempaa versiota:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Tässä protokollassa pyyntösanoma liitetään URL-osoitteeseen ja vastausviesti palautetaan pelkkänä merkkijonona.

Palvelukonfiguraatio voidaan kuvata palvelun nimellä, porttien kokoelmalla ja joillakin riippuvuuksilla. On olemassa muutamia mahdollisia tapoja esittää kaikkia näitä elementtejä Scalassa (esim. HList, algebralliset tietotyypit). Tätä viestiä varten käytämme kakkukuviota ja edustamme yhdisteltäviä kappaleita (moduuleja) piirteinä. (Kakkukuvio ei ole vaatimus tälle käännettävälle konfigurointitavalle. Se on vain yksi idean mahdollinen toteutus.)

Riippuvuudet voidaan esittää käyttämällä kakkumallia muiden solmujen päätepisteinä:

  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-palvelu tarvitsee vain määritetyn portin. Ja julistamme, että tämä portti tukee echo-protokollaa. Huomaa, että meidän ei tarvitse määrittää tiettyä porttia tällä hetkellä, koska ominaisuus sallii abstraktien menetelmien määrittelyn. Jos käytämme abstrakteja menetelmiä, kääntäjä vaatii toteutuksen konfigurointiinstanssissa. Täällä olemme tarjonneet toteutuksen (8081) ja sitä käytetään oletusarvona, jos ohitamme sen konkreettisessa kokoonpanossa.

Voimme ilmoittaa riippuvuuden kaikupalveluasiakkaan kokoonpanossa:

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

Riippuvuus on samaa tyyppiä kuin echoService. Erityisesti se vaatii samaa protokollaa. Näin ollen voimme olla varmoja, että jos yhdistämme nämä kaksi riippuvuutta, ne toimivat oikein.

Palvelujen toteutus

Palvelu tarvitsee toiminnon käynnistyäkseen ja sulkeutuakseen sulavasti. (Mahdollisuus sulkea palvelu on kriittinen testauksen kannalta.) Jälleen on olemassa muutamia vaihtoehtoja tällaisen funktion määrittämiseksi tietylle kokoonpanolle (voimme esimerkiksi käyttää tyyppiluokkia). Tässä viestissä käytämme taas kakkukuviota. Voimme edustaa palvelua käyttämällä cats.Resource joka tarjoaa jo haarukoinnin ja resurssien vapauttamisen. Resurssin hankkimiseksi meidän tulee tarjota kokoonpano ja jokin ajonaikainen konteksti. Palvelun käynnistystoiminto voi siis näyttää tältä:

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

jossa

  • Config — tämän palvelun käynnistäjän vaatima kokoonpanotyyppi
  • AddressResolver — ajonaikainen objekti, joka pystyy hankkimaan muiden solmujen todellisia osoitteita (jatka lukemista saadaksesi lisätietoja).

muut tyypit ovat peräisin cats:

  • F[_] - tehostetyyppi (yksinkertaisimmassa tapauksessa F[A] voisi olla vain () => A. Tässä viestissä käytämme cats.IO.)
  • Reader[A,B] — on enemmän tai vähemmän synonyymi funktiolle A => B
  • cats.Resource — on tapoja hankkia ja vapauttaa
  • Timer - sallii nukkua/mittaa aikaa
  • ContextShift - analogi ExecutionContext
  • Applicative - voimassa olevien toimintojen kääre (melkein monadi) (saamme lopulta korvata sen jollain muulla)

Tämän käyttöliittymän avulla voimme toteuttaa muutamia palveluita. Esimerkiksi palvelu, joka ei tee mitään:

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

(Katso Lähdekoodi muiden palveluiden toteutuksissa — kaikupalvelu,
kaiku asiakas ja elinikäiset säätimet.)

Solmu on yksittäinen objekti, joka suorittaa muutamia palveluita (resurssiketjun käynnistäminen on mahdollista Cake Patternin avulla):

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

Huomaa, että määritämme solmussa tarkan kokoonpanotyypin, jonka tämä solmu tarvitsee. Kääntäjä ei anna meidän rakentaa objektia (Cake), jonka tyyppi on riittämätön, koska jokainen palveluominaisuus ilmoittaa rajoituksen Config tyyppi. Emme myöskään voi käynnistää solmua ilman täydellisiä määrityksiä.

Solmun osoitteen resoluutio

Yhteyden muodostamiseksi tarvitsemme todellisen isäntäosoitteen jokaiselle solmulle. Se saattaa olla tiedossa myöhemmin kuin muut kokoonpanon osat. Siksi tarvitsemme tavan tarjota solmutunnuksen ja sen todellisen osoitteen välinen kartoitus. Tämä kartoitus on funktio:

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

On olemassa muutamia mahdollisia tapoja toteuttaa tällainen toiminto.

  1. Jos tiedämme todelliset osoitteet ennen käyttöönottoa, solmuisäntien ilmentymisen aikana, voimme luoda Scala-koodin todellisilla osoitteilla ja suorittaa koontiversion sen jälkeen (joka suorittaa käännösaikatarkistuksia ja suorittaa sitten integraatiotestipaketin). Tässä tapauksessa kartoitusfunktiomme tunnetaan staattisesti ja se voidaan yksinkertaistaa esimerkiksi a Map[NodeId, NodeAddress].
  2. Joskus saamme varsinaiset osoitteet vasta myöhemmässä vaiheessa, kun solmu on todella käynnistetty, tai meillä ei ole osoitteita solmuista, joita ei ole vielä aloitettu. Tässä tapauksessa meillä saattaa olla etsintäpalvelu, joka käynnistetään ennen kaikkia muita solmuja ja jokainen solmu saattaa mainostaa osoitettaan kyseisessä palvelussa ja tilata riippuvuuksia.
  3. Jos voimme muuttaa /etc/hosts, voimme käyttää ennalta määritettyjä isäntänimiä (esim my-project-main-node ja echo-backend) ja yhdistä tämä nimi IP-osoitteeseen käyttöönottohetkellä.

Tässä viestissä emme käsittele näitä tapauksia tarkemmin. Itse asiassa leluesimerkissämme kaikilla solmuilla on sama IP-osoite - 127.0.0.1.

Tässä viestissä tarkastelemme kahta hajautettua järjestelmäasettelua:

  1. Yhden solmun asettelu, jossa kaikki palvelut sijoitetaan yhteen solmuun.
  2. Kahden solmun asettelu, jossa palvelu ja asiakas ovat eri solmuissa.

Kokoonpano a yksi solmu asettelu on seuraava:

Yhden solmun konfiguraatio

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

Täällä luomme yhden kokoonpanon, joka laajentaa sekä palvelimen että asiakkaan kokoonpanon. Lisäksi määritämme elinkaariohjaimen, joka normaalisti lopettaa asiakkaan ja palvelimen sen jälkeen lifetime intervalli kulkee.

Saman sarjan palvelutoteutuksia ja -konfiguraatioita voidaan käyttää järjestelmän asettelun luomiseen kahdella erillisellä solmulla. Meidän on vain luotava kaksi erillistä solmukonfiguraatiota asianmukaisilla palveluilla:

Kahden solmun kokoonpano

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

Katso kuinka määritämme riippuvuuden. Mainitsemme toisen solmun tarjoaman palvelun nykyisen solmun riippuvuutena. Riippuvuuden tyyppi tarkistetaan, koska se sisältää protokollaa kuvaavan phantom-tyypin. Ja ajon aikana meillä on oikea solmutunnus. Tämä on yksi ehdotetun konfigurointimenetelmän tärkeistä näkökohdista. Se antaa meille mahdollisuuden asettaa portti vain kerran ja varmistaa, että viittaamme oikeaan porttiin.

Kahden solmun toteutus

Tässä kokoonpanossa käytämme täsmälleen samoja palvelutoteutuksia. Ei muutoksia ollenkaan. Luomme kuitenkin kaksi erilaista solmutoteutusta, jotka sisältävät erilaisia ​​palveluja:

  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
  }

Ensimmäinen solmu toteuttaa palvelimen ja se tarvitsee vain palvelinpuolen konfiguroinnin. Toinen solmu toteuttaa asiakkaan ja tarvitsee toisen osan konfiguraatiosta. Molemmat solmut vaativat jonkin verran käyttöikää. Tämän post-palvelun tarkoituksiin solmulla on loputon käyttöikä, joka voidaan lopettaa käyttämällä SIGTERM, kun taas echo-asiakasohjelma päättyy määritetyn rajallisen keston jälkeen. Katso aloitussovellus lisätietoja.

Kokonaiskehitysprosessi

Katsotaanpa, kuinka tämä lähestymistapa muuttaa tapaamme työskennellä kokoonpanon kanssa.

Konfiguraatio koodina käännetään ja tuottaa artefaktin. Näyttää järkevältä erottaa konfigurointiartefaktit muista koodiartefakteista. Usein meillä voi olla useita konfiguraatioita samalla koodipohjalla. Ja tietysti meillä voi olla useita versioita eri kokoonpanohaaroista. Konfiguraatiossa voimme valita tiettyjä kirjastojen versioita, ja tämä pysyy vakiona aina, kun otamme tämän kokoonpanon käyttöön.

Konfiguraatiomuutoksesta tulee koodin muutos. Joten sen pitäisi kuulua saman laadunvarmistusprosessin piiriin:

Lippu -> PR -> tarkistus -> yhdistäminen -> jatkuva integrointi -> jatkuva käyttöönotto

Lähestymistavalla on seuraavat seuraukset:

  1. Kokoonpano on johdonmukainen tietyn järjestelmän esiintymän osalta. Vaikuttaa siltä, ​​että solmujen välillä ei ole mahdollista saada väärää yhteyttä.
  2. Kokoonpanon muuttaminen vain yhdessä solmussa ei ole helppoa. Tuntuu kohtuuttomalta kirjautua sisään ja muuttaa joitain tekstitiedostoja. Joten kokoonpanon ajautuminen on vähemmän mahdollista.
  3. Pieniä kokoonpanomuutoksia ei ole helppo tehdä.
  4. Suurin osa kokoonpanomuutoksista noudattaa samaa kehitysprosessia, ja se läpäisee jonkin verran tarkistusta.

Tarvitsemmeko erillisen arkiston tuotantomäärityksiä varten? Tuotantokokoonpano saattaa sisältää arkaluontoisia tietoja, jotka haluamme pitää poissa monien ihmisten ulottuvilta. Joten saattaa olla syytä pitää erillinen arkisto, jolla on rajoitettu pääsy ja joka sisältää tuotantokokoonpanon. Voimme jakaa kokoonpanon kahteen osaan - toiseen, joka sisältää avoimimmat tuotannon parametrit ja toiseen, joka sisältää konfiguroinnin salaisen osan. Tämä mahdollistaisi useimpien kehittäjien pääsyn useimpiin parametreihin ja rajoittaisi pääsyn todella arkaluonteisiin asioihin. Tämä on helppo saavuttaa käyttämällä keskitason ominaisuuksia oletusparametriarvoilla.

Muunnelmia

Katsotaanpa ehdotetun lähestymistavan hyvät ja huonot puolet verrattuna muihin kokoonpanonhallintatekniikoihin.

Ensinnäkin luettelemme muutamia vaihtoehtoja ehdotetun konfiguroinnin eri puolille:

  1. Tekstitiedosto kohdekoneella.
  2. Keskitetty avainarvojen tallennus (esim etcd/zookeeper).
  3. Osaprosessin osat, jotka voidaan määrittää uudelleen/käynnistää uudelleen ilman prosessin uudelleenkäynnistystä.
  4. Konfigurointi artefaktien ja versionhallinnan ulkopuolella.

Tekstitiedosto antaa jonkin verran joustavuutta ad-hoc-korjausten suhteen. Järjestelmänvalvoja voi kirjautua sisään kohdesolmuun, tehdä muutoksen ja käynnistää palvelun uudelleen. Tämä ei ehkä ole yhtä hyvä isommille järjestelmille. Muutoksen taakse ei jää jälkiä. Muutosta ei tarkastele toinen silmäpari. Saattaa olla vaikeaa selvittää, mikä muutoksen on aiheuttanut. Sitä ei ole testattu. Hajautetun järjestelmän näkökulmasta järjestelmänvalvoja voi yksinkertaisesti unohtaa päivittää jonkin muun solmun kokoonpanon.

(Btw, jos lopulta tulee tarve aloittaa tekstiasetustiedostojen käyttö, meidän on vain lisättävä jäsennys + validaattori, joka voi tuottaa saman Config kirjoita ja se riittää aloittamaan tekstikonfiguraatioiden käytön. Tämä osoittaa myös, että käännösajan konfiguroinnin monimutkaisuus on hieman pienempi kuin tekstipohjaisten asetusten monimutkaisuus, koska tekstipohjaisessa versiossa tarvitsemme lisäkoodia.)

Keskitetty avainarvojen tallennus on hyvä mekanismi sovellusten metaparametrien jakamiseen. Tässä meidän on mietittävä, mitä pidämme konfiguraatioarvoina ja mikä on vain dataa. Annettu funktio C => A => B kutsumme yleensä harvoin muuttuvia arvoja C "kokoonpano", kun taas tietoja vaihdetaan usein A - syötä vain tiedot. Toiminnolle tulee antaa konfiguraatio aikaisemmin kuin tiedot A. Tämän idean perusteella voimme sanoa, että se on odotettu muutostiheys, jota voidaan käyttää erottamaan konfigurointitiedot pelkistä tiedoista. Myös tiedot tulevat tyypillisesti yhdestä lähteestä (käyttäjä) ja määritykset tulevat eri lähteestä (admin). Alustusprosessin jälkeen muutettavissa olevien parametrien käsittely lisää sovelluksen monimutkaisuutta. Tällaisten parametrien kohdalla meidän on hoidettava niiden toimitusmekanismi, jäsentäminen ja validointi sekä väärien arvojen käsittely. Siksi ohjelman monimutkaisuuden vähentämiseksi meidän on parempi vähentää niiden parametrien määrää, jotka voivat muuttua ajon aikana (tai jopa poistaa ne kokonaan).

Tämän postauksen näkökulmasta meidän pitäisi tehdä ero staattisten ja dynaamisten parametrien välillä. Jos palvelulogiikka vaatii joidenkin parametrien harvoin muuttamista ajon aikana, voimme kutsua niitä dynaamisiksi parametreiksi. Muuten ne ovat staattisia ja ne voidaan konfiguroida ehdotetun lähestymistavan avulla. Dynaamiseen uudelleenkonfigurointiin saatetaan tarvita muita lähestymistapoja. Esimerkiksi järjestelmän osia voidaan käynnistää uudelleen uusilla konfigurointiparametreilla samalla tavalla kuin hajautetun järjestelmän erillisten prosessien uudelleenkäynnistys.
(Nöyrä mielipiteeni on välttää ajonaikaista uudelleenkonfigurointia, koska se lisää järjestelmän monimutkaisuutta.
Saattaa olla yksinkertaisempaa luottaa vain käyttöjärjestelmän tukeen prosessien uudelleenkäynnistämisessä. Aina se ei kuitenkaan välttämättä ole mahdollista.)

Yksi tärkeä näkökohta staattisen konfiguroinnin käytössä, joka saa ihmiset joskus harkitsemaan dynaamista konfigurointia (ilman muita syitä), on palvelun seisokki kokoonpanopäivityksen aikana. Todellakin, jos meidän on tehtävä muutoksia staattiseen kokoonpanoon, meidän on käynnistettävä järjestelmä uudelleen, jotta uudet arvot tulevat voimaan. Vaatimukset seisokkeille vaihtelevat eri järjestelmissä, joten se ei välttämättä ole niin kriittistä. Jos se on kriittinen, meidän on suunniteltava etukäteen kaikki järjestelmän uudelleenkäynnistykset. Voisimme esimerkiksi toteuttaa AWS ELB liitännän tyhjennys. Tässä skenaariossa aina kun meidän on käynnistettävä järjestelmä uudelleen, käynnistämme järjestelmän uuden ilmentymän rinnakkain, vaihdamme sitten ELB:n siihen samalla, kun annamme vanhan järjestelmän suorittaa valmiiksi olemassa olevien yhteyksien huolto.

Entä konfiguroinnin pitäminen versioidussa artefaktissa tai sen ulkopuolella? Konfiguroinnin säilyttäminen artefaktissa tarkoittaa useimmissa tapauksissa, että tämä kokoonpano on läpäissyt saman laadunvarmistusprosessin kuin muut artefaktit. Joten voi olla varma, että kokoonpano on laadukas ja luotettava. Päinvastoin konfigurointi erillisessä tiedostossa tarkoittaa, että ei ole jälkiä siitä, kuka ja miksi on tehnyt muutoksia kyseiseen tiedostoon. Onko tämä tärkeää? Uskomme, että useimmissa tuotantojärjestelmissä on parempi olla vakaa ja laadukas kokoonpano.

Artefaktin versio mahdollistaa sen, että saat selville, milloin se luotiin, mitä arvoja se sisältää, mitkä ominaisuudet ovat käytössä/pois käytöstä, kuka oli vastuussa kunkin kokoonpanon muutoksen tekemisestä. Saattaa vaatia vaivaa, jotta kokoonpano pysyy artefaktin sisällä, ja se on suunnittelun valinta.

Plussat miinukset

Tässä haluamme korostaa joitakin ehdotetun lähestymistavan etuja ja keskustella joistakin haitoista.

edut

Täydellisen hajautetun järjestelmän käännettävän kokoonpanon ominaisuudet:

  1. Konfiguroinnin staattinen tarkistus. Tämä antaa korkean tason varmuuden siitä, että konfiguraatio on oikea tyyppirajoitusten mukaan.
  2. Rikas asetuskieli. Tyypillisesti muut konfigurointilähestymistavat rajoittuvat korkeintaan vaihteleviin substituutioihin.
    Scalaa käyttämällä voidaan käyttää monenlaisia ​​kieliominaisuuksia konfiguroinnin parantamiseksi. Esimerkiksi voimme käyttää ominaisuuksia tarjota oletusarvoja, esineitä asettaa eri laajuus, voimme viitata vals määritellään vain kerran ulkoisessa laajuudessa (DRY). On mahdollista käyttää kirjaimellisia sekvenssejä tai tiettyjen luokkien esiintymiä (Seq, Map, Jne.).
  3. DSL. Scalalla on kunnollinen tuki DSL-kirjoittajille. Näillä ominaisuuksilla voidaan luoda konfigurointikieli, joka on helpompi ja loppukäyttäjäystävällisempi, jotta lopullinen konfiguraatio on ainakin verkkotunnuksen käyttäjien luettavissa.
  4. Eheys ja johdonmukaisuus solmujen välillä. Yksi koko hajautetun järjestelmän konfiguroinnin eduista on se, että kaikki arvot määritellään tiukasti kerran ja niitä käytetään sitten uudelleen kaikissa paikoissa, joissa niitä tarvitaan. Myös kirjoita safe port -määritykset varmistavat, että kaikissa mahdollisissa oikeissa kokoonpanoissa järjestelmän solmut puhuvat samaa kieltä. Solmujen välillä on selkeitä riippuvuuksia, mikä vaikeuttaa joidenkin palvelujen tarjoamisen unohtamista.
  5. Muutosten korkea laatu. Kokonaisvaltainen lähestymistapa konfiguraatiomuutosten läpiviemiseen normaalin PR-prosessin kautta asettaa korkeat laatuvaatimukset myös konfiguroinnissa.
  6. Samanaikaiset konfiguraatiomuutokset. Aina kun teemme muutoksia kokoonpanoon, automaattinen käyttöönotto varmistaa, että kaikki solmut päivitetään.
  7. Sovelluksen yksinkertaistaminen. Sovelluksen ei tarvitse jäsentää ja vahvistaa määritystä eikä käsitellä vääriä määritysarvoja. Tämä yksinkertaistaa yleistä sovellusta. (Jonkin verran monimutkaisuutta on lisätty itse kokoonpanossa, mutta se on tietoinen kompromissi turvallisuutta kohtaan.) On melko yksinkertaista palata tavalliseen kokoonpanoon - lisää vain puuttuvat osat. On helpompi aloittaa käännetty konfiguraatio ja lykätä lisäosien käyttöönottoa myöhempään aikaan.
  8. Versioitu kokoonpano. Koska konfiguraatiomuutokset noudattavat samaa kehitysprosessia, tuloksena saamme artefaktin ainutlaatuisella versiolla. Sen avulla voimme vaihtaa kokoonpanoa takaisin tarvittaessa. Voimme jopa ottaa käyttöön vuosi sitten käytetyn kokoonpanon ja se toimii täsmälleen samalla tavalla. Vakaa konfiguraatio parantaa hajautetun järjestelmän ennustettavuutta ja luotettavuutta. Kokoonpano on kiinteä käännöshetkellä, eikä sitä voida helposti peukaloida tuotantojärjestelmässä.
  9. Modulaarisuus. Ehdotettu kehys on modulaarinen ja moduuleja voidaan yhdistää eri tavoin
    tukee erilaisia ​​kokoonpanoja (asetukset/asettelut). Erityisesti on mahdollista käyttää pienimuotoista yhden solmun asettelua ja suuren mittakaavan monisolmuasetusta. On järkevää käyttää useita tuotantoasetelmia.
  10. Testaus. Testaustarkoituksiin voidaan toteuttaa valepalvelu ja käyttää sitä riippuvuutena tyyppiturvallisella tavalla. Muutamia erilaisia ​​testausasetteluja, joissa useat osat on korvattu pilkoilla, voitaisiin ylläpitää samanaikaisesti.
  11. Integraatiotestaus. Joskus hajautetuissa järjestelmissä integraatiotestien suorittaminen on vaikeaa. Käyttämällä kuvattua lähestymistapaa täydellisen hajautetun järjestelmän turvallisen konfiguroinnin kirjoittamiseen, voimme ajaa kaikkia hajautettuja osia yhdellä palvelimella ohjatulla tavalla. Tilannetta on helppo jäljitellä
    kun jokin palveluista ei ole käytettävissä.

Haitat

Käytetty konfigurointitapa eroaa "normaalista" konfiguraatiosta, eikä se välttämättä sovi kaikkiin tarpeisiin. Tässä on joitain käännetyn konfiguraation haittoja:

  1. Staattinen kokoonpano. Se ei ehkä sovellu kaikkiin sovelluksiin. Joissakin tapauksissa konfiguraatio on tarpeen korjata nopeasti tuotannossa ohittaen kaikki turvatoimenpiteet. Tämä lähestymistapa tekee siitä vaikeampaa. Kääntäminen ja uudelleensijoittaminen vaaditaan, kun kokoonpanoon on tehty muutoksia. Tämä on sekä ominaisuus että taakka.
  2. Kokoonpanon luominen. Kun jokin automaatiotyökalu luo konfiguraation, tämä lähestymistapa vaatii myöhemmän käännöksen (joka puolestaan ​​voi epäonnistua). Tämän lisävaiheen integroiminen rakennusjärjestelmään saattaa vaatia lisäponnistuksia.
  3. Instrumentit. Nykyään käytössä on paljon työkaluja, jotka perustuvat tekstipohjaisiin määrityksiin. Jotkut heistä
    ei ole käytettävissä, kun kokoonpano käännetään.
  4. Ajattelutavan muutosta tarvitaan. Kehittäjät ja DevOps tuntevat tekstimääritystiedostot. Ajatus kokoonpanon kokoamisesta saattaa vaikuttaa heistä oudolta.
  5. Ennen käännettävän kokoonpanon käyttöönottoa vaaditaan korkealaatuinen ohjelmistokehitysprosessi.

Toteutetulla esimerkillä on joitain rajoituksia:

  1. Jos tarjoamme lisämäärityksiä, joita solmun toteutus ei vaadi, kääntäjä ei auta meitä havaitsemaan puuttuvaa toteutusta. Tämä voitaisiin ratkaista käyttämällä HList tai ADT:t (tapausluokat) solmukonfiguroinnille ominaisuuksien ja kakkukuvion sijaan.
  2. Meidän on annettava konfiguraatiotiedostossa jonkinlainen kattila: (package, import, object ilmoitukset;
    override def's parametreille, joilla on oletusarvot). Tämä voidaan osittain korjata DSL:n avulla.
  3. Tässä viestissä emme kata samankaltaisten solmujen klustereiden dynaamista uudelleenkonfigurointia.

Yhteenveto

Tässä viestissä olemme keskustelleet ideasta esittää konfiguraatio suoraan lähdekoodissa tyyppiturvallisella tavalla. Lähestymistapaa voitaisiin käyttää monissa sovelluksissa korvaamaan xml- ja muut tekstipohjaiset asetukset. Huolimatta siitä, että esimerkkimme on toteutettu Scalassa, se voidaan kääntää myös muille käännettäville kielille (kuten Kotlin, C#, Swift jne.). Tätä lähestymistapaa voisi kokeilla uudessa projektissa ja, jos se ei sovi hyvin, siirtyä vanhaan tapaan.

Tietenkin käännettävä konfiguraatio vaatii korkealaatuista kehitysprosessia. Vastineeksi se lupaa tarjota yhtä korkealaatuisen vankan kokoonpanon.

Tätä lähestymistapaa voidaan laajentaa useilla tavoilla:

  1. Voidaan käyttää makroja konfiguraatiotarkistuksen suorittamiseen ja epäonnistua käännöshetkellä, jos liiketoimintalogiikan rajoitukset epäonnistuvat.
  2. DSL voidaan toteuttaa edustamaan konfiguraatiota verkkotunnuksen käyttäjäystävällisellä tavalla.
  3. Dynaaminen resurssien hallinta automaattisilla konfiguraatiosäädöillä. Esimerkiksi kun säädämme klusterisolmujen määrää, saatamme haluta (1) solmujen saavan hieman muokatun konfiguraation; (2) Cluster Manager vastaanottaa uusia solmutietoja.

Kiitos

Haluan kiittää Andrey Saksonovia, Pavel Popovia ja Anton Nehaevia inspiroivan palautteen antamisesta tämän postauksen luonnokselle, joka auttoi minua selventämään sitä.

Lähde: will.com