【転載】ネットワークプログラミングにおけるNagleアルゴリズムとDelayed ACKのテスト

7988 ワード


Nagleアルゴリズム
ネットワークに小さなパッケージが充填されないようにすることで,ネットワークの利用率を向上させることができるという趣旨である.しかしNagleアルゴリズムが
delayed ACK
悲劇が起こった.Delayed ACKの本意は,TCP性能を向上させるために,応答データにACKを伝送し,同時に回避することである.
愚かな窓症候群
また、1つのACKが複数のセグメントを確認してコストを節約することもできる.
悲劇は,一方がデータを送信して他方の応答を待つと仮定し,プロトコル上はヘッダとデータに分けられ,送信時に不幸にもwrite-writeを選択し,その後read,すなわち先にヘッダを送信し,データを送信し,最後に応答を待つ.送信側の疑似コードは次のとおりです.
write(head); 
write(body); 
read(response);

受信側の処理コードは、次のようになります.
read(request); 
process(request); 
write(response);

ここではheadとbodyが比較的小さいと仮定し、デフォルトでNagleアルゴリズムが有効になり、最初の送信である場合、Nagleアルゴリズムによれば、確認待ちのセグメントがないため、最初のセグメントheadはすぐに送信することができる.受信側はheadを受信するが、パケットが不完全で、bodyがACKに達するのを待ち続け、遅延する.送信側がbodyに書き込み続けると、Nagleアルゴリズムが機能し、headはまだACKされていないため、bodyは送信を遅らせる.これにより、送信側と受信側が両方とも相手がデータを送信するのを待っている現象が発生し、送信側は受信側ACK headを待ってbodyを送信し続け、受信側はbodyを待ってACKを遅延し、悲劇的な言葉がない.この場合、一端がタイムアウトしてデータが送信されるのを待つしかありません.
Nagleアルゴリズムとdelayed ACKの影響だけに、このようなwrite-write-readのプログラミング方式は、なぜ自分が書いたネットワークプログラムの性能がそんなに悪いのかを議論する多くのウェブサイトをもたらした.そして多くの人が投稿でNagleアルゴリズムを無効にすることをお勧めします.TCPを設定します.NODELAYがtrueの場合、Nagleアルゴリズムは無効になります.しかし、これは本当に問題を解決する唯一の方法と最善の方法ですか?
実は問題はNagleアルゴリズムにあるのではなく、write-write-readというアプリケーションのプログラミングにある.Nagleアルゴリズムの無効化は一時的に問題を解決することができるが、Nagleアルゴリズムの無効化も大きなデメリットをもたらし、ネットワークには小さなパッケージが詰まっており、ネットワークの利用率が低下し、極端な場合、大量の小さなパッケージがネットワークの混雑や崩壊を招く.だから、禁止しないほうがいいのか、禁止しないほうがいいのか、後でどんな場合にNagleアルゴリズムを無効にする必要があるのかを話します.ほとんどのアプリケーションでは、一般的に連続的なリクエスト応答モデルであり、リクエストが同時に応答すると、リクエストパケットのACKは応答とともに送信されるまで遅延することができます.この場合、write-write-read形式の呼び出しを避けるだけで遅延現象を回避することができます.writevを利用して集約したり、headとbodyを一緒に書いたりして、readします.Write-read-write-readの形式になって呼び出すと、Nagleアルゴリズムを無効にすることなく遅延しないことができます.
次に、実際のコードテストを行い、議論を終了します.この例は簡単で、クライアントは1行のデータをサーバに送信し、サーバは簡単にこの行のデータを返します.クライアントが送信するときは、2回に分けて送信するか、1回に送信するかを選択できます.2回に分けるとwrite-write-read、1回にwrite-read-write-readとなり、2つの形式での遅延の違いを見ることができます.
Windowsで次のコードをテストするには、クライアントとサーバを2台のマシンに分けなければなりません.winsockのloopback接続の処理が異なるようです.
 
サーバのソース:
package  net.fnil.nagle; 

import  java.io.BufferedReader; 
import  java.io.InputStream; 
import  java.io.InputStreamReader; 
import  java.io.OutputStream; 
import  java.net.InetSocketAddress; 
import  java.net.ServerSocket; 
import  java.net.Socket; 


