Összeállított elosztott rendszerkonfiguráció

Szeretnék elmondani egy érdekes mechanizmust az elosztott rendszer konfigurációjával való munkavégzéshez. A konfiguráció közvetlenül egy lefordított nyelven (Scala) van ábrázolva biztonságos típusok használatával. Ez a bejegyzés egy ilyen konfiguráció példáját mutatja be, és a lefordított konfigurációk általános fejlesztési folyamatba való beépítésének különböző szempontjait tárgyalja.

Összeállított elosztott rendszerkonfiguráció

(angol)

Bevezetés

A megbízható elosztott rendszer felépítése azt jelenti, hogy minden csomópont a megfelelő konfigurációt használja, szinkronizálva más csomópontokkal. A DevOps technológiákat (terraform, ansible vagy valami hasonló) általában a konfigurációs fájlok automatikus generálására használják (gyakran mindegyik csomópontra jellemző). Biztosak akarunk lenni abban is, hogy minden kommunikáló csomópont azonos protokollt használ (beleértve ugyanazt a verziót is). Ellenkező esetben az inkompatibilitás beépül az elosztott rendszerünkbe. A JVM világában ennek a követelménynek az egyik következménye, hogy a protokollüzeneteket tartalmazó könyvtár ugyanazt a verzióját kell mindenhol használni.

Mi a helyzet egy elosztott rendszer tesztelésével? Természetesen feltételezzük, hogy minden komponens rendelkezik egységteszttel, mielőtt továbblépnénk az integrációs tesztelésre. (Ahhoz, hogy a teszteredményeket futási időre extrapolálhassuk, a tesztelési szakaszban és a futásidőben is biztosítanunk kell egy azonos könyvtárkészletet.)

Ha integrációs tesztekkel dolgozik, gyakran könnyebb ugyanazt az osztályútvonalat használni mindenhol, minden csomóponton. Csak annyit kell tennünk, hogy ugyanazt az osztályútvonalat használjuk futás közben. (Bár teljesen lehetséges különböző csomópontok futtatása különböző osztályútvonalakkal, ez bonyolultabbá teszi az általános konfigurációt, és nehézségeket okoz a telepítési és integrációs teszteknél.) A bejegyzés szempontjából feltételezzük, hogy minden csomópont ugyanazt az osztályútvonalat fogja használni.

A konfiguráció az alkalmazással együtt fejlődik. Verziókat használunk a programfejlődés különböző szakaszainak azonosítására. Logikusnak tűnik a konfigurációk különböző verzióinak azonosítása is. És helyezze magát a konfigurációt a verziókezelő rendszerbe. Ha csak egy konfiguráció van a termelésben, akkor egyszerűen használhatjuk a verziószámot. Ha sok éles példányt használunk, akkor többre lesz szükségünk
konfigurációs ágak és egy további címke a verzió mellett (például az ág neve). Így egyértelműen azonosítani tudjuk a pontos konfigurációt. Minden konfigurációs azonosító egyedileg megfelel az elosztott csomópontok, portok, külső erőforrások és könyvtárverziók meghatározott kombinációjának. Ebben a bejegyzésben feltételezzük, hogy csak egy elágazás létezik, és a konfigurációt a szokásos módon három, ponttal elválasztott számmal azonosíthatjuk (1.2.3).

A modern környezetben a konfigurációs fájlok ritkán jönnek létre manuálisan. Gyakrabban a telepítés során jönnek létre, és többé nem érintik őket (így ne törj el semmit). Felmerül a természetes kérdés: miért használunk még mindig szöveges formátumot a konfiguráció tárolására? Egy életképes alternatíva a rendszeres kód használata a konfigurációhoz, és a fordítási idő ellenőrzése.

Ebben a bejegyzésben megvizsgáljuk egy összeállított műterméken belüli konfiguráció ábrázolásának ötletét.

Összeállított konfiguráció

