Sadalītās sistēmas kompilējamā konfigurācija

Å ajā rakstā mēs vēlamies dalÄ«ties ar interesantu veidu, kā rÄ«koties ar izplatÄ«tās sistēmas konfigurÄ“Å”anu.
Konfigurācija ir attēlota tieÅ”i Scala valodā droŔā veidā. ÄŖstenoÅ”anas piemērs ir detalizēti aprakstÄ«ts. Tiek apspriesti dažādi priekÅ”likuma aspekti, tostarp ietekme uz kopējo izstrādes procesu.

Sadalītās sistēmas kompilējamā konfigurācija

(krieviski)

Ievads

Lai izveidotu stabilas sadalÄ«tas sistēmas, visos mezglos ir jāizmanto pareiza un saskaņota konfigurācija. Tipisks risinājums ir izmantot tekstuālu izvietoÅ”anas aprakstu (terraformu, ansible vai kaut ko lÄ«dzÄ«gu) un automātiski Ä£enerētus konfigurācijas failus (bieži ā€” katram mezglam/lomai). Mēs arÄ« vēlamies izmantot tos paÅ”us vienas un tās paÅ”as versijas protokolus katrā saziņas mezglā (pretējā gadÄ«jumā radÄ«sies nesaderÄ«bas problēmas). JVM pasaulē tas nozÄ«mē, ka vismaz ziņojumapmaiņas bibliotēkai ir jābÅ«t vienādai versijai visos saziņas mezglos.

Kā ar sistēmas testÄ“Å”anu? Protams, pirms integrācijas testiem mums vajadzētu veikt visu komponentu vienÄ«bu testus. Lai varētu ekstrapolēt testa rezultātus izpildlaikā, mums ir jāpārliecinās, ka visu bibliotēku versijas tiek saglabātas identiskas gan izpildlaika, gan testÄ“Å”anas vidēs.

Veicot integrācijas testus, bieži vien ir daudz vieglāk, ja visos mezglos ir viens klases ceļŔ. Mums tikai jāpārliecinās, ka izvietoÅ”anā tiek izmantots tas pats klases ceļŔ. (Ir iespējams izmantot dažādus klases ceļus dažādos mezglos, taču ir grÅ«tāk attēlot Å”o konfigurāciju un pareizi to izvietot.) Tāpēc, lai lietas bÅ«tu vienkārÅ”as, mēs ņemsim vērā tikai identiskus klases ceļus visos mezglos.

Konfigurācijai ir tendence attīstīties kopā ar programmatūru. Mēs parasti izmantojam versijas, lai identificētu dažādas
programmatÅ«ras evolÅ«cijas posmi. Å Ä·iet saprātÄ«gi iekļaut versiju pārvaldÄ«bā konfigurāciju un identificēt dažādas konfigurācijas ar dažām iezÄ«mēm. Ja ražoÅ”anā ir tikai viena konfigurācija, mēs varam izmantot vienu versiju kā identifikatoru. Dažreiz mums var bÅ«t vairākas ražoÅ”anas vides. Un katrai videi mums var bÅ«t nepiecieÅ”ama atseviŔķa konfigurācijas nozare. Tādējādi konfigurācijas var apzÄ«mēt ar filiāli un versiju, lai unikāli identificētu dažādas konfigurācijas. Katra filiāles etiÄ·ete un versija atbilst vienai sadalÄ«to mezglu, portu, ārējo resursu un klases ceļa bibliotēkas versiju kombinācijai katrā mezglā. Å eit mēs aptversim tikai vienu atzaru un identificēsim konfigurācijas pēc trÄ«skomponentu decimāldaļas (1.2.3), tāpat kā citi artefakti.

Mūsdienu vidē konfigurācijas faili vairs netiek modificēti manuāli. Parasti mēs ģenerējam
konfigurācijas faili izvietoÅ”anas laikā un nekad nepieskarieties tiem pēc tam. Tātad varētu jautāt, kāpēc mēs joprojām izmantojam teksta formātu konfigurācijas failiem? DzÄ«votspējÄ«ga iespēja ir ievietot konfigurāciju kompilācijas vienÄ«bā un gÅ«t labumu no kompilÄ“Å”anas laika konfigurācijas validācijas.

Å ajā ierakstā mēs apskatÄ«sim ideju par konfigurācijas saglabāŔanu apkopotajā artefaktā.

Kompilējamā konfigurācija

Å ajā sadaļā mēs apspriedÄ«sim statiskās konfigurācijas piemēru. Tiek konfigurēti un ieviesti divi vienkārÅ”i pakalpojumi - atbalss pakalpojums un atbalss servisa klients. Pēc tam tiek izveidotas divas dažādas sadalÄ«tas sistēmas ar abiem pakalpojumiem. Viens ir paredzēts viena mezgla konfigurācijai, bet otrs - divu mezglu konfigurācijai.

