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

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

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

圖片最終排名

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

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

Dubbo Mesh 是什麼?

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

如果你用過 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。一圖勝千言:

圖片Dubbo Mesh

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

賽題剖析

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

語言的選擇,初賽結束後的感受,大家主要還是在 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 實現:

圖片dubbo mesh初始方案

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

  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數=43=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

https://code.aliyun.com/250577914/agent-demo.git

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章