天池中間件大賽 dubboMesh 優化總結(qps 從 1000 到 6850)

天池中間件大賽的初賽在今早終於正式結束了,公衆號停更了一個月,主要緣由就是博主的空餘時間幾乎全花在這個比賽上,第一賽季結束,作下參賽總結,總的來講,收穫不小。java

最終排名

先說結果,最終榜單排名是第 15 名(除去前排大佬的兩個小號,加上做弊的第一名,勉強能算是第 12 名),說實話是挺滿意的成績。這篇文章主要是分享給如下讀者:比賽中使用了 netty 卻沒有達到理想 qps 的朋友,netty 剛入門的朋友,對 dubbo mesh 感興趣的朋友。node

在比賽以前我我的對 netty 的認識也僅僅停留在瞭解的層面,在以前解讀 RPC 原理的系列文章中涉及到 netty 傳輸時曾瞭解過一二,基本能夠算零基礎使用 netty 參賽,因此我會更多地站在一個小白的視角來闡述本身的優化歷程,一步步地提升 qps,也不會繞開那些本身踩過的坑以及負優化。另外一方面,因爲本身對 netty 的理解並非很深,因此文中若是出現錯誤,敬請諒解,歡迎指正。python

Dubbo Mesh 是什麼?

爲了照顧那些不太瞭解此次比賽內容的讀者,我先花少許的篇幅介紹下此次阿里舉辦的天池中間件大賽到底比的是個什麼東西,那就不得不先介紹下 Dubbo Mesh 這個概念。react

若是你用過 dubbo,而且對 service mesh 有所瞭解,那麼必定能夠秒懂 Dubbo Mesh 是爲了解決什麼問題。說白了,dubbo 原先是爲了 java 語言而準備的,沒有考慮到跨語言的問題,這意味着 nodejs,python,go 要想無縫使用 dubbo 服務,要麼藉助於各自語言的 dubbo 客戶端,例如:node-dubbo-client,python-dubbo-client,go-dubbo-client;要麼就是藉助於 service mesh 的解決方案,讓 dubbo 本身提供跨語言的解決方案,來屏蔽不一樣語言的處理細節,因而乎,dubbo 生態的跨語言 service mesh 解決方案就被命名爲了 dubbo mesh。一圖勝千言:linux

Dubbo Mesh

在原先的 dubbo 生態下,只有 consumer,provider,註冊中心的概念。dubbo mesh 生態下爲每一個服務(每一個 consumer,provider 實例)啓動一個 agent,服務間再也不進行直接的通訊,而是經由各自的 agent 完成交互,而且服務的註冊發現也由 agent 完成。圖中紅色的 agent 即是此次比賽的核心,選手們能夠選擇合適的語言來實現 agent,最終比拼高併發下各自 agent 實現的 qps,qps 即最終排名的依據。c++

賽題剖析

此次比賽的主要考察點在於高併發下網絡通訊模型的實現,能夠涵蓋如下幾個關鍵點:reactor 模型,負載均衡,線程,鎖,io 通訊,阻塞與非阻塞,零拷貝,序列化,http/tcp/udp與自定義協議,批處理,垃圾回收,服務註冊發現等。它們對最終程序的 qps 起着或大或小的影響,對它們的理解越深,越可以編寫出高性能的 dubbo mesh 方案。git

語言的選擇,初賽結束後的感覺,你們主要仍是在 java,c++,go 中進行了抉擇。語言的選擇考慮到了諸多的因素,通用性,輕量級,性能,代碼量和qps的性價比,選手的習慣等等。雖然前幾名貌似都是 c++,但整體來講,排名 top 10 以外,毫不會是由於語言特性在從中阻撓。c++ 選手高性能的背後,多是犧牲了 600 多行代碼在本身維護一個 etcd-lib(比賽限制使用 etcd,但據使用 c++ 的選手說,c++ 沒有提供 etcd 的 lib);且此次比賽提供了預熱環節,java 黨也露出了欣慰的笑容。java 的主流框架仍是在 nio,akka,netty 之間的抉擇,netty 應該是衆多 java 選手中較爲青睞的,博主也選擇了 netty 做爲 dubbo mesh 的實現;go 的協程和網絡庫也是兩把利器,並不比 java 弱,加上其進程輕量級的特性,也做爲了一個選擇。算法

