Pola arsitektur yang nyaman

Hei Habr!

Mengingat kejadian terkini akibat virus corona, sejumlah layanan Internet mulai menerima peningkatan beban. Misalnya, Salah satu jaringan ritel Inggris menghentikan situs pemesanan online-nya., karena kapasitasnya tidak mencukupi. Dan tidak selalu mungkin untuk mempercepat server hanya dengan menambahkan peralatan yang lebih kuat, tetapi permintaan klien harus diproses (atau permintaan tersebut akan diteruskan ke pesaing).

Pada artikel ini saya akan membahas secara singkat tentang praktik populer yang memungkinkan Anda membuat layanan yang cepat dan toleran terhadap kesalahan. Namun, dari kemungkinan skema pengembangan, saya hanya memilih yang ada saat ini mudah digunakan. Untuk setiap item, Anda memiliki perpustakaan yang sudah jadi, atau Anda memiliki kesempatan untuk menyelesaikan masalah menggunakan platform cloud.

Penskalaan horizontal

Poin paling sederhana dan paling terkenal. Secara konvensional, dua skema distribusi beban yang paling umum adalah penskalaan horizontal dan vertikal. Dalam kasus pertama Anda mengizinkan layanan berjalan secara paralel, sehingga mendistribusikan beban di antara layanan tersebut. Di kedua Anda memesan server yang lebih kuat atau mengoptimalkan kode.

Misalnya, saya akan mengambil penyimpanan file cloud abstrak, yaitu beberapa analog dari OwnCloud, OneDrive, dan sebagainya.

Gambar standar rangkaian seperti itu ada di bawah, tetapi ini hanya menunjukkan kompleksitas sistem. Bagaimanapun, kita perlu menyinkronkan layanan. Apa yang terjadi jika pengguna menyimpan file dari tablet dan kemudian ingin melihatnya dari ponsel?

Pola arsitektur yang nyaman
Perbedaan antara pendekatan-pendekatan tersebut: dalam penskalaan vertikal, kami siap meningkatkan kekuatan node, dan dalam penskalaan horizontal, kami siap menambahkan node baru untuk mendistribusikan beban.

CQRS

Pemisahan Tanggung Jawab Permintaan Perintah Pola yang cukup penting, karena memungkinkan klien yang berbeda tidak hanya terhubung ke layanan yang berbeda, tetapi juga menerima aliran peristiwa yang sama. Manfaatnya tidak begitu jelas untuk aplikasi sederhana, namun sangat penting (dan sederhana) untuk layanan yang sibuk. Intinya: aliran data masuk dan keluar tidak boleh berpotongan. Artinya, Anda tidak dapat mengirim permintaan dan mengharapkan respons; sebaliknya, Anda mengirim permintaan ke layanan A, namun menerima respons dari layanan B.

Bonus pertama dari pendekatan ini adalah kemampuan untuk memutus koneksi (dalam arti luas) saat menjalankan permintaan yang panjang. Misalnya, mari kita ambil urutan yang kurang lebih standar:

  1. Klien mengirimkan permintaan ke server.
  2. Server memulai waktu pemrosesan yang lama.
  3. Server merespons klien dengan hasilnya.

Bayangkan pada poin 2 koneksi terputus (atau jaringan tersambung kembali, atau pengguna membuka halaman lain, memutus koneksi). Dalam hal ini, akan sulit bagi server untuk mengirimkan respons kepada pengguna dengan informasi tentang apa yang sebenarnya sedang diproses. Menggunakan CQRS, urutannya akan sedikit berbeda:

  1. Klien telah berlangganan pembaruan.
  2. Klien mengirimkan permintaan ke server.
  3. Server menjawab “permintaan diterima.”
  4. Server merespons dengan hasilnya melalui saluran dari titik “1”.

Pola arsitektur yang nyaman

Seperti yang Anda lihat, skemanya sedikit lebih rumit. Selain itu, pendekatan permintaan-respons yang intuitif tidak ada di sini. Namun, seperti yang Anda lihat, putusnya koneksi saat memproses permintaan tidak akan menyebabkan kesalahan. Selain itu, jika sebenarnya pengguna terhubung ke layanan dari beberapa perangkat (misalnya, dari ponsel dan tablet), Anda dapat memastikan bahwa responsnya datang ke kedua perangkat.