Tipiska izplatīta sistēma sastāv no dažiem mezgliem. Mezglus var identificēt, izmantojot dažus veidus:

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

vai vienkārŔi

case class NodeId(hostName: String)

vai pat

object Singleton
type NodeId = Singleton.type

Šie mezgli pilda dažādas lomas, vada dažus pakalpojumus un tiem jāspēj sazināties ar citiem mezgliem, izmantojot TCP/HTTP savienojumus.

TCP savienojumam ir nepiecieÅ”ams vismaz porta numurs. Mēs arÄ« vēlamies pārliecināties, ka klients un serveris runā vienā protokolā. Lai modelētu savienojumu starp mezgliem, deklarēsim Ŕādu klasi:

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

kur Port ir tikai an Int atļautajā diapazonā:

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

Rafinēti veidi

redzēt rafinēts bibliotēka. ÄŖsāk sakot, tas ļauj citiem veidiem pievienot kompilÄ“Å”anas laika ierobežojumus. Å ajā gadÄ«jumā Int ir atļauts izmantot tikai 16 bitu vērtÄ«bas, kas var attēlot porta numuru. Å ai konfigurācijas pieejai Ŕī bibliotēka nav jāizmanto. Å Ä·iet, ka tas tikai ļoti labi iederas.

HTTP (REST) ā€‹ā€‹mums var bÅ«t nepiecieÅ”ams arÄ« pakalpojuma ceļŔ:

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

Fantoma tips

Lai kompilācijas laikā identificētu protokolu, mēs izmantojam tipa argumenta deklarÄ“Å”anas lÄ«dzekli Scala Protocol kas klasē netiek izmantots. Tas ir tā sauktais fantoma tips. Izpildes laikā mums reti ir nepiecieÅ”ams protokola identifikatora gadÄ«jums, tāpēc mēs to nesaglabājam. Kompilācijas laikā Å”is fantoma tips sniedz papildu tipa droŔību. Mēs nevaram iziet cauri portam ar nepareizu protokolu.

Viens no visplaŔāk izmantotajiem protokoliem ir REST API ar Json serializāciju:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

kur RequestMessage ir pamata ziņu veids, ko klients var nosūtīt serverim un ResponseMessage ir atbildes ziņojums no servera. Protams, mēs varam izveidot citus protokolu aprakstus, kas norāda sakaru protokolu ar vēlamo precizitāti.

Å Ä«s ziņas nolÅ«kos mēs izmantosim vienkārŔāku protokola versiju:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Å ajā protokolā pieprasÄ«juma ziņojums tiek pievienots url, un atbildes ziņojums tiek atgriezts kā vienkārÅ”a virkne.

Pakalpojuma konfigurāciju var aprakstÄ«t ar pakalpojuma nosaukumu, portu kolekciju un dažām atkarÄ«bām. Ir daži iespējamie veidi, kā visus Å”os elementus attēlot programmā Scala (piemēram, HList, algebrisko datu tipi). Å Ä«s ziņas nolÅ«kos mēs izmantosim kÅ«kas modeli un attēlosim kombinējamus gabalus (moduļus) kā iezÄ«mes. (Å ai kompilējamai konfigurācijas pieejai kÅ«kas raksts nav obligāta prasÄ«ba. Tā ir tikai viena no idejas realizācijas iespējām.)

Atkarības var attēlot, izmantojot kūka modeli kā citu mezglu galapunktus:

  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 pakalpojumam ir nepiecieÅ”ams tikai konfigurēts ports. Un mēs paziņojam, ka Å”is ports atbalsta atbalss protokolu. Ņemiet vērā, ka Å”obrÄ«d mums nav jānorāda konkrēts ports, jo iezÄ«me ļauj deklarēt abstraktas metodes. Ja mēs izmantojam abstraktas metodes, kompilatoram bÅ«s nepiecieÅ”ama ievieÅ”ana konfigurācijas instancē. Å eit mēs esam nodroÅ”inājuÅ”i ievieÅ”anu (8081), un tā tiks izmantota kā noklusējuma vērtÄ«ba, ja mēs to izlaidÄ«sim konkrētā konfigurācijā.

Mēs varam deklarēt atkarību echo pakalpojuma klienta konfigurācijā:

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

AtkarÄ«bai ir tāds pats veids kā echoService. Jo Ä«paÅ”i tas prasa to paÅ”u protokolu. Tādējādi mēs varam bÅ«t pārliecināti, ka, savienojot Ŕīs divas atkarÄ«bas, tās darbosies pareizi.

