1. 程式人生 > 程式設計 >dubbo啟用服務瞬間出現超時呼叫

dubbo啟用服務瞬間出現超時呼叫

簡單介紹下:

 dubbo是阿里開源出來的一款高效能遠端呼叫框架,可以使開發者像使用本地服務一樣呼叫遠端服務,目前已經畢業為apache的頂級專案。

dubbo

背景

 目前生產環境發版可以簡化為如下三個步驟: 假設服務A有10臺機器A1~A10在提供服務:

  • 先針對機器A1~A5操作服務禁用,使呼叫請求不會發到即將部署的機器上。
  • 對機器A1~A5進行部署,檢查應用啟動情況。
  • 對機器A1~A5操作服務啟用。
  • 對A6~A10重複以上3步。

問題來了:基本上每次進行釋出的時候,都會有呼叫方反饋出現呼叫超時。通過查檢查上下游日誌總結出以下規律:

  • 超時呼叫發生在上線過程的第三步,也就是操作服務啟用瞬間。
  • 呼叫方超時的請求在服務方沒有對應日誌。

猜想

  1. 或許是因為應用剛啟動完成,程式碼還沒有進行jit編譯,導致呼叫超時;

  2. 與dubbo的lazy連線有關?閱讀原始碼可知,在開啟lazy連線的情況下,消費端並不會著急與服務端建立tcp連線,而是在有呼叫發生時在去建立;

 DubboProtocol.java
 ------------------
 private ExchangeClient initClient(URL url) {
    ...
        ExchangeClient client;
        try {
            // connection should be lazy
            if
(url.getParameter(Constants.LAZY_CONNECT_KEY,false)) { client = new LazyConnectExchangeClient(url,requestHandler); } else { client = Exchangers.connect(url,requestHandler); } } catch (RemotingException e) { throw new RpcException("Fail to create remoting client for service("
+ url + "): " + e.getMessage(),e); } return client; } 複製程式碼
  1. 啟用服務瞬間有大量消費者來建立tcp連線,造成服務端來不及響應;

tcp三次握手

 在這裡回顧下tcp的三次握手過程:  

tcp_1
作為必考題之一的tcp三次握手,他的過程肯定已經再熟悉不過了:

  1. 客戶端發生syn到服務端,客戶端狀態轉為syn_send狀態。
  2. 服務端接受到客戶端發生的syn握手包,回覆syn+ack,進入syn_rcvd狀態。此時,服務端為半連線。
  3. 客戶端收到服務端回傳的syn+ack,並回傳ack。客戶端服務端進入established狀態,變為全連線。

那麼,問題來了

 大家都知道網路是天然的併發環境,server端在收到第三次握手的ack後如何去校驗傳入的ack值是否合法呢?

答案是系統維護了兩個佇列,分別稱為半連線佇列,和全連線佇列。

第一步server收到client的syn後,把相關資訊放到半連線佇列中,同時回覆syn+ack給client

第三步server收到client的ack,從半連線佇列拿出相關資訊放入到全連線佇列中。

所以,完整的tcp三次握手過程應該是下面這樣:

tcp_2
(圖來自這裡:www.cnxct.com/something-a…

所以,既然有佇列存在,那麼就一定會有長度限制。

dubbo連線模型

 再回到dubbo上。這裡只介紹兩個與啟用服務超時相關的點。

tcp連線數

阿里這樣介紹dubbo:以少量提供者支援大量的消費者呼叫(原話我不記得了,反正是這個意思)。

我認為其原因之一是dubbo使用了共享連線:

簡單來說,共享連線意思就是在同一組消費者,提供者機器上,只維護一個tcp長連線,即使該消費者需要呼叫提供者提供的多個服務。

當然,目前生產環境都是叢集部署,針對單臺提供者來看的話,他所建立的tcp連線應該是下圖這樣:

dubbo_tcp_conn
如果所示,如果應用customer1有3臺機器,customer2有4臺機器,且他們都需要呼叫provider的服務,那麼prodiver就需要維護最多3+4=7條tcp連線。

(為什麼是最多,因為dubbo後臺有一個執行緒去關閉有段時間不用的tcp連線。如果qps效高,且負載均衡策略會落在每一臺機器上的話,連線可能被關閉的不會很多)。

lazy 連線

當客戶端與服務端建立代理時,暫不建立 tcp長連線,當有資料請求時再做連線初始化。

超時問題

 有了上面這些背景知識,再看最初的超時問題就比較容易理解了:

首先,dubbo服務端需要和每一天客戶端建立一個tcp連線,如果呼叫方部署非常多,那麼每個服務端需要建立的連線也是非常多的。 tcp連線的個數可以用這個命令來估算一下:

netstat -ant  | wc -l
複製程式碼

其次,由於我們開啟了lazy連線,那麼在啟用服務後,消費者收到zk事件,開始分配呼叫到新的提供者上,在qps較高的情況下,服務端受到的壓力情況可以用下面這段虛擬碼來描述:

CountDownLatch latch = new CountDownLatch(NUMBER_OF_TCP_CONN);
int i = 0;
while (i++< NUMBER_OF_TCP_CONN){
    new Thread(()->{
        latch.countDown();
        latch.await();
        //傳送syn握手包
        send_syn();
        
    }).start();
}
複製程式碼

可以想到,大家都在幾乎同一時間來建立tcp連線,這種情況是不是讓你想起了一種古老的拒絕服務攻擊?

沒錯,就是syn flood攻擊。簡單來說就是通過傳送大量的syn握手包,使服務端半連線佇列溢位,從而無法提供服務。具體可以參考[zh.wikipedia.org/wiki/SYN_fl…](SYN flood)。

如果懷疑tcp連線佇列溢位,可以使用以下命令確認:

netstat -s | egrep "listen|LISTEN" 
複製程式碼

解決方案

知道了原因,那麼問題就解決了一半。

最直觀的做法就是調整tcp的半連線佇列和全連線佇列。

全連線佇列的大小取決於:min(backlog,somaxconn) 。 backlog是在socket建立的時候傳入的,somaxconn是一個os級別的系統引數。

半連線佇列的大小取決於:max(64,/proc/sys/net/ipv4/tcp_max_syn_backlog)。

其中值得注意的是,在jdk中,backlog預設的設定的為50。

java.net.ServerSocket
---------------------
 public ServerSocket(int port,int backlog,InetAddress bindAddr) throws IOException {
        setImpl();
        if (port < 0 || port > 0xFFFF)
            throw new IllegalArgumentException(
                       "Port value out of range: " + port);
        if (backlog < 1)
          backlog = 50;
        try {
            bind(new InetSocketAddress(bindAddr,port),backlog);
        } catch(SecurityException e) {
            close();
            throw e;
        } catch(IOException e) {
            close();
            throw e;
        }
    }
    public ServerSocket(int port) throws IOException {
        this(port,50,null);
    }
複製程式碼

所以,對於backlog引數,僅修改系統引數是不起作用的,需要將建立ServerSocket是傳入的backlog引數一同修改。

參考資料

jm.taobao.org/2017/05/25/…

zh.wikipedia.org/wiki/SYN_fl…

dubbo.apache.org/zh-cn/docs/…