2013年9月29日

クラウド時代の core の解析法

クラウド・コンピューティングというと次の2つを主眼とした技術になります。

  • リソースを共有、適切に分散することで、ハードウェア購入・維持費を下げよう
  • 瞬間的に大量の計算機リソースを借りる事で、必要になる計算機能力がバースト的に増大する場合に備えよう

しかし、現在の仮想化技術には、複数の物理マシンを仮想的に1台のマシンに編み上げる方法は存在しません。この事は次の単純な事実をもたらします:

物理的に保持しているマシンの台数より
仮想的に保持しているマシンの台数のほうが多い

一方で、ハードウェアをいくら仮想化しようが、その上で動作するアプリケーションの数は減りません。使うアプリケーションの種類は減りませんし、分散コンピューティングとかですと、複数のマシンで同じソフトウェアを動作させて協調分散して仕事をさせるのですから、むしろプロセス数とかは増えます。


それどころか協調分散システムでは、どこかのプログラムがヘコッとコケるのは当たり前になっているので、

こけても
即座に
別のプロセスや
別の仮想マシンを
立ち上げればええやん

という発想がまかり通っています。つまりソフトウェアの品質は低くなっている。

仮想マシンの台数は増え、ソフトウェアの動作プロセス数も増え、なおかつ品質が悪くなると何が起こるか…

システムの面倒を見る側が
見なくちゃいけない core ファイルの数が
爆発的に増大する

コアファイルを見て、適切なプログラマに
これはテメーの間違いだ!!
直しやがれ
と押し付ける交通整理役の人が大変になるってことです。

先日も、とあるシステムのコアの数を調べたら 6000 を超えてまして…総量を計算したら 3Tbyteを越えてまして…ちょっとどうしようかと思案中。

こういう作業を全部手作業だけで1つ1つ処理していくのは大変すぎます。少なくとも 深さ優先(1つづつコアを分析していく) でやったのでは死んでしまいます。ここは 幅優先(表面を撫でるように簡単なチェックをして、共通な物同士をまとめ、不要なものを破棄しながら分析する) 戦略が必要です。


というわけで、私が普段使っているやり方を少しここに書いてみようと思います。

対象は基本的に Linux になりますが、BSD などの Unix 系 OS なら、ある程度は共通していると思います。

デバッガは gdb を前提とします。多分他のデバッガでもできるんでしょうが、バッチ処理的に実施する上では gdb は便利なので。

最後に。ソースコードが無いと仮定します
あなたの仕事はこけているコアファイルを適切なプログラマのもとに送り届けることだとします。しかもあなたは開発部隊に所属していないので、ソースコードは読めない。
そんな条件下での分析と、プログラマが出鱈目八百を答えてきた時に反論できる程度の知識を予め引きずり出す事が主な仕事とします。

まぁ、実行バイナリがあって、コアファイルがあるんだから、じっくりと時間を掛けて、本気で分析すればそりゃ問題点は判るでしょうが、どうせ直せませんからね。



コアファイルの命名規則を調べる

unix 系 OS では伝統的に、デフォルトのコアファイル名は core です。
厳密に言うと、そのプログラムにとっての「カレントディレクトリ」に core というファイル名で作られます。

すぐ判ることですが、cron で動かしたとか、daemon であるとかのプロセスのカレントディレクトリ…それももうすでにコケて core ファイルを吐いた後のプロセスのカレントディレクトリを探すのは容易なことではありません。
また、同じディレクトリで core というファイルをどんどん上書きされるのも困りますし、逆に色んなディレクトリに core ファイルを撒き散らかされるのも運用上ありがたいことではありません。
だいたい、「core」だけじゃ、何の core ファイルなんだか判らないし。

という訳で、最近の unix 系 OS は何らかの方法で、coreファイル名を指定する方法があります。ファイル名を指定する方法があるという事は、上手に使えば core ファイルを保存する絶対パスを指定できる、と言うことです。これがちゃんと設定してあれば、core ファイルがファイルシステム中に撒き散らされる、なんてことはありません。

Linuxの場合、2.6 か 2.4.21以降であれば、
    /proc/sys/kernel/core_pattern
というファイルに、パス名パターンを指定することで core ファイル名を指定できます。これは
    /etc/sysctl.conf
か、そんな感じのファイル名/ディレクトリ名の下にある設定ファイルで設定できます。ココらへんは distribution/version 依存なので、各自調べて下さい。


