Dubbo中的連接控制,你真的理解嗎?

前言

剛發現微信公衆號有了標籤功能,於是乎,我將我 Dubbo 相關的文章都打上了標籤,仔細一統計,這已經是我第 41 篇原創的 Dubbo 文章了,如果你希望看到我其他的 Dubbo 文章,可以從話題標籤點擊進入。

這是一篇很久之前就想動筆寫的文章,最近正好看到羣裏有小夥伴分享了 Dubbo 連接相關的文章,才又讓我想起了這個話題。今天想跟大家聊的便是 Dubbo 中的連接控制這一話題。說到“連接控制”,可能有讀者還沒反應過來,但你對下面的配置可能不會感到陌生:

<dubbo:reference interface="com.foo.BarService" connections="10" />

如果你還不瞭解 Dubbo 中連接控制的用法,可以參考官方文檔:https://dubbo.apache.org/zh/docs/advanced/config-connections/ ,話說最近 Dubbo 官方文檔來了一次大換血,好多熟悉的文檔差點都沒找到在哪兒 Orz。

衆所周知,dubbo 協議通信默認是長連接,連接配置功能用於決定消費者與提供者建立的長連接數。但官方文檔只給出了該功能的使用方法,卻並沒有說明什麼時候應該配置連接控制,本文將主要圍繞該話題進行探討。

本文也會涉及長連接相關的一些知識點。

使用方式:

先來看一個 Dubbo 構建的簡單 demo,啓動一個消費者(192.168.4.226)和一個提供者(192.168.4.224),配置他們的直連。

消費者:

<dubbo:reference id="userService" check="false"
interface="org.apache.dubbo.benchmark.service.UserService"
    url="dubbo://192.168.4.224:20880"/>

提供者:

<dubbo:service interface="org.apache.dubbo.benchmark.service.UserService" ref="userService" />
<bean id="userService" class="org.apache.dubbo.benchmark.service.UserServiceServerImpl"/>

長連接是看不見摸不着的東西,我們需要一個觀測性工作來”看到“它。啓動提供者和消費者之後,可以使用如下的命令查看 tcp 連接情況

  • Mac 下可使用: lsof -i:20880
  • Linux 下可使用: netstat -ano | grep 20880

提供者:

[root ~]# netstat -ano | grep 20880
tcp6       0      0 192.168.4.224:20880      :::*                    LISTEN      off (0.00/0/0)
tcp6    2502      0 192.168.4.224:20880      192.168.4.226:59100     ESTABLISHED off (0.00/0/0)

消費者:

[root@ ~]# netstat -ano | grep 20880
tcp6     320    720 192.168.4.226:59110     192.168.4.224:20880      ESTABLISHED on (0.00/0/0)

通過上述觀察到的現象我們可以發現幾個事實。

僅僅是啓動了提供者和消費者,上述的 TCP 連接就已經存在了,要知道我並沒有觸發調用。也就是說,Dubbo 建連的默認策略是在地址發現時,而不是在調用時。當然,你也可以通過延遲加載 lazy="true" 來修改這一行爲,這樣可以將建聯延遲到調用時。

<dubbo:reference id="userService" check="false"
interface="org.apache.dubbo.benchmark.service.UserService"
url="dubbo://${server.host}:${server.port}"
lazy="true"/>

除此之外,還可以發現消費者和提供者之間只有一條長連接,20880 是 Dubbo 提供者默認開放的端口,就跟 tomcat 默認開放的 8080 一個地位,而 59110 是消費者隨機生成的一個端口。(我之前跟一些朋友交流過,發現很多人不知道消費者也是需要佔用一個端口的)

而今天的主角”連接控制“便可以控制長連接的數量,例如我們可以進行如下的配置

<dubbo:reference id="userService" check="false"
   interface="org.apache.dubbo.benchmark.service.UserService"
   url="dubbo://192.168.4.224:20880"
connections="2" />

再啓動一次消費者,觀察長連接情況

提供者:

[root@ ~]# netstat -ano | grep 20880
tcp6       0      0 192.168.4.224:20880      :::*                    LISTEN      off (0.00/0/0)
tcp6    2508     96 192.168.4.224:20880      192.168.4.226:59436     ESTABLISHED on (0.00/0/0)
tcp6    5016    256 192.168.4.224:20880      192.168.4.226:59434     ESTABLISHED on (0.00/0/0)

消費者:

[root@ ~]# netstat -ano | grep 20880
tcp6       0   2520 192.168.4.226:59436     192.168.4.224:20880      ESTABLISHED on (0.00/0/0)
tcp6      48   1680 192.168.4.226:59434     192.168.4.224:20880      ESTABLISHED on (0.00/0/0)

可以看到,這裏已經變成兩條長連接了。