Pakalpojumu ievieŔana

Pakalpojumam ir nepiecieÅ”ama funkcija, lai palaistu un graciozi izslēgtu. (Spēja izslēgt pakalpojumu ir ļoti svarÄ«ga pārbaudei.) Atkal ir dažas iespējas, kā norādÄ«t Ŕādu funkciju konkrētai konfigurācijai (piemēram, mēs varētu izmantot tipu klases). Å ajā rakstā mēs atkal izmantosim kÅ«kas modeli. Mēs varam pārstāvēt pakalpojumu, izmantojot cats.Resource kas jau nodroÅ”ina dublÄ“Å”anu un resursu atbrÄ«voÅ”anu. Lai iegÅ«tu resursu, mums ir jānodroÅ”ina konfigurācija un kāds izpildlaika konteksts. Tātad pakalpojuma palaiÅ”anas funkcija varētu izskatÄ«ties Ŕādi:

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

kur

  • Config ā€” konfigurācijas veids, kas nepiecieÅ”ams Å”im pakalpojuma starterim
  • AddressResolver ā€” izpildlaika objekts, kam ir iespēja iegÅ«t citu mezglu reālas adreses (sÄ«kāku informāciju lasiet tālāk).

citi veidi nāk no cats:

  • F[_] ā€” efekta veids (vienkārŔākajā gadÄ«jumā F[A] varētu bÅ«t tikai () => A. Å ajā amatā mēs izmantosim cats.IO.)
  • Reader[A,B] ā€” ir vairāk vai mazāk sinonÄ«ms funkcijai A => B
  • cats.Resource ā€” ir veidi, kā iegÅ«t un atbrÄ«vot
  • Timer ā€” ļauj gulēt/mērÄ«t laiku
  • ContextShift - analogs ExecutionContext
  • Applicative ā€” spēkā esoÅ”o funkciju iesaiņojums (gandrÄ«z monāde) (mēs to varētu aizstāt ar kaut ko citu)

Izmantojot Å”o saskarni, mēs varam ieviest dažus pakalpojumus. Piemēram, pakalpojums, kas neko nedara:

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

(SkatÄ«t Pirmkods citu pakalpojumu ievieÅ”anai ā€” atbalss pakalpojums,
echo klients un mūža kontrolieri.)

Mezgls ir viens objekts, kas darbina dažus pakalpojumus (resursu ķēdes sākÅ”anu iespējo Cake Pattern):

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

Ņemiet vērā, ka mezglā mēs norādām precÄ«zu konfigurācijas veidu, kas nepiecieÅ”ams Å”im mezglam. Kompilators neļaus mums izveidot objektu (Cake) ar nepietiekamu tipu, jo katra pakalpojuma iezÄ«me deklarē ierobežojumu Config veids. Bez pilnÄ«gas konfigurācijas mēs nevarēsim palaist mezglu.

Mezglu adreses izŔķirtspēja

Lai izveidotu savienojumu, katram mezglam ir nepiecieÅ”ama reāla resursdatora adrese. Tas varētu bÅ«t zināms vēlāk nekā citas konfigurācijas daļas. Tādējādi mums ir nepiecieÅ”ams veids, kā nodroÅ”ināt kartÄ“Å”anu starp mezgla ID un tā faktisko adresi. Å Ä« kartÄ“Å”ana ir funkcija:

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

Ir daži iespējamie veidi, kā Ä«stenot Ŕādu funkciju.

  1. Ja mēs zinām faktiskās adreses pirms izvietoÅ”anas, mezgla saimniekdatora inscenÄ“Å”anas laikā, mēs varam Ä£enerēt Scala kodu ar faktiskajām adresēm un pēc tam palaist bÅ«vējumu (kas veic kompilÄ“Å”anas laika pārbaudes un pēc tam palaiž integrācijas testu komplektu). Å ajā gadÄ«jumā mÅ«su kartÄ“Å”anas funkcija ir zināma statiski, un to var vienkārÅ”ot lÄ«dz a Map[NodeId, NodeAddress].
  2. Dažreiz mēs iegÅ«stam faktiskās adreses tikai vēlākā brÄ«dÄ«, kad mezgls faktiski tiek palaists, vai arÄ« mums nav vēl nesāktu mezglu adreses. Å ajā gadÄ«jumā mums var bÅ«t atklāŔanas pakalpojums, kas tiek palaists pirms visiem citiem mezgliem, un katrs mezgls var reklamēt savu adresi Å”ajā pakalpojumā un abonēt atkarÄ«bas.
  3. Ja mēs varam mainÄ«t /etc/hosts, mēs varam izmantot iepriekÅ” definētus resursdatora nosaukumus (piemēram, my-project-main-node un echo-backend) un vienkārÅ”i saistiet Å”o nosaukumu ar IP adresi izvietoÅ”anas laikā.