官方提供了一個 qps 並非很高的 demo,來方便選手們理解題意,能夠說是很是貼心了,來回顧一下最簡易的 dubbo mesh 實現:spring

dubbo mesh初始方案

如上圖所示,是整個初始 dubbo mesh 的架構圖,其中 consumer 和 provider 以灰色表示,由於選手是不能修改其實現的,綠色部分的 agent 是能夠由選手們自由發揮的部分。比賽中 consumer,consumer-agent 爲 單個實例,provider、provider-agent 分別啓動了三個性能不一的實例:small,medium,large,這點我沒有在圖中表示出來,你們自行腦補。因此全部選手都須要完成如下幾件事:shell

  1. consumer-agent 須要啓動一個 http 服務器,接收來自 consumer 的 http 請求
  2. consumer-agent 須要轉發該 http 請求給 provider-agent,而且因爲 provider-agent 有多個實例,因此須要作負載均衡。consumer-agent 與 provider-agent 之間如何通訊能夠自由發揮。
  3. provider-agent 拿到 consumer-agent 的請求以後,須要組裝成 dubbo 協議, 使用 tcp 與 provider 完成通訊。

這樣一個跨語言的簡易 dubbo mesh 便呈如今你們面前了,從 consumer 發出的 http 協議,最終成功調用到了使用 java 語言編寫的 dubbo 服務。這中間如何優化,如何使用各類黑科技成就了一場很是有趣的比賽。博主全部的優化都不是一蹴而就的,都是一每天的提交試出來的,因此剛好可使用時間線順序敘述本身的改造歷程。

優化歷程

Qps 1000 到 2500 (CA 與 PA 使用異步 http 通訊)

官方提供的 demo 直接跑通了整個通訊流程,省去了咱們大量的時間,初始版本評測能夠達到 1000+ 的 qps,因此 1000 能夠做爲 baseline 給你們提供參考。demo 中 consumer 使用 asyncHttpClient 發送異步的 http 請求, consumer-agent 使用了 springmvc 支持的 servlet3.0 特性;而 consumer-agent 到 provider-agent 之間的通訊卻使用了同步 http,因此 C 到 CA 這一環節相比 CA 到 PA 這一環節性能是要強不少的。改造起來也很簡單,參照 C 到 CA 的設計,直接將 CA 到 PA 也替換成異步 http,qps 能夠直接到達 2500。

主要得益於 async-http-client 提供的異步 http-client,以及 servlet3.0 提供的非阻塞 api。

<dependency>
    <groupId>org.asynchttpclient</groupId>
    <artifactId>async-http-client</artifactId>
    <version>2.4.7</version>
</dependency>
複製代碼
// 非阻塞發送 http 請求
ListenableFuture<org.asynchttpclient.Response> responseFuture = asyncHttpClient.executeRequest(request);

// 非阻塞返回 http 響應
@RequestMapping(value = "/invoke")
public DeferredResult<ResponseEntity> invoke(){}
複製代碼

Qps 2500 到 2800 (負載均衡優化爲加權輪詢)

demo 中提供的負載均衡算法是隨機算法,在 small-pa,medium-pa,large-pa 中隨機選擇一個訪問,每一個服務的性能不同,響應時間天然也不一樣,隨機負載均衡算法存在嚴重的不穩定性,沒法按需分配請求,因此成了天然而然的第二個改造點。

優化爲加權輪詢算法,這一塊的實現參考了 motan(weibo 開源的 rpc 框架)的實現,詳見 com.alibaba.dubbo.performance.demo.agent.cluster.loadbalance.WeightRoundRobinLoadBalance(文末貼 git 地址)。

