Konfigurasi Sistem Teragih Tersusun

Saya ingin memberitahu anda satu mekanisme menarik untuk bekerja dengan konfigurasi sistem teragih. Konfigurasi diwakili secara langsung dalam bahasa yang disusun (Scala) menggunakan jenis selamat. Siaran ini memberikan contoh konfigurasi sedemikian dan membincangkan pelbagai aspek melaksanakan konfigurasi yang disusun ke dalam proses pembangunan keseluruhan.

Konfigurasi Sistem Teragih Tersusun

(bahasa inggeris)

Pengenalan

Membina sistem teragih yang boleh dipercayai bermakna semua nod menggunakan konfigurasi yang betul, disegerakkan dengan nod lain. Teknologi DevOps (terraform, ansible atau sesuatu seperti itu) biasanya digunakan untuk menjana fail konfigurasi secara automatik (selalunya khusus untuk setiap nod). Kami juga ingin memastikan bahawa semua nod berkomunikasi menggunakan protokol yang sama (termasuk versi yang sama). Jika tidak, ketidakserasian akan dibina ke dalam sistem edaran kami. Dalam dunia JVM, satu akibat daripada keperluan ini ialah versi perpustakaan yang sama yang mengandungi mesej protokol mesti digunakan di mana-mana sahaja.

Bagaimana pula dengan menguji sistem yang diedarkan? Sudah tentu, kami menganggap bahawa semua komponen mempunyai ujian unit sebelum kami beralih kepada ujian penyepaduan. (Untuk membolehkan kami mengekstrapolasi keputusan ujian kepada masa jalan, kami juga mesti menyediakan set perpustakaan yang sama pada peringkat ujian dan semasa masa jalan.)

Apabila bekerja dengan ujian penyepaduan, selalunya lebih mudah untuk menggunakan laluan kelas yang sama di semua tempat pada semua nod. Apa yang perlu kita lakukan ialah memastikan bahawa classpath yang sama digunakan semasa runtime. (Walaupun adalah mungkin untuk menjalankan nod yang berbeza dengan laluan kelas yang berbeza, ini menambahkan kerumitan pada konfigurasi keseluruhan dan kesukaran dengan ujian penempatan dan penyepaduan.) Untuk tujuan siaran ini, kami mengandaikan bahawa semua nod akan menggunakan laluan kelas yang sama.

Konfigurasi berkembang dengan aplikasi. Kami menggunakan versi untuk mengenal pasti pelbagai peringkat evolusi program. Nampaknya logik juga untuk mengenal pasti versi konfigurasi yang berbeza. Dan letakkan konfigurasi itu sendiri dalam sistem kawalan versi. Jika terdapat hanya satu konfigurasi dalam pengeluaran, maka kita hanya boleh menggunakan nombor versi. Jika kita menggunakan banyak contoh pengeluaran, maka kita akan memerlukan beberapa
cawangan konfigurasi dan label tambahan sebagai tambahan kepada versi (contohnya, nama cawangan). Dengan cara ini kita boleh mengenal pasti konfigurasi yang tepat. Setiap pengecam konfigurasi secara unik sepadan dengan gabungan khusus nod, port, sumber luaran dan versi perpustakaan yang diedarkan. Untuk tujuan siaran ini, kami akan menganggap bahawa hanya terdapat satu cawangan dan kami boleh mengenal pasti konfigurasi dengan cara biasa menggunakan tiga nombor yang dipisahkan oleh titik (1.2.3).

Dalam persekitaran moden, fail konfigurasi jarang dibuat secara manual. Lebih kerap ia dijana semasa penggunaan dan tidak lagi disentuh (supaya jangan pecahkan apa-apa). Persoalan semula jadi timbul: mengapa kita masih menggunakan format teks untuk menyimpan konfigurasi? Alternatif yang berdaya maju nampaknya adalah keupayaan untuk menggunakan kod biasa untuk konfigurasi dan mendapat manfaat daripada semakan masa kompilasi.

Dalam siaran ini kita akan meneroka idea untuk mewakili konfigurasi di dalam artifak yang disusun.

Konfigurasi tersusun