Ez a szakasz statikusan lefordított konfigurációra mutat példát. Két egyszerű szolgáltatás van megvalósítva - az echo szolgáltatás és az echo szolgáltatás kliens. E két szolgáltatás alapján két rendszeropció kerül összeállításra. Az egyik lehetőség szerint mindkét szolgáltatás ugyanazon a csomóponton található, egy másik opcióban - különböző csomópontokon.

Egy elosztott rendszer általában több csomópontot tartalmaz. A csomópontokat bizonyos típusú értékek segítségével azonosíthatja NodeId:

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

vagy

case class NodeId(hostName: String)

vagy

object Singleton
type NodeId = Singleton.type

A csomópontok különféle szerepeket töltenek be, szolgáltatásokat futtatnak, és közöttük TCP/HTTP kapcsolatok létesíthetők.

A TCP-kapcsolat leírásához legalább egy portszámra van szükségünk. Szeretnénk az adott porton támogatott protokollt is tükrözni annak biztosítása érdekében, hogy az ügyfél és a kiszolgáló is ugyanazt a protokollt használja. A kapcsolatot a következő osztály segítségével írjuk le:

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

ahol Port - csak egy egész szám Int feltüntetve az elfogadható értékek tartományát:

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

Finomított típusok

Lásd a könyvtárat kifinomult и én jelentés. Röviden, a könyvtár lehetővé teszi, hogy megszorításokat adjon a fordításkor ellenőrzött típusokhoz. Ebben az esetben a portszám érvényes értéke 16 bites egész szám. Lefordított konfiguráció esetén a finomított könyvtár használata nem kötelező, de javítja a fordító azon képességét, hogy ellenőrizze a konfigurációt.

HTTP (REST) ​​protokollok esetén a portszámon kívül szükségünk lehet a szolgáltatás elérési útjára is:

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

Fantom típusok

A protokoll fordítási idejének azonosításához egy típusparamétert használunk, amelyet az osztályon belül nem használunk. Ez a döntés annak köszönhető, hogy futás közben nem használunk protokollpéldányt, de szeretnénk, ha a fordító ellenőrizné a protokoll kompatibilitását. A protokoll megadásával nem tudunk nem megfelelő szolgáltatást függőségként átadni.

Az egyik gyakori protokoll a REST API Json szerializációval:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

ahol RequestMessage - Kérelem típusa, ResponseMessage - válasz típusa.
Természetesen használhatunk más protokollleírásokat is, amelyek az általunk igényelt leírás pontosságát biztosítják.

Ebben a bejegyzésben a protokoll egyszerűsített változatát fogjuk használni:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Itt a kérés az url-hez fűzött karakterlánc, a válasz pedig a HTTP-válasz törzsében visszaadott karakterlánc.

A szolgáltatás konfigurációját a szolgáltatás neve, a portok és a függőségek írják le. Ezek az elemek a Scalában többféleképpen ábrázolhatók (pl. HList-s, algebrai adattípusok). Ebben a bejegyzésben a tortamintát fogjuk használni, és a modulokat ábrázoljuk trait'ov. (A tortaminta nem kötelező eleme ennek a megközelítésnek. Ez egyszerűen egy lehetséges megvalósítás.)

A szolgáltatások közötti függőségek portokat visszaadó metódusokként ábrázolhatók EndPointmás csomópontok számai:

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

Egy echo szolgáltatás létrehozásához mindössze egy portszámra van szüksége, és annak jelzésére, hogy a port támogatja az echo protokollt. Lehet, hogy nem adunk meg egy konkrét portot, mert... A tulajdonságok lehetővé teszik a módszerek deklarálását megvalósítás nélkül (absztrakt módszerek). Ebben az esetben egy konkrét konfiguráció létrehozásakor a fordító megköveteli, hogy biztosítsuk az absztrakt módszer megvalósítását, és adjunk meg egy portszámot. Mivel a metódust megvalósítottuk, egy adott konfiguráció létrehozásakor előfordulhat, hogy nem adunk meg másik portot. Az alapértelmezett érték kerül felhasználásra.