在啓動腳本中配置權重信息,伴隨 pa 啓動註冊服務地址到 etcd 時,順帶將權重信息一併註冊到 etcd 中,ca 拉取服務列表時便可獲取到負載比例。

large:
-Dlb.weight=3
medium:
-Dlb.weight=2
small:
-Dlb.weight=1
複製代碼

預熱賽時最高併發爲 256 鏈接,這樣的比例能夠充分發揮每一個 pa 的性能。

Qps 2800 到 3500 (future->callback)

c 到 ca 以及 ca 到 pa 此時儘管是 http 通訊,但已經實現了非阻塞的特性(請求不會阻塞 io 線程),但 dubbo mesh 的 demo 中 pa 到 p 的這一通訊環節仍是使用的 future.get + countDownLatch 的阻塞方式,一旦整個環節出現了鎖和阻塞,qps 必然上不去。關於幾種獲取結果的方式,也是老生常談的話題:

基礎通訊模型

future 方式在調用過程當中不會阻塞線程,但獲取結果是會阻塞線程,provider 固定 sleep 了 50 ms,因此獲取 future 結果依舊是一個耗時的過程,加上這種模型通常會使用鎖來等待,性能會形成明顯的降低。替換成 callback 的好處是,io 線程專一於 io 事件,下降了線程數,這和 netty 的 io 模型也是很是契合的。

Promise<Integer> agentResponsePromise = new DefaultPromise<>(ctx.executor());
agentResponsePromise.addListener();
複製代碼

netty 爲此提供了默認的 Promise 的抽象,以及 DefaultPromise 的默認實現,咱們能夠 out-of-box 的使用 callback 特性。在 netty 的入站 handler 的 channelRead 事件中建立 promise,拿到 requestId,創建 requestId 和 promise 的映射;在出站 handler 的channelRead 事件中拿到返回的 requestId,查到 promise,調用 done 方法,便完成了非阻塞的請求響應。可參考: 入站 handler ConsumerAgentHttpServerHandler 和 和出站 handler ConsumerAgentClientHandler 的實現。

Qps 3500 到 4200 (http通訊替換爲tcp通訊)

ca 到 pa 的通訊本來是異步 http 的通訊方式,徹底能夠參考 pa 到 p 的異步 tcp 通訊進行改造。自定義 agent 之間的通訊協議也很是容易,考慮到 tcp 粘包的問題,使用定長頭+字節數組來做爲自定義協議是一個較爲經常使用的作法。這裏踩過一個坑,本來想使用 protoBuffer 來做爲自定義協議,netty 也很友好的提供了基於 protoBuffer 協議的編解碼器,只須要編寫好 DubboMeshProto.proto 文件便可:

message AgentRequest {
    int64 requestId = 1;
    string interfaceName = 2;
    string method = 3;
    string parameterTypesString = 4;
    string parameter = 5;
}

message AgentResponse {
    int64 requestId = 1;
    bytes hash = 2;
}
複製代碼

protoBuffer 在實際使用中的優點是毋庸置疑的,其能夠儘量的壓縮字節,減小 io 碼流。在正式賽以前一直用的好好的,但後來的 512 併發下經過 jprofile 發現,DubboMeshProto 的 getSerializedSize ,getDescriptorForType 等方法存在沒必要要的耗時,對於此次比賽中如此簡單的數據結構而言 protoBuffer 並非那麼優秀。最終仍是採起了定長頭+字節數組的自定義協議。參考:com.alibaba.dubbo.performance.demo.agent.protocol.simple.SimpleDecoder

http 通訊既然換了,乾脆一換到底,ca 的 springmvc 服務器也可使用 netty 實現,這樣更加有利於實現 ca 總體的 reactive。使用 netty 實現 http 服務器很簡單,使用 netty 提供的默認編碼解碼器便可。

