Compiled Distributed System Configuration

Nais kong sabihin sa iyo ang isang kawili-wiling mekanismo para sa pagtatrabaho sa pagsasaayos ng isang distributed system. Direktang kinakatawan ang configuration sa isang pinagsama-samang wika (Scala) gamit ang mga ligtas na uri. Ang post na ito ay nagbibigay ng isang halimbawa ng naturang pagsasaayos at tinatalakay ang iba't ibang aspeto ng pagpapatupad ng isang pinagsama-samang pagsasaayos sa pangkalahatang proseso ng pag-unlad.

Compiled Distributed System Configuration

(Ingles)

Pagpapakilala

Ang pagbuo ng isang maaasahang distributed system ay nangangahulugan na ang lahat ng mga node ay gumagamit ng tamang configuration, na naka-synchronize sa iba pang mga node. Ang mga teknolohiya ng DevOps (terraform, ansible o isang katulad nito) ay karaniwang ginagamit upang awtomatikong bumuo ng mga configuration file (kadalasang partikular para sa bawat node). Nais din naming makatiyak na ang lahat ng nakikipag-usap na mga node ay gumagamit ng magkatulad na mga protocol (kabilang ang parehong bersyon). Kung hindi, ang hindi pagkakatugma ay mabubuo sa aming ipinamahagi na sistema. Sa mundo ng JVM, ang isang kahihinatnan ng pangangailangang ito ay ang parehong bersyon ng library na naglalaman ng mga protocol na mensahe ay dapat gamitin sa lahat ng dako.

Paano naman ang pagsubok sa isang distributed system? Siyempre, ipinapalagay namin na ang lahat ng mga bahagi ay may mga pagsubok sa yunit bago kami magpatuloy sa pagsubok sa pagsasama. (Upang ma-extrapolate namin ang mga resulta ng pagsubok sa runtime, dapat din kaming magbigay ng magkaparehong hanay ng mga library sa yugto ng pagsubok at sa runtime.)