Menariknya, kode pemrosesan pesan masuk menjadi sama (tidak 100%) baik untuk event yang dipengaruhi oleh klien itu sendiri, maupun untuk event lain, termasuk dari klien lain.

Namun, kenyataannya kami mendapat bonus tambahan karena aliran searah dapat ditangani dengan gaya fungsional (menggunakan RX dan sejenisnya). Dan ini sudah menjadi nilai tambah yang serius, karena pada dasarnya aplikasi dapat dibuat sepenuhnya reaktif, dan juga menggunakan pendekatan fungsional. Untuk program yang gemuk, hal ini dapat menghemat sumber daya pengembangan dan dukungan secara signifikan.

Jika kita menggabungkan pendekatan ini dengan penskalaan horizontal, maka sebagai bonusnya kita mendapatkan kemampuan untuk mengirim permintaan ke satu server dan menerima tanggapan dari server lain. Dengan demikian, klien dapat memilih layanan yang nyaman baginya, dan sistem di dalamnya tetap dapat memproses peristiwa dengan benar.

Sumber Acara

Seperti yang Anda ketahui, salah satu fitur utama sistem terdistribusi adalah tidak adanya waktu yang sama, bagian kritis yang sama. Untuk satu proses, Anda dapat melakukan sinkronisasi (pada mutex yang sama), di mana Anda yakin tidak ada orang lain yang mengeksekusi kode ini. Namun, hal ini berbahaya untuk sistem terdistribusi, karena memerlukan overhead, dan juga akan mematikan semua keindahan penskalaan - semua komponen akan tetap menunggu satu hal.

Dari sini kita mendapatkan fakta penting - sistem terdistribusi yang cepat tidak dapat disinkronkan, karena kita akan menurunkan kinerja. Di sisi lain, seringkali kita membutuhkan konsistensi tertentu antar komponen. Dan untuk ini Anda dapat menggunakan pendekatan dengan konsistensi akhirnya, yang dijamin jika tidak ada perubahan data selama jangka waktu tertentu setelah pembaruan terakhir (“akhirnya”), semua kueri akan mengembalikan nilai pembaruan terakhir.

Penting untuk dipahami bahwa ini cukup sering digunakan untuk database klasik konsistensi yang kuat, di mana setiap node memiliki informasi yang sama (hal ini sering kali dicapai jika transaksi dianggap selesai hanya setelah server kedua merespons). Ada beberapa relaksasi di sini karena tingkat isolasi, tetapi gagasan umumnya tetap sama - Anda dapat hidup di dunia yang sepenuhnya harmonis.

Namun, mari kita kembali ke tugas awal. Jika bagian dari sistem dapat dibangun dengan konsistensi akhirnya, maka kita dapat membuat diagram berikut.

Pola arsitektur yang nyaman

Ciri-ciri penting dari pendekatan ini:

  • Setiap permintaan yang masuk ditempatkan dalam satu antrian.
  • Saat memproses permintaan, layanan juga dapat menempatkan tugas di antrean lain.
  • Setiap peristiwa yang masuk memiliki pengidentifikasi (yang diperlukan untuk deduplikasi).
  • Antrian secara ideologis bekerja sesuai dengan skema “tambahkan saja”. Anda tidak dapat menghapus elemen darinya atau mengatur ulangnya.
  • Antrian bekerja sesuai skema FIFO (maaf tautologinya). Jika Anda perlu melakukan eksekusi paralel, maka pada satu tahap Anda harus memindahkan objek ke antrian yang berbeda.

Izinkan saya mengingatkan Anda bahwa kami sedang mempertimbangkan kasus penyimpanan file online. Dalam hal ini, sistem akan terlihat seperti ini:

Pola arsitektur yang nyaman

Penting bahwa layanan dalam diagram tidak berarti server terpisah. Bahkan prosesnya mungkin sama. Hal lain yang penting: secara ideologis, hal-hal tersebut dipisahkan sedemikian rupa sehingga penskalaan horizontal dapat dengan mudah diterapkan.

Dan untuk dua pengguna, diagramnya akan terlihat seperti ini (layanan yang ditujukan untuk pengguna berbeda ditunjukkan dalam warna berbeda):

Pola arsitektur yang nyaman

