Elosztott rendszer lefordítható konfigurációja

Ebben a bejegyzésben szeretnénk megosztani egy érdekes módszert az elosztott rendszer konfigurációjának kezelésére.
A konfiguráció típusbiztonságos módon közvetlenül a Scala nyelven jelenik meg. Egy példa megvalósítást ismertetünk részletesen. A javaslat különböző szempontjait megvitatják, beleértve az általános fejlesztési folyamatra gyakorolt ​​hatást.

Elosztott rendszer lefordítható konfigurációja

(на русском)

Bevezetés

A robusztus elosztott rendszerek felépítéséhez minden csomóponton helyes és koherens konfigurációra van szükség. Tipikus megoldás a szöveges telepítési leírás (terraform, ansible vagy hasonló) és automatikusan generált konfigurációs fájlok használata (gyakran minden csomóponthoz/szerepkörhöz dedikált). Ugyanazon verziójú protokollokat szeretnénk használni minden kommunikáló csomóponton (különben inkompatibilitási problémákat tapasztalnánk). A JVM világában ez azt jelenti, hogy legalább az üzenetküldő könyvtárnak azonos verziójúnak kell lennie az összes kommunikáló csomóponton.

Mi a helyzet a rendszer tesztelésével? Természetesen minden komponenshez egységtesztet kell végeznünk, mielőtt az integrációs tesztekhez kezdenénk. Ahhoz, hogy a teszteredményeket futásidejű extrapolálással tudjuk extrapolálni, gondoskodnunk kell arról, hogy az összes könyvtár verziója azonos legyen futásidejű és tesztelési környezetben is.

Integrációs tesztek futtatásakor gyakran sokkal könnyebb ugyanazt az osztályútvonalat használni minden csomóponton. Csak meg kell győződnünk arról, hogy ugyanazt az osztályútvonalat használják a telepítéskor. (Lehetőség van különböző osztályútvonalak használatára a különböző csomópontokon, de ezt a konfigurációt nehezebb reprezentálni és helyesen telepíteni.) Így az egyszerűség érdekében csak azonos osztályútvonalakat fogunk figyelembe venni minden csomóponton.

A konfiguráció a szoftverrel együtt fejlődik. Általában változatokat használunk a különféle azonosítására
a szoftverfejlődés szakaszai. Ésszerűnek tűnik a konfigurációt a verziókezelés alatt lefedni, és a különböző konfigurációkat bizonyos címkékkel azonosítani. Ha csak egy konfiguráció van a termelésben, akkor egyetlen verziót is használhatunk azonosítóként. Néha több termelési környezetünk is lehet. És minden környezethez szükségünk lehet egy külön konfigurációs ágra. Így a konfigurációk felcímkézhetők ággal és verzióval a különböző konfigurációk egyedi azonosítása érdekében. Minden ágcímke és verzió az elosztott csomópontok, portok, külső erőforrások és osztályútvonal-könyvtárverziók egyetlen kombinációjának felel meg az egyes csomópontokon. Itt csak az egyetlen ágat fedjük le, és a konfigurációkat háromkomponensű decimális verzióval (1.2.3) azonosítjuk, ugyanúgy, mint a többi mellékterméket.

A modern környezetben a konfigurációs fájlokat már nem módosítják manuálisan. Általában generálunk
konfigurációs fájlok a telepítéskor és soha ne érintse meg őket később. Felmerülhet tehát a kérdés, hogy miért használunk még mindig szöveges formátumot a konfigurációs fájlokhoz? Egy életképes lehetőség, hogy a konfigurációt egy fordítási egységbe helyezzük, és kihasználjuk a fordítási idejű konfigurációellenőrzés előnyeit.

Ebben a bejegyzésben megvizsgáljuk a konfiguráció megtartásának ötletét az összeállított műtermékben.

Lefordítható konfiguráció

