Karena pertanyaan ini mendapat banyak penayangan, saya memutuskan untuk memposting "jawaban", tetapi secara teknis ini bukan jawaban, tetapi kesimpulan akhir saya untuk saat ini, jadi saya akan menandainya sebagai jawaban.
Tentang pendekatan:
async/await
fungsi cenderung menghasilkan async Tasks
yang dapat ditunggu ditetapkan ke TaskScheduler
runtime dotnet, sehingga memiliki ribuan koneksi simultan, oleh karena itu ribuan atau operasi baca/tulis akan memulai ribuan Tugas. Sejauh yang saya tahu ini menciptakan ribuan StateMachines yang disimpan dalam ram dan pergantian konteks yang tak terhitung jumlahnya di utas tempat mereka ditugaskan, menghasilkan overhead CPU yang sangat tinggi. Dengan sedikit koneksi/panggilan asinkron, ini menjadi lebih seimbang, tetapi seiring bertambahnya jumlah Tugas yang ditunggu, kecepatan menjadi lambat secara eksponensial.
BeginReceive/EndReceive/BeginSend/EndSend
metode soket secara teknis adalah metode asinkron tanpa Tugas yang dapat ditunggu, tetapi dengan panggilan balik di akhir panggilan, yang sebenarnya lebih mengoptimalkan multithreading, tetapi masih batasan desain dotnet dari metode soket ini menurut saya buruk, tetapi untuk solusi sederhana (atau jumlah koneksi yang terbatas) inilah caranya.
SocketAsyncEventArgs/ReceiveAsync/SendAsync
jenis implementasi soket adalah yang terbaik di Windows karena suatu alasan. Ini menggunakan Windows IOCP di latar belakang untuk mencapai panggilan soket asinkron tercepat dan menggunakan I/O Tumpang Tindih dan mode soket khusus. Solusi ini adalah yang "paling sederhana" dan tercepat di bawah Windows. Tetapi di bawah mono/linux, tidak akan pernah secepat itu, karena mono mengemulasi Windows IOCP dengan menggunakan linux epoll
, yang sebenarnya jauh lebih cepat daripada IOCP, tetapi harus meniru IOCP untuk mencapai kompatibilitas dotnet, ini menyebabkan beberapa overhead.
Tentang ukuran buffer:
Ada banyak cara untuk menangani data pada soket. Membacanya mudah, data datang, Anda tahu panjangnya, Anda cukup menyalin byte dari buffer soket ke aplikasi Anda dan memprosesnya. Mengirim data sedikit berbeda.
- Anda dapat mengirimkan data lengkap Anda ke soket dan itu akan memotongnya menjadi potongan-potongan, menyalin chuck ke buffer soket sampai tidak ada lagi untuk dikirim dan metode pengiriman soket akan kembali ketika semua data dikirim (atau ketika kesalahan terjadi).
- Anda dapat mengambil data Anda, memotongnya menjadi potongan-potongan dan memanggil metode pengiriman soket dengan potongan, dan ketika kembali kemudian mengirim potongan berikutnya sampai tidak ada lagi.
Bagaimanapun Anda harus mempertimbangkan ukuran buffer soket apa yang harus Anda pilih. Jika Anda mengirim data dalam jumlah besar, maka semakin besar buffernya, semakin sedikit potongan yang harus dikirim, oleh karena itu lebih sedikit panggilan di loop Anda (atau di soket internal) yang harus dipanggil, lebih sedikit salinan memori, lebih sedikit overhead. Tapi mengalokasikan buffer soket besar dan buffer data program akan menghasilkan penggunaan memori yang besar, terutama jika Anda memiliki ribuan koneksi, dan mengalokasikan (dan mengosongkan) memori besar beberapa kali selalu mahal.
Di sisi pengiriman, ukuran buffer soket 1-2-4-8kB sangat ideal untuk sebagian besar kasus, tetapi jika Anda bersiap untuk mengirim file besar (lebih dari beberapa MB) secara teratur, maka ukuran buffer 16-32-64kB adalah cara yang tepat. Lebih dari 64 kB biasanya tidak ada gunanya.
Tapi ini hanya menguntungkan jika sisi penerima juga memiliki buffer penerima yang relatif besar.
Biasanya melalui koneksi internet (bukan jaringan lokal) tidak ada gunanya melebihi 32 kB, bahkan 16 kB sudah ideal.
Berada di bawah 4-8 kB dapat mengakibatkan peningkatan jumlah panggilan secara eksponensial dalam loop baca/tulis, menyebabkan beban CPU yang besar dan pemrosesan data yang lambat dalam aplikasi.
Gunakan di bawah 4 kB hanya jika Anda tahu bahwa pesan Anda biasanya lebih kecil dari 4 kB, atau sangat jarang di atas 4 KB.
Kesimpulan saya:
Mengenai eksperimen saya, kelas/metode/solusi soket bawaan di dotnet baik-baik saja, tetapi tidak efisien sama sekali. Program uji C linux sederhana saya menggunakan soket non-pemblokiran dapat mengungguli solusi soket dotnet tercepat dan "berkinerja tinggi" (SocketAsyncEventArgs
).
Ini tidak berarti tidak mungkin memiliki pemrograman soket cepat di dotnet, tetapi di Windows saya harus membuat implementasi Windows IOCP saya sendiri dengan berkomunikasi langsung dengan Kernel Windows melalui InteropServices/Marshaling, memanggil langsung metode Winsock2 , menggunakan banyak kode tidak aman untuk meneruskan struktur konteks koneksi saya sebagai penunjuk antara kelas/panggilan saya, membuat ThreadPool saya sendiri, membuat utas penangan kejadian IO, membuat Penjadwal Tugas saya sendiri untuk membatasi jumlah panggilan async simultan untuk menghindari banyak hal yang sia-sia sakelar konteks.
Ini adalah pekerjaan yang banyak dengan banyak penelitian, eksperimen, dan pengujian. Jika Anda ingin melakukannya sendiri, lakukan hanya jika menurut Anda itu layak dilakukan. Mencampur kode yang tidak aman/tidak dikelola dengan kode yang dikelola memang menyebalkan, tetapi pada akhirnya itu sepadan, karena dengan solusi ini saya dapat mencapai dengan server http saya sendiri sekitar 36000 permintaan http/detik pada lan 1gbit, pada Windows 7, dengan i7 4790.
Ini adalah kinerja yang sangat tinggi yang tidak pernah dapat saya capai dengan soket bawaan dotnet.
Saat menjalankan server dotnet saya di i9 7900X di Windows 10, terhubung ke Intel Atom NAS 4c/8t di Linux, melalui lan 10gbit, saya dapat menggunakan bandwidth lengkap (oleh karena itu menyalin data dengan 1GB/s) tidak masalah jika saya hanya punya 1 atau 10.000 koneksi simultan.
Pustaka soket saya juga mendeteksi jika kode berjalan di linux, dan alih-alih Windows IOCP (jelas) ia menggunakan panggilan kernel linux melalui InteropServices/Marshalling untuk membuat, menggunakan soket, dan menangani kejadian soket langsung dengan linux epoll, berhasil maksimalkan kinerja mesin uji.
Kiat desain:
Ternyata sulit untuk mendesain perpustakaan jaringan dari scatch, terutama yang mungkin sangat universal untuk semua tujuan. Anda harus mendesainnya untuk memiliki banyak pengaturan, atau terutama untuk tugas yang Anda butuhkan. Ini berarti menemukan ukuran buffer soket yang tepat, jumlah utas pemrosesan I/O, jumlah utas Pekerja, jumlah tugas async yang diizinkan, semua ini harus disetel ke mesin tempat aplikasi berjalan dan ke hitungan koneksi, dan tipe data yang ingin Anda transfer melalui jaringan. Inilah sebabnya mengapa soket bawaan tidak berfungsi sebaik itu, karena harus universal, dan tidak memungkinkan Anda menyetel parameter ini.
Dalam kasus saya, menggunakan lebih dari 2 utas khusus untuk pemrosesan peristiwa I/O sebenarnya memperburuk kinerja secara keseluruhan, karena hanya menggunakan 2 Antrean RSS, dan menyebabkan lebih banyak peralihan konteks daripada yang ideal.
Memilih ukuran buffer yang salah akan mengakibatkan penurunan performa.
Selalu tolok ukur implementasi yang berbeda untuk tugas yang disimulasikan. Anda perlu mencari tahu solusi atau setelan mana yang terbaik.
Pengaturan yang berbeda dapat menghasilkan hasil kinerja yang berbeda pada mesin dan/atau sistem operasi yang berbeda!
Inti Mono vs Dotnet:
Karena saya telah memprogram pustaka soket saya dengan cara yang kompatibel dengan FW/Core, saya dapat mengujinya di bawah linux dengan mono, dan dengan kompilasi inti asli. Yang paling menarik, saya tidak dapat mengamati perbedaan kinerja yang luar biasa, keduanya cepat, tetapi tentu saja meninggalkan mono dan mengompilasi dalam inti harus menjadi cara yang tepat.
Tips performa bonus:
Jika kartu jaringan Anda mampu RSS (Receive Side Scaling) maka aktifkan di Windows di pengaturan perangkat jaringan di properti lanjutan, dan atur Antrean RSS dari 1 ke setinggi yang Anda bisa/setinggi yang terbaik untuk kinerja Anda.
Jika didukung oleh kartu jaringan Anda maka biasanya disetel ke 1, ini menugaskan acara jaringan untuk diproses hanya oleh satu inti CPU oleh kernel. Jika Anda dapat meningkatkan jumlah antrean ini ke angka yang lebih tinggi, hal itu akan mendistribusikan kejadian jaringan di antara lebih banyak inti CPU, dan akan menghasilkan kinerja yang jauh lebih baik.
Di linux juga dimungkinkan untuk mengatur ini, tetapi dengan cara yang berbeda, lebih baik mencari informasi driver distro/lan linux Anda.
Saya harap pengalaman saya akan membantu sebagian dari Anda!
Saya memiliki masalah yang sama. Anda harus melihat ke dalam:NetCoreServer
Setiap utas di .NET clr threadpool dapat menangani satu tugas pada satu waktu. Jadi untuk menangani lebih banyak async connects/reads dll., Anda harus mengubah ukuran threadpool dengan menggunakan:
ThreadPool.SetMinThreads(Int32, Int32)
Menggunakan EAP (pola asinkron berbasis peristiwa) adalah cara untuk menggunakan Windows. Saya akan menggunakannya di Linux juga karena masalah yang Anda sebutkan dan menurunkan kinerja.
Yang terbaik adalah port penyelesaian io di Windows, tetapi tidak portabel.
PS:dalam hal membuat serialisasi objek, Anda sangat disarankan untuk menggunakan protobuf-net . Ini membuat serial biner objek hingga 10x kali lebih cepat daripada serializer biner .NET dan juga menghemat sedikit ruang!