public   class  Server { 
     public   static   void  main(String[] args)  throws  Exception { 
        ServerSocket serverSocket =  new  ServerSocket(); 
        serverSocket.bind( new  InetSocketAddress(8000)); 
        System.out.println("Server startup at 8000"); 
         for  (;;) { 
            Socket socket = serverSocket.accept(); 
            InputStream in = socket.getInputStream(); 
            OutputStream out = socket.getOutputStream(); 

             while  ( true ) { 
                 try  { 
                    BufferedReader reader =  new  BufferedReader( new  InputStreamReader(in)); 
                    String line = reader.readLine(); 
                    out.write((line + "\r
").getBytes()); } catch (Exception e) { break ; } } } } }

サービス側はローカル8000ポートにバインドされ、接続を傍受し、接続されると1行のデータの読み取りをブロックし、クライアントにデータを返します.
クライアントコード:
package  net.fnil.nagle; 

import  java.io.BufferedReader; 
import  java.io.InputStream; 
import  java.io.InputStreamReader; 
import  java.io.OutputStream; 
import  java.net.InetSocketAddress; 
import  java.net.Socket; 


public   class  Client { 

     public   static   void  main(String[] args)  throws  Exception { 
         //       head body 
         boolean  writeSplit =  false ; 
        String host = "localhost"; 
         if  (args.length >= 1) { 
            host = args[0]; 
        } 
         if  (args.length >= 2) { 
            writeSplit = Boolean.valueOf(args[1]); 
        } 

        System.out.println("WriteSplit:" + writeSplit); 

        Socket socket =  new  Socket(); 

        socket.connect( new  InetSocketAddress(host, 8000)); 
        InputStream in = socket.getInputStream(); 
        OutputStream out = socket.getOutputStream(); 

        BufferedReader reader =  new  BufferedReader( new  InputStreamReader(in)); 

        String head = "hello "; 
        String body = "world\r
"; for ( int i = 0; i < 10; i++) { long label = System.currentTimeMillis(); if (writeSplit) { out.write(head.getBytes()); out.write(body.getBytes()); } else { out.write((head + body).getBytes()); } String line = reader.readLine(); System.out.println("RTT:" + (System.currentTimeMillis() - label) + " ,receive:" + line); } in.close(); out.close(); socket.close(); } }

      
クライアントはwriteSplit変数でheadとbodyを別々に書くかどうかを制御し、trueの場合はheadを書いてからbodyを書く.そうしないとheadにbodyを加えて一度に書き込む.クライアントのロジックも簡単で、サーバーに接続して、1行を送信して、応答を待ってRTTを印刷して、10回循環して最後に接続を閉じます.
まず、writeSplitをtrueに設定します.つまり、2回に分けて1行を書きます.私の本機でテストした結果、私の機械はubuntu 11.10です.
WriteSplit: true 
RTT:8 ,receive:hello world 
RTT:40 ,receive:hello world 
RTT:40 ,receive:hello world 
RTT:40 ,receive:hello world 
RTT:39 ,receive:hello world 
RTT:40 ,receive:hello world 
RTT:40 ,receive:hello world 
RTT:40 ,receive:hello world 
RTT:40 ,receive:hello world 
RTT:40 ,receive:hello world 

応答を要求するたびに40 msの間隔が最初を除いて見られる.linuxのdelayed ackは200 msではなく40 msです.初めての即刻ACKは、linuxのquickack modeと関係があるようですが、ここでは特に詳しくありませんので、詳しい方教えてください.
      
次に、writeSplitをtrueに設定しますが、クライアントはNagleアルゴリズムを無効にします.つまり、クライアントコードはconnectの前に行を追加します.
        Socket socket =  new  Socket(); 
        socket.setTcpNoDelay( true ); 
        socket.connect( new  InetSocketAddress(host, 8000));

テストを実行します.
WriteSplit: true 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:1 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 

このときは正常になり、ほとんどのRTT時間は1ミリ秒以下です.やはりNagleアルゴリズムを無効にすることで遅延問題を解決できる.
Nagleアルゴリズムを無効にせずにwriteSplitをfalseに設定した場合、headとbodyを一度に書き込み、テストを再実行します(settcpNoDelay行を削除します):
WriteSplit: false 
RTT:7 ,receive:hello world 
RTT:1 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 
RTT:0 ,receive:hello world 

結果はNagleアルゴリズムの無効化と同様である.それなら、Nagleアルゴリズムを無効にしなければならない理由は何ですか?私を通して
xmemcached
の圧力測定でのテストでは、Nagleアルゴリズムを有効にすることは、小さなデータへのアクセスにおいても一定の効率的な利点があり、memcachedプロトコル自体は連続的な要求応答のモデルである.上記のテストはwindowsを走るとRTTが最大200 ms以上になることがわかり、winsockのdelayed ackタイムアウトが200 msであることがわかります.
最後の質問ですが、Nagleアルゴリズムを無効にするのはどのような場合ですか?このような連続的な要求応答モデルではなく、多くの小さなデータをリアルタイムで一方向に送信する必要がある場合や、要求が間隔を置いている場合は、Nagleアルゴリズムを無効にして応答性を向上させる必要があります.最も明らかな例はtelnetアプリケーションで、あなたはいつも1行のデータを入力した後にすぐにサーバーに送信することを望んで、それからすぐに応答を見て、私が多くのコマンドを連続的に入力したり、200 ms待ってから応答を見ることができるというわけではありません.
上は私のNagleアルゴリズムとdelayed ACKに対する理解とテストで、間違ったところがあれば教えてください.