Az ügyfélkonfigurációban deklarálunk egy függőséget az echo szolgáltatástól:

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

A függőség ugyanolyan típusú, mint az exportált szolgáltatás echoService. Különösen az echo kliensben van szükségünk ugyanarra a protokollra. Ezért két szolgáltatás összekapcsolásakor biztosak lehetünk abban, hogy minden megfelelően fog működni.

Szolgáltatások megvalósítása

A szolgáltatás indításához és leállításához funkció szükséges. (A szolgáltatás leállításának képessége kritikus a teszteléshez.) Ismét számos lehetőség van egy ilyen szolgáltatás megvalósítására (például használhatunk típusosztályokat a konfiguráció típusa alapján). Ebben a bejegyzésben a tortamintát fogjuk használni. A szolgáltatást osztály segítségével fogjuk képviselni cats.Resource, mert Ez az osztály már biztosítja az erőforrások biztonságos felszabadítását problémák esetén. Az erőforrás megszerzéséhez konfigurációt és kész futási környezetet kell biztosítanunk. A szolgáltatás indítási funkciója így nézhet ki:

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

ahol

  • Config — a szolgáltatás konfigurációs típusa
  • AddressResolver - egy futásidejű objektum, amely lehetővé teszi más csomópontok címének megtudását (lásd alább)

és más típusok a könyvtárból cats:

  • F[_] - a hatás típusa (a legegyszerűbb esetben F[A] csak egy funkció lehet () => A. Ebben a bejegyzésben fogjuk használni cats.IO.)
  • Reader[A,B] - többé-kevésbé a funkció szinonimája A => B
  • cats.Resource - megszerezhető és felszabadítható erőforrás
  • Timer - időzítő (lehetővé teszi, hogy elaludjon egy ideig, és mérje az időintervallumokat)
  • ContextShift - analóg ExecutionContext
  • Applicative — egy effekttípus-osztály, amely lehetővé teszi az egyes effektek kombinálását (majdnem monád). Bonyolultabb alkalmazásokban jobbnak tűnik a használata Monad/ConcurrentEffect.

Ezzel a függvényaláírással több szolgáltatást is megvalósíthatunk. Például egy szolgáltatás, amely nem csinál semmit:

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

(Cm. forrás, amelyben más szolgáltatásokat is megvalósítanak - visszhang szolgáltatás, echo kliens
и élettartam vezérlők.)

A csomópont olyan objektum, amely több szolgáltatást is elindíthat (az erőforráslánc elindítását a Cake Pattern biztosítja):

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

Kérjük, vegye figyelembe, hogy pontosan meghatározzuk az ehhez a csomóponthoz szükséges konfiguráció típusát. Ha elfelejtjük megadni az adott szolgáltatás által megkövetelt konfigurációtípusok valamelyikét, akkor fordítási hiba lép fel. Ezenkívül nem tudunk csomópontot elindítani, hacsak nem biztosítunk valamilyen megfelelő típusú objektumot az összes szükséges adattal.

Gazdanév felbontása

A távoli gazdagéphez való csatlakozáshoz valódi IP-címre van szükségünk. Lehetséges, hogy a cím később válik ismertté, mint a konfiguráció többi része. Tehát szükségünk van egy függvényre, amely leképezi a csomópont azonosítóját egy címre:

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

Számos módja van ennek a funkciónak a megvalósítására:

  1. Ha a címek a telepítés előtt ismertté válnak, akkor Scala kódot generálhatunk
    címeket, majd futtassa a buildet. Ez összeállítja és futtatja a teszteket.
    Ebben az esetben a függvény statikusan ismert lesz, és kódban leképezésként ábrázolható Map[NodeId, NodeAddress].
  2. Egyes esetekben a tényleges cím csak a csomópont elindulása után ismert.
    Ebben az esetben megvalósíthatunk egy „felfedezési szolgáltatást”, amely a többi csomópont előtt fut, és minden csomópont regisztrálni fog ehhez a szolgáltatáshoz, és lekéri a többi csomópont címét.
  3. Ha módosítani tudjuk /etc/hosts, akkor használhat előre meghatározott gazdagépneveket (pl my-project-main-node и echo-backend), és egyszerűen kapcsolja össze ezeket a neveket
    IP-címekkel a telepítés során.

