Käännetty hajautetun järjestelmän kokoonpano

Haluaisin kertoa sinulle yhden mielenkiintoisen mekanismin hajautetun järjestelmän kokoonpanon kanssa työskentelemiseen. Kokoonpano esitetään suoraan käännetyllä kielellä (Scala) käyttämällä turvallisia tyyppejä. Tämä viesti tarjoaa esimerkin tällaisesta kokoonpanosta ja käsittelee eri näkökohtia käännetyn kokoonpanon käyttöönotosta yleiseen kehitysprosessiin.

Käännetty hajautetun järjestelmän kokoonpano

(englanti)

Esittely

Luotettavan hajautetun järjestelmän rakentaminen tarkoittaa, että kaikki solmut käyttävät oikeaa konfiguraatiota, joka on synkronoitu muiden solmujen kanssa. DevOps-tekniikoita (terraform, ansible tai jotain vastaavaa) käytetään yleensä konfigurointitiedostojen automaattiseen luomiseen (usein jokaiselle solmukohtaiselle). Haluamme myös olla varmoja, että kaikki kommunikoivat solmut käyttävät identtisiä protokollia (mukaan lukien sama versio). Muuten hajautettuun järjestelmäämme rakennetaan yhteensopimattomuus. JVM-maailmassa yksi seuraus tästä vaatimuksesta on, että kaikkialla on käytettävä samaa versiota protokollaviestit sisältävästä kirjastosta.

Entä hajautetun järjestelmän testaus? Tietenkin oletamme, että kaikilla komponenteilla on yksikkötestit ennen kuin siirrymme integraatiotestaukseen. (Jotta voimme ekstrapoloida testitulokset ajon aikana, meidän on myös tarjottava identtinen joukko kirjastoja testausvaiheessa ja ajon aikana.)

Integrointitestejä käytettäessä on usein helpompi käyttää samaa luokkapolkua kaikkialla kaikissa solmuissa. Meidän tarvitsee vain varmistaa, että samaa luokkapolkua käytetään suorituksen aikana. (Vaikka on täysin mahdollista ajaa eri solmuja eri luokkapoluilla, tämä lisää monimutkaisuutta yleiseen kokoonpanoon ja vaikeuttaa käyttöönotto- ja integrointitestejä.) Tämän viestin tarkoituksia varten oletamme, että kaikki solmut käyttävät samaa luokkapolkua.

Kokoonpano kehittyy sovelluksen mukana. Käytämme versioita tunnistaaksemme ohjelman kehitysvaiheet. Tuntuu loogiselta tunnistaa myös eri kokoonpanojen versioita. Ja aseta itse kokoonpano versionhallintajärjestelmään. Jos tuotannossa on vain yksi kokoonpano, voimme käyttää vain versionumeroa. Jos käytämme useita tuotantoesiintymiä, tarvitsemme useita
konfiguraatiohaarat ja lisätarra version lisäksi (esimerkiksi haaran nimi). Näin voimme selvästi tunnistaa tarkan kokoonpanon. Jokainen kokoonpanotunniste vastaa yksilöllisesti hajautettujen solmujen, porttien, ulkoisten resurssien ja kirjastoversioiden tiettyä yhdistelmää. Tätä viestiä varten oletetaan, että on vain yksi haara ja voimme tunnistaa kokoonpanon tavalliseen tapaan käyttämällä kolmea pisteellä erotettua numeroa (1.2.3).

Nykyaikaisissa ympäristöissä määritystiedostoja luodaan harvoin manuaalisesti. Useammin ne luodaan käyttöönoton aikana, eikä niihin enää kosketa (niin älä riko mitään). Herää luonnollinen kysymys: miksi käytämme edelleen tekstimuotoa asetusten tallentamiseen? Elinkelpoinen vaihtoehto näyttää olevan kyky käyttää tavallista koodia määrityksessä ja hyötyä käännösajan tarkistuksista.

Tässä viestissä tutkimme ajatusta kokoonpanon esittämisestä kootun artefaktin sisällä.

Käytetty kokoonpano

