HTTP/1.1 の同時接続数について

が盛り上がってたので,机上の話だけですが,いまさら書いてみます。

RFC (2616) での記述

Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. A proxy SHOULD use up to 2*N connections to another server or proxy, where N is the number of simultaneously active users. These guidelines are intended to improve HTTP response times and avoid congestion.

Hypertext Transfer Protocol - HTTP/1.1 - 8.1.4 Practical Considerations

持続的接続を使用するクライアントは、サーバへ維持する同時接続の数を制限すべきである。シングルユーザクライアントは、どんなサーバやプロクシへも 2 接続より多く維持すべきではない。プロクシは、N は同時のアクティブユーザの数として、別のサーバやプロクシへの接続使用数を多くても 2*N までとすべきである。これらのガイドラインは HTTP レスポンスタイムを改善し、ネットワークの混雑を避けようとするものである。

ハイパーテキスト転送プロトコル - HTTP/1.1 - 8.1.4 現実的な考察

HTTP/1.0 のころは同時接続数 4 といわれていたのに*1,HTTP/1.1 では 2 接続までって劣化してない?

と思いますが,そんなことはありません。さきほどの文書で「持続的接続を」「サーバへ維持する同時接続の数を」という表現があったように,HTTP/1.1 では Keep-Alive や Pipelining*2などの高速化技術が盛り込まれています。ですから 2 接続でも十分でしょ,ということなのだと思います。

Keep-Alive や Pipelining とは何か

Keep-Alive や Pipelining についてわかりやすい説明をどこかで見かけた気がするのですが,うまく探せなかったので自分なりのたとえ話を書いてみます。

Keep-Alive も Pipelining もない HTTP/1.0 でのハンバーガー(セット)の注文は以下のようになります。

(カウンターに近づく)
C: こんにちは
S: こんにちは
C: ハンバーガーください
S: ハンバーガーどうぞ
S: さようなら
C: さようなら
(カウンターからはなれる)

(カウンターに近づく; 以下略)
C: こんにちは
S: こんにちは
C: ポテトください
S: ポテトどうぞ
S: さようなら
C: さようなら

C: こんにちは
S: こんにちは
C: ドリンクください
S: ドリンクどうぞ
S: さようなら
C: さようなら

はい面倒くさいですね。わざわざ単品で買ってるから損してそうだし :)

これにたいして Keep-Alive は以下のようになります。

(カウンターに近づく)
C: こんにちは
S: こんにちは

C: ハンバーガーください
S: ハンバーガーどうぞ

C: ポテトください
S: ポテトどうぞ

C: ドリンクください
S: ドリンクどうぞ

C: さようなら
S: さようなら
(カウンターからはなれる)

たくさんの商品(コンテンツ)を取得するのにいちいち「こんにちは」「さようなら」というのがばかにならないので,最初にカウンターに近づいたあとは,カウンターに張り付きっぱなしにしてます。これだけでだいぶん早くなります。

いっぽう,Pipelining の場合,以下のようになります。

C: こんにちは
S: こんにちは

C: ハンバーガーください
C: ポテトください
C: ドリンクください

S: ハンバーガーどうぞ
S: ポテトどうぞ
S: ドリンクどうぞ

C: さようなら
S: さようなら

ご覧の通り,Pipelining は Keep-Alive していることが前提になっています。さて,行数だけ見ると Keep-Alive の場合とたいしてかわらない気もしますが,注文をまとめて発行しているところが異なります。注文をまとめておくと,バックヤードではハンバーガーとポテトとドリンクを並行して用意しているかもしれません。なので早くなる可能性があります。Keep-Alive の例ではハンバーガーを用意できるまで次の注文がわからないので,次の注文を聞いてからポテトを揚げる必要があり非効率的です。


HTTP パイプラインについてハンバーガーの注文で例えましたが,より生のプロトコルに近い実例が 404 Blog Not Found:HTTPサーバーのパイプライン対応 に載っています。また,考慮すべきいくつかのポイントについては HTTP Pipelining FAQ | MDN が詳しいです。Polipo のしくみを解説してある インターネットを過激に加速するアプリdolipoの種明かし− @IT も参考になります。