Ebben a bejegyzésben ezekkel az esetekkel nem foglalkozunk részletesebben. A miénkért
egy játék példában minden csomópontnak ugyanaz az IP-címe - 127.0.0.1.

Ezután megvizsgálunk két lehetőséget az elosztott rendszerhez:

  1. Az összes szolgáltatás elhelyezése egy csomóponton.
  2. És az echo szolgáltatás és az echo kliens különböző csomópontokon történő üzemeltetése.

Konfiguráció ehhez egy csomópont:

Egy csomópont konfigurációja

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

Az objektum megvalósítja mind a kliens, mind a szerver konfigurációját. Időtartamú konfiguráció is használatos, hogy az intervallum után lifetime szakítsa meg a programot. (A Ctrl-C is működik, és megfelelően felszabadítja az összes erőforrást.)

A konfigurációs és megvalósítási jellemzők azonos halmaza használható egy olyan rendszer létrehozására, amely a két külön csomópont:

Két csomópont konfiguráció

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

Fontos! Figyelje meg, hogyan kapcsolódnak a szolgáltatások. Az egyik csomópont által megvalósított szolgáltatást egy másik csomópont függőségi metódusának megvalósításaként határozzuk meg. A függőségi típust a fordító ellenőrzi, mert tartalmazza a protokoll típusát. Futtatáskor a függőség a megfelelő célcsomópont-azonosítót fogja tartalmazni. Ennek a sémának köszönhetően pontosan egyszer adjuk meg a portszámot, és garantáltan mindig a megfelelő portra hivatkozunk.

Két rendszercsomópont megvalósítása

Ehhez a konfigurációhoz ugyanazokat a szolgáltatásmegvalósításokat használjuk változtatások nélkül. Az egyetlen különbség az, hogy most két objektumunk van, amelyek különböző szolgáltatáskészleteket valósítanak meg:

  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
  }

Az első csomópont megvalósítja a kiszolgálót, és csak kiszolgálókonfigurációra van szüksége. A második csomópont az ügyfelet valósítja meg, és a konfiguráció egy másik részét használja. Mindkét csomópontnak élettartam-kezelésre van szüksége. A szerver csomópont korlátlan ideig fut, amíg le nem áll SIGTERM'om, és a kliens csomópont egy idő után leáll. Cm. indító alkalmazás.

Általános fejlesztési folyamat

Nézzük meg, hogyan hat ez a konfigurációs megközelítés az általános fejlesztési folyamatra.

A konfiguráció a kód többi részével együtt le lesz fordítva, és létrejön egy melléktermék (.jar). Úgy tűnik, ésszerű a konfigurációt külön műtermékbe helyezni. Ennek az az oka, hogy több konfigurációnk is lehet ugyanazon a kódon. Itt is lehetséges a különböző konfigurációs ágaknak megfelelő műtermékek generálása. A programkönyvtárak adott verzióitól függő függőségek a konfigurációval együtt mentésre kerülnek, és ezek a verziók örökre mentésre kerülnek, amikor úgy döntünk, hogy telepítjük a konfiguráció adott verzióját.

Bármilyen konfigurációmódosítás kódmódosítássá válik. És ezért mindegyik
a változásra a normál minőségbiztosítási eljárás vonatkozik:

Jegy a hibakövetőben -> PR -> áttekintés -> egyesítse a megfelelő ágakkal ->
integráció -> telepítés

A lefordított konfiguráció megvalósításának főbb következményei a következők:

  1. A konfiguráció konzisztens lesz az elosztott rendszer összes csomópontján. Annak a ténynek köszönhetően, hogy minden csomópont ugyanazt a konfigurációt kapja egyetlen forrásból.

  2. Problémás a konfiguráció módosítása csak az egyik csomópontban. Ezért a „konfiguráció eltolódása” nem valószínű.

  3. A konfiguráción kisebb változtatások végrehajtása nehezebbé válik.

  4. A legtöbb konfigurációs módosítás az általános fejlesztési folyamat részeként történik, és felülvizsgálható.