Bonus dari kombinasi ini:

  • Layanan pemrosesan informasi dipisahkan. Antriannya juga dipisahkan. Jika kita perlu meningkatkan throughput sistem, kita hanya perlu meluncurkan lebih banyak layanan di lebih banyak server.
  • Saat kami menerima informasi dari pengguna, kami tidak perlu menunggu hingga data disimpan sepenuhnya. Sebaliknya, kita hanya perlu menjawab “ok” dan kemudian mulai bekerja secara bertahap. Pada saat yang sama, antrian memuluskan puncak, karena penambahan objek baru terjadi dengan cepat, dan pengguna tidak perlu menunggu sampai seluruh siklus selesai.
  • Sebagai contoh, saya menambahkan layanan deduplikasi yang mencoba menggabungkan file identik. Jika ini berfungsi untuk waktu yang lama dalam 1% kasus, klien hampir tidak akan menyadarinya (lihat di atas), yang merupakan nilai tambah yang besar, karena kami tidak lagi diharuskan untuk XNUMX% cepat dan dapat diandalkan.

Namun, kekurangannya langsung terlihat:

  • Sistem kami telah kehilangan konsistensinya yang ketat. Artinya jika, misalnya, Anda berlangganan layanan yang berbeda, maka secara teoritis Anda bisa mendapatkan status yang berbeda (karena salah satu layanan mungkin tidak punya waktu untuk menerima pemberitahuan dari antrian internal). Konsekuensi lainnya, sistem sekarang tidak memiliki waktu bersama. Artinya, tidak mungkin, misalnya, mengurutkan semua peristiwa hanya berdasarkan waktu kedatangan, karena jam antar server mungkin tidak sinkron (selain itu, waktu yang sama di dua server adalah utopia).
  • Tidak ada peristiwa yang dapat dibatalkan dengan mudah (seperti yang dapat dilakukan dengan database). Sebaliknya, Anda perlu menambahkan acara baru - acara kompensasi, yang akan mengubah status terakhir menjadi status yang diperlukan. Sebagai contoh dari area serupa: tanpa menulis ulang riwayat (yang buruk dalam beberapa kasus), Anda tidak dapat mengembalikan komit di git, tetapi Anda dapat membuat yang khusus kembalikan komit, yang pada dasarnya hanya mengembalikan keadaan lama. Namun, baik komitmen yang salah maupun kemunduran akan tetap ada dalam sejarah.
  • Skema data dapat berubah dari rilis ke rilis, tetapi peristiwa lama tidak lagi dapat diperbarui ke standar baru (karena pada prinsipnya peristiwa tidak dapat diubah).

Seperti yang Anda lihat, Sumber Acara berfungsi baik dengan CQRS. Selain itu, mengimplementasikan sistem dengan antrian yang efisien dan nyaman, tetapi tanpa memisahkan aliran data, itu sendiri sudah sulit, karena Anda harus menambahkan titik sinkronisasi yang akan menetralisir seluruh efek positif dari antrian. Menerapkan kedua pendekatan sekaligus, perlu sedikit penyesuaian kode program. Dalam kasus kami, saat mengirim file ke server, respons yang muncul hanya “ok”, yang berarti “operasi penambahan file telah disimpan”. Secara formal, ini tidak berarti bahwa data sudah tersedia di perangkat lain (misalnya, layanan deduplikasi dapat membangun kembali indeks). Namun, setelah beberapa waktu, klien akan menerima notifikasi berupa “file X telah disimpan”.

Hasil dari:

  • Jumlah status pengiriman file meningkat: alih-alih “file terkirim” klasik, kita mendapatkan dua: “file telah ditambahkan ke antrian di server” dan “file telah disimpan di penyimpanan”. Yang terakhir berarti perangkat lain sudah dapat mulai menerima file (disesuaikan dengan fakta bahwa antrian beroperasi pada kecepatan yang berbeda).
  • Karena informasi pengiriman kini datang melalui saluran yang berbeda, kami perlu mencari solusi untuk menerima status pemrosesan file. Sebagai konsekuensinya: tidak seperti respons-permintaan klasik, klien dapat dimulai ulang saat memproses file, namun status pemrosesan itu sendiri akan benar. Selain itu, item ini pada dasarnya berfungsi di luar kotak. Konsekuensinya: kita sekarang lebih toleran terhadap kegagalan.