ちなみに私の見ているシステムでは
     /var/cores/%e.%p.%t.%s
になっていました。これを解釈すると:

  1. 全てのコアファイルは /var/cores/ ディレクトリ下にあります
  2. ファイル名の最初は %e つまり「プログラムファイルの名前」をパス名無しで…になります
  3. . で分けた後、%p つまりプロセスIDが続きます。
    これは同時刻に同じプログラムが連続してこけた場合でも区別できるようにするための工夫ですね
  4. . を挟んで、次が %t …つまり epoch time が来ます。これはこの core ファイルが作られた(作る要因が発生した)時刻を、1970年1月1日 00:00:00 (UTC) からの時刻で表したものになります
  5. . を挟んで、最後に %s …つまりこの core file の発生要因となったシグナル番号が来ます。


見ての通り、このファイル名をちゃんと設定するだけで、色々と面倒がスキップできることが判ります。特に最後の %s はとても有効です。

もし、この辺りを自由にできる権限があるならば、是非多くの情報がファイル名だけからでも判るように設定することを強くおすすめします。



命名規則に従って、コアファイルの表面的な情報を調べ、分類する

さて、以下の議論では /proc/sys/kernel/core_pattern が /var/cores/%e.%p.%t.%s であると仮定します。

ここで役に立ちそうなのは %t と %s です。%e は最初から human readable ですし、%p は単なる番号なので readable もへったくれもありません。

そこで %t と %s をもっと human readable にしてあげましょう。なお、以下の説明では
「もっと簡単な方法がある」
「もっと判りやすい方法がある」
どちらも受け付けません ( w )。


簡単な方から。

%s は 番号⇒文字列 変換配列があればよいでしょう。bash は配列が使えますので、それを使うと便利。LinuxのSIGNALのマニュアルページに書いてある表の真ん中を元に作ったのがこれです。

declare -a SIGNAL_NAME
SIGNAL_NAME[1]='SIGHUP'
SIGNAL_NAME[2]='SIGINT'
SIGNAL_NAME[3]='SIGQUIT'
SIGNAL_NAME[4]='SIGILL'
SIGNAL_NAME[5]='SIGTRAP'
SIGNAL_NAME[6]='SIGABRT'
SIGNAL_NAME[7]='SIGBUS'
SIGNAL_NAME[8]='SIGFPE'
SIGNAL_NAME[9]='SIGKILL'
SIGNAL_NAME[10]='SIGUSR1'
SIGNAL_NAME[11]='SIGSEGV'
SIGNAL_NAME[12]='SIGUSR2'
SIGNAL_NAME[13]='SIGPIPE'
SIGNAL_NAME[14]='SIGALRM'
SIGNAL_NAME[15]='SIGTERM'
SIGNAL_NAME[16]='SIGSTKFLT'
SIGNAL_NAME[17]='SIGCHLD'
SIGNAL_NAME[18]='SIGCONT'
SIGNAL_NAME[19]='SIGSTOP'
SIGNAL_NAME[20]='SIGTSTP'
SIGNAL_NAME[21]='SIGTTIN'
SIGNAL_NAME[22]='SIGTTOU'
SIGNAL_NAME[23]='SIGURG'
SIGNAL_NAME[24]='SIGXCPU'
SIGNAL_NAME[25]='SIGXFSZ'
SIGNAL_NAME[26]='SIGVTALRM'
SIGNAL_NAME[27]='SIGPROF'
SIGNAL_NAME[28]='SIGWINCH'
SIGNAL_NAME[29]='SIGIO'
SIGNAL_NAME[30]='SIGPWR'
SIGNAL_NAME[31]='SIGSYS'
signum=$( echo $filename | sed -r -e 's%.+\.[[:digit:]]+\.[[:digit:]]+\.([[:digit:]]+)$%\1%g' )
signame=${SIGNAL_NAME[$signum]}


$filename という変数に元のファイル名が入っているとします。フォーマットは最初に指定した通り。で、最後の数値の部分を取り出して、そいつで SIGNAL_NAME 配列を引っ張ると $signame にSIGNAL名が入ります。


%t はもう少し面倒と言うべきか簡単というべきか…

epoch=$( echo $filename | sed -r -e 's%.+\.[[:digit:]]+\.([[:digit:]]+)\.[[:digit:]]+$%\1%g' ) gendate=$(echo $epoch | gawk '{x = strftime("%F-%H-%M-%S",$0); print x}' )