什麼時候需要配置多條長連接

現在我們知道了如何進行連接控制,但什麼時候我們應該配置多少條長連接呢?這個時候我可以跟你說,具體視生產情況而定,但你如果你經常看我的公衆號,肯定會知道這不是我的風格,我的風格是什麼?benchmark!

寫作之前,我跟幾個同事和網友對這個話題進行了簡單的討論,其實也沒有什麼定論,無非是對單連接和多連接吞吐量高低不同的論調。參考既往 Dubbo github 中的 issue,例如:https://github.com/apache/dubbo/pull/2457,我也參與了這個 pr 的討論,講道理,我是持懷疑態度的,我當時的觀點是多連接不一定能夠提升服務的吞吐量(還是挺保守的,沒有這麼絕對)。

那接下來,還是用 benchmark 來說話吧,測試工程還是我們的老朋友,使用 Dubbo 官方提供的 dubbo-benchmark 工程。

  • 測試工程地址:https://github.com/apache/dubbo-benchmark.git
  • 測試環境:2 臺阿里雲 Linux  4c8g ECS

測試工程在之前的文章介紹過,這裏就不過多贅述了,測試方案也非常簡單,兩輪 benchmark,分別測試 connections=1 和 connections=2 時,觀察測試方法的吞吐量。

說幹就幹,省略一堆測試步驟,直接給出測試結果。

connections=1

Benchmark           Mode  Cnt      Score      Error  Units
Client.createUser  thrpt    3  22265.286 ± 3060.319  ops/s
Client.existUser   thrpt    3  33129.331 ± 1488.404  ops/s
Client.getUser     thrpt    3  19916.133 ± 1745.249  ops/s
Client.listUser    thrpt    3   3523.905 ±  590.250  ops/s

connections=2

Benchmark           Mode  Cnt      Score      Error  Units
Client.createUser  thrpt    3  31111.698 ± 3039.052  ops/s
Client.existUser   thrpt    3  42449.230 ± 2964.239  ops/s
Client.getUser     thrpt    3  30647.173 ± 2551.448  ops/s
Client.listUser    thrpt    3   6581.876 ±  469.831  ops/s

從測試結果來看,似乎單連接和多連接的差距是非常大的,近乎可以看做是 2 倍了!看起來連接控制的效果真是好呀,那麼事實真的如此嗎?

按照這種方案第一次測試下來之後,我也不太相信這個結果,因爲我之前按照其他方式做過多連接的測試,並且我也參加過第三屆中間件挑戰賽,使得我對長連接的認知是:大多數時候,單連接往往能發揮出最優的性能。即使由於硬件原因,這個差距也不應該是兩倍。懷着這樣的疑問,我開始研究,是不是我的測試場景出了什麼問題呢?

發現測試方案的問題

經過和閃電俠的討論,他的一席話最終讓我定位到了問題的所在。

image-20210907212036374

不知道大家看完我和閃電俠的對話,有沒有立刻定位到問題所在。

之前測試方案最大的問題便是沒有控制好變量,殊不知:在連接數變化的同時,實際使用的 IO 線程數實際也發生了變化

Dubbo 使用 Netty 來實現長連接通信,提到長連接和 IO 線程的關係,這裏就要介紹到 Netty 的連接模型了。一言以蔽之,Netty 的設置 IO worker 線程和 channel 是一對多的綁定關係,即一個 channel 在建連之後,便會完全由一個 IO 線程來負責全部的 IO 操作。再來看看 Dubbo 是如何設置 NettyClient 和 NettyServer 的 worker 線程組的:

客戶端 org.apache.dubbo.remoting.transport.netty4.NettyClient

    private static final EventLoopGroup NIO_EVENT_LOOP_GROUP = eventLoopGroup(Constants.DEFAULT_IO_THREADS, "NettyClientWorker");
    
    @Override
    protected void doOpen() throws Throwable {
        final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this);
        bootstrap = new Bootstrap();
        bootstrap.group(NIO_EVENT_LOOP_GROUP)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
      ...
    }

Constants.DEFAULT_IO_THREADSorg.apache.dubbo.remoting.Constants 中被寫死了

int DEFAULT_IO_THREADS = Math.min(Runtime.getRuntime().availableProcessors() + 132);

在我的 4c8g 的機器上,默認等於 5。

服務端 org.apache.dubbo.remoting.transport.netty4.NettyServer

protected void doOpen() throws Throwable {
        bootstrap = new ServerBootstrap();

        bossGroup = NettyEventLoopFactory.eventLoopGroup(1"NettyServerBoss");
        workerGroup = NettyEventLoopFactory.eventLoopGroup(
                getUrl().getPositiveParameter(IO_THREADS_KEY, Constants.DEFAULT_IO_THREADS),
                "NettyServerWorker");

        final NettyServerHandler nettyServerHandler = new NettyServerHandler(getUrl(), this);
        channels = nettyServerHandler.getChannels();

        ServerBootstrap serverBootstrap = bootstrap.group(bossGroup, workerGroup)
            .channel(NettyEventLoopFactory.serverSocketChannelClass());
        .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
                .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
                .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
                
 }

