Panduan untuk Model Server Jaringan Multi-pemrosesan

Diterbitkan: 2022-03-11

Sebagai seseorang yang telah menulis kode jaringan berkinerja tinggi selama beberapa tahun sekarang (disertasi doktoral saya tentang topik Server Cache untuk Aplikasi Terdistribusi yang Diadaptasi ke Sistem Multicore), saya melihat banyak tutorial tentang subjek yang benar-benar melewatkan atau menghilangkan diskusi apa pun. dasar-dasar model server jaringan. Oleh karena itu, artikel ini dimaksudkan sebagai gambaran umum dan perbandingan model server jaringan yang mudah-mudahan bermanfaat, dengan tujuan untuk mengambil beberapa misteri dari penulisan kode jaringan kinerja tinggi.

Model Server Jaringan Mana yang Harus Saya Pilih

Artikel ini ditujukan untuk "pemrogram sistem", yaitu, pengembang back-end yang akan bekerja dengan detail tingkat rendah dari aplikasi mereka, menerapkan kode server jaringan. Ini biasanya akan dilakukan dalam C++ atau C, meskipun saat ini sebagian besar bahasa dan kerangka kerja modern menawarkan fungsionalitas tingkat rendah yang layak, dengan berbagai tingkat efisiensi.

Saya akan mengambil pengetahuan umum bahwa karena lebih mudah untuk menskalakan CPU dengan menambahkan inti, wajar saja untuk mengadaptasi perangkat lunak untuk menggunakan inti ini sebaik mungkin. Jadi, pertanyaannya menjadi bagaimana mempartisi perangkat lunak di antara utas (atau proses) yang dapat dieksekusi secara paralel pada banyak CPU.

Saya juga akan menerima bahwa pembaca menyadari bahwa "konkurensi" pada dasarnya berarti "multitasking", yaitu beberapa contoh kode (apakah kode yang sama atau berbeda, tidak masalah), yang aktif pada waktu yang sama. Konkurensi dapat dicapai pada satu CPU, dan sebelum era modern, biasanya demikian. Secara khusus, konkurensi dapat dicapai dengan beralih cepat di antara beberapa proses atau utas pada satu CPU. Ini adalah berapa lama, sistem CPU tunggal berhasil menjalankan banyak aplikasi pada saat yang sama, dengan cara yang dianggap pengguna sebagai aplikasi yang dijalankan secara bersamaan, meskipun sebenarnya tidak. Paralelisme, di sisi lain, berarti secara khusus bahwa kode sedang dieksekusi pada saat yang sama, secara harfiah, oleh banyak CPU atau inti CPU.

Mempartisi Aplikasi (menjadi beberapa proses atau utas)

Untuk tujuan diskusi ini, sebagian besar tidak relevan jika kita berbicara tentang utas atau proses lengkap. Sistem operasi modern (dengan pengecualian Windows) memperlakukan proses hampir sama ringannya dengan utas (atau dalam beberapa kasus, sebaliknya, utas telah memperoleh fitur yang membuatnya seberat proses). Saat ini, perbedaan utama antara proses dan utas adalah pada kemampuan komunikasi lintas proses atau lintas utas dan berbagi data. Di mana perbedaan antara proses dan utas penting, saya akan membuat catatan yang sesuai, jika tidak, aman untuk menganggap kata "utas" dan "proses" di bagian ini dapat dipertukarkan.

Tugas Aplikasi Jaringan Umum dan Model Server Jaringan

Artikel ini secara khusus membahas kode server jaringan, yang mengimplementasikan tiga tugas berikut:

  • Tugas #1: Membangun (dan meruntuhkan) koneksi jaringan
  • Tugas #2: Komunikasi jaringan (IO)
  • Tugas #3: Pekerjaan yang bermanfaat; yaitu, muatan atau alasan mengapa aplikasi itu ada

Ada beberapa model server jaringan umum untuk mempartisi tugas-tugas ini di seluruh proses; yaitu:

  • MP: Multi-Proses
  • SPED: Proses Tunggal, Didorong oleh Peristiwa
  • SEDA: Arsitektur Berbasis Acara Bertahap
  • AMPED: Berbasis Peristiwa Multi-Proses Asimetris
  • SYMPED: Didorong oleh Peristiwa Multi-Proses simetris

