Blok bangunan aplikasi terdistribusi. Pendekatan pertama

Blok bangunan aplikasi terdistribusi. Pendekatan pertama

Terakhir Artikel Kami memeriksa landasan teoritis arsitektur reaktif. Saatnya membicarakan aliran data, cara menerapkan sistem Erlang/Elixir reaktif, dan pola pengiriman pesan di dalamnya:

  • Respon permintaan
  • Respons yang Dipotong-potong
  • Tanggapan dengan Permintaan
  • Publikasikan-berlangganan
  • Terbalik Publikasikan-berlangganan
  • Pembagian tugas

SOA, MSA dan Pesan

SOA, MSA adalah arsitektur sistem yang menentukan aturan untuk membangun sistem, sementara perpesanan menyediakan primitif untuk implementasinya.

Saya tidak ingin mempromosikan arsitektur sistem ini atau itu. Saya ingin menggunakan praktik yang paling efektif dan berguna untuk proyek dan bisnis tertentu. Apapun paradigma yang kita pilih, lebih baik membuat blok sistem dengan memperhatikan cara Unix: komponen dengan konektivitas minimal, bertanggung jawab atas entitas individual. Metode API melakukan tindakan paling sederhana dengan entitas.

Perpesanan, seperti namanya, adalah perantara pesan. Tujuan utamanya adalah untuk menerima dan mengirim pesan. Ia bertanggung jawab atas antarmuka pengiriman informasi, pembentukan saluran logis untuk transmisi informasi dalam sistem, perutean dan penyeimbangan, serta penanganan kesalahan di tingkat sistem.
Pesan yang kami kembangkan tidak mencoba untuk bersaing dengan atau menggantikan Rabbitmq. Fitur utamanya:

  • Distribusi.
    Titik pertukaran dapat dibuat di semua node cluster, sedekat mungkin dengan kode yang menggunakannya.
  • Kesederhanaan.
    Fokus pada meminimalkan kode boilerplate dan kemudahan penggunaan.
  • Performa lebih baik.
    Kami tidak mencoba mengulangi fungsi kelincimq, tetapi hanya menyoroti lapisan arsitektur dan transportasi, yang kami masukkan ke dalam OTP sesederhana mungkin, sehingga meminimalkan biaya.
  • Fleksibilitas.
    Setiap layanan dapat menggabungkan banyak templat pertukaran.
  • Ketahanan berdasarkan desain.
  • Skalabilitas.
    Perpesanan berkembang seiring dengan aplikasi. Saat beban bertambah, Anda dapat memindahkan titik pertukaran ke masing-masing mesin.

Komentar. Dalam hal organisasi kode, proyek meta sangat cocok untuk sistem Erlang/Elixir yang kompleks. Semua kode proyek terletak di satu repositori - proyek payung. Pada saat yang sama, layanan mikro diisolasi sebanyak mungkin dan melakukan operasi sederhana yang bertanggung jawab atas entitas terpisah. Dengan pendekatan ini, mudah untuk memelihara API seluruh sistem, mudah untuk membuat perubahan, lebih mudah untuk menulis pengujian unit dan integrasi.

Komponen sistem berinteraksi secara langsung atau melalui broker. Dari perspektif perpesanan, setiap layanan memiliki beberapa fase kehidupan:

  • Inisialisasi layanan.
    Pada tahap ini, proses yang menjalankan layanan dan dependensinya dikonfigurasi dan diluncurkan.
  • Membuat titik pertukaran.
    Layanan ini dapat menggunakan titik pertukaran statis yang ditentukan dalam konfigurasi node, atau membuat titik pertukaran secara dinamis.
  • Pendaftaran layanan.
    Agar layanan dapat melayani permintaan, layanan harus didaftarkan di titik pertukaran.
  • Berfungsi normal.
    Pelayanan tersebut menghasilkan karya yang bermanfaat.
  • Matikan.
    Ada 2 jenis penghentian yang mungkin dilakukan: normal dan darurat. Selama pengoperasian normal, layanan terputus dari titik pertukaran dan berhenti. Dalam situasi darurat, perpesanan menjalankan salah satu skrip failover.

Kelihatannya cukup rumit, tapi kodenya tidak terlalu menakutkan. Contoh kode dengan komentar akan diberikan dalam analisis template nanti.