$epoch 変数に %t の部分を取り出すのは、sed による強引な書き換えで実装しています。で、gawk の strftime() を使って、無理やり 年-月-日-時-分-秒 に置き換えます。これを $gendate 変数に入れておきます。


ここまできたら、後は簡単。

newname=$(echo $filename | sed -r -e "s%^(.+\.[[:digit:]]+\.)[[:digit:]]+\.[[:digit:]]+\$%\1$gendate.$signame%g")

これでファイル名を作り出し、必要に応じて新しいファイル名の置き場所のディレクトリ部分とかを書き換えたら

ln $filename $newname

これで、$newname で、$filename の中身を参照できるようになりました。ハードリンクにしておけば、ディスクを消費する量も少なく、元のファイル名を壊すこともなく、新しい名前でコアファイルを参照することができます。

そうやって出来上がったファイル名がこんな感じ…

# ls
httpd.worker.811.2013-08-18-02-41-24.SIGSEGV
js.10354.2013-07-27-16-03-37.SIGSEGV
js.10536.2013-07-17-20-28-30.SIGSEGV
js.1081.2013-07-23-20-08-01.SIGSEGV
js.10878.2013-06-25-16-39-17.SIGSEGV
js.11114.2013-06-24-18-30-48.SIGSEGV
js.11476.2013-07-12-19-13-40.SIGSEGV
js.11863.2013-06-01-17-29-44.SIGSEGV
js.11950.2013-08-05-19-22-43.SIGSEGV
js.12204.2013-07-26-17-21-56.SIGSEGV
             :
mauiss.6915.2013-06-06-04-30-52.SIGABRT
mauiss.719.2013-08-05-06-15-14.SIGABRT
mauiss.794.2013-05-11-15-17-30.SIGABRT
mauiss.9527.2013-06-08-10-05-47.SIGABRT
mauiss.9578.2013-05-11-19-39-35.SIGABRT
mauiss.9914.2013-07-05-11-00-33.SIGABRT
mds_10401.20200.2013-06-01-22-28-45.SIGABRT
mongrel_rails.16093.2012-09-17-18-34-17.SIGABRT
mongrel_rails.16528.2013-02-14-21-05-59.SIGABRT

これだけでもいくつかのことが判ります。

  1. mauiss, mds_10401, mongrel_rails の3種類はやたらと SIGABRT で死んでいる。

    SIGABRT はそもそもプログラマが assert() 構文で
    「こんなことになったら終わりや…コア吐いて死によし」
    と組み込んでおいた、まさにその条件にヒットしたので core 吐いて死んだ、と言うことです。これらはまさにソースコードの assert() 文の中身を見ない限り、何が悪いのか判らない、と言う事。これらに関して深堀りはするだけ無駄です。
    つーても一応この次の一手分まで実行しておきますが。
  2. http.worker と js というプログラムは SIGSEGVで死んでいるので、多分 bad pointer 系の何かだろう。

    ということは、stack の状態を調べて、同じ call tree を辿って、同じ場所でコケてる場合は、同じような理由で死んでいる可能性が高いってことです。
  3. PID邪魔かも…

    いつのコアファイルなのかを知る上で、PID順に並んでいるのはとてつもなく邪魔である事がよく判ります。PIDを切り出して、一番最後に回せばよかったよ…
まぁ、とりあえず反省点は見つけたので、めでたく次の一手へと進みます。



どんなコアファイルでも最初に調べるべき内容を吐き出させる

さて次は。

いよいよgdbにコアファイルを分析させるのですが。

当たり前ですが、コアファイルの分析には、そのコアファイルを吐いたプログラムの実行ファイルが必要になります。いまどきのOSですとそのような実行ファイルは動作するのに動的ライブラリを必要としますが、動的ライブラリのバージョンもぴったり一致している必要があります。

コアファイルは、正確には「stackとheap」のメモリイメージをファイルに吐き出したものです。ですので、実行ファイルとかロードした動的ライブラリのイメージは含まれていません(a.out形式ならあり得たかもしれませんが)。こいつらを適切にメモリ上に展開してやらないと、

「0x00238d93fc で SIGSEGVを起こしました」
「そんな所に実行プログラムあらへんわ」

とか

「0x00238d93fc で SIGSEGVを起こしました」
「そこにあるのは~ おぉ malloc() やな(古いライブラリでは free でした)」

とか、間違った解析になりかねません。ですので、コア解析でベストなのは、実行環境そのもので実施する事なのですが…それが無理なら、せめて同じインストールセットの環境を整える必要があります。