Sharding

Seperti dijelaskan di atas, sistem sumber acara kurang memiliki konsistensi yang ketat. Artinya kita dapat menggunakan beberapa penyimpanan tanpa ada sinkronisasi di antara keduanya. Mendekati masalah kita, kita dapat:

  • Pisahkan file berdasarkan jenisnya. Misalnya, gambar/video dapat diterjemahkan dan format yang lebih efisien dapat dipilih.
  • Pisahkan akun berdasarkan negara. Karena banyak undang-undang, hal ini mungkin diperlukan, namun skema arsitektur ini memberikan peluang seperti itu secara otomatis

Pola arsitektur yang nyaman

Jika Anda ingin mentransfer data dari satu penyimpanan ke penyimpanan lainnya, maka cara standar saja tidak lagi cukup. Sayangnya, dalam kasus ini, Anda perlu menghentikan antrean, melakukan migrasi, lalu memulainya. Dalam kasus umum, data tidak dapat ditransfer “on the fly”, namun, jika antrean peristiwa disimpan sepenuhnya, dan Anda memiliki cuplikan status penyimpanan sebelumnya, maka kami dapat memutar ulang peristiwa sebagai berikut:

  • Di Sumber Peristiwa, setiap peristiwa memiliki pengidentifikasinya sendiri (idealnya, tidak menurun). Ini berarti kita dapat menambahkan field ke penyimpanan - id elemen yang terakhir diproses.
  • Kami menduplikasi antrian sehingga semua peristiwa dapat diproses untuk beberapa penyimpanan independen (yang pertama sudah berisi data, dan yang kedua baru, tetapi masih kosong). Antrean kedua tentunya belum diproses.
  • Kami meluncurkan antrian kedua (yaitu, kami mulai memutar ulang acara).
  • Ketika antrian baru relatif kosong (yaitu, perbedaan waktu rata-rata antara menambahkan elemen dan mengambilnya dapat diterima), Anda dapat mulai mengalihkan pembaca ke penyimpanan baru.

Seperti yang Anda lihat, kami tidak memiliki, dan masih belum memiliki, konsistensi yang ketat dalam sistem kami. Yang ada hanyalah konsistensi akhir, yaitu jaminan bahwa peristiwa diproses dalam urutan yang sama (tetapi mungkin dengan penundaan yang berbeda). Dan, dengan menggunakan ini, kita dapat dengan relatif mudah mentransfer data tanpa menghentikan sistem ke belahan dunia lain.

Jadi, melanjutkan contoh kita tentang penyimpanan file online, arsitektur seperti itu telah memberi kita sejumlah bonus:

  • Kita dapat memindahkan objek lebih dekat ke pengguna secara dinamis. Dengan cara ini Anda dapat meningkatkan kualitas layanan.
  • Kami mungkin menyimpan beberapa data di dalam perusahaan. Misalnya, pengguna Perusahaan sering kali mengharuskan data mereka disimpan di pusat data yang terkontrol (untuk menghindari kebocoran data). Melalui sharding kita dapat dengan mudah mendukung hal ini. Dan tugasnya menjadi lebih mudah jika pelanggan memiliki cloud yang kompatibel (misalnya, Azure dihosting sendiri).
  • Dan yang paling penting adalah kita tidak perlu melakukan hal ini. Bagaimanapun, sebagai permulaan, kami akan cukup senang dengan satu penyimpanan untuk semua akun (untuk mulai bekerja dengan cepat). Dan fitur utama dari sistem ini adalah meskipun dapat diperluas, pada tahap awal sistem ini cukup sederhana. Anda hanya tidak perlu langsung menulis kode yang berfungsi dengan sejuta antrian independen terpisah, dll. Jika perlu, hal ini bisa dilakukan di masa depan.

Hosting Konten Statis

Poin ini mungkin tampak cukup jelas, tetapi masih diperlukan untuk aplikasi yang dimuat secara standar. Esensinya sederhana: semua konten statis didistribusikan bukan dari server yang sama tempat aplikasi berada, tetapi dari server khusus yang didedikasikan khusus untuk tugas ini. Hasilnya, operasi ini dilakukan lebih cepat (nginx bersyarat menyajikan file lebih cepat dan lebih murah dibandingkan server Java). Ditambah arsitektur CDN (Konten Pengiriman Jaringan) memungkinkan kami menempatkan file kami lebih dekat dengan pengguna akhir, yang berdampak positif pada kenyamanan bekerja dengan layanan.