Szükségem van egy külön tárolóra az éles konfiguráció tárolására? Ez a konfiguráció jelszavakat és egyéb bizalmas információkat tartalmazhat, amelyekhez korlátozni szeretnénk a hozzáférést. Ez alapján célszerűnek tűnik a végleges konfigurációt külön tárolóban tárolni. A konfigurációt két részre oszthatja – az egyik a nyilvánosan elérhető konfigurációs beállításokat, a másik pedig a korlátozott beállításokat tartalmazza. Ez lehetővé teszi a legtöbb fejlesztő számára, hogy hozzáférjen az általános beállításokhoz. Ez az elkülönítés könnyen megvalósítható az alapértelmezett értékeket tartalmazó köztes tulajdonságok használatával.

Lehetséges variációk

Próbáljuk meg összehasonlítani az összeállított konfigurációt néhány gyakori alternatívával:

  1. Szövegfájl a célgépen.
  2. Központosított kulcsérték tároló (etcd/zookeeper).
  3. A folyamat újraindítása nélkül újrakonfigurálható/újraindítható folyamatösszetevők.
  4. Konfiguráció tárolása a műtermék- és verzióvezérlésen kívül.

A szöveges fájlok jelentős rugalmasságot biztosítanak az apró változtatások tekintetében. A rendszergazda bejelentkezhet a távoli csomópontba, módosíthatja a megfelelő fájlokat, és újraindíthatja a szolgáltatást. Nagy rendszerek esetében azonban előfordulhat, hogy ez a rugalmasság nem kívánatos. Az elvégzett változtatások nem hagynak nyomot más rendszerekben. Senki nem nézi át a változtatásokat. Nehéz megállapítani, hogy pontosan ki és milyen okból végezte el a változtatásokat. A változtatásokat nem tesztelik. Ha a rendszer elosztott, akkor az adminisztrátor elfelejtheti elvégezni a megfelelő módosítást más csomópontokon.

(Azt is meg kell jegyezni, hogy a lefordított konfiguráció használata nem zárja ki a szöveges fájlok jövőbeni használatának lehetőségét. Elég lesz hozzáadni egy értelmezőt és érvényesítőt, amely ugyanazt a típust állítja elő, mint a kimenet Config, és használhat szöveges fájlokat. Ebből rögtön az következik, hogy egy lefordított konfigurációval rendelkező rendszer bonyolultsága valamivel kisebb, mint egy szöveges fájlokat használó rendszer bonyolultsága, mert A szöveges fájlok további kódot igényelnek.)

A központi kulcsérték tároló jó mechanizmus az elosztott alkalmazások metaparamétereinek elosztására. El kell döntenünk, hogy melyek a konfigurációs paraméterek, és melyek csak adatok. Legyen egy funkciónk C => A => Bés a paramétereket C ritkán változik, és az adatok A - gyakran. Ebben az esetben azt mondhatjuk C - konfigurációs paraméterek, és A - adatok. Úgy tűnik, hogy a konfigurációs paraméterek abban különböznek az adatoktól, hogy általában ritkábban változnak, mint az adatok. Ezenkívül az adatok általában egy forrásból származnak (a felhasználótól), a konfigurációs paraméterek pedig egy másikból (a rendszergazdától).

Ha a ritkán változó paramétereket a program újraindítása nélkül kell frissíteni, akkor ez gyakran a program bonyolításához vezethet, mert valamilyen módon paramétereket kell szállítanunk, tárolnunk, elemezni és ellenőrizni, valamint hibás értékeket kell feldolgoznunk. Ezért a program bonyolultságának csökkentése szempontjából célszerű csökkenteni azon paraméterek számát, amelyek a program működése során változhatnak (vagy egyáltalán nem támogatják ezeket a paramétereket).