Tässä osassa on esimerkki staattisesta käännetystä kokoonpanosta. Kaksi yksinkertaista palvelua toteutetaan - kaikupalvelu ja kaikupalveluasiakas. Näiden kahden palvelun perusteella kootaan kaksi järjestelmävaihtoehtoa. Yhdessä vaihtoehdossa molemmat palvelut sijaitsevat samassa solmussa, toisessa vaihtoehdossa - eri solmuissa.

Tyypillisesti hajautettu järjestelmä sisältää useita solmuja. Voit tunnistaa solmut käyttämällä jonkin tyyppisiä arvoja NodeId:

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

tai

case class NodeId(hostName: String)

tai

object Singleton
type NodeId = Singleton.type

Solmut suorittavat erilaisia ​​rooleja, ne suorittavat palveluita ja niiden välille voidaan muodostaa TCP/HTTP-yhteyksiä.

TCP-yhteyden kuvaamiseksi tarvitsemme ainakin portin numeron. Haluamme myös ottaa huomioon kyseisen portin tukeman protokollan varmistaaksemme, että sekä asiakas että palvelin käyttävät samaa protokollaa. Kuvaamme yhteyttä käyttämällä seuraavaa luokkaa:

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

missä Port - vain kokonaisluku Int ilmaisee hyväksyttävien arvojen alueen:

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

Hienostuneita tyyppejä

Katso kirjasto puhdistettu и minun raportti. Lyhyesti sanottuna kirjaston avulla voit lisätä rajoituksia tyyppeihin, jotka tarkistetaan käännöshetkellä. Tässä tapauksessa kelvolliset porttinumerot ovat 16-bittisiä kokonaislukuja. Tarkennetun kirjaston käyttö ei ole pakollista käännetyssä kokoonpanossa, mutta se parantaa kääntäjän kykyä tarkistaa kokoonpano.

HTTP (REST) ​​-protokollia varten saatamme tarvita portin numeron lisäksi myös polun palveluun:

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

Phantom-tyypit

Protokollan tunnistamiseksi käännöshetkellä käytämme tyyppiparametria, jota ei käytetä luokassa. Tämä päätös johtuu siitä, että emme käytä protokolla-instanssia ajon aikana, mutta haluaisimme kääntäjän tarkistavan protokollien yhteensopivuuden. Protokollaa määrittämällä emme voi välittää sopimatonta palvelua riippuvuudeksi.

Yksi yleisimmistä protokollista on REST API Json-serialisoinnilla:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

missä RequestMessage - pyynnön tyyppi, ResponseMessage - vastauksen tyyppi.
Tietenkin voimme käyttää muita protokollakuvauksia, jotka tarjoavat tarvitsemamme kuvauksen tarkkuuden.

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

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Tässä pyyntö on merkkijono, joka on liitetty URL-osoitteeseen, ja vastaus on HTTP-vastauksen rungossa oleva palautettu merkkijono.

Palvelun konfiguraatio kuvataan palvelun nimellä, porteilla ja riippuvuuksilla. Nämä elementit voidaan esittää Scalassa useilla tavoilla (esim. HList-s, algebralliset tietotyypit). Tätä viestiä varten käytämme kakkumallia ja edustamme moduuleja käyttämällä trait'ov. (Kakkumalli ei ole pakollinen osa tätä lähestymistapaa. Se on vain yksi mahdollinen toteutus.)

Palvelujen väliset riippuvuudet voidaan esittää menetelminä, jotka palauttavat portit EndPointmuiden solmujen:

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

Kaikupalvelun luomiseen tarvitset vain portin numeron ja ilmoituksen siitä, että portti tukee echo-protokollaa. Emme ehkä määritä tiettyä porttia, koska... ominaisuuksien avulla voit ilmoittaa menetelmiä ilman toteutusta (abstraktit menetelmät). Tässä tapauksessa konkreettista konfiguraatiota luodessaan kääntäjä vaatisi meitä toimittamaan abstraktin menetelmän toteutuksen ja antamaan portin numeron. Koska olemme toteuttaneet menetelmän, emme välttämättä määritä eri porttia luodessasi tiettyä kokoonpanoa. Oletusarvoa käytetään.

