Broken Pipe
Broken Pipe發生的原因
當某個程序試圖往一個已收到RST的SOCKET連線寫資料,就會出現Broken Pipe。
(由於TCP協議層已經處於RST狀態了,因此不會將資料發出,而是發一個SIGPIPE訊號給應用層,SIGPIPE訊號的預設處理動作是終止程式。)
那麼確定什麼時候TCP會發送RST報文段,就可以確定Broken Pipe發生的具體原因。
之前已經分析了TCP RST報文產生的幾種情景了。
原因分析
broken pipe出現的前提條件是程序試圖往一個已經在RST狀態的TCP連線寫入資料。
那麼這個寫入資料,到底應該怎麼理解呢?到底是程序試圖往本地SendQ傳送快取區寫入資料還是TCP協議試圖將SendQ的資料傳送到對端的RecvQ呢?按照字面意思應該是前者。
之前我們已經分析了幾種會出現RST報文的情況。
結合我們出現該異常的介面分析。我們發現我們出錯的介面,返回的資料,最小的8K多,最大的超過128K。查閱了幾天的異常日誌,都沒有發現一個報出broken pipe異常錯誤的介面的返回資料小於8K。
根據RST報文產生的情況,我們可以做出如下推斷,當Client端與我們的伺服器建立了TCP連結之後。當TCP協議將服務端SendQ佇列裡的內容傳送到對端(Client)的ReceQ佇列中後,Client關閉了程序,此時ReceQ 讀取快取區還有資料未被讀取(不管ReceQ的資料Client端有沒有讀取過,也不管TCP將多少服務端的資料發到了ReceQ,總之,就是在關閉的時候,還有資料存在於讀取快取區中),這時候關閉socket,會導致端Client端產生一個RST重置報文。
這時候服務端的資料還沒有寫完,會繼續寫入,當再次寫入的時候,TCP協議已經是RST狀態的了,這個時候,就會發生broken pipe。
上面的推論能夠解釋broken pipe發生的一整個流程。
那我們來檢視下linux伺服器的預設SendQ大小與我們介面返回的資料大小的對比,就能夠是否確實是這些介面的資料寫入都需要多次寫入快取區。
那麼如何檢視linux預設的SendQ緩衝區大小呢。linux給我們提供了相應的命令
ubuntu@VM-104-50-ubuntu:/data/iyourcar$ cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
最小 預設 最大
我們發現,預設寫快取區大小是16k,可是我們的介面返回的資料最小的是8k多呀,這個介面返回的資料是可以一次寫入快取區的呀。咋回事呀,怎麼這樣也會broken呢。如果服務端一次性寫入16k資料到寫快取區,那麼是不可能出現broken pipe的呀。那隻能證明我們的程式並不是一次性寫入16k的資料給快取區,這個大小肯定是要比8k多要小的。那我們就來求證一下,用一個會報出異常的介面在出現異常的地方進行debug,主要debug寫入資料的流程。
使用的內建容器是Tomcat
OutputBuffer#appendByteArray
public static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
public OutputBuffer() {
this(DEFAULT_BUFFER_SIZE);
}
private void appendByteArray(byte src[], int off, int len) throws IOException {
if (len == 0) {
return;
}
int limit = bb.capacity();
//我們發現,每一次寫入的位元組數bb.capacity()大小,而預設的capacity大小就是8*1024,也就是8k
while (len >= limit) {
realWriteBytes(ByteBuffer.wrap(src, off, limit));
len = len - limit;
off = off + limit;
}
if (len > 0) {
transfer(src, off, len, bb);
}
}
我們發現,實際上,tomcat幫我們向socket寫入資料的時候,是每8k寫入一次SendQ(但是真正TCP傳送資料,可能是分很多塊去傳送到對端的ReceQ的)
後來我們將內建web容器換成了undertow,發現依舊會發生這種情況,應該預設也是一次寫入8k吧,具體還沒有debug。
所以當使用的容器是tomcat的時候,只要介面返回資料的大小大於8k,就可能會出現broken pipe。