Å ajā ziņojumā mēs sÄ«kāk neaptveram Å”os gadÄ«jumus. Faktiski mÅ«su rotaļlietu piemērā visiem mezgliem bÅ«s viena un tā pati IP adrese - 127.0.0.1.

Šajā ziņojumā mēs apskatīsim divus izplatītās sistēmas izkārtojumus:

  1. Viena mezgla izkārtojums, kur visi pakalpojumi ir izvietoti vienā mezglā.
  2. Divu mezglu izkārtojums, kur pakalpojums un klients atrodas dažādos mezglos.

Konfigurācija a viens mezgls izkārtojums ir Ŕāds:

Viena mezgla konfigurācija

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

Å eit mēs izveidojam vienu konfigurāciju, kas paplaÅ”ina gan servera, gan klienta konfigurāciju. Mēs arÄ« konfigurējam dzÄ«ves cikla kontrolieri, kas pēc tam parasti pārtrauc klienta un servera darbÄ«bu lifetime intervāls iet.

To paÅ”u pakalpojumu ievieÅ”anu un konfigurāciju kopu var izmantot, lai izveidotu sistēmas izkārtojumu ar diviem atseviŔķiem mezgliem. Mums tikai jārada divas atseviŔķas mezgla konfigurācijas ar atbilstoÅ”iem pakalpojumiem:

Divu mezglu konfigurācija

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

Skatiet, kā mēs norādām atkarÄ«bu. Mēs pieminam otra mezgla sniegto pakalpojumu kā paÅ”reizējā mezgla atkarÄ«bu. AtkarÄ«bas veids ir pārbaudÄ«ts, jo tajā ir fantoma tips, kas apraksta protokolu. Un izpildlaikā mums bÅ«s pareizais mezgla ID. Å is ir viens no svarÄ«gākajiem piedāvātās konfigurācijas pieejas aspektiem. Tas nodroÅ”ina mums iespēju iestatÄ«t portu tikai vienu reizi un pārliecināties, ka mēs atsaucamies uz pareizo portu.

Divu mezglu ievieŔana

Å ai konfigurācijai mēs izmantojam tieÅ”i tādas paÅ”as pakalpojumu ievieÅ”anas. Izmaiņu vispār nav. Tomēr mēs izveidojam divus dažādus mezglu implementācijas, kas satur dažādas pakalpojumu kopas:

  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
  }

Pirmais mezgls ievieÅ” serveri, un tam ir nepiecieÅ”ama tikai servera puses konfigurācija. Otrais mezgls ievieÅ” klientu, un tam ir nepiecieÅ”ama cita konfigurācijas daļa. Abiem mezgliem ir nepiecieÅ”ama noteikta ekspluatācijas laika specifikācija. Å Ä« pasta pakalpojuma vajadzÄ«bām mezglam bÅ«s bezgalÄ«gs kalpoÅ”anas laiks, ko var pārtraukt, izmantojot SIGTERM, savukārt echo klienta darbÄ«ba tiks pārtraukta pēc konfigurētā ierobežotā ilguma. SkatÄ«t startera lietojumprogramma sÄ«kāku informāciju.

Kopējais attīstības process

ApskatÄ«sim, kā Ŕī pieeja maina veidu, kā mēs strādājam ar konfigurāciju.

Konfigurācija kā kods tiks apkopota un izveidos artefaktu. Å Ä·iet saprātÄ«gi nodalÄ«t konfigurācijas artefaktus no citiem koda artefaktiem. Bieži vien mums var bÅ«t daudz konfigurāciju vienā koda bāzē. Un, protams, mums var bÅ«t vairākas dažādu konfigurācijas atzaru versijas. Konfigurācijā mēs varam atlasÄ«t noteiktas bibliotēku versijas, un tas paliks nemainÄ«gs ikreiz, kad izvietosim Å”o konfigurāciju.

Konfigurācijas maiņa kļūst par koda maiņu. Tātad uz to bÅ«tu jāattiecas vienā un tajā paŔā kvalitātes nodroÅ”ināŔanas procesā:

Biļete -> PR -> pārskatīŔana -> sapludināŔana -> nepārtraukta integrācija -> nepārtraukta izvietoŔana

Šādai pieejai ir Ŕādas sekas:

  1. Konfigurācija ir saskaņota konkrētas sistēmas gadījumam. Šķiet, ka nav iespējams izveidot nepareizu savienojumu starp mezgliem.
  2. Nav viegli mainīt konfigurāciju tikai vienā mezglā. Šķiet nesaprātīgi pieteikties un mainīt dažus teksta failus. Tādējādi konfigurācijas novirze kļūst mazāk iespējama.
  3. Nelielas konfigurācijas izmaiņas nav viegli veikt.
  4. Lielākā daļa konfigurācijas izmaiņu tiks veiktas vienā un tajā paŔā izstrādes procesā, un tās tiks pārskatÄ«tas.

