聊聊如何設計千萬級吞吐量的.Net Core網路通訊!

weixin_34119545發表於2018-10-27
原文:聊聊如何設計千萬級吞吐量的.Net Core網路通訊!

聊聊如何設計千萬級吞吐量的.Net Core網路通訊!

  • 作者:大石頭
  • 時間:2018-10-26 晚上 20:00
  • 地點:QQ群-1600800
  • 內容:網路通訊,
    1. 網路庫使用方式
    2. 網路庫設計理念,高效能要點

介紹

1.1 開始網路程式設計

簡單的網路程式示例

  • 相關使用介紹:https://www.cnblogs.com/nnhy/p/newlife_net_echo.html
  • 克隆上面的程式碼,執行EchoTest專案,開啟編譯的exe,開啟兩次,一個選1作為伺服器,一個選2作為客戶端
    1057748-20181027002148820-2146341896.png
    1057748-20181027001609291-1374054519.png
  • 在客戶端連線伺服器和給服務端傳送資料的時候,分別觸發StartOnReceive方法,連線之後服務端傳送了Welcome 的訊息,客戶端傳送5次“你好”。服務端回傳收到的資料,打了一個日誌,把收到的資訊轉成字串輸出到控制檯。
  • NetServer是應用級網路伺服器,支援tcp/udp/ipv4/ipv6。上面可以看到,同時監聽了四個埠。
  • 碼神工具也可以連線上來
    1057748-20181027003502567-1186544414.png

解釋

  • 對於網路會話來說,最關鍵的就是客戶端連上來,以及收到資料包,這兩部分,對應上面StartOnReceive兩個方法
    1057748-20181027004204160-2079112329.png
    1057748-20181027004211636-1775245956.png

服務端

  • 上面是最小的網路庫例程,簡單演示了服務端和客戶端,連線和收發資訊。網路應用分為NetServer/NetSession,服務端、會話,N個客戶端連線伺服器,就會有N個會話。來一個客戶端連線,服務端就new一個新的NetSession,並執行Start,收到一個資料包,就執行OnReceive,連線斷開,就執行OnDispose,這便是服務端的全部。
  • 客戶端連線剛上來的時候,沒有資料包等其它資訊,所以這個時候沒有引數。客戶端發資料包過來,OnReceive函式在處理。
  • 服務端的建立,可以是很簡單,看以下截圖。這裡為了測試方便,開了很多Log,實際使用的時候,根據需要註釋。
    1057748-20181027005200798-1579528699.png
  • 長連線、心跳第二節設計理念再講。

客戶端

  • 跟很多網路庫不同,NewLife.Net除了服務端,還封裝了客戶端。客戶端的核心,也就是Send函式和Received事件,同步傳送,非同步接收。
    1057748-20181027005818755-893878361.png
  • 因為是長連線,所以服務端隨時可以向客戶端傳送資料包,客戶端也可以收到。tcp在不做設定的時候,預設長連線2小時。
  • NetServer預設20分鐘,在沒有心跳的時候,20分鐘沒有資料包往來,服務端會幹掉這個會話。
  • 雖然上面講的NetServer和Client,都是tcp,但是換成其它協議也是可以的。這裡的NetServer和NetUri.CreateRemote,同時支援Tcp/Udp/IPv4/IPv6等,CreateRemote內部,就是根據地址的不同,去new不同的客戶端。所以我們寫的程式碼,根本不在意用的是tcp還是udp,或者IPv6。有興趣的可以看看原始碼

1.2 構建可靠網路服務

  • 相關部落格
  • 要真正形成一個網路服務,那得穩定可靠。上面例程EchoTest只是簡單演示,接下來看下一個例程EchoAgent。
    1057748-20181027011612919-738776179.png
    1057748-20181027011636747-278701050.png
    1057748-20181027011653992-732330205.png

安裝執行

  • 這是一個標準的Windows服務,有了這個東西,我們就可以妥妥的註冊到Windows裡面去。這也是目前我們大量資料分析程式的必備。
  • 首先執行EchoAgent,按2,安裝註冊服務,用管理員身份執行。安裝成功然後可以在服務裡面找到剛剛安裝的服務。
    1057748-20181027011738924-103714494.png
    1057748-20181027011905411-1360917063.png
    1057748-20181027011925902-1428458419.png
    1057748-20181027011944405-1268711412.png
  • 安裝完成可以在服務上找到,再次按2就是解除安裝,這個是XAgent提供的功能
    1057748-20181027012009457-2019839893.png
    1057748-20181027012036765-698037494.png
  • 這時候按3,啟動服務
    1057748-20181027012255110-866863531.png

程式碼解釋

  • 接下來看程式碼,服務啟動的時候,執行StartWork。在這個時候例項化並啟動NetServer,得到的效果就跟例程EchoTest一樣,區別是一個是控制檯一個是服務。停止服務時執行StopWork,我們可以在這裡關閉NetServer。詳細請看原始碼
    1057748-20181027012650150-767564618.png
  • 必須有這個東西,你的網路服務程式,才有可能達到產品級。linux上直接控制檯,上nohup,當然還有很多其它辦法。以後希望這個XAgent能夠支援linux吧,這樣就一勞永逸了