Ebben a bejegyzésben különbséget teszünk statikus és dinamikus paraméterek között. Ha a szolgáltatás logikája megköveteli a paraméterek megváltoztatását a program működése során, akkor ezeket a paramétereket dinamikusnak nevezzük. Egyébként az opciók statikusak, és a lefordított konfigurációval konfigurálhatók. A dinamikus újrakonfiguráláshoz szükségünk lehet egy olyan mechanizmusra, amely a program egyes részeit új paraméterekkel indítja újra, hasonlóan az operációs rendszer folyamatainak újraindításához. (Véleményünk szerint a valós idejű újrakonfigurálást célszerű kerülni, mert ez növeli a rendszer bonyolultságát. A folyamatok újraindításához lehetőség szerint érdemes az operációs rendszer szabványos képességeit használni.)

A statikus konfiguráció használatának egyik fontos szempontja, amely arra készteti az embereket, hogy fontolóra vegyék a dinamikus újrakonfigurálást, az az idő, amely alatt a rendszer újraindul a konfiguráció frissítése után (leállás). Valójában, ha módosítanunk kell a statikus konfigurációt, újra kell indítanunk a rendszert, hogy az új értékek érvénybe lépjenek. Az állásidő-probléma a különböző rendszerekben eltérő súlyosságú. Egyes esetekben ütemezheti az újraindítást olyan időpontban, amikor a terhelés minimális. Ha folyamatos szolgáltatást kell nyújtani, akkor megvalósíthatja AWS ELB csatlakozás leeresztés. Ugyanakkor, amikor újra kell indítanunk a rendszert, elindítjuk ennek a rendszernek egy párhuzamos példányát, átkapcsoljuk rá a kiegyenlítőt, és megvárjuk, amíg a régi kapcsolatok befejeződnek. Miután minden régi kapcsolat megszakadt, leállítjuk a rendszer régi példányát.

Tekintsük most a konfigurációnak a műterméken belüli vagy kívüli tárolásának kérdését. Ha a konfigurációt egy műterméken belül tároljuk, akkor legalább lehetőségünk volt ellenőrizni a konfiguráció helyességét a műtermék összeállítása során. Ha a konfiguráció kívül esik a szabályozott mellékterméken, nehéz nyomon követni, hogy ki és miért módosította ezt a fájlt. Mennyire fontos? Véleményünk szerint sok termelési rendszernél fontos a stabil és jó minőségű konfiguráció.

A műtermék verziója lehetővé teszi annak meghatározását, hogy mikor hozták létre, milyen értékeket tartalmaz, milyen funkciók vannak engedélyezve/letiltva, és ki a felelős a konfiguráció bármilyen változásáért. Természetesen a konfiguráció műterméken belüli tárolása némi erőfeszítést igényel, ezért tájékozott döntést kell hoznia.

Előnye és hátránya

Szeretnék kitérni a javasolt technológia előnyeire és hátrányaira.

Előnyei

