聊聊如何设计千万级吞吐量的.Net Core网络通信!
作者:大石頭
時間:2018-10-26 晚上 20:00
地點:QQ群-1600800
內(nèi)容:網(wǎng)絡(luò)通信,
網(wǎng)絡(luò)庫使用方式
網(wǎng)絡(luò)庫設(shè)計理念,高性能要點
介紹
首先看下面這張很具有代表性的圖,2018年5月份做的測試。當時單服務(wù)器得到?2256tps(Transactions Per Second,每秒事務(wù)數(shù)) 的吞吐率。這次測試只是說明一個問題,.Net可以做超高吞吐率的應(yīng)用。
當時測試相關(guān)記錄和代碼地址
記錄:https://www.cnblogs.com/nnhy/p/newlife_net_benchmark.html
代碼國外地址:https://github.com/nnhy/NewLife.Net.Tests
代碼國內(nèi)地址:http://git.newlifex.com/Stone/NewLife.Net.Tests
1.1 開始網(wǎng)絡(luò)編程
簡單的網(wǎng)絡(luò)程序示例
相關(guān)使用介紹:https://www.cnblogs.com/nnhy/p/newlife_net_echo.html
克隆上面的代碼,運行EchoTest項目,打開編譯的exe,打開兩次,一個選1作為服務(wù)器,一個選2作為客戶端
在客戶端連接服務(wù)器和給服務(wù)端發(fā)送數(shù)據(jù)的時候,分別觸發(fā)Start和OnReceive方法,連接之后服務(wù)端發(fā)送了Welcome 的消息,客戶端發(fā)送5次“你好”。服務(wù)端回傳收到的數(shù)據(jù),打了一個日志,把收到的信息轉(zhuǎn)成字符串輸出到控制臺。
NetServer是應(yīng)用級網(wǎng)絡(luò)服務(wù)器,支持tcp/udp/ipv4/ipv6。上面可以看到,同時監(jiān)聽了四個端口。
碼神工具也可以連接上來
解釋
對于網(wǎng)絡(luò)會話來說,最關(guān)鍵的就是客戶端連上來,以及收到數(shù)據(jù)包,這兩部分,對應(yīng)上面Start和OnReceive兩個方法
服務(wù)端
上面是最小的網(wǎng)絡(luò)庫例程,簡單演示了服務(wù)端和客戶端,連接和收發(fā)信息。網(wǎng)絡(luò)應(yīng)用分為NetServer/NetSession,服務(wù)端、會話,N個客戶端連接服務(wù)器,就會有N個會話。來一個客戶端連接,服務(wù)端就new一個新的NetSession,并執(zhí)行Start,收到一個數(shù)據(jù)包,就執(zhí)行OnReceive,連接斷開,就執(zhí)行OnDispose,這便是服務(wù)端的全部。
客戶端連接剛上來的時候,沒有數(shù)據(jù)包等其它信息,所以這個時候沒有參數(shù)。客戶端發(fā)數(shù)據(jù)包過來,OnReceive函數(shù)在處理。
服務(wù)端的創(chuàng)建,可以是很簡單,看以下截圖。這里為了測試方便,開了很多Log,實際使用的時候,根據(jù)需要注釋。
長連接、心跳第二節(jié)設(shè)計理念再講。
客戶端
跟很多網(wǎng)絡(luò)庫不同,NewLife.Net除了服務(wù)端,還封裝了客戶端??蛻舳说暮诵?#xff0c;也就是Send函數(shù)和Received事件,同步發(fā)送,異步接收。
因為是長連接,所以服務(wù)端隨時可以向客戶端發(fā)送數(shù)據(jù)包,客戶端也可以收到。tcp在不做設(shè)置的時候,默認長連接2小時。
NetServer默認20分鐘,在沒有心跳的時候,20分鐘沒有數(shù)據(jù)包往來,服務(wù)端會干掉這個會話。
雖然上面講的NetServer和Client,都是tcp,但是換成其它協(xié)議也是可以的。這里的NetServer和NetUri.CreateRemote,同時支持Tcp/Udp/IPv4/IPv6等,CreateRemote內(nèi)部,就是根據(jù)地址的不同,去new不同的客戶端。所以我們寫的代碼,根本不在意用的是tcp還是udp,或者IPv6。有興趣的可以看看源碼
1.2 構(gòu)建可靠網(wǎng)絡(luò)服務(wù)
相關(guān)博客
要真正形成一個網(wǎng)絡(luò)服務(wù),那得穩(wěn)定可靠。上面例程EchoTest只是簡單演示,接下來看下一個例程EchoAgent。
安裝運行
這是一個標準的Windows服務(wù),有了這個東西,我們就可以妥妥的注冊到Windows里面去。這也是目前我們大量數(shù)據(jù)分析程序的必備。
首先運行EchoAgent,按2,安裝注冊服務(wù),用管理員身份運行。安裝成功然后可以在服務(wù)里面找到剛剛安裝的服務(wù)。
安裝完成可以在服務(wù)上找到,再次按2就是卸載,這個是XAgent提供的功能
這時候按3,啟動服務(wù)
代碼解釋
接下來看代碼,服務(wù)啟動的時候,執(zhí)行StartWork。在這個時候?qū)嵗覰etServer,得到的效果就跟例程EchoTest一樣,區(qū)別是一個是控制臺一個是服務(wù)。停止服務(wù)時執(zhí)行StopWork,我們可以在這里關(guān)閉NetServer。詳細請看源碼
必須有這個東西,你的網(wǎng)絡(luò)服務(wù)程序,才有可能達到產(chǎn)品級。linux上直接控制臺,上nohup,當然還有很多其它辦法。以后希望這個XAgent能夠支持linux吧,這樣就一勞永逸了
1.3 壓測
相關(guān)博客
只需要記住一個兩個數(shù)字,.net應(yīng)用打出來2266萬tps,流量峰值4.5Gbps
兩千萬吞吐量的數(shù)字,當然,只能看不能用。因為服務(wù)端只是剛才的Echo而已,并沒有帶什么業(yè)務(wù)。實際工作中,帶著業(yè)務(wù)和數(shù)據(jù)庫,能跑到10萬已經(jīng)非常非常牛逼了。
我們工作中的服務(wù)可以跑到100萬,但是我不敢,怕它不小心就崩了。所以我們都是按照10萬的上限來設(shè)計,不夠就堆服務(wù)器好了,達到5萬以上后,穩(wěn)定性更重要
網(wǎng)絡(luò)編程的坑
主要有粘包
程序員中會網(wǎng)絡(luò)編程的少,會解決粘包的更少!
1.4 網(wǎng)絡(luò)編程的坎——粘包
普遍情況,上萬的程序員,會寫網(wǎng)絡(luò)程序的不到20%,會解決粘包問題的不到1%,如果大家會寫網(wǎng)絡(luò)程序,并且能解決粘包,那么至少已經(jīng)達到了網(wǎng)絡(luò)編程的中級水平。
什么是粘包
舉個栗子:客戶端連續(xù)發(fā)了5個包,服務(wù)端就收到了一個大包。代碼就不演示了,把第一個例程的這個睡眠去掉。
客戶端連續(xù)發(fā)了5個包,服務(wù)端就收到了一個大包。
原因
很多人可能都聽說Tcp是流式協(xié)議,但是很少人去問,什么叫流式吧?流式,就是它把數(shù)據(jù)像管道一樣傳輸過去。
剛才我們發(fā)了5個 “你好”,它負責把這10個字發(fā)到對方,至于發(fā)多少次,每次發(fā)幾個字,不用我們操心,tcp底層自己處理。tcp負責把數(shù)據(jù)一個不丟的按順序的發(fā)過去。所以,為了性能,它一般會把相近的數(shù)據(jù)包湊到一起發(fā)過去。對方收到一個大包,5個小包都粘在了一起,這就是最簡單的粘包。
這個特性由NoDelay設(shè)置決定。NoDelay默認是false,需要自己設(shè)置。如果設(shè)置了,就不會等待。但是不要想得那么美好,因為對方可能合包。
局域網(wǎng)MTU(Maximum Transmission Unit,最大傳輸單元)是1500,處于ip tcp 頭部等,大概1472多點的樣子。
更復雜的粘包及解決方法
A 1000 字節(jié) B 也是 1000字節(jié),對方可能收到兩個包,1400 + 600。對方可能收到兩個包,1400 + 600。
凡是以特殊符號開頭或結(jié)尾來處理粘包的辦法,都會有這樣那樣的缺陷,最終是給自己挖坑。所以,tcp粘包,絕大部分解決方案,偏向于指定數(shù)據(jù)包長度。這其中大部分使用4字節(jié)長度,長度+數(shù)據(jù)。對方收到的時候,根據(jù)長度判斷后面數(shù)據(jù)足夠了沒有。
這是粘包的處理代碼:http://git.newlifex.com/NewLife/X/Blob/master/NewLife.Core/Net/Handlers/MessageCodec.cs
每次判斷長度,接收一個或多個包,如果接收不完,留下,存起來。等下一個包到來的時候,拼湊完整。
雖然tcp確保數(shù)據(jù)不丟,但是難免我們自己失手,弄丟了一點點數(shù)據(jù)。為了避免禍害后面所有包,就需要進行特殊處理了。
每個數(shù)據(jù)幀,自己把頭部長度和數(shù)據(jù)體湊一起發(fā)送啊,tcp確保順序。這里我們把超時時間設(shè)置為3~5秒,每次湊包,如果發(fā)現(xiàn)上次有殘留,并且超時了,那么就扔了它,省得禍害后面。
根據(jù)以上,粘包的關(guān)鍵解決辦法,就是設(shè)定數(shù)據(jù)格式,可以看看我們的SRMP協(xié)議,1字節(jié)標識,1字節(jié)序號,2字節(jié)長度
如果客戶端發(fā)送太頻繁,服務(wù)端tcp緩沖區(qū)阻塞,發(fā)送窗口會逐步縮小到0,不再接受客戶端數(shù)據(jù)。
1.5 .NetCore版RPC框架
NetCore版RPC框架NewLife.ApiServer。
先看看這個效果
代碼分析
我們看這部分代碼,4次調(diào)用遠程函數(shù),成功獲取結(jié)果,包括二進制高速調(diào)用、返回復雜對象、捕獲遠程異常,沒錯,這就是一個RPC。
服務(wù)端
有沒有發(fā)現(xiàn),這個ApiServer跟前面的NetServer有點像?其實ApiServer內(nèi)部就有一個NetServer
這么些行代碼,就幾個地方有價值,一個是注冊了兩個控制器。你可以直接理解為Mvc的控制器,只不過我們沒有路由管理系統(tǒng),直接手工注冊。
第二個是指定編碼器為Json,用Json傳輸參數(shù)和返回值。其實內(nèi)部默認就是Json,可以不用指定
看看我們的控制器,特別像Mvc,只不過這里的Controller沒有基類,各個Action返回值不是ActionResult,是的,ApiServer就是一個按照Mvc風格設(shè)置的RPC框架
返回復雜對象
做請求預處理,甚至攔截異常
像下面這樣寫RPC服務(wù),然后把它注冊到ApiServer上,客戶端就可以在1234端口上請求這些接口服務(wù)啦
客戶端
客戶端是ApiClient,這里的MyClient繼承自ApiClient
這些就是我們剛才客戶端遠程調(diào)用的stub代碼啦,當然,我們沒有自動生成stub,也沒有要求客戶端跟服務(wù)端共用接口之類。實際上,我們認為完全沒有必要做接口約束,大部分項目的服務(wù)接口很少,并且要求靈活多變
stub就是類似于,剛才那個MyController實現(xiàn)IAbc接口,然后客戶端根據(jù)服務(wù)端元數(shù)據(jù)自動在內(nèi)存里面生成一個stub類并編譯,這個類實現(xiàn)了IAbc接口。
客戶端直接操作接口,還以為在調(diào)用服務(wù)端 的函數(shù)呢其實stub代碼內(nèi)部,就是封裝了 這里的InvokeAsync這些代碼,等同于自動生成這些代碼,包括gRPC、Thrift等都是這么干的
框架解析
這個RPC框架,封包協(xié)議就是剛才的SRMP,負載數(shù)據(jù)也就是協(xié)議是json
當需要高速傳輸?shù)臅r候,參數(shù)用Byte[],它就會直接傳輸,不經(jīng)json序列化,這是多年經(jīng)驗得到的靈活性與性能的最佳結(jié)合點
2.1 人人都有一個自己的高性能網(wǎng)絡(luò)庫
網(wǎng)絡(luò)庫核心代碼:http://git.newlifex.com/NewLife/X/Tree/master/NewLife.Core/Net
我們一開始就是讓Tcp/Udp可以混合使用,網(wǎng)絡(luò)庫設(shè)計于2005年,應(yīng)該要比現(xiàn)如今絕大部分網(wǎng)絡(luò)框架要老。服務(wù)端清一色采用 Server+Session 的方式。
網(wǎng)絡(luò)庫的幾個精髓文件
其中比較重要的一個,里面實現(xiàn)了 Open/Close/Send/Receive 系列封裝,Tcp/Udp略有不同,重載就好了。打開關(guān)閉比較簡單,就不講了
所有對象,不管客戶端服務(wù)端,都實現(xiàn)ISocket。然后客戶端Client,服務(wù)端Server+Session。tcp+udp同時支持并不難,因為它們都基于Socket。
目前無狀態(tài)無會話的通信架構(gòu),做不到高性能。我們就是依靠長連接以及合并小包,實現(xiàn)超高吞吐量
一般靈活性和高性能都是互相矛盾的
2.2 高性能設(shè)計要點
第一要點:同步發(fā)送,因為要做發(fā)送隊列、拆分、合并,等等,異步發(fā)送大大增加了復雜度。大家如果將來遇到詭異的40ms延遲,非??赡芫褪莟cp的nodelay作怪,可以設(shè)為true解決
第二要點:IOCP,高吞吐率的服務(wù)端,一定是異步接收,而不是多線程同步。當然,可以指定若干個線程去select,也就是Linux里面常見的poll,那個不在這里討論,Windows極少人這么干,大量資料表明,iocp更厲害。
SAEA是.net/.netcore當下最流行的網(wǎng)絡(luò)架構(gòu),我們可以通俗理解為,把這個緩沖區(qū)送給操作系統(tǒng)內(nèi)核,待會有數(shù)據(jù)到來的時候,直接放在里面,這樣子就減少了一次內(nèi)核態(tài)到用戶態(tài)的拷貝過程。
我們測試4.5Gbps,除以8,大概是 540M字節(jié),這個拷貝成本極高
第三要點:零拷貝ZeroCopy,這也是netty的核心優(yōu)勢。iocp是為了減少內(nèi)核態(tài)到用戶態(tài)的拷貝,zerocopy進一步把這個數(shù)據(jù)交給用戶層,不用拷貝了。
數(shù)據(jù)處理,我們采用了鏈式管道,
這些都是管道的編碼器
第四要點:合并小包,NoDelay=false,允許tcp合并小包,MTU=1500,除了頭部,一般是1472
第五要點:二進制序列化,消息報文盡可能短小,每個包1k,對于100Mbps,也就12M,理論上最多12000包,所以大量Json協(xié)議或者字符串協(xié)議,吞吐量都在1萬上下
SRMP頭部4字節(jié),ApiServer的消息報文,一般二三十個字節(jié),甚至十幾個字節(jié)
第五要點:批量操作,User FindByID(int id); User[] FindByIDs(int[] ids);
最后
整理不全,大家湊合著看。中途錄屏,語音啥的還掉了,準備得不是很好,下周再來一次吧,選Redis
原文地址:?https://www.cnblogs.com/xxred/p/9859893.html
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結(jié)
以上是生活随笔為你收集整理的聊聊如何设计千万级吞吐量的.Net Core网络通信!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ASP.NET Core 2调用Azur
- 下一篇: [译]ASP.NET Core中使用Me