cURL MultiインターフェースでHTTP Pipeliningリクエストの送信

cURLC APIマニュアルを読んでいたらCURLMOPT_PIPELININGというおもしろそうなオプションを見つけた。これはlibcurl 7.16.0 より加わった並列実行用Multiインターフェースのオプションで、設定することでHTTP Pipeliningなリクエストが送信できるようになる。HTTP Pipeliningとは個々のレスポンスを待つことなく複数のリクエストを投げることを意味するHTTP/1.1よりサポートされた通信パフォーマンス向上のためのテクニックである。

通常N個のリクエストを処理する際はN個ソケットがオープンされOPEN → REQUEST → RESPONSE → CLOSEなサイクルがN回行われる。Multiインターフェースによる並列処理の場合はOPEN → REQUEST → RESPONSE → CLOSEが並列に行われる。 またこれがkeep-aliveな接続であればOPEN → (REQUEST → RESPONSE) x N → CLOSEのようにクローズされるまで1ソケットが再利用される。 そしてHTTP Pipeliningはkeep-aliveな接続で使うテクニックであり、これが有効な場合は1ソケットオープン後にOPEN → (REQUEST x N) → (RESPONSE x N) → CLOSEのようにリクエストN個をレスポンスを待つことなく1ソケットに書き込むことができる。 まとめて送信される分パケット効率がアップし全体的なネットワークを流れるパケットの数を減らすことができ、 さらにまとめてリクエスト送信するので高レイテンシーなネットワークにおいては速度面で効果的といえる。 see also 「Mozilla HTTP/1.1 パイプライン化 FAQ」。

(1) NOT keep-alive, single         (OPEN → REQUEST → RESPONSE → CLOSE) x N
(2) NOT keep-alive, multi          (OPEN → REQUEST → RESPONSE → CLOSE) をN並列で行う
(3) keep-alive                     OPEN → (REQUEST → RESPONSE) x N → CLOSE
(4) keep-alive, Pipelining         OPEN → (REQUEST x N) → (RESPONSE x N) → CLOSE

というわけでいつものようにテストプログラムを作ってMultiインターフェースによる並列処理をHTTP Pipeliningありとなしで実行してみる。
(環境 libcurl-7.19.5、httpd-2.2.2 on Debian-5.0.1 )

テストツールとそのコンパイル

cURL CAPIのMultiインターフェースを使って複数URLからfetchしてくるツールを作成した。

http://github.com/yokawasa/any/blob/master/libcurl/multi_fetch.cpp

標準的なcurl Multiインターフェースを使ったコードであるがポイントは次の部分。各リクエスト用のCURLハンドルでソケットの送信待ち時間を最小限にするCURLOPT_TCP_NODELAYオプション(詳しくは「Linuxにおけるソケット機能の向上」を参照ください)と挙動把握のためにlibcurlのINFO情報を出力するCURLOPT_VERBOSEオプションを有効にしている。またHTTP Pipeliningモード指定のときにMulti用ハンドルでCURLMOPT_PIPELININGオプションを有効にしている。 以下該当箇所のコード断片。

...
for(vector<string>::iterator it=urls.begin();
            it!=urls.end(); ++it) {
    curl_easy_setopt(c_handles[i], CURLOPT_URL, (*it).c_str());
    curl_easy_setopt(c_handles[i], CURLOPT_TCP_NODELAY, 1L);
    curl_easy_setopt(c_handles[i], CURLOPT_VERBOSE, 1L);
    curl_multi_add_handle(m_handle, c_handles[i]);
    i++;
}
if (pipelining)
    curl_multi_setopt(m_handle, CURLMOPT_PIPELINING, 1L);
...

上記URLのソースコードを取得して次のようにコンパイルを行う。

g++ multi_fetch.cpp -o multi_fetch -I/usr/include -L/usr/lib -lcurl

使い方は次のように複数URLの書かれたファイルを指定する。-pオプションを加えてやることでHTTP Pipeliningモードでリクエスト送信を行う。 これはオプショナル。

Usage: ./multi_fetch <options>
Options: -f file  URL list file
         -p       HTTP pipelining mode (optional)
 
例) urls.txtに書かれた複数URLからHTTP Pipeliningモードでfetch
$ ./multi_fetch -f urls.txt -p

NON HTTP Pipelining リクエスト送信

テスト用にホストfooとWebサーバ(apache)のあるホストbarを用意する。まずはHTTP Pipeliningオプションをはずした通常のmultiインターフェースによる並列実行を行う。リクエスト用CURLハンドルでCURLOPT_VERBOSEオプションが有効になっているので実行結果にlibcurlのINFO 情報も一緒に出力される。出力内容を全てここに貼り付けるには量が多すぎるので内容を簡略化してHTTP接続部分と各リクエストとそのレスポンスの最初の一行だけに絞る。