Asiakaskokoonpanossa julistamme riippuvuuden kaikupalvelusta:

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

Riippuvuus on samaa tyyppiä kuin viety palvelu echoService. Erityisesti echo-asiakasohjelmassa vaadimme samaa protokollaa. Siksi, kun yhdistät kaksi palvelua, voimme olla varmoja, että kaikki toimii oikein.

Palvelujen toteuttaminen

Palvelun käynnistämiseen ja pysäyttämiseen tarvitaan toiminto. (Palvelun pysäyttäminen on kriittinen testauksen kannalta.) Taaskin on olemassa useita vaihtoehtoja tällaisen ominaisuuden toteuttamiseen (voimme esimerkiksi käyttää tyyppiluokkia konfiguraatiotyypin perusteella). Tämän postauksen tarkoituksiin käytämme kakkukuviota. Edustamme palvelua luokan avulla cats.Resource, koska Tämä luokka tarjoaa jo keinot taata turvallisesti resurssien vapauttaminen ongelmatilanteissa. Resurssin saamiseksi meidän on annettava kokoonpano ja valmis ajonaikainen konteksti. Palvelun käynnistystoiminto voi 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]
  }

missä

  • Config — tämän palvelun määritystyyppi
  • AddressResolver - ajonaikainen objekti, jonka avulla voit selvittää muiden solmujen osoitteet (katso alla)

ja muita tyyppejä kirjastosta cats:

  • F[_] - vaikutuksen tyyppi (yksinkertaisimmassa tapauksessa F[A] voi olla vain toiminto () => A. Tässä viestissä käytämme cats.IO.)
  • Reader[A,B] - enemmän tai vähemmän synonyymi toiminnolle A => B
  • cats.Resource - resurssi, joka voidaan hankkia ja vapauttaa
  • Timer - ajastin (voit nukahtaa hetkeksi ja mitata aikavälejä)
  • ContextShift - analoginen ExecutionContext
  • Applicative — efektityyppiluokka, jonka avulla voit yhdistää yksittäisiä tehosteita (melkein monadi). Monimutkaisemmissa sovelluksissa sitä näyttää paremmalta käyttää Monad/ConcurrentEffect.

Tämän funktion allekirjoituksen avulla voimme toteuttaa useita 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](()))
  }

(cm. lähdekoodi, jossa toteutetaan muita palveluita - kaikupalvelu, kaiku asiakas
и elinikäiset säätimet.)

Solmu on objekti, joka voi käynnistää useita palveluita (resurssiketjun käynnistämisen varmistaa Cake Pattern):

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 tarkan kokoonpanotyypin, joka vaaditaan tälle solmulle. Jos unohdamme määrittää jonkin tietyn palvelun vaatimista konfiguraatiotyypeistä, tapahtuu käännösvirhe. Emme myöskään voi käynnistää solmua, ellemme toimita jollekin sopivan tyyppiselle objektille kaikkia tarvittavia tietoja.

Isäntänimen resoluutio

Tarvitsemme oikean IP-osoitteen muodostaaksemme yhteyden etäisäntään. On mahdollista, että osoite tulee tunnetuksi myöhemmin kuin muu kokoonpano. Tarvitsemme siis funktion, joka yhdistää solmun tunnuksen osoitteeseen:

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

On olemassa useita tapoja toteuttaa tämä toiminto:

  1. Jos osoitteet tulevat tietoomme ennen käyttöönottoa, voimme luoda Scala-koodin
    osoitteet ja suorita sitten koontiversio. Tämä kokoaa ja suorittaa testejä.
    Tässä tapauksessa funktio tunnetaan staattisesti ja se voidaan esittää koodissa kartoituksena Map[NodeId, NodeAddress].
  2. Joissakin tapauksissa todellinen osoite tiedetään vasta solmun käynnistyttyä.
    Tässä tapauksessa voimme toteuttaa "etsintäpalvelun", joka suoritetaan ennen muita solmuja ja kaikki solmut rekisteröityvät tähän palveluun ja pyytävät muiden solmujen osoitteita.
  3. Jos voimme muuttaa /etc/hosts, voit käyttää ennalta määritettyjä isäntänimiä (esim my-project-main-node и echo-backend) ja linkitä nämä nimet
    IP-osoitteilla käyttöönoton aikana.

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