Bursa

Exchange point adalah proses pengiriman pesan yang mengimplementasikan logika interaksi dengan komponen dalam template pengiriman pesan. Dalam semua contoh yang disajikan di bawah, komponen berinteraksi melalui titik pertukaran, yang kombinasinya membentuk pesan.

Pola pertukaran pesan (MEP)

Secara global, pola pertukaran dapat dibagi menjadi dua arah dan satu arah. Yang pertama menyiratkan respons terhadap pesan masuk, yang kedua tidak. Contoh klasik dari pola dua arah dalam arsitektur klien-server adalah pola Permintaan-respons. Mari kita lihat template dan modifikasinya.

Permintaan-tanggapan atau RPC

RPC digunakan ketika kita perlu menerima respon dari proses lain. Proses ini mungkin berjalan di node yang sama atau berlokasi di benua berbeda. Di bawah ini adalah diagram interaksi antara klien dan server melalui pesan.

Blok bangunan aplikasi terdistribusi. Pendekatan pertama

Karena perpesanan sepenuhnya asinkron, pertukaran untuk klien dibagi menjadi 2 fase:

  1. Mengirim permintaan

    messaging:request(Exchange, ResponseMatchingTag, RequestDefinition, HandlerProcess).

    Bursa β€’ nama unik titik pertukaran
    ResponseMatchingTag β€’ label lokal untuk memproses respons. Misalnya, dalam hal mengirimkan beberapa permintaan identik milik pengguna yang berbeda.
    Definisi Permintaan - badan permintaan
    Proses Penangan β€’ PID pengendali. Proses ini akan menerima respon dari server.

  2. Memproses respons

    handle_info(#'$msg'{exchange = EXCHANGE, tag = ResponseMatchingTag,message = ResponsePayload}, State)

    ResponPayload - respons server.

Untuk server, prosesnya juga terdiri dari 2 tahap:

  1. Menginisialisasi titik pertukaran
  2. Pemrosesan permintaan yang diterima

Mari kita ilustrasikan template ini dengan kode. Katakanlah kita perlu mengimplementasikan layanan sederhana yang menyediakan satu metode waktu yang tepat.

Kode Server

Mari kita definisikan API layanan di api.hrl:

%% =====================================================
%%  entities
%% =====================================================
-record(time, {
  unixtime :: non_neg_integer(),
  datetime :: binary()
}).

-record(time_error, {
  code :: non_neg_integer(),
  error :: term()
}).

%% =====================================================
%%  methods
%% =====================================================
-record(time_req, {
  opts :: term()
}).
-record(time_resp, {
  result :: #time{} | #time_error{}
}).

Mari kita tentukan pengontrol layanan di time_controller.erl

%% Π’ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅ ΠΏΠΎΠΊΠ°Π·Π°Π½ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π·Π½Π°Ρ‡ΠΈΠΌΡ‹ΠΉ ΠΊΠΎΠ΄. Вставив Π΅Π³ΠΎ Π² шаблон gen_server ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ Ρ€Π°Π±ΠΎΡ‡ΠΈΠΉ сСрвис.

%% инициализация gen_server
init(Args) ->
  %% ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΊ Ρ‚ΠΎΡ‡ΠΊΠ΅ ΠΎΠ±ΠΌΠ΅Π½Π°
  messaging:monitor_exchange(req_resp, ?EXCHANGE, default, self())
  {ok, #{}}.

%% ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° события ΠΏΠΎΡ‚Π΅Ρ€ΠΈ связи с Ρ‚ΠΎΡ‡ΠΊΠΎΠΉ ΠΎΠ±ΠΌΠ΅Π½Π°. Π­Ρ‚ΠΎ ΠΆΠ΅ событиС ΠΏΡ€ΠΈΡ…ΠΎΠ΄ΠΈΡ‚, Ссли Ρ‚ΠΎΡ‡ΠΊΠ° ΠΎΠ±ΠΌΠ΅Π½Π° Π΅Ρ‰Π΅ Π½Π΅ Π·Π°ΠΏΡƒΡΡ‚ΠΈΠ»Π°ΡΡŒ.
handle_info(#exchange_die{exchange = ?EXCHANGE}, State) ->
  erlang:send(self(), monitor_exchange),
  {noreply, State};

%% ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° API
handle_info(#time_req{opts = _Opts}, State) ->
  messaging:response_once(Client, #time_resp{
result = #time{ unixtime = time_utils:unixtime(now()), datetime = time_utils:iso8601_fmt(now())}
  });
  {noreply, State};

%% Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΠ΅ Ρ€Π°Π±ΠΎΡ‚Ρ‹ gen_server
terminate(_Reason, _State) ->
  messaging:demonitor_exchange(req_resp, ?EXCHANGE, default, self()),
  ok.

Kode klien

Untuk mengirim permintaan ke layanan, Anda dapat memanggil API permintaan perpesanan di mana saja di klien:

case messaging:request(?EXCHANGE, tag, #time_req{opts = #{}}, self()) of
    ok -> ok;
    _ -> %% repeat or fail logic
end

Dalam sistem terdistribusi, konfigurasi komponen bisa sangat berbeda dan pada saat permintaan dibuat, pengiriman pesan mungkin belum dimulai, atau pengontrol layanan tidak akan siap untuk melayani permintaan tersebut. Oleh karena itu, kita perlu memeriksa respons pesan dan menangani kasus kegagalan.
Setelah pengiriman berhasil, klien akan menerima respon atau error dari layanan.
Mari kita tangani kedua kasus di handle_info:

handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time{unixtime = Utime}}}, State) ->
  ?debugVal(Utime),
  {noreply, State};

handle_info(#'$msg'{exchange = ?EXCHANGE, tag = tag, message = #time_resp{result = #time_error{code = ErrorCode}}}, State) ->
  ?debugVal({error, ErrorCode}),
  {noreply, State};

Respons yang Dipotong-potong

Sebaiknya hindari mengirim pesan berukuran besar. Responsif dan pengoperasian stabil seluruh sistem bergantung pada hal ini. Jika respons terhadap kueri memakan banyak memori, maka membaginya menjadi beberapa bagian adalah wajib.

Blok bangunan aplikasi terdistribusi. Pendekatan pertama

Izinkan saya memberi Anda beberapa contoh kasus seperti itu:

  • Komponen bertukar data biner, seperti file. Memecah respons menjadi bagian-bagian kecil membantu Anda bekerja secara efisien dengan file dengan ukuran berapa pun dan menghindari kelebihan memori.
  • Daftar. Misalnya, kita perlu memilih semua record dari tabel besar di database dan mentransfernya ke komponen lain.

Saya menyebut respons ini sebagai lokomotif. Bagaimanapun, 1024 pesan berukuran 1 MB lebih baik daripada satu pesan berukuran 1 GB.

Di cluster Erlang, kami mendapatkan manfaat tambahan - mengurangi beban pada titik pertukaran dan jaringan, karena tanggapan segera dikirim ke penerima, melewati titik pertukaran.

Tanggapan dengan Permintaan

Ini adalah modifikasi pola RPC yang agak jarang untuk membangun sistem dialog.

Blok bangunan aplikasi terdistribusi. Pendekatan pertama

Publikasikan-berlangganan (pohon distribusi data)

Sistem berbasis peristiwa mengirimkannya ke konsumen segera setelah datanya siap. Dengan demikian, sistem lebih rentan terhadap model push dibandingkan model pull atau polling. Fitur ini memungkinkan Anda menghindari pemborosan sumber daya dengan terus-menerus meminta dan menunggu data.
Gambar tersebut menunjukkan proses pendistribusian pesan kepada konsumen yang berlangganan topik tertentu.

Blok bangunan aplikasi terdistribusi. Pendekatan pertama

Contoh klasik penggunaan pola ini adalah distribusi negara: dunia game dalam permainan komputer, data pasar di bursa, informasi berguna dalam data feed.

Mari kita lihat kode pelanggan:

init(_Args) ->
  %% подписываСмся Π½Π° ΠΎΠ±ΠΌΠ΅Π½Π½ΠΈΠΊ, ΠΊΠ»ΡŽΡ‡ = key
  messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
  {ok, #{}}.

handle_info(#exchange_die{exchange = ?SUBSCRIPTION}, State) ->
  %% Ссли Ρ‚ΠΎΡ‡ΠΊΠ° ΠΎΠ±ΠΌΠ΅Π½Π° нСдоступна, Ρ‚ΠΎ пытаСмся ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒΡΡ
  messaging:subscribe(?SUBSCRIPTION, key, tag, self()),
  {noreply, State};

%% ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅ΠΌ ΠΏΡ€ΠΈΡˆΠ΅Π΄ΡˆΠΈΠ΅ сообщСния
handle_info(#'$msg'{exchange = ?SUBSCRIPTION, message = Msg}, State) ->
  ?debugVal(Msg),
  {noreply, State};

%% ΠΏΡ€ΠΈ остановкС потрСбитСля - ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π°Π΅ΠΌΡΡ ΠΎΡ‚ Ρ‚ΠΎΡ‡ΠΊΠΈ ΠΎΠ±ΠΌΠ΅Π½Π°
terminate(_Reason, _State) ->
  messaging:unsubscribe(?SUBSCRIPTION, key, tag, self()),
  ok.

Sumber dapat memanggil fungsi untuk mempublikasikan pesan di tempat mana pun yang nyaman:

messaging:publish_message(Exchange, Key, Message).

Bursa - nama titik pertukaran,
kunci - kunci perutean
Sambutan dari Manajer Umum PT. LUHAI INDUSTRIAL - muatan

Terbalik Publikasikan-berlangganan

Blok bangunan aplikasi terdistribusi. Pendekatan pertama

Dengan memperluas pub-sub, Anda bisa mendapatkan pola yang nyaman untuk login. Kumpulan sumber dan konsumen bisa sangat berbeda. Gambar tersebut menunjukkan kasus dengan satu konsumen dan berbagai sumber.

Pola pembagian tugas

Hampir setiap proyek melibatkan tugas pemrosesan yang tertunda, seperti membuat laporan, mengirimkan pemberitahuan, dan mengambil data dari sistem pihak ketiga. Throughput sistem yang melakukan tugas-tugas ini dapat dengan mudah ditingkatkan dengan menambahkan penangan. Yang tersisa bagi kami hanyalah membentuk sekelompok prosesor dan mendistribusikan tugas secara merata di antara mereka.

Mari kita lihat situasi yang muncul dengan menggunakan contoh 3 penangan. Bahkan pada tahap pembagian tugas, muncul pertanyaan tentang keadilan distribusi dan melimpahnya penangan. Distribusi round-robin akan bertanggung jawab atas keadilan, dan untuk menghindari situasi melimpahnya penangan, kami akan memberlakukan pembatasan batas_prefetch. Dalam kondisi sementara batas_prefetch akan mencegah satu penangan menerima semua tugas.

Perpesanan mengelola antrian dan prioritas pemrosesan. Prosesor menerima tugas saat mereka tiba. Tugas dapat diselesaikan dengan sukses atau gagal:

  • messaging:ack(Tack) - dipanggil jika pesan berhasil diproses
  • messaging:nack(Tack) - dipanggil dalam semua situasi darurat. Setelah tugas dikembalikan, pesan akan meneruskannya ke penangan lain.

Blok bangunan aplikasi terdistribusi. Pendekatan pertama

Mari kita asumsikan bahwa kegagalan kompleks terjadi saat memproses tiga tugas: prosesor 1, setelah menerima tugas, mogok tanpa sempat melaporkan apa pun ke titik pertukaran. Dalam hal ini, titik pertukaran akan mentransfer tugas ke pengendali lain setelah batas waktu ack habis. Karena alasan tertentu, handler 3 meninggalkan tugas tersebut dan mengirimkan nack; akibatnya, tugas tersebut juga dipindahkan ke handler lain yang berhasil menyelesaikannya.

Ringkasan pendahuluan

Kami telah membahas dasar-dasar sistem terdistribusi dan memperoleh pemahaman dasar tentang penggunaannya di Erlang/Elixir.

Dengan menggabungkan pola-pola dasar, Anda dapat membangun paradigma kompleks untuk memecahkan masalah yang muncul.

Di bagian akhir seri ini, kita akan melihat masalah umum pengorganisasian layanan, perutean dan penyeimbangan, serta berbicara tentang sisi praktis skalabilitas dan toleransi kesalahan sistem.

Akhir bagian kedua.

foto Marius Christensen
Ilustrasi disiapkan menggunakan websequencediagrams.com

Sumber: www.habr.com

Tambah komentar