Ebben a részben a statikus konfiguráció példáját tárgyaljuk. Két egyszerű szolgáltatás – az echo szolgáltatás és az echo szolgáltatás kliense – konfigurálása és megvalósítása folyamatban van. Ezután két különböző, mindkét szolgáltatással rendelkező elosztott rendszer példányosodik. Az egyik az egy csomópont konfigurációjához, a másik pedig a két csomópont konfigurációjához.

Egy tipikus elosztott rendszer néhány csomópontból áll. A csomópontok azonosíthatók valamilyen típus használatával:

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

vagy csak

case class NodeId(hostName: String)

vagy

object Singleton
type NodeId = Singleton.type

Ezek a csomópontok különféle szerepeket töltenek be, bizonyos szolgáltatásokat futtatnak, és képesnek kell lenniük a többi csomóponttal TCP/HTTP kapcsolatokon keresztül kommunikálni.

A TCP-kapcsolathoz legalább egy portszám szükséges. Azt is szeretnénk megbizonyosodni arról, hogy a kliens és a szerver ugyanazt a protokollt beszéli. A csomópontok közötti kapcsolat modellezéséhez deklaráljuk a következő osztályt:

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

ahol Port csak egy Int a megengedett tartományon belül:

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

Finomított típusok

Lát kifinomult könyvtár. Röviden, lehetővé teszi fordítási időkorlátok hozzáadását más típusokhoz. Ebben az esetben Int csak 16 bites értékeket tartalmazhat, amelyek a portszámot jelenthetik. Ehhez a konfigurációs megközelítéshez nincs szükség ennek a könyvtárnak a használatára. Csak úgy tűnik, nagyon jól illeszkedik.

HTTP (REST) ​​esetén 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ípus

A fordítás során a protokoll azonosítására a Scala funkciót használjuk a típus argumentum deklarálására Protocol amit nem használnak az osztályban. Ez egy ún fantom típus. Futás közben ritkán van szükségünk protokollazonosító példányra, ezért nem tároljuk. Az összeállítás során ez a fantomtípus további típusbiztonságot ad. Nem tudjuk átadni a portot helytelen protokollal.

Az egyik legszélesebb körben használt protokoll a REST API Json szerializációval:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

ahol RequestMessage az üzenetek alaptípusa, amelyet az ügyfél küldhet a szervernek és ResponseMessage a válaszüzenet a szervertől. Természetesen más protokollleírásokat is készíthetünk, amelyek a kívánt pontossággal határozzák meg a kommunikációs protokollt.

Ebben a bejegyzésben a protokoll egy egyszerűbb verzióját fogjuk használni:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Ebben a protokollban a kérésüzenet hozzá van fűzve az url-hez, és a válaszüzenet egyszerű karakterláncként jelenik meg.

A szolgáltatás konfigurációja leírható a szolgáltatásnévvel, a portok gyűjteményével és néhány függőséggel. Számos lehetséges módja van ezeknek az elemeknek a Scalában való megjelenítésére (például HList, algebrai adattípusok). Ennek a bejegyzésnek a céljaira a tortamintát használjuk, és a kombinálható darabokat (modulokat) jellemvonásként ábrázoljuk. (A tortaminta nem követelmény ehhez a lefordítható konfigurációs megközelítéshez. Ez csak egy lehetséges megvalósítása az ötletnek.)

A függőségek a tortamintával ábrázolhatók más csomópontok végpontjaként:

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

Az Echo szolgáltatásnak csak egy konfigurált portra van szüksége. És kijelentjük, hogy ez a port támogatja az echo protokollt. Ne feledje, hogy jelenleg nem kell megadnunk egy adott portot, mert a tulajdonságok lehetővé teszik absztrakt metódusok deklarációit. Ha absztrakt metódusokat használunk, a fordítónak implementációra lesz szüksége egy konfigurációs példányban. Itt biztosítjuk a megvalósítást (8081), és ez lesz az alapértelmezett érték, ha kihagyjuk egy konkrét konfigurációban.