Seuraavaksi tarkastelemme kahta vaihtoehtoa hajautetulle järjestelmälle:

  1. Kaikkien palveluiden sijoittaminen yhteen solmuun.
  2. Ja kaikupalvelun ja kaikuasiakkaan isännöiminen eri solmuissa.

Kokoonpano kohteelle yksi solmu:

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

Objekti toteuttaa sekä asiakkaan että palvelimen konfiguroinnin. Time-to-live -konfiguraatiota käytetään myös siten, että intervallin jälkeen lifetime lopettaa ohjelman. (Ctrl-C toimii myös ja vapauttaa kaikki resurssit oikein.)

Samaa joukkoa konfigurointi- ja toteutusominaisuuksia voidaan käyttää luomaan järjestelmä, joka koostuu kaksi erillistä solmua:

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

Tärkeä! Huomaa, miten palvelut on linkitetty. Määritämme yhden solmun toteuttaman palvelun toisen solmun riippuvuusmenetelmän toteutuksena. Kääntäjä tarkistaa riippuvuustyypin, koska sisältää protokollatyypin. Ajettaessa riippuvuus sisältää oikean kohdesolmun tunnuksen. Tämän järjestelmän ansiosta määritämme portin numeron täsmälleen kerran ja taataan, että viittaamme aina oikeaan porttiin.

Kahden järjestelmäsolmun toteutus

Tässä kokoonpanossa käytämme samoja palvelutoteutuksia ilman muutoksia. Ainoa ero on, että meillä on nyt kaksi objektia, jotka toteuttavat 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 tarvitsee vain palvelimen konfiguroinnin. Toinen solmu toteuttaa asiakkaan ja käyttää kokoonpanon eri osaa. Myös molemmat solmut tarvitsevat eliniän hallinnan. Palvelinsolmu toimii toistaiseksi, kunnes se pysäytetään SIGTERM'om, ja asiakassolmu päättyy jonkin ajan kuluttua. cm. käynnistyssovellus.

Yleinen kehitysprosessi

Katsotaanpa, kuinka tämä konfigurointitapa vaikuttaa yleiseen kehitysprosessiin.

Kokoonpano käännetään yhdessä muun koodin kanssa ja artefakti (.jar) luodaan. Näyttää järkevältä laittaa kokoonpano erilliseen artefakttiin. Tämä johtuu siitä, että meillä voi olla useita samaan koodiin perustuvia määrityksiä. Jälleen on mahdollista luoda artefakteja, jotka vastaavat eri konfiguraatiohaaroja. Tiettyjen kirjastojen versioiden riippuvuudet tallennetaan kokoonpanon mukana, ja nämä versiot tallennetaan ikuisesti aina, kun päätämme ottaa kyseisen version kokoonpanosta käyttöön.

Mikä tahansa asetusmuutos muuttuu koodin muutokseksi. Ja siksi jokainen
muutoksen kattaa normaali laadunvarmistusprosessi:

Lippu vikaseurantaan -> PR -> tarkista -> yhdistä asiaankuuluviin sivuliikkeisiin ->
integrointi -> käyttöönotto

Käännetyn kokoonpanon toteuttamisen tärkeimmät seuraukset ovat:

  1. Kokoonpano on johdonmukainen kaikissa hajautetun järjestelmän solmuissa. Johtuen siitä, että kaikki solmut saavat saman kokoonpanon yhdestä lähteestä.

  2. On ongelmallista muuttaa konfiguraatiota vain yhdessä solmussa. Siksi "kokoonpanon ajautuminen" on epätodennäköistä.

  3. Pienten muutosten tekeminen kokoonpanoon tulee vaikeammaksi.

  4. Suurin osa kokoonpanomuutoksista tapahtuu osana yleistä kehitysprosessia, ja niitä tarkistetaan.