1.3 壓測

  • 相關部落格
  • 只需要記住一個兩個數字,.net應用打出來2266萬tps,流量峰值4.5Gbps
  • 兩千萬吞吐量的數字,當然,只能看不能用。因為服務端只是剛才的Echo而已,並沒有帶什麼業務。實際工作中,帶著業務和資料庫,能跑到10萬已經非常非常牛逼了。
  • 我們工作中的服務可以跑到100萬,但是我不敢,怕它不小心就崩了。所以我們都是按照10萬的上限來設計,不夠就堆伺服器好了,達到5萬以上後,穩定性更重要

    網路程式設計的坑

  • 主要有粘包
  • 程式設計師中會網路程式設計的少,會解決粘包的更少!

1.4 網路程式設計的坎——粘包

  • 普遍情況,上萬的程式設計師,會寫網路程式的不到20%,會解決粘包問題的不到1%,如果大家會寫網路程式,並且能解決粘包,那麼至少已經達到了網路程式設計的中級水平。

什麼是粘包

  • 舉個例子:客戶端連續發了5個包,服務端就收到了一個大包。程式碼就不演示了,把第一個例程的這個睡眠去掉。
    1057748-20181027013515866-248752418.png
    1057748-20181027013527707-1703518104.png
  • 客戶端連續發了5個包,服務端就收到了一個大包。

原因

  • 很多人可能都聽說Tcp是流式協議,但是很少人去問,什麼叫流式吧?流式,就是它把資料像管道一樣傳輸過去。
  • 剛才我們發了5個 “你好”,它負責把這10個字發到對方,至於發多少次,每次發幾個字,不用我們操心,tcp底層自己處理。tcp負責把資料一個不丟的按順序的發過去。所以,為了效能,它一般會把相近的資料包湊到一起發過去。對方收到一個大包,5個小包都粘在了一起,這就是最簡單的粘包。
  • 這個特性由NoDelay設定決定。NoDelay預設是false,需要自己設定。如果設定了,就不會等待。但是不要想得那麼美好,因為對方可能合包。
  • 區域網MTU(Maximum Transmission Unit,最大傳輸單元)是1500,處於ip tcp 頭部等,大概1472多點的樣子。

更復雜的粘包及解決方法

  • A 1000 位元組 B 也是 1000位元組,對方可能收到兩個包,1400 + 600。對方可能收到兩個包,1400 + 600。
  • 凡是以特殊符號開頭或結尾來處理粘包的辦法,都會有這樣那樣的缺陷,最終是給自己挖坑。所以,tcp粘包,絕大部分解決方案,偏向於指定資料包長度。這其中大部分使用4位元組長度,長度+資料。對方收到的時候,根據長度判斷後面資料足夠了沒有。
  • 這是粘包的處理程式碼:http://git.newlifex.com/NewLife/X/Blob/master/NewLife.Core/Net/Handlers/MessageCodec.cs
    1057748-20181027014607168-577879264.png
  • 每次判斷長度,接收一個或多個包,如果接收不完,留下,存起來。等下一個包到來的時候,拼湊完整。
  • 雖然tcp確保資料不丟,但是難免我們自己失手,弄丟了一點點資料。為了避免禍害後面所有包,就需要進行特殊處理了。
  • 每個資料幀,自己把頭部長度和資料體湊一起傳送啊,tcp確保順序。這裡我們把超時時間設定為3~5秒,每次湊包,如果發現上次有殘留,並且超時了,那麼就扔了它,省得禍害後面。
    1057748-20181027014934043-1338449804.png
  • 根據以上,粘包的關鍵解決辦法,就是設定資料格式,可以看看我們的SRMP協議,1位元組標識,1位元組序號,2位元組長度
    1057748-20181027015258134-313626553.png
  • 如果客戶端傳送太頻繁,服務端tcp緩衝區阻塞,傳送視窗會逐步縮小到0,不再接受客戶端資料。

1.5 .NetCore版RPC框架

程式碼分析

  • 我們看這部分程式碼,4次呼叫遠端函式,成功獲取結果,包括二進位制高速呼叫、返回複雜物件、捕獲遠端異常,沒錯,這就是一個RPC。
    1057748-20181027020020172-1996432747.png
    1057748-20181027020215851-1188284918.png

