DotNetty發送請求的最佳實踐

来源:https://www.cnblogs.com/egmkang/archive/2020/02/12/DotNetty-Gathering-Write.html
-Advertisement-
Play Games

長鏈接發送request/response時, 絕大部分包都是小包, 而每個小包都要消耗一個IP包, 成本大約是20-30us, 普通千兆網卡的pps大約是60Wpps, 所以想要提高長鏈接密集IO的應用性能, 需要做包的合併, 也稱為了scatter/gather io或者vector io. 在 ...


長鏈接發送request/response時, 絕大部分包都是小包, 而每個小包都要消耗一個IP包, 成本大約是20-30us, 普通千兆網卡的pps大約是60Wpps, 所以想要提高長鏈接密集IO的應用性能, 需要做包的合併, 也稱為了scatter/gather io或者vector io.

 

在linux下有readv/writev就是對應這個需求的, 減少系統調用, 減少pps, 提高網卡的吞吐量. 關於readv提高讀的速度, 可以看看陳碩muduo裡面對於readv的使用, 思路是就是在棧上面弄一個64KB的數組, 組成readv的第二塊buffer, 從而儘可能一次性把socket緩衝區的內容全部出來(參見5). 這裡不再贅述, 重點描述DotNetty下麵怎麼做Gathering Write.

 

首先得有一個Channel<IMessage>, 用來做寫的緩衝, 讓業務關心業務, 網路關心網路, 否則每個業務都WriteAndFlushAsync, 那是不太可能有合併發送的.

 

然後就是SendingLoop的主迴圈, 裡面不停的從Channel裡面TryRead包, 然後WriteAsync, 隔幾個包Flush一次. 類似的思想在Orleans Network裡面也存在.

 

 1 public void RunSendLoopAsync(IChannel channel)
 2 {
 3     var allocator = channel.Allocator;
 4     var reader = this.queue.Reader;
 5     Task.Run(async () => 
 6     {
 7         while (!this.stop) 
 8         {
 9             var more = await reader.WaitToReadAsync();
10             if (!more) 
11             {
12                 break;
13             }
14 
15             IOutboundMessage message = default;
16             var number = 0;
17             try 
18             {
19                 while (number < 4 && reader.TryRead(out message)) 
20                 {
21                     Interlocked.Decrement(ref this.queueCount);
22                     var msg = message.Inner as IMessage;
23                     var buffer = msg.ToByteBuffer(allocator);
24                     await channel.WriteAsync(buffer);
25                     number++;
26                 }
27                 channel.Flush();
28                 number = 0;
29             }
30             catch (Exception e)  when(message != default)
31             {
32                 logger.LogError("SendOutboundMessage Fail, SessionID:{0}, Exception:{1}",
33                     this.sessionID, e.Message);
34                 this.messageCenter.OnMessageFail(message);
35             }
36         }
37         this.logger.LogInformation("SessionID:{0}, SendingLoop Exit", this.sessionID);
38     });
39 }