Deklarálhatunk függőséget az echo szolgáltatás kliens konfigurációjában:

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

A függőségnek ugyanaz a típusa, mint a echoService. Különösen ugyanazt a protokollt követeli meg. Ezért biztosak lehetünk abban, hogy ha összekapcsoljuk ezt a két függőséget, akkor megfelelően fognak működni.

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

A szolgáltatásnak szüksége van egy funkcióra az induláshoz és a kecsesen leállításhoz. (A szolgáltatás leállítása kritikus fontosságú a teszteléshez.) Ismét van néhány lehetőség egy ilyen függvény megadására egy adott konfigurációhoz (például használhatunk típusosztályokat). Ebben a bejegyzésben ismét a tortamintát fogjuk használni. A szolgáltatás igénybevételével tudunk képviselni cats.Resource amely már biztosítja a sorozatfelvételt és az erőforrás-felszabadítást. Az erőforrás beszerzéséhez konfigurációt és futási környezetet kell biztosítanunk. Tehát a szolgáltatásindítási funkció í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ásindító által igényelt konfiguráció típusa
  • AddressResolver — egy futásidejű objektum, amely képes más csomópontok valós címeinek megszerzésére (a részletekért olvassa tovább).

a többi típus innen származik cats:

  • F[_] — effektus típus (a legegyszerűbb esetben F[A] lehet igazságos () => A. Ebben a bejegyzésben fogjuk használni cats.IO.)
  • Reader[A,B] — többé-kevésbé egy funkció szinonimája A => B
  • cats.Resource — megvannak a megszerzésének és elengedésének módjai
  • Timer - lehetővé teszi az alvást/idő mérést
  • ContextShift - analógja ExecutionContext
  • Applicative — érvényben lévő függvények burkolója (majdnem monád) (esetleg lecserélhetjük valami másra)

Ezen a felületen néhány szolgáltatást tudunk megvalósítani. 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](()))
  }

(Lát Forráskód egyéb szolgáltatások megvalósításához — visszhang szolgáltatás,
echo kliens és a élettartam vezérlők.)

A csomópont egyetlen objektum, amely néhány szolgáltatást futtat (az erőforráslánc elindítását a Cake Pattern engedélyezi):

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

Ne feledje, hogy a csomópontban megadjuk a csomóponthoz szükséges konfiguráció pontos típusát. A fordító nem engedi, hogy az objektumot (Cake) elégtelen típussal építsük fel, mert minden szolgáltatási tulajdonság megszorítást deklarál a Config típus. Ezenkívül nem tudjuk elindítani a csomópontot a teljes konfiguráció megadása nélkül.

Csomópont cím felbontása

A kapcsolat létrehozásához minden csomóponthoz valódi gazdagép címre van szükségünk. Ez később ismertté válhat, mint a konfiguráció többi része. Ezért szükségünk van egy módra a csomópontazonosító és a tényleges cím közötti leképezés biztosítására. Ez a leképezés egy függvény:

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

Egy ilyen funkció megvalósításának néhány lehetséges módja van.

  1. Ha ismerjük a tényleges címeket a telepítés előtt, a csomóponti gazdagépek példányosítása során, akkor a tényleges címekkel generálhatunk Scala kódot, és utána futtathatjuk a buildet (amely fordítási időellenőrzéseket hajt végre, majd lefuttatja az integrációs tesztcsomagot). Ebben az esetben a leképezési függvényünk statikusan ismert, és leegyszerűsíthető olyasmire, mint a Map[NodeId, NodeAddress].
  2. Előfordulhat, hogy a tényleges címeket csak egy későbbi pontban kapjuk meg, amikor a csomópont ténylegesen elindul, vagy nincs olyan csomópont címe, amelyet még nem indítottak el. Ebben az esetben előfordulhat, hogy van egy felderítési szolgáltatásunk, amely az összes többi csomópont előtt indul el, és mindegyik csomópont hirdetheti a címét abban a szolgáltatásban, és előfizethet a függőségekre.
  3. Ha módosítani tudjuk /etc/hosts, használhatunk előre meghatározott hosztneveket (pl my-project-main-node és a echo-backend), és csak társítsa ezt a nevet az IP-címhez a telepítéskor.