余談: Polipo の Poor Man's Multiplexing (PMM) とは何か

むかし Polipo のドキュメントを読んだときには PMM のしくみがよくわからなかったのですが(分割並行ダウンロードしてるだけじゃねーのと思っていた),先にあげた Mozilla のドキュメントを読んで,なんとなく腑に落ちました。

そのうえ、最初のリクエストが完了するのに長い時間がかかると、長いパイプライン化は実際にユーザに知覚されてしまうほどの遅れを引き起こします。

HTTP Pipelining FAQ | MDN

さきほどのハンバーガーショップの例でいうと,ハンバーガーの準備に時間がかかった場合,ドキュメント全体ではそこが律速になってしまい,コンテンツがブラウザに表示されるのが結局早くなりません。そこで Range ヘッダ等を使用した 部分的 GET併せて使うことで,細切れなオブジェクトのリクエストの Pipelining にしているのではないでしょうか。

つまり,以下のようなシーケンスです(Keep-Alive と Pipelining が成立している前提)。

C: こんにちは
S: こんにちは

C: バンズください
C: ポテトください
C: ピクルスください
C: ドリンクください
C: ハンバーグください
C: レタスください
C: 氷ください
C: チーズください

S: バンズどうぞ
S: ポテトどうぞ
S: ピクルスどうぞ
S: ドリンクどうぞ
S: ハンバーグどうぞ
S: レタスどうぞ
S: 氷どうぞ
S: チーズどうぞ

C: さようなら
S: さようなら

C: (ハンバーガーとポテトとドリンク,揃った!)

ハンバーガー全体をリクエストすると時間がかかるところを,いくつかのパーツに分割することでレスポンスの遅延を防いでいます。もちろんこのようにしてもバンズの用意に時間がかかった場合にそこが律速になってしまうのですが,ハンバーガー単品よりは早く準備できるでしょうし,他のコンテンツ(の断片)のリクエストとランダマイズすることで,統計的には全体として早く準備できるでしょう。

余談: 理想と現実と

しかし残念ながら Pipelining をきちんとサポートしていないサーバ構成・設定・実装があるため(出典不明),たとえば Firefox ではデフォルトで Pipeline 機能が off になっています。

しかし、接続をまとめる、すなわちpersistent connectionの方は事実上どのWebサイトも対応しているのだが、リクエストのパイプラインの方は、一部Webサイトがまだ未対応のようなのだ。(……中略……)、フロントエンド、すなわちクライアントと直接接続しているサーバー(たいていreverse proxy)がリクエストをまとめなければならないが、実際にコンテンツを持っているサーバーから見れば、このリクエストはまとまって見えるとは限らない。
(……中略……)
Firefoxがパイプラインをデフォルトではoffにしているのは、こういった事情があるのだと思われる。
(……中略……)
パイプラインのご利益は、むしろサーバーの方にこそあるので、もっと普及してほしい技術なのだが。

ちなみに、Apacheなど通常のWebサーバー単位では、すでにこの技術に対応している。設定変更などは不要である。むしろ問題はリクナビNEXTなど、高負荷で複数のサーバーから構成されているWebサイトが全体としてこれをサポートしているかだろう。

404 Blog Not Found:HTTPサーバーのパイプライン対応

紺屋の……

実のところ,私は Firefox 3.0.3 on Ubuntu 8.04 でのデフォルト同時接続数設定で遅いなーと思ったためしはありません。

about:config で設定をみてみましょう。

network.http.max-connections 30
network.http.max-connections-per-server 15
network.http.max-persistent-connections-per-server 6

……あれあれあれ?

423377 - (wedidntstartthefire) Change max-persistent-connections-per-server to 6.

http 接続数の上限が緩和されました。

 // http 接続の最大数。
-pref("network.http.max-connections", 24);
+pref("network.http.max-connections", 30);

 // ホスト毎の http 接続の最大数。
 // プロキシサーバが有効になっている場合サーバはプロキシサーバ、
 // そうでない場合はサーバは http オリジナルサーバをあらわす。