Bahagian ini menyediakan contoh konfigurasi tersusun statik. Dua perkhidmatan mudah dilaksanakan - perkhidmatan gema dan pelanggan perkhidmatan gema. Berdasarkan dua perkhidmatan ini, dua pilihan sistem dipasang. Dalam satu pilihan, kedua-dua perkhidmatan terletak pada nod yang sama, dalam pilihan lain - pada nod yang berbeza.

Biasanya sistem teragih mengandungi beberapa nod. Anda boleh mengenal pasti nod menggunakan nilai beberapa jenis NodeId:

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

atau

case class NodeId(hostName: String)

atau bahkan

object Singleton
type NodeId = Singleton.type

Nod melaksanakan pelbagai peranan, mereka menjalankan perkhidmatan dan sambungan TCP/HTTP boleh diwujudkan di antara mereka.

Untuk menerangkan sambungan TCP kita memerlukan sekurang-kurangnya nombor port. Kami juga ingin menggambarkan protokol yang disokong pada port tersebut untuk memastikan kedua-dua klien dan pelayan menggunakan protokol yang sama. Kami akan menerangkan sambungan menggunakan kelas berikut:

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

mana Port - hanya integer Int menunjukkan julat nilai yang boleh diterima:

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

Jenis halus

Lihat perpustakaan ditapis ΠΈ saya lapor. Ringkasnya, perpustakaan membenarkan anda menambah kekangan pada jenis yang disemak pada masa penyusunan. Dalam kes ini, nilai nombor port yang sah ialah integer 16-bit. Untuk konfigurasi yang disusun, menggunakan perpustakaan yang diperhalusi adalah tidak wajib, tetapi ia meningkatkan keupayaan pengkompil untuk menyemak konfigurasi.

Untuk protokol HTTP (REST), sebagai tambahan kepada nombor port, kami juga mungkin memerlukan laluan ke perkhidmatan:

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

Jenis hantu

Untuk mengenal pasti protokol pada masa penyusunan, kami menggunakan parameter jenis yang tidak digunakan dalam kelas. Keputusan ini disebabkan oleh fakta bahawa kami tidak menggunakan contoh protokol pada masa jalan, tetapi kami ingin pengkompil menyemak keserasian protokol. Dengan menyatakan protokol, kami tidak akan dapat memberikan perkhidmatan yang tidak sesuai sebagai tanggungan.

Salah satu protokol biasa ialah REST API dengan siri Json:

sealed trait JsonHttpRestProtocol[RequestMessage, ResponseMessage]

mana RequestMessage - jenis permintaan, ResponseMessage - jenis tindak balas.
Sudah tentu, kami boleh menggunakan perihalan protokol lain yang memberikan ketepatan penerangan yang kami perlukan.

Untuk tujuan siaran ini, kami akan menggunakan versi ringkas protokol:

sealed trait SimpleHttpGetRest[RequestMessage, ResponseMessage]

Di sini permintaan ialah rentetan yang dilampirkan pada url dan respons ialah rentetan yang dikembalikan dalam badan respons HTTP.

Konfigurasi perkhidmatan diterangkan oleh nama perkhidmatan, port dan kebergantungan. Unsur-unsur ini boleh diwakili dalam Scala dalam beberapa cara (contohnya, HList-s, jenis data algebra). Untuk tujuan siaran ini, kami akan menggunakan Corak Kek dan mewakili modul menggunakan trait's. (Corak Kek bukanlah elemen yang diperlukan dalam pendekatan ini. Ia hanyalah satu kemungkinan pelaksanaan.)

Ketergantungan antara perkhidmatan boleh diwakili sebagai kaedah yang mengembalikan port EndPointdaripada nod lain:

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

Untuk mencipta perkhidmatan gema, anda hanya perlukan nombor port dan petunjuk bahawa port menyokong protokol gema. Kami mungkin tidak menentukan port tertentu, kerana... ciri membolehkan anda mengisytiharkan kaedah tanpa pelaksanaan (kaedah abstrak). Dalam kes ini, apabila mencipta konfigurasi konkrit, pengkompil memerlukan kami menyediakan pelaksanaan kaedah abstrak dan menyediakan nombor port. Memandangkan kami telah melaksanakan kaedah tersebut, apabila mencipta konfigurasi khusus, kami mungkin tidak menentukan port yang berbeza. Nilai lalai akan digunakan.