Kapag nagtatrabaho sa mga pagsubok sa pagsasama, kadalasan ay mas madaling gamitin ang parehong classpath sa lahat ng dako sa lahat ng node. Ang kailangan lang nating gawin ay tiyakin na ang parehong classpath ay ginagamit sa runtime. (Bagama't ganap na posible na magpatakbo ng iba't ibang mga node na may iba't ibang mga classpath, ito ay nagdaragdag ng pagiging kumplikado sa pangkalahatang configuration at mga kahirapan sa pag-deploy at mga pagsubok sa pagsasama.) Para sa mga layunin ng post na ito, ipinapalagay namin na ang lahat ng mga node ay gagamit ng parehong classpath.

Ang pagsasaayos ay nagbabago kasama ang application. Gumagamit kami ng mga bersyon upang matukoy ang iba't ibang yugto ng ebolusyon ng programa. Mukhang lohikal din na tukuyin ang iba't ibang bersyon ng mga pagsasaayos. At ilagay ang configuration mismo sa version control system. Kung mayroon lamang isang configuration sa produksyon, maaari lang nating gamitin ang numero ng bersyon. Kung gagamit tayo ng maraming pagkakataon sa produksyon, kakailanganin natin ng ilan
mga sangay ng pagsasaayos at isang karagdagang label bilang karagdagan sa bersyon (halimbawa, ang pangalan ng sangay). Sa ganitong paraan, malinaw nating matukoy ang eksaktong configuration. Ang bawat configuration identifier ay katangi-tanging tumutugma sa isang partikular na kumbinasyon ng mga ibinahagi na node, port, panlabas na mapagkukunan, at mga bersyon ng library. Para sa mga layunin ng post na ito, ipagpalagay namin na mayroon lamang isang sangay at matutukoy namin ang pagsasaayos sa karaniwang paraan gamit ang tatlong numero na pinaghihiwalay ng isang tuldok (1.2.3).

Sa modernong mga kapaligiran, ang mga configuration file ay bihirang ginawa nang manu-mano. Mas madalas na nabuo ang mga ito sa panahon ng pag-deploy at hindi na nahawakan (kaya huwag sirain ang anumang bagay). Isang natural na tanong ang lumitaw: bakit gumagamit pa rin kami ng format ng teksto upang mag-imbak ng configuration? Ang isang mabubuhay na alternatibo ay tila ang kakayahang gumamit ng regular na code para sa pagsasaayos at makinabang mula sa mga pagsusuri sa oras ng pag-compile.

Sa post na ito, tuklasin natin ang ideya ng kumakatawan sa isang pagsasaayos sa loob ng isang pinagsama-samang artifact.

Pinagsama-samang pagsasaayos

Ang seksyong ito ay nagbibigay ng isang halimbawa ng isang static na pinagsama-samang pagsasaayos. Dalawang simpleng serbisyo ang ipinatupad - ang echo service at ang echo service client. Batay sa dalawang serbisyong ito, dalawang opsyon sa system ang binuo. Sa isang pagpipilian, ang parehong mga serbisyo ay matatagpuan sa parehong node, sa isa pang pagpipilian - sa iba't ibang mga node.

Karaniwan ang isang distributed system ay naglalaman ng ilang node. Maaari mong matukoy ang mga node gamit ang mga halaga ng ilang uri NodeId:

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

o

case class NodeId(hostName: String)

o kahit na

object Singleton
type NodeId = Singleton.type

Gumaganap ang mga node ng iba't ibang tungkulin, nagpapatakbo sila ng mga serbisyo at maaaring maitatag ang mga koneksyon sa TCP/HTTP sa pagitan nila.

Upang ilarawan ang isang koneksyon sa TCP kailangan namin ng hindi bababa sa isang numero ng port. Gusto rin naming ipakita ang protocol na sinusuportahan sa port na iyon upang matiyak na pareho ang protocol na ginagamit ng kliyente at server. Ilalarawan namin ang koneksyon gamit ang sumusunod na klase:

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

saan Port - isang integer lang Int na nagpapahiwatig ng hanay ng mga katanggap-tanggap na halaga:

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

Mga pinong uri

Tingnan ang library pino ΠΈ aking ulat. Sa madaling salita, pinapayagan ka ng library na magdagdag ng mga hadlang sa mga uri na sinusuri sa oras ng pag-compile. Sa kasong ito, ang mga wastong halaga ng numero ng port ay mga 16-bit na integer. Para sa isang pinagsama-samang pagsasaayos, ang paggamit ng pinong library ay hindi sapilitan, ngunit pinapabuti nito ang kakayahan ng compiler na suriin ang pagsasaayos.

Para sa mga protocol ng HTTP (REST), bilang karagdagan sa numero ng port, maaaring kailanganin din namin ang landas patungo sa serbisyo:

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

Mga uri ng multo

Upang matukoy ang protocol sa oras ng pag-compile, gumagamit kami ng isang uri ng parameter na hindi ginagamit sa loob ng klase. Ang desisyong ito ay dahil sa katotohanang hindi kami gumagamit ng instance ng protocol sa runtime, ngunit gusto naming suriin ng compiler ang compatibility ng mga protocol. Sa pamamagitan ng pagtukoy sa protocol, hindi namin maipapasa ang isang hindi naaangkop na serbisyo bilang isang dependency.

Ang isa sa mga karaniwang protocol ay ang REST API na may serialization ng Json:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

saan RequestMessage - Uri ng Kahilingan, ResponseMessage - uri ng tugon.
Siyempre, maaari naming gamitin ang iba pang mga paglalarawan ng protocol na nagbibigay ng katumpakan ng paglalarawan na kailangan namin.

Para sa mga layunin ng post na ito, gagamit kami ng pinasimpleng bersyon ng protocol:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Narito ang kahilingan ay isang string na nakadugtong sa url at ang tugon ay ang ibinalik na string sa katawan ng tugon ng HTTP.

Ang configuration ng serbisyo ay inilalarawan ng pangalan ng serbisyo, mga port, at mga dependency. Ang mga elementong ito ay maaaring katawanin sa Scala sa maraming paraan (halimbawa, HList-s, algebraic na mga uri ng data). Para sa mga layunin ng post na ito, gagamitin namin ang Cake Pattern at kumakatawan sa mga module na gumagamit trait's. (Ang Cake Pattern ay hindi kinakailangang elemento ng diskarteng ito. Isa lamang itong posibleng pagpapatupad.)

Ang mga dependency sa pagitan ng mga serbisyo ay maaaring kinakatawan bilang mga pamamaraan na nagbabalik ng mga port EndPointng iba pang mga node:

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

Upang lumikha ng serbisyo ng echo, ang kailangan mo lang ay isang numero ng port at isang indikasyon na sinusuportahan ng port ang echo protocol. Maaaring hindi kami tumukoy ng isang partikular na port, dahil... Ang mga katangian ay nagpapahintulot sa iyo na magpahayag ng mga pamamaraan nang walang pagpapatupad (mga abstract na pamamaraan). Sa kasong ito, kapag lumilikha ng isang kongkretong pagsasaayos, hihilingin sa amin ng compiler na magbigay ng pagpapatupad ng abstract na pamamaraan at magbigay ng numero ng port. Dahil ipinatupad namin ang pamamaraan, kapag gumagawa ng isang partikular na configuration, maaaring hindi kami tumukoy ng ibang port. Gagamitin ang default na halaga.

Sa pagsasaayos ng kliyente, ipinapahayag namin ang isang dependency sa serbisyo ng echo:

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

Ang dependency ay kapareho ng uri ng na-export na serbisyo echoService. Sa partikular, sa echo client kailangan namin ang parehong protocol. Samakatuwid, kapag kumokonekta sa dalawang serbisyo, maaari naming siguraduhin na ang lahat ay gagana nang tama.

Pagpapatupad ng mga serbisyo

Ang isang function ay kinakailangan upang simulan at ihinto ang serbisyo. (Ang kakayahang ihinto ang serbisyo ay kritikal para sa pagsubok.) Muli, mayroong ilang mga opsyon para sa pagpapatupad ng naturang tampok (halimbawa, maaari kaming gumamit ng mga uri ng klase batay sa uri ng pagsasaayos). Para sa mga layunin ng post na ito, gagamitin namin ang Cake Pattern. Kakatawanin namin ang serbisyo gamit ang isang klase cats.Resource, dahil Nagbibigay na ang klase na ito ng mga paraan upang ligtas na magarantiya ang pagpapalabas ng mga mapagkukunan kung sakaling magkaroon ng mga problema. Upang makakuha ng mapagkukunan, kailangan naming magbigay ng configuration at isang handa na runtime na konteksto. Ang function ng startup ng serbisyo ay maaaring magmukhang ganito:

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

saan

  • Config β€” uri ng pagsasaayos para sa serbisyong ito
  • AddressResolver β€” isang runtime object na nagpapahintulot sa iyo na malaman ang mga address ng iba pang mga node (tingnan sa ibaba)

at iba pang uri mula sa aklatan cats:

  • F[_] β€” uri ng epekto (sa pinakasimpleng kaso F[A] maaaring isang function lamang () => A. Sa post na ito ay gagamitin natin cats.IO.)
  • Reader[A,B] - higit pa o hindi gaanong kasingkahulugan ng function A => B
  • cats.Resource - isang mapagkukunan na maaaring makuha at ilabas
  • Timer β€” timer (nagbibigay-daan sa iyo na makatulog nang ilang sandali at sukatin ang mga agwat ng oras)
  • ContextShift - analogue ExecutionContext
  • Applicative β€” isang klase ng uri ng epekto na nagpapahintulot sa iyo na pagsamahin ang mga indibidwal na epekto (halos isang monad). Sa mas kumplikadong mga application ay tila mas mahusay na gamitin Monad/ConcurrentEffect.

Gamit ang signature ng function na ito maaari naming ipatupad ang ilang mga serbisyo. Halimbawa, isang serbisyo na walang ginagawa:

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

(Cm. pinagmulan, kung saan ipinapatupad ang iba pang mga serbisyo - echo serbisyo, echo client
ΠΈ panghabambuhay na controllers.)

Ang isang node ay isang bagay na maaaring maglunsad ng ilang mga serbisyo (ang paglulunsad ng isang hanay ng mga mapagkukunan ay sinisiguro ng Cake Pattern):

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

Pakitandaan na tinutukoy namin ang eksaktong uri ng configuration na kinakailangan para sa node na ito. Kung nakalimutan naming tukuyin ang isa sa mga uri ng configuration na kinakailangan ng isang partikular na serbisyo, magkakaroon ng error sa compilation. Gayundin, hindi kami makakapagsimula ng node maliban kung magbibigay kami ng ilang bagay na may naaangkop na uri kasama ang lahat ng kinakailangang data.

Host Name Resolution

Upang kumonekta sa isang malayong host, kailangan namin ng isang tunay na IP address. Posibleng malalaman ang address sa ibang pagkakataon kaysa sa iba pang configuration. Kaya kailangan namin ng isang function na nagmamapa ng node ID sa isang address:

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

Mayroong ilang mga paraan upang ipatupad ang function na ito:

  1. Kung malalaman sa amin ang mga address bago ang pag-deploy, maaari kaming bumuo ng Scala code gamit ang
    mga address at pagkatapos ay patakbuhin ang build. Ito ay mag-compile at magpapatakbo ng mga pagsubok.
    Sa kasong ito, ang function ay malalaman nang statically at maaaring katawanin sa code bilang isang pagmamapa Map[NodeId, NodeAddress].
  2. Sa ilang mga kaso, ang aktwal na address ay malalaman lamang pagkatapos magsimula ang node.
    Sa kasong ito, maaari kaming magpatupad ng "serbisyo ng pagtuklas" na tumatakbo bago ang iba pang mga node at lahat ng mga node ay magrerehistro sa serbisyong ito at humiling ng mga address ng iba pang mga node.
  3. Kung kaya nating baguhin /etc/hosts, pagkatapos ay maaari mong gamitin ang mga paunang natukoy na hostname (tulad ng my-project-main-node ΠΈ echo-backend) at i-link lamang ang mga pangalang ito
    na may mga IP address sa panahon ng pag-deploy.

Sa post na ito hindi namin isasaalang-alang ang mga kasong ito nang mas detalyado. Para sa ating
sa isang halimbawa ng laruan, ang lahat ng mga node ay magkakaroon ng parehong IP address - 127.0.0.1.

Susunod, isinasaalang-alang namin ang dalawang opsyon para sa isang distributed system:

  1. Paglalagay ng lahat ng serbisyo sa isang node.
  2. At pagho-host ng echo service at echo client sa iba't ibang node.

Configuration para sa isang node:

Pag-configure ng solong node

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

Ang object ay nagpapatupad ng configuration ng parehong client at server. Ginagamit din ang isang time-to-live na configuration upang pagkatapos ng agwat lifetime wakasan ang programa. (Gumagana rin ang Ctrl-C at pinapalaya nang tama ang lahat ng mapagkukunan.)

Ang parehong hanay ng mga katangian ng pagsasaayos at pagpapatupad ay maaaring gamitin upang lumikha ng isang system na binubuo ng dalawang magkahiwalay na node:

Dalawang node configuration

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

Mahalaga! Pansinin kung paano naka-link ang mga serbisyo. Tinukoy namin ang isang serbisyong ipinatupad ng isang node bilang isang pagpapatupad ng paraan ng dependency ng isa pang node. Ang uri ng dependency ay sinuri ng compiler, dahil naglalaman ng uri ng protocol. Kapag tumakbo, ang dependency ay maglalaman ng tamang target na node ID. Salamat sa scheme na ito, tinukoy namin ang numero ng port nang eksaktong isang beses at palaging ginagarantiyahan na sumangguni sa tamang port.

Pagpapatupad ng dalawang node ng system

Para sa configuration na ito, ginagamit namin ang parehong mga pagpapatupad ng serbisyo nang walang pagbabago. Ang pagkakaiba lang ay mayroon na tayong dalawang bagay na nagpapatupad ng magkakaibang hanay ng mga serbisyo:

  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
  }