public class ConsumerAgentHttpServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    public void initChannel(SocketChannel ch) {
        ChannelPipeline p = ch.pipeline();
        p.addLast("encoder", new HttpResponseEncoder());
        p.addLast("decoder", new HttpRequestDecoder());
        p.addLast("aggregator", new HttpObjectAggregator(10 * 1024 * 1024));
        p.addLast(new ConsumerAgentHttpServerHandler());
    }
}
複製代碼

http 服務器的實現也踩了一個坑,解碼 http request 請求時沒注意好 ByteBuf 的釋放,致使 qps 跌倒了 2000+,反而不如 springmvc 的實現。在隊友@閃電俠的幫助下成功定位到了內存泄露的問題。

public static Map<String, String> parse(FullHttpRequest req) {
    Map<String, String> params = new HashMap<>();
    // 是POST請求
    HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(new DefaultHttpDataFactory(false), req);
    List<InterfaceHttpData> postList = decoder.getBodyHttpDatas();
    for (InterfaceHttpData data : postList) {
        if (data.getHttpDataType() == InterfaceHttpData.HttpDataType.Attribute) {
            MemoryAttribute attribute = (MemoryAttribute) data;
            params.put(attribute.getName(), attribute.getValue());
        }
    }
    // resolve memory leak
    decoder.destroy();
    return params;
}
複製代碼

在正式賽後發現還有更快的 decode 方式,不須要藉助於上述的 HttpPostRequestDecoder,而是改用 QueryStringDecoder:

public static Map<String, String> fastParse(FullHttpRequest httpRequest) {
    String content = httpRequest.content().toString(StandardCharsets.UTF_8);
    QueryStringDecoder qs = new QueryStringDecoder(content, StandardCharsets.UTF_8, false);
    Map<String, List<String>> parameters = qs.parameters();
    String interfaceName = parameters.get("interface").get(0);
    String method = parameters.get("method").get(0);
    String parameterTypesString = parameters.get("parameterTypesString").get(0);
    String parameter = parameters.get("parameter").get(0);
    Map<String, String> params = new HashMap<>();
    params.put("interface", interfaceName);
    params.put("method", method);
    params.put("parameterTypesString", parameterTypesString);
    params.put("parameter", parameter);
    return params;
}
複製代碼

節省篇幅,直接在這兒將以後的優化貼出來,後續再也不對這個優化贅述了。

Qps 4200 到 4400 (netty複用eventLoop)

這個優化點來自於比賽認識的一位好友@半杯水,因爲沒有使用過 netty,比賽期間惡補了一下 netty 的線程模型,得知了 netty 能夠從客戶端引導 channel,從而複用 eventLoop。不瞭解 netty 的朋友能夠把 eventLoop 理解爲 io 線程,若是入站的 io 線程和 出站的 io 線程使用相同的線程,能夠減小沒必要要的上下文切換,這一點在 256 併發下可能還不明顯,只有 200 多 qps 的差距,但在 512 下尤其明顯。複用 eventLoop 在《netty實戰》中是一個專門的章節,篇幅雖然很少,但很是清晰地向讀者闡釋瞭如何複用 eventLoop(注意複用同時存在於 ca 和 pa 中)。

// 入站服務端的 eventLoopGroup
private EventLoopGroup workerGroup;

// 爲出站客戶端預先建立好的 channel
private void initThreadBoundClient(EventLoopGroup workerGroup) {
    for (EventExecutor eventExecutor : eventLoopGroup) {
        if (eventExecutor instanceof EventLoop) {
            ConsumerAgentClient consumerAgentClient = new ConsumerAgentClient((EventLoop) eventExecutor);
            consumerAgentClient.init();
            ConsumerAgentClient.put(eventExecutor, consumerAgentClient);
        }

    }
}
複製代碼

使用入站服務端的 eventLoopGroup 爲出站客戶端預先建立好 channel,這樣能夠達到複用 eventLoop 的目的。而且此時還有一個伴隨的優化點,就是將存儲 Map<requestId,Promise> 的數據結構,從 concurrentHashMap 替換爲了 ThreadLocal ,由於入站線程和出站線程都是相同的線程,省去一個 concurrentHashMap 能夠進一步下降鎖的競爭。