Vai mums ir nepiecieÅ”ama atseviŔķa repozitorija ražoÅ”anas konfigurācijai? RažoÅ”anas konfigurācijā var bÅ«t ietverta sensitÄ«va informācija, kuru mēs vēlētos glabāt daudziem cilvēkiem nepieejamā vietā. Tāpēc varētu bÅ«t vērts saglabāt atseviŔķu repozitoriju ar ierobežotu piekļuvi, kurā bÅ«s ietverta ražoÅ”anas konfigurācija. Mēs varam sadalÄ«t konfigurāciju divās daļās - vienā, kas satur visatvērtākos ražoÅ”anas parametrus, un tajā, kas satur slepeno konfigurācijas daļu. Tas ļautu lielākajai daļai izstrādātāju piekļūt lielākajai daļai parametru, vienlaikus ierobežojot piekļuvi patieŔām jutÄ«gām lietām. To ir viegli paveikt, izmantojot starpposma pazÄ«mes ar noklusējuma parametru vērtÄ«bām.

Variācijas

Apskatīsim piedāvātās pieejas plusus un mīnusus salīdzinājumā ar citām konfigurācijas pārvaldības metodēm.

Pirmkārt, mēs uzskaitīsim dažas alternatīvas dažādiem piedāvātā konfigurācijas veida aspektiem:

  1. Teksta fails mērÄ·a maŔīnā.
  2. Centralizēta atslēgu vērtību krātuve (piemēram, etcd/zookeeper).
  3. ApakÅ”procesa komponenti, kurus var pārkonfigurēt/restartēt bez procesa restartÄ“Å”anas.
  4. Konfigurācija ārpus artefaktu un versiju kontroles.

Teksta fails nodroÅ”ina zināmu elastÄ«bu ad-hoc labojumu ziņā. Sistēmas administrators var pieteikties mērÄ·a mezglā, veikt izmaiņas un vienkārÅ”i restartēt pakalpojumu. Tas var nebÅ«t tik labs lielākām sistēmām. Aiz pārmaiņām nav atstātas nekādas pēdas. Izmaiņas nepārskata cits acu pāris. Var bÅ«t grÅ«ti noskaidrot, kas ir izraisÄ«jis izmaiņas. Tas nav pārbaudÄ«ts. No sadalÄ«tās sistēmas viedokļa administrators var vienkārÅ”i aizmirst atjaunināt konfigurāciju vienā no citiem mezgliem.

(Btw, ja galu galā bÅ«s jāsāk lietot teksta konfigurācijas faili, mums bÅ«s jāpievieno tikai parsētājs + validators, kas varētu radÄ«t to paÅ”u Config ierakstiet un ar to pietiktu, lai sāktu lietot teksta konfigurācijas. Tas arÄ« parāda, ka kompilÄ“Å”anas laika konfigurācijas sarežģītÄ«ba ir nedaudz mazāka nekā teksta konfigurāciju sarežģītÄ«ba, jo teksta versijā mums ir nepiecieÅ”ams papildu kods.)

Centralizēta atslēgu vērtÄ«bu krātuve ir labs mehānisms lietojumprogrammas meta parametru izplatÄ«Å”anai. Å eit mums ir jādomā par to, ko mēs uzskatām par konfigurācijas vērtÄ«bām un kas ir tikai dati. Dota funkcija C => A => B mēs parasti saucam par reti mainÄ«gām vērtÄ«bām C "konfigurācija", bet bieži mainÄ«ti dati A - tikai ievadiet datus. Konfigurācija ir jānodroÅ”ina funkcijai agrāk nekā dati A. Ņemot vērā Å”o ideju, mēs varam teikt, ka paredzamais izmaiņu biežums var tikt izmantots, lai atŔķirtu konfigurācijas datus no tikai datiem. ArÄ« dati parasti nāk no viena avota (lietotāja), un konfigurācija nāk no cita avota (administrators). Darbs ar parametriem, kurus var mainÄ«t pēc inicializācijas procesa, palielina lietojumprogrammu sarežģītÄ«bu. Šādiem parametriem mums bÅ«s jāapstrādā to piegādes mehānisms, parsÄ“Å”ana un validācija, kā arÄ« nepareizu vērtÄ«bu apstrāde. Tāpēc, lai samazinātu programmas sarežģītÄ«bu, mēs labāk samazinām to parametru skaitu, kas var mainÄ«ties izpildes laikā (vai pat tos pilnÄ«bā likvidējam).