Dalam konfigurasi pelanggan kami mengisytiharkan pergantungan pada perkhidmatan gema:

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

Kebergantungan adalah jenis yang sama seperti perkhidmatan yang dieksport echoService. Khususnya, dalam klien gema kami memerlukan protokol yang sama. Oleh itu, apabila menyambungkan dua perkhidmatan, kami boleh memastikan bahawa semuanya akan berfungsi dengan betul.

Pelaksanaan perkhidmatan

Fungsi diperlukan untuk memulakan dan menghentikan perkhidmatan. (Keupayaan untuk menghentikan perkhidmatan adalah penting untuk ujian.) Sekali lagi, terdapat beberapa pilihan untuk melaksanakan ciri sedemikian (contohnya, kita boleh menggunakan kelas jenis berdasarkan jenis konfigurasi). Untuk tujuan siaran ini, kami akan menggunakan Corak Kek. Kami akan mewakili perkhidmatan menggunakan kelas cats.Resource, kerana Kelas ini sudah menyediakan cara untuk menjamin pelepasan sumber dengan selamat sekiranya berlaku masalah. Untuk mendapatkan sumber, kami perlu menyediakan konfigurasi dan konteks masa jalan sedia dibuat. Fungsi permulaan perkhidmatan boleh kelihatan seperti ini:

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

mana

  • Config β€” jenis konfigurasi untuk perkhidmatan ini
  • AddressResolver β€” objek masa jalan yang membolehkan anda mengetahui alamat nod lain (lihat di bawah)

dan jenis lain dari perpustakaan cats:

  • F[_] β€” jenis kesan (dalam kes paling mudah F[A] hanya boleh menjadi fungsi () => A. Dalam post ini kami akan gunakan cats.IO.)
  • Reader[A,B] - lebih kurang sinonim dengan fungsi A => B
  • cats.Resource - sumber yang boleh diperoleh dan dikeluarkan
  • Timer β€” pemasa (membolehkan anda tertidur seketika dan mengukur selang masa)
  • ContextShift - analog ExecutionContext
  • Applicative β€” kelas jenis kesan yang membolehkan anda menggabungkan kesan individu (hampir monad). Dalam aplikasi yang lebih kompleks nampaknya lebih baik untuk digunakan Monad/ConcurrentEffect.

Menggunakan tandatangan fungsi ini kita boleh melaksanakan beberapa perkhidmatan. Sebagai contoh, perkhidmatan yang tidak melakukan apa-apa:

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

(Cm. kod sumber, di mana perkhidmatan lain dilaksanakan - perkhidmatan gema, pelanggan gema
ΠΈ pengawal seumur hidup.)

Nod ialah objek yang boleh melancarkan beberapa perkhidmatan (pelancaran rantaian sumber dipastikan oleh Corak Kek):

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

Sila ambil perhatian bahawa kami menyatakan jenis konfigurasi tepat yang diperlukan untuk nod ini. Jika kita terlupa untuk menentukan salah satu jenis konfigurasi yang diperlukan oleh perkhidmatan tertentu, akan terdapat ralat penyusunan. Selain itu, kami tidak akan dapat memulakan nod melainkan kami menyediakan beberapa objek jenis yang sesuai dengan semua data yang diperlukan.

Resolusi Nama Hos

Untuk menyambung ke hos jauh, kami memerlukan alamat IP sebenar. Ada kemungkinan bahawa alamat akan diketahui kemudian daripada konfigurasi yang lain. Jadi kita memerlukan fungsi yang memetakan ID nod ke alamat:

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

