天天看點

網絡40ms延遲問題

問題背景:

我 們一個企業使用者準備把線上業務從共享的mysql服務遷移到獨立型mysql rds上。企業使用者那邊先搞了一個test版本到我們rds環境,發現網站響應時間從3s變為40s。由于是php應用,故我們讓使用者應用開啟  xhprof調試後,看一次請求有1200多次mysql查詢,對mysql的查詢量非常大,并且請求的時間90%以上都花在了mysql上。而使用者使用 共享性mysql和使用rds的差別,是使用者runtime到rds之間多了一個proxy服務,通過proxy服務把使用者請求代理到了rds。

問題跟蹤:

1,由于使用者使用共享mysql和rds的差別,就是一個proxy服務。故就把問題定位在proxy這個服務上。

2,使用tcpdump在proxy機器(15.212)上抓runtime(21.102)到proxy,proxy到rds(144.139)的資料包,具體如下:

<a href="http://s4.51cto.com/wyfs02/M02/7E/54/wKiom1b8kKHw5eZCAAKKET3TZf0213.png" target="_blank"></a>

proxy收到runtime發過來的查詢資料包到proxy給runtime ack确認,花費了40ms,這個時間太長。并且,proxy把runtime發的查詢資料包從一個截斷為了2個資料包。  

1,id為22953:runtime102發送select語句到proxy 212。資料包長度為296byte 時間:23.877515

2,id為22954:proxy 212發送了一部分select語句128byte到rds 139。時間花銷(秒):23.877611-23.877515=0.000096

3,id為22955:proxy 212回 runtime 103的ack這一步時間花銷(秒):23.917294-23.877611=0.039683(這次時間花銷過大,一千次請求就是39s)

4,id為22956:rds 139回proxy  212的ack。這一步時間花銷(秒):23.918398-23.917294=0.001104

5,id為22957:proxy 212發送剩餘部分select語句168byte到rds  139。這一步時間花銷(秒):23.918415-23.918398=0.000017

3,又在proxy機器上進行了strace跟蹤,結果如下:

1

2

3

4

5

6

7

8

9

<code>11:00:57.923865 epoll_wait(7, {{EPOLLIN, {u32=12025792, u64=12025792}}}, 1024, 500) = 1</code>

<code>11:00:57.923964 recvfrom(8, </code><code>"\237\0\0\0\3SELECT cat_id, cat_name, parent_id, is_show FROM `jiewang300`.`jw_category`WHERE parent_id = '628' AND is_show = 1 ORDER BY"</code><code>, 128, 0, NULL, NULL) = 128</code>

<code>11:00:57.924041 recvfrom(8, </code><code>" sort_order ASC, cat_id ASC limit 8"</code><code>, 128, 0, NULL, NULL) = 35</code>

<code>11:00:57.924102 recvfrom(8, 0xb7b0b3, 93, 0, 0, 0) = -1 EAGAIN (Resource temporarily unavailable)</code>

<code>11:00:57.924214 epoll_wait(7, {{EPOLLOUT, {u32=12048160, u64=12048160}}}, 1024, 500) = 1</code>

<code>11:00:57.924340 sendto(9, </code><code>"\237\0\0\0\3SELECT cat_id, cat_name, parent_id, is_show FROM `jiewang300`.`jw_category`WHERE parent_id = '628' AND is_show = 1 ORDER BY"</code><code>, 128, 0, NULL, 0) = 128</code>

<code>11:00:57.924487 sendto(9, </code><code>" sort_order ASC, cat_id ASC limit 8"</code><code>, 35, 0, NULL, 0) = 35</code>

<code>11:00:57.924681 epoll_wait(7, {{EPOLLIN, {u32=12048160, u64=12048160}}}, 1024, 500) = 1</code>

<code>11:00:57.964162 recvfrom(9, </code><code>"\1\0\0\1\4B\0\0\2\3def\njiewang300\vjw_category\vjw_category\6cat_id\6cat_id\f?\0\5\0\0\0\2#B\0\0\0F\0\0\3\3def\njiewang300\vjw_category\vjw_category\10cat_name\10"</code><code>, 128, 0, NULL, NULL) = 128</code>

發現是proxy機器在write write read操作的時候,write write和read操作之間,epoll_wait了40ms才read到資料。

結論:

為什麼延遲不高不低正好 40ms 呢?果斷 Google 一下找到了答案。原來這是 TCP 協定中的 Nagle‘s Algorithm 和 TCP Delayed Acknoledgement 共同起作 用所造成的結果。

Nagle’s Algorithm 是為了提高帶寬使用率設計的算法,其做法是合并小的TCP 包為一個,避免了過多的小封包的 TCP 頭所浪費的帶寬。如果開啟了這個算法 (預設),則協定棧會累積資料直到以下兩個條件之一滿足的時候才真正發送出去:

1,積累的資料量到達最大的 TCP Segment Size

2,收到了一個 Ack

TCP Delayed Acknoledgement 也是為了類似的目的被設計出來的,它的作用就 是延遲 Ack 包的發送,使得協定棧有機會合并多個 Ack,提高網絡性能。

如果一個 TCP 連接配接的一端啟用了 Nagle‘s Algorithm,而另一端啟用了 TCP Delayed Ack,而發送的資料包又比較小,則可能會出現這樣的情況:發送端在等 待接收端對上一個packet 的 Ack 才發送目前的 packet,而接收端則正好延遲了 此 Ack 的發送,那麼這個正要被發送的 packet 就會同樣被延遲。當然 Delayed Ack 是有個逾時機制的,而預設的逾時正好就是 40ms。

現代的 TCP/IP 協定棧實作,預設幾乎都啟用了這兩個功能,你可能會想,按我 上面的說法,當協定封包很小的時候,豈不每次都會觸發這個延遲問題?事實不 是那樣的。僅當協定的互動是發送端連續發送兩個 packet,然後立刻 read 的 時候才會出現問題。

為什麼隻有 Write-Write-Read 時才會出問題,維基百科上的有一段僞代碼來介紹 Nagle’s Algorithm:

10

11

<code>if</code> <code>there is </code><code>new</code> <code>data to send</code>

<code>  </code><code>if</code> <code>the window size &gt;= MSS and available data is &gt;= MSS</code>

<code>    </code><code>send complete MSS segment now</code>

<code>  </code><code>else</code>

<code>    </code><code>if</code> <code>there is unconfirmed data still in the pipe</code>

<code>      </code><code>enqueue data in the buffer until an acknowledge is received</code>

<code>    </code><code>else</code>

<code>      </code><code>send data immediately</code>

<code>    </code><code>end </code><code>if</code>

<code>  </code><code>end </code><code>if</code>

<code>end </code><code>if</code>

可以看到,當待發送的資料比 MSS 小的時候(外層的 else 分支),還要再判斷 時候還有未确認的資料。隻有當管道裡還有未确認資料的時候才會進入緩沖區, 等待 Ack。

是以發送端發送的第一個 write 是不會被緩沖起來,而是立刻發送的(進入内層 的else 分支),這時接收端收到對應的資料,但它還期待更多資料才進行處理, 是以不會往回發送資料,是以也沒機會把 Ack 給帶回去,根據Delayed Ack 機制, 這個 Ack 會被 Hold 住。這時發送端發送第二個包,而隊列裡還有未确認的資料 包,是以進入了内層 if 的 then 分支,這個 packet 會被緩沖起來。此時,發 送端在等待接收端的 Ack;接收端則在 Delay 這個 Ack,是以都在等待,直到接 收端 Deplayed Ack 逾時(40ms),此 Ack 被發送回去,發送端緩沖的這個 packet 才會被真正送到接收端,進而繼續下去。

再看我上面的 strace 記錄也能發現端倪,因為設計的一些不足,我沒能做到把 短小的 HTTP Body 連同 HTTP Headers 一起發送出去,而是分開成兩次調用實 現的,之後進入 epoll_wait 等待下一個 Request 被發送過來(相當于阻塞模 型裡直接 read)。正好是 write-write-read 的模式。

那麼 write-read-write-read 會不會出問題呢?維基百科上的解釋是不會:

    “The user-level solution is to avoid write-write-read sequences on sockets. write-read-write-read is fine. write-write-write is fine. But write-write-read is a killer. So, if you can, buffer up your little writes to TCP and send them all at once. Using the standard UNIX I/O package and flushing write before each read usually works.”

我的了解是這樣的:因為第一個 write 不會被緩沖,會立刻到達接收端,如果是 write-read-write-read 模式,此時接收端應該已經得到所有需要的資料以進行 下一步處理。接收端此時處理完後發送結果,同時也就可以把上一個packet 的 Ack 可以和資料一起發送回去,不需要 delay,進而不會導緻任何問題。

解決辦法:

在proxy服務上開啟 TCP_NODELAY選項,這個選項的作用就是禁用 Nagle’s Algorithm。

plus:

tcpdump和strace結果對比:

可以看到strace結果中是proxy機器先write write兩次,然後等待大概40ms。而tcpdump看到的則是proxy機器先write了一次到rds,等待40ms收到rds的ack,再write了剩餘的資料到rds。strace這個結果是正常的,因為strace隻能看到的是系統調用,實際上第二次write被緩沖了(strace是看不到的),等待了40ms後rds的ack到來,這第二次write才被真正的發送出去。這樣strace就和tcpdump的結果一緻了。

本文轉自 leejia1989 51CTO部落格,原文連結:http://blog.51cto.com/leejia/1758560,如需轉載請自行聯系原作者