No Ŕīs ziņas viedokļa mums vajadzētu noŔķirt statiskos un dinamiskos parametrus. Ja pakalpojumu loÄ£ika prasa retas dažu parametru izmaiņas izpildes laikā, mēs tos varam saukt par dinamiskiem parametriem. Pretējā gadÄ«jumā tie ir statiski un var tikt konfigurēti, izmantojot piedāvāto pieeju. Dinamiskajai pārkonfigurācijai var bÅ«t nepiecieÅ”amas citas pieejas. Piemēram, sistēmas daļas var restartēt ar jaunajiem konfigurācijas parametriem lÄ«dzÄ«gi kā atseviŔķu sadalÄ«tas sistēmas procesu restartÄ“Å”ana.
(Mans pazemīgais viedoklis ir izvairīties no izpildlaika pārkonfigurācijas, jo tas palielina sistēmas sarežģītību.
Varētu bÅ«t vienkārŔāk paļauties uz OS atbalstu procesu restartÄ“Å”anai. Tomēr tas ne vienmēr var bÅ«t iespējams.)

Viens svarÄ«gs statiskās konfigurācijas izmantoÅ”anas aspekts, kas dažreiz liek cilvēkiem apsvērt dinamisku konfigurāciju (bez citiem iemesliem), ir pakalpojuma dÄ«kstāve konfigurācijas atjaunināŔanas laikā. PatieŔām, ja mums ir jāveic izmaiņas statiskajā konfigurācijā, mums ir jārestartē sistēma, lai jaunas vērtÄ«bas kļūtu efektÄ«vas. PrasÄ«bas attiecÄ«bā uz dÄ«kstāvi dažādām sistēmām atŔķiras, tāpēc tas var nebÅ«t tik kritisks. Ja tas ir kritisks, mums ir iepriekÅ” jāplāno sistēmas restartÄ“Å”ana. Piemēram, mēs varētu Ä«stenot AWS ELB savienojuma iztukÅ”oÅ”ana. Å ajā scenārijā ikreiz, kad mums ir jārestartē sistēma, mēs paralēli startējam jaunu sistēmas gadÄ«jumu, pēc tam pārslēdzam uz to ELB, vienlaikus ļaujot vecajai sistēmai pabeigt esoÅ”o savienojumu apkalpoÅ”anu.

Kā ar konfigurācijas saglabāŔanu artefakta versijā vai ārpusē? Konfigurācijas saglabāŔana artefaktā vairumā gadÄ«jumu nozÄ«mē, ka Ŕī konfigurācija ir izturējusi tādu paÅ”u kvalitātes nodroÅ”ināŔanas procesu kā citi artefakti. Tātad varētu bÅ«t pārliecināts, ka konfigurācija ir labas kvalitātes un uzticama. Gluži pretēji, konfigurācija atseviŔķā failā nozÄ«mē, ka nav izsekojamÄ«bas par to, kurÅ” un kāpēc veica izmaiņas Å”ajā failā. Vai tas ir svarÄ«gi? Mēs uzskatām, ka lielākajai daļai ražoÅ”anas sistēmu labāk ir nodroÅ”ināt stabilu un kvalitatÄ«vu konfigurāciju.

Artefakta versija ļauj noskaidrot, kad tas tika izveidots, kādas vērtÄ«bas tas satur, kādas funkcijas ir iespējotas/atspējotas, kurÅ” bija atbildÄ«gs par katras izmaiņas konfigurācijā. Lai saglabātu konfigurāciju artefaktā, var bÅ«t vajadzÄ«gas zināmas pÅ«les, un tā ir dizaina izvēle.

Plusi mīnusi

Å eit mēs vēlamies izcelt dažas piedāvātās pieejas priekÅ”rocÄ«bas un apspriest dažus trÅ«kumus.

PriekŔrocības