判ると思いますが、これが実運用環境をなかなか変更するべきではない本当の理由です。逆にこの手の分析をしないのなら、どんどんバージョンアップをするべきなのですよ!! それなのに日本人が運用すると、意味もなくゴネるばかりでブツブツブツブツ…はっ、閑話休題、閑話休題。

とにかく、実行ファイルとかライブラリが全部あって、それもコアファイルを作った環境と全く同じ環境が作れるとしましょう。

現実問題として core file からこれらの環境を推測するのは多分不可能ではないとは思うのですが、1つ1つやってたらたまったものではないので、今回はパスします。
さて、実行ファイルの名前は、コアファイルの先頭にあります。先の例なら mauiss とか js とか言うやつですよね。こいつを見つけ出します。同じ名前のものは、基本的に同じ場所にあると考えてよいでしょう。

which httpd.worker
which mauiss
which js
とかやればきっと出てくる。出てこない場合は泣きながら探す。もし沢山コアファイルがあるなら、きっとその涙はダイヤモンド。1つしか無かったらすねて諦めて寝るのとどっちが効率が良いか、一考の余地がありますが。




実行ファイルが見つかって、コアファイルもある、となればあとは gdb にお任せ…とはまいりません。「何をするのか」を教えてあげなくちゃいけない。

幸い、gdb には「バッチモード」というのがあります。事前に何をして欲しいかを指定しておけば、OK…なのですが、このgdbのバッチ、2つ問題があります。


  1. 分岐もループも書けない
    厳密に言うと「私は知らない」だけで、もしかするとあるのかもしれませんが、取得した情報をもとに条件分岐するとか、繰り返しループを書くとか、そういう方法が、無いのか、私が無知すぎるだけなのか…
    とにかくきれいな方法が提供されていません。
  2. 途中でエラーを起こすとそこから先を実行してくれない。
    例えば SIGSEGV で倒れた理由が NULL pointer を実行アドレスとした call 命令の結果だったとしましょう。stack の frame 0 はこの null pointer を実行するためのアドレスになっています。

    ここで「frame0の Instruction Pointer が指している辺りを disassemble して」とお願いすると、通常モードなら gdb は
        No function contains specific address.
    とか何とか言ってエラーになった後、プロンプトを出すのですが、バッチモードの場合は、そのまま gdb を終了してしまうのです。

    な・ん・の・た・め・の・バッチモードなのやら。

ですので、gdb のバッチコマンドは、なるべく失敗しそうな命令を別々のバッチファイルにして、それらをシェルなどで駆動する…あるいは gdb に対するフロントエンドを作ってやって、それを使ってコマンドをくべてやる必要があるのです。

(以下、未完)


2013年8月18日

神保町 小学館ビル に行ってみました。

やっぱりほら、流行りモノは押さえないと ( w )

アルバム ビューアー で見る場合はこちら

こんな感じに結構良い天気でした。



小学館ビル前その一。
実は奥側の半分だけしか見えない。



小学館ビル前その二。
微妙な人だかり。 かつ皆携帯カメラで撮影しているので、滞在時間はムダに長い(私は Q10 で撮影しているのでもっと長い…)。



建物の中その一。
これはいろいろ補正した後ですが、ガラスへの映り込みは激しいですな。 まだ立入禁止状態なので、ガラス越しとはいえ中がそれなりに見えます。



建物の中その二。



ブヒョー
神保町の交差点に戻ってきました。
やはりぶっ飛んで天気が良かった、という話。

2013年7月29日

100Mbps と 1Gbps と 10Gbps と…

世の中素晴らしいもので、ついこの間まで Lan で100Mbps 回線を使えれば速い方だったのに、今じゃ1Gbpsは当たり前、お金持ちな方は 10Gbps なんて速度の回線を使ってたりします。
Wanだって負けていませんで、1Mbps がやっとこだったのに100Mbpsなら個人でも、企業なら1Gbps位出ないと…てなもんです。

素晴らしい

素晴らしい…んですが。皆さんなんか忘れちゃいませんかね?? えぇ、通信ってのはbpsだけで決まるもんじゃないんですよ。
ほら、「光速の壁」って奴があるじゃありませんか…。どんなに転送ビットレートが速くなっても、ある1ビットが目的地に到達するのには絶対越えられない最短時間ってのがあるはずですよね?えぇ、だからLatencyとか、TCP/IPだとRound Trip Time(RTT)は改善しないはずで、なのに回線上は沢山ビットが飛び回れるって事は…
送るべきデータも、予め沢山用意しないといけないって事です。