到了這一步,總體架構已經清晰了,c->ca,ca->pa,pa->p 都實現了異步非阻塞的 reactor 模型,qps 在 256 併發下,也達到了 4400 qps。

優化後的dubbo mesh方案

正式賽 512 鏈接帶來的新格局

上述這份代碼在預熱賽 256 併發下表現尚可,但正式賽爲了體現出你們的差距,將最高併發數直接提高了一倍,但 qps 卻並無獲得很好的提高,卡在了 5400 qps。和 256 鏈接下一樣 4400 的朋友交流事後,發現咱們之間的差距主要體如今 ca 和 pa 的 io 線程數,以及 pa 到 p 的鏈接數上。5400 qps 顯然低於個人預期,爲了下降鏈接數,我修改了原來 provider-agent 的設計。從如下優化開始,是正式賽 512 鏈接下的優化,預熱賽只有 256 鏈接。

Qps 5400 到 5800 (下降鏈接數)

對 netty 中 channel 的優化搜了不少文章,依舊不是很肯定鏈接數究竟是不是影響我代碼的關鍵因素,在和小夥伴溝通以後實在找不到 qps 卡在 5400 的緣由,因而乎抱着試試的心態修改了下 provider-agent 的設計,採用了和 consumer-agent 同樣的設計,預先拿到 provder-agent 入站服務器的 woker 線程組,建立出站請求的 channel,將原來的 4 個線程,4 個 channel 下降到了 1 個線程,一個 channel。其餘方面未作任何改動,qps 順利達到了 5800。

理論上來講,channel 數應該不至於成爲性能的瓶頸,可能和 provider dubbo 的線程池策略有關,最終得出的經驗就是:在 server 中合理的在 io 事件處理能力的承受範圍內,使用盡量少的鏈接數和線程數,能夠提高 qps,減小沒必要要的線程切換。順帶一提(此時 ca 的線程數爲 4,入站鏈接爲 http 鏈接,最高爲 512 鏈接,出站鏈接因爲和線程綁定,又須要作負載均衡,因此爲

線程數*pa數=4*3=12

這個階段,還存在另外一個問題,因爲 provider 線程數固定爲 200 個線程,若是 large-pa 繼續分配 3/1+2+3=0.5 即 50% 的請求,很容易出現 provider 線程池飽滿的異常,因此調整了加權值爲 1:2:2。限制加權負載均衡的再也不僅僅是機器性能,還要考慮到 provider 的鏈接處理能力。

Qps 5800 到 6100 (Epoll替換Nio)

依舊感謝@半杯水的提醒,因爲評測環境使用了 linux 做爲評測環境,因此可使用 netty 本身封裝的 EpollSocketChannel 來代替 NioSocketChannel,這個提高遠超個人想象,直接幫助我突破了 6000 的關卡。

private EventLoopGroup bossGroup = Epoll.isAvailable() ? new EpollEventLoopGroup(1) : new NioEventLoopGroup(1);
private EventLoopGroup workerGroup = Epoll.isAvailable() ? new EpollEventLoopGroup(2) : new NioEventLoopGroup(2);
bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
複製代碼

本地調試因爲我是 mac 環境,無法使用 Epoll,因此加了如上的判斷。

NioServerSocketChannel 使用了 jdk 的 nio,其會根據操做系統選擇使用不一樣的 io 模型,在 linux 下一樣是 epoll,但默認是 level-triggered ,而 netty 本身封裝的 EpollSocketChannel 默認是 edge-triggered。 我原先覺得是 et 和 lt 的差距致使了 qps 如此大的懸殊,但後續優化 Epoll 參數時發現 EpollSocketChannel 也能夠配置爲 level-triggered,qps 並無降低,在比賽的特殊條件下,我的猜測並非這兩種觸發方式帶來的差距,而僅僅是 netty 本身封裝 epoll 帶來的優化。

//默認
bootstrap.option(EpollChannelOption.EPOLL_MODE, EpollMode.EDGE_TRIGGERED);
//可修改觸發方式
bootstrap.option(EpollChannelOption.EPOLL_MODE, EpollMode.LEVEL_TRIGGERED);
複製代碼

Qps 6100 到 6300 (agent自定義協議優化)

agent 之間的自定義協議我以前已經介紹過了,因爲一開始我使用了 protoBuf,發現了性能問題,就是在這兒發現的。在 512 下 protoBuf 的問題尤其明顯,最終爲了保險起見,以及爲了和我後面的一個優化兼容,最終替換爲了自定義協議—Simple 協議,這一點優化以前提到了,不在過多介紹。

Qps 6300 到 6500 (參數調優與zero-copy)

這一段優化來自於和 @折袖-許華建 的交流,很是感謝。又是一個對 netty 不太瞭解而沒注意的優化點:

  1. 關閉 netty 的內存泄露檢測:
-Dio.netty.leakDetectionLevel=disabled
複製代碼

netty 會在運行期按期抽取 1% 的 ByteBuf 進行內存泄露的檢測,關閉這個參數後,能夠得到性能的提高。

  1. 開啓 quick_ack:
bootstrap.option(EpollChannelOption.TCP_QUICKACK, java.lang.Boolean.TRUE)
複製代碼

tcp 相比 udp ,一個區別即是爲了可靠傳輸而進行的 ack,netty 爲 Epoll 提供了這個參數,能夠進行 quick ack,具體原理沒來及研究。

  1. 開啓 TCP_NODELAY
serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true)
複製代碼