Ini adalah nama model server jaringan yang digunakan dalam komunitas akademik, dan saya ingat menemukan sinonim "di alam liar" untuk setidaknya beberapa di antaranya. (Nama-nama itu sendiri, tentu saja, kurang penting – nilai sebenarnya adalah bagaimana mempertimbangkan apa yang terjadi dalam kode.)

Masing-masing model server jaringan ini dijelaskan lebih lanjut di bagian berikut.

Model Multi-Proses (MP)

Model server jaringan MP adalah yang pertama kali dipelajari semua orang, terutama ketika belajar tentang multithreading. Dalam model MP, ada proses "master" yang menerima koneksi (Tugas #1). Setelah koneksi dibuat, proses master membuat proses baru dan meneruskan soket koneksi ke sana, jadi ada satu proses per koneksi. Proses baru ini kemudian biasanya bekerja dengan koneksi secara sederhana, berurutan, langkah kunci: ia membaca sesuatu darinya (Tugas #2), kemudian melakukan beberapa perhitungan (Tugas #3), lalu menulis sesuatu padanya (Tugas #2 lagi).

Model MP sangat sederhana untuk diterapkan, dan benar-benar bekerja dengan sangat baik selama jumlah total proses tetap cukup rendah. Seberapa rendah? Jawabannya sangat tergantung pada apa yang dibutuhkan oleh Tugas #2 dan #3. Sebagai aturan praktis, katakanlah jumlah proses atau utas tidak boleh melebihi sekitar dua kali jumlah inti CPU. Begitu ada terlalu banyak proses yang aktif pada saat yang sama, sistem operasi cenderung menghabiskan terlalu banyak waktu untuk meronta-ronta (yaitu, menyulap proses atau utas di sekitar inti CPU yang tersedia) dan aplikasi semacam itu umumnya menghabiskan hampir semua CPU mereka. waktu dalam kode "sys" (atau kernel), melakukan sedikit pekerjaan yang benar-benar berguna.

Kelebihan: Sangat sederhana untuk diterapkan, bekerja dengan sangat baik selama jumlah koneksinya kecil.

Kekurangan: Cenderung membebani sistem operasi jika jumlah proses bertambah terlalu besar, dan mungkin memiliki jitter latensi saat IO jaringan menunggu hingga fase payload (komputasi) selesai.

Model Berbasis Peristiwa (SPED) Proses Tunggal

Model server jaringan SPED dibuat terkenal oleh beberapa aplikasi server jaringan profil tinggi yang relatif baru, seperti Nginx. Pada dasarnya, ia melakukan ketiga tugas dalam proses yang sama, multiplexing di antara mereka. Agar efisien, diperlukan beberapa fungsionalitas kernel yang cukup canggih seperti epoll dan kqueue. Dalam model ini, kode didorong oleh koneksi masuk dan "peristiwa" data, dan mengimplementasikan "perulangan peristiwa" yang terlihat seperti ini:

  • Tanyakan pada sistem operasi apakah ada "peristiwa" jaringan baru (seperti koneksi baru atau data masuk)
  • Jika ada koneksi baru yang tersedia, buatlah (Tugas #1)
  • Jika ada data yang tersedia, bacalah (Tugas #2) dan tindak lanjuti (Tugas #3)
  • Ulangi sampai server keluar

Semua ini dilakukan dalam satu proses, dan dapat dilakukan dengan sangat efisien karena sepenuhnya menghindari pengalihan konteks antarproses, yang biasanya mematikan kinerja dalam model MP. Satu-satunya sakelar konteks di sini berasal dari panggilan sistem, dan itu diminimalkan dengan hanya bertindak pada koneksi tertentu yang memiliki beberapa peristiwa yang menyertainya. Model ini dapat menangani puluhan ribu koneksi secara bersamaan, selama pekerjaan muatan (Tugas #3) tidak terlalu rumit atau intensif sumber daya.

Namun, ada dua kelemahan utama dari pendekatan ini:

  1. Karena ketiga tugas dilakukan secara berurutan dalam satu pengulangan loop, pekerjaan muatan (Tugas #3) dilakukan secara serempak dengan yang lainnya, artinya jika perlu waktu lama untuk menghitung respons terhadap data yang diterima oleh klien, yang lainnya berhenti saat ini sedang dilakukan, menyebabkan fluktuasi latensi yang berpotensi besar.
  2. Hanya satu inti CPU yang digunakan. Ini memiliki keuntungan, sekali lagi, benar-benar membatasi jumlah sakelar konteks yang diperlukan dari sistem operasi, yang meningkatkan kinerja secara keseluruhan, tetapi memiliki kelemahan signifikan bahwa inti CPU lain yang tersedia tidak melakukan apa-apa sama sekali.

Untuk alasan inilah model yang lebih maju diperlukan.

Kelebihan: Dapat berperforma tinggi dan mudah pada sistem operasi (yaitu, memerlukan intervensi OS minimal). Hanya membutuhkan satu inti CPU.

Kekurangan: Hanya menggunakan satu CPU (terlepas dari jumlah yang tersedia). Jika pekerjaan muatan tidak seragam, menghasilkan latensi respons yang tidak seragam.

Model Arsitektur Berbasis Peristiwa (SEDA) Bertahap

Model server jaringan SEDA agak rumit. Ini menguraikan aplikasi yang kompleks dan digerakkan oleh peristiwa menjadi satu set tahapan yang dihubungkan oleh antrian. Namun, jika tidak diterapkan dengan hati-hati, kinerjanya dapat mengalami masalah yang sama dengan kasus MP. Ini bekerja seperti ini:

  • Pekerjaan muatan (Tugas #3) dibagi menjadi sebanyak mungkin tahap, atau modul. Setiap modul mengimplementasikan satu fungsi spesifik (pikirkan "layanan mikro" atau "mikrokernel") yang berada dalam prosesnya sendiri yang terpisah, dan modul-modul ini berkomunikasi satu sama lain melalui antrian pesan. Arsitektur ini dapat direpresentasikan sebagai grafik node, di mana setiap node adalah proses, dan edge adalah antrian pesan.
  • Satu proses melakukan Tugas #1 (biasanya mengikuti model SPED), yang melepaskan koneksi baru ke node titik masuk tertentu. Node tersebut dapat berupa node jaringan murni (Tugas #2) yang meneruskan data ke node lain untuk komputasi, atau dapat juga mengimplementasikan pemrosesan payload (Tugas #3). Biasanya tidak ada proses "master" (misalnya, proses yang mengumpulkan dan menggabungkan respons dan mengirimkannya kembali melalui koneksi) karena setiap node dapat merespons dengan sendirinya.

Secara teori, model ini bisa sangat kompleks, dengan grafik simpul mungkin memiliki loop, koneksi ke aplikasi serupa lainnya, atau di mana simpul benar-benar dieksekusi pada sistem jarak jauh. Namun, dalam praktiknya, bahkan dengan pesan yang terdefinisi dengan baik dan antrian yang efisien, dapat menjadi sulit untuk berpikir, dan bernalar tentang perilaku sistem secara keseluruhan. Overhead yang lewat pesan dapat merusak kinerja model ini, dibandingkan dengan model SPED, jika pekerjaan yang dilakukan pada setiap node pendek. Efisiensi model ini secara signifikan lebih rendah daripada model SPED, sehingga biasanya digunakan dalam situasi di mana pekerjaan muatan rumit dan memakan waktu.

Kelebihan: Impian arsitek perangkat lunak utama: semuanya dipisahkan menjadi modul independen yang rapi.

Cons: Kompleksitas dapat meledak hanya dari jumlah modul, dan antrian pesan masih jauh lebih lambat daripada berbagi memori langsung.

Model Asimetris Multi-Proses Event-Driven (AMPED)

Server jaringan AMPED adalah versi SEDA yang lebih jinak dan mudah dimodelkan. Tidak banyak modul dan proses yang berbeda, dan tidak banyak antrian pesan. Berikut cara kerjanya:

  • Terapkan Tugas #1 dan #2 dalam satu proses "master", dalam gaya SPED. Ini adalah satu-satunya proses yang melakukan IO jaringan.
  • Terapkan Tugas #3 dalam proses "pekerja" terpisah (mungkin dimulai dalam beberapa contoh), terhubung ke proses master dengan antrian (satu antrian per proses).
  • Ketika data diterima dalam proses "master", temukan proses pekerja yang kurang dimanfaatkan (atau menganggur) dan teruskan data ke antrian pesannya. Proses master dikirimi pesan oleh proses ketika respons sudah siap, pada titik mana respons itu diteruskan ke koneksi.

Yang penting di sini adalah bahwa pekerjaan muatan dilakukan dalam jumlah proses yang tetap (biasanya dapat dikonfigurasi), yang tidak bergantung pada jumlah koneksi. Manfaatnya di sini adalah bahwa muatannya bisa menjadi rumit secara sewenang-wenang, dan itu tidak akan memengaruhi IO jaringan (yang bagus untuk latensi). Ada juga kemungkinan untuk meningkatkan keamanan, karena hanya satu proses yang melakukan IO jaringan.

Kelebihan: Pemisahan jaringan IO dan pekerjaan muatan yang sangat bersih.

Cons: Memanfaatkan antrian pesan untuk meneruskan data bolak-balik antara proses, yang, tergantung pada sifat protokol, dapat menjadi hambatan.

Model SYMPED (Symmetric Multi-Process Event-Driven)

Model server jaringan SYMPED dalam banyak hal adalah "cawan suci" dari model server jaringan, karena itu seperti memiliki beberapa contoh proses "pekerja" SPED independen. Ini diimplementasikan dengan memiliki satu proses yang menerima koneksi dalam satu lingkaran, kemudian meneruskannya ke proses pekerja, yang masing-masing memiliki loop peristiwa seperti SPED. Ini memiliki beberapa konsekuensi yang sangat menguntungkan:

  • CPU dimuat sesuai dengan jumlah proses yang dihasilkan, yang pada setiap titik waktu melakukan IO jaringan atau pemrosesan muatan. Tidak ada cara untuk meningkatkan penggunaan CPU lebih lanjut.
  • Jika koneksi independen (seperti dengan HTTP), tidak ada komunikasi antarproses antara proses pekerja.

Sebenarnya, inilah yang dilakukan oleh versi Nginx yang lebih baru; mereka menelurkan sejumlah kecil proses pekerja, yang masing-masing menjalankan loop peristiwa. Untuk membuat segalanya lebih baik, sebagian besar sistem operasi menyediakan fungsi di mana beberapa proses dapat mendengarkan koneksi masuk pada port TCP secara independen, menghilangkan kebutuhan untuk proses khusus yang didedikasikan untuk bekerja dengan koneksi jaringan. Jika aplikasi yang sedang Anda kerjakan dapat diimplementasikan dengan cara ini, saya sarankan untuk melakukannya.

Kelebihan: Batas atas penggunaan CPU yang ketat, dengan jumlah loop seperti SPED yang dapat dikontrol.

Cons: Karena setiap proses memiliki loop seperti SPED, jika pekerjaan payload tidak seragam, latensi dapat kembali bervariasi, sama seperti model SPED normal.

Beberapa Trik Tingkat Rendah

Selain memilih model arsitektur terbaik untuk aplikasi Anda, ada beberapa trik tingkat rendah yang dapat digunakan untuk lebih meningkatkan kinerja kode jaringan. Berikut adalah daftar singkat dari beberapa yang lebih efektif:

  1. Hindari alokasi memori dinamis. Sebagai penjelasan, lihat saja kode untuk pengalokasi memori yang populer – mereka menggunakan struktur data yang kompleks, mutex, dan ada begitu banyak kode di dalamnya (jemalloc, misalnya, sekitar 450 KiB kode C!). Sebagian besar model di atas dapat diimplementasikan dengan jaringan dan/atau buffer yang sepenuhnya statis (atau telah dialokasikan sebelumnya) yang hanya mengubah kepemilikan antar utas jika diperlukan.
  2. Gunakan maksimum yang dapat disediakan oleh OS. Sebagian besar sistem operasi mengizinkan beberapa proses untuk mendengarkan pada satu soket, dan menerapkan fitur di mana koneksi tidak akan diterima sampai byte pertama (atau bahkan permintaan penuh pertama!) diterima pada soket. Gunakan sendfile() jika Anda bisa.
  3. Pahami protokol jaringan yang Anda gunakan! Misalnya, biasanya masuk akal untuk menonaktifkan algoritme Nagle, dan masuk akal untuk menonaktifkan berlama-lama jika tingkat koneksi (ulang) tinggi. Pelajari tentang algoritme kontrol kemacetan TCP dan lihat apakah masuk akal untuk mencoba salah satu yang lebih baru.

Saya dapat berbicara lebih banyak tentang ini, serta teknik dan trik tambahan untuk digunakan, di posting blog mendatang. Namun untuk saat ini, mudah-mudahan ini memberikan landasan yang berguna dan informatif mengenai pilihan arsitektur untuk menulis kode jaringan kinerja tinggi, dan keuntungan dan kerugian relatifnya.