どうも最近、あちこちのお客さんがこの問題に直面しています。今まで気にしなくても良かった事が、通信速度が速くなる事で徐々にそうは行かなくなってきている…。下手をしなくても、古い機械/OSじゃ対応できない事態になってきている…。そういうことが判っていない人がどツボにハマり始めている…。

というわけで、ちょっとその辺の話を書いてみようかと。


以下の説明の一部は間違っている/嘘を含んでいます。例えば通信では octet という単位を使うものであって byte じゃねー、とか。ヘッダサイズ0ってなんだよーとか。

ちゃんとした説明は、真面目な本とか、この辺:

に任せる事にします。この章の説明はあくまでも今回の議題を理屈的に判ればそれで十分、という精度で書いていくことにします。

TCP Window

例えば今読んでいるこの blog とか、Youtube の動画をダウンロードしているとか、インターネット上で情報をやり取りする多くの場合、我々は TCP/IP という通信プロトコルを利用しています。例外はNTPとか実時間動画配信とか…そういう「今すぐ でないと 情報に価値が無くなる場合」ぐらいなものでしょう。

TCP/IP は IP … Internet Protocol を使ってバイト列を送るプロトコルの一種でして、最も重要なのはバイト「列」を送る、という所です。つまり
A, B, C, D...
のようにバイト列が存在したら、
「Aが見えてからでなくてはBが見えるようにならない」
「Bが見える前にCは見えない」
ようするに A の後に B が来て、その後に C が来る…という順序保証された状態を保証してくれる、と言うことです。

このバイト列の順序保証は送信・受信を実際に行うマシンのメモリ容量よりも大きなバイト列に対しても保証されます。つまり、全部のデータを送って、正しい順番に並べ直して、それから「こんなんでました~」と表示する訳にはいかない、ということ。


一方、IP はパケット通信です。データは塊(パケット)に分割され、パケット単位で送信されます。パケットがネットワーク回線のどこをどう通るのか、本当に受信側に届くのか、誰も何も保証してくれません。
なるべく頑張る (best effort)
以上の事は誰も言ってくれない。

いや、仮に運良く到達するとしても。パケットは送った順序通りに届くことは保証されない。もちろん LAN…近距離であれば到達するためのコースが1通りしか無いのでほぼ順序通りに到着するでしょうが、長距離になると複数の経路が考えられて、時々の都合で経路が変化し、後から送ったはずのパケットが先に到着する…なんてことも考えられる。


こんな性質の IP を使って TCP の順序保証をどのように実装するか?

順序を保証する部分は「パケットに番号を振れば良い」というのはすぐわかると思います。実際番号は「パケット」ではなく
通信を開始してから頭から何バイト目のデータを送っているのか
を表すシークエンス番号というものを使いますが、そんなのは些細な部分。


失われた…届かないパケットは?

受け取った側はシークエンス番号の何番までは「全部連続して届きましたよ」ということを送信側に教えることができます。その先のパケットは、そもそも送り側が送っていないのか、送ったけど届いていないだけなのか、受け取った側には見分けはつきませんが。だって届いていませんからね
送信側は、どの部分をいつ送ったのか、いつ届いたと教えてもらったのか、という履歴を元に、本来ならいつぐらいに届くはずなのかを推測します。

「あれ?もう届くはずなのに、届いたって言わないなぁ…もう一度送ってみるか」

本来届いたって教えてもらえるはずの時間が過ぎても届いたといってもらえない場合、同じデータを再度送ります(再送)。再送を使って届かないデータを何度も送ることで、到着する確率を上げます。


でも。

もし、送信側が 1024kbyte のデータを持っていて、受信側のメモリが 1kbyte しか無かったら? 送信側が勝手気ままに 1024kbyte 分を送っても 1023kbyte 分はほぼ確実に再送対象になっちゃいますよね? だって受信側は 1kbyte しかメモリがないので、シークエンス番号を見て、最初の 1kbyte の外にあるデータは捨ててしまいます。
TCPの順序保証機能を実装するためには、最初の 1kbyte 以外の部分を受け取るゆとりはないからです。

最初の1kbyteの部分を順番通りのバイト列に戻して、アプリケーションとかにそのバイト列を渡してから、次の1kbyteのことが考えられるようになる。
そこに 1024kbyte 分のパケットを送りつけられても、最初の 1kbyte 分以外は全部棄てるしかありませんよね?