Tarvitsenko erillisen arkiston tuotantokokoonpanon tallentamiseen? Tämä kokoonpano voi sisältää salasanoja ja muita arkaluontoisia tietoja, joihin haluamme rajoittaa pääsyn. Tämän perusteella näyttää järkevältä tallentaa lopullinen konfiguraatio erilliseen arkistoon. Voit jakaa kokoonpanon kahteen osaan, joista toinen sisältää julkisesti käytettävissä olevat kokoonpanoasetukset ja toinen rajoitetut asetukset. Näin useimmat kehittäjät voivat käyttää yleisiä asetuksia. Tämä erottelu on helppo saavuttaa käyttämällä oletusarvoja sisältäviä väliominaisuuksia.

Mahdolliset variaatiot

Yritetään verrata koottua kokoonpanoa joihinkin yleisiin vaihtoehtoihin:

  1. Tekstitiedosto kohdekoneella.
  2. Keskitetty avainarvovarasto (etcd/zookeeper).
  3. Prosessikomponentit, jotka voidaan määrittää uudelleen / käynnistää uudelleen käynnistämättä prosessia uudelleen.
  4. Konfiguroinnin tallentaminen artefaktien ja versionhallinnan ulkopuolelle.

Tekstitiedostot tarjoavat huomattavaa joustavuutta pienten muutosten suhteen. Järjestelmänvalvoja voi kirjautua etäsolmuun, tehdä muutoksia asianmukaisiin tiedostoihin ja käynnistää palvelun uudelleen. Suurissa järjestelmissä tällainen joustavuus ei kuitenkaan välttämättä ole toivottavaa. Muutokset eivät jätä jälkiä muihin järjestelmiin. Kukaan ei arvioi muutoksia. On vaikea määrittää, kuka tarkalleen teki muutokset ja mistä syystä. Muutoksia ei testata. Jos järjestelmä on hajautettu, järjestelmänvalvoja saattaa unohtaa tehdä vastaavan muutoksen muihin solmuihin.

(Huomaa myös, että käännetyn kokoonpanon käyttö ei sulje mahdollisuutta käyttää tekstitiedostoja tulevaisuudessa. Riittää, kun lisäät jäsentimen ja validaattorin, joka tuottaa saman tyypin kuin tulos Config, ja voit käyttää tekstitiedostoja. Tästä seuraa välittömästi, että käännetyn kokoonpanon järjestelmän monimutkaisuus on jonkin verran pienempi kuin tekstitiedostoja käyttävän järjestelmän monimutkaisuus, koska tekstitiedostot vaativat lisäkoodin.)

Keskitetty avainarvovarasto on hyvä mekanismi hajautetun sovelluksen metaparametrien jakamiseen. Meidän on päätettävä, mitkä ovat konfigurointiparametreja ja mitkä vain dataa. Tehdään funktio C => A => B, ja parametrit C harvoin muuttuu, ja tiedot A - usein. Tässä tapauksessa voimme sanoa niin C - konfigurointiparametrit ja A - tiedot. Näyttää siltä, ​​että konfigurointiparametrit eroavat tiedoista siinä, että ne yleensä muuttuvat harvemmin kuin tiedot. Lisäksi tiedot tulevat yleensä yhdestä lähteestä (käyttäjältä) ja konfigurointiparametrit toisesta (järjestelmänvalvojalta).

Jos harvoin muuttuvat parametrit on päivitettävä käynnistämättä ohjelmaa uudelleen, tämä voi usein johtaa ohjelman monimutkaisuuteen, koska joudumme jotenkin toimittamaan parametreja, tallentamaan, jäsentämään ja tarkistamaan sekä käsittelemään vääriä arvoja. Siksi ohjelman monimutkaisuuden vähentämisen kannalta on järkevää vähentää parametrien määrää, jotka voivat muuttua ohjelman toiminnan aikana (tai eivät tue tällaisia ​​parametreja ollenkaan).