這個優化可能大多數人都知道,放在這兒一塊兒羅列出來。網上搜到了一篇阿里畢玄的 rpc 優化文章,提到高併發下 ChannelOption.TCP_NODELAY=false 可能更好,但實測以後發現並不會。

其餘調優的參數可能都是玄學了,對最終的 qps 影響微乎其微。參數調優並不能體現太多的技巧,但對結果產生的影響倒是很可觀的。

在這個階段還同時進行了一個優化,和參數調優一塊兒進行的,因此不知道哪一個影響更大一些。demo 中 dubbo 協議編碼沒有作到 zero-copy,這無形中增長了一份數據從內核態到用戶態的拷貝;自定義協議之間一樣存在這個問題,在 dubbo mesh 的實踐過程當中應該儘量作到:能用 ByteBuf 的地方就不要用其餘對象,ByteBuf 提供的 slice 和 CompositeByteBuf 均可以很方便的實現 zero-copy。

Qps 6500 到 6600 (自定義http協議編解碼)

看着榜單上的人 qps 逐漸上升,而本身依舊停留在 6500,因而乎動了歪心思,GTMD 的通用性,本身解析 http 協議得了,不要 netty 提供的 http 編解碼器,不須要比 HttpPostRequestDecoder 更快的 QueryStringDecoder,就一個偏向於固定的 http 請求,實現自定義解析很是簡單。

POST / HTTP/1.1\r\n
content-length: 560\r\n
content-type: application/x-www-form-urlencoded\r\n
host: 127.0.0.1:20000\r\n
\r\n
interface=com.alibaba.dubbo.performance.demo.provider.IHelloService&method=hash&parameterTypesString=Ljava%32lang%32String;&parameter=xxxxx
複製代碼

http 文本協議自己仍是稍微有點複雜的,因此 netty 的實現考慮到通用性,必然不如咱們本身解析來得快,具體的粘包過程就不敘述了,有點 hack 的傾向。

同理,response 也本身解析:

HTTP/1.1 200 OK\r\n
Connection: keep-alive\r\n
Content-Type: text/plain;charset=UTF-8\r\n
Content-Length: 6\r\n
\r\n
123456
複製代碼

Qps 6600 到 6700 (去除對象)

