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 とか使ってる奴ら、大概にしろっ!!