dubbo啟用服務瞬間出現超時呼叫
簡單介紹下:
dubbo是阿里開源出來的一款高效能遠端呼叫框架,可以使開發者像使用本地服務一樣呼叫遠端服務,目前已經畢業為apache的頂級專案。
背景
目前生產環境發版可以簡化為如下三個步驟: 假設服務A有10臺機器A1~A10在提供服務:
- 先針對機器A1~A5操作服務禁用,使呼叫請求不會發到即將部署的機器上。
- 對機器A1~A5進行部署,檢查應用啟動情況。
- 對機器A1~A5操作服務啟用。
- 對A6~A10重複以上3步。
問題來了:基本上每次進行釋出的時候,都會有呼叫方反饋出現呼叫超時。通過查檢查上下游日誌總結出以下規律:
- 超時呼叫發生在上線過程的第三步,也就是操作服務啟用瞬間。
- 呼叫方超時的請求在服務方沒有對應日誌。
猜想
-
或許是因為應用剛啟動完成,程式碼還沒有進行jit編譯,導致呼叫超時;
-
與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;
}
複製程式碼
- 啟用服務瞬間有大量消費者來建立tcp連線,造成服務端來不及響應;
tcp三次握手
在這裡回顧下tcp的三次握手過程:
作為必考題之一的tcp三次握手,他的過程肯定已經再熟悉不過了:- 客戶端發生syn到服務端,客戶端狀態轉為syn_send狀態。
- 服務端接受到客戶端發生的syn握手包,回覆syn+ack,進入syn_rcvd狀態。此時,服務端為半連線。
- 客戶端收到服務端回傳的syn+ack,並回傳ack。客戶端服務端進入established狀態,變為全連線。
那麼,問題來了
大家都知道網路是天然的併發環境,server端在收到第三次握手的ack後如何去校驗傳入的ack值是否合法呢?
答案是系統維護了兩個佇列,分別稱為半連線佇列,和全連線佇列。
第一步server收到client的syn後,把相關資訊放到半連線佇列中,同時回覆syn+ack給client
第三步server收到client的ack,從半連線佇列拿出相關資訊放入到全連線佇列中。
所以,完整的tcp三次握手過程應該是下面這樣:
(圖來自這裡:www.cnxct.com/something-a…所以,既然有佇列存在,那麼就一定會有長度限制。
dubbo連線模型
再回到dubbo上。這裡只介紹兩個與啟用服務超時相關的點。
tcp連線數
阿里這樣介紹dubbo:以少量提供者支援大量的消費者呼叫(原話我不記得了,反正是這個意思)。
我認為其原因之一是dubbo使用了共享連線:
簡單來說,共享連線意思就是在同一組消費者,提供者機器上,只維護一個tcp長連線,即使該消費者需要呼叫提供者提供的多個服務。
當然,目前生產環境都是叢集部署,針對單臺提供者來看的話,他所建立的tcp連線應該是下圖這樣:
如果所示,如果應用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引數一同修改。