Pilnīgas sadalītās sistēmas kompilējamās konfigurācijas iezīmes:

  1. Statiskā konfigurācijas pārbaude. Tas nodroÅ”ina augstu pārliecÄ«bas lÄ«meni, ka konfigurācija ir pareiza, ņemot vērā veida ierobežojumus.
  2. Bagātīga konfigurācijas valoda. Parasti citas konfigurācijas pieejas aprobežojas ar maksimāli mainīgu aizstāŔanu.
    Izmantojot Scala, var izmantot plaÅ”u valodu funkciju klāstu, lai uzlabotu konfigurāciju. Piemēram, mēs varam izmantot iezÄ«mes, lai nodroÅ”inātu noklusējuma vērtÄ«bas, objektus, lai iestatÄ«tu dažādu darbÄ«bas jomu, mēs varam atsaukties uz tiem vals definēts tikai vienu reizi ārējā tvērumā (DRY). Ir iespējams izmantot burtiskas secÄ«bas vai noteiktu klaÅ”u gadÄ«jumus (Seq, Map, Utt.)
  3. DSL. Scala nodroÅ”ina pienācÄ«gu atbalstu DSL rakstÄ«tājiem. Å os lÄ«dzekļus var izmantot, lai izveidotu ērtāku un lietotājam draudzÄ«gāku konfigurācijas valodu, lai galÄ«gā konfigurācija bÅ«tu vismaz lasāma domēna lietotājiem.
  4. Integritāte un saskaņotÄ«ba mezglos. Viena no priekÅ”rocÄ«bām, ko sniedz visas izplatÄ«tās sistēmas konfigurācija vienuviet, ir tā, ka visas vērtÄ«bas tiek stingri noteiktas vienreiz un pēc tam tiek izmantotas atkārtoti visās vietās, kur mums tās ir vajadzÄ«gas. ArÄ« droŔā porta deklarāciju ierakstÄ«Å”ana nodroÅ”ina, ka visās iespējamās pareizajās konfigurācijās sistēmas mezgli runās vienā valodā. Starp mezgliem ir skaidra atkarÄ«ba, kas apgrÅ«tina dažu pakalpojumu sniegÅ”anu.
  5. Augsta izmaiņu kvalitāte. Kopējā pieeja konfigurācijas izmaiņu nodoÅ”anai parastajā PR procesā nosaka augstus kvalitātes standartus arÄ« konfigurācijā.
  6. VienlaicÄ«gas konfigurācijas izmaiņas. Ikreiz, kad veicam izmaiņas konfigurācijā, automātiskā izvietoÅ”ana nodroÅ”ina, ka visi mezgli tiek atjaunināti.
  7. Lietojumprogrammas vienkārÅ”oÅ”ana. Lietojumprogrammai nav jāparsē un jāapstiprina konfigurācija un jāapstrādā nepareizas konfigurācijas vērtÄ«bas. Tas vienkārÅ”o vispārējo lietojumprogrammu. (Neliels sarežģītÄ«bas pieaugums ir paŔā konfigurācijā, taču tas ir apzināts kompromiss pret droŔību.) Atgriezties pie parastās konfigurācijas ir diezgan vienkārÅ”i ā€” vienkārÅ”i pievienojiet trÅ«kstoŔās daļas. VienkārŔāk ir sākt ar apkopoto konfigurāciju un atlikt papildu daļu ievieÅ”anu uz vēlāku laiku.
  8. Versionēta konfigurācija. Tā kā konfigurācijas izmaiņas notiek vienā un tajā paŔā izstrādes procesā, mēs iegÅ«stam artefaktu ar unikālu versiju. Tas ļauj mums pārslēgt konfigurāciju atpakaļ, ja nepiecieÅ”ams. Mēs pat varam izvietot konfigurāciju, kas tika izmantota pirms gada, un tā darbosies tieÅ”i tāpat. Stabila konfigurācija uzlabo sadalÄ«tās sistēmas paredzamÄ«bu un uzticamÄ«bu. Konfigurācija tiek fiksēta kompilÄ“Å”anas laikā, un to nevar viegli mainÄ«t ražoÅ”anas sistēmā.
  9. Modularitāte. Piedāvātā sistēma ir modulāra, un moduļus var kombinēt dažādos veidos
    atbalsta dažādas konfigurācijas (iestatÄ«jumus/izkārtojumus). Jo Ä«paÅ”i ir iespējams izmantot maza mēroga viena mezgla izkārtojumu un liela mēroga vairāku mezglu iestatÄ«jumu. Ir saprātÄ«gi izmantot vairākus ražoÅ”anas izkārtojumus.
  10. TestÄ“Å”ana. TestÄ“Å”anas nolÅ«kos var ieviest viltotu pakalpojumu un izmantot to kā atkarÄ«bu tipa droŔā veidā. Vienlaikus varētu uzturēt dažus dažādus testÄ“Å”anas izkārtojumus ar dažādām daļām, kas aizstātas ar izspēles.
  11. Integrācijas pārbaude. Dažreiz sadalÄ«tās sistēmās ir grÅ«ti palaist integrācijas testus. Izmantojot aprakstÄ«to pieeju visas sadalÄ«tās sistēmas droÅ”as konfigurācijas ievadÄ«Å”anai, mēs varam kontrolēt visas sadalÄ«tās daļas vienā serverÄ«. Ir viegli lÄ«dzināties situācijai
    kad kāds no pakalpojumiem kļūst nepieejams.