Tätä viestiä varten teemme eron staattisten ja dynaamisten parametrien välillä. Jos palvelun logiikka vaatii parametrien muuttamista ohjelman toiminnan aikana, kutsumme sellaisia ​​parametreja dynaamiksi. Muuten vaihtoehdot ovat staattisia ja ne voidaan konfiguroida käännetyn kokoonpanon avulla. Dynaamista uudelleenmäärittelyä varten saatamme tarvita mekanismin ohjelman osien käynnistämiseksi uudelleen uusilla parametreilla samalla tavalla kuin käyttöjärjestelmän prosessit käynnistetään uudelleen. (Mielestämme on suositeltavaa välttää reaaliaikaista uudelleenkonfigurointia, koska se lisää järjestelmän monimutkaisuutta. Jos mahdollista, on parempi käyttää käyttöjärjestelmän vakioominaisuuksia prosessien uudelleenkäynnistämiseen.)

Yksi tärkeä näkökohta staattisen määrityksen käyttämisessä, joka saa ihmiset harkitsemaan dynaamista uudelleenmääritystä, on aika, joka kuluu järjestelmän uudelleenkäynnistykseen kokoonpanopäivityksen jälkeen (seisokki). Itse asiassa, jos meidän on tehtävä muutoksia staattiseen kokoonpanoon, meidän on käynnistettävä järjestelmä uudelleen, jotta uudet arvot tulevat voimaan. Katkosaikaongelman vakavuus vaihtelee eri järjestelmissä. Joissakin tapauksissa voit ajoittaa uudelleenkäynnistyksen ajankohtaan, jolloin kuormitus on vähäistä. Jos tarvitset jatkuvaa palvelua, voit toteuttaa AWS ELB liitännän tyhjennys. Samanaikaisesti, kun meidän on käynnistettävä järjestelmä uudelleen, käynnistämme tämän järjestelmän rinnakkaisinstanssin, kytkemme tasapainottimen siihen ja odotamme, että vanhat yhteydet valmistuvat. Kun kaikki vanhat yhteydet on katkaistu, suljemme järjestelmän vanhan esiintymän.

Tarkastellaan nyt kysymystä kokoonpanon tallentamisesta artefaktin sisään tai ulkopuolelle. Jos tallennamme konfiguraation artefaktin sisään, meillä oli ainakin mahdollisuus varmistaa konfiguraation oikeellisuus artefaktin kokoonpanon aikana. Jos kokoonpano on valvotun artefaktin ulkopuolella, on vaikea seurata, kuka teki muutoksia tähän tiedostoon ja miksi. Kuinka tärkeää se on? Mielestämme monille tuotantojärjestelmille on tärkeää saada vakaa ja laadukas kokoonpano.

Artefaktin version avulla voit määrittää, milloin se on luotu, mitä arvoja se sisältää, mitkä toiminnot ovat käytössä/pois käytöstä ja kuka on vastuussa kokoonpanon muutoksista. Tietenkin kokoonpanon tallentaminen artefakttiin vaatii jonkin verran vaivaa, joten sinun on tehtävä tietoinen päätös.

Hyödyt ja haitat

Haluaisin tarkastella ehdotetun tekniikan etuja ja haittoja.

Edut