Ebben a bejegyzésben ezekkel az esetekkel nem foglalkozunk részletesebben. Valójában a játékpéldánkban minden csomópontnak ugyanaz az IP-címe - 127.0.0.1.

Ebben a bejegyzésben két elosztott rendszerelrendezést fogunk megvizsgálni:

  1. Egy csomópontos elrendezés, ahol az összes szolgáltatás egyetlen csomóponton van elhelyezve.
  2. Két csomópont-elrendezés, ahol a szolgáltatás és az ügyfél különböző csomópontokon található.

A konfiguráció a egyetlen csomópont az elrendezés a következő:

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

Itt egyetlen konfigurációt hozunk létre, amely kiterjeszti a szerver és a kliens konfigurációját is. Ezenkívül beállítunk egy életciklus-vezérlőt, amely általában leállítja a klienst és a szervert lifetime intervallum múlik.

Ugyanaz a szolgáltatásmegvalósítás és konfigurációkészlet használható két különálló csomóponttal rendelkező rendszerelrendezés létrehozására. Csak alkotnunk kell két különálló csomópont-konfiguráció megfelelő szolgáltatásokkal:

Két csomópont konfigurációja

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

Nézze meg, hogyan határozzuk meg a függőséget. A másik csomópont által nyújtott szolgáltatást az aktuális csomópont függőségeként említjük. A függőség típusa ellenőrzött, mert fantomtípust tartalmaz, amely leírja a protokollt. És futás közben megkapjuk a megfelelő csomópontazonosítót. Ez a javasolt konfigurációs megközelítés egyik fontos szempontja. Lehetővé teszi számunkra, hogy csak egyszer állítsuk be a portot, és győződjön meg arról, hogy a megfelelő portra hivatkozunk.

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

Ehhez a konfigurációhoz pontosan ugyanazokat a szolgáltatások megvalósítását használjuk. Egyáltalán nincs változás. Létrehozunk azonban két különböző csomópont-megvalósítást, amelyek különböző szolgáltatáskészletet tartalmaznak:

  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 a szervert valósítja meg, és csak kiszolgálóoldali konfigurációra van szüksége. A második csomópont a klienst valósítja meg, és szüksége van a konfiguráció egy másik részére. Mindkét csomópont élettartamra szóló specifikációt igényel. Ennek a szolgáltatásnak a céljaira a csomópont végtelen élettartammal rendelkezik, amelyet le lehet állítani SIGTERM, míg az echo kliens a beállított véges időtartam után leáll. Lásd a indító alkalmazás a részletekért.

Átfogó fejlesztési folyamat

Nézzük meg, hogyan változtatja meg ez a megközelítés a konfigurációval való munkamódszerünket.

A konfiguráció kódként le lesz fordítva, és egy műterméket hoz létre. Ésszerűnek tűnik a konfigurációs melléktermékek elkülönítése más kódtermékektől. Gyakran számos konfigurációnk lehet ugyanazon a kódbázison. És természetesen a különféle konfigurációs ágak több verziója is elérhető. Egy konfigurációban kiválaszthatjuk a könyvtárak bizonyos verzióit, és ez állandó marad, amikor ezt a konfigurációt telepítjük.

A konfiguráció módosítása kódmódosítássá válik. Tehát ugyanaz a minőségbiztosítási eljárás kell, hogy lefedje:

Jegy -> PR -> áttekintés -> összevonás -> folyamatos integráció -> folyamatos telepítés