Trūkumi

Apkopotā konfigurācijas pieeja atŔķiras no ā€œparastāsā€ konfigurācijas, un tā var neatbilst visām vajadzÄ«bām. Å eit ir daži no kompilētās konfigurācijas trÅ«kumiem:

  1. Statiskā konfigurācija. Tas var nebÅ«t piemērots visām lietojumprogrammām. Dažos gadÄ«jumos ir nepiecieÅ”ams ātri noteikt konfigurāciju ražoÅ”anā, apejot visus droŔības pasākumus. Å Ä« pieeja padara to grÅ«tāku. Pēc jebkādu konfigurācijas izmaiņu veikÅ”anas ir nepiecieÅ”ama kompilācija un pārizvietoÅ”ana. Tā ir gan iezÄ«me, gan slogs.
  2. Konfigurācijas Ä£enerÄ“Å”ana. Ja konfigurāciju Ä£enerē kāds automatizācijas rÄ«ks, Å”ai pieejai ir nepiecieÅ”ama turpmāka kompilācija (kas savukārt var neizdoties). Iespējams, bÅ«s nepiecieÅ”amas papildu pÅ«les, lai integrētu Å”o papildu darbÄ«bu veidoÅ”anas sistēmā.
  3. Instrumenti. Mūsdienās tiek izmantots daudz rīku, kas balstās uz teksta konfigurācijām. Daži no tiem
    nebūs piemērojams, kad tiks apkopota konfigurācija.
  4. Ir nepiecieÅ”ama domāŔanas veida maiņa. Izstrādātāji un DevOps ir pazÄ«stami ar teksta konfigurācijas failiem. Ideja par konfigurācijas apkopoÅ”anu viņiem varētu Ŕķist dÄ«vaina.
  5. Pirms kompilējamās konfigurācijas ievieÅ”anas ir nepiecieÅ”ams augstas kvalitātes programmatÅ«ras izstrādes process.

Ieviestajam piemēram ir daži ierobežojumi:

  1. Ja mēs nodroÅ”inām papildu konfigurāciju, kas nav nepiecieÅ”ama mezgla ievieÅ”anai, kompilators mums nepalÄ«dzēs noteikt neesoÅ”u implementāciju. To var atrisināt, izmantojot HList vai ADT (gadÄ«juma klases) mezgla konfigurācijai, nevis pazÄ«mēm un kÅ«ka modelim.
  2. Mums konfigurācijas failā ir jānodroŔina daži parametri: (package, import, object deklarācijas;
    override def's parametriem, kuriem ir noklusējuma vērtības). To var daļēji atrisināt, izmantojot DSL.
  3. Šajā rakstā mēs neaptveram līdzīgu mezglu klasteru dinamisko pārkonfigurāciju.

Secinājumi

Å ajā ziņojumā mēs esam apsprieduÅ”i ideju par konfigurācijas attēloÅ”anu tieÅ”i avota kodā droŔā veidā. Å o pieeju varētu izmantot daudzās lietojumprogrammās, aizstājot xml un citas teksta konfigurācijas. Neskatoties uz to, ka mÅ«su piemērs ir ieviests programmā Scala, to var tulkot arÄ« citās kompilējamās valodās (piemēram, Kotlin, C#, Swift utt.). Å o pieeju varētu izmēģināt jaunā projektā un gadÄ«jumā, ja tā neder, pāriet uz vecmodÄ«go metodi.

Protams, kompilējamai konfigurācijai nepiecieÅ”ams augstas kvalitātes izstrādes process. Savukārt tas sola nodroÅ”ināt tikpat augstas kvalitātes robustu konfigurāciju.

Šo pieeju var paplaŔināt dažādos veidos:

  1. Var izmantot makro, lai veiktu konfigurācijas validāciju un kompilÄ“Å”anas laikā neizdodas, ja rodas jebkādas biznesa loÄ£ikas ierobežojumu kļūmes.
  2. DSL var ieviest, lai attēlotu konfigurāciju domēnam lietotājam draudzīgā veidā.
  3. Dinamiska resursu pārvaldība ar automātiskām konfigurācijas korekcijām. Piemēram, kad mēs pielāgojam klastera mezglu skaitu, mēs varētu vēlēties, lai (1) mezgli iegūtu nedaudz modificētu konfigurāciju; (2) klasteru pārvaldnieks, lai saņemtu informāciju par jauniem mezgliem.

Paldies

Es vēlos pateikties Andrejam Saksonovam, Pāvelam Popovam un Antonam Ņehaevam par iedvesmojoŔām atsauksmēm par Ŕī ieraksta melnrakstu, kas man palÄ«dzēja to padarÄ«t skaidrāku.

Avots: www.habr.com