ネットワーク40 ms遅延問題
問題の背景:私たちの企業ユーザーは、オンラインビジネスを共有するmysqlサービスから独立型mysql rdsに移行するつもりです.エンタープライズユーザー側はまずtestバージョンをrds環境に導入し、サイトの応答時間が3 sから40 sに変わったことを発見しました.phpアプリケーションなので、ユーザーアプリケーションをオンにします. xhprofデバッグ後、一度に1200回以上のmysqlクエリが要求されているのを見て、mysqlに対するクエリの量は非常に大きく、リクエストの時間の90%以上がmysqlに費やされています.ユーザが共有性mysqlを使用するのとrdsを使用するのとの違いは,ユーザruntimeからrdsの間にproxyサービスが1つ増え,proxyサービスを通じてユーザ要求をrdsにエージェントしたことである.問題追跡:1、ユーザーが共有mysqlとrdsを使用する違いのため、proxyサービスです.だから問題をproxyというサービスに位置づけた.2,tcpdumpを用いてproxyマシン(15.212)上でruntime(21.12)からproxy,proxyからrds(144.139)のパケットを捉え、具体的には以下の通りである.
Proxyはruntimeから送られてきたクエリーパケットをproxyに受け取ってruntime ackに確認し、40 msかかりました.この時間は長すぎます.さらにproxyはruntimeから送信されたクエリーパケットを1つから2つのパケットに切断する. 1,idは22953:runtime 102でselect文をproxy 212に送信する.パケット長296 byte時間:23.8775152、id 22954:proxy 212はselect文128 byteの一部をrds 139に送信した.時間コスト(秒):23.877611-23.877515=0.0000963、idは22955:proxy 212回runtime 103のackこのステップ時間コスト(秒):3.97294-23.877611=0.039683(今回の時間コストが大きすぎて、千回の要求は39 s)4、idは22956:rds 139回proxy 212のack.このステップ時間経過(秒):23.91898-23.91294=0.0011045、id 22957:proxy 212は残りのselect文168 byteをrdsに送信する 139.このステップは、3.918415-23.918398=0.0000173、proxyマシン上でstrace追跡を行い、結果は以下の通りです.
発見はproxyマシンがwrite write readで操作しているとき、write writeとread操作の間、epoll_waitは40 msでデータをreadした.結論:なぜ遅延が高くなく低くなくちょうど40 msなのか.思い切ってGoogleが答えを見つけた.これはTCPプロトコルにおけるNagle‘s AlgorithmとTCP Delayed Acknoledgementが共に作用した結果である. Nagle’s Algorithmは帯域幅利用率を向上させるために設計されたアルゴリズムであり,小さなTCPパケットを1つに統合し,過剰な小さなメッセージのTCPヘッダが浪費する帯域幅を回避する方法である.このアルゴリズム(デフォルト)をオンにすると、プロトコルスタックは、次の2つの条件の1つが満たされるまでデータを蓄積します.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にはタイムアウトメカニズムがありますが、デフォルトのタイムアウトはちょうど40 msです. 現代のTCP/IPプロトコルスタックが実現し、デフォルトではほとんどこの2つの機能が有効になっています.私の上の言い方では、プロトコルメッセージが小さいとき、毎回この遅延問題をトリガーしているのではないでしょうか.事実はそうではない.問題は、プロトコルのインタラクションが送信側が2つのpacketを連続的に送信し、すぐにreadする場合にのみ発生します. なぜWrite-Write-Readだけが問題になるのか、ウィキペディアの偽コードがNagle's Algorithmを紹介しています.
送信されるデータがMSSよりも小さい場合(外層のelse分岐)、また再判断される場合に未確認のデータがあることがわかる.パイプラインに未確認データがある場合にのみバッファに入り、Ackを待つ.したがって,送信側から送信される最初のwriteはバッファリングされずにすぐに送信される(内層に入るelse分岐)ので,受信側は対応するデータを受信するが,より多くのデータを期待して処理するので,送信データに戻ることはないので,Ackを持ち帰る機会もなく,Delayed Ackメカニズムによれば,このAckはHoldされる.このとき送信側は2番目のパケットを送信し,キューには未確認のパケットがあるため,内層ifのthenブランチに入り,このpacketはバッファリングされる.このとき、送信側は受信側のAckを待っている.受信側はDelayというAckにあるので、受信側Delplayed Ackがタイムアウト(40 ms)するまで待機しています.このAckは送り返され、送信側バッファのこのpacketは本当に受信側に送られ、継続します.私の上のstraceの記録を見ても手がかりが見つかります.設計のいくつかの不足のため、私は短いHTTP BodyをHTTP Headersと一緒に送信することができませんでした.2回の呼び出しに分けて実現しました.その後、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モードであれば、受信側は次の処理のために必要なすべてのデータを得ているはずです.受信側はこの時点で処理後に結果を送信するとともに,delayを必要とせずに前のpacketのAckをデータとともに送信することができ,何の問題も生じない.
解決策:proxyサービスでTCPを開くNODELAYオプション、このオプションの役割はNagle’s Algorithmを無効にすることです.
plus:
tcpdumpとstraceの結果の比較:
strace結果ではproxyマシンがwrite writeを2回先にし、約40 ms待つことがわかります.tcpdumpが見たのはproxyマシンがrdsに一度writeし、40 msでrdsのackを受け取るのを待ってから、残りのデータをrdsにwriteしたことだ.straceこの結果は正常で、straceはシステム呼び出ししか見えないため、実際には2回目のwriteがバッファリングされ(straceは見えない)、40 ms後にrdsのackが来るのを待っていたが、この2回目のwriteは本格的に送信された.これでstraceはtcpdumpの結果と一致した.
Proxyはruntimeから送られてきたクエリーパケットをproxyに受け取ってruntime ackに確認し、40 msかかりました.この時間は長すぎます.さらにproxyはruntimeから送信されたクエリーパケットを1つから2つのパケットに切断する. 1,idは22953:runtime 102でselect文をproxy 212に送信する.パケット長296 byte時間:23.8775152、id 22954:proxy 212はselect文128 byteの一部をrds 139に送信した.時間コスト(秒):23.877611-23.877515=0.0000963、idは22955:proxy 212回runtime 103のackこのステップ時間コスト(秒):3.97294-23.877611=0.039683(今回の時間コストが大きすぎて、千回の要求は39 s)4、idは22956:rds 139回proxy 212のack.このステップ時間経過(秒):23.91898-23.91294=0.0011045、id 22957:proxy 212は残りのselect文168 byteをrdsに送信する 139.このステップは、3.918415-23.918398=0.0000173、proxyマシン上でstrace追跡を行い、結果は以下の通りです.
11:00:57.923865 epoll_wait(7, {{EPOLLIN, {u32=12025792, u64=12025792}}}, 1024, 500) = 1
11:00:57.923964 recvfrom(8, "\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", 128, 0, NULL, NULL) = 128
11:00:57.924041 recvfrom(8, " sort_order ASC, cat_id ASC limit 8", 128, 0, NULL, NULL) = 35
11:00:57.924102 recvfrom(8, 0xb7b0b3, 93, 0, 0, 0) = -1 EAGAIN (Resource temporarily unavailable)
11:00:57.924214 epoll_wait(7, {{EPOLLOUT, {u32=12048160, u64=12048160}}}, 1024, 500) = 1
11:00:57.924340 sendto(9, "\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", 128, 0, NULL, 0) = 128
11:00:57.924487 sendto(9, " sort_order ASC, cat_id ASC limit 8", 35, 0, NULL, 0) = 35
11:00:57.924681 epoll_wait(7, {{EPOLLIN, {u32=12048160, u64=12048160}}}, 1024, 500) = 1
11:00:57.964162 recvfrom(9, "\1\0\0\1\4B\0\0\2\3def
jiewang300\vjw_category\vjw_category\6cat_id\6cat_id\f?\0\5\0\0\0\2#B\0\0\0F\0\0\3\3def
jiewang300\vjw_category\vjw_category\10cat_name\10", 128, 0, NULL, NULL) = 128
発見はproxyマシンがwrite write readで操作しているとき、write writeとread操作の間、epoll_waitは40 msでデータをreadした.結論:なぜ遅延が高くなく低くなくちょうど40 msなのか.思い切ってGoogleが答えを見つけた.これはTCPプロトコルにおけるNagle‘s AlgorithmとTCP Delayed Acknoledgementが共に作用した結果である. Nagle’s Algorithmは帯域幅利用率を向上させるために設計されたアルゴリズムであり,小さなTCPパケットを1つに統合し,過剰な小さなメッセージのTCPヘッダが浪費する帯域幅を回避する方法である.このアルゴリズム(デフォルト)をオンにすると、プロトコルスタックは、次の2つの条件の1つが満たされるまでデータを蓄積します.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にはタイムアウトメカニズムがありますが、デフォルトのタイムアウトはちょうど40 msです. 現代のTCP/IPプロトコルスタックが実現し、デフォルトではほとんどこの2つの機能が有効になっています.私の上の言い方では、プロトコルメッセージが小さいとき、毎回この遅延問題をトリガーしているのではないでしょうか.事実はそうではない.問題は、プロトコルのインタラクションが送信側が2つのpacketを連続的に送信し、すぐにreadする場合にのみ発生します. なぜWrite-Write-Readだけが問題になるのか、ウィキペディアの偽コードがNagle's Algorithmを紹介しています.
if there is new data to send
if the window size >= MSS and available data is >= MSS
send complete MSS segment now
else
if there is unconfirmed data still in the pipe
enqueue data in the buffer until an acknowledge is received
else
send data immediately
end if
end if
end if
送信されるデータがMSSよりも小さい場合(外層のelse分岐)、また再判断される場合に未確認のデータがあることがわかる.パイプラインに未確認データがある場合にのみバッファに入り、Ackを待つ.したがって,送信側から送信される最初のwriteはバッファリングされずにすぐに送信される(内層に入るelse分岐)ので,受信側は対応するデータを受信するが,より多くのデータを期待して処理するので,送信データに戻ることはないので,Ackを持ち帰る機会もなく,Delayed Ackメカニズムによれば,このAckはHoldされる.このとき送信側は2番目のパケットを送信し,キューには未確認のパケットがあるため,内層ifのthenブランチに入り,このpacketはバッファリングされる.このとき、送信側は受信側のAckを待っている.受信側はDelayというAckにあるので、受信側Delplayed Ackがタイムアウト(40 ms)するまで待機しています.このAckは送り返され、送信側バッファのこのpacketは本当に受信側に送られ、継続します.私の上のstraceの記録を見ても手がかりが見つかります.設計のいくつかの不足のため、私は短いHTTP BodyをHTTP Headersと一緒に送信することができませんでした.2回の呼び出しに分けて実現しました.その後、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モードであれば、受信側は次の処理のために必要なすべてのデータを得ているはずです.受信側はこの時点で処理後に結果を送信するとともに,delayを必要とせずに前のpacketのAckをデータとともに送信することができ,何の問題も生じない.
解決策:proxyサービスでTCPを開くNODELAYオプション、このオプションの役割はNagle’s Algorithmを無効にすることです.
plus:
tcpdumpとstraceの結果の比較:
strace結果ではproxyマシンがwrite writeを2回先にし、約40 ms待つことがわかります.tcpdumpが見たのはproxyマシンがrdsに一度writeし、40 msでrdsのackを受け取るのを待ってから、残りのデータをrdsにwriteしたことだ.straceこの結果は正常で、straceはシステム呼び出ししか見えないため、実際には2回目のwriteがバッファリングされ(straceは見えない)、40 ms後にrdsのackが来るのを待っていたが、この2回目のwriteは本格的に送信された.これでstraceはtcpdumpの結果と一致した.