A megközelítésnek a következő következményei vannak:

  1. A konfiguráció egy adott rendszerpéldányhoz koherens. Úgy tűnik, hogy nincs mód a csomópontok közötti hibás kapcsolatra.
  2. Nem könnyű egyetlen csomópontban megváltoztatni a konfigurációt. Ésszerűtlennek tűnik bejelentkezni és módosítani néhány szöveges fájlt. Így a konfigurációs sodródás kevésbé lehetséges.
  3. Kisebb konfigurációs változtatásokat nem könnyű végrehajtani.
  4. A legtöbb konfigurációs módosítás ugyanazt a fejlesztési folyamatot követi majd, és átmennek bizonyos felülvizsgálaton.

Szükségünk van külön tárolóra az éles konfigurációhoz? Az éles konfiguráció érzékeny információkat tartalmazhat, amelyeket sok embertől távol szeretnénk tartani. Ezért érdemes lehet egy különálló, korlátozott hozzáférésű tárolót tartani, amely tartalmazza az éles konfigurációt. A konfigurációt két részre bonthatjuk – az egyikre a legnyitottabb gyártási paramétereket, a másikra pedig a konfiguráció titkos részét. Ez lehetővé tenné, hogy a legtöbb fejlesztő hozzáférjen a paraméterek túlnyomó többségéhez, miközben korlátozza a hozzáférést az igazán érzékeny dolgokhoz. Ez könnyen megvalósítható köztes tulajdonságokkal, alapértelmezett paraméterértékekkel.

Változatok

Nézzük meg a javasolt megközelítés előnyeit és hátrányait a többi konfigurációkezelési technikához képest.

Először is felsorolunk néhány alternatívát a konfigurálás javasolt kezelési módjának különböző szempontjaihoz:

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

A szöveges fájl némi rugalmasságot biztosít az ad-hoc javítások terén. A rendszeradminisztrátor bejelentkezhet a célcsomópontba, módosíthat, és egyszerűen újraindíthatja a szolgáltatást. Lehet, hogy ez nem olyan jó a nagyobb rendszerek számára. Nem marad nyoma a változásnak. A változást egy másik szempár nem vizsgálja felül. Lehet, hogy nehéz kideríteni, hogy mi okozta a változást. Nem tesztelték. Az elosztott rendszer szempontjából az adminisztrátor egyszerűen elfelejtheti frissíteni a konfigurációt a többi csomópontban.

(Btw, ha végül el kell kezdenünk használni a szöveges konfigurációs fájlokat, akkor csak egy értelmező + érvényesítőt kell hozzáadnunk, amely ugyanazt tudja előállítani Config írja be, és ez elég lenne a szöveges konfigurációk használatához. Ez azt is mutatja, hogy a fordítási idejű konfiguráció bonyolultsága valamivel kisebb, mint a szöveges konfigurációké, mivel a szöveges verzióban szükségünk van némi kiegészítő kódra.)

A központi kulcsérték-tárolás jó mechanizmus az alkalmazások metaparamétereinek elosztására. Itt át kell gondolnunk, hogy mit tekintünk konfigurációs értékeknek, és mit tekintünk csak adatoknak. Adott egy függvény C => A => B ritkán változó értékeket szoktunk nevezni C "konfiguráció", miközben gyakran változott adatok A - csak adatbevitel. A konfigurációt korábban kell megadni a funkcióhoz, mint az adatokhoz A. Ezen elképzelés alapján azt mondhatjuk, hogy a változások várható gyakorisága használható a konfigurációs adatok megkülönböztetésére a pusztán adatoktól. Ezenkívül az adatok általában egy forrásból (felhasználóból) származnak, a konfiguráció pedig egy másik forrásból (adminisztrátor). Az inicializálási folyamat után megváltoztatható paraméterek kezelése az alkalmazás bonyolultságának növekedéséhez vezet. Az ilyen paramétereknél kezelnünk kell a szállítási mechanizmusukat, az elemzést és az érvényesítést, valamint a helytelen értékek kezelését. Ezért a program bonyolultságának csökkentése érdekében jobb, ha csökkentjük azon paraméterek számát, amelyek futás közben változhatnak (vagy akár teljesen megszüntetjük).