そこで、TCPには TCP Window という考えがあります。ようするに送信側、受信側で
「オラーこんだけ一気に送れるだよ?」
「オラーこんだけしか受け取れねーだよ?」
というサイズを教えあうのです。で、小さい方を TCP Window とする。

送信側は、受信側が「受け取ったー」と言ってきたシークエンス番号(通信を開始して以来、先頭から何バイト目かを表す番号)にこの TCP Window の大きさを足した分までの範囲だけを送ることにする。
受信側が送ってきたシークエンス番号よりも前の部分はもう受信側も受け取ったので送り直す必要はない。
シークエンス番号にTCP Windowの大きさを足した分を超えた先の部分のデータは、受信側は受け取れない可能性があるので、あえて送らない。

こうして送らなくてはいけない全データ、ではなくデータのごく一部に集中することで、少ないリソースで再送機能を実装して、確実に順序保証ができる形でデータをやり取りするわけです。


このTCP Window の「大きさ」のことを TCP Window size と言います。


Round Trip Time

パケットをA地点からB地点まで運ぶのにどれぐらいかかるでしょう…??
え?そんなの簡単??予め2つ時計を用意しておいて、一定時刻にAからパケットを送ってBで受け取った時刻を記録すればいい??

いえ、それは簡単ではありません。A,B両地点の時計を完璧に同期させるのは不可能です。光の速度で数msecかかるということは時計だって同期させると数msecの誤差が出るということ。

ましてやパケットはどのような経路を通って伝わるのか分かりませんし、本当の本当にA<->B地点間の通信ケーブルの長さが何メートルなのかも判りません…。

だから別の手を使います。

A地点からB地点へとパケットを飛ばします。B地点ではそれを受けたら「受け取ったー」というACKを返すようにします。で、パケットを送ってからACKがかえってくるまでにかかる時間を計測するのです。これなら時計は一種類ですし距離は2倍に伸びますから、計測しやすく誤差も出にくい。
この「行って・来い」一周(Round Trip)にかかる時間のことを Round Trip Time(RTT)と言います。

TCPの場合、受信側はパケットを受信し、なおかつそれが今まで受信してきた部分の続きの場合、「ここまで受信したよ」というシーケンス番号を返してくれます。これを ACK と言います。traceroute などはこの データパケットを送ってから ACK パケットが返ってくるまでの時間を計測して RTT を算出します。

類似の方法に ICMP echo と ICMP echo reply パケットを使う方法もあります。これは ping が使う方法ですね。

ここでは TCP/ACK の方で RTT を測る、としましょう。


TCP Window size が同じサイズで通信速度が違うと何が起こる?

この章では TCP Window size が同じサイズだと仮定しましょう。 RFC793(RFC793和訳) の時代に戻って 65,535 byte と仮定します。

先出の通り、TCP Window size は「受信側が受け取ったとしたシーケンス番号以降、TCP Window size までのデータを送信して良い」サイズです。

65,535byte は 1byte = 8bit なので 524,280bit です。本当はこれはペイロードなので TCP header とか IP header とか色々くっつくのですが、面倒なのでそれらのサイズは全部 0 だとして、代わりに諸々計算する時にどんどん「切り上げ」することにしましょう。


100Mbps

100Mbps の世界では 524280bit を送信するのに 5.2428msec かかります。つまり、5.2428msec …大雑把に 6msec ですか…以内に ACK が帰ってこないと、送信側はこれ以降のデータを送れなくなります。

6msecの RTT というのは…大雑把に言うと東京・大阪間を直結した場合がそれぐらいです。
Google 先生によると光の速さは 299,792,458m/sec で、東京・大阪間の距離がだいたい 515km だそうですので、片道
515*1000/299,792,458(sec) = 1.7178(msec)
往復で3.5mseecぐらい。電気信号でLANの中を走り回ったり、信号を変換したり、1packet のデータ分をビットに変換したりしてると、だいたい 6msec のRTTは妥当だ、と言うことになります。

東京・名古屋だと 5msec ぐらい。東京・仙台間や、大阪広島間も同じぐらいの距離なので直結線であれば時間も同じぐらいです。

なので、100Mbpsの回線を東京・大阪間でフルパワーで使えると仮定するなら、古い TCP の実装でも Window Size を最大にすれば、全力で通信できる、ということができます。