Terdapat beberapa cara untuk melaksanakan fungsi ini:

  1. Jika alamat diketahui oleh kami sebelum penggunaan, maka kami boleh menjana kod Scala dengan
    alamat dan kemudian jalankan binaan. Ini akan menyusun dan menjalankan ujian.
    Dalam kes ini, fungsi akan diketahui secara statik dan boleh diwakili dalam kod sebagai pemetaan Map[NodeId, NodeAddress].
  2. Dalam sesetengah kes, alamat sebenar hanya diketahui selepas nod dimulakan.
    Dalam kes ini, kami boleh melaksanakan "perkhidmatan penemuan" yang berjalan sebelum nod lain dan semua nod akan mendaftar dengan perkhidmatan ini dan meminta alamat nod lain.
  3. Jika kita boleh mengubah suai /etc/hosts, maka anda boleh menggunakan nama hos yang dipratentukan (seperti my-project-main-node ΠΈ echo-backend) dan hanya pautkan nama-nama ini
    dengan alamat IP semasa penggunaan.

Dalam siaran ini kami tidak akan mempertimbangkan kes-kes ini dengan lebih terperinci. Untuk kami
dalam contoh mainan, semua nod akan mempunyai alamat IP yang sama - 127.0.0.1.

Seterusnya, kami mempertimbangkan dua pilihan untuk sistem teragih:

  1. Meletakkan semua perkhidmatan pada satu nod.
  2. Dan mengehos perkhidmatan gema dan klien gema pada nod yang berbeza.

Konfigurasi untuk satu nod:

Konfigurasi nod tunggal

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

Objek melaksanakan konfigurasi kedua-dua klien dan pelayan. Konfigurasi masa-ke-hidup juga digunakan supaya selepas jeda lifetime menamatkan program. (Ctrl-C juga berfungsi dan membebaskan semua sumber dengan betul.)

Set konfigurasi dan ciri pelaksanaan yang sama boleh digunakan untuk mencipta sistem yang terdiri daripada dua nod yang berasingan:

Konfigurasi dua nod

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

Penting! Perhatikan bagaimana perkhidmatan dipautkan. Kami menentukan perkhidmatan yang dilaksanakan oleh satu nod sebagai pelaksanaan kaedah pergantungan nod lain. Jenis pergantungan disemak oleh pengkompil, kerana mengandungi jenis protokol. Apabila dijalankan, kebergantungan akan mengandungi ID nod sasaran yang betul. Terima kasih kepada skim ini, kami menentukan nombor port tepat sekali dan sentiasa dijamin untuk merujuk kepada port yang betul.

Pelaksanaan dua nod sistem

Untuk konfigurasi ini, kami menggunakan pelaksanaan perkhidmatan yang sama tanpa perubahan. Satu-satunya perbezaan ialah kami kini mempunyai dua objek yang melaksanakan set perkhidmatan yang berbeza:

  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
  }

Nod pertama melaksanakan pelayan dan hanya memerlukan konfigurasi pelayan. Nod kedua melaksanakan klien dan menggunakan bahagian konfigurasi yang berbeza. Juga kedua-dua nod memerlukan pengurusan seumur hidup. Nod pelayan berjalan selama-lamanya sehingga ia dihentikan SIGTERM'om, dan nod pelanggan ditamatkan selepas beberapa ketika. Cm. aplikasi pelancar.

Proses pembangunan umum

Mari lihat bagaimana pendekatan konfigurasi ini mempengaruhi keseluruhan proses pembangunan.

Konfigurasi akan disusun bersama-sama dengan kod yang lain dan artifak (.jar) akan dihasilkan. Nampaknya masuk akal untuk meletakkan konfigurasi dalam artifak yang berasingan. Ini kerana kita boleh mempunyai berbilang konfigurasi berdasarkan kod yang sama. Sekali lagi, adalah mungkin untuk menjana artifak yang sepadan dengan cabang konfigurasi yang berbeza. Kebergantungan pada versi perpustakaan tertentu disimpan bersama-sama dengan konfigurasi, dan versi ini disimpan selama-lamanya apabila kami memutuskan untuk menggunakan versi konfigurasi tersebut.

Sebarang perubahan konfigurasi bertukar menjadi perubahan kod. Dan oleh itu, masing-masing
perubahan akan dilindungi oleh proses jaminan kualiti biasa:

Tiket dalam penjejak pepijat -> PR -> semakan -> bergabung dengan cawangan yang berkaitan ->
integrasi -> penempatan