A bejegyzés szempontjából különbséget kell tennünk statikus és dinamikus paraméterek között. Ha a szolgáltatási logika megköveteli néhány paraméter ritka megváltoztatását futás közben, akkor ezeket nevezhetjük dinamikus paramétereknek. Ellenkező esetben statikusak, és a javasolt megközelítéssel konfigurálhatók. A dinamikus újrakonfiguráláshoz más megközelítésekre lehet szükség. Például a rendszer egyes részei újraindíthatók az új konfigurációs paraméterekkel, hasonlóan az elosztott rendszer külön folyamatainak újraindításához.
(Szerény véleményem az, hogy kerüljük a futásidejű újrakonfigurálást, mert az növeli a rendszer összetettségét.
Lehet, hogy egyszerűbb az operációs rendszer támogatására hagyatkozni a folyamatok újraindításakor. Bár lehet, hogy ez nem mindig lehetséges.)

A statikus konfiguráció használatának egyik fontos szempontja, amely időnként a dinamikus konfiguráció megfontolására készteti az embereket (egyéb okok nélkül), a szolgáltatás leállása a konfiguráció frissítése során. Való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őre vonatkozó követelmények a különböző rendszerekben eltérőek, ezért előfordulhat, hogy ez nem olyan kritikus. Ha kritikus, akkor előre meg kell terveznünk a rendszer újraindítását. Például megvalósíthatnánk AWS ELB csatlakozás leeresztés. Ebben a forgatókönyvben, amikor újra kell indítanunk a rendszert, párhuzamosan elindítjuk a rendszer új példányát, majd ELB-t váltunk rá, miközben hagyjuk, hogy a régi rendszer befejezze a meglévő kapcsolatok kiszolgálását.

Mi a helyzet, ha a konfigurációt a verziójú műterméken belül vagy azon kívül tartod? A konfigurációnak egy műterméken belüli tartása a legtöbb esetben azt jelenti, hogy ez a konfiguráció ugyanazon a minőségbiztosítási eljáráson ment keresztül, mint a többi műtermék. Így biztos lehet benne, hogy a konfiguráció jó minőségű és megbízható. Ellenkezőleg, a külön fájlban történő konfiguráció azt jelenti, hogy nincs nyoma annak, hogy ki és miért módosította a fájlt. Ez fontos? Meggyőződésünk, hogy a legtöbb termelési rendszernél jobb, ha stabil és jó minőségű konfigurációval rendelkezik.

A műtermék verziója lehetővé teszi, hogy megtudja, mikor jött létre, milyen értékeket tartalmaz, milyen funkciók vannak engedélyezve/letiltva, ki volt a felelős a konfiguráció minden módosításáért. Előfordulhat, hogy némi erőfeszítést igényel, hogy a konfigurációt egy műterméken belül tartsák, és ez egy tervezési döntés.

Előnyök és hátrányok

Itt szeretnénk kiemelni a javasolt megközelítés néhány előnyét és megvitatni néhány hátrányát.

Előnyök