服務端倒是可以配置,例如我們可以通過 protocol 來控制服務端的 IO 線程數:

<dubbo:protocol name="dubbo" host="${server.host}" server="netty4" port="${server.port}" iothreads="5"/>

如果不設置,則跟客戶端邏輯一致,是 core + 1 個線程。

好了,問題就在這兒,由於我並沒有進行任何 IO 線程的設置,所以客戶端和服務端都會默認開啓 5 個 IO 線程。當 connections=1 時,Netty 會將 channel1 綁定到一個 IO 線程上,而當 connections=2 時,Netty 會將 channel1 和 channel2 按照順序綁定到 NettyWorkerThread-1和 NettyWorkerThread-2 上,這樣就會有兩個 IO 線程在工作,這樣的測試結果當然是不公平的。

這裏需要考慮實際情況,在實際生產中,大多數時候都是分佈式場景,連接數一定都是大於 IO 線程數的,所以基本不會出現測試場景中的 channel 數少於 IO 線程數的場景。

解決方案也很簡單,我們需要控制變量,讓 IO 線程數一致,僅僅觀察連接數對吞吐量的影響。針對服務端,可以在 protocol 層配置 iothreads=1;針對客戶端,由於源碼被寫死了,這裏我只能通過修改源碼的方式,重新本地打了一個包,使得客戶端 IO 線程數也可以通過 -D 參數指定。

改造之後的,我們得到了如下的測試結果:

1 IO 線程 1 連接

Benchmark           Mode  Cnt      Score      Error  Units
Client.createUser  thrpt    3  22265.286 ± 3060.319  ops/s
Client.existUser   thrpt    3  33129.331 ± 1488.404  ops/s
Client.getUser     thrpt    3  19916.133 ± 1745.249  ops/s
Client.listUser    thrpt    3   3523.905 ±  590.250  ops/s

1 IO 線程 2 連接

Benchmark           Mode  Cnt      Score      Error  Units
Client.createUser  thrpt    3  21776.436 ± 1888.845  ops/s
Client.existUser   thrpt    3  31826.320 ± 1350.434  ops/s
Client.getUser     thrpt    3  19354.470 ±  369.486  ops/s
Client.listUser    thrpt    3   3506.714 ±   18.924  ops/s

可以發現,單純提升連接數並不會提升服務的吞吐量,這樣的測試結果也更加符合我認知的預期。

總結

從上述測試的結果來看,一些配置參數並不是越大就代表了越好,類似的例子我也在多線程寫文件等場景分析過,唯有理論分析+實際測試才能得出值得信服的結論。當然個人的測試,也可能會因爲局部性關鍵信息的遺漏,導致誤差,例如,如果我最終沒有發現 IO 線程數和連接數之間的隱性關聯,很容易就得出連接數和吞吐量成正比的錯誤結論了。當然,也不一定就代表本文最終的結論是靠譜的,說不定還是不夠完善的,也歡迎大家留言,提出意見和建議。

最終回到最初的問題,我們什麼時候應該配置 Dubbo 的連接控制呢?按照我個人的經驗,大多數時候,生產環境下連接數是非常多的,你可以挑選一臺線上的主機,通過 netstat -ano| grep 20880| wc -l 來大概統計下,一般是遠超 IO 線程數的,沒必要再多配置成倍的連接數,連接數和吞吐量並不是一個線性增長的關係。

Dubbo 框架有這個能力和大家真的需要用這個能力完全是兩碼事,我相信大多數讀者應該已經過了技術新鮮感驅動項目的階段了吧?如果有一天你需要控制連接數,去達到一定特殊的用途,你就會真心感嘆,Dubbo 真是強呀,這個擴展點都有。

Dubbo 的連接控制真的完全沒有用嗎?也不盡然,我的測試場景還是非常有限的,可能在不同硬件上會跑出不一樣的效果,例如我在第三屆中間件性能挑戰賽中,就是用 2 連接跑出了最好的成績,並非單連接。

最後,你如果僅僅使用 Dubbo 去維繫你們的微服務架構,大部分情況不需要關注到連接控制這個特性,多花點時間搬磚吧,就醬,我也去搬磚了。

END -

「技術分享」某種程度上,是讓作者和讀者,不那麼孤獨的東西。歡迎關注我的微信公衆號:「Kirito的技術分享」


本文分享自微信公衆號 - Kirito的技術分享(cnkirito)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

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