Akibat utama melaksanakan konfigurasi yang disusun ialah:

  1. Konfigurasi akan konsisten merentas semua nod sistem yang diedarkan. Disebabkan fakta bahawa semua nod menerima konfigurasi yang sama dari satu sumber.

  2. Adalah bermasalah untuk menukar konfigurasi hanya dalam satu daripada nod. Oleh itu, "hanyut konfigurasi" tidak mungkin.

  3. Ia menjadi lebih sukar untuk membuat perubahan kecil pada konfigurasi.

  4. Kebanyakan perubahan konfigurasi akan berlaku sebagai sebahagian daripada proses pembangunan keseluruhan dan akan tertakluk kepada semakan.

Adakah saya memerlukan repositori berasingan untuk menyimpan konfigurasi pengeluaran? Konfigurasi ini mungkin mengandungi kata laluan dan maklumat sensitif lain yang kami ingin hadkan aksesnya. Berdasarkan ini, nampaknya masuk akal untuk menyimpan konfigurasi akhir dalam repositori yang berasingan. Anda boleh membahagikan konfigurasi kepada dua bahagianβ€”satu mengandungi tetapan konfigurasi yang boleh diakses secara umum dan satu mengandungi tetapan terhad. Ini akan membolehkan kebanyakan pembangun mempunyai akses kepada tetapan biasa. Pemisahan ini mudah dicapai menggunakan ciri perantaraan yang mengandungi nilai lalai.

Kemungkinan variasi

Mari cuba bandingkan konfigurasi yang disusun dengan beberapa alternatif biasa:

  1. Fail teks pada mesin sasaran.
  2. Kedai nilai kunci terpusat (etcd/zookeeper).
  3. Komponen proses yang boleh dikonfigurasikan semula/dimulakan semula tanpa memulakan semula proses.
  4. Menyimpan konfigurasi di luar artifak dan kawalan versi.

Fail teks memberikan fleksibiliti yang ketara dari segi perubahan kecil. Pentadbir sistem boleh log masuk ke nod jauh, membuat perubahan pada fail yang sesuai dan memulakan semula perkhidmatan. Untuk sistem yang besar, bagaimanapun, fleksibiliti sedemikian mungkin tidak diingini. Perubahan yang dibuat tidak meninggalkan kesan dalam sistem lain. Tiada siapa yang menyemak perubahan. Sukar untuk menentukan siapa sebenarnya yang membuat perubahan dan atas sebab apa. Perubahan tidak diuji. Jika sistem diedarkan, maka pentadbir mungkin terlupa untuk membuat perubahan yang sepadan pada nod lain.

(Perlu juga diperhatikan bahawa menggunakan konfigurasi yang disusun tidak menutup kemungkinan menggunakan fail teks pada masa hadapan. Ia akan mencukupi untuk menambah penghurai dan pengesah yang menghasilkan jenis yang sama seperti output Config, dan anda boleh menggunakan fail teks. Ia serta-merta berikutan bahawa kerumitan sistem dengan konfigurasi yang disusun agak kurang daripada kerumitan sistem menggunakan fail teks, kerana fail teks memerlukan kod tambahan.)

Stor nilai kunci terpusat ialah mekanisme yang baik untuk mengedarkan parameter meta bagi aplikasi yang diedarkan. Kita perlu memutuskan apakah parameter konfigurasi dan apakah itu hanya data. Mari kita mempunyai fungsi C => A => B, dan parameter C jarang berubah, dan data A - selalunya. Dalam kes ini kita boleh mengatakan bahawa C - parameter konfigurasi, dan A - data. Nampaknya parameter konfigurasi berbeza daripada data kerana ia biasanya berubah kurang kerap daripada data. Juga, data biasanya datang daripada satu sumber (daripada pengguna), dan parameter konfigurasi daripada yang lain (daripada pentadbir sistem).

Jika parameter yang jarang berubah perlu dikemas kini tanpa memulakan semula program, maka ini selalunya boleh membawa kepada komplikasi program, kerana kami perlu menghantar parameter, menyimpan, menghuraikan dan menyemak serta memproses nilai yang salah. Oleh itu, dari sudut pandangan mengurangkan kerumitan program, adalah masuk akal untuk mengurangkan bilangan parameter yang boleh berubah semasa operasi program (atau tidak menyokong parameter tersebut sama sekali).