A komplett elosztott rendszer lefordítható konfigurációjának jellemzői:

  1. A konfiguráció statikus ellenőrzése. Ez magas szintű biztonságot ad arra vonatkozóan, hogy a konfiguráció helyes a típuskorlátozások függvényében.
  2. Gazdag konfigurációs nyelv. Tipikusan más konfigurációs megközelítések legfeljebb változó helyettesítésre korlátozódnak.
    A Scala használatával a nyelvi funkciók széles skálája használható a jobb konfiguráció érdekében. Például tulajdonságokat használhatunk alapértelmezett értékek megadására, objektumokat különböző hatókör beállítására, amelyekre hivatkozhatunk vals csak egyszer van megadva a külső hatókörben (DRY). Lehetőség van szó szerinti sorozatok vagy bizonyos osztályok példányainak használatára (Seq, Map, Stb.)
  3. DSL. A Scala megfelelő támogatást nyújt a DSL-íróknak. Ezekkel a szolgáltatásokkal kényelmesebb és végfelhasználóbarátabb konfigurációs nyelvet lehet létrehozni, így a végső konfigurációt legalább a tartomány felhasználói olvashatják.
  4. Integritás és koherencia a csomópontok között. Az egyik előnye annak, ha a teljes elosztott rendszer konfigurációja egy helyen van, hogy minden értéket szigorúan egyszer definiálnak, majd minden olyan helyen újra felhasználják, ahol szükségünk van rá. A biztonságos port deklaráció beírása is biztosítja, hogy minden lehetséges helyes konfigurációban a rendszer csomópontjai ugyanazt a nyelvet beszéljék. A csomópontok között kifejezett függőségek vannak, ami megnehezíti, hogy elfelejtsünk bizonyos szolgáltatásokat nyújtani.
  5. A változtatások magas színvonala. A konfigurációs változásoknak a normál PR-folyamatokon keresztül történő átadásának átfogó megközelítése magas minőségi követelményeket támaszt a konfigurációban is.
  6. Egyidejű konfigurációmódosítások. Amikor bármilyen változtatást végzünk a konfigurációban, az automatikus üzembe helyezés biztosítja, hogy az összes csomópont frissüljön.
  7. Alkalmazás egyszerűsítése. Az alkalmazásnak nem kell elemeznie és ellenőriznie a konfigurációt, és nem kell kezelnie a helytelen konfigurációs értékeket. Ez leegyszerűsíti az általános alkalmazást. (Néhányan bonyolultabbá válik magában a konfiguráció, de ez tudatos kompromisszum a biztonság felé.) Nagyon egyszerű visszatérni a szokásos konfigurációhoz – csak hozzá kell adni a hiányzó részeket. Könnyebb elkezdeni a lefordított konfigurációt, és későbbre halasztani a további elemek megvalósítását.
  8. Verziózott konfiguráció. Tekintettel arra, hogy a konfigurációs változtatások ugyanazt a fejlesztési folyamatot követik, így egyedi verziójú artefaktumot kapunk. Lehetővé teszi, hogy szükség esetén visszaállítsuk a konfigurációt. Akár egy évvel ezelőtt használt konfigurációt is telepíthetünk, és 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. A konfiguráció a fordítási időben rögzített, és nem módosítható könnyen éles rendszeren.
  9. Modularitás. A javasolt keretrendszer moduláris, és a modulok többféleképpen kombinálhatók
    különböző konfigurációk (beállítások/elrendezések) támogatása. Különösen lehetséges egy kis léptékű egy csomópontos elrendezés és egy nagy léptékű több csomópont beállítás. Ésszerű több gyártási elrendezés alkalmazása.
  10. Tesztelés. Tesztelési célokra megvalósítható egy álszolgáltatás, és típusbiztos módon függőségként használható. Egyidejűleg néhány különböző tesztelési elrendezést is fenn lehet tartani, amelyekben különböző részeket helyettesítenek a modellek.
  11. Integrációs tesztelés. Elosztott rendszerekben néha nehézkes az integrációs tesztek futtatása. A leírt megközelítést használva a teljes elosztott rendszer biztonságos konfigurációjának típusára, az összes elosztott részt egyetlen szerveren irányítható módon futtathatjuk. Könnyű utánozni a helyzetet
    amikor valamelyik szolgáltatás elérhetetlenné válik.

Hátrányok