$ cat urls.txt
http://bar.yk55.com/test1.html
http://bar.yk55.com/test2.html
http://bar.yk55.com/test3.html
http://bar.yk55.com/test4.html

$ ./multi_fetch -f urls.txt  2>&1 |tee /tmp/nonpipelined.out
[出力結果]
$ cat /tmp/nonpipelined.out

* About to connect() to bar.yk55.com port 80 (#0)          
* About to connect() to bar.yk55.com port 80 (#1)
* About to connect() to bar.yk55.com port 80 (#2)
* About to connect() to bar.yk55.com port 80 (#3)
* Connected to bar.yk55.com (192.168.1.5) port 80 (#0)
* Connected to bar.yk55.com (192.168.1.5) port 80 (#1)
* Connected to bar.yk55.com (192.168.1.5) port 80 (#2)
* Connected to bar.yk55.com (192.168.1.5) port 80 (#3)
> GET /test1.html HTTP/1.1                                      
> GET /test2.html HTTP/1.1                                      
> GET /test3.html HTTP/1.1                                      
> GET /test4.html HTTP/1.1                                      
< HTTP/1.1 200 OK                                                  
* Connection #1 to host bar.yk55.com left intact          
< HTTP/1.1 200 OK                                                  
* Connection #2 to host bar.yk55.com left intact          
< HTTP/1.1 200 OK                                                  
* Connection #3 to host bar.yk55.com left intact          
< HTTP/1.1 200 OK                                                  
* Connection #0 to host bar.yk55.com left intact

これを見ると並列にConnection #[0-3]の4つのソケットがオープンしてそれぞれにリクエスト、レスポンスが書き出され終了していることが分かる。 「(2)NOT keep-alive, multi」のパターンになっているといえる。想定どおり。

HTTP Pipeliningリクエスト送信

次に前テストと同様にホストfooからホストbar(Webサーバ)の4ファイル対してにHTTP Pipeliningなリクエストを送信してみる。

$ cat urls.txt
http://bar.yk55.com/test1.html
http://bar.yk55.com/test2.html
http://bar.yk55.com/test3.html
http://bar.yk55.com/test4.html
 
$ ./multi_fetch -f urls.txt -p  2>&1 |tee /tmp/pipelined.out
[出力結果]
$ cat /tmp/pipelined.out

* About to connect() to bar.yk55.com port 80 (#0)
* Re-using existing connection! (#0) with host bar.yk55.com
* Re-using existing connection! (#0) with host bar.yk55.com
* Re-using existing connection! (#0) with host bar.yk55.com
* Connected to bar.yk55.com (192.168.1.5) port 80 (#0)
> GET /test1.html HTTP/1.1                                        
< HTTP/1.1 200 OK                                                    
> GET /test2.html HTTP/1.1                                        
> GET /test3.html HTTP/1.1                                        
> GET /test4.html HTTP/1.1                                        
< HTTP/1.1 200 OK                                                    
< HTTP/1.1 200 OK                                                    
< HTTP/1.1 200 OK                                                    
* Connection #0 to host bar.yk55.com left intact

出力結果から、1つのソケットオープン(Connection #0)後に他のリクエスト達はConnectoin#0を 再利用しているのが分かる。 またtest1.htmlをGETするためのリクエスト送信後に「HTTP/1.1 200 OK….」とレスポンスを受け、次にtest2.html~test4.html GETの3リクエストがレスポンスを待つことなく連続で送られ、その後それらのレスポンスを受けている。 「(4) keep-alive, Pipelining」のパターンになると予想していたのだが1つ目のリクエスト(REQUEST-a)だけがPipeliningではない。 試しにリクエスト数を増やしてみても同じ、1つ目のリクエストがPipeliningではなく、2番目以降のリクエストはPipeliningになっている。 今一理由がわからない。 ひょっとして実際の処理とは別にログ出力に問題があるのではないかと思いtcpdumpで実際のTCPパケットのやりとり確認してみる。

$ sudo tcpdump -lX -s 1024 -i eth0 port 80 |tee /tmp/tcpdump.txt
$ cat /tmp/tcpdump.txt

*** SYN: Socketオープン
01:27:14.155974 IP foo.43242 > bar.http: S 3856091341:3856091341(0) win 5840 <mss 1460,sackOK,timestamp 31004352 0,nop,wscale 6>
01:27:14.158135 IP bar.http > foo.43242: S 2366776094:2366776094(0) ack 3856091342 win 5792 <mss 1460,sackOK,timestamp 224898337 31004352,nop,wscale 2>
01:27:14.158197 IP foo.43242 > bar.http: . ack 1 win 92 <nop,nop,timestamp 31004352 224898337>

*** PUSH: 1stリクエスト
01:27:14.156012 IP foo.43242 > bar.http: P 1:63(62) ack 1 win 92 <nop,nop,timestamp 31004352 224898337>
01:27:14.156080 IP bar.http > foo.43242: . ack 63 win 1448 <nop,nop,timestamp 224898337 31004352>

*** PUSH: 1stレスポンス
01:27:14.158768 IP bar.http > foo.43242: P 1:198(197) ack 63 win 1448 <nop,nop,timestamp 224898340 31004352>
01:27:14.158930 IP foo.43242 > bar.http: . ack 198 win 108 <nop,nop,timestamp 31004353 224898340>

*** PUSH: 2nd, 3rd, 4thリクエスト
01:27:14.159183 IP foo.43242 > bar.http: P 63:125(62) ack 198 win 108 <nop,nop,timestamp 31004353 224898340>
01:27:14.159277 IP foo.43242 > bar.http: P 125:187(62) ack 198 win 108 <nop,nop,timestamp 31004353 224898340>
01:27:14.159361 IP foo.43242 > bar.http: P 187:249(62) ack 198 win 108 <nop,nop,timestamp 31004353 224898340>

*** PUSH:  2nd, 3rd, 4thまとめてレスポンス
01:27:14.163514 IP bar.http > foo.43242: P 198:789(591) ack 249 win 1448 <nop,nop,timestamp 224898344 31004353>

*** FIN:  Socket クローズ
01:27:14.164128 IP foo.43242 > bar.http: F 249:249(0) ack 789 win 127 <nop,nop,timestamp 31004354 224898344>
01:27:14.164634 IP bar.http > foo.43242: F 789:789(0) ack 250 win 1448 <nop,nop,timestamp 224898345 31004354>
01:27:14.164780 IP foo.43242 > bar.http: . ack 790 win 127 <nop,nop,timestamp 31004354 224898345>

tcpdumpの結果も変わらず同じ。 ログを見る限りオープン処理(SYNフラグの出力部分、いわゆる3way handshake)が行われた後、1つ目のリクエスト直後にレスポンスを受け、その後2番目以降のリクエストはレスポンスを待つことなくソケット書き込み、つまりPipeliningリクエスト送受信が行われている。 どうして1つ目だけがダメで、2つ目以降からうまくいくのだろう?? (T . T)

細かくlibcurlのコードを追えばよいのだろうがこれ以上調査する気持ちにならないので取り敢えずここで終えておく。 続きはいつか。

おわり。

No related posts.

Posted in: Programming

Tags: , , , , , , , ,



5 Comments

rssComments RSS transmitTrackBack Identifier URI


>1つ目のリクエストがPipeliningではなく、2番目以降のリクエストはPipeliningになっている。 今一理由がわからない。

1つ目のレスポンスでサーバが「俺Pipelining対応してるよ」って答えるのを確認してから、2番目以降で使ってるからだと思う

Mozilla HTTP/1.1 パイプライン化 FAQから

>Q:パイプライン化したリクエストをいつすべきですか?
>A:(略)新しいコネクションの上でもまた、パイプライン化したリクエストをすべきではありません。なぜなら、相手のサーバ (もしくはプロキシ) が HTTP/1.1 をサポートしているかどうかまだわからないからです。

Comment by bero on May 10, 2010 5:30 pm


コメントありがとうございます。 なるほど。 確かにしっかりそのように書かれていますね ^^ > 「Mozilla HTTP/1.1 パイプライン化 FAQ」

Comment by yoichi on May 26, 2010 8:41 am


What theme are you utilizing on your site ? I really like the layout. Thank you for the posting.

Comment by buy phentermine 37.5 on November 21, 2010 9:10 am


I loved what youve done here. The design is elegant, your content classy. Yet, youve got an edginess to what youre offering here. Ill definitely come back for more if you keep this up. Dont lose hope if not too many people see your vision, know youve got a fan right here who values what youve got to say and the way youve presented yourself. Good on you!

Comment by phentermine on November 21, 2010 9:42 am


The topic cURL MultiインターフェースでHTTP Pipeliningリクエストの送信 – Yoichi Kawasaki blog is totally new for me, but it´s very interesting. I will read more about this topic and make me my own opinion. Thanks, Wein Muller

Comment by Wein Muller on December 3, 2010 10:10 am

addLeave a comment






+ 7 = eleven