Ang unang node ay nagpapatupad ng server at kailangan lang ng configuration ng server. Ang pangalawang node ay nagpapatupad ng kliyente at gumagamit ng ibang bahagi ng pagsasaayos. Gayundin ang parehong mga node ay nangangailangan ng panghabambuhay na pamamahala. Ang server node ay tumatakbo nang walang katiyakan hanggang sa ito ay tumigil SIGTERM'om, at ang client node ay magwawakas pagkatapos ng ilang oras. Cm. launcher app.

Pangkalahatang proseso ng pag-unlad

Tingnan natin kung paano nakakaapekto ang diskarte sa pagsasaayos na ito sa pangkalahatang proseso ng pag-unlad.

Isasama ang configuration kasama ang natitirang code at bubuo ng artifact (.jar). Mukhang makatuwirang ilagay ang configuration sa isang hiwalay na artifact. Ito ay dahil maaari tayong magkaroon ng maraming configuration batay sa parehong code. Muli, posibleng makabuo ng mga artifact na naaayon sa iba't ibang sangay ng pagsasaayos. Ang mga dependency sa mga partikular na bersyon ng mga library ay sine-save kasama ng configuration, at ang mga bersyong ito ay nase-save magpakailanman sa tuwing magpapasya kaming i-deploy ang bersyon na iyon ng configuration.