Alla on luettelo käännetyn hajautetun järjestelmän kokoonpanon pääominaisuuksista:

  1. Staattisen konfiguraation tarkistus. Antaa sinun olla varma siitä
    kokoonpano on oikea.
  2. Rikas asetuskieli. Tyypillisesti muut konfigurointimenetelmät rajoittuvat enintään merkkijonomuuttujien korvaamiseen. Kun käytät Scalaa, käytettävissä on laaja valikoima kieliominaisuuksia kokoonpanosi parantamiseksi. Voimme käyttää esimerkiksi
    oletusarvojen piirteitä käyttämällä objekteja parametrien ryhmittelyyn, voimme viitata vain kerran ilmoitettuihin arvoihin (DRY) sulkevassa laajuudessa. Voit luoda minkä tahansa luokat suoraan kokoonpanon sisällä (Seq, Map, mukautetut luokat).
  3. DSL. Scalassa on useita kieliominaisuuksia, jotka helpottavat DSL:n luomista. Näitä ominaisuuksia on mahdollista hyödyntää ja toteuttaa kohdekäyttäjäryhmälle sopiva konfigurointikieli siten, että konfiguraatio on ainakin toimialueen asiantuntijoiden luettavissa. Asiantuntijat voivat osallistua esimerkiksi kokoonpanon tarkistusprosessiin.
  4. Eheys ja synkronointi solmujen välillä. Yksi koko hajautetun järjestelmän kokoonpanon yhteen pisteeseen tallentamisen eduista on se, että kaikki arvot ilmoitetaan täsmälleen kerran ja niitä käytetään sitten uudelleen missä tahansa niitä tarvitaan. Phantom-tyyppien käyttäminen porttien ilmoittamiseen varmistaa, että solmut käyttävät yhteensopivia protokollia kaikissa oikeissa järjestelmäkokoonpanoissa. Selkeät pakolliset riippuvuudet solmujen välillä takaavat, että kaikki palvelut ovat yhteydessä toisiinsa.
  5. Laadukkaat muutokset. Muutosten tekeminen konfiguraatioon yhteisellä kehitysprosessilla mahdollistaa korkean laatutason saavuttamisen myös konfiguraatiolle.
  6. Samanaikainen asetusten päivitys. Automaattinen järjestelmän käyttöönotto kokoonpanomuutosten jälkeen varmistaa, että kaikki solmut päivitetään.
  7. Sovelluksen yksinkertaistaminen. Sovellus ei tarvitse jäsentämistä, asetusten tarkistusta tai väärien arvojen käsittelyä. Tämä vähentää sovelluksen monimutkaisuutta. (Osa esimerkissämme havaitusta konfiguroinnin monimutkaisuudesta ei ole käännetyn konfiguraation attribuutti, vaan vain tietoinen päätös, jonka taustalla on halu tarjota suurempaa tyyppiturvallisuutta.) On melko helppoa palata tavanomaiseen kokoonpanoon - vain toteuttaa puuttuva osat. Siksi voit esimerkiksi aloittaa käännetyllä kokoonpanolla lykkäämällä tarpeettomien osien toteuttamista siihen hetkeen, jolloin sitä todella tarvitaan.
  8. Vahvistettu kokoonpano. Koska konfiguraatiomuutokset seuraavat kaikkien muiden muutosten tavallista kohtaloa, saamamme tulos on artefakti, jolla on ainutlaatuinen versio. Näin voimme esimerkiksi palata tarvittaessa kokoonpanon aikaisempaan versioon. Voimme jopa käyttää vuoden takaista kokoonpanoa ja järjestelmä toimii täsmälleen samoin. Vakaa kokoonpano parantaa hajautetun järjestelmän ennustettavuutta ja luotettavuutta. Koska kokoonpano on kiinteä käännösvaiheessa, sitä on melko vaikea väärentää tuotannossa.
  9. Modulaarisuus. Ehdotettu viitekehys on modulaarinen ja moduuleita voidaan yhdistää eri tavoin erilaisten järjestelmien luomiseksi. Erityisesti voit määrittää järjestelmän toimimaan yhdessä solmussa yhdessä suoritusmuodossa ja useissa solmuissa toisessa. Voit luoda useita kokoonpanoja järjestelmän tuotantoesiintymille.
  10. Testaus. Korvaamalla yksittäisiä palveluita valeobjekteilla saat järjestelmästä useita testaukseen sopivia versioita.
  11. Integraatiotestaus. Koko hajautetun järjestelmän yksi konfiguraatio mahdollistaa kaikkien komponenttien ajamisen valvotussa ympäristössä osana integraatiotestausta. On helppo jäljitellä esimerkiksi tilannetta, jossa jotkut solmut tulevat saataville.

Haitat ja rajoitukset