Az alábbiakban felsoroljuk a lefordított elosztott rendszerkonfiguráció főbb jellemzőit:

  1. Statikus konfiguráció ellenőrzése. Lehetővé teszi, hogy megbizonyosodjon arról
    a konfiguráció helyes.
  2. Gazdag konfigurációs nyelv. Az egyéb konfigurációs módszerek általában legfeljebb a karakterlánc-változók helyettesítésére korlátozódnak. A Scala használatakor számos nyelvi funkció érhető el a konfiguráció javítása érdekében. Például használhatjuk
    Az alapértelmezett értékek jellemzőit, objektumokat használva a paraméterek csoportosítására, a befoglaló hatókörben csak egyszer deklarált értékekre (DRY) hivatkozhatunk. Bármely osztályt példányosíthat közvetlenül a konfiguráción belül (Seq, Map, egyéni osztályok).
  3. DSL. A Scala számos nyelvi funkcióval rendelkezik, amelyek megkönnyítik a DSL létrehozását. Lehetőség van ezen funkciók kihasználására, és a felhasználók célcsoportja számára kényelmesebb konfigurációs nyelv megvalósítására, hogy a konfiguráció legalább a tartományi szakértők számára olvasható legyen. A szakemberek például részt vehetnek a konfiguráció-ellenőrzési folyamatban.
  4. Integritás és szinkron a csomópontok között. Az egyik előnye annak, ha egy teljes elosztott rendszer konfigurációját egyetlen ponton tárolják, hogy minden értéket pontosan egyszer deklarálnak, majd újra felhasználják, ahol szükség van rá. A fantomtípusok használata a portok deklarálására biztosítja, hogy a csomópontok minden megfelelő rendszerkonfigurációban kompatibilis protokollokat használjanak. A csomópontok közötti kifejezett kötelező függőségek biztosítják, hogy minden szolgáltatás csatlakozik.
  5. Kiváló minőségű változások. A konfiguráció módosítása közös fejlesztési folyamattal lehetővé teszi a konfiguráció magas minőségi színvonalának elérését is.
  6. Egyidejű konfiguráció frissítés. A konfigurációs módosítások utáni automatikus rendszertelepítés biztosítja az összes csomópont frissítését.
  7. Az alkalmazás egyszerűsítése. Az alkalmazásnak nincs szüksége elemzésre, konfiguráció-ellenőrzésre vagy hibás értékek kezelésére. Ez csökkenti az alkalmazás bonyolultságát. (A példánkban megfigyelhető konfigurációs bonyolultság egy része nem az összeállított konfiguráció attribútuma, hanem csak tudatos döntés, amelyet a nagyobb típusbiztonság megteremtésének vágya vezérel.) Elég könnyű visszatérni a megszokott konfigurációhoz - elég megvalósítani a hiányzót. alkatrészek. Ezért például elkezdhet egy összeállított konfigurációval, elhalasztva a szükségtelen részek megvalósítását addig az időpontig, amikor valóban szükség lesz rá.
  8. Ellenőrzött konfiguráció. Mivel a konfigurációs változtatások minden más változtatás szokásos sorsát követik, a kapott kimenet egy egyedi verziójú műtermék. Ez lehetővé teszi például, hogy szükség esetén visszatérjünk a konfiguráció egy korábbi verziójához. Akár az egy évvel ezelőtti konfigurációt is használhatjuk, és a rendszer pontosan ugyanúgy fog működni. A stabil konfiguráció javítja az elosztott rendszer kiszámíthatóságát és megbízhatóságát. Mivel a konfigurációt az összeállítási szakaszban rögzítik, meglehetősen nehéz meghamisítani a gyártás során.
  9. Modularitás. A javasolt keretrendszer moduláris, és a modulok különböző módon kombinálhatók különböző rendszerek létrehozásához. Konkrétan beállíthatja a rendszert úgy, hogy az egyik kiviteli alakban egyetlen csomóponton, a másikban pedig több csomóponton fusson. Több konfigurációt is létrehozhat a rendszer éles példányaihoz.
  10. Tesztelés. Az egyes szolgáltatásokat álobjektumokra cserélve a rendszer több, tesztelésre alkalmas verzióját is megkaphatja.
  11. Integrációs tesztelés. A teljes elosztott rendszer egyetlen konfigurációja lehetővé teszi az összes összetevő ellenőrzött környezetben történő futtatását az integrációs tesztelés részeként. Könnyen emulálható például egy olyan helyzet, amikor egyes csomópontok elérhetővé válnak.

Hátrányok és korlátok