Az összeállított konfigurációs megközelítés eltér a „normál” konfigurációtól, és előfordulhat, hogy nem felel meg minden igénynek. Íme néhány hátránya a lefordított konfigurációnak:

  1. Statikus konfiguráció. Lehet, hogy nem minden alkalmazáshoz alkalmas. Bizonyos esetekben szükség van a konfiguráció gyors javítására a gyártás során, minden biztonsági intézkedés megkerülésével. Ez a megközelítés megnehezíti. A konfiguráció módosítása után a fordításra és az újratelepítésre van szükség. Ez egyszerre jellemző és teher.
  2. Konfiguráció generálása. Amikor a konfigurációt valamilyen automatizálási eszköz hozza létre, ez a megközelítés utólagos fordítást igényel (ami viszont meghiúsulhat). További erőfeszítéseket igényelhet ennek a további lépésnek az összeállítási rendszerbe való integrálása.
  3. Hangszerek. Manapság rengeteg olyan eszközt használnak, amelyek szöveges konfigurációkra támaszkodnak. Néhány közülük
    nem lesz alkalmazható a konfiguráció összeállításakor.
  4. Szemléletváltásra van szükség. A fejlesztők és a DevOps ismerik a szöveges konfigurációs fájlokat. A konfiguráció összeállításának ötlete furcsának tűnhet számukra.
  5. A fordítható konfiguráció bevezetése előtt magas színvonalú szoftverfejlesztési folyamatra van szükség.

A megvalósított példának van néhány korlátozása:

  1. Ha olyan extra konfigurációt biztosítunk, amelyet a csomópont-megvalósítás nem igényel, a fordító nem segít a hiányzó megvalósítás észlelésében. Ezt a használatával lehetne orvosolni HList vagy ADT-k (esetosztályok) a csomópont-konfigurációhoz a tulajdonságok és a tortaminta helyett.
  2. Meg kell adnunk néhány kazánt a konfigurációs fájlban: (package, import, object nyilatkozatok;
    override def's az alapértelmezett értékkel rendelkező paraméterekhez). Ez részben megoldható DSL használatával.
  3. Ebben a bejegyzésben nem foglalkozunk a hasonló csomópontok klasztereinek dinamikus újrakonfigurálásával.

Következtetés

Ebben a bejegyzésben megvitattuk azt az ötletet, hogy a konfigurációt közvetlenül a forráskódban, típusbiztos módon ábrázoljuk. Ez a megközelítés sok alkalmazásban használható az xml- és más szövegalapú konfigurációk helyettesítésére. Annak ellenére, hogy példánkat Scalában implementáltuk, más lefordítható nyelvekre is lefordítható (például Kotlin, C#, Swift stb.). Ki lehet próbálni ezt a megközelítést egy új projektben, és ha nem illik jól, váltson át a régi módra.

Természetesen a lefordítható konfiguráció magas színvonalú fejlesztési folyamatot igényel. Cserébe ugyanilyen jó minőségű robusztus konfigurációt ígér.

Ez a megközelítés többféleképpen bővíthető:

  1. A makrók segítségével konfigurációellenőrzést hajthat végre, és a fordítási időben meghiúsulhat az üzleti logikai megszorítások meghibásodása esetén.
  2. A DSL-t a konfiguráció tartomány-felhasználóbarát módon történő megjelenítésére lehet megvalósítani.
  3. Dinamikus erőforrás-kezelés automatikus konfigurációmódosításokkal. Például, amikor módosítjuk a fürtcsomópontok számát, azt szeretnénk, hogy (1) a csomópontok kissé módosított konfigurációt kapjanak; (2) fürtkezelő az új csomópontok információinak fogadásához.

Kösz

Szeretnék köszönetet mondani Andrey Saksonovnak, Pavel Popovnak és Anton Nehaevnek, hogy inspiráló visszajelzést adtak e bejegyzés tervezetéhez, és segítettek abban, hogy világosabb legyen.

Forrás: will.com