第19-27行是關鍵, 這邊每4個包做一下flush, 然後flush會觸發DotNetty的DoWrite:

 1 protected override void DoWrite(ChannelOutboundBuffer input)
 2 {
 3     List<ArraySegment<byte>> sharedBufferList = null;
 4     try
 5     {
 6         while (true)
 7         {
 8             int size = input.Size;
 9             if (size == 0)
10             {
11                 // All written
12                 break;
13             }
14             long writtenBytes = 0;
15             bool done = false;
16 
17             // Ensure the pending writes are made of ByteBufs only.
18             int maxBytesPerGatheringWrite = ((TcpSocketChannelConfig)this.config).GetMaxBytesPerGatheringWrite();
19             sharedBufferList = input.GetSharedBufferList(1024, maxBytesPerGatheringWrite);
20             int nioBufferCnt = sharedBufferList.Count;
21             long expectedWrittenBytes = input.NioBufferSize;
22             Socket socket = this.Socket;
23 
24             List<ArraySegment<byte>> bufferList = sharedBufferList;
25             // Always us nioBuffers() to workaround data-corruption.
26             // See https://github.com/netty/netty/issues/2761
27             switch (nioBufferCnt)
28             {
29                 case 0:
30                     // We have something else beside ByteBuffers to write so fallback to normal writes.
31                     base.DoWrite(input);
32                     return;
33                 default:
34                     for (int i = this.Configuration.WriteSpinCount - 1; i >= 0; i--)
35                     {
36                         long localWrittenBytes = socket.Send(bufferList, SocketFlags.None, out SocketError errorCode);
37                         if (errorCode != SocketError.Success && errorCode != SocketError.WouldBlock)
38                         {
39                             throw new SocketException((int)errorCode);
40                         }

DotNetty TcpSocketChannel類的DoWrite函數, 19行獲取當前ChannelOutboundBuffer的Segment<byte>數組, 然後在36行調用Socket.Send一次性發出去, 這個是Gathering Write的關鍵. 有了這個, 就可以不在業務層用CompositeByteBuffer.

DotNetty Libuv Transport的實現可以看6, 思想是類似的.

 

實際上Orleans 3.x做的網路優化, 也有類似的思想:

 1 private async Task ProcessOutgoing()
 2 {
 3     await Task.Yield();
 4 
 5     Exception error = default;   
 6     PipeWriter output = default;
 7     var serializer = this.serviceProvider.GetRequiredService<IMessageSerializer>();
 8     try
 9     {
10         output = this.Context.Transport.Output;
11         var reader = this.outgoingMessages.Reader;
12         if (this.Log.IsEnabled(LogLevel.Information))
13         {
14             this.Log.LogInformation(
15                 "Starting to process messages from local endpoint {Local} to remote endpoint {Remote}",
16                 this.LocalEndPoint,
17                 this.RemoteEndPoint);
18         }
19 
20         while (true)
21         {
22             var more = await reader.WaitToReadAsync();
23             if (!more)
24             {
25                 break;
26             }
27 
28             Message message = default;
29             try
30             {
31                 while (inflight.Count < inflight.Capacity && reader.TryRead(out message) && this.PrepareMessageForSend(message))
32                 {
33                     inflight.Add(message);
34                     var (headerLength, bodyLength) = serializer.Write(ref output, message);
35                     MessagingStatisticsGroup.OnMessageSend(this.MessageSentCounter, message, headerLength + bodyLength, headerLength, this.ConnectionDirection);
36                 }
37             }
38             catch (Exception exception) when (message != default)
39             {
40                 this.OnMessageSerializationFailure(message, exception);
41             }
42 
43             var flushResult = await output.FlushAsync();
44             if (flushResult.IsCompleted || flushResult.IsCanceled)
45             {
46                 break;
47             }
48 
49             inflight.Clear();
50         }

核心在31行, 開始寫, 43行開始flush, 只不過Orleans用的pipelines io, DotNetty是傳統模型.

 

這樣做, 可以在有限的pps下, 支撐更高的吞吐量.

 

個人感覺DotNetty更好用一些.

 

參考:

1. https://github.com/Azure/DotNetty/blob/dev/src/DotNetty.Transport/Channels/Sockets/TcpSocketChannel.cs#L271-L288

2. https://github.com/dotnet/orleans/blob/master/src/Orleans.Core/Networking/Connection.cs#L282-L294

3. https://docs.microsoft.com/zh-cn/windows/win32/winsock/scatter-gather-i-o-2

4. https://linux.die.net/man/2/writev

5. https://github.com/chenshuo/muduo/blob/d980315dc054b122612f423ee2e1316cb14bd3b5/muduo/net/Buffer.cc#L28-L38

6. https://github.com/Azure/DotNetty/blob/dev/src/DotNetty.Transport.Libuv/Native/WriteRequest.cs#L106-L128

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 去除內嵌tomcat和添加jsp依賴 去除內嵌tomcat 在springboot啟動依賴中去除內嵌tomcat org.springframework.boot spring-boot-starter-web ... ...
  • 每日一句英語學習,每天進步一點點: “Action may not always bring happiness; but there is no happiness without action.” 「行動不見得一定帶來快樂,但沒有行動就沒有快樂。」 前言 我在閱讀 《Effective C++ ...
  • 在前面幾篇文章的例子中也可以看到mybatis中輸入映射和輸出映射的身影,但是沒有系統的總結一下,這篇博客主要對這兩個東東做一個總結。我們知道mybatis中輸入映射和輸出映射可以是基本數據類型、hashmap或者pojo的包裝類型,這裡主要來總結一下pojo包裝類型的使用,因為這個在開發中比較常用 ...
  • 4.1 數組的相關概念和名詞(瞭解) 1、數組(array): 一組具有相同數據類型的數據的按照一定順序排列的集合。 把有限的幾個相同類型的變數使用一個名稱來進行統一管理。 2、數組名: (1)這個數組名,代表的是一組數 (2)這個數組名中存儲的整個數組的“首地址” 3、下標(index): 我們使 ...
  • 由於Api的介面需要返回多語言,因此參考了網上很多篇文章,,有些文章寫的太過於理論,看起來比較費勁,今天下午搞了一個下午,總結了一下經驗,, 做這個功能時,主要參考了兩篇文章: https://blog.johnwu.cc/article/ironman-day21-asp-net-core-loc ...
  • 在 寫xaml的使用遇到了一些特殊字元,這裡記錄一下特殊字元轉義。 這些特殊字元遵循用於編碼的萬維網聯合會(W3C) XML 標準。 下表顯示這組特殊字元的編碼語法: 字元語法描述 < &lt; 小於符號。 > &gt; 大於符號。 & &amp; & 符號。 " &quot; 雙引號。 參見: h ...
  • 樣式提供了重用一組屬性設置的實用方法。它們為幫助構建一致的、組織良好的界面邁出了重要的第一步——但是它們也是有許多限制。 問題是在典型的應用程式中,屬性設置僅是用戶界面基礎結構的一小部分。甚至最基本的程式通常也需要大量的用戶界面代碼,這些代碼與應用程式的功能無關。在許多程式中,用於用戶界面任務的代碼 ...
  • static void Main(string[] args) { InsertionSortDemo(); Console.ReadLine(); } static void InsertionSortDemo() { Random rnd = new Random(); int[] arr = ...
一周排行
    -Advertisement-
    Play Games
  • 1. 說明 /* Performs operations on System.String instances that contain file or directory path information. These operations are performed in a cross-pla ...
  • 視頻地址:【WebApi+Vue3從0到1搭建《許可權管理系統》系列視頻:搭建JWT系統鑒權-嗶哩嗶哩】 https://b23.tv/R6cOcDO qq群:801913255 一、在appsettings.json中設置鑒權屬性 /*jwt鑒權*/ "JwtSetting": { "Issuer" ...
  • 引言 集成測試可在包含應用支持基礎結構(如資料庫、文件系統和網路)的級別上確保應用組件功能正常。 ASP.NET Core 通過將單元測試框架與測試 Web 主機和記憶體中測試伺服器結合使用來支持集成測試。 簡介 集成測試與單元測試相比,能夠在更廣泛的級別上評估應用的組件,確認多個組件一起工作以生成預 ...
  • 在.NET Emit編程中,我們探討了運算操作指令的重要性和應用。這些指令包括各種數學運算、位操作和比較操作,能夠在動態生成的代碼中實現對數據的處理和操作。通過這些指令,開發人員可以靈活地進行算術運算、邏輯運算和比較操作,從而實現各種複雜的演算法和邏輯......本篇之後,將進入第七部分:實戰項目 ...
  • 前言 多表頭表格是一個常見的業務需求,然而WPF中卻沒有預設實現這個功能,得益於WPF強大的控制項模板設計,我們可以通過修改控制項模板的方式自己實現它。 一、需求分析 下圖為一個典型的統計表格,統計1-12月的數據。 此時我們有一個需求,需要將月份按季度劃分,以便能夠直觀地看到季度統計數據,以下為該需求 ...
  • 如何將 ASP.NET Core MVC 項目的視圖分離到另一個項目 在當下這個年代 SPA 已是主流,人們早已忘記了 MVC 以及 Razor 的故事。但是在某些場景下 SSR 還是有意想不到效果。比如某些靜態頁面,比如追求首屏載入速度的時候。最近在項目中回歸傳統效果還是不錯。 有的時候我們希望將 ...
  • System.AggregateException: 發生一個或多個錯誤。 > Microsoft.WebTools.Shared.Exceptions.WebToolsException: 生成失敗。檢查輸出視窗瞭解更多詳細信息。 內部異常堆棧跟蹤的結尾 > (內部異常 #0) Microsoft ...
  • 引言 在上一章節我們實戰了在Asp.Net Core中的項目實戰,這一章節講解一下如何測試Asp.Net Core的中間件。 TestServer 還記得我們在集成測試中提供的TestServer嗎? TestServer 是由 Microsoft.AspNetCore.TestHost 包提供的。 ...
  • 在發現結果為真的WHEN子句時,CASE表達式的真假值判斷會終止,剩餘的WHEN子句會被忽略: CASE WHEN col_1 IN ('a', 'b') THEN '第一' WHEN col_1 IN ('a') THEN '第二' ELSE '其他' END 註意: 統一各分支返回的數據類型. ...
  • 在C#編程世界中,語法的精妙之處往往體現在那些看似微小卻極具影響力的符號與結構之中。其中,“_ =” 這一組合突然出現還真不知道什麼意思。本文將深入剖析“_ =” 的含義、工作原理及其在實際編程中的廣泛應用,揭示其作為C#語法奇兵的重要角色。 一、下劃線 _:神秘的棄元符號 下劃線 _ 在C#中並非 ...