-pref("network.http.max-connections-per-server", 8);
+pref("network.http.max-connections-per-server", 15);
 
 // network.http.keep-alive が true でプロキシ経由の接続ではない場合、
 // アクティブな接続持続数が max-persistent-connections-per-server より
 // 少ない場合にのみ新規接続が試みられる。.
-pref("network.http.max-persistent-connections-per-server", 2);
+pref("network.http.max-persistent-connections-per-server", 6);
http://level.s69.xrea.com/mozilla/index.cgi?id=20080319_max-persistent-connections-per-server

な,なんだっ(AA略

そもそも RFC をちゃんと読解すると(& RFC の SHOULD とは)

動揺したのでもうちょっといろんな意見を見てみます。

(……略……)これは、「永続的なコネクションの仕組みを使うときに今まで通りたくさん繋がれると折角の仕組みが逆効果だからやめてくれ」っていう記述であって、したがって "close" なコネクションなら HTTP/1.0 と同様に無制限なんじゃないのかな。

権威は正しく使おう - 某日記(中期)権威は正しく使おう - 某日記(中期)

なるほど。同時接続数じたいが問題というより,にぎりっぱなしの持続性接続の数が増えることが問題だ,と。Firefox の設定で max-connections-per-servermax-persistent-connections-per-server の2つがあるのはそういう意味もあるわけですね。

だとしても max-persistent-connections-per-server = 6 っていうのは RFC「違反」なような……

SHOULD だよ。MUST じゃない。これを「明確に定められている」というのはどうなのかなぁ。SHOULD ってのは制限を逸脱したときに発生しうる問題をちゃーんと理解した上で、そうするだけの妥当な理由があるのならば無視してもかまわない、ってことであって、けっして強制はしていないし、破ってもただちに違反とはいえない。

RFC 詭弁術 - どさにっき

なるほど。たとえば RFC による推奨値が有名無実化している現況を斟酌しコストとベネフィットを天秤にかけて 6 でもいいのではないか,と Firefox 開発チームが決断したのであれば問題ないのではないか,ということですね*3

RFC における SHOULD や MUST については,下記のリソースが参考になります。

とここまで書くと「だよねだよね MUST じゃないから守る必要なんかないじゃんね」という論調になりがちなので,先ほど引用したページから別の箇所を引用しておきます。

なんとなく誤解されてそうな気がするので補足しておく。MUST NOT じゃないならやってもいいじゃん、といってるわけではない。日本語で「べきである」というとなんだか努力目標ぐらいの弱い意味にとらえられるかもしれないけど、英語の should はそれよりも意味が強く、日本語では「しなければならない」に近いぐらいの意味になる。要するに、RFC で SHOULD NOT だったら、基本的にはやっちゃダメと解するのが妥当。それでもどうしても必要で、万人を納得させられるような極めて合理的な理由がある場合にかぎり逸脱してもかまわない、ということ。強制はしていないし、破ってもただちに違反とはいえないけれど、原則は禁止、である。

RFC 詭弁術 - どさにっき


2008-10-27 追記: やや自分本位に引用をしてしまいましたが,先に上げた 塩崎さんの引用もやまやさんの引用も,元はそれぞれの文脈にもとづいた意見の一部です。くわしくはそれぞれの引用先について全文を読んでください。

増やせばいいってものでもない?

また,下記のような報告もあります。

この設定では、画像の多いサイトで表示されない画像、所謂「歯抜け」が減りました。特にnetwork.http.max-persistent-connections-per-server を増減してみるとそれが顕著で、増やすと歯抜けが出る頻度が増えて、負荷も増えてる様な気がしました。それ以外の項目については、値を変更してもあまり変化は感じられなかったり。

恐らく個々の環境が大いに関係してくる話だと思いますが、なんでもかんでも増やすせばいいってもんじゃないって分かったのはよかったかも。

Firefox 3だとサーバへの持続的接続数は減らした方がいいのかも - 鳥獣保護区

個人の環境(PC の能力やネットワークの能力)やアクセス先によってかわる可能性が高い*4でしょうが,このような(主観的ですが)現象もあるということで。

Fasterfox の問題点

  • RFC の推奨値を越える同時接続数を設定できる
  • (かつて)(アホな)先読み機構が存在した

前者はここまでの議論から「まあしゃあないか」ということにしましょう。後者についてですが,

del.icio.us開いたら片っ端から先読みし始めるし、既読のページにもかかわらず先読みするし節操が無い。読みもしないページを片っ端からダウンロードすることが有効活用と言えるのか?

それに比べるとFasterfoxはひどいっていうか知性が足りないように思う。

http://la.ma.la/blog/diary_200510171428.htm

ということで,「節操のない」先読み機構が存在したそうです。

ここで先読み機構の弁護をしておくと,それなりに紳士的かつインテリジェントに設計してある先読み機構があるとすればーーたとえば,同一ドメインにたいして先読み数を絞るとか,あらかじめ HEAD だけして先読み先を絞るとか,ユーザの操作や嗜好を解析して先読み先を決定するとかーーサーバにそんなに迷惑をかけるものではありません。またそこまでインテリジェントではなくても,同一接続先に対する同時接続数が少なければ,かつ,持続接続をうまく利用すれば問題にはならないでしょう*5クライアントサイドで自律的に先読みを行うと,ユーザの意図に反した災害が起こり得ます。後述します。

また同時接続数自体が多かったとしても,先読みを行わない場合,画像などの埋め込みリソースが大して多くないコンテンツではリクエストストームは一瞬でおさまるでしょう。

問題は,この両者が併用されたとき,ひとつひとつのコンテンツがたいしたものでないサイトに対しても,一度に大量のリクエスト&コネクションが殺到&継続することです。


なお現在は先読み機能が削除されているそうです。

余談: 先読みについて

未整理; 古いネタが多い?

2008-10-27 追記

# 2008年10月24日 hitotakuchan 先読み(Googleアクセラレータ)がだめなのは、WEBサービスの実装者がGET(冪等性を備えているべき)とPOSTの違いを明確に意識していないために、GETで意図せずにデータが削除されることが大量に発生したからだと聞いた

はてなブックマーク - HTTP/1.1 の同時接続数について - daily dayflower

なるほど!

冪等性については WEB+DB PRESS Vol.42の連載「RESTレシピ【第5回】」に書いてありました。

考えなしに先読み機能を実装すると,たとえばブログ記事の削除やログアウトなどが GET でできてしまう場合に,ユーザの意図に反して勝手に行われてしまいますね。

この URL なら安全だよ,というのはクライアントサイドでは判別のしようがありませんね。サーバサイドと協調すれば先読みをうまく使えるかもしれませんが。

Google の検索結果の場合,「先読みできるならしていいよー」というふうになっているらしいです。

一方、これとは別に、Firefox はネイティブのリンク先読み機能をもっています。こちらは、Fasterfox とは異なり、ウェブページ側が指定した文書だけが先読みの対象となります。この機能はデフォルトで有効になっており、about:config で network.prefetch-next を false に設定することで無効にできます。詳細はMDC のリンク先読みに関する FAQ 文書を参照してください。この機能は、2005年4月ころから Google の検索ページで有効になっています。

http://level.s69.xrea.com/mozilla/index.cgi?id=20081026_LinkPrefetch

Google の検索結果など,最初のいくつかはクリックされる可能性が高いでしょうし,GET してもたいていの場合安全でしょう。

2008-10-27 追記 おわり

現在のサーバ構築指針

先に述べたように,最近のブラウザでは(持続性)同時接続数が増えていますし,一部の「インターネット高速化」ソフトウェアは同時接続数を増やしているようです。

これからサーバを構築する場合,これまでより多数の同時接続数をさばけるよう考慮したほうがよいかもしれません。

とはいえサーバ増強できない or 負荷に耐えられないんですけど

それなりに大規模なサイトであれば帯域制御装置をいれてハイおしまい,ですが,個人サイトではそうもいきません。

もしあくまでサーバの「負荷」が気になる,ということであれば(Apache prefork の場合ですが)[http://httpd.apache.org/docs/2.2/mod/mpm_common.html#maxclients:title=MaxClients] の設定値を落とすことで,同時接続クライアント数を絞ることができます。

しかし,この対策をやると問題がおきます。これまで片側10車線の道路があったときに片側3車線に絞ったとしましょう。同時接続数の多いブラウザは,片側3車線をふさぐ幅のダンプカーのようなものです。1車線幅の通常のブラウザはなかなかアクセスすることができなくなってしまいます。不平等ですよね。

制限するためのもっとうまい楽なしくみはないものでしょうか。

2008-10-27 追記 - あわせておさえておきたい Apache の設定子

それ mod_limitipconn

# 2008年10月22日 kmachu firefox RFCの最大コネクション数以上の接続を拒否するApacheモジュールを作れば話題がループしないと思う。

はてなブックマーク - Fasterfoxが最強すぎる件 - 真性引き篭もり

すでにそのような実装はあります。mod_limitipconn がそうです。しくみや使い方については 接続数/帯域制限で無法なダウンローダを撃退 (4/4):実用 Apache 2.0運用・管理術(最終回) - @IT を参照してください。

さらに毛色(というか用途)が違いますが,id:stanaka さんが作成した mod_dosdetector*6 というものもあります。

iptables でなんとかする

とはいっても

# 2008年10月23日 pitworks ソフト サーバ管理者からすると迷惑なソフト。同時接続数を大量にあげられるとWebサーバが泣きます。サーバ側でApacheならmod_limitipconnとかで制限も出来るんだけど標準じゃないからね・・・。

はてなブックマーク - Fasterfoxが最強すぎる件 - 真性引き篭もり

たしかに Apache の標準添付モジュールではないんですよね。

チラ見したところ mod_limitipconn は 400 行にも満たない lightweight な実装なので悪くはないと思うんですが。

じゃあたいていのディストリビューションに付属している機能で接続を絞ってみましょう。そう,iptables を使うのです。

余談: SSH ピンポンダッシュ問題

最近自分の環境で同じようなことを経験したなぁと思ったら,SSH ピンポンダッシュ問題でした。

iptableshashlimit モジュール(→DSAS開発者の部屋:ssh の brute force アタックパケットの制限 -- DOS 的パケットをフィルタリングする)や recent モジュール(→iptables の ipt_recent で ssh の brute force attack 対策ipt_recent - (ひ)メモ)を使うと対処できます。

でもこれらの対策は「単位時間あたりの同一 IP からの接続回数制限」でした。「同時」接続数ではありません。

connlimit で同一 IP アドレスの同時接続数を制限する(ただしわたしは未検証)

同時接続数を絞るには connlimit モジュールを使います。http://www.designandcommunication.co.jp/Security/iptables/modules/ での説明をもとに設定をおこすと下記のような感じになります。

-A INPUT -p tcp --syn --dport 80 -m connlimit --connlimit-above 2 -j DROP

RHEL 5 だと,もともと下記のような設定があるとして,

-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT

この行の前に下記の内容を挿入することになります。

-A RH-Firewall-1-INPUT -m state --state NEW -m tcp -p tcp --dport 80
   -m connlimit --connlimit-above 2 -j DROP

最初の例だと --syn オプションで接続スタートを検出していましたが,もともとの内容にならって state モジュールを使っています(実質同じ意味ですよね?)。


なお connlimit は RHEL 4 の場合利用できないそうです(→centos4 における iptables の connlimit モジュールについて - tashenの日記)。個人的には RHEL 5 の環境しかないので問題はないですが。

iptables でできたけど……

Apache 側の対策(mod_limitipconn)とどう違うの?

mod_limitipconn の場合,同時接続数制限をこえると「503 Service Unavailable」となります。iptables の場合,制限をこえると接続自体ができません(たぶん DROP にすると timeout,REJECT にすると即切断となるのではないでしょうか)。

クライアントがプロキシ経由で来ているとどうなるの?

同一 IP としてみなされるので,同一プロキシから複数ユーザが接続していたとしても(全体の)接続数が制限をこえると遮断されてしまいます。上記の例では同時接続数 2 に制限していますが,実際にはもっと増やしたほうがよいと思われます。

なお,mod_limitipconn の場合でもプロキシ経由かどうかの判別は行っていない模様です。まぁプロキシかどうかなんて自主申告ですしね。

クライアントが NAT 経由で来ているとどうなるの?

プロキシの場合と同様同一 IP としてみなされます。またプロキシと違い,自主申告手段すらありませんからサーバサイドで検出することができません。重ねて書きますが,上記の例では同時接続数 2 に制限していますが,実際にはもっと増やしたほうがよいと思われます。

まとめ

  • HTTP/1.1 は高速化技術がもりこまれているので,クライアント(ブラウザ)の(持続的)同時接続数を大幅にあげる必要はないはずだよ
  • でも Pipelining は,うまく動かないサーバ構成,サーバ実装,クライアント実装がある(らしい)よ(要出典)
  • RFC 2616 での「同時接続数 2」とは
    • 「持続的」同時接続数 2 のことだよ
    • 「MUST」でなく「SHOULD」なので,したがわなかったら即「違反」ではないよ
    • でも「SHOULD」なので,それなりの裏付け・担保・評定がないならば,守るべきだよ
  • とはいえ最近は Firefox 3 や IE 8 など,デフォルトの持続的同時接続数が 6 などのように大きくなっているブラウザがでてきているよ
    • 今は過渡期かもしれないよ
  • つまりサーバを想定同時接続クライアント数×2の待ちソケットで設計するとキツキツの可能性があるよ
    • 対応できるように増強するのは義務ではないよ
  • アクセスや負荷の多い個人サーバなどは,サーバサイドで絞ることで「平等性」を提供するのもいいかもしれないよ
  • 先読みという技術自体は,紳士的に実装してあれば悪いものではないよ
  • 先読みがユーザの被害をおこす Web アプリもあるので,現状では先読み機能は実装しないほうがいいよ
    • サーバ側で「先読み OK」と宣言するしくみもあるよ
    • とはいえアホなインプリメントをするとサーバにすごい迷惑がかかるよ


2008-10-27 追記: Status: 503 やパケットレイヤーで REJECT すると,たいていの User-Agent は件のリソースに対してリトライしないと思います(DROP の場合どうなるんだろう)。それがよろしくないという状況もあると思います。

個人的感想

RFC 2616 で「MUST」でなく「SHOULD」になっているので,RFC 原理主義的に考えたとしても,目くじらをたてて怒る必要はないのかもしれません。ですが,逆に「MUST」にしなかった理由を考えてみると,HTTP(/1.1) というプロトコルを縛りの厳しいものにしたくなかったからじゃないかなぁと思うのです。たとえば RFC xxxx というのがでて「MUST NOT maintain more than 4 connections」という記述になったとき,5 コネクション目の接続を強制切断するようなサーバ側の実装が蔓延するような状況はあまりに寂しいし HTTP が柔軟性の欠けるプロトコルとなってしまうのではないか,と。そのような含みももたせて紳士協定的に「SHOULD」になっているのではないか,と。そういう意味で Firefox 3 や IE 8(こちらはベータ版という位置づけですが)はクライアント実装側が先走りすぎたのかなぁという気がします。

ま,上に書いたのは個人的な妄想です。そんな偉そうなことをいっても Firefox 3 の(デフォルト)設定値を変える予定も当面はないですし。

さいごに

そんなことよりもRSSリーダーを使ったり食生活の改善に取り組んだりしたほうが効果的なのではないか、と思った。

http://la.ma.la/blog/diary_200510171428.htm

*1:RFC 1945 をナナメ読みしてみたんですが,出典がわかりませんでした。ひょっとすると初期の NetscapeIE での紳士協定 or 当時のクライアントサイドの能力限界による設定から,このような数字がでてきたのかもしれません。

*2:他ここでは説明しませんが chunked Transfer-Coding や partial GET なども高速化技術に含まれると思います。

*3:423377 - (wedidntstartthefire) Change max-persistent-connections-per-server to 6. を読むと,設定値を上げたら快適になったぜイエー,というノリな気もしますが ;P

*4:同時接続があまりに大量かつ高頻度だとポートが枯渇したり,プロバイダによる制限,サーバサイドによる制限がかかったりするからではないでしょうか。

*5:さすがにむかしのアホアホクローラみたいにリクエストがループしていたら迷惑&マナー違反になると思いますが。

*6:参照→サーバにDoS耐性を付ける - stanaka's blogmod_dosdetectorの反応への反応 - stanaka's blog