しかし仙台・大阪間のように、明らかに6msec以上かかる距離になると、通信は間欠泉のように「データを送れないタイミング」ができてしまう。

例えば仙台・大阪間が10msecかかるとします。最初の6msecかけてTCP Windowに格納されていたデータが全部送られるわけですが、最初のパケットに対する ACK はまだ返って来ません。結果、6msec経ったポイントからしばらく沈黙が発生します…最初のパケットに対するACKが返ってくる4msec後までは。

4msecしてACKが返ってくると、受信が確認された分だけ TCP Window に空きができますから、その分だけ新しいデータを相手に送れるようになります。その間にもACKが次々と返って来ますので、TCP Window に空きができ、その分データが送れるようになります。
データが送れるようになればその分また新しいパケットを送るようになるわけで…その状態が6msec続きます。で、またACK待ちになり…

つまり 6msec データを送っては 4msec 沈黙、6msec データを送っては 4msec 沈黙、を繰り返すようになるのです。


ただし、このような状態は WAN …中でも日本を半分近く縦断するような WAN 通信でない限り起こりません。LANと呼べるレベルならほとんど心配は無いはずです。逆に言うと海外との通信に於いてはほぼ確実にこれは起こってしまいます。

1Gbps

1Gbps の世界では 524280bit を送信するのに 0.52428msec かかります。つまり、大雑把に 0.6msec ですか…以内に ACK が帰ってこないと、送信側はこれ以降のデータを送れなくなります。

RTTが0.6msecかかるには、ちょっと大きめの工場の端から端までぐらいの 規模の LAN が必要でしょう。同じビル程度であれば 0.15msec とか 0.2msec 程度になります。

つまり、1Gbpsの世界で WAN を貼ろうとすると、 RFC793(RFC793和訳) の世界だと 100Mbps で海外と通信しようとした時に生じる問題が、となり町との通信でも発生する、と言うことです。

もちろん、東京・大阪間などひどい状態に…なにしろ RTT 自体は変わらないのです。6msec中最初の0.6msec でデータを送り終わり、5.4msec 沈黙を守った後、また0.6msecデータを送り…を繰り返すことになります。
つまり実質 100Mbps でしか通信できない…。原因はもちろん、TCP Windowサイズの小ささと RTT の大きさが通信幅にマッチしていないことにあります。


10Gbps

10Gbps の世界では 524280bit を送信するのに 0.052428msec かかります。つまり、大雑把に 0.06msec ですか…以内に ACK が帰ってこないと、送信側はこれ以降のデータを送れなくなります。

これは正直、スイッチを一台挟んで隣のラック上のマシンと通信してもクリアするのは難しい。Fibre で直結すれば 7kmぐらいまではどうにかなるでしょうが…

このレベルになると、ケーブル上をデータが流れていくのにかかる時間よりも、スイッチやルーターで信号処理をしたり、リダイレクションを掛けたりするのにかかる時間の方が長くなります。


新しい問題は何もありません
全ては新しい問題です

これでお分かりいただけたでしょう。

10Gbpsの世界では隣同士のマシンで起こる事象は、100Mbpsの時代には長距離通信において起こっていた現象でしかありません。別に新しい問題ではないのです。新しい問題ではないのですが…

同時にほとんどの人が意識したこともなく、意識することもなかった問題が噴出する、ということでもあります。ほとんどの人が気にして来なかった、太古から存在する問題が襲ってくる。


解決策は簡単です。TCP Window size を大きくしてやればいいのです。RTTが大きいといっても人間が感知できるほどの大きさではありません。通信格闘ゲームでもやってれば別ですが。なので TCP Window size の default 値を大きくすれば…いい…ん…で…す…が……


実はこれが容易なこっちゃありません。

あぁ、もちろん、64bit 環境においてはアホのように簡単な問題です。問題はこれが「2000年1桁台」初期の頃のマシンや、その頃以前のソフトウェア…特にOSと混ざった場合…32bit CPU とかその程度の環境で発症します。


話を応用性が効くようにするために 10Gbps で東京・大阪間を通信する場合を考えましょう。

RTTは相変わらず 6msec です。常時通信し続けるのに必要な TCP Window size は 60Mbit …大雑把に 8Mbyte 弱です。最近の TCP には Scaling Factor という機能があって、16bit の変数を 2n倍しろ、とすることができます。この n を 0 から 14 まで指定することができるので Window size は (1024-16)Mbyte = 1008Mbyte まで指定することができます。


