Protože tato otázka má mnoho zhlédnutí, rozhodl jsem se zveřejnit „odpověď“, ale technicky to není odpověď, ale prozatím můj konečný závěr, takže ji označím jako odpověď.
O přístupech:
async/await
funkce mají tendenci vytvářet očekávané asynchronní Tasks
přiřazeno k TaskScheduler
běhového prostředí dotnet, takže tisíce současných připojení, tedy tisíce operací čtení/zápisu spustí tisíce úloh. Pokud vím, toto vytváří tisíce StateMachines uložených v paměti RAM a nespočet přepínání kontextu ve vláknech, ke kterým jsou přiřazeny, což má za následek velmi vysokou režii CPU. S několika připojeními/asynchronními voláními je to lépe vyvážené, ale jak roste počet očekávaných úkolů, zpomaluje se exponenciálně.
BeginReceive/EndReceive/BeginSend/EndSend
metody soketu jsou technicky asynchronní metody bez očekávaných úkolů, ale se zpětnými voláními na konci volání, což ve skutečnosti více optimalizuje multithreading, ale stále omezení dotnetového designu těchto metod soketu jsou podle mého názoru špatné, ale pro jednoduchá řešení (nebo omezený počet připojení) je to správná cesta.
SocketAsyncEventArgs/ReceiveAsync/SendAsync
typ implementace socketu je z nějakého důvodu nejlepší na Windows. Využívá Windows IOCP na pozadí, abyste dosáhli nejrychlejšího asynchronního volání soketu a použili překrývající se I/O a speciální režim soketu. Toto řešení je „nejjednodušší“ a nejrychlejší pod Windows. Ale pod mono/linuxem to nikdy nebude tak rychlé, protože mono emuluje Windows IOCP pomocí linux epoll
, který je ve skutečnosti mnohem rychlejší než IOCP, ale k dosažení kompatibility dotnet musí emulovat IOCP, což způsobuje určitou režii.
O velikostech vyrovnávací paměti:
Existuje nespočet způsobů, jak zacházet s daty na soketech. Čtení je jednoduché, data přicházejí, znáte jejich délku, pouze zkopírujete bajty z vyrovnávací paměti soketu do vaší aplikace a zpracujete je. Odesílání dat je trochu jiné.
- Můžete předat svá kompletní data do soketu a ten je rozřeže na kousky, zkopíruje chucky do vyrovnávací paměti soketu, dokud již nebudou k odeslání, a způsob odesílání soketu se vrátí, až budou všechna data odeslána (nebo když dojde k chybě).
- Můžete vzít svá data, rozřezat je na kusy a zavolat metodu odeslání soketu s blokem, a když se vrátí, odeslat další blok, dokud nebudou žádné další.
V každém případě byste měli zvážit, jakou velikost vyrovnávací paměti soketu byste měli zvolit. Pokud posíláte velké množství dat, pak čím větší je vyrovnávací paměť, tím méně kusů musí být odesláno, proto je třeba volat méně volání ve vaší (nebo v interní smyčce soketu), méně kopií paměti, méně režie. Přidělení velkých vyrovnávacích pamětí soketů a vyrovnávacích pamětí programových dat bude mít za následek velké využití paměti, zvláště pokud máte tisíce připojení a vícenásobné přidělení (a uvolnění) velké paměti je vždy drahé.
Na straně odesílání je pro většinu případů ideální velikost vyrovnávací paměti soketu 1-2-4-8kB, ale pokud se chystáte pravidelně posílat velké soubory (přes několik MB), pak je velikost vyrovnávací paměti 16-32-64kB správnou cestou. Nad 64 kB obvykle nemá smysl jít.
To má ale výhodu pouze v případě, že strana přijímače má také relativně velké přijímací vyrovnávací paměti.
Obvykle přes připojení k internetu (nikoli místní síť) nemá smysl se dostat přes 32 kB, ideální je i 16 kB.
Klesání pod 4–8 kB může mít za následek exponenciálně zvýšený počet hovorů ve smyčce čtení/zápisu, což způsobuje velké zatížení procesoru a pomalé zpracování dat v aplikaci.
Jděte pod 4 kB, pouze pokud víte, že vaše zprávy budou obvykle menší než 4 kB nebo jen velmi zřídka přes 4 kB.
Můj závěr:
Pokud jde o mé experimenty, vestavěná třída / metody / řešení v dotnetu jsou v pořádku, ale vůbec ne efektivní. Moje jednoduché testovací programy pro linux C používající neblokující sokety by mohly překonat nejrychlejší a „vysoce výkonné“ řešení dotnetových soketů (SocketAsyncEventArgs
).
To neznamená, že je nemožné mít rychlé programování soketů v dotnetu, ale pod Windows jsem si musel vytvořit vlastní implementaci Windows IOCP přímou komunikací s jádrem Windows prostřednictvím InteropServices/Marshaling, přímým voláním metod Winsock2 , používáním mnoha nebezpečných kódů k předávání kontextových struktur mých připojení jako ukazatelů mezi mými třídami/voláním, vytváření vlastního fondu vláken, vytváření vláken obsluhy událostí IO, vytváření vlastního TaskScheduleru k omezení počtu souběžných asynchronních volání, abych se vyhnul zbytečně velkému množství kontextové přepínače.
Bylo to hodně práce se spoustou výzkumu, experimentů a testování. Pokud to chcete udělat sami, udělejte to, pouze pokud si myslíte, že to opravdu stojí za to. Míchání nebezpečného/unmanage kódu se spravovaným kódem je oříšek, ale nakonec to stojí za to, protože s tímto řešením jsem mohl dosáhnout s vlastním http serverem asi 36 000 http request/sec na 1gbit LAN, na Windows 7, s i7 4790.
To je tak vysoký výkon, kterého bych nikdy nemohl dosáhnout s vestavěnými sockety dotnet.
Když běžím na svém dotnet serveru na i9 7900X na Windows 10, připojeném k 4c/8t Intel Atom NAS na Linuxu, přes 10gbit LAN, můžu využít celou šířku pásma (proto kopírování dat s 1GB/s) bez ohledu na to, jestli mám 1 nebo 10 000 současných připojení.
Moje knihovna soketů také zjišťuje, zda kód běží na linuxu, a pak místo Windows IOCP (samozřejmě) používá volání linuxového jádra prostřednictvím InteropServices/Marshalling k vytvoření, použití soketů a zpracování událostí soketu přímo pomocí linux epoll. maximální výkon testovacích strojů.
Tip k designu:
Jak se ukázalo, je obtížné navrhnout síťovou knihovnu ze scatch, zejména takovou, která je pravděpodobně velmi univerzální pro všechny účely. Musíte jej navrhnout tak, aby měl mnoho nastavení, nebo zejména pro úlohu, kterou potřebujete. To znamená najít správnou velikost vyrovnávací paměti soketu, počet vláken zpracování I/O, počet pracovních vláken, povolený počet asynchronních úloh, to vše musí být naladěn na stroj, na kterém běží aplikace a na počet připojení a typ dat, která chcete přenášet po síti. To je důvod, proč vestavěné zásuvky nefungují tak dobře, protože musí být univerzální a neumožňují vám nastavit tyto parametry.
V mém případě přiřazení více než 2 vyhrazených vláken ke zpracování I/O událostí ve skutečnosti zhoršuje celkový výkon, protože použití pouze 2 front RSS a způsobuje více přepínání kontextu, než je ideální.
Výběr nesprávné velikosti vyrovnávací paměti bude mít za následek ztrátu výkonu.
Vždy porovnávejte různé implementace pro simulovaný úkol. Musíte zjistit, které řešení nebo nastavení je nejlepší.
Různá nastavení mohou mít na různých počítačích a/nebo operačních systémech různé výsledky!
Mono vs. Dotnet Core:
Protože jsem naprogramoval svou socketovou knihovnu způsobem kompatibilním s FW/Core, mohl jsem je otestovat pod linuxem s mono a nativní kompilací jádra. Nejzajímavější je, že jsem nemohl pozorovat žádné výrazné rozdíly ve výkonu, oba byly rychlé, ale samozřejmě by mělo jít o ponechání mono a kompilaci v jádru.
Bonusový tip na výkon:
Pokud vaše síťová karta podporuje RSS (Receive Side Scaling), povolte ji ve Windows v nastavení síťového zařízení v pokročilých vlastnostech a nastavte frontu RSS od 1 do tak vysoké, jakou můžete/tak vysoká, jaká je pro váš výkon nejlepší.
Pokud je podporována vaší síťovou kartou, pak je obvykle nastavena na 1, tím je síťová událost přiřazena ke zpracování pouze jedním jádrem CPU jádrem. Pokud můžete zvýšit tento počet front na vyšší čísla, pak to rozdělí síťové události mezi více jader CPU a povede to k mnohem lepšímu výkonu.
V linuxu je to také možné nastavit, ale různými způsoby, lepší je vyhledat informace o ovladači vašeho linux distro/lan.
Doufám, že moje zkušenost některým z vás pomůže!
Měl jsem stejný problém. Měli byste se podívat na:NetCoreServer
Každé vlákno ve fondu vláken .NET clr může zpracovat jednu úlohu najednou. Chcete-li tedy zvládnout více asynchronních připojení/čtení atd., musíte změnit velikost fondu vláken pomocí:
ThreadPool.SetMinThreads(Int32, Int32)
Použití EAP (asynchronní vzor založený na událostech) je způsob, jak jít v systému Windows. Použil bych to také na Linuxu kvůli problémům, které jste zmínil, a snížil výkon.
Nejlepší by byly io dokončení portů v systému Windows, ale nejsou přenosné.
PS:pokud jde o serializaci objektů, důrazně se doporučuje používat protobuf-net . Binární serializuje objekty až 10x rychleji než binární serializátor .NET a také šetří trochu místa!