Contoh konten statis yang paling sederhana dan standar adalah sekumpulan skrip dan gambar untuk situs web. Semuanya sederhana dengan mereka - mereka diketahui sebelumnya, kemudian arsip diunggah ke server CDN, dari mana mereka didistribusikan ke pengguna akhir.

Namun, pada kenyataannya, untuk konten statis, Anda dapat menggunakan pendekatan yang agak mirip dengan arsitektur lambda. Mari kembali ke tugas kita (penyimpanan file online), di mana kita perlu mendistribusikan file ke pengguna. Solusi paling sederhana adalah dengan membuat layanan yang, untuk setiap permintaan pengguna, melakukan semua pemeriksaan yang diperlukan (otorisasi, dll.), dan kemudian mengunduh file langsung dari penyimpanan kami. Kerugian utama dari pendekatan ini adalah bahwa konten statis (dan file dengan revisi tertentu, pada kenyataannya, adalah konten statis) didistribusikan oleh server yang sama yang berisi logika bisnis. Sebagai gantinya, Anda dapat membuat diagram berikut:

  • Server menyediakan URL unduhan. Bisa dalam bentuk file_id + key, dimana key adalah tanda tangan digital mini yang memberikan hak untuk mengakses resource selama XNUMX jam ke depan.
  • File ini didistribusikan oleh nginx sederhana dengan opsi berikut:
    • Penyimpanan konten dalam cache. Karena layanan ini dapat ditempatkan di server terpisah, kami memiliki cadangan untuk masa depan dengan kemampuan untuk menyimpan semua file unduhan terbaru di disk.
    • Memeriksa kunci pada saat pembuatan koneksi
  • Opsional: pemrosesan konten streaming. Misal kita kompres semua file yang ada di service, maka kita bisa melakukan unzip langsung di modul ini. Konsekuensinya: Operasi IO dilakukan di tempat yang seharusnya. Pengarsip di Java akan dengan mudah mengalokasikan banyak memori ekstra, tetapi menulis ulang layanan dengan logika bisnis ke dalam kondisi Rust/C++ juga mungkin tidak efektif. Dalam kasus kami, proses (atau bahkan layanan) yang berbeda digunakan, dan oleh karena itu kami dapat memisahkan logika bisnis dan operasi IO dengan cukup efektif.

Pola arsitektur yang nyaman

Skema ini tidak terlalu mirip dengan pendistribusian konten statis (karena kami tidak mengunggah seluruh paket statis di suatu tempat), namun kenyataannya, pendekatan ini justru berkaitan dengan pendistribusian data yang tidak dapat diubah. Selain itu, skema ini dapat digeneralisasikan ke kasus lain di mana kontennya tidak sekadar statis, namun dapat direpresentasikan sebagai sekumpulan blok yang tidak dapat diubah dan tidak dapat dihapus (walaupun dapat ditambahkan).

Sebagai contoh lain (untuk penguatan): jika Anda pernah bekerja dengan Jenkins/TeamCity, maka Anda tahu bahwa kedua solusi tersebut ditulis dalam Java. Keduanya adalah proses Java yang menangani orkestrasi build dan manajemen konten. Secara khusus, keduanya memiliki tugas seperti “mentransfer file/folder dari server.” Sebagai contoh: mengeluarkan artefak, mentransfer kode sumber (ketika agen tidak mengunduh kode langsung dari repositori, tetapi server melakukannya untuknya), akses ke log. Semua tugas ini berbeda dalam beban IO-nya. Artinya, server yang bertanggung jawab atas logika bisnis yang kompleks pada saat yang sama harus mampu secara efektif mendorong aliran data yang besar melalui dirinya sendiri. Dan yang paling menarik adalah operasi semacam itu dapat didelegasikan ke nginx yang sama sesuai dengan skema yang persis sama (kecuali kunci data harus ditambahkan ke permintaan).

Namun, jika kita kembali ke sistem kita, kita mendapatkan diagram serupa:

Pola arsitektur yang nyaman

Seperti yang Anda lihat, sistem ini menjadi jauh lebih kompleks. Sekarang bukan hanya proses mini yang menyimpan file secara lokal. Sekarang yang diperlukan bukanlah dukungan paling sederhana, kontrol versi API, dll. Oleh karena itu, setelah semua diagram digambar, yang terbaik adalah mengevaluasi secara rinci apakah ekstensibilitas sepadan dengan biayanya. Namun, jika Anda ingin dapat memperluas sistem (termasuk bekerja dengan lebih banyak pengguna), maka Anda harus mencari solusi serupa. Namun, sebagai hasilnya, sistem secara arsitektur siap untuk peningkatan beban (hampir setiap komponen dapat dikloning untuk penskalaan horizontal). Sistem dapat diperbarui tanpa menghentikannya (hanya saja beberapa operasi akan sedikit melambat).

Seperti yang saya katakan di awal, kini sejumlah layanan Internet mulai mendapat peningkatan beban. Dan beberapa di antaranya mulai berhenti bekerja dengan benar. Faktanya, sistem tersebut gagal tepat pada saat bisnis seharusnya menghasilkan uang. Artinya, alih-alih menunda pengiriman, alih-alih menyarankan kepada pelanggan “rencanakan pengiriman Anda untuk beberapa bulan mendatang”, sistem hanya mengatakan “pergi ke pesaing Anda”. Faktanya, inilah akibat dari produktivitas yang rendah: kerugian akan terjadi tepat pada saat keuntungan berada pada titik tertinggi.

Kesimpulan

Semua pendekatan ini telah diketahui sebelumnya. VK yang sama telah lama menggunakan ide Hosting Konten Statis untuk menampilkan gambar. Banyak game online yang menggunakan skema Sharding untuk membagi pemain ke dalam wilayah atau memisahkan lokasi game (jika dunia itu sendiri adalah satu wilayah). Pendekatan Sumber Acara secara aktif digunakan dalam email. Sebagian besar aplikasi perdagangan di mana data terus-menerus diterima sebenarnya dibangun dengan pendekatan CQRS agar dapat memfilter data yang diterima. Nah, penskalaan horizontal telah digunakan di banyak layanan sejak lama.

Namun, yang terpenting, semua pola ini menjadi sangat mudah diterapkan dalam aplikasi modern (tentu saja jika sesuai). Clouds langsung menawarkan Sharding dan penskalaan horizontal, yang jauh lebih mudah daripada memesan sendiri server khusus yang berbeda di pusat data yang berbeda. CQRS menjadi lebih mudah, hanya karena pengembangan perpustakaan seperti RX. Sekitar 10 tahun yang lalu, sebuah situs web langka dapat mendukung hal ini. Pengadaan Acara juga sangat mudah diatur berkat container siap pakai dengan Apache Kafka. 10 tahun yang lalu hal ini merupakan sebuah inovasi, sekarang sudah menjadi hal yang lumrah. Sama halnya dengan Hosting Konten Statis: karena teknologi yang lebih nyaman (termasuk fakta bahwa terdapat dokumentasi terperinci dan database jawaban yang besar), pendekatan ini menjadi lebih sederhana.

Alhasil, penerapan sejumlah pola arsitektur yang agak rumit kini menjadi lebih sederhana, yang berarti sebaiknya dicermati terlebih dahulu. Jika dalam aplikasi berusia sepuluh tahun salah satu solusi di atas ditinggalkan karena tingginya biaya implementasi dan pengoperasian, sekarang, dalam aplikasi baru, atau setelah pemfaktoran ulang, Anda dapat membuat layanan yang secara arsitektural dapat diperluas ( dalam hal kinerja) dan siap pakai untuk permintaan baru dari klien (misalnya, untuk melokalisasi data pribadi).

Dan yang paling penting: mohon jangan gunakan pendekatan ini jika Anda memiliki aplikasi sederhana. Ya, mereka indah dan menarik, tetapi untuk situs dengan kunjungan puncak 100 orang, Anda sering kali dapat menggunakan monolit klasik (setidaknya di luar, semua yang ada di dalamnya dapat dibagi menjadi beberapa modul, dll.).

Sumber: www.habr.com

Tambah komentar