服務端

  • 有沒有發現,這個ApiServer跟前面的NetServer有點像?其實ApiServer內部就有一個NetServer
    1057748-20181027020254151-308928025.png
  • 這麼些行程式碼,就幾個地方有價值,一個是註冊了兩個控制器。你可以直接理解為Mvc的控制器,只不過我們沒有路由管理系統,直接手工註冊。
  • 第二個是指定編碼器為Json,用Json傳輸引數和返回值。其實內部預設就是Json,可以不用指定
  • 看看我們的控制器,特別像Mvc,只不過這裡的Controller沒有基類,各個Action返回值不是ActionResult,是的,ApiServer就是一個按照Mvc風格設定的RPC框架
    1057748-20181027020400338-831300481.png
  • 返回複雜物件
    1057748-20181027020435974-1210144251.png
  • 做請求預處理,甚至攔截異常
    1057748-20181027020506481-1237236197.png
  • 像下面這樣寫RPC服務,然後把它註冊到ApiServer上,客戶端就可以在1234埠上請求這些介面服務啦
    1057748-20181027020557726-2096270337.png
    1057748-20181027020610445-1123270905.png

客戶端

  • 客戶端是ApiClient,這裡的MyClient繼承自ApiClient
    1057748-20181027020744510-1475570757.png
    1057748-20181027020751604-513118689.png
  • 這些就是我們剛才客戶端遠端呼叫的stub程式碼啦,當然,我們沒有自動生成stub,也沒有要求客戶端跟服務端共用介面之類。實際上,我們認為完全沒有必要做介面約束,大部分專案的服務介面很少,並且要求靈活多變
  • stub就是類似於,剛才那個MyController實現IAbc介面,然後客戶端根據服務端後設資料自動在記憶體裡面生成一個stub類並編譯,這個類實現了IAbc介面。
    客戶端直接操作介面,還以為在呼叫服務端 的函式呢
  • 其實stub程式碼內部,就是封裝了 這裡的InvokeAsync這些程式碼,等同於自動生成這些程式碼,包括gRPC、Thrift等都是這麼幹的

框架解析

  • 這個RPC框架,封包協議就是剛才的SRMP,負載資料也就是協議是json
  • 當需要高速傳輸的時候,引數用Byte[],它就會直接傳輸,不經json序列化,這是多年經驗得到的靈活性與效能的最佳結合點

2.1 人人都有一個自己的高效能網路庫

  • 網路庫核心程式碼:http://git.newlifex.com/NewLife/X/Tree/master/NewLife.Core/Net
  • 我們一開始就是讓Tcp/Udp可以混合使用,網路庫設計於2005年,應該要比現如今絕大部分網路框架要老。服務端清一色採用 Server+Session 的方式。
  • 網路庫的幾個精髓檔案
    1057748-20181027021743823-606212944.png
  • 其中比較重要的一個,裡面實現了 Open/Close/Send/Receive 系列封裝,Tcp/Udp略有不同,過載就好了。開啟關閉比較簡單,就不講了
    1057748-20181027021924221-1935855185.png
  • 所有物件,不管客戶端服務端,都實現ISocket。然後客戶端Client,服務端Server+Session。tcp+udp同時支援並不難,因為它們都基於Socket。
  • 目前無狀態無會話的通訊架構,做不到高效能。我們就是依靠長連線以及合併小包,實現超高吞吐量
  • 一般靈活性和高效能都是互相矛盾的

2.2 高效能設計要點

  • 第一要點:同步傳送,因為要做傳送佇列、拆分、合併,等等,非同步傳送大大增加了複雜度。大家如果將來遇到詭異的40ms延遲,非常可能就是tcp的nodelay作怪,可以設為true解決
  • 第二要點:IOCP,高吞吐率的服務端,一定是非同步接收,而不是多執行緒同步。當然,可以指定若干個執行緒去select,也就是Linux裡面常見的poll,那個不在這裡討論,Windows極少人這麼幹,大量資料表明,iocp更厲害。
    1057748-20181027022817636-1204939346.png
    1057748-20181027023013231-973376447.png
    • SAEA是.net/.netcore當下最流行的網路架構,我們可以通俗理解為,把這個緩衝區送給作業系統核心,待會有資料到來的時候,直接放在裡面,這樣子就減少了一次核心態到使用者態的拷貝過程。
    • 我們測試4.5Gbps,除以8,大概是 540M位元組,這個拷貝成本極高
  • 第三要點:零拷貝ZeroCopy,這也是netty的核心優勢。iocp是為了減少核心態到使用者態的拷貝,zerocopy進一步把這個資料交給使用者層,不用拷貝了。
    • 資料處理,我們採用了鏈式管道,
      1057748-20181027023723251-931494927.png
    • 這些都是管道的編碼器
      1057748-20181027023800151-952885401.png
  • 第四要點:合併小包,NoDelay=false,允許tcp合併小包,MTU=1500,除了頭部,一般是1472
  • 第五要點:二進位制序列化,訊息報文儘可能短小,每個包1k,對於100Mbps,也就12M,理論上最多12000包,所以大量Json協議或者字串協議,吞吐量都在1萬上下
    • SRMP頭部4位元組,ApiServer的訊息報文,一般二三十個位元組,甚至十幾個位元組
  • 第五要點:批量操作User FindByID(int id); User[] FindByIDs(int[] ids);

最後

  • 整理不全,大家湊合著看。中途錄屏,語音啥的還掉了,準備得不是很好,下週再來一次吧,選Redis

相關文章