Apa itu layanan jaringan? Ini adalah program yang menerima permintaan masuk melalui jaringan dan memprosesnya, mungkin mengembalikan respons.
Ada banyak aspek di mana layanan jaringan berbeda satu sama lain. Pada artikel ini, saya fokus pada bagaimana menangani permintaan yang masuk.
Memilih metode pemrosesan permintaan memiliki konsekuensi yang luas. Bagaimana cara membuat layanan obrolan dengan 100.000 koneksi simultan? Pendekatan mana yang harus diambil untuk mengekstrak data dari aliran file yang tidak terstruktur dengan baik? Pilihan yang salah akan menyebabkan pemborosan waktu dan energi.
Artikel ini membahas pendekatan seperti kumpulan proses / utas, pemrosesan berorientasi peristiwa, setengah sinkronisasi / setengah pola async dan banyak lainnya. Banyak contoh diberikan, pro dan kontra dari pendekatan, fitur dan aplikasi mereka dipertimbangkan.
Pendahuluan
Topik metode pemrosesan kueri bukanlah hal baru, lihat, misalnya: satu , dua . Namun, sebagian besar artikel menganggapnya hanya sebagian. Artikel ini dimaksudkan untuk mengisi kekosongan dan memberikan presentasi yang konsisten tentang masalah ini.
Pendekatan berikut akan dipertimbangkan:
- pemrosesan berurutan
- proses permintaan
- minta aliran
- pool proses / utas
- pemrosesan berorientasi peristiwa (pola reaktor)
- half sync / half async pattern
- pengolahan conveyor
Perlu dicatat bahwa layanan yang memproses permintaan belum tentu merupakan layanan jaringan. Ini mungkin layanan yang menerima tugas baru dari database atau antrian tugas. Dalam artikel ini, layanan jaringan dimaksudkan, tetapi Anda perlu memahami bahwa pendekatan yang dipertimbangkan memiliki cakupan yang lebih luas.
TL; DR
Di akhir artikel adalah daftar dengan deskripsi singkat dari setiap pendekatan.
Pemrosesan berurutan
Aplikasi terdiri dari utas tunggal dalam satu proses tunggal. Semua permintaan diproses hanya secara berurutan. Tidak ada paralelisme. Jika beberapa permintaan datang ke layanan pada saat yang sama, salah satunya diproses, sisanya diantrekan.
Plus, pendekatan ini mudah diterapkan. Tidak ada kunci dan persaingan untuk sumber daya. Minus yang jelas adalah ketidakmampuan untuk skala dengan sejumlah besar pelanggan.
Proses permintaan
Aplikasi terdiri dari proses inti yang menerima permintaan dan alur kerja yang masuk. Untuk setiap permintaan baru, proses utama membuat alur kerja yang memproses permintaan. Penskalaan dengan jumlah permintaan itu sederhana: setiap permintaan mendapatkan prosesnya sendiri.
Tidak ada yang rumit dalam arsitektur ini, tetapi ada masalah keterbatasan :
- Prosesnya menghabiskan banyak sumber daya.
Coba buat 10.000 koneksi bersamaan ke RDBMS PostgreSQL dan lihat hasilnya. - Proses tidak memiliki memori bersama (default). Jika Anda memerlukan akses ke data bersama atau cache bersama, Anda harus memetakan memori bersama (memanggil linux mmap, munmap) atau menggunakan penyimpanan eksternal (memcahed, redis)
Masalah-masalah ini sama sekali tidak berhenti. Berikut ini akan menunjukkan bagaimana mereka dikelola di PostgeSQL RDBMS.
Kelebihan arsitektur ini:
- Jatuhnya salah satu proses tidak akan mempengaruhi yang lain. Misalnya, kesalahan pemrosesan kasus yang jarang terjadi tidak akan menjatuhkan seluruh aplikasi, hanya permintaan yang diproses yang akan menderita
- Diferensiasi hak akses pada level sistem operasi. Karena proses adalah inti dari OS, Anda dapat menggunakan mekanisme standarnya untuk membatasi hak akses ke sumber daya OS
- Anda dapat mengubah proses yang sedang berjalan dengan cepat. Misalnya, jika skrip terpisah digunakan untuk memproses permintaan, maka untuk mengganti algoritma pemrosesan, cukup untuk mengubah skrip. Contoh akan dipertimbangkan di bawah ini.
- Mesin multicore digunakan secara efisien
Contoh:
- RDBMS PostgreSQL menciptakan proses baru untuk setiap koneksi baru. Memori bersama digunakan untuk bekerja dengan data umum. PostgreSQL dapat menangani konsumsi sumber daya yang tinggi dari proses dengan berbagai cara. Jika ada beberapa klien (sebuah dudukan khusus untuk analis), maka tidak ada masalah seperti itu. Jika ada satu aplikasi yang mengakses database, Anda bisa membuat kumpulan koneksi database di level aplikasi. Jika ada banyak aplikasi, Anda bisa menggunakan pgbouncer
- sshd mendengarkan permintaan yang masuk pada port 22 dan fork di setiap koneksi. Setiap koneksi ssh adalah fork dari daemon sshd yang menerima dan mengeksekusi perintah pengguna secara berurutan. Berkat arsitektur ini, sumber daya OS itu sendiri digunakan untuk membedakan hak akses
- Contoh dari praktik kita sendiri. Ada aliran file tidak terstruktur dari mana Anda perlu mendapatkan metadata. Proses layanan utama mendistribusikan file di antara proses-proses handler. Setiap proses handler adalah skrip yang mengambil path file sebagai parameter. Pemrosesan file terjadi dalam proses terpisah, oleh karena itu, karena kesalahan pemrosesan, seluruh layanan tidak macet. Untuk memperbarui algoritma pemrosesan, cukup mengubah skrip pemrosesan tanpa menghentikan layanan.
Secara umum, saya harus mengatakan bahwa pendekatan ini memiliki kelebihan, yang menentukan cakupannya, tetapi skalabilitasnya sangat terbatas.
Minta aliran
Pendekatan ini sangat mirip dengan yang sebelumnya. Perbedaannya adalah bahwa utas digunakan sebagai ganti proses. Ini memungkinkan Anda untuk menggunakan memori bersama di luar kotak. Namun, keuntungan lain dari pendekatan sebelumnya tidak dapat digunakan lagi, sementara konsumsi sumber daya juga akan tinggi.
Pro:
- Keluar dari kotak memori bersama
- Kemudahan implementasi
- Penggunaan multi-core CPU secara efisien
Cons:
- Aliran menghabiskan banyak sumber daya. Pada sistem operasi mirip unix, sebuah thread mengkonsumsi sumber daya yang hampir sama banyaknya dengan suatu proses
Contoh penggunaannya adalah MySQL. Tetapi perlu dicatat bahwa MySQL menggunakan pendekatan campuran, jadi contoh ini akan dibahas pada bagian selanjutnya.
Proses / kumpulan benang
Streaming (proses) membuat mahal dan panjang. Agar tidak membuang sumber daya, Anda dapat menggunakan utas yang sama berulang kali. Selain membatasi jumlah maksimum utas, kami memperoleh kumpulan utas (proses). Sekarang utas utama menerima permintaan masuk dan menempatkannya dalam antrian. Alur kerja menerima permintaan dari antrian dan memprosesnya. Pendekatan ini dapat diambil sebagai penskalaan alami pemrosesan sekuensial permintaan: setiap utas pekerja hanya dapat memproses aliran secara berurutan, dengan menggabungkannya memungkinkan Anda memproses permintaan secara paralel. Jika setiap aliran dapat menangani 1000 rps, maka 5 aliran akan menangani beban mendekati 5000 rps (tunduk pada kompetisi minimal untuk sumber daya bersama).
Kolam dapat dibuat terlebih dahulu pada awal layanan atau dibentuk secara bertahap. Menggunakan kumpulan utas lebih umum memungkinkan Anda menerapkan memori bersama.
Ukuran kumpulan thread tidak harus terbatas. Layanan dapat menggunakan utas gratis dari kumpulan, dan jika tidak ada, buat utas baru. Setelah memproses permintaan, utas bergabung dengan kumpulan dan menunggu permintaan berikutnya. Opsi ini merupakan kombinasi dari pendekatan utas berdasarkan permintaan dan kumpulan utas. Contoh akan diberikan di bawah ini.
Pro:
- penggunaan banyak core CPU
- pengurangan biaya untuk membuat utas / proses
Cons:
- Skalabilitas terbatas dalam jumlah klien bersamaan. Menggunakan kumpulan memungkinkan kita untuk menggunakan kembali utas yang sama beberapa kali tanpa biaya sumber daya tambahan, namun, itu tidak memecahkan masalah mendasar dari sejumlah besar sumber daya yang dihabiskan oleh utas / proses. Membuat layanan obrolan yang dapat menahan 100.000 koneksi simultan menggunakan pendekatan ini akan gagal.
- Skalabilitas dibatasi oleh sumber daya bersama, misalnya, jika utas menggunakan memori bersama dengan menyesuaikan akses ke sana menggunakan semaphores / mutex. Ini adalah batasan dari semua pendekatan yang menggunakan sumber daya bersama.
Contoh:
- Aplikasi python berjalan dengan uWSGI dan nginx. Proses uWSGI utama menerima permintaan masuk dari nginx dan mendistribusikannya di antara proses Python dari interpreter yang memproses permintaan. Aplikasi ini dapat ditulis pada kerangka kerja yang kompatibel dengan uWSGI - Django, Flask, dll.
- MySQL menggunakan kumpulan utas: setiap koneksi baru diproses oleh salah satu utas gratis dari kumpulan. Jika tidak ada utas gratis, maka MySQL membuat utas baru. Ukuran kumpulan thread gratis dan jumlah maksimum thread (koneksi) dibatasi oleh pengaturan.
Mungkin ini adalah salah satu pendekatan paling umum untuk membangun layanan jaringan, jika bukan yang paling umum. Ini memungkinkan Anda untuk skala dengan baik, mencapai rps besar. Keterbatasan utama dari pendekatan ini adalah jumlah koneksi jaringan yang diproses secara bersamaan. Bahkan, pendekatan ini hanya berfungsi baik jika permintaannya pendek atau sedikit pelanggan.
Pemrosesan berorientasi peristiwa (pola reaktor)
Dua paradigma - sinkron dan asinkron - adalah pesaing abadi satu sama lain. Sejauh ini, hanya pendekatan sinkron yang telah dibahas, tetapi akan salah jika mengabaikan pendekatan asinkron. Pemrosesan permintaan yang berorientasi peristiwa atau reaktif adalah pendekatan di mana setiap operasi IO dilakukan secara serempak, dan pada akhir operasi, penangan dipanggil. Sebagai aturan, pemrosesan setiap permintaan terdiri dari banyak panggilan tidak sinkron diikuti oleh eksekusi penangan. Pada saat tertentu, aplikasi berulir tunggal mengeksekusi kode hanya satu penangan, tetapi eksekusi penangan berbagai permintaan bergantian satu sama lain, yang memungkinkan Anda untuk secara bersamaan (pseudo-parallel) memproses banyak permintaan paralel.
Diskusi lengkap tentang pendekatan ini berada di luar cakupan artikel ini. Untuk melihat lebih dalam, Anda dapat merekomendasikan Reactor (Reactor) , Apa rahasia kecepatan NodeJS? , Di dalam NGINX . Di sini kami membatasi diri untuk mempertimbangkan pro dan kontra dari pendekatan ini.
Pro:
- Penskalaan efektif oleh rps dan jumlah koneksi simultan. Layanan reaktif secara bersamaan dapat memproses sejumlah besar koneksi (puluhan ribu) jika sebagian besar koneksi menunggu I / O untuk menyelesaikan
Cons:
- Kompleksitas pengembangan. Pemrograman dalam gaya asinkron lebih sulit daripada sinkron. Logika pemrosesan permintaan lebih kompleks, debugging juga lebih sulit daripada dalam kode sinkron.
- Kesalahan yang menyebabkan pemblokiran seluruh layanan. Jika bahasa atau runtime awalnya tidak dirancang untuk pemrosesan asinkron, maka operasi sinkron tunggal dapat memblokir seluruh layanan, meniadakan kemungkinan penskalaan.
- Sulit untuk skala di seluruh core CPU. Pendekatan ini mengasumsikan utas tunggal dalam satu proses tunggal, sehingga Anda tidak dapat menggunakan beberapa inti CPU secara bersamaan. Perlu dicatat bahwa ada cara untuk mengatasi batasan ini.
- Akibat dari paragraf sebelumnya: pendekatan ini tidak menskala dengan baik untuk permintaan yang membutuhkan CPU. Jumlah rps untuk pendekatan ini berbanding terbalik dengan jumlah operasi CPU yang diperlukan untuk memproses setiap permintaan. Menuntut permintaan CPU meniadakan manfaat dari pendekatan ini.
Contoh:
- Node.js menggunakan pola reaktor out-of-box. Untuk detail lebih lanjut, lihat Apa rahasia kecepatan NodeJS?
- nginx: proses pekerja nginx menggunakan pola reaktor untuk memproses permintaan secara paralel. Lihat Di Dalam NGINX untuk lebih jelasnya.
- Program C / C ++ yang langsung menggunakan alat OS (epoll di linux, IOCP di windows, kqueue di FreeBSD), atau menggunakan kerangka kerja (libev, libevent, libuv, dll.).
Setengah sinkronisasi / setengah asinkron
Nama ini diambil dari POSA: Pola untuk Objek Bersamaan dan Jaringan . Dalam aslinya, pola ini ditafsirkan secara luas, tetapi untuk tujuan artikel ini saya akan memahami pola ini agak lebih sempit. Half sync / half async adalah pendekatan pemrosesan permintaan yang menggunakan aliran kontrol ringan (benang hijau) untuk setiap permintaan. Suatu program terdiri dari satu atau lebih utas pada tingkat sistem operasi, namun, sistem pelaksanaan program mendukung utas hijau yang tidak dilihat dan tidak dapat dikendalikan oleh OS.
Beberapa contoh untuk membuat pertimbangan lebih spesifik:
- Layanan dalam bahasa Go. Bahasa Go mendukung banyak utas eksekusi ringan - goroutine. Program ini menggunakan satu atau lebih thread OS, tetapi programmer beroperasi dengan goroutine, yang didistribusikan secara transparan antara thread OS untuk menggunakan CPU multi-core
- Layanan python dengan perpustakaan gevent. Pustaka gevent memungkinkan pemrogram untuk menggunakan utas hijau di tingkat pustaka. Seluruh program dieksekusi dalam utas OS tunggal.
Intinya, pendekatan ini dirancang untuk menggabungkan kinerja tinggi dari pendekatan asinkron dengan kesederhanaan pemrograman kode sinkron.
Dengan menggunakan pendekatan ini, terlepas dari ilusi sinkronisasi, program akan bekerja secara tidak sinkron: sistem eksekusi program akan mengontrol loop peristiwa, dan setiap operasi "sinkron" sebenarnya akan tidak sinkron. Ketika operasi seperti itu dipanggil, sistem eksekusi akan memanggil operasi asinkron menggunakan alat OS dan mendaftarkan pawang penyelesaian operasi. Ketika operasi asinkron selesai, sistem eksekusi akan memanggil penangan yang terdaftar sebelumnya, yang akan terus menjalankan program pada titik doa operasi "sinkron".
Akibatnya, pendekatan half sync / half async mengandung beberapa kelebihan dan beberapa kelemahan dari pendekatan asinkron. Volume artikel tidak memungkinkan kita untuk mempertimbangkan pendekatan ini secara rinci. Bagi mereka yang tertarik, saya menyarankan Anda untuk membaca bab dengan nama yang sama di buku POSA: Pola untuk Objek Bersamaan dan Jaringan .
Pendekatan setengah sinkronisasi / setengah asinkron itu sendiri memperkenalkan entitas "aliran hijau" baru - aliran kontrol ringan di tingkat program atau sistem eksekusi perpustakaan. Apa yang harus dilakukan dengan utas hijau adalah pilihan programmer. Itu bisa menggunakan kumpulan benang hijau, itu bisa membuat utas hijau baru untuk setiap permintaan baru. Perbedaannya dibandingkan dengan thread / proses OS adalah bahwa thread hijau jauh lebih murah: mereka mengkonsumsi RAM jauh lebih sedikit dan dibuat lebih cepat. Ini memungkinkan Anda membuat sejumlah besar utas hijau, misalnya, ratusan ribu dalam bahasa Go. Jumlah yang sangat besar membenarkan penggunaan pendekatan green flow-on-request.
Pro:
- Ini berskala baik dalam rps dan jumlah koneksi simultan
- Kode lebih mudah untuk ditulis dan di-debug dibandingkan dengan pendekatan asinkron
Cons:
- Karena pelaksanaan operasi sebenarnya tidak sinkron, kesalahan pemrograman dimungkinkan ketika satu operasi sinkron memblokir seluruh proses. Ini terutama dirasakan dalam bahasa-bahasa di mana pendekatan ini diimplementasikan melalui perpustakaan, misalnya Python.
- Keburaman program. Saat menggunakan utas atau proses OS, algoritma eksekusi program jelas: setiap utas / proses melakukan operasi dalam urutan yang ditulis dalam kode. Menggunakan pendekatan half sync / half async, operasi yang ditulis secara berurutan dalam kode dapat bergantian secara tak terduga dengan operasi yang memproses permintaan secara bersamaan.
- Tidak cocok untuk sistem waktu nyata. Pemrosesan permintaan yang tidak sinkron sangat menyulitkan penyediaan jaminan untuk waktu pemrosesan dari setiap permintaan individu. Ini adalah konsekuensi dari paragraf sebelumnya.
Tergantung pada implementasinya, pendekatan ini berskala baik di seluruh inti CPU (Golang) atau tidak skala sama sekali (Python).
Pendekatan ini, serta asinkron, memungkinkan Anda untuk menangani sejumlah besar koneksi simultan. Tetapi pemrograman layanan menggunakan pendekatan ini lebih mudah, karena kode ditulis dalam gaya sinkron.
Pemrosesan conveyor
Sesuai namanya, dalam pendekatan ini, permintaan diproses melalui pipa. Proses pemrosesan terdiri dari beberapa utas OS yang diatur dalam sebuah rantai. Setiap utas adalah tautan dalam rantai, ia melakukan bagian tertentu dari operasi yang diperlukan untuk memproses permintaan. Setiap permintaan secara berurutan melewati semua tautan dalam rantai, dan tautan yang berbeda pada setiap saat memproses permintaan yang berbeda.
Pro:
- Pendekatan ini berskala baik dalam rps. Semakin banyak tautan dalam rantai, semakin banyak permintaan diproses per detik.
- Menggunakan banyak utas memungkinkan Anda untuk menskalakan inti CPU dengan baik.
Cons:
- Tidak semua kategori permintaan cocok untuk pendekatan ini. Misalnya, mengatur pemungutan suara lama menggunakan pendekatan ini akan sulit dan tidak nyaman.
- Kompleksitas implementasi dan debugging. Mengalahkan pemrosesan sekuensial sehingga produktivitas tinggi bisa sulit. Debugging suatu program di mana setiap permintaan diproses secara berurutan dalam beberapa utas paralel lebih sulit daripada pemrosesan sekuensial.
Contoh:
- Contoh yang menarik dari pemrosesan conveyor dijelaskan dalam laporan highload 2018 Evolusi arsitektur sistem perdagangan dan kliring Bursa Moskow
Pipelining digunakan secara luas, tetapi paling sering tautannya adalah komponen individual dalam proses independen yang bertukar pesan, misalnya melalui antrian pesan atau basis data.
Ringkasan
Ringkasan singkat dari pendekatan yang dipertimbangkan:
- Pemrosesan sinkron.
Pendekatan sederhana, tetapi sangat terbatas dalam skalabilitas, baik dalam rps dan dalam jumlah koneksi simultan. Itu tidak memungkinkan penggunaan beberapa core CPU secara bersamaan. - Proses baru untuk setiap permintaan.
. , . . ( , ). - .
, , . , . - /.
/. . rps . . . - - (reactor ).
rps . - , . CPU - Half sync/half async.
rps . CPU (Golang) (Python). , () . reactor , , reactor . - .
, . (, long polling ).
, .
: ? , ?
Referensi
- :
- - :
- :
- Half sync/half async:
- :
- :