A lefordított konfiguráció eltér a többi konfigurációs megközelítéstől, és előfordulhat, hogy egyes alkalmazásokhoz nem megfelelő. Az alábbiakban felsorolunk néhány hátrányt:

  1. Statikus konfiguráció. Néha gyorsan ki kell javítania a konfigurációt a gyártás során, megkerülve az összes védelmi mechanizmust. Ezzel a megközelítéssel nehezebb lehet. Legalább még mindig szükség lesz fordításra és automatikus telepítésre. Ez a megközelítés hasznos tulajdonsága és bizonyos esetekben hátránya is.
  2. Konfiguráció generálása. Abban az esetben, ha a konfigurációs fájlt egy automatikus eszköz hozza létre, további erőfeszítésekre lehet szükség az összeállítási parancsfájl integrálásához.
  3. Eszközök. Jelenleg a konfigurációval való együttműködésre tervezett segédprogramok és technikák szövegfájlokon alapulnak. Nem minden ilyen segédprogram/technika lesz elérhető összeállított konfigurációban.
  4. Szemléletváltásra van szükség. A fejlesztők és a DevOps-ok hozzászoktak a szöveges fájlokhoz. A konfiguráció összeállításának ötlete kissé váratlan és szokatlan lehet, és elutasítást okozhat.
  5. Kiváló minőségű fejlesztési folyamatra van szükség. Az összeállított konfiguráció kényelmes használatához az alkalmazás (CI/CD) felépítésének és telepítésének folyamatának teljes automatizálására van szükség. Ellenkező esetben meglehetősen kényelmetlen lesz.

Maradjunk a vizsgált példa számos korlátozásán is, amelyek nem kapcsolódnak a lefordított konfiguráció gondolatához:

  1. Ha szükségtelen konfigurációs információkat adunk meg, amelyeket a csomópont nem használ, akkor a fordító nem segít a hiányzó megvalósítás észlelésében. Ezt a problémát úgy lehet megoldani, ha elhagyjuk a tortamintát, és merevebb típusokat használunk, pl. HList vagy algebrai adattípusok (esetosztályok) a konfiguráció ábrázolására.
  2. Vannak olyan sorok a konfigurációs fájlban, amelyek nem kapcsolódnak magához a konfigurációhoz: (package, import,objektum deklarációk; override def's az alapértelmezett értékkel rendelkező paraméterekhez). Ez részben elkerülhető, ha saját DSL-t alkalmaz. Ezenkívül más típusú konfigurációk (például XML) is bizonyos korlátozásokat támasztanak a fájlszerkezettel kapcsolatban.
  3. E bejegyzés céljaira nem fontolgatjuk a hasonló csomópontok fürtjének dinamikus újrakonfigurálását.

Következtetés

Ebben a bejegyzésben megvizsgáltuk azt az ötletet, hogy a konfigurációt a Scala típusú rendszer fejlett képességeinek felhasználásával forráskódban ábrázoljuk. Ez a megközelítés különféle alkalmazásokban használható az xml- vagy szövegfájlokon alapuló hagyományos konfigurációs módszerek helyettesítésére. Annak ellenére, hogy példánkat Scalában valósítottuk meg, ugyanazok az ötletek átvihetők más lefordított nyelvekre (például Kotlin, C#, Swift stb.). Kipróbálhatja ezt a megközelítést a következő projektek egyikében, és ha nem működik, lépjen tovább a szövegfájlra, hozzáadva a hiányzó részeket.

Természetesen egy összeállított konfiguráció magas színvonalú fejlesztési folyamatot igényel. Cserébe a konfigurációk magas minősége és megbízhatósága biztosított.

A mérlegelt megközelítés bővíthető:

  1. A fordítási idő ellenőrzésére makrókat használhat.
  2. Megvalósíthat egy DSL-t, hogy a konfigurációt a végfelhasználók számára elérhető módon mutassa be.
  3. A dinamikus erőforrás-kezelést automatikus konfigurációmódosítással valósíthatja meg. Például egy fürtben lévő csomópontok számának megváltoztatásához szükséges, hogy (1) minden csomópont kissé eltérő konfigurációt kapjon; (2) a fürtkezelő információkat kapott az új csomópontokról.

Köszönetnyilvánítás

Szeretnék köszönetet mondani Andrej Saksonovnak, Pavel Popovnak és Anton Nekhaevnek a cikktervezettel kapcsolatos építő jellegű kritikájáért.

Forrás: will.com

Hozzászólás