Käännetty kokoonpano eroaa muista konfigurointimenetelmistä, eikä se välttämättä sovellu joihinkin sovelluksiin. Alla on joitain haittoja:

  1. Staattinen kokoonpano. Joskus sinun on korjattava nopeasti tuotannon kokoonpano ohittamalla kaikki suojamekanismit. Tällä lähestymistavalla se voi olla vaikeampaa. Ainakin kokoamista ja automaattista käyttöönottoa tarvitaan edelleen. Tämä on sekä menetelmän hyödyllinen ominaisuus että joissakin tapauksissa haitta.
  2. Kokoonpanon luominen. Jos määritystiedosto on luotu automaattisella työkalulla, koontikomentosarjan integrointi saattaa vaatia lisätoimia.
  3. Työkalut. Tällä hetkellä konfiguroinnin kanssa toimimaan suunnitellut apuohjelmat ja tekniikat perustuvat tekstitiedostoihin. Kaikki tällaiset apuohjelmat/tekniikat eivät ole saatavilla käännetyssä kokoonpanossa.
  4. Tarvitaan asennemuutosta. Kehittäjät ja DevOps ovat tottuneet tekstitiedostoihin. Ajatus kokoonpanon kokoamisesta voi olla hieman odottamaton ja epätavallinen ja aiheuttaa hylkäämisen.
  5. Vaaditaan korkealaatuista kehitysprosessia. Käännettyjen kokoonpanojen käyttäminen mukavasti edellyttää sovelluksen (CI/CD) rakennus- ja käyttöönottoprosessin täydellistä automatisointia. Muuten se on melko epämukavaa.

Tarkastelkaamme myös useita tarkasteltavan esimerkin rajoituksia, jotka eivät liity käännetyn kokoonpanon ideaan:

  1. Jos annamme tarpeettomia konfigurointitietoja, joita solmu ei käytä, kääntäjä ei auta meitä havaitsemaan puuttuvaa toteutusta. Tämä ongelma voidaan ratkaista luopumalla kakkukuviosta ja käyttämällä jäykempiä tyyppejä, esim. HList tai algebrallisia tietotyyppejä (tapausluokkia) edustamaan konfiguraatiota.
  2. Asetustiedostossa on rivejä, jotka eivät liity itse kokoonpanoon: (package, import,objektiilmoitukset; override def's parametreille, joilla on oletusarvot). Tämä voidaan osittain välttää, jos otat käyttöön oman DSL:n. Lisäksi muun tyyppiset konfiguraatiot (esimerkiksi XML) asettavat myös tiettyjä rajoituksia tiedostorakenteelle.
  3. Tätä viestiä varten emme harkitse samankaltaisten solmujen klusterin dynaamista uudelleenkonfigurointia.

Johtopäätös

Tässä viestissä tutkimme ajatusta konfiguroinnin esittämisestä lähdekoodissa käyttämällä Scala-tyyppisen järjestelmän edistyneitä ominaisuuksia. Tätä lähestymistapaa voidaan käyttää useissa sovelluksissa korvaamaan perinteiset xml- tai tekstitiedostoihin perustuvat konfigurointimenetelmät. Vaikka esimerkkimme on toteutettu Scalassa, samat ideat voidaan siirtää muille käännetyille kielille (kuten Kotlin, C#, Swift, ...). Voit kokeilla tätä lähestymistapaa jossakin seuraavista projekteista, ja jos se ei toimi, siirry tekstitiedostoon ja lisää puuttuvat osat.

Luonnollisesti koottu konfiguraatio vaatii korkealaatuisen kehitysprosessin. Vastineeksi taataan kokoonpanojen korkea laatu ja luotettavuus.

Harkittua lähestymistapaa voidaan laajentaa:

  1. Voit käyttää makroja käännösajan tarkistamiseen.
  2. Voit ottaa käyttöön DSL:n esitelläksesi kokoonpanon tavalla, joka on loppukäyttäjien saatavilla.
  3. Voit toteuttaa dynaamisen resurssienhallinnan automaattisella konfiguraatiosäädöllä. Esimerkiksi klusterin solmujen lukumäärän muuttaminen edellyttää, että (1) jokainen solmu saa hieman erilaisen konfiguraation; (2) klusterin johtaja sai tietoa uusista solmuista.

Kiitokset

Haluan kiittää Andrei Saksonovia, Pavel Popovia ja Anton Nekhaevia artikkeliluonnoksen rakentavasta kritiikistä.

Lähde: will.com

Lisää kommentti