Ang anumang pagbabago sa configuration ay nagiging pagbabago ng code. At samakatuwid, bawat isa
ang pagbabago ay sasakupin ng normal na proseso ng pagtiyak ng kalidad:

Ticket sa bug tracker -> PR -> pagsusuri -> pagsamahin sa mga nauugnay na sangay ->
integration -> deployment

Ang mga pangunahing kahihinatnan ng pagpapatupad ng isang pinagsama-samang pagsasaayos ay:

  1. Magiging pare-pareho ang configuration sa lahat ng node ng distributed system. Dahil sa katotohanan na ang lahat ng mga node ay tumatanggap ng parehong pagsasaayos mula sa iisang pinagmulan.

  2. May problemang baguhin ang configuration sa isa lamang sa mga node. Samakatuwid, ang "configuration drift" ay hindi malamang.

  3. Nagiging mas mahirap na gumawa ng maliliit na pagbabago sa configuration.

  4. Karamihan sa mga pagbabago sa configuration ay magaganap bilang bahagi ng pangkalahatang proseso ng pag-unlad at sasailalim sa pagsusuri.

Kailangan ko ba ng isang hiwalay na imbakan upang maiimbak ang pagsasaayos ng produksyon? Ang pagsasaayos na ito ay maaaring maglaman ng mga password at iba pang sensitibong impormasyon kung saan nais naming paghigpitan ang pag-access. Batay dito, tila makatuwirang iimbak ang panghuling pagsasaayos sa isang hiwalay na imbakan. Maaari mong hatiin ang configuration sa dalawang bahagiβ€”isa na naglalaman ng mga setting ng configuration na naa-access ng publiko at isa na naglalaman ng mga pinaghihigpitang setting. Ito ay magbibigay-daan sa karamihan ng mga developer na magkaroon ng access sa mga karaniwang setting. Ang paghihiwalay na ito ay madaling makamit gamit ang mga intermediate na katangian na naglalaman ng mga default na halaga.

Posibleng mga pagkakaiba-iba

Subukan nating ihambing ang pinagsama-samang pagsasaayos sa ilang karaniwang alternatibo:

  1. Text file sa target na makina.
  2. Sentralisadong key-value store (etcd/zookeeper).
  3. Mga bahagi ng proseso na maaaring i-reconfigure/i-restart nang hindi na-restart ang proseso.
  4. Pag-iimbak ng configuration sa labas ng artifact at version control.

Ang mga text file ay nagbibigay ng makabuluhang flexibility sa mga tuntunin ng maliliit na pagbabago. Ang administrator ng system ay maaaring mag-log in sa malayong node, gumawa ng mga pagbabago sa naaangkop na mga file at i-restart ang serbisyo. Para sa malalaking sistema, gayunpaman, ang ganitong flexibility ay maaaring hindi kanais-nais. Ang mga pagbabagong ginawa ay hindi nag-iiwan ng mga bakas sa ibang mga system. Walang nagre-review sa mga pagbabago. Mahirap matukoy kung sino ang eksaktong gumawa ng mga pagbabago at sa anong dahilan. Hindi sinusubok ang mga pagbabago. Kung ibinahagi ang system, maaaring makalimutan ng administrator na gawin ang kaukulang pagbabago sa iba pang mga node.

(Dapat ding tandaan na ang paggamit ng isang pinagsama-samang pagsasaayos ay hindi nagsasara ng posibilidad ng paggamit ng mga text file sa hinaharap. Ito ay sapat na upang magdagdag ng isang parser at validator na gumagawa ng parehong uri ng output Config, at maaari kang gumamit ng mga text file. Kaagad itong sumusunod na ang pagiging kumplikado ng isang system na may pinagsama-samang pagsasaayos ay medyo mas mababa kaysa sa pagiging kumplikado ng isang sistema gamit ang mga text file, dahil Ang mga text file ay nangangailangan ng karagdagang code.)

Ang isang sentralisadong key-value store ay isang mahusay na mekanismo para sa pamamahagi ng mga meta parameter ng isang distributed application. Kailangan nating magpasya kung ano ang mga parameter ng pagsasaayos at kung ano ang data lamang. Magkaroon tayo ng function C => A => B, at ang mga parameter C bihirang pagbabago, at data A - madalas. Sa kasong ito masasabi natin iyan C - mga parameter ng pagsasaayos, at A - data. Lumilitaw na ang mga parameter ng pagsasaayos ay naiiba sa data dahil sa pangkalahatan ay hindi gaanong madalas na nagbabago ang mga ito kaysa sa data. Gayundin, ang data ay karaniwang nagmumula sa isang source (mula sa user), at configuration parameters mula sa isa pa (mula sa system administrator).

Kung ang mga bihirang pagbabago ng mga parameter ay kailangang i-update nang hindi na-restart ang programa, madalas itong humantong sa komplikasyon ng programa, dahil kakailanganin nating maghatid ng mga parameter, mag-imbak, mag-parse at magsuri, at magproseso ng mga maling halaga. Samakatuwid, mula sa punto ng view ng pagbawas sa pagiging kumplikado ng programa, makatuwiran na bawasan ang bilang ng mga parameter na maaaring magbago sa panahon ng pagpapatakbo ng programa (o hindi sinusuportahan ang mga naturang parameter sa lahat).

Para sa mga layunin ng post na ito, pag-iiba namin sa pagitan ng static at dynamic na mga parameter. Kung ang lohika ng serbisyo ay nangangailangan ng pagbabago ng mga parameter sa panahon ng pagpapatakbo ng programa, pagkatapos ay tatawagin namin ang mga naturang parameter na dynamic. Kung hindi, ang mga opsyon ay static at maaaring i-configure gamit ang pinagsama-samang pagsasaayos. Para sa dynamic na reconfiguration, maaaring kailanganin namin ang isang mekanismo upang i-restart ang mga bahagi ng program na may mga bagong parameter, katulad ng kung paano ni-restart ang mga proseso ng operating system. (Sa aming opinyon, ipinapayong iwasan ang real-time na muling pagsasaayos, dahil pinapataas nito ang pagiging kumplikado ng system. Kung maaari, mas mainam na gamitin ang karaniwang mga kakayahan ng OS para sa pag-restart ng mga proseso.)

Ang isang mahalagang aspeto ng paggamit ng static na configuration na ginagawang isaalang-alang ng mga tao ang dynamic na reconfiguration ay ang oras na kinakailangan para mag-reboot ang system pagkatapos ng pag-update ng configuration (downtime). Sa katunayan, kung kailangan nating gumawa ng mga pagbabago sa static na configuration, kailangan nating i-restart ang system para magkabisa ang mga bagong value. Ang problema sa downtime ay nag-iiba sa kalubhaan para sa iba't ibang mga system. Sa ilang mga kaso, maaari kang mag-iskedyul ng pag-reboot sa oras na kaunti lang ang pag-load. Kung kailangan mong magbigay ng tuluy-tuloy na serbisyo, maaari mong ipatupad Nauubos ang koneksyon ng AWS ELB. Kasabay nito, kapag kailangan naming i-reboot ang system, maglulunsad kami ng parallel na instance ng system na ito, ilipat ang balancer dito, at hintayin na makumpleto ang mga lumang koneksyon. Kapag natapos na ang lahat ng lumang koneksyon, isinara namin ang lumang instance ng system.

Isaalang-alang natin ngayon ang isyu ng pag-iimbak ng configuration sa loob o labas ng artifact. Kung iimbak namin ang configuration sa loob ng isang artifact, at least nagkaroon kami ng pagkakataon na i-verify ang kawastuhan ng configuration sa panahon ng assembly ng artifact. Kung ang configuration ay nasa labas ng kinokontrol na artifact, mahirap subaybayan kung sino ang gumawa ng mga pagbabago sa file na ito at kung bakit. Gaano ito kahalaga? Sa aming opinyon, para sa maraming mga sistema ng produksyon mahalaga na magkaroon ng isang matatag at mataas na kalidad na pagsasaayos.

Ang bersyon ng isang artifact ay nagbibigay-daan sa iyo upang matukoy kung kailan ito nilikha, kung anong mga halaga ang nilalaman nito, kung anong mga function ang pinagana/hindi pinagana, at kung sino ang responsable para sa anumang pagbabago sa pagsasaayos. Siyempre, ang pag-iimbak ng configuration sa loob ng isang artifact ay nangangailangan ng ilang pagsisikap, kaya kailangan mong gumawa ng matalinong desisyon.

Mga kalamangan at kahinaan

Gusto kong pag-isipan ang mga kalamangan at kahinaan ng iminungkahing teknolohiya.

Kalamangan

Nasa ibaba ang isang listahan ng mga pangunahing tampok ng isang pinagsama-samang distributed system configuration:

  1. Static configuration check. Binibigyang-daan kang makatiyak na
    tama ang configuration.
  2. Rich configuration language. Karaniwan, ang iba pang mga paraan ng pagsasaayos ay limitado sa string variable substitution sa karamihan. Kapag gumagamit ng Scala, isang malawak na hanay ng mga feature ng wika ang magagamit upang mapabuti ang iyong configuration. Halimbawa maaari nating gamitin
    mga katangian para sa mga default na halaga, gamit ang mga bagay sa mga parameter ng pangkat, maaari tayong sumangguni sa mga val na ipinahayag nang isang beses lamang (DRY) sa kalakip na saklaw. Maaari mong i-instantiate ang anumang mga klase nang direkta sa loob ng pagsasaayos (Seq, Map, mga custom na klase).
  3. DSL. Ang Scala ay may ilang feature ng wika na nagpapadali sa paggawa ng DSL. Posibleng samantalahin ang mga feature na ito at magpatupad ng configuration language na mas maginhawa para sa target na grupo ng mga user, upang ang configuration ay kahit man lang nababasa ng mga eksperto sa domain. Ang mga espesyalista ay maaaring, halimbawa, lumahok sa proseso ng pagsusuri ng pagsasaayos.
  4. Integridad at synchrony sa pagitan ng mga node. Ang isa sa mga bentahe ng pagkakaroon ng pagsasaayos ng isang buong distributed system na naka-imbak sa isang punto ay ang lahat ng mga halaga ay idineklara nang isang beses nang eksakto at pagkatapos ay muling ginagamit saanman sila kailangan. Ang paggamit ng mga uri ng phantom upang magdeklara ng mga port ay tinitiyak na ang mga node ay gumagamit ng mga katugmang protocol sa lahat ng tamang configuration ng system. Ang pagkakaroon ng tahasang mandatoryong mga dependency sa pagitan ng mga node ay nagsisiguro na ang lahat ng mga serbisyo ay konektado.
  5. Mataas na kalidad ng mga pagbabago. Ang paggawa ng mga pagbabago sa configuration gamit ang isang karaniwang proseso ng pag-develop ay ginagawang posible na makamit din ang mataas na kalidad na mga pamantayan para sa configuration.
  6. Sabay-sabay na pag-update ng configuration. Ang awtomatikong pag-deploy ng system pagkatapos ng mga pagbabago sa configuration ay tiyaking naa-update ang lahat ng node.
  7. Pagpapasimple ng aplikasyon. Ang application ay hindi nangangailangan ng pag-parse, pagsuri ng configuration, o paghawak ng mga maling halaga. Binabawasan nito ang pagiging kumplikado ng application. (Ang ilan sa mga kumplikadong pagsasaayos na naobserbahan sa aming halimbawa ay hindi isang katangian ng pinagsama-samang pagsasaayos, ngunit isang nakakamalay na desisyon lamang na hinihimok ng pagnanais na magbigay ng higit na kaligtasan ng uri.) Medyo madaling bumalik sa karaniwang pagsasaayos - ipatupad lamang ang nawawala mga bahagi. Samakatuwid, maaari mong, halimbawa, magsimula sa isang pinagsama-samang pagsasaayos, na ipinagpaliban ang pagpapatupad ng mga hindi kinakailangang bahagi hanggang sa oras na ito ay talagang kinakailangan.
  8. Na-verify na configuration. Dahil ang mga pagbabago sa configuration ay sumusunod sa karaniwang kapalaran ng anumang iba pang mga pagbabago, ang output na nakukuha namin ay isang artifact na may natatanging bersyon. Nagbibigay-daan ito sa amin, halimbawa, na bumalik sa nakaraang bersyon ng configuration kung kinakailangan. Maaari pa nga nating gamitin ang configuration mula noong isang taon at ang system ay gagana nang eksakto sa parehong paraan. Ang isang matatag na configuration ay nagpapabuti sa predictability at pagiging maaasahan ng isang distributed system. Dahil ang pagsasaayos ay naayos sa yugto ng compilation, medyo mahirap na pekein ito sa paggawa.
  9. Modularity. Ang iminungkahing balangkas ay modular at ang mga module ay maaaring pagsamahin sa iba't ibang paraan upang lumikha ng iba't ibang mga sistema. Sa partikular, maaari mong i-configure ang system na tumakbo sa isang node sa isang embodiment, at sa maraming node sa isa pa. Maaari kang lumikha ng ilang mga pagsasaayos para sa mga pagkakataon ng produksyon ng system.
  10. Pagsubok. Sa pamamagitan ng pagpapalit ng mga indibidwal na serbisyo ng mga mock na bagay, maaari kang makakuha ng ilang bersyon ng system na maginhawa para sa pagsubok.
  11. Pagsubok sa pagsasama. Ang pagkakaroon ng iisang configuration para sa buong distributed system ay ginagawang posible na patakbuhin ang lahat ng bahagi sa isang kinokontrol na kapaligiran bilang bahagi ng integration testing. Madaling tularan, halimbawa, isang sitwasyon kung saan nagiging accessible ang ilang node.

Mga disadvantages at limitasyon