繼續喪心病狂,不考慮通用性,把以前全部的中間對象都省略,encode 和 decode 盡一切可能壓縮到 handler 中去處理,這樣的代碼看起來很是難受,存在很多地方的 hardcoding。但效果是存在的,ygc 的次數下降了很多,全程使用 ByteBuf 和 byte[] 來進行數據交互。這個優化點一樣存在存在 hack 傾向,不過多贅述。

Qps 6700 到 6850 (批量flush,批量decode)

事實上到了 6700 有時候仍是須要看運氣的,從羣裏的吐槽現象就能夠發現,512 下的網路 io 很是抖,不清楚是機器的問題仍是高併發下的固有現象,6700的代碼都能抖到 5000 分。因此 6700 升 6850 的過程比較曲折,並且很不穩定,提交 20 次一共就上過兩次 6800+。

所作的優化是來自隊友@閃電俠的批量flush類,一次傳輸的字節數能夠提高,使得網絡 io 次數能夠下降,原理能夠簡單理解爲:netty 中 write 10 次,flush 1 次。一共實現了兩個版本的批量 flush。一個版本是根據同一個 channel write 的次數積累,最終觸發 flush;另外一個版本是根據一次 eventLoop 結束才強制flush。通過不少測試,因爲環境抖動太厲害,這二者沒測出多少差距。

handler(new ChannelInitializer<SocketChannel>() {
	@Override
	protected void initChannel(SocketChannel ch) {
	ch.pipeline()
		.addLast(new SimpleDecoder())
		.addLast(new BatchFlushHandler(false))
		.addLast(new ConsumerAgentClientHandler());
	}
});
複製代碼

批量 decode 的思想來自於螞蟻金服的 rpc 框架 sofa-bolt 中提供的一個抽象類:AbstractBatchDecoder

img

Netty 提供了一個方便的解碼工具類 ByteToMessageDecoder ,如圖上半部分所示,這個類具有 accumulate 批量解包能力,能夠儘量的從 socket 裏讀取字節,而後同步調用 decode 方法,解碼出業務對象,並組成一個 List 。最後再循環遍歷該 List ,依次提交到 ChannelPipeline 進行處理。此處咱們作了一個細小的改動,如圖下半部分所示,即將提交的內容從單個 command ,改成整個 List 一塊兒提交,如此能減小 pipeline 的執行次數,同時提高吞吐量。這個模式在低併發場景,並無什麼優點,而在高併發場景下對提高吞吐量有不小的性能提高。

值得指出的一點:這個對於 dubbo mesh 複用 eventLoop 的特殊場景下的優化效果實際上是存疑的,但個人最好成績的確是使用了 AbstractBatchDecoder 以後跑出來的。我曾經單獨將 ByteToMessageDecoder 和 AbstractBatchDecoder 拉出跑了一次分,的確是後者 qps 更高。

總結

其實在 qps 6500 時,總體代碼仍是挺漂亮的,至少感受能拿的出手給別人看。但最後爲了性能,加上時間比較趕,很多地方都進行了 hardcoding,而實際能投入生產使用的代碼必然要求通用性和擴展性,賽後有空會整理出兩個分支:一個 highest-qps 追求性能,另外一個分支保留下通用性。此次比賽從一個 netty 小白,最終學到了很多的知識點,仍是收穫很大的,最後感謝一下比賽中給過我指導的各位老哥。

最高 qps 分支:highest-qps

考慮通用性的分支(適合 netty 入門):master

code.aliyun.com/250577914/a…

最後幫隊友@閃電俠推廣下他的 netty 視頻教程,比賽中兩個比較難的優化點,都是由他進行的改造。imooc.com 搜索 Netty,能夠獲取 netty 源碼分析視頻。

歡迎關注個人微信公衆號:「Kirito的技術分享」,關於文章的任何疑問都會獲得回覆,帶來更多 Java 相關的技術分享。

關注微信公衆號
相關文章
相關標籤/搜索