しかし。プログラムからすればこれから開始する TCP session が「どれぐらい遠く」としゃべるためのものなのか、知るすべはありません。というかプログラムを動かしている「持ち主」だって知らない。インターネットの通信は、サーバがどこにあるのかが「シームレス」だというのも特徴の一つですから。

と言うことは、仮に最長が東京・大阪間で、それ以外はLANの中に収まるとしても、TCP Window size は「全ての session に対して」8Mbyte 用意しなくちゃいけない、と言う事。例えば私が今これを書いている Windows7 マシンは、netstat で ESTABLISHED 状態のセッションを数えただけで18個あります。全部に 8Mbyte を与えると 144Mbyte * 送受信それぞれ = 288Mbyte 、kernel の作業領域から持っていかれることになります。

私が仕事で面倒を見ているマシンになると、常時なんだかんだで 800session ぐらい貼り続けていますから、12800Mbyte = 12.5Gbyte 必要なわけで…

32bit の Linux Kernel の場合、Kernel 自身が動作できるメモリの大きさは 892Mbyte ぐらいです。この内 288Mbyte を通信で消費されただけでも動作への影響は著しくでかい。12.5Gbyte 頂戴なんて言われたら絶対無理。

ましてやこれは東京・大阪間の 6msec の RTT の場合です。世界中を股にかけるとなると 300msec ぐらいは RTT があり得る。このさらに 50倍…600Gbyte…


昔の 1Gbyte ぐらいまでしかメモリが搭載できないマシンに、Windows2003 とか RHEL5 とか載っけて、無理やり動かしているマシンなんて疾うの昔にまともに動けるような状態じゃない、と言うことです。
64bit CPU に 64bit CPU 用OSを用意して、クライアントマシンでも 16Gbyte …サーバなら 1Tbyte ぐらいは搭載しないと、まともな通信なんかできなくなりつつあるってことです。

もちろん、世界中を股にかけた通信なんて、まだ 10Gbps 出ません。100Mbps だって出るかどうか。でも 1/100 にしても 6Gbyte は当たり前の世界なんですよ。

判りますか? HWだけ新しくして、ふっるい distribution …例えば RHEL 5 とか…を使って
「TCO下げてます」
なんて大間違いなんですよ。全然 TCO 下がってないんです。単純に不便になってるだけ。しかも通信していない理由なんか統計情報からは全然判りませんから、問題があるようには見えないかもしれませんが、それは問題を発見するための物差しが間違っているだけなんです。


また、新しく回線を敷設する場合は、通信速度だけではなく、RTT も確認するべきです。もし、本来直結線で 6msec の RTT の回線が 12msec かかるようなサービスがあったら…そりゃ値段を叩いて安くさせるべきです。だってその分メモリを浪費して、サーバの動作効率が落ちるんですから。IO cache とかどれぐらいヒット率が下がってどれぐらい無駄に計算時間が浪費されると思ってるんですか…

RTTは短い場合はルーティング候補がほとんどありませんのでいいのですが、RTTが不必要に長い場合、複数のルーティングの中から適宜選んでいる…という場合があり得ます。この場合、性能を出し続けるためには 一番長いRTT にパラメータを合わせなくちゃいけません。たとえば、60%が 13msec、20% が 20msec、10% が 25msec、5%が 30msec、残り 5% が 40msec 以上…と言われたなら、例えば
「合計95%までの確率で通信速度が全力であることを保持したい」
ならばRTTの短い方95%の中で最も長い RTT … 30msec で 値を決めなくちゃいけません。


え? TCP Window size を RTT に合わせて長くしなおせばいいんじゃないかって?? 今の所、RWINなどは、最初に Window Size を大きく negociation しておいて、実際には少ししか使わない、と言う方法で Window size の調整をしています。
そうではなく TCP Window sizeを最初は小さく、後で option か何かで大きく同意し直す、という方法は、RFCを書く所から始めなくてはいけないレベルの話です。仮に今日、RFCを書いたとしても実装が普及してあなたが使えるようになるのは 10年後ぐらいの話ですよ…。


結論

ようするにこういうことです。


  • TCP Window size こそが真の通信速度の律速条件となる世界がやってきた。
  • 昔WANで気にしなくちゃいけないことは、今やLANでも気にしなくちゃいけない。
  • 古いOSや、32bit HW だともう速い速度にはついていけない。いい加減諦めて買い換えろ。特に Windows を 2003 とか XP とか使ってる奴ら、大概にしろっ!!