Ang pinagsama-samang pagsasaayos ay naiiba sa iba pang mga diskarte sa pagsasaayos at maaaring hindi angkop para sa ilang mga aplikasyon. Nasa ibaba ang ilang mga disadvantages:

  1. Static na pagsasaayos. Minsan kailangan mong mabilis na iwasto ang pagsasaayos sa produksyon, na lampasan ang lahat ng mga mekanismo ng proteksiyon. Sa diskarteng ito maaari itong maging mas mahirap. Hindi bababa sa, kakailanganin pa rin ang compilation at awtomatikong pag-deploy. Ito ay parehong kapaki-pakinabang na tampok ng diskarte at isang kawalan sa ilang mga kaso.
  2. Pagbuo ng configuration. Kung sakaling ang configuration file ay nabuo sa pamamagitan ng isang awtomatikong tool, maaaring kailanganin ang mga karagdagang pagsisikap upang maisama ang build script.
  3. Mga gamit. Sa kasalukuyan, ang mga utility at diskarte na idinisenyo upang gumana sa configuration ay batay sa mga text file. Hindi lahat ng naturang kagamitan/teknika ay magagamit sa isang pinagsama-samang pagsasaayos.
  4. Kailangan ang pagbabago sa ugali. Ang mga developer at DevOps ay sanay sa mga text file. Ang mismong ideya ng pag-compile ng isang pagsasaayos ay maaaring medyo hindi inaasahan at hindi karaniwan at maging sanhi ng pagtanggi.
  5. Ang isang mataas na kalidad na proseso ng pag-unlad ay kinakailangan. Upang kumportableng magamit ang pinagsama-samang pagsasaayos, ang buong automation ng proseso ng pagbuo at pag-deploy ng application (CI/CD) ay kinakailangan. Kung hindi, ito ay magiging medyo hindi maginhawa.

Isaalang-alang din natin ang ilang mga limitasyon ng itinuturing na halimbawa na hindi nauugnay sa ideya ng isang pinagsama-samang pagsasaayos:

  1. Kung nagbibigay kami ng hindi kinakailangang impormasyon sa pagsasaayos na hindi ginagamit ng node, hindi kami tutulungan ng compiler na matukoy ang nawawalang pagpapatupad. Ang problemang ito ay maaaring malutas sa pamamagitan ng pag-abandona sa Cake Pattern at paggamit ng mas mahigpit na mga uri, halimbawa, HList o algebraic na mga uri ng data (mga klase ng kaso) upang kumatawan sa configuration.
  2. May mga linya sa configuration file na hindi nauugnay sa configuration mismo: (package, import,mga pagpapahayag ng bagay; override defPara sa mga parameter na may mga default na halaga). Maaari itong bahagyang maiiwasan kung ipapatupad mo ang iyong sariling DSL. Bilang karagdagan, ang iba pang mga uri ng pagsasaayos (halimbawa, XML) ay nagpapataw din ng ilang mga paghihigpit sa istraktura ng file.
  3. Para sa mga layunin ng post na ito, hindi namin isinasaalang-alang ang dynamic na muling pagsasaayos ng isang kumpol ng mga katulad na node.

Konklusyon

Sa post na ito, ginalugad namin ang ideya ng kumakatawan sa pagsasaayos sa source code gamit ang mga advanced na kakayahan ng sistema ng uri ng Scala. Ang diskarte na ito ay maaaring gamitin sa iba't ibang mga application bilang isang kapalit para sa mga tradisyonal na paraan ng pagsasaayos batay sa xml o mga text file. Kahit na ang aming halimbawa ay ipinatupad sa Scala, ang parehong mga ideya ay maaaring ilipat sa iba pang pinagsama-samang mga wika (tulad ng Kotlin, C#, Swift, ...). Maaari mong subukan ang diskarteng ito sa isa sa mga sumusunod na proyekto, at, kung hindi ito gumana, magpatuloy sa text file, idagdag ang mga nawawalang bahagi.

Naturally, ang isang pinagsama-samang pagsasaayos ay nangangailangan ng isang mataas na kalidad na proseso ng pag-unlad. Bilang kapalit, tinitiyak ang mataas na kalidad at pagiging maaasahan ng mga pagsasaayos.

Ang isinasaalang-alang na diskarte ay maaaring mapalawak:

  1. Maaari kang gumamit ng mga macro upang magsagawa ng mga pagsusuri sa oras ng pag-compile.
  2. Maaari kang magpatupad ng DSL upang ipakita ang configuration sa paraang naa-access ng mga end user.
  3. Maaari mong ipatupad ang dynamic na resource management na may awtomatikong pagsasaayos ng configuration. Halimbawa, ang pagpapalit ng bilang ng mga node sa isang cluster ay nangangailangan na (1) ang bawat node ay makatanggap ng bahagyang naiibang configuration; (2) nakatanggap ang cluster manager ng impormasyon tungkol sa mga bagong node.

Mga Pasasalamat

Gusto kong pasalamatan sina Andrei Saksonov, Pavel Popov at Anton Nekhaev para sa kanilang nakabubuo na pagpuna sa draft na artikulo.

Pinagmulan: www.habr.com

Magdagdag ng komento