Untuk tujuan siaran ini, kami akan membezakan antara parameter statik dan dinamik. Jika logik perkhidmatan memerlukan perubahan parameter semasa operasi program, maka kami akan memanggil parameter tersebut dinamik. Jika tidak, pilihan adalah statik dan boleh dikonfigurasikan menggunakan konfigurasi yang disusun. Untuk konfigurasi semula dinamik, kami mungkin memerlukan mekanisme untuk memulakan semula bahagian program dengan parameter baharu, sama seperti cara proses sistem pengendalian dimulakan semula. (Pada pendapat kami, adalah dinasihatkan untuk mengelakkan konfigurasi semula masa nyata, kerana ini meningkatkan kerumitan sistem. Jika boleh, lebih baik menggunakan keupayaan OS standard untuk memulakan semula proses.)

Satu aspek penting dalam menggunakan konfigurasi statik yang membuatkan orang mempertimbangkan konfigurasi semula dinamik ialah masa yang diperlukan untuk sistem but semula selepas kemas kini konfigurasi (masa henti). Sebenarnya, jika kita perlu membuat perubahan pada konfigurasi statik, kita perlu memulakan semula sistem untuk nilai baharu berkuat kuasa. Masalah masa henti berbeza dalam keterukan untuk sistem yang berbeza. Dalam sesetengah kes, anda boleh menjadualkan but semula pada masa beban adalah minimum. Jika anda perlu menyediakan perkhidmatan berterusan, anda boleh melaksanakan Sambungan AWS ELB terputus. Pada masa yang sama, apabila kami perlu but semula sistem, kami melancarkan contoh selari sistem ini, menukar pengimbang kepadanya dan tunggu sambungan lama selesai. Selepas semua sambungan lama telah ditamatkan, kami menutup contoh lama sistem.

Sekarang mari kita pertimbangkan isu penyimpanan konfigurasi di dalam atau di luar artifak. Jika kita menyimpan konfigurasi di dalam artifak, maka sekurang-kurangnya kita mempunyai peluang untuk mengesahkan ketepatan konfigurasi semasa pemasangan artifak. Jika konfigurasi berada di luar artifak terkawal, sukar untuk menjejak siapa yang membuat perubahan pada fail ini dan sebabnya. Betapa pentingnya? Pada pendapat kami, bagi kebanyakan sistem pengeluaran adalah penting untuk mempunyai konfigurasi yang stabil dan berkualiti tinggi.

Versi artifak membolehkan anda menentukan bila ia dicipta, nilai yang terkandung di dalamnya, fungsi apa yang didayakan/dilumpuhkan, dan siapa yang bertanggungjawab untuk sebarang perubahan dalam konfigurasi. Sudah tentu, menyimpan konfigurasi di dalam artifak memerlukan sedikit usaha, jadi anda perlu membuat keputusan termaklum.

Kebaikan dan keburukan

Saya ingin membincangkan kebaikan dan keburukan teknologi yang dicadangkan.

Kelebihan

Di bawah ialah senarai ciri utama konfigurasi sistem teragih yang disusun:

  1. Semakan konfigurasi statik. Membolehkan anda memastikan bahawa
    konfigurasi adalah betul.
  2. Bahasa konfigurasi yang kaya. Biasanya, kaedah konfigurasi lain terhad kepada penggantian pembolehubah rentetan paling banyak. Apabila menggunakan Scala, pelbagai ciri bahasa tersedia untuk menambah baik konfigurasi anda. Contohnya kita boleh gunakan
    ciri untuk nilai lalai, menggunakan objek untuk mengumpulkan parameter, kita boleh merujuk kepada val yang diisytiharkan sekali sahaja (DRY) dalam skop yang disertakan. Anda boleh membuat instantiate mana-mana kelas terus di dalam konfigurasi (Seq, Map, kelas tersuai).
  3. DSL. Scala mempunyai beberapa ciri bahasa yang memudahkan untuk mencipta DSL. Adalah mungkin untuk memanfaatkan ciri ini dan melaksanakan bahasa konfigurasi yang lebih mudah untuk kumpulan sasaran pengguna, supaya konfigurasi sekurang-kurangnya boleh dibaca oleh pakar domain. Pakar boleh, sebagai contoh, mengambil bahagian dalam proses semakan konfigurasi.
  4. Integriti dan penyegerakan antara nod. Salah satu kelebihan mempunyai konfigurasi keseluruhan sistem yang diedarkan disimpan pada satu titik ialah semua nilai diisytiharkan tepat sekali dan kemudian digunakan semula di mana sahaja ia diperlukan. Menggunakan jenis hantu untuk mengisytiharkan port memastikan nod menggunakan protokol yang serasi dalam semua konfigurasi sistem yang betul. Mempunyai kebergantungan mandatori yang jelas antara nod memastikan semua perkhidmatan disambungkan.
  5. Perubahan berkualiti tinggi. Membuat perubahan pada konfigurasi menggunakan proses pembangunan biasa memungkinkan untuk mencapai standard kualiti tinggi untuk konfigurasi juga.
  6. Kemas kini konfigurasi serentak. Penggunaan sistem automatik selepas perubahan konfigurasi memastikan semua nod dikemas kini.
  7. Memudahkan aplikasi. Aplikasi tidak memerlukan penghuraian, penyemakan konfigurasi atau pengendalian nilai yang salah. Ini mengurangkan kerumitan aplikasi. (Beberapa kerumitan konfigurasi yang diperhatikan dalam contoh kami bukanlah atribut konfigurasi yang disusun, tetapi hanya keputusan sedar yang didorong oleh keinginan untuk menyediakan keselamatan jenis yang lebih besar.) Agak mudah untuk kembali ke konfigurasi biasa - hanya laksanakan yang hilang bahagian. Oleh itu, anda boleh, sebagai contoh, mulakan dengan konfigurasi yang disusun, menangguhkan pelaksanaan bahagian yang tidak perlu sehingga masa ia benar-benar diperlukan.
  8. Konfigurasi disahkan. Memandangkan perubahan konfigurasi mengikut nasib biasa mana-mana perubahan lain, output yang kami dapat ialah artifak dengan versi unik. Ini membolehkan kami, sebagai contoh, untuk kembali ke versi sebelumnya konfigurasi jika perlu. Kita juga boleh menggunakan konfigurasi dari setahun yang lalu dan sistem akan berfungsi sama. Konfigurasi yang stabil meningkatkan kebolehramalan dan kebolehpercayaan sistem teragih. Memandangkan konfigurasi ditetapkan pada peringkat penyusunan, agak sukar untuk memalsukannya dalam pengeluaran.
  9. Modulariti. Rangka kerja yang dicadangkan adalah modular dan modul boleh digabungkan dengan cara yang berbeza untuk mencipta sistem yang berbeza. Khususnya, anda boleh mengkonfigurasi sistem untuk berjalan pada satu nod dalam satu penjelmaan, dan pada berbilang nod dalam satu lagi. Anda boleh membuat beberapa konfigurasi untuk contoh pengeluaran sistem.
  10. Menguji. Dengan menggantikan perkhidmatan individu dengan objek olok-olok, anda boleh mendapatkan beberapa versi sistem yang mudah untuk diuji.
  11. Ujian integrasi. Mempunyai konfigurasi tunggal untuk keseluruhan sistem yang diedarkan memungkinkan untuk menjalankan semua komponen dalam persekitaran terkawal sebagai sebahagian daripada ujian integrasi. Ia mudah untuk dicontohi, sebagai contoh, situasi di mana beberapa nod boleh diakses.

Kelemahan dan batasan

Konfigurasi tersusun berbeza daripada pendekatan konfigurasi lain dan mungkin tidak sesuai untuk sesetengah aplikasi. Di bawah adalah beberapa kelemahan:

  1. Konfigurasi statik. Kadangkala anda perlu membetulkan konfigurasi dalam pengeluaran dengan cepat, memintas semua mekanisme perlindungan. Dengan pendekatan ini ia boleh menjadi lebih sukar. Sekurang-kurangnya, kompilasi dan penggunaan automatik masih diperlukan. Ini adalah ciri berguna pendekatan dan kelemahan dalam beberapa kes.
  2. Penjanaan konfigurasi. Sekiranya fail konfigurasi dijana oleh alat automatik, usaha tambahan mungkin diperlukan untuk menyepadukan skrip binaan.
  3. Alatan. Pada masa ini, utiliti dan teknik yang direka untuk berfungsi dengan konfigurasi adalah berdasarkan fail teks. Tidak semua utiliti/teknik sedemikian akan tersedia dalam konfigurasi yang disusun.
  4. Perubahan sikap diperlukan. Pembangun dan DevOps terbiasa dengan fail teks. Idea untuk menyusun konfigurasi mungkin agak tidak dijangka dan luar biasa dan menyebabkan penolakan.
  5. Proses pembangunan berkualiti tinggi diperlukan. Untuk menggunakan konfigurasi yang disusun dengan selesa, automasi penuh proses membina dan menggunakan aplikasi (CI/CD) adalah perlu. Jika tidak, ia akan menjadi agak menyusahkan.

Marilah kita juga memikirkan beberapa batasan contoh yang dipertimbangkan yang tidak berkaitan dengan idea konfigurasi yang disusun:

  1. Jika kami memberikan maklumat konfigurasi yang tidak perlu yang tidak digunakan oleh nod, maka pengkompil tidak akan membantu kami mengesan pelaksanaan yang hilang. Masalah ini boleh diselesaikan dengan meninggalkan Corak Kek dan menggunakan jenis yang lebih tegar, contohnya, HList atau jenis data algebra (kelas kes) untuk mewakili konfigurasi.
  2. Terdapat baris dalam fail konfigurasi yang tidak berkaitan dengan konfigurasi itu sendiri: (package, import,pengisytiharan objek; override def's untuk parameter yang mempunyai nilai lalai). Ini boleh dielakkan sebahagiannya jika anda melaksanakan DSL anda sendiri. Selain itu, jenis konfigurasi lain (contohnya, XML) juga mengenakan sekatan tertentu pada struktur fail.
  3. Untuk tujuan siaran ini, kami tidak mempertimbangkan konfigurasi semula dinamik sekumpulan nod yang serupa.

Kesimpulan

Dalam siaran ini, kami meneroka idea untuk mewakili konfigurasi dalam kod sumber menggunakan keupayaan lanjutan sistem jenis Scala. Pendekatan ini boleh digunakan dalam pelbagai aplikasi sebagai pengganti kaedah konfigurasi tradisional berdasarkan fail xml atau teks. Walaupun contoh kami dilaksanakan dalam Scala, idea yang sama boleh dipindahkan ke bahasa yang disusun lain (seperti Kotlin, C#, Swift, ...). Anda boleh mencuba pendekatan ini dalam salah satu projek berikut, dan, jika ia tidak berfungsi, teruskan ke fail teks, menambah bahagian yang hilang.

Sememangnya, konfigurasi yang disusun memerlukan proses pembangunan berkualiti tinggi. Sebagai balasan, kualiti tinggi dan kebolehpercayaan konfigurasi dipastikan.

Pendekatan yang dipertimbangkan boleh diperluaskan:

  1. Anda boleh menggunakan makro untuk melakukan semakan masa kompilasi.
  2. Anda boleh melaksanakan DSL untuk membentangkan konfigurasi dengan cara yang boleh diakses oleh pengguna akhir.
  3. Anda boleh melaksanakan pengurusan sumber dinamik dengan pelarasan konfigurasi automatik. Sebagai contoh, menukar bilangan nod dalam kelompok memerlukan (1) setiap nod menerima konfigurasi yang sedikit berbeza; (2) pengurus kluster menerima maklumat tentang nod baharu.

Ucapan terima kasih

Saya ingin mengucapkan terima kasih kepada Andrei Saksonov, Pavel Popov dan Anton Nekhaev atas kritikan membina mereka terhadap draf artikel.

Sumber: www.habr.com

Tambah komen