network
xv6にネットワーク機能を実装した
在宅勤務に移行してから1ヶ月半ほど経過しました。通勤という概念が消滅したおかげで午前中から活動できるようになった @pandax381 です。
要約
フルスクラッチで自作した TCP/IP プロトコルスタックを xv6 に組み込み、一通りの機能が動作するようになりました。
I publish the implementation of TCP/IP network stack on xv6. I ported my user-mode TCP/IP stack, which was originally developed for learning, and added the e1000 driver and socket system calls. Some parts are still not enough, but they are working.https://t.co/nht9JDMVbl
— YAMAMOTO Masaya (@pandax381) March 11, 2020
経緯
かれこれ7〜8年くらい経つんですけど、ライフワークとしてお勉強用のTCP/IPプロトコルスタックの開発をほそぼそと続けています。
一般的に、プロトコルスタックはOSの機能の一部としてカーネル内部に存在しているのですが、この自作プロトコルスタックは、作りやすさを重視した都合でユーザ空間のアプリケーション用のライブラリとして実装しています。
最下層に位置するリンクレベルの入出力は PF_PACKET / BPF / TAP を利用するお手軽仕様で、純粋にパケット処理に専念できるようになっています。
やや作りが粗い部分もありますが、Ethernetフレームの送受信からTCPセグメントのやり取りまで全てを自前で処理しています。「プロトコルスタックってどんな作りになっているんだろう」と興味を持った人が、手を出して雰囲気を掴むのに丁度いいくらいのボリュームになっているんじゃないかと思います。
(物理デバイスや論理インタフェースの抽象化とかそれなりに作り込んであるので、興味があったらコード見てください)
毎年、インターンシップを希望する学生を受け入れていて、彼らの頑張りによって DHCPクライアント機能やパケット転送機能、更にはDPDKサポートといった改良が加えられいますが、いずれも1〜2週間という短い期間で成果を出してくれました。
その流れで、昨夏には KLab Expert Camp という合宿イベントを開催しました。
KLab Expert Camp 開催決定!第1回テーマは「TCP/IPプロトコルスタック自作開発」で、講師は僕が担当します。日程は 8/26〜29 の4日間での開催、交通費・宿泊費は全て KLab が負担します。プロトコルスタック自作に興味のある学生のみなさん、一緒に楽しい夏を過ごしましょう!https://t.co/SVyMOzaeq8
— YAMAMOTO Masaya (@pandax381) April 26, 2019
4日間、ひたすらプロトコルスタックを開発するというマニアックな合宿イベントにもかかわらず、全国からたくさんの学生が参加してくれ、実装解説を通じて基本的な仕組みを学んだり、別言語への移植や機能追加に取り組んでくれました。
参加者からのフィードバックがすごく良くて今後も定期的に開催したいなと思う中で、最近の動向を見ていると「自作CPUで動かしている自作コンパイラでビルドした自作OSに自作プロトコルスタックを組み込みたい」というヤバイ人が出現する可能性も十分ありそうな気がしてきます。
少なくとも、ユーザ空間での動作を前提としてたライブラリの状態からOSのカーネルに組み込もうとした際にどんな罠が潜んでいるかを把握しておかないことには、十分にサポートしてあげられないことは目に見えています。そんなわけで「まずは自分でやってみるか」というのが、この取り組みのそもそもの動機です。
手頃なOSを選定
恥ずかしながら、僕はOS自作を嗜んでおらず「自分のOS」を所有していません。30日OS自作本も「いつか読もう」と積んである始末なので、OS自作からはじめていると時間が掛かりすぎてしまします。だからと言って Linux や BSD だと、ベースにするには規模が大きすぎます。
こういった用途には、コンパクトな xv6 が向いていそうな気がします。
CPU自作関連の話題でも頻繁に登場していますし、いまさら説明するまでもないと思いますが、xv6 は Version 6 UNIX (V6) を x86(マルチプロセッサ環境)向けに再実装したものです。MITがオペレーティングシステムの講義用に開発し、MIT以外にも数多くの教育機関が教材として採用しています。
コード量は10,000行程度で、OSの実装にさほど詳しくなくても全体を見渡すことができるボリュームです。充実したドキュメントと先人たちが公開してくれている情報が豊富にあるのも心強いです。
最近は RISC-V 向けの実装に移行したようで、MITの講義資料も最新のものは RISC-V 版がベースになっています。x86 版は 2018 年度以前のアーカイブを参照しましょう。
どこからはじめるか
xv6 にはネットワーク機能が一切含まれていないため、NICのドライバから書く必要があります。自作プロトコルスタックではリンクアクセスはOSの機能(PF_PACKET や BPF)を利用していてドライバにはノータッチだったので、もう一段下の世界へ降りることになります。
xv6 は QEMU 上で動作させながら開発を進めることになるので、NIC は QEMU で使えて情報も豊富そうな e1000 (Intel 8254x) をターゲットにするのが良さそうです。
まずは、ドライバのエントリポイントのダミーを用意して Hello, World! でも出力するようなコードを書き、PCI デバイスのドライバのリストに追加してあげるところからはじめたら良さそう ...と思ったのですが、 探してもそれっぽいコードが全く見当たりません。
はい、xv6 は PCI をサポートしていないのでした。
したがって xv6 で NIC を扱うためには
- PCIバスをスキャン
- 接続されているデバイスを検出
- 対応するドライバを呼び出す
という一連の処理から作り込む必要があり、更にもう一段下の世界へ降りることになるわけです。だんだんと上昇負荷が厳しくなりそうですね。
PCI周りの処理
PCIバスのスキャン とか言うとなんだかすごく難しそうに聞こえますが、I/Oポートを叩いて情報を読む、という処理の繰り返しです。そして、この辺はOS自作界隈のみなさんが素晴らしい情報を公開してくれています。
コンフィグレーション領域ってなんぞ?という状態でしたが uchanさん の資料を読んで PCI を完全に理解しました(してません)。
以下の Linux Kernel を解説したドキュメントも参考になりました。
これらの解説を読みながら少しづつ進めていけば PCI バスをスキャンしてデバイスを検出するコードが書くのはさほど難しくないと思います。なお、xv6-net では PCI の最終的なコードは JOS という別の OS から拝借することにしました。
- https://pdos.csail.mit.edu/6.828/2018/jos.git
JOS は、xv6 と同様に MIT がオペレーティングシステムの講義で使うために開発している OS です。JOS は一部が意図的に削られている未完成のOSであり、xv6 の解説を聞いたあとに、JOS の実装を進めて完成させるという構成のようです。また、xv6 がモノリシックなカーネルであるのに対して JOS はマイクロカーネルという違いもあるようです。
読み比べてみると JOS の方が諸々整理されていてキレイに書かれている気がするものの、全体の雰囲気はかなり似ています。そして、JOS には PCI 周りのコードと e1000 ドライバのスケルトン(ほぼなにも書かれていない空っぽのファイル)が用意されているため、これを使わせてもらうことにしました。
上記の pci.c は JOS のコードそのものです。xv6 に手を出す人は高確率で JOS のコードも読んでいるはずで、それであれば僕が書き散らかしたコードよりも JOS のコードを使わせてもらったほうが読む人が理解しやすいだろうと考えた結果です(コードをそのまま組み込むため、辻褄合わせに少し手間を掛けていたりします)。
pciinit() を main() の中から呼んであげると、起動時に PCI バスをスキャンして接続されているデバイスが検出されます。
PCI: 0:0.0: 8086:1237: class: 6.0 (Bridge device) irq: 0 PCI: 0:1.0: 8086:7000: class: 6.1 (Bridge device) irq: 0 PCI: 0:1.1: 8086:7010: class: 1.1 (Storage controller) irq: 0 PCI: 0:1.3: 8086:7113: class: 6.80 (Bridge device) irq: 9 PCI: 0:2.0: 1234:1111: class: 3.0 (Display controller) irq: 0 PCI: 0:3.0: 8086:100e: class: 2.0 (Network controller) irq: 11
ベンダIDとデバイスIDをキーにドライバのエントリポイントを定義しておくと、デバイス検出時にそれを叩いてくれます。
struct pci_driver pci_attach_vendor[] = { { 0x8086, 0x100e, &e1000_init }, { 0, 0, 0 }, };
NICのドライバ
PCI のデバイスを検出できるようになったら、次は NIC のドライバを書きます。
JOS のソースには kernel/e1000.c と kernel/e1000.h が含まれていますが、どちらも空っぽで、Intel のドキュメントをちゃんと読めという圧が伝わってきます。
参考になりそうなコードがないか探してみると、xv6 の RISC-V 版のリポジトリには NIC の初期化処理まで書かれたドライバの雛形がありました。初期化処理を参考にしつつ、ヘッダファイルはそのまま利用させてもらいました。
- https://github.com/mit-pdos/xv6-riscv-fall19/blob/net/kernel/e1000.c
- https://github.com/mit-pdos/xv6-riscv-fall19/blob/net/kernel/e1000_dev.h
また、大神さん が公開されている本がめちゃくちゃわかりやすくて助かりました。PCIの情報も詳しく書かれていて、こんな情報をタダで読ませてもらって良いのか...と感動しながら e1000 を完全に理解しました(してません)。EPUB版でたら買います。
OSDev.org で紹介されている以下のコードも参考にしました。
他にも、世の中には xv6 向けの e1000 ドライバを書いている人は沢山いるので、github 上でもいくつかプロジェクトを見つけることができます。ただし、どれもポーリングする前提で、割り込みがちゃんと動く状態のものは見つけられませんでした。
一連の作業の中で一番苦戦したのが、パケット受信時に割り込みを発生させることで、1週間くらいハマっていました。ちゃんと書いたつもりでもなかなか割り込みが発生しなく心が折れそうになっていたので、動いたときはめちゃくちゃ嬉しかったです。
(たぶん送受信で使うDMA用のバッファ設定がうまくできていなくて動いていなかったんじゃないかと思っています)
xv6 用の e1000 ドライバが動いたーーー!!!これで自作プロトコルスタックを xv6 へ載せるための準備が整った💪https://t.co/nht9JDMVbl pic.twitter.com/TJYkWVAKAV
— YAMAMOTO Masaya (@pandax381) February 29, 2020
この辺の詳しいことは、また別の記事か薄い本でも書こうかなと思っています。
プロトコルスタック本体の移植
NIC のドライバが動いたので、いよいよ本題のプロトコルスタック本体の移植に取り掛かるのですが、あまり躓くことなくあっさり移植できてしまいまったので、あまり書くことがなかったりします。
pthread を使っていたので mutex を spinlock に置き換えたり、cond をタスクの sleep/wakeup に置き換えたりしたくらいです。あと、タイマーが使えていないので、TCPの再送とかタイマーに依存した処理はまだ動かせていません。
まず、ARP に応答できるようになって
ARPに応答できるようになった!#プロトコルスタック自作#xv6 pic.twitter.com/zjBszDkK6t
— YAMAMOTO Masaya (@pandax381) March 2, 2020
ICMP に応答できるようになって
順調に進んで ping に応答できるようになった!#プロトコルスタック自作#xv6 https://t.co/I7BGOz4jVw pic.twitter.com/0xBivGA9LZ
— YAMAMOTO Masaya (@pandax381) March 3, 2020
UDP で通信できるようになり
自作プロトコルスタック on xv6 の進捗が素晴らしくて UDP 通信に成功した🎉🎉🎉(雑に実装したソケット風のシステムコールを通じてユーザ空間のアプリケーションがカーネル内のプロトコルスタックを利用して通信してる) pic.twitter.com/VUbbeWr0TO
— YAMAMOTO Masaya (@pandax381) March 6, 2020
最終的に TCP も動くようになりました
うぉぉぉ!!!TCP も動いたぞ!!!もともとユーザ空間で動かす前提でこしらえた自作プロトコルスタックが xv6 のカーネル空間で完全に動作してる!!! https://t.co/juL0ntccyY pic.twitter.com/5tDU1T0B8K
— YAMAMOTO Masaya (@pandax381) March 6, 2020
ソケット
カーネル内でプロトコルスタックが動くようになってもソケットがなければユーザ空間で通信アプリケーションを書くことができません。
自作プロトコルスタックにはソケット風のAPIもあるのですが、これは単なるライブラリ関数なのでプロトコルスタックをカーネルに組み込んだ状態ではユーザ空間のアプリケーションから呼びだせません。
そんなわけで、ソケット風 ではなくガチのソケット(関連のシステムコール)を実装しました。
システムコールの追加にあたっては、xv6 の既存のシステムコールの中から似たようなプロトタイプのものを探して同じように実装しています。
socket() で作られるディスクリプタは、もともと存在しているファイルディスクリプタと互換性を持つように作っているので、ソケットのディスクリプタを close() で閉じたり、recv() / senc() の代わりに read() / write() を使うことが出来ます。
単純なソケット通信のプログラムであれば、Linux 用に書いたコードがそのまま動く程度にはちゃんと作っています。
自作プロトコルスタック on xv6 がいい感じに仕上がった!ソケット関連のシステムコールを真面目に実装したのでよくあるこんなコードがそのまま動くようになった!ちゃんとファイルディスクリプタに紐づけているので read/write を使っても動くし fork しても大丈夫!https://t.co/nht9JDMVbl pic.twitter.com/LQNvWwOpj9
— YAMAMOTO Masaya (@pandax381) March 10, 2020
ifconfig コマンド
シェルからインタフェースの状態を確認したりIPアドレスを設定するために、当初は適当なコマンド(ifget / ifset / ifup / ifdown)と対応するシステムコールを作って、その場をしのいでいました。
雑なシステムコールを実装してコマンドでネットワークインタフェースを制御できるようになった💪#xv6#プロトコルスタック自作 pic.twitter.com/T6LZNEtwOK
— YAMAMOTO Masaya (@pandax381) March 5, 2020
ただ、これだとちょっとダサいので、最終的に ifconfig コマンドを作りました。ip コマンドじゃないのは NETLINK の実装はさすがに厳しいからという理由です。
だいぶ雑だけど ifconfig 作って動くようになった(たぶん大幅に書き直すと思うけどとりあえず push した)#xv6#networking https://t.co/TEbKeGWe3m pic.twitter.com/2mh5tVGiuy
— YAMAMOTO Masaya (@pandax381) March 30, 2020
ifconfig は ioctl() を通じてインタフェースの情報を取得/設定しているので、ioctl のシステムコールを追加し、SIOCGXXX や SIOCSXXX をひたすら作り込んでいます。
おわりに
あれもこれもと作り込んでいたら「自作プロトコルスタックを xv6 に移植した」というよりは「xv6 にネットワーク機能を実装した」という気持ちになったので、このようなタイトルになりました。
あと、勢い余って reddit デビューもしました。
reddit でコメントくれた方の自作OSがしっかり作られていて感動したので紹介しておきます。
まだ先の予定を立てるのは難しいですが、イベントが開催できるような状況になったら、また KLab Expert Camp でプロトコルスタック自作の合宿をやりたいと思っています。その際は自作OSへの組み込みもサポートできるように準備しておきますので楽しみにしていてください!
@pandax381
第一回 KLab Expert Camp「TCP/IPプロトコルスタック自作開発」を開催しました
KLab Expert Camp というイベントを8/26〜29の4日間で開催しました。
KLab Expert Camp とは
KLab Expert Camp は、技術的に深いテーマに取り組んでいる学生の発掘・育成を目的とした KLab の新しい取り組みです。
記念すべき第1回のテーマは「TCP/IPプロトコルスタック自作開発」で、以下のような触れ込みで開催しました。/p>
「OSを作ってみたい」「コンパイラを作ってみたい」と思う人が多いように「プロトコルスタックを作ってみたい」と思う人も多いのではないでしょうか。今回の KLab Expert Camp のテーマは、そんな皆さんにピッタリの「TCP/IPプロトコルスタック自作開発」です。担当講師が開発している教育用のプロトコルスタック(https://github.com/pandax381/microps)を教材に、Ethernetフレームを組み立てて送受信するところから ARP、IP、ICMP、UDP、TCP などのプロトコルを全て自分の手で実装することで、これまでブラックボックスだったプロトコルスタックの処理を紐解いていきます。
参加にあたっては、開発環境やプログラミング言語に関する初歩的な知識は身につけていることを期待していますが、期間中は KLab の講師陣が手厚くサポートしますのでご安心下さい。また、意欲的な参加者においては、期間中に追加機能の実装や組み込み機器への移植などへのチャレンジングな取り組みも支援します。
また、参加者には各自の取り組みたい内容に応じて2つのコースの何れかを選択してもらう形をとりました。
【基本コース】 教材となるリファレンス実装の解説を通じてプロトコルスタックそのものへの理解を深める
【応用コース】 リファレンス実装の別言語への移植や機能追加など各自がテーマを決めて取り組む
開催の経緯
定期的にこんなツイートをしながら、年に1〜2人のペースで前途有望な学生を低レイヤ沼に引きずり込んでるんですけど、たまたま5人くらいの学生が同じタイミングでリアクションしてくれまして。それだけの反響があるのは嬉しいものの、個別に対応してあげるのはおじさんの体力的にちょっと厳しそうだなという感じもあって「もういっそのこと全員集めて一緒にやればいいのでは???」という考えに至ったわけです。
ネットワークプログラミングやりたい学生からのインターン応募いつでもお待ちしてますよ。TCPを自力で再実装したりオレオレトランスポートプロトコルやプロトコルスタックそのものを作ったり..楽しそうでしょ?一緒に面白いもの作ろうよ!
— YAMAMOTO Masaya (@pandax381) April 17, 2018
そして、インターン関連をとりまとめている人事の担当者に「こんな感じで何人か集めてやりたいんだけど」と雑に話をしたところ「イイっすね!なんならもう少し集めてイベントにしちゃいます???」みたいな凄いノリの良い返事をもらって、1週間くらい経ったら「例のイベントの企画、Go出ました!」と、あっさり開催が決まってしまった感じです。
取り急ぎ告知に向けてイベントの名称を決めようということになったのですが、これが結構難しくて... パッと思いつくものはだいたい既に他で使われてるんですよね。Expert Camp は僕の出した案なんですけど「Expert...ちょっとイキりすぎでは...?」みたいな意見もあったりして。最終的には「カッコ良さそうだからいいじゃん」「僕(自称)エキスパートだから無問題」という感じで落ち着きました。
そんなこんなで漕ぎ着けたイベント告知が以下のツイートです。
KLab Expert Camp 開催決定!第1回テーマは「TCP/IPプロトコルスタック自作開発」で、講師は僕が担当します。日程は 8/26〜29 の4日間での開催、交通費・宿泊費は全て KLab が負担します。プロトコルスタック自作に興味のある学生のみなさん、一緒に楽しい夏を過ごしましょう!https://t.co/SVyMOzaeq8
— YAMAMOTO Masaya (@pandax381) April 26, 2019
万人に向けたイベントでもないし、SNSを通じてニッチな層に届けばいいやくらいの軽い気持ちだったので、イベントの告知は僕の個人アカウントによる上記のツイートのみです。 この時点ではマジでどのくらい集まるのか未知数で「追加で数名集まったらわいわい楽しくできそうだけど、果たして応募してくれる人はいるのだろうか...」と不安な気持ちでいっぱいでした。
そんな不安をよそに、つよつよの学生達や「拙者、この分野は素人でござるが」みたいなこと言い出しそうな強い大人の方々にもRTされ、それなりに拡散されることになりました。面白そう!と反応してくれたみなさん、本当にありがとうございました。
そして、いざ蓋を開けてみると総勢20名以上の学生がエントリーしてくれ、最終的に13名の参加者が決まりました。 募集しておいてなんですが「世にはプロトコルスタックを自作したい学生がそんなにいるのか...」と正直びっくりしましたね。
(多数エントリーいただいて嬉しかった一方で、運営のリソース的に希望者全員を受け入れることができず、参加者を絞らなければならなかった点はすごく心残りです。残念ながら今回は参加できなかった方も次回の開催につなげることができたらその際はまたエントリーして欲しいなと思います)
開催までの準備
告知から開催まではちょうど4ヶ月くらいあったので、そのあいだに人事の担当者と細かなことを決めつつ準備を進めていきました。といっても、僕が雑に「あれやりたい」「これやりたい」と言ったものを、イベント慣れしている人事の担当者がよしなにアレンジして的確に落とし込んでくれるという連携プレーで進めていきました。だいぶ無茶振りもしたと思うのに、いい感じでまとめてくれて本当にありがとうございました。
以下は個人的に気に入っていて満足しているポイントです。
- 参加者と運営メンバー全員分のIDカード(プラスチック製)
- お昼のお弁当
- 最終日に授与した修了証
どれも過去に自分が参加したり運営に携わったイベントで「これ良かったなぁ」「嬉しかったなぁ」と印象に残っていたものを盛り込んでもらいました。 参加者からのアンケートでも「お弁当おいしかった!」とか「記念に残るものがもらえて嬉しかった!」とか、それなりに喜んでもらえていたようで、すごく嬉しいです。
あとは、教材として使う予定の「リファレンス実装」をもう少し理解しやすい作りに改良したり、解説用の資料を作ったりと、なかなか忙しくギリギリまで準備を進めていました。
イベント期間中の様子
運営スタッフや参加者が以下のハッシュタグを付けてツイートしてくれているので、これらのタイムラインを追ってもらうと、どんな雰囲気で開催されていたのか分かる思います。(ごはんのツイートが多いので飯テロ注意)
開催してみて
まず、これだけの人数の参加者に集まってもらえたことが驚きと共に一番うれしかったです。そして、皆さん起床ミッションに失敗することもなく初日から最終日まで黙々と開発に取り組んでくれていたのが本当に凄いと思いました。
あと、このイベントを開催した目的のうちの1つでもあるんですけど、参加者同士の交流が活発に行われていたようで良かったです!尊い。いや、これまで個別にインターン生として受け入れていた学生達とのやり取りの中で「ニッチな分野になるとなかなか周りで同じレベル感で話せる友達や仲間がいなくて寂しい(とくに地方の場合)」という話が上がることが多かったんですよ。なので、今回のイベントでは全国各地から同じニッチな分野に興味を持っている学生が集まる絶好の機会だし、そういった交流の場としても機能して欲しいなと思っていたのでした。
このイベントに参加して「TCP/IPを完全に理解した」となった方、その先に進んで「TCP/IPまるでわからん」となった方「TCP/IPチョットデキル」の域に達した方、得られたものはそれぞれ異なると思いますが、参加者全員に「楽しかった」と思ってもらえていたら開催した甲斐があったかなと思うし、第2回はもっとクオリティ高くヤルぞ!という気持ちになります。
謝辞
もともとこのイベントは僕が一人で講師を務めるつもりで企画がスタートしたのですが、予想を上回る参加人数となったことから、社内のエンジニアに協力を仰いで講師やサポート役を引き受けてもらいました。
また、社内からだけではなく外部の方にも講師役を引き受けて頂きました。
チューターお待ちしています🤗
— YAMAMOTO Masaya (@pandax381) June 7, 2019
ねりさんはプロトコルスタック自作クラスタとして以前から認識していて、#tcfm のミートアップでのLTやサイボウズ・ラボユースでの活躍を見て「ヤバイ人がいる」とウォッチしていたのですが、こんな雑な絡み(なんとこれが初コンタクトなんですよ)にも関わらず、快く引き受けていただき本当にありがとうございました。
僕はR&D部門に所属しているものの、学問として研究をしたことがないため、研究者の立場として参加者に対してアドバイスや相談に対応してくれていたのはものすごく心強かったし、めちゃくちゃカッコいいなと感じました。
他にも、めんどくさい細々としたタスクを含めキッチリこなしてくれた人事の担当者や、仕事の合間にIDカードのデザインを引き受けてくれたデザイナーさんなど運営に関わってくれたみなさん、ほんとうにありがとうございました!
参加者のブログ記事
最後に、観測できている範囲で参加してくれたみなさんが書いてくれたブログ記事へのリンクを貼っておきます。
TCP/IP プロトコルスタックを自作した - kawasin73のブログ
KLab Expert Campに参加したよ。 - よくきたわね、いらっしゃい
KLab Expert Camp に参加しました - veqcc’s diary
KLab Expert Camp にチューターとして参加した - Around The Computer
KLab Expert Campに参加してきました - teru_0x01.log
第1回 KLab Expert Campに行ってきました(TCP/IPプロトコルスタック自作インターン) - 迫真の氷結晶
外部向け HTTP 通信の再送検知ツールを作った話
このエントリは KLab Advent Calendar 2017 の最終日の記事です。
こんにちは。インフラ担当 高橋です。
このエントリでは、つい先日、少し変わった仕組みの外部向け HTTP 通信の 3way ハンドシェイクの再送検知ツールを作りましたのでご紹介します。
きっかけ
このツールを作ったのは、DSAS の外部で発生するネットワーク障害を検知できるようにするためです。
例えばこのような障害がありました。
DSAS は複数のデータセンタで稼働しているのですが、ある日、特定データセンタの Web サーバから外部 API への接続が重くなる事象が発生しました。
原因は外部ネットワークからの DDoS 攻撃により、データセンタのネットワークの一部区間が飽和状態になったことによるものでした。
またある日は、特定のデータセンタの Web サーバから外部の特定 API に接続ができなくなりました。
原因は外部ネットワークの途中経路の ISP で伝送装置が故障したことで、経路変動が発生し、通信が不安定になったことによるものでした。
このように DSAS 外部で発生するネットワーク障害の原因は様々であり、対応も状況によって都度変化します。DSAS 内のネットワーク、データセンタの回線、その先の途中経路、外部 API のサーバなど、切り分けしなければならない箇所は多くあります。
特に途中経路の障害というのは、データセンタの回線や外部 API には問題が発生しておらず、障害箇所の切り分けにも時間がかかります。
このような障害はサービスへの影響に直結するため、インフラ担当はできるだけ早く障害に気づき、対応を開始しなければなりません。
そのため、インフラ担当が DSAS 外部で発生するネットワーク障害を検知できる仕組みを導入することにしました。
どのようにして検知するか
障害を検知するには、障害が発生しそうな箇所を想定し、そこを監視する必要があります。
DSAS 内部とは異なり、DSAS 外部で発生するネットワーク障害の原因は千差万別で、且つ監視できる箇所は限られてきます。
何を検知するか
まずは、先ほど例に挙げた障害を振り返ってみることにします。
このような障害の場合、DSAS のサーバやスイッチは正常なためアラートは発生しません。案件担当からの「外部 API の応答が遅い、もしくはタイムアウトしてエラーになる」といった問い合わせがきっかけで障害に気づくことになります。
つまり、外部 API への HTTP 接続の応答が遅くなったり、タイムアウトが発生していることをきっかけに障害を検知しているということになります。
それでは、外部 API への HTTP 接続の応答時間が長ければネットワーク障害が発生しているかというと、外部 API の処理に時間がかかっている可能性もあり、そうとは言い切れません。
このような障害が発生すると、インフラ担当は モニタリングシステム (Ganglia) のグラフを確認します。
グラフには HTTP の外向きの通信についての項目があり、該当時刻に Web サーバで SYN パケットの再送が大量に記録されていました。
HTTP 通信の SYN パケットの再送が起きているということは、外部 API に SYN パケットが届かなかったり、外部 API からの SYN/ACK パケットが Web サーバに届かなかったりしている可能性が高いです。
これを監視すれば DSAS 外部のネットワーク障害を検知できそうです。
何を使って検知するか
直接、監視サーバから外部への HTTP 監視を行うには、ヘルスチェックのような監視用 URL が必要になりますが、インフラ担当では用意することができませんので、案件担当と連携して監視対象 URL のリストを作成し、管理しなければなりません。
DSAS では複数案件のサービスが稼働しており、その全ての案件のリストを管理するのは大変ですので、それは避けたいという思いがありました。
それに加えて今回は、HTTP 接続の応答時間やステータスコードではなく、SYN パケットの再送を監視する必要があります。
「監視対象 URL を管理せずに、外部向け HTTP 通信の SYN パケットの再送を検知したい」
この要望を叶えることのできるツールが、意外と身近にあることに気づきました。
モニタリングシステムのグラフ作成に使用している tcpeek です。
tcpeek とは
tcpeek は KLab が作成した 3way ハンドシェイク時に発生するエラーを監視・集計するネットワークモニタで、エラー検出、再送検出、フィルタ、データ出力といった機能を備えています。
GitHub - pandax381/tcpeek: TCP 3way-handshake monitor
こちらのエントリに詳細が書いてあります。
ログからは見えてこない高負荷サイトのボトルネック再送検出
指定したインタフェースを監視し、タイムアウト時間内に SYN の再送が発生すると SYN Segment Duplicate (dupsyn)、接続先から SYN/ACK が再送されてくると S/A Segment Duplicate (dupsynack) がカウントされます。
タイムアウト時にも再送は発生していますが、tcpeek の再送検出は 3way ハンドシェイク成功時に再送が発生していればカウントされるようになっています。
想定される原因としては、途中経路の帯域の輻輳やパケ落ちなどが挙げられます。
エラー検出
指定したインタフェースを監視し、RST パケットを受け取ると Connection Rejected (reject)、ICMP Destination Unreachable パケットを受け取ると ICMP Unreachable (unreach)、接続がタイムアウトすると Connection Timed Out (timeout) がカウントされます。
タイムアウトについては、アプリケーションによりタイムアウトまでの時間は異なりますので、tcpeek 起動時に指定するタイムアウト時間 (デフォルト 60秒) を超えたセッションは Connection Timed Out としてカウントされるようになっています。
想定される原因としては、接続先の TCP リセット、途中経路のルーティングテーブルの消失、接続先の無応答などが挙げられます。
tcpeek を使った SYN パケット再送検知の仕組み
tcpeek を使えば外部向け HTTP 通信に絞った 3way ハンドシェイクの再送発生 (dupsyn / dupsynack) や接続失敗 (reject / unreach / timeout) の計測値を取得することができますので、そのカウントが増えたらインフラ担当に通知するツールを作成することにしました。
====== TCPEEK SUMMARY ======
----------------------------
http-out
----------------------------
success
total 17457
dupsyn 3
dupsynack 0
failure
total 113
reject 39
unreach 45
timeout 29
============================
tcpeek は Web サーバで動作しますので計測値は Web サーバで取得することになります。
Web サーバから直接インフラ担当に通知する仕組みだと、多数の Web サーバで HTTP の再送が発生すると通知の数が大量になってしまいます。
そのため、Web サーバの計測値をキャッシュに格納して集約し、検知ツールでキャッシュにある統計情報をチェックする仕組みにしました。
計測値のカウントが増えており、その数がしきい値を超えていると slack とメールでインフラ担当に通知します。こちらは slack への通知例です。
おわりに
今回、このツールを作る際に一番悩んだのが、通知内容としきい値の設定方法でした。
Web サーバ単位でカウントを通知すると、台数が多いと通知が縦に長くなってしまい、特に slack では扱いづらくなるため、案件単位で通知をまとめるようにしました。
また、Web サーバ毎にしきい値を設定すると、特定の接続先の障害で再送などが発生した場合、何台の Web サーバで再送が発生しているのか状況を把握しづらくなるため、案件の Web サーバ全台の合計カウント数をしきい値としています。
この 2点はまだまだ改善の余地があると思っており、これからもブラッシュアップしていきたいです。
もちろん tcpeek は外部向け HTTP 通信だけではなく、様々な通信のフィルタを作成し集計できますので、他の通信に対してもこの検知の仕組みは適用できます。
Brocade VDX NOS 7.0 は Python が動く!
KLab では物理のサーバインフラを複数システム運用していますが、最近そのうちの1つでネットワークスイッチのリプレースを実施しようとしています。新たに導入するスイッチは Brocade 社の VDX6740 なのですが、このスイッチの OS である NOS 7.0 では、なんとスイッチ上で Python を動かすことができます。ということで少し触ってみました。
とりあえず触ってみる
使い方は、 UNIX の shell から Python を使うのと同じ感覚です。まず、通常通りスイッチに ssh などでログインします。
$ ssh -l admin vdx-switchこの状態で VDX の CLI コマンドを入力するのと同様に、
Welcome to the Brocade Network Operating System Software
admin connected from 192.168.0.10 using ssh on sw01
sw01#
python
と入力すれば、python が対話モードで起動します。
sw01# python見て分かるように、バージョンは 3.3.2 です(ちょっと古いですね…)。
Python 3.3.2 (default, Apr 28 2016, 15:57:52)
[GCC 4.3.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
探索
普段 Python のスクリプトを利用するのと同様に python
コマンドの引数にスクリプトファイル名や -m
でモジュール名を指定すれば、対話モードを起動するのではなく、指定した Python スクリプトを動かすこともできます。
sw01# python -m pydoc
pydoc - the Python documentation tool
pydoc...
Show text documentation on something.may be the name of a
Python keyword, topic, function, module, or package, or a dotted
reference to a class or function within a module or module in a
package. Ifcontains a '/', it is used as the path to a
Python source file to document. If name is 'keywords', 'topics',
or 'modules', a listing of these things is displayed.
(略)
ライブラリ類はどうなっているのか、 python -m pydoc modules
の結果を整形したものを、Linux OS にインストールされていた同じバージョン 3.3.2 の Python のものと比べてみました。比較対象の Python 環境は概ね素に近い環境のはずです(ちょっと余計なものが見えてますが…)。左が VDX のもので、右が Linux のものになります。
2d11つめの
< CLI
25a25
> _dbm
29a30
> _gdbm
36a38
> _lzma
103d104
< curl
137a139
> idlelib
147a150
> lib2to3
192d194
< pycurl
196d197
< pythonStarup
203d203
< requests
252a253,254
> turtle
> turtledemo
254a257
> unittest
CLI
は、Pythonスクリプトから VDX の CLI コマンドを発行するためのライブラリです。それ以外では HTTP 関連のライブラリが追加されているようです…
さすがに setuptools
や pip
は入ってないので VDX の
Python 環境内から新規パッケージを追加するのは無理そうですが、標準ライブラリの範囲内であればひと通り使えそうです。
ユーティリティの作成
ということで、早速ちょっとしたスクリプトを作ってみました。こちらで公開しています。diff.py
スイッチの運用をする上で、同じ設定を複数のポートに施すことはよくありますが、本当に同じ設定になっているのか、という確認を CLI 上で行うには、人間の目で頑張ってチェックするしかなく、機械的行うにはスイッチの外に show
コマンドの結果などをもっていって行う必要があります。しかし、毎回スイッチ上でのコマンドの実行結果を手元にコピーして比較するのは面倒でした。
Pythonの標準ライブラリには difflib
があるので、diff
コマンドぽいものを作ってみました。スイッチ上のファイル同士や引数に渡されたコマンド列の実行結果を使ってdiff
を取ります。詳しい使い方は github
にある README.md
を見て下さい。
sample__port_parse_and_compose.py
こちらは別に作ったスクリプトの、コマンド引数の解析部分を抜き出したものです。スクリプトに対して操作するスイッチのポート番号を指定するための引数を解析する argparse
コードとチェック用関数、VDXスタイルのポート番号表現を組み立てる関数が入っています。VDX用のスクリプトを作成される際に活用できそうでしたら、作成されるスクリプトの目的に合うように修正しつつ、お使いください。
ラズパイで作るネットワークエミュレータ(前編)
ネットワークが絡んだ通信プログラムを開発していると、テストのために遅延やパケロスを意図的に発生させたくなることがあります。いまどきは IDE にネットワークエミュレーション機能が組み込まれていたり、仮想環境で容易に再現できたりもしますが、箱物のネットワークエミュレータがあるとネットワークの構成を気にせずカンタンに設置できるのですごく便利だったりします。世の中にはそういった製品が沢山あるので安価なものを買ってもいいのですが、新たにラズパイが届いたばかりだったので、これを使って超小型のネットワークエミュレータを自作してみました。前編と後編の二回に分けて紹介します。
最近、社内で「ラズパイおじさん」と呼ばれるようになりました。@pandax381 です。
ラズパイ + Linux = ネットワークエミュレータ
「ネットワークエミュレータを自作」と言うとなんだか凄そうな感じがしますが、実はものすごく簡単につくれます。なぜなら、ネットワークエミュレータに必要な機能を Linux が全て備えているからです。Linux をインストールしたラズパイがあれば、数万円の価格帯の製品と同等のことができます(というより、この価格帯の製品は単に Linux の機能を利用しただけのものが多いです)。
動作イメージ
作成したネットワークエミュレータは、ブリッジ(中継機)として動作するタイプのものです。ネットワーク機器同士の間に接続し、通過するパケットに対して遅延やパケロスを発生させます。
また、イーサネットポートを備えていないラップトップやスマートフォンの場合には、ネットワークシミュレータを「アクセスポイント〜スイッチ間」に設置することで、そのアクセスポイントに接続している端末の通信パケットに対して制御を加えることができます。例えば、Wi-Fi につないだままモバイルネットワークと同等の品質で通信するといったことができるようになります。
イーサネットポートが足りない問題
ブリッジとして動作させると言ったものの、ラズパイに搭載されているイーサネットポートは1つだけなので、物理的にポートが足りなくてネットワークをブリッジできません(個人的には2ポートのモデルがあるとすごく幸せなんですけど、一般的にはあまり需要ないんですかねぇ...)。
これはもう足りないなら増やすしかないので、ちょっと格好悪いですがUSBイーサネットアダプタを接続してポートを増設します。ラズパイに内臓されているイーサネットポートは100Mなので、増設するイーサネットアダプタも100Mで十分です。無駄にギガビットのアダプタを接続しても、ブリッジした際にもう片方の性能に引っ張られるので意味がないです。個人的には Logitec LAN-TXU2C あたりが安価で入手性もいいのでオススメです。Raspbian にはこのアダプタが使っているチップのドライバ(asix.ko)が入っているので、USBポートに接続するだけで認識します。
なお、USBイーサネットアダプタはそれなりに電力を消費するため、調子に乗って何個も接続するとラズパイ本体が電力不足で不安定になって落ちたりするので自重しましょう。
VLANスイッチと組み合わせるとイーサネットポートを増設しなくても済むようになりますが、それは後編で紹介します。
ブリッジを作ってパケットを通過させる
まず、ブリッジを作成してラズパイの持つ2つのイーサネットポート間でパケットが通過できるようにします。
ブリッジを作成するには、bridge-utils がインストールされている必要があります。Raspbian のイメージならインストールされているはずですが、なければ apt でインストールします。
# apt-get install bridge-utils
ブリッジの作成や操作は brctl
コマンドで行います。brctl addbr
でブリッジ を作成し、brctl addif
でブリッジにインタフェースを追加します。ここでは、br0 という名前で新しくブリッジを作成し、そこに eth0 と eth1 を追加します。
# brctl addbr br0
# brctl addif br0 eth0 eth1
作成したブリッジは brctl show
で確認できます。
# brctl show
bridge name bridge id STP enabled interfaces
br0 8000.3495db294586 no eth0
eth1
仕上げに、ip
コマンドでインタフェースとブリッジを起動させれば、このラズパイは2ポートのハブのように振る舞いはじめます。試しに、PCとスイッチの間にこのラズパイを接続しても、PCは何事もなくこれまでと同じように通信できるはずです。
# ip link set eth0 up
# ip link set eth1 up
# ip link set br0 up
遅延とパケロスを発生させる
ブリッジを作成してパケットがラズパイを通過できるようになったので、次は通過するパケットに対して遅延やパケロスを発生させる設定をします。
Linux カーネルにはトラフィックコントロールやネットワークエミュレーションの機能が備わっていて、Raspbian を含めたほとんどのディストリビューションで有効になっています。
iproute2
に含まれる tc
コマンドを使って、送信トラフィックに対して以下の制御を加えることができます。
- 帯域制限
- 帯域保証
- 遅延
- パケットロス
- ビットエラー
- 重複
- 順序入れ替え
ネットワークエミュレータを名乗るからには全ての機能を取り入れたいところですが、今回は取り急ぎ必要だった 遅延
と パケロス
のみを扱います。
tc コマンドを使いこなすと高度なトラフィック制御ができるようになりますが、Linux のトラフィックコントロールの概念はなかなかに複雑で、tc のコマンド体系もそれに習って難解です。ここでは決まった使い方しか示しませんが、tc コマンドやトラフィックコントロールの詳細について興味がある方には、GREEさんのエンジニアブログに掲載されている「よくわかるLinux帯域制限」という記事が秀逸でオススメです。
tc コマンドは複雑ですが、遅延とパケロスを設定するだけなら簡単です。例えば、eth0 に対して遅延とパケットロスを設定するには以下のコマンドを実行するだけです。これで、eth0 から「出て行く」パケットに対して 100ms の遅延が加わり、5% の確率でパケットロスが発生します。
# tc qdisc add dev eth0 root netem delay 100ms loss 5%
なお、新しく設定を追加する時は tc qdisc add
ですが、設定済みの値を変更する際は tc qdisc change
です。値が設定されていないのに change を指定したり、設定済みなのに add を指定するとエラーになります。
# tc qdisc change dev eth0 root netem delay 200ms loss 10%
設定した値は tc qdisc del
で消せます。消去した後の再設定は tc qdisc add
です。
# tc qdisc del dev eth0 root
ここでは、遅延やパケロスを固定的に発生させる方法を示していますが、遅延時間やパケロス発生頻度を一定の範囲内で分散させたりすることもできます。
UIについて考える
このように、Linux の標準的な機能を組み合わせることで、ラズパイを超小型のネットワークシミュレータに仕立て上げることができます。ただし、この状態だとめちゃくちゃ使いにくいです。遅延やパケロスの値を変えるのに、その都度シリアル経由でログインして tc コマンドを実行するとかありえないです。自分が手元で使うにしても、もう少し気の利いた UI が欲しいと思ってしまいます。
ネットワーク機器なので、シリアルでCLIを提供するというのも個人的にはアリですが、他の人に勧めようとした際に「USBシリアル変換ケーブルとか持ってないよ」と言われてしまう可能性が高いので微妙です。無難に Web で GUI を提供するのが現実的だと思いますが、メンテナンス用のイーサネットポートがないのでIPアドレスを割り当てる先がなく、これも微妙です。
どうにも決めかねていたので、たまたま通りがかった同僚に相談してみたところ「直感的なやつ」「アナログな感じで」「つまみでぐりぐり設定したい」「液晶もつけよう」「アキバ行こう」などと煽られました。
「まぁラズパイを使ってるんだから電子工作でアナログな UI を作るのも面白いなぁ〜」と思いはじめ、悪乗りしてネタにマジレスすることにしました。
UI(物理)を試作
パーツ一覧
まず、ラズパイ本体とケース、外付けのUSBイーサネットアダプタです。前回はRSコンポーネンツ製の純正ケースを買ったのですが、Eleduino のアクリルケースがお洒落だったのでこちらを選んでみました。ヒートシンクの効果は不明ですがカッコいいので付けてます。
- Raspberry Pi 2 Model B
- Eleduino Raspberry Pi 2 Model B black Acrylic Case
- Logitec 有線LANアダプタ USB2.0 LAN-TXU2C
続いて、UI用の工作パーツです。全て秋月で調達できます。コンセプトの「ぐりぐりして設定する」を実現するために、ボリューム(可変抵抗)を使います。なお、ラズパイはアナログ入力を備えていないので、SPI接続のADコンバータを使ってボリュームの値を読み取ります。設定した値を表示するために、I2C接続のキャラクタLCDも用意します。
- ミニブレッドボード BB-601(白)
- 10ビット2ch ADコンバータ MCP3002-I/P
- 小型ボリューム 10KΩA(16K4)
- カラーつまみ(黒+青)
- カラーつまみ(黒+桃)
- I2C接続小型LCDモジュール(8x2行)ピッチ変換キット
UI用の工作パーツだけなら千円程度で揃います。
組み立て
回路図とか書けないクソザコなので、配線図的な何かを載せておきます。
設定した値を表示させるためのキャラクタLCDです。8文字×2行なので表示できる情報量は少ないですが、ものすごく小さいです。ピッチが狭いので変換基盤への半田付けが少し大変ですが、電子工作初心者で半田付けに不慣れな僕でもなんとかできたので大丈夫です。ちなみに、弊社では、その辺に FLUKE のオシロスコープが転がっていたりするのですが、なぜか半田ごてが備品として存在しておらず「これは由々しき問題だ」ということで、新たに温調式の半田ごてが備品に仲間入りすることなりました。
ボリュームは、GND(左)OUT(中)VCC(右)に接続するので、ジャンプワイヤを半田づけしておきます。
あとは、ブレッドボードとボリュームを両面テープでラズパイの上に固定して、ひたすら配線します。ブレッドボード付属の両面テープはかなり強力なので慎重に...貼ってしまうと剥がすのが大変です。ボリュームはクッション入りの両面テープで貼るとグラつかずに固定できます。
ちょっとジャンプワイヤがごちゃごちゃしていますが、まぁプロトタイプなのでこれでいいでしょう。ついでにUSBイーサネットアダプタも両面テープで側面に貼ってしまいます。なんだかそれっぽいガジェットに見えるようになってきましたね!
I2C接続のLCDが認識できない問題
秋月で調達したI2C接続のLCD「AE-AQM0802」が認識できないという問題に遭遇しました。ピッチ変換基盤への半田付けをミスったのかと思い、完成品を買ってきても認識してくれませんでした。少し調べてみたところ、同じようにラズパイで認識されないと報告している方がいました(逆にちゃんと動作したという方もいました)。
電子工作初心者のため、あまり良くわかってないのですが、どうも「I2C で使う GPIO ピンはラズパイ本体側に固定的にプルアップ抵抗が実装されていて、その抵抗値が小さすぎるためにうまく認識できない」ということのようです。解決策っぽいものの中で一番確実で分かりやすかったのが「ラズパイ本体側のプルアップ抵抗を取り除く」でした。ちょっと荒技な気もしますが、えいやでやってみたら見事に動いてくれました。
ラズパイ本体側のプルアップ抵抗とは、GPIOピンのすぐそばに表面実装されている R23 / R24 のチップ抵抗です。そのまま半田ごてを当ててもなかなか取れないので、半田を盛ってから吸い取り線で一緒に吸ってしまうとキレイに取れます。
ピッチ変換キットには10KΩのプルアップ抵抗が実装されていてるので、これをショートさせて有効にします。
その後、さらに調べてみたところ、ストロベリーリナックス製の LCD「SB0802GN」が、秋月のものとほぼ同じ仕様(形状もコントローラも同じ)で、これなら問題なく認識されるようです。ラズパイ本体に手を加えたくない場合には、代わりにこちらの LCD を使うのがいいかもしれません。
コードを書く
ラズパイの設定変更
ADコンバータとLCDの接続に、それぞれ SPI と I2C を使うので、これらを有効にするための設定をします。
/boot/config.txt
に以下の内容を追記します。
dtparam=i2c_arm=on
dtparam=spi=on
/etc/modules
に以下の内容を追記します。
i2c-bcm2708
i2c-dev
spi_bcm2835
再起動後に lsmod
を実行して指定したモジュールがロードされていれば大丈夫です。
# lsmod
Module Size Used by
sch_netem 8032 0
bridge 93064 0
ipv6 339514 17 bridge
stp 1479 1 bridge
llc 3600 2 stp,bridge
i2c_dev 6047 0
asix 19981 0
libphy 24139 1 asix
i2c_bcm2708 5014 0
spi_bcm2835 7248 0
uio_pdrv_genirq 2966 0
uio 8235 1 uio_pdrv_genirq
必要なツールとライブラリのインストール
制御プログラムをC言語で書きますが、超絶便利なライブラリ「Wiring Pi」を使います。Wiring Pi は、ラズパイの GPIO を制御するためのライブラリで SPI や I2C にも対応しています。今回はADコンバータからボリュームの値を読み出す処理で Wiring Pi を使います。LCDの制御に関しては手抜きをして i2c-tools に頼ります。
- i2c-tools
- libi2c-dev
- Wiring Pi
i2c-tools と libi2c-dev を apt でインストールします。Wiring Pi は git のリポジトリを clone してビルド&インストールします。
# apt-get install i2c-tools libi2c-dev
# git clone git://git.drogon.net/wiringPi
# cd wiringPi
# ./build
制御プログラム
アナログUIの制御プログラムです。一秒おきにADコンバータからボリュームの値を読み取り、前回の値から変改していたら tc コマンドの実行とLCDへの描画を行います。ほとんどの処理を system() 経由で外部コマンドに丸投げしていたり、エラー処理が手抜きだったり、かなり雑に作っていますがプロトタイプなのでご愛嬌。
/* * packdrop.c */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <wiringPi.h> #include <wiringPiSPI.h> #include <mcp3002.h> #define APP_NAME "PackDrop" #define APP_VERSION "v0.4" #define SPI_CHANNEL (0) #define PINBASE (100) #define VOLUME_DELAY (PINBASE) #define VOLUME_LOSS (PINBASE+1) char **devices; int devnum; static void tc_set (const char *dev, int delay, int loss) { char cmd[128]; snprintf(cmd, sizeof(cmd), "tc qdisc change dev %s root netem delay %dms loss %d%%", dev, delay, loss); system(cmd); } static void tc_init (const char *dev) { char cmd[128]; snprintf(cmd, sizeof(cmd), "tc qdisc del dev %s root >/dev/null 2>&1", dev); system(cmd); snprintf(cmd, sizeof(cmd), "tc qdisc add dev %s root netem delay 0ms loss 0%%", dev); system(cmd); } static void lcd_write_core (const char *upper, const char *lower) { char cmd[128]; system("i2cset -y 1 0x3e 0 0x80 b"); snprintf(cmd, sizeof(cmd), "i2cset -y 1 0x3e 0x40 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x i", upper[0], upper[1], upper[2], upper[3], upper[4], upper[5], upper[6], upper[7]); system(cmd); system("i2cset -y 1 0x3e 0 0xc0 b"); snprintf(cmd, sizeof(cmd), "i2cset -y 1 0x3e 0x40 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x i", lower[0], lower[1], lower[2], lower[3], lower[4], lower[5], lower[6], lower[7]); system(cmd); } static void lcd_write (int delay, int loss) { char upper[9], lower[9]; snprintf(upper, sizeof(upper), " %3d ms", delay); snprintf(lower, sizeof(lower), " %3d %%", loss); lcd_write_core(upper, lower); } static void lcd_init (void) { char name[9], ver[9]; system("i2cset -y 1 0x3e 0 0x38 0x39 0x14 0x78 0x5f 0x6a i"); system("i2cset -y 1 0x3e 0 0x0c 0x01 i"); system("i2cset -y 1 0x3e 0 0x06 i"); snprintf(name, sizeof(name), "%8s", APP_NAME); snprintf(ver, sizeof(ver), "%8s", APP_VERSION); lcd_write_core(name, ver); } int main (int argc, char *argv[]){ int n, v, d, l; int delay = 0, loss = 0; lcd_init(); if (mcp3002Setup(PINBASE, SPI_CHANNEL) < 0) { printf("mpc3002Setup: failed\n"); return -1; } devices = argv + 1; devnum = argc - 1; for (n = 0; n < devnum; n++) { tc_init(devices[n]); } while (1) { v = analogRead(VOLUME_DELAY); d = ++v / 2; v = analogRead(VOLUME_LOSS); l = ((float)++v / 1024) * 100; if (d != delay || l != loss) { delay = d; loss = l; for (n = 0; n < devnum; n++) { tc_set(devices[n], delay, loss); } lcd_write(delay, loss); } sleep(1); } return 0; }
Wiring Pi のライブラリを使っているので、-lwiringPi
を指定してコンパイルします。
# gcc -W -Wall -o packdrop packdrop.c -lwiringPi
動かしてみる
実際に制御プログラムを動かして、ちゃんと遅延とパケロスが発生するのかを確認します。
まず、事前にブリッジを作成しておきます。
# brctl addbr br0
# brctl addif br0 eth0 eth1
# ip link set eth0 up
# ip link set eth1 up
# ip link set br0 up
続けて、制御プログラムの packdrop を起動します。packdrop の引数には、ブリッジに追加したインタフェースを指定します。
# ./packdrop eth0 eth1
実際に動作させている動画です。
動画を見ると一目瞭然ですが、青いボリュームを回すと「遅延」が増加し、ピンクのボリュームを回すと「パケロス」が増加します。遅延もパケロスも「上りと下りの両方に適用」されるため、ping の RTT には設定した遅延の2倍の時間が加算され、パケロスが 10% の場合には 0.9 x 0.9 = 0.81(81%)の確率で応答パケットが届きます。
余談
このネットワークエミュレータには「PackDrop」という名前を付けたのですが、なぜか社内では「パケ落ちくん」という愛称で呼ばれています。
次回予告
半分ネタで雑に作ったネットワークエミュレータですが、実際に案件でデバッグに使ってみてもらったところ、わりと評判が良く、各所から「それ欲しい」という要望が出てきました。
ラズパイの在庫は豊富にあるので何台でも作れるのですが、指を引っ掛けたら配線が抜けてしまうんじゃないかとか、両面テープで固定しているだけなのでボリュームが取れてしまうんじゃないかとか、ちょっと物理的に脆弱すぎるという問題が...
他にも、ちゃんと起動スクリプトを作ったり電プチに対応したり、アプライアンス的に使えるようにした方が使い勝手が良さそうだったりします。そんな訳で、後編ではもう少し真面目に作った「量産型」を紹介します。
Thundering herd 対策の本命、 EPOLLEXCLUSIVE を試してみた
epoll を使った prefork 型アプリケーションサーバーにおける Thundering herd 対策の決定版として注目されていた EPOLLEXCLUSIVE が、 3/13 にリリースされた Linux 4.5 で導入されました。
昨年 SO_REUSEPORT というソケットオプションが登場して、 Thundering herd 対策として話題になったものの、ワーカーごとに listen キューが作られるため graceful restart するときに listen キューに入ってるリクエストを取りこぼす可能性があり利用するのが難しい状況でした。
参考: epoll の thundering herd 問題について解説しているサイト
- http://tech.geniee.co.jp/entry/so_reuseport
- http://uwsgi-docs.readthedocs.org/en/latest/articles/SerializingAccept.html
一方、 EPOLLEXCLUSIVE
は、 listen キューは一本のまま、その listen キューを待っている複数プロセスの epoll_wait()
を全部起こすのではなく、1つ以上(1つとは限らない)だけ起こすという機能です。
graceful shutdown 中は epoll_wait()
をしていないはずなので、 listen キューに来たリクエストは他のワーカープロセスの epoll_wait()
に通知され、取りこぼしがありません。
http://man7.org/linux/man-pages/man2/epoll_ctl.2.html
EPOLLEXCLUSIVE (since Linux 4.5) Sets an exclusive wakeup mode for the epoll file descriptor that is being attached to the target file descriptor, fd. When a wakeup event occurs and multiple epoll file descriptors are attached to the same target file using EPOLLEXCLUSIVE, one or more of the epoll file descriptors will receive an event with epoll_wait(2). The default in this scenario (when EPOLLEXCLUSIVE is not set) is for all epoll file descriptors to receive an event. EPOLLEXCLUSIVE is thus useful for avoiding thundering herd problems in certain scenarios.
使い方は、 EPOLL_CTL_ADD
するときに、 events で EPOLLIN
などと bitwise OR で組み合わせて利用します。
struct epoll_event ev = {EPOLLIN | EPOLLEXCLUSIVE, NULL}; epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
環境準備
Ubuntu 16.04 LTS xenial xerus や Amazon Linux 2016.03 といった最新(あるいはもうすぐリリースされる)のディストリビューションでも、採用しているのは Linux 4.4 なので、そのままでは試すことができません。 (Fedora 24 は Linux 4.5 らしいですが、普段使わないので除外しました)
そこで、 Ubuntu 15.10 を元に、 https://wiki.ubuntu.com/KernelTeam/GitKernelBuild にある手順に従い Linux 4.5 をソースからビルドして再起動することにしました。 書いてある手順通りで問題ないのですが、私の場合 Ubuntu 15.10 の公式 AMI のデフォルトディスクサイズ (8GB) で容量が足らずに一度ビルドに失敗して32GBに拡張したので、同じくAWSを使われる方は注意してください。
カーネルのインストールが終わって再起動したら、Linux 4.5 カーネルを利用していることを確認します。
また、 <sys/epoll.h>
にはまだ EPOLLEXCLUSIVE
が定義されていないので、定数の値を確認しておきます。
$ uname -a Linux ip-10-0-1-221 4.5.0-custom #1 SMP Thu Mar 31 07:35:29 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux ~/linux-4.5/include$ ag EPOLLEXC uapi/linux/eventpoll.h 30:#define EPOLLEXCLUSIVE (1 << 28)
EPOLLEXCLUSIVE を使った場合にどれくらい性能に影響がでるか、簡単なベンチマークをしたいので、 TIME_WAIT で詰まる問題を回避するおまじないをしておきます。
$ echo 'net.ipv4.tcp_tw_reuse = 1' | sudo tee -a /etc/sysctl.conf $ sudo sysctl -p net.ipv4.tcp_tw_reuse = 1
meinheld
meinheld は奥一穂さんの picoev を利用した Python アプリケーション用の HTTP サーバーです。
listen ソケット以外に、ワーカー間で共有しつつ epoll を利用する fd はないので、 EPOLL_CTL_ADD
しているところで無条件に EPOLLEXCLUSIVE
してしまいます。
diff --git a/meinheld/server/picoev_epoll.c b/meinheld/server/picoev_epoll.c index 06e1dbb..773cf3c 100644 --- a/meinheld/server/picoev_epoll.c +++ b/meinheld/server/picoev_epoll.c @@ -115,6 +115,7 @@ int picoev_update_events_internal(picoev_loop* _loop, int fd, int events) SET(EPOLL_CTL_MOD, 0); if (epoll_ret != 0) { assert(errno == ENOENT); + ev.events |= (1 << 28); // EPOLLEXCLUSIVE (from linux 4.5) SET(EPOLL_CTL_ADD, 1); } } @@ -124,7 +125,12 @@ int picoev_update_events_internal(picoev_loop* _loop, int fd, int events) if ((events & PICOEV_READWRITE) == 0) { SET(EPOLL_CTL_DEL, 1); } else { - SET(target->events == 0 ? EPOLL_CTL_ADD : EPOLL_CTL_MOD, 1); + if (target->events == 0) { + ev.events |= (1 << 28); // EPOLLEXCLUSIVE (from linux 4.5) + SET(EPOLL_CTL_ADD, 1); + } else { + SET(EPOLL_CTL_MOD, 1); + } } #endif
試験に使っていたのは AWS の c4.xlarge で、4コアなので、8ワーカー起動し、 "hello, world" を返すだけのアプリに ab で4並列で負荷をかけてみました。 (wrk ではなく ab を使ったのは、今回は keep-alive しない試験だったからです。)
結果は、改造前がだいたい 15k req/sec で、 改造後がだいたい 20k req/sec を超えるくらいでした。
uWSGI
Python だけでなくいろんな言語に対応している uWSGI というサーバーでも試してみました。
epoll に追加する関数が read と write で別れているので、accept で利用する read 側にだけ EPOLLEXCLUSIVE
を追加します。
diff --git a/core/event.c b/core/event.c index 36751a6..32a1934 100644 --- a/core/event.c +++ b/core/event.c @@ -514,7 +514,7 @@ int event_queue_add_fd_read(int eq, int fd) { struct epoll_event ee; memset(&ee, 0, sizeof(struct epoll_event)); - ee.events = EPOLLIN; + ee.events = EPOLLIN | (1 << 28); ee.data.fd = fd; if (epoll_ctl(eq, EPOLL_CTL_ADD, fd, &ee)) {
こちらもだいたい meinheld と同じような数値 (15k req/sec -> 20+k req/sec) で性能向上を確認できました。
また、1並列で ab を実行したときの accept の回数を、 strace で確認してみました。
ubuntu:~/app$ strace -f ~/.local/bin/uwsgi --master --workers=8 --http-socket :8080 --wsgi hello 2>trace.log ubuntu:~$ ab -c1 -n1000 http://localhost:8080/ # 別セッションから ubuntu:~/app$ grep -c '] accept4' trace.log # 改造前 6884 ubuntu:~/app$ grep -c '] accept4' trace.log # 改造後 1557
改造前は 6884 回ですが、これは 8 プロセスのうち accept に成功した 1 プロセスが、レスポンスを返してから epoll し直している間に、他のプロセスが次のリクエストを accept しているから、7000回弱になっているのでしょう。
一方改造後は 1557 回になっていて、リクエストの回数の約 1.5 倍になっています。 man に書いてあるとおり、必ずしも1つのイベントに対して1つの epoll だけが起こされるわけではなく、複数の epoll が起こされる場合があるのだと思います。
雑感
meinheld を strace していて気づいたのですが、 thundering herd 問題が起きている状況で multi accept (1度 epoll が起きたときに、 accept を1度だけでなく、エラーが出るまで複数回実行する) をすると、「起こされたのにすることがない」状況が発生しやすくなります。
keep-alive に対応しないシンプルなアプリケーション・サーバーなら、レスポンスを返したあと次にすることがなので、「自分(レスポンスを返したばかりのプロセス)が寝るか、他人(起こされたばかり)が寝るか」の違いでしかありません。自分で実行したほうが、コンテキストスイッチを待たずに処理を始められるので、ベンチマークスコアは上がると思います。 一方、 keep-alive に対応していたり、 nginx のようにいろんな処理を行う場合は、 multi accept を無効化した方が綺麗にコネクションを分散させることができて良いかもしれません。
あと、今回はかなり雑な感じに EPOLLEXCLUSIVE
対応したのですが、古いディストリビューションを使いつつもカーネルをアップグレードすることはよくあるので、 #ifdef EPOLLEXCLUSIVE
せずに、今回のような強引な方法で対応するのは意外と現実的な気がします。
実際、 xenial の標準のカーネル (Linux 4.4) で EPOLLEXCLUSIVE
を有効にしても、 epoll_ctl
がエラーを返すこともなく普通に動作しました。
@methane
Raspberry Pi 2 で NAT64 箱をつくってみた
最近の VirtualBox の記事の人気っぷりに嫉妬している @pandax381 です。
OSX 10.11(El Capitan)がリリースされ、NAT64 の話題をチラホラ見かけるようになってきましたね。弊社内でも例に漏れず話題に上がってきまして、なぜかデスクの上にラズパイが大量に転がっていたので、そこから1台使って NAT64 箱を作ってみました。
準備するもの
とりあえず Raspbian か 自前で Debian をインストールした Rapberry Pi 2 が1台あれば大丈夫です。ここに登場するラズパイは、ブートローダとカーネル(とカーネルモジュール)だけ Raspbian のものを使って、rootfs は debootstrap で wheezy を構築したものですが、純粋な Raspbian でも変わらないと思います。
NAT64とは?DNS64とは?
ぼくがヘタクソな説明しなくても、わかりやすい情報がたくさんあると思うのでそちらにお任せします。
http://qiita.com/shao1555/items/4433803419dfc72bf80b
DNS64 Server
NAT64 環境を構築するためには DNS64 が必ずセットになります。DNS64の説明は(ry。メジャーどころだと BIND と Unbound が DNS64 をサポートしています。ここでは、使い慣れている Unbound を使うことにします(どの宗派にも属してなければ、El Capitan も DNS64 のために Unbound を使っているので、とりえず Unbound 使っておいたらいいと思います)。
Unbound のインストール
Unbound の場合、1.5.0 から DNS64 をサポートしていますが、Debian だと sid 以外は 1.4 系のパッケージしかないので、最新版のソースコードをダウンロードしてビルドします。
$ wget https://www.unbound.net/downloads/unbound-1.5.6.tar.gz $ tar zxvf unbound-1.5.6.tar.gz $ cd unbound-1.5.6 $ ./configure --with-conf-file=/etc/unbound/unbound.conf $ make $ sudo make install
unbound.conf を以下のように記述します。
# /etc/unbound/unbound.conf server: verbosity: 1 pidfile: "/var/run/unbound.pid" module-config: "dns64 iterator" dns64-prefix: 64:ff9b::/96 dns64-synthall: yes interface: ::0 access-control: ::0/0 allow forward-zone: name: "." forward-addr: 8.8.8.8
付属の起動スクリプトをコピーして Unbound のデーモンを起動させます。
$ sudo cp contrib/unbound.init /etc/init.d/unbound $ sudo chmod +x /etc/init.d/unbound $ sudo insserv unbound $ sudo /etc/init.d/unbound start
AAAA を持たないホストに対する問い合わせ
まずは、通常のリゾルバ(8.8.8.8)経由で AAAA を持たないホスト(例えば www.klab.com)の AAAA を問い合わせてみます。当然ですが、AAAA を持たないホストの AAAA を問い合わせても、ANSWER SECTION が空の応答が返ってきます。
$ dig www.klab.com AAAA ; <<>> DiG 9.8.4-rpz2+rl005.12-P1 <<>> www.klab.com AAAA ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 1953 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0 ;; QUESTION SECTION: ;www.klab.com. IN AAAA ;; AUTHORITY SECTION: klab.com. 1799 IN SOA ns10.klab.org. postmaster.klab.org. 1446444958 16384 2048 1048576 2560 ;; Query time: 128 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Thu Nov 12 15:45:40 2015 ;; MSG SIZE rcvd: 90
続いてローカルで動いている Unbound に対して、先ほどのホスト(www.klab.com)の AAAA を問い合わせてみます。こちらは Unbound の DNS64 機能によって生成された AAAA が得られます。
$ dig @localhost www.klab.com AAAA ; <<>> DiG 9.8.4-rpz2+rl005.12-P1 <<>> @localhost www.klab.com AAAA ; (2 servers found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57883 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;www.klab.com. IN AAAA ;; ANSWER SECTION: www.klab.com. 3553 IN AAAA 64:ff9b::85f2:574 ;; Query time: 39 msec ;; SERVER: ::1#53(::1) ;; WHEN: Thu Nov 12 15:45:44 2015 ;; MSG SIZE rcvd: 58
なお、この AAAA は unbound.conf の「dns64-prefix」に設定したプレフィックス(64:ff9b::/96)と、問い合わせ対象ホストの A レコードを合成した結果になります。この例だと、64:ff9b::85f2:574 の下位32ビットを8ビット毎にドットで区切ると「133.242.5.116」となり、これは www.klab.com の A レコードと一致します。
AAAA を持っているホストに対する問い合わせ
もともと AAAA を持っているホストの場合にはどうなるのかも試してみます。通常のリゾルバ経由で AAAA も持っているホスト(例えば www.debian.org)の AAAA を問い合わせます。ごくごく普通に AAAA が得られます。
$ dig www.debian.org AAAA ; <<>> DiG 9.8.4-rpz2+rl005.12-P1 <<>> www.debian.org AAAA ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4534 ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;www.debian.org. IN AAAA ;; ANSWER SECTION: www.debian.org. 299 IN AAAA 2001:41c8:1000:21::21:4 www.debian.org. 299 IN AAAA 2001:610:1908:b000::148:14 ;; Query time: 322 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Thu Nov 12 16:40:22 2015 ;; MSG SIZE rcvd: 88
続いてローカルの Unbound 経由で問い合わせてみます。こちらも AAAA を得られましたが、さきほどの問い合わせ結果とは AAAA の内容が異なります。IPv6アドレスのプレフィクスからわかるように、これは DNS64 された結果です。
$ dig @localhost www.debian.org AAAA ; <<>> DiG 9.8.4-rpz2+rl005.12-P1 <<>> @localhost www.debian.org AAAA ; (2 servers found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 46630 ;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 0 ;; QUESTION SECTION: ;www.debian.org. IN AAAA ;; ANSWER SECTION: www.debian.org. 223 IN AAAA 64:ff9b::801f:3e www.debian.org. 223 IN AAAA 64:ff9b::599:e704 www.debian.org. 223 IN AAAA 64:ff9b::8259:940e ;; Query time: 1 msec ;; SERVER: ::1#53(::1) ;; WHEN: Thu Nov 12 16:40:26 2015 ;; MSG SIZE rcvd: 116
本来、Unbound の DNS64 のデフォルト動作は、AAAA を持つホストへの問い合わせは DNS64 の対象にせず、オリジナルの AAAA をパススルーで応答します。IPv6 のアップリンクがある環境ならなにも問題ないのですが、アップリンクが IPv4 のみの場合、ここで得られた IPv6 グローバルアドレスに到達することができません。その場合、もともと AAAA を持っているホストであっても DNS64 の対象にして、NAT64 経由で IPv4 のアップリンクを通じて通信する必要があります。
幸い、Unbound にはそのための機能があります。上記にて AAAA を持つホストも DNS64 の対象になっているのは、unbound.conf に「dns64-synthall: yes」と設定してあるためです。dns64-synthall を yes に設定すると、問い合わせ対象のホストが AAAA を持っているかどうかに関わらず、すべての問い合わせが DNS64 の対象になります(Unbound 自身が AAAA の問い合わせを行わずに、A レコードしか問い合わせなくなります)。
OSX 10.11(El Capitan)の unbound.conf でもこの機能が有効になっていますので、動作を合わせるという意味でも有効にしておくのがいいと思います。
IPv6 ネットワークの構築
ラズパイに NIC を追加して IPv6 Only のネットワークを構築します。
ネットワークインタフェースの設定
NAT64 箱を作るためには NIC が最低でも2つ必要ですが、ラズパイには NIC が1つしかありません。USB Ethernet アダプタで物理的に NIC を増設してもいいと思いますが、ここでは VLAN NIC を使うことにします。物理 NIC を増設している場合には、eth0.64 を eth1 などに読みかえてください。
IPv6 ネットワークのために eth0.64 という VLAN NIC を追加します。この NIC で使う IPv6 プレフィックスは、グローバルアドレスでなくて構いません。ユニークローカルアドレス(ULA)を生成して使うのがいいと思いますが、ここでは El Capitan と同じように 2001:2:0:aab1::/64 を使っています。また、eth0 には IPv6 アドレスはいらないので無効にしています。
NICのオフロード機能を無効にしていますが、これは後述する NAT64 の設定で必要になるためです。
# /etc/network/interfaces auto lo iface lo inet loopback auto eth0 iface eth0 inet static address 192.168.0.100 netmask 255.255.255.0 gateway 192.168.0.1 offload-tso off offload-ufo off offload-gso off offload-gro off offload-lro off up sysctl -w net.ipv6.conf.$IFACE.disable_ipv6=1 >/dev/null 2>&1 auto eth0.64 iface eth0.64 inet6 static address 2001:2:0:aab1::1 netmask 64 offload-tso off offload-ufo off offload-gso off offload-gro off offload-lro off
interfaces ファイルが書けたら eth0.64 を起動させます。
$ sudo ifup eth0.64
Router Advertisement
IPv6 ネットワークに接続したクライアントに IPv6 アドレスの自動生成をさせるために RA(Router Advertisement)を投げてあげます。RA を投げるために radvd をインストールします。
$ sudo apt-get install radvd
RA では DNS サーバを通知できないので(できるけど対応してるクライアントが少ない)、AdvOtherConfigFlag を ON にして DHCPv6 サーバへと追加情報を問い合わせるよう通知します。
# /etc/radvd.conf interface eth0.64 { AdvSendAdvert on; MinRtrAdvInterval 3; MaxRtrAdvInterval 10; AdvOtherConfigFlag on; prefix 2001:2:0:aab1::/64 { AdvOnLink on; AdvAutonomous on; AdvRouterAddr on; }; };
radvd.conf が書けたら radvd を再起動させます。
$ /etc/init.d/radvd restart
DHCPv6
IPv6 クライアントに対して DNS サーバを通知するために DHCPv6 サーバをインストールします。
$ sudo apt-get install isc-dhcp-server
/etc/default/isc-dhcp-server で DHCPv6 サーバの起動オプションなどを指定します。
# /etc/default/isc-dhcp-server DHCPD_CONF=/etc/dhcp/dhcpd6.conf DHCPD_PID=/var/run/dhcpd6.pid OPTIONS="-6" INTERFACES="eth0.64"
DHCPv6 用の設定ファイル /etc/dhcp/dhcpd6.conf を新しく作成します。
# /etc/dhcp/dhcpd6.conf default-lease-time 600; max-lease-time 7200; log-facility local7; subnet6 2001:2:0:aab1::/64 { option dhcp6.name-servers 2001:2:0:aab1::1; }
DNS(DNS64)サーバは自分自身なので、option dhcp6.name-servers には自分自身の IPv6 アドレスを指定します。
接続テスト
IPv6 ネットワークにクライアントを接続して、RA による IPv6 アドレスの自動生成と、DHCPv6 による DNS サーバの設定がなされていることを確認します。
clinet:~$ ifconfig en0 en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500 ether 5c:f9:38:xx:xx:xx inet6 fe80::xxxx:xxxx:xxxx:ad7e%en0 prefixlen 64 scopeid 0x4 inet6 2001:2::aab1:xxxx:xxxx:xxxx:ad7e prefixlen 64 autoconf inet6 2001:2::aab1:xxxx:xxxx:xxxx:ae10 prefixlen 64 autoconf temporary nd6 options=1<PERFORMNUD> media: autoselect status: active
clinet:~$ cat /etc/resolv.conf # # Mac OS X Notice # # This file is not used by the host name and address resolution # or the DNS query routing mechanisms used by most processes on # this Mac OS X system. # # This file is automatically generated. # nameserver 2001:2:0:aab1::1
正常に動いていれば、この状態で IPv6 クライアントから DNS64 の問い合わせができるようになっています。
clinet:~$ dig +short www.klab.com AAAA 64:ff9b::85f2:574
NAT64
いよいよ本題の NAT64 です。OpenBSD なら 標準機能の PF が NAT64 に対応していますが、Linux の場合にはサードパーティのソフトウェアをインストールする必要あります。ざっと調べた限り Linux なら「Jool」を使うのが一番良さそうです。Jool は Netfilter と連携して NAT64 を実現するカーネルモジュールで、NIC Mexico(メキシコの国別インターネットレジストリ)が開発しています。他の選択肢として Ecdysis というプロダクトもありますが、こちらは 2014年から更新されておらず、対応している Kernel が 2.6 までのようです。
カーネルヘッダのインストール
Jool はカーネルモジュールなので、これをビルドすためには Linux のカーネルヘッダが必要です。通常であれば $ sudo apt-get install linux-headers-$(uname -r) といった感じでコマンド一発でインストールできるのですが、rpi-update で最新のカーネル(4.1.y)を入れている場合、それに対応するカーネルヘッダのパッケージがラズパイのリポジトリで提供されていません。https://github.com/raspberrypi/linux/ にラズパイ用カーネルのソースがあるのでそれを持って来るのが正しいと思いますが、ちょっと面倒なので、ここでは親切な人が公開してくれているオレオレパッケージを使うことにします。
http://www.niksula.hut.fi/~mhiienka/Rpi/linux-headers-rpi/ にラズパイ用のカーネルヘッダの deb パッケージが公開されているので、カーネルバージョンに対応するパッケージをダウンロードしてインストールします。
$ wget http://www.niksula.hut.fi/~mhiienka/Rpi/linux-headers-rpi/linux-headers-4.1.7-v7+_4.1.7-v7+-2_armhf.deb $ sudo dpkg -i linux-headers-4.1.7-v7+_4.1.7-v7+-2_armhf.deb
Jool のインストール
カーネルヘッダがあれば Jool のインストールは簡単です。
$ wget https://nicmx.github.io/jool-doc/download/Jool-3.3.5.zip $ unzip Jool-3.3.5.zip $ cd Jool-3.3.5/mod $ sudo make $ sudo make modules_install
何故だかモジュールが /lib/modules/4.1.7-v7+/ ではなく /lib/modules/4.1.7-v7/ に作られてしまうため、ダサいですが移動させます..
$ sudo mkdir /lib/modules/4.1.7-v7+/extra $ sudo mv /lib/modules/4.1.7-v7/extra/jool* /lib/modules/4.1.7-v7+/extra/ $ sudo depmod -a
Jool は、NAT64 のためにホストに設定されている IPv4 アドレス以外に、サービス用の IPv4 アドレスを必要とします(ホストの IPv4 アドレスをサービス用に使うこともできますが、そうするとホスト自身が外部と通信できなくなってしまいます)。そこで、あらかじめ eth0 に NAT64 のサービス用 IPv4 アドレスを追加してから jool のカーネルモジュールをロードします。また、IPv4 と IPv6 のパケットフォワードも有効にしておく必要があります。sysctl で設定するだけだと再起動すると消えてしまうので、良い子は /etc/sysctl.conf にも記述しておきましょう。
$ sudo ip addr add 192.168.0.101/24 dev eth0 $ sudo modprobe jool pool6=64:ff9b::/96 pool4=192.168.0.101 $ sudo sysctl -w net.ipv4.conf.all.forwarding=1 $ sudo sysctl -w net.ipv6.conf.all.forwarding=1
接続テスト
IPv6 ネットワークに接続したクライアントから、本来は AAAA を持たないホスト宛に ping6 を投げてみます。正常に稼働していれば HTTP や SSH などの TCP 通信も問題なく行えるはずです。
clinet:~$ ping6 www.klab.com PING6(56=40+8+8 bytes) 2001:2::aab1:xxxx:xxxx:xxxx:ae10 --> 64:ff9b::85f2:574 16 bytes from 64:ff9b::85f2:574, icmp_seq=0 hlim=50 time=17.319 ms 16 bytes from 64:ff9b::85f2:574, icmp_seq=1 hlim=50 time=17.983 ms 16 bytes from 64:ff9b::85f2:574, icmp_seq=2 hlim=50 time=16.872 ms
Jool のユーザコマンド
Jool には、カーネルモジュール以外にユーザコマンドも付属しています。usr ディレクトリに入って Jool のユーザコマンドをビルド&インストールします。
$ cd Jool-3.3.5/usr $ ./autogen.sh $ ./configure $ make $ sudo make install
jool コマンドで、NAT64 のアクティブなセッションを確認したり、他にもいろいろできます。詳しくは Jool のマニュアルを読んでください。
$ sudo jool --session TCP: --------------------------------- (empty) UDP: --------------------------------- (empty) ICMP: --------------------------------- (empty)
おまけ
前置きが長くなりましたがここからが本題です。
最新の Jool-3.4.0 を使ってみる
Jool-3.3.5 では、ホストに設定されている IPv4 アドレス以外に、NAT64 サービス用に IPv4 アドレスが必要でしたが、最新の 3.4.0 のリリースメモには「サービス用の(セカンド)IPv4 アドレスが不要になった」と書かれています。サービス用の IPv4 アドレスが不要なら、NAT64 箱が DHCPv4 環境下でも動作するということになるので、設置のハードルがさらに下がることが期待できます。
先ほどと同じように Jool-3.4.0 のソースコードをダウンロードしてビルドします。
$ wget https://nicmx.github.io/jool-doc/download/Jool-3.4.0.zip $ unzip Jool-3.4.0.zip $ cd Jool-3.4.0/mod $ make make -C stateless make[1]: Entering directory `/home/pandax381/LOCAL/Jool-3.4.0/mod/stateless' make -C /lib/modules/4.1.7-v7+/build M=$PWD JOOL_FLAGS="" make[2]: Entering directory `/usr/src/linux-headers-4.1.7-v7+' CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/rfc6145/4to6.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/rfc6145/6to4.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/rfc6145/common.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/rfc6145/core.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/address.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/types.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/str_utils.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/packet.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/stats.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/log_time.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/icmp_wrapper.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/ipv6_hdr_iterator.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/pool6.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/rfc6052.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/nl_buffer.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/rbtree.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/config.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/nl_handler.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/route.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/send_packet.o CC [M] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/core.o /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/core.c: In function 'check_namespace': /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/core.c:70:25: error: invalid operands to binary != (have 'possible_net_t' and 'struct net *') make[3]: *** [/home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/../common/core.o] Error 1 make[2]: *** [_module_/home/pandax381/LOCAL/Jool-3.4.0/mod/stateless] Error 2 make[2]: Leaving directory `/usr/src/linux-headers-4.1.7-v7+' make[1]: *** [all] Error 2 make[1]: Leaving directory `/home/pandax381/LOCAL/Jool-3.4.0/mod/stateless' make: *** [stateless] Error 2
が、なんか make がエラー吐いて fail します...ここで諦めずに該当コードを確認します。
/* * Jool-3.4.0/mod/stateless/common/core.c */ static bool check_namespace(const struct net_device *dev) { #ifdef CONFIG_NET_NS if (dev && dev->nd_net != joolns_get()) return false; #endif return true; }
どうやら network namespace のチェックをしているようですね。dev->nd_net と joolns_get() の型が一致しないと怒られているので、先に joolns_get() の戻り値の型を確認してみます。
/* * Jool-3.4.0/mod/stateless/common/namespace.c */ struct net *jool_net; ... struct net *joolns_get(void) { return jool_net; }
joolns_get() の戻り値は struct net * だと判明したので、次は dev->nd_net です。struct net_device は linux/netdevice.h に定義されています。
/* * /usr/src/linux-headers-4.1.7-v7+/include/linux/netdevice.h */ struct net_device { char name[IFNAMSIZ]; struct hlist_node name_hlist; char *ifalias; ... possible_net_t nd_net; ... }
dev->nd_net の型は struct net * ではなく、possible_net_t のようですね、単に typedef しているだけかどうか確認します。possible_net_t は net/net_namespace.h に定義されていました。
/* * /usr/src/linux-headers-4.1.7-v7+/include/net/net_namespace.h */ typedef struct { #ifdef CONFIG_NET_NS struct net *net; #endif } possible_net_t;
dev->nd_net は possible_net_t の実体で struct net のポインタではないので比較できなくて当然です。CONFIG_NET_NS が有効な場合なので、エラーとなっている箇所を dev->nd_net.net != joolns_get() と書けばよさそうです。
Jool のコードを修正したら、気を取り直してもう一度 make します。
$ make make -C stateless make[1]: Entering directory `/home/pandax381/LOCAL/Jool-3.4.0/mod/stateless' make -C /lib/modules/4.1.7-v7+/build M=$PWD JOOL_FLAGS="" make[2]: Entering directory `/usr/src/linux-headers-4.1.7-v7+' ... /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/nf_hook.c:59:3: warning: initialization from incompatible pointer type [enabled by default] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/nf_hook.c:59:3: warning: (near initialization for 'nfho[0].hook') [enabled by default] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/nf_hook.c:66:3: warning: initialization from incompatible pointer type [enabled by default] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateless/nf_hook.c:66:3: warning: (near initialization for 'nfho[1].hook') [enabled by default] ... /home/pandax381/LOCAL/Jool-3.4.0/mod/stateful/nf_hook.c:82:3: warning: initialization from incompatible pointer type [enabled by default] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateful/nf_hook.c:82:3: warning: (near initialization for 'nfho[0].hook') [enabled by default] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateful/nf_hook.c:89:3: warning: initialization from incompatible pointer type [enabled by default] /home/pandax381/LOCAL/Jool-3.4.0/mod/stateful/nf_hook.c:89:3: warning: (near initialization for 'nfho[1].hook') [enabled by default] ... make[2]: Leaving directory `/usr/src/linux-headers-4.1.7-v7+' make[1]: Leaving directory `/home/pandax381/LOCAL/Jool-3.4.0/mod/stateful' # Running the dependencies is enough.
make は完了したけど、なにやらポインタの型が一致しないという不穏な warning が出ているので念のため確認してみます。
/* * Jool-3.4.0/mod/stateless/nf_hook.c */ #if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 13, 0) #define HOOK_ARG_TYPE const struct nf_hook_ops * #else #define HOOK_ARG_TYPE unsigned int #endif static unsigned int hook_ipv4(HOOK_ARG_TYPE hook, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { return core_4to6(skb, in); } static unsigned int hook_ipv6(HOOK_ARG_TYPE hook, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)) { return core_6to4(skb, in); } static struct nf_hook_ops nfho[] = { { .hook = hook_ipv6, .owner = NULL, .pf = PF_INET6, .hooknum = NF_INET_PRE_ROUTING, .priority = NF_IP6_PRI_JOOL, }, { .hook = hook_ipv4, .owner = NULL, .pf = PF_INET, .hooknum = NF_INET_PRE_ROUTING, .priority = NF_IP_PRI_JOOL, }, };
struct nf_hook_ops は Netfilter にフック関数を登録する際に使うもので、.hook はフック関数への関数ポインタですね。これらは linux/netfilter.h に定義されているので確認してみます。
/* * /usr/src/linux-headers-4.1.7-v7+/include/linux/netfilter.h */ typedef unsigned int nf_hookfn(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct nf_hook_state *state); struct nf_hook_ops { struct list_head list; /* User fills in from here down. */ nf_hookfn *hook; struct module *owner; void *priv; u_int8_t pf; unsigned int hooknum; /* Hooks are ordered in ascending priority. */ int priority; };
なんと、struct nf_hook_ops は同じですが、nf_hookfn のプロトタイプが違いますね...なんか書き間違えたってわけじゃなさそうなくらい違いますし、そもそもこれで動かしてもカーネルパニック起きるような気がするので、もしかしてAPIが変わったのかな?と当たりをつけてもう少し深追いしてみます...
こんな時は、http://lxr.free-electrons.com/ を使うと Linux カーネルの各バージョンを横断しながらコード検索できるのでとても便利です。
調べた結果、kernel 4.0 以前と kernel 4.1 以降で Netfilter のフック関数のプロトタイプが変更になっていることがわかりました。以下が kernel 4.0 のもので、Jool 側が期待しているプロトタイプと一致しています。
/* * linux/netfilter.h */ typedef unsigned int nf_hookfn(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)); struct nf_hook_ops { struct list_head list; /* User fills in from here down. */ nf_hookfn *hook; struct module *owner; void *priv; u_int8_t pf; unsigned int hooknum; /* Hooks are ordered in ascending priority. */ int priority; };
ということで、Jool のコードが kernel 4.1 以降の Netfilter の変更に対応していなかったのが原因でした。原因がわかったので、新しい Netfilter のフック関数のプロトタイプに合わせるよう修正を試みます。幸いほとんどの引数を使っておらず、新しいプロトタイプで消えてしまったパケットを受信したデバイスを示す struct net_device *in も、struct sk_buf *skb が skb->dev として持っているのでそれを使えば大丈夫そうです。
そんなわけで、こんなパッチを書きました。
https://gist.github.com/pandax381/04a718b9ccc0e0a94d7f
これを適用してもう一度ビルドしてみます。
$ wget https://gist.githubusercontent.com/pandax381/04a718b9ccc0e0a94d7f/raw/a797a3e1148d11dcf09ae024c88e1a6c8dafd881/Jool-3.4.0.patch $ patch -u -p1 -d Jool-3.4.0 < Jool-3.4.0.patch $ cd Jool-3.4.0/mod $ sudo make $ sudo make modules_install $ sudo mv /lib/modules/4.1.7-v7/extra/jool* /lib/modules/4.1.7-v7+/extra/ $ sudo depmod -a
無事にビルドできたので、カーネルモジュールの jool をリロードします。3.4.0 からは、pool4 オプションでサービス用の IPv4 アドレスが指定されない場合、ホストに設定されている IPv4 アドレスを共有して動作するようになっているはずなので、eth0 に設定したサービス用のIPv4アドレスを削除し、modprobe で jool をロードする際の pool4 オプションも取り除いて実行します。
$ sudo modprobe -r jool $ sudo ip addr del 192.168.0.101/24 dev eth0 $ sudo modprobe jool pool6=64:ff9b::/96
IPv6 ネットワークに接続しているクライアントから ping6 を投げて確認してみます。
clinet:~$ ping6 www.klab.com PING6(56=40+8+8 bytes) 2001:2::aab1:xxxx:xxxx:xxxx:ae10 --> 64:ff9b::85f2:574 16 bytes from 64:ff9b::85f2:574, icmp_seq=0 hlim=50 time=17.319 ms 16 bytes from 64:ff9b::85f2:574, icmp_seq=1 hlim=50 time=17.983 ms 16 bytes from 64:ff9b::85f2:574, icmp_seq=2 hlim=50 time=16.872 ms
無事に疎通が確認できました!もちろん TCP や UDP の通信もちゃんと動いているので、このまま Webブラウジングしていると IPv6 Only なネットワークにいるということを忘れてしまいそうなくらいです。
まとめ
お約束ですが、このパッチを元に Jool の開発元にプルリク投げました。そして無事にマージされ、既に Jool-3.4.1 として正式リリースされています。みなさんは何も気にせず、最新バージョンを使ってください。おしまい。
TCP高速化プロキシ「AccelTCP」を公開しました
昨年末からずっとこんなことをしてまして、この時期になってようやく今年初のブログ記事です。 進捗的なアレがアレでごめんなさい。そろそろ3年目に突入の @pandax381です。
RTT > 100ms との戦い
経緯はこのへんとか見ていただけるとわかりますが「日本と海外の間を結ぶ長距離ネットワーク(いわゆるLong Fat pipe Network)において、通信時間を削減するにはどうしたらいいか?」ということを、昨年末くらいからずっとアレコレやっていました。
送信したパケットが相手に到達するまでの時間(伝送遅延)を削減するのは、光ファイバーの効率の研究とかしないと物理的に無理なので、ここで言う通信時間とは「TCP通信」における一連の通信を完了するまでの時間です。
伝送遅延については、日本国内のホスト同士であれば、RTT(往復遅延時間)はだいたい10〜30ms程度ですが、日本・北米間だと100ms以上、日本・欧州間だと200ms以上になります。
TCPは、実際にデータを送る前に3Wayハンドシェイクによってコネクションの確立する必要がありますが、伝送遅延の大きな回線では、このハンドシェイクを行うだけでそれなりの時間が掛かってしまいます。
また、データ送信時に発生するACK(確認応答)待ちによるアイドルタイムの増加もTCPのパフォーマンス低下の大きな要因です。TCPは、信頼性のある通信を担保するために、受信側からのACK応答を待ちながらデータを送信します。つまり、ACK応答が届くまでに時間が掛かると、それだけ何もせずに待っている時間が増えてしまい、その結果として通信全体に掛かる時間が大幅に増加してしまうのです。そして、通信全体に掛かる時間が増加するということは、スループットが低下するということです。
TCPのスループットの理論値は以下の計算式で求められます。
TCPスループット(bps)= ウィンドウサイズ(bit)/ RTT(sec)
例えば、ウィンドウサイズを64KBと仮定した場合、RTTが10msだとTCPスループットの理論値は51.2Mbpsとなりますが、RTTが100msの場合には5.12Mbpsになってしまいます。つまり、RTTが10倍になればスループットは1/10になるのです。
これは、例え1Gbpsや10Gbpsといった帯域の広い回線を使っていても同じです。TCPのスループットは伝送遅延に大きく影響されます。
このように、伝送遅延の大きなネットワークではTCPは性能を十分に発揮できないため、伝送遅延が大きいことを前提として、通信時間を短縮するための方法を模索するところが今回の出発点です。
TCPがダメならUDPを使えばいいじゃない!
TCPのハンドシェイクによるオーバーヘッドは、その後にやりとりするデータ量が少ないほど、通信全体に掛かる時間に対して占める割合が大きくなります。
例えば、HTTP通信でリクエストもレスポンスもデータ量が少ない場合、通信時間の50%はハンドシェイクによるオーバーヘッドが占めることになります。(※ KeepAliveを使わない場合)
ゲーム内で発生する通信の傾向を見てみると、ほとんどが数KB〜数十KBの小さなデータのやりとりだったため、ハンドシェイクによるオーバーヘッドの影響を大きく受けていそう、というかむしろハンドシェイクを省略するだけでかなり効果がありそうな感じです。
「Google先生のQUICみたいな感じで、UDPベースで信頼性のあるオレオレプロトコル自作すれば結構早くなりそうだよなー」とか妄想が膨らみますね。
そんなわけで、勢いでこんなもの作ってみました。
「IDTP(Iikanji ni Datagram de Transport Protocol)」(大人の事情で公開はなしですm(_)m)
名前の通り、データをUDPでイイ感じに運んでくれるプロトコルです。プロキシサーバでTCPのペイロードをUDPに乗せかえて運ぶことを想定して作りました。QUICに手を出さなかったのは、上に乗せられるプロトコルがSPDYに限定される(らしい)からということと、自分で作ってみるのも楽しいよなーと思ったからです。
とりあえず、数KBのファイルをRTTに近い時間でHTTP GET出来ることを目標にプロトタイプを作成したのですが、最初の実装では以下の機能を盛り込みました。
- IPアドレス+ポート番号+セッションIDによるセッションの識別
- SACKのような選択式確認応答
- タイムアウトによる強制再送(SRTTベースのRTO計算めんどいので固定値)
とりあえず動くようになったので、北米のサーバと日本国内のサーバでベンチマークとった結果が⇩です。
お、10KB未満のデータに関してはバッチリRTTと同じくらい(=理論値でのほぼ最速)の時間になってますね!めでたしめでたし。
はい、これで完成とか言っちゃったら怒られちゃいますよね...
フロー制御も輻輳制御もない、つまり送信側は常に全力で送信しているので早くて当然なんですね。そして、トラフィックが一定量を超えるとパケロスが顕著に出はじめます。こうなると再送→パケロス→再送...と、絵に描いたような輻輳により通信が破綻してしまいます。
その後、フロー制御や輻輳制御、スロースタートの機能を盛り込み、一応「信頼性のあるトランスポートプロトコル」と名乗っても大丈夫そうなところまで作り込みました。
TCPへの回帰
ここまでやって、ふとあることに気づきます。
「コレ、もうTCPじゃね?」
確認応答、再送、フロー制御、輻輳制御、スロースタート...どこからどう見ても完全にTCPです。
TCPの欠点を補うために「UDPベースで信頼性のあるトランスポートプロトコルを作ろう」という試みは、誰もが一度はやったことあるんじゃないかと思いますが、ぼくのような素人だと真面目に作り込めば作り込むほどTCPになってしまうんですよね。
こうなると、ハンドシェイクを省略している意外はTCPとの差がほとんどありません。むしろ、標準のプロトコルスタックの方が圧倒的に洗練されているため、どう考えてもそっちを使うべきです。
そんなこんなで、半分ネタで作ったIDTPは、速攻でお役御免になりました。
結局、ふりだしに戻り「ベースの通信はTCPに任せたい!でもハンドシェイクは省略したい!」という、なんとも他力本願な考えに至ります。
TCPでハンドシェイクを省略...あれ、どっかで聞いたことあるような...
そうだよ!TFO(TCP Fast Open)だよ!TFOでやればいいんだよ!
結論:素直にクライアントとサーバのカーネルを3.6/3.7以上にしてTFOを使う。めでたしめでたし。
...まぁ、それで片付いてくれれば苦労しないんですが...^^;
TFOを採用するにあたって、以下のような懸念事項があります
- 比較的新しいカーネルでなければ利用できない
- クライアントアプリの改修が必要(iOSって対応してたっけ?)
- NATやF/Wの介在によってTFOが失敗し、通常のハンドシェイクにフォールバックする可能性がある
全くの新規で環境を用意するなら問題にならないかもしれませんが、既に運用中のサービスに適用するとなると、めちゃくちゃハードルが高くなります。
そんなわけで、単純にTFOで全て解決!とはならないのでした。
ようやく本題の「AccelTCP」の話
長々と前置きを書いてしまいましたが、紆余曲折(だいぶ遠回り)した結果、今回の記事のタイトルにある「AccelTCP」を開発するに至りました。
AccelTCP(ACCELerlate TCP proxy)
AccelTCPは、伝送遅延の大きな回線におけるTCP通信を高速化するためのプロキシサーバ型のソフトウェアです。下記の図のように、クライアント側とオリジンサーバ側にそれぞれAccelTCPのプロキシを立てて、伝送遅延の大きな長距離ネットワーク上での通信を代理で行います。
コネクションプーリングによりTCP接続のオーバーヘッドを削減
プロキシサーバ間で、あらかじめ確立されたTCPコネクションを再利用する「コネクションプーリング」を行います。プロキシサーバ間のコネクションプーリングにより、TCPコネクションの確立時に発生する3Wayハンドシェイクのオーバーヘッドを削減し、比較的小さなデータのやりとりを行う通信の待ち時間を大幅に短縮できます。
TCPパラメータの最適化による高速化
プロキシサーバ間のTCP通信は、ウィンドウサイズなどのTCPパラメータをネットワークの特性に合わせ最適化することにより、データ転送効率が大きく向上します。
プロキシサーバ型によるメリット
プロキシサーバ型を採用することにより、クライアントおよびサーバサイドのプログラムを改修することなく「AccelTCP」を利用できます。また、プロキシサーバの設置により通信区間が分割され、各通信区間の往復遅延時間が減少します。これは、パケット消失時の再送時間の短縮につながり、通信全体の高速化が期待できます。
HTTPプロキシモード
ネームベースのバーチャルホストに対応するために、プロキシサーバによるHTTPリクエストのホストヘッダ書換えとXFFヘッダ挿入を行うHTTPプロキシモードを備えています。
通信データの暗号化とSSLオフロード機能を搭載
プロキシサーバ間の通信はSSL/TLSにより暗号化され、安全にやりとりできます。また、SSLオフロード機能を搭載しているためSSL非対応サーバのSSL化や、サーバからSSLの処理を分離することも可能です。
ぶっちゃけ大したことやってないけど、データ量が小さい場合はそれなりに効果あります。
使い方
READMEだけ見ても分かりにくいと思うので、簡単な使い方の説明です。
ビルド
AccelTCPをビルドするには以下のライブラリが必要です。
ライブラリが揃っていれば、makeするだけでビルド終了です。$ git clone https://github.com/KLab/AccelTCP.git
$ cd AccelTCP
$ make
プロキシの起動方法
まず、サーバプロキシを起動します。クライアントプロキシからの接続を 10381 ポートで待ち受け、オリジンサーバ(133.242.5.116:80)に転送するには、下記のように起動します。--server がサーバプロキシとして動くためのオプションです。
$ ./acceltcp -- --server 10381:133.242.5.116:80
次に、クライアントプロキシを起動します。クライアントからの接続を8080ポートで待ち受け、サーバプロキシ(192.168.0.1:10381)に転送するには、下記のように起動します。--http オプションは、HTTPプロキシモードを有効にするためのオプションです。HOSTヘッダを --http-host オプションで指定した内容に書き換えます。--connection-num オプションは、クライアントプロキシからサーバプロキシに対して事前に接続しておくTCPコネクションの数です。
$ ./acceltcp -- --http --http-host=www.klab.com --connection-num=100 8080:192.168.0.1:10381
これで、クライアントプロキシの8080ポートへのアクセスが、オリジンサーバ(133.242.5.116)の80ポートへ転送されます。
オプションの説明とか
オリジンサーバに対してSSLで接続する場合には、サーバプロキシを以下のように起動します。
$ ./acceltcp -- --server --ssl-connect 10381:133.242.5.116:80
クライアントプロキシとサーバプロキシの間もSSL化する場合には、それぞれ以下のように起動させます。なお、デフォルトではカレントディレクトリにある server.crt と server.key が使用されますが、それぞれ --ssl-certificate と --ssl-privatekey で上書き指定できます。
$ ./acceltcp -- --server --ssl-accept --ssl-connect 10381:133.242.5.116:80
$ ./acceltcp -- --ssl-connect --http --http-host=www.klab.com --connection-num=100 8080:192.168.0.1:10381
更に、クライアントプロキシもSSLで待ち受ける場合には下記のようになります。これで全区間の通信がSSL化されます。
$ ./acceltcp -- --server --ssl-accept --ssl-connect 10381:133.242.5.116:80
$ ./acceltcp -- --ssl-accept --ssl-connect --http --http-host=www.klab.com --connection-num=100 8080:192.168.0.1:10381
その他に、--rbuf と --sbuf オプションで受信送信それぞれのソケットバッファのサイズが変更できたりもします。
おわりに
本件はIDCフロンティアさんとの共同研究ということで、超特急で検証用のサーバ設定してもらったり、IDTPが暴発して大量のトラフィック流し過ぎてご迷惑をお掛けしたりしちゃってホントごめんなさいなこともありましたが、久しぶりに没頭できるネタを振ってもらって有り難い限りです。まだまだこれで終わりではないので、もっと突っ込んで輻輳制御のモジュールとかも書いてみたりしたいなぁと思っています。
外部の方から見たら「KLab=ケータイのゲーム作ってる会社」というイメージが強いと思うんですけど、中にはこんなことやってる人もいるよーってことで、こういうのが好きな人がKLabに興味持ってくれると個人的にとっても嬉しいです。
おしまい。
@pandax381
Mac OSX で vmnet が BIOCSETIF できなくてハマった話し
1年くらいブログ記事書くのをサボっていたので、ちょっとマニアックなネタを投下します。ISUCON3 は予選で敗退してしまった @pandax381 です。
vmnet が BIOCSETIF できない!
Mac OSX(marvericks)で VMware Fusion 6 を使っているのですが、ホスト側の仮想インタフェース(vmnetX)のパケットがキャプチャできないという問題に遭遇しました。具体的には、パケットをキャプチャするためにBPFデバイスをオープンして、vmnetX を ioctl(BIOCSETIF) でアタッチするの処理で失敗してしまうのです。一見普通のEthernetデバイスに見えるのに、どうしてBPFでパケットをキャプチャできないのか調べてみました。
tcpdump で試してみる
僕の作っていたプログラムがイケてないのかもしれないので、まずは tcpdump を使ってキャプチャできるか試してみます。そして、tcpdump先生でもダメならあきらめましょう。
tcpdump に -i オプションで vmnet8 を指定して実行してみます。
$ sudo tcpdump -i vmnet8 tcpdump: vmnet8: No such device exists (BIOCSETIF failed: Device not configured)
あれ、うまくいかない。。そういえば、OSX の tcpdump はインタフェース指定しなければ全てのインタフェースからキャプチャできたはずなので、-i オプションなしで実行してみます。
$ sudo tcpdump tcpdump: data link type PKTAP tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on pktap, link-type PKTAP (Packet Tap), capture size 65535 bytes 18:18:51.715943 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 54865, seq 1, length 64 18:18:51.715969 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 54865, seq 1, length 64 18:18:52.715822 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 54865, seq 2, length 64 18:18:52.715862 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 54865, seq 2, length 64 18:18:53.716669 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 54865, seq 3, length 64 18:18:53.716705 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 54865, seq 3, length 64
ちゃんと vmnet8 に接続されている仮想マシンとのパケットが拾えています。tcpdump は何かしらの方法で vmnet8 を覗けているようです。
なにかオプションを指定する必要があるかもしれないので、マニュアルを見てみると・・・
On Darwin systems version 13 or later, when the interface is unspecified, tcpdump will use a pseudo interface to capture packets on a set of interfaces determined by the kernel (excludes by default loopback and tunnel interfaces). Alternatively, to capture on more than one interface at a time, one may use "pktap" as the interface parameter followed by an optional list of comma separated interface names to include. For example, to capture on the loopback and en0 interface: tcpdump -i pktap,lo0,en0
-i オプションの項目に、ずばり答えが書いてありました...^^;
・OSX(Darwin)の場合には、-i オプションを指定しないと「擬似インタフェース」を使用して全インタフェースからキャプチャする
・任意の複数のインタフェースでキャプチャする場合には -i pktap,lo0,en0 と指定する
どうやら pktap というのが「擬似インタフェース」で、これを使用すれば vmnet8 のパケットがキャプチャできそうなので、早速試してみます。
$ sudo tcpdump -i pktap,vmnet8 tcpdump: data link type PKTAP tcpdump: verbose output suppressed, use -v or -vv for full protocol decode listening on pktap,vmnet8, link-type PKTAP (Packet Tap), capture size 65535 bytes 18:48:35.799610 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 56686, seq 13, length 64 18:48:35.799679 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 56686, seq 13, length 64 18:48:36.802213 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 56686, seq 14, length 64 18:48:36.802252 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 56686, seq 14, length 64 18:48:37.803839 IP 192.168.2.128 > 192.168.2.1: ICMP echo request, id 56686, seq 15, length 64 18:48:37.803876 IP 192.168.2.1 > 192.168.2.128: ICMP echo reply, id 56686, seq 15, length 64
ばっちりキャプチャできました!
tcpdump はどんな裏技を使っているのか
tcpdump がどんな処理をしているのかソースを解析してみます。
とりあえず、カレントバージョンの tcpdump と libpcap のソースを grep みるも「pktap」や「PKTAP」というキーワードが見当たらない。。。
仕方ないので、tcpdumpのバージョンを確認してみると
$ tcpdump -h tcpdump version 4.3.0 -- Apple version 56 libpcap version 1.3.0 - Apple version 41 Usage: tcpdump [-aAbdDefhHgIJkKlLnNOpPqQ:RStuUvxX] [ -B size ] [ -c count ] [ -C file_size ] [ -E algo:secret ] [ -F file ] [ -G seconds ] [ -i interface ] [ -j tstamptype ] [ -M secret ] [ -Q metadata-filter-expression ] [ -r file ] [ -s snaplen ] [ -T type ] [ -w file ] [ -W filecount ] [ -y datalinktype ] [ -z command ] [ -Z user ] [ expression ]
ほほう、Apple version ですとな。カスタムバージョンのようなので、http://www.opensource.apple.com/source/ から tcpdump と libpcap を探します。
- http://www.opensource.apple.com/source/tcpdump/tcpdump-56/tcpdump/
- http://www.opensource.apple.com/source/libpcap/libpcap-42/libpcap/
この中をひたすら漁っていると、libpcap の中に「pcap-darwin.c」という、いかにもそれっぽいソースファイルが見つかりました。
pcap_setup_pktap_interface() や pcap_cleanup_pktap_interface() などの関数があるので、これで当たりのようです。
ざっとソースを眺めて、以下のことが分かりました。
- ioctl(SIOCIFCREATE) :PKTAPインタフェースを作成
- ioctl(SIOCSDRVSPEC) :PKTAPインタフェースのフィルタを設定(キャプチャするデバイスと除外するデバイスを設定できる)
- ioctl(SIOCIFDESTROY):PKTAPインタフェースを削除
そして、一番の知りたかった BPF で BIOCSETIF する際の処理では、実デバイスではなくPKTAPインタフェースを指定していました。
なるほど、謎は全て解けた!
自力でやってみる
なにをすれば良いか分かったので、早速自分でコードを書いてみました。
https://github.com/pandax381/pktap_demo/
- PKTAPインタフェースを作成
- 引数で指定されたデバイスをキャプチャ対象としてフィルタを設定(複数可、指定がなければ全てのデバイスが対象)
- BPFデバイスをオープンしてPKTAPインタフェース(pktapX)を指定
- 明示的にプロミスキャスモードにはしない(BIOCPROMISC が失敗する)
- BPFからパケットをキャプチャしてデバッグ出力
PKTAPインタフェースから取得したパケットには、struct pktap_header型のリンクヘッダが付いているので、詳細出力します。
※ /usr/include/ 配下に net/pktap.h が見当たらなかったので、XNU のソースツリーから拝借するようにしています
実行結果はこんな感じ
$ sudo ./pktap_demo vmnet8 ### pktap_debug_print ### pth_length: 108 pth_type_next: 1 pth_dlt: 1 pth_ifname: vmnet8 pth_flags: 1 pth_protocol_family: 2 pth_frame_pre_length: 14 pth_frame_post_length: 0 pth_pid: -1 pth_comm: pth_svc: 0 pth_iftype: 6 pth_ifunit: 8 pth_epid: -1 pth_ecomm: +------+-------------------------------------------------+------------------+ | 0000 | 00 50 56 c0 00 08 00 0c 29 6b b6 e9 08 00 45 00 | .PV.....)k....E. | | 0010 | 00 54 00 00 40 00 40 01 b4 d7 c0 a8 02 80 c0 a8 | .T..@.@......... | | 0020 | 02 01 08 00 ac fe ec 31 00 01 1e 14 9b 52 00 00 | .......1.....R.. | | 0030 | 00 00 e5 94 01 00 00 00 00 00 10 11 12 13 14 15 | ................ | | 0040 | 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 | .......... !"#$% | | 0050 | 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 | &'()*+,-./012345 | | 0060 | 36 37 | 67 | +------+-------------------------------------------------+------------------+ ### pktap_debug_print ### pth_length: 108 pth_type_next: 1 pth_dlt: 1 pth_ifname: vmnet8 pth_flags: 2 pth_protocol_family: 2 pth_frame_pre_length: 14 pth_frame_post_length: 0 pth_pid: -1 pth_comm: pth_svc: 0 pth_iftype: 6 pth_ifunit: 8 pth_epid: -1 pth_ecomm: +------+-------------------------------------------------+------------------+ | 0000 | 00 0c 29 6b b6 e9 00 50 56 c0 00 08 08 00 45 00 | ..)k...PV.....E. | | 0010 | 00 54 b5 a8 40 00 40 01 ff 2e c0 a8 02 01 c0 a8 | .T..@.@......... | | 0020 | 02 80 00 00 b4 fe ec 31 00 01 1e 14 9b 52 00 00 | .......1.....R.. | | 0030 | 00 00 e5 94 01 00 00 00 00 00 10 11 12 13 14 15 | ................ | | 0040 | 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 | .......... !"#$% | | 0050 | 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 | &'()*+,-./012345 | | 0060 | 36 37 | 67 | +------+-------------------------------------------------+------------------+
余談
Google先生に聞いても、唯一ヒットする情報は Apple先生のソースコードだけだったので、意地になって動くコードを書く所までやってみました。社内でドヤってみたけど、レイヤが低すぎたのか誰も反応してくれないのでブログを書いてみたという落ちです。後悔はしていない。
なお、PKTAP は ifconfig でも作成できます。pktap_demo のコード書き終わってから ifconfig のコードを読んでいて発見しました...orz
$ sudo ifconfig pktap create pktap0
$ sudo ifconfig pktap destroy
コマンドで PKTAP にフィルタを設定するには、pktapctl を自分でコンパイルすればできます。
http://opensource.apple.com/source/network_cmds/network_cmds-433/pktapctl/pktapctl.c
あと、そもそもの目的は vmnet に対して出力もしたかったのですが、PKTAPだとキャプチャ専用で出力はできないっぽいです。
@pandax381
#isucon2 に向けて、かなり間違った方向に本気出してみた(recaro 誕生秘話)
先日、NHNさん主催の #isucon2 に @methane と参加してきたので、事前準備や当日の状況などを数回に分けてレポートしようと思います。#isucon2 が終わって少し体調を崩していた @pandax381 です。
すべてはここから始まった
社内のIRCチャンネルで #isucon2 の開催が話題になっていて、隣の席の @methane が真っ先に参加を表明し、パートナーを募集していました。僕はというと、面白そうだなぁと思いつつも、WebアプリとかDBとよくわかんないし戦力にならんだろうと「椅子投げコンテストw」とか言ってスルーしていたんですが、@methane から「一緒に出ようぜ!」とルフィばりの熱い誘いを受け、参加を決意することになりました。ちょうど #isucon2 開催1ヶ月前の話です。
L7未満は全部なんとかしてくれ!
そんなこんなで #isucon2 への参加が決まり、準備とか何すっかなーと考えてたところ、ペアの @methane が驚愕のツイートをしてるのを発見。
ちょっwwwこれはヤバい。草とか生やしてる場合じゃない。「きっと @methane が全部なんとかしてくれる!」とか考えてたら完全に先を越された。まぁ、役割分担的に @methane がアプリを全部 Python で書き下ろしたり、SQLのチューニングをやるだろうから、僕はネットワーク周りとかロードバランサ・Webサーバあたりをやるのかなと思っていたけど。そんなわけで LVS で DSR 構成組んだり nginx をチューニングしたり、sysctl でネットワーク周りのパラメータ調整したりと、ベタな内容で社内にテスト環境を構築して ab でベンチマーク比較しながら最速の構成を考えてました。
最速のWebサーバを求めて
「やっぱり最速は nginx かねー」とか「meinheld を更にいじって nginx より早くしたったw」とか最速Webサーバ話に花を咲かせていたら、どこからともなく「つ khttpd・・」という声が。「khttpdとかオワコンw kernel 2.6系じゃ動かないし」と軽くスルーしてたんですが、またもや @methane から凶悪ツイートががが・・・
まぁ、ab ベンチマーク対決のために kernel module で Webサーバ書き下ろすとか完全に頭おかしいんですけど、ネタにマジレスしてみるのも面白いかなとか思いはじめて、お遊びでカーネル空間で動くWebサーバ「tkhttpd」を作ってみました。そしたら、コレが思った以上に性能が出て、物理サーバだと nginx の 5倍くらいの性能を叩き出し、見事「最速Webサーバ」の座を得たのでした。(Hello World のような単純なページを返すだけだと keep-aliveなしで 30,000 req/sec、keep-aliveありで 150,000 req/sec くらい出ていました)
ただ、このバージョンは ab 最速を叩き出すためのチート仕様(手抜き仕様)だったので、折角なのでもう少しまともな作りにしようと、joyent/http-parser(nginx のパーサを切り出したもので、node.js で使ってるやつ)を組み込んだり、リバースプロクシ機能を組み込んだりして、幾度となくカーネルパニックに泣かされつつも、Webサーバと公言して恥ずかしくない程度に仕上げていきました。
ユーザー空間に出たら負け。カーネル空間に引きこもろう!
「うぎゃ!カーネルごと死んだw」とか「カーネル先生まじパネェ!」とか独り言いいながら作っていたら、次第に周りの人達も興味を持ちはじめて、僕の周りでは空前のカーネル空間ブームが到来!パートナーの @methane も「じゃぁ俺はカーネル空間で動く memcached つくるわ!」とか言い出す始末。そして「kmemcached」というプロジェクトを見つけてきて、次はこれを動かしてみようということになりました。
しかし、この kmemcached がなかなかの曲者で、2年くらいメンテされていなくて、とりあえず動かしてはみたものの、並列処理ができなかったり、そのままカーネルごとお亡くなりになったりと、そのまま使うには課題の多い代物でした。まぁ、さすがに memcached まではやりすぎだよな・・・とか思っていたら、なんと @methane が数日で kmemcached を魔改造して実用レベルに仕上げるというマジキチっぷりを発揮してくれました。
この頃から、僕たち二人は「#isucon1 は DBへアクセスが行ったら負けだったようだけど、#isucon2 はユーザー空間へアクセスが行ったら負けだよね!」とカーネル空間に引きこもることを決意。(はい、どうみても完全に間違った方向に進んでます。本当にありがとうございました)どうせやるなら考え得るなかで最高のパフォーマンスを求めようと言うことで、可能な限りカーネル空間で処理するという作戦を選びました。
対 #isucon2 用 最終決戦兵器「recaro」誕生
こうして、僕と @methane が 約1ヶ月の期間を掛けて作り上げたのが、最終決戦兵器「recaro」です。kernel module で実装した Webサーバ「tkhttpd」と「kmemcached」をリンクして、tkhttpd から kmemcached のストレージにダイレクトにアクセスすることでmemcachedプロトコルのオーバーヘッドを取り除き、別次元とも言えるスピードを実現しました。
また、#isucon1 の教訓を生かして、キャッシュが腐らないように SSI も実装し、動的ページの生成にも対応しました。どうしてもカーネル空間での処理が難しいケースに備えて、tkhttpd のリバースプロクシ機能で、ユーザー空間で動いている別のWebサーバへリダイレクトして、キャッシュ更新までの処理を委譲する仕組みも盛り込みました。
その結果、最終的には #isucon1 のベンチマークで「DBへアクセスが行ったら負け」と言っていたお遊びチームのスコアを余裕で超える性能を出し「ユーザー空間へアクセスが行ったら負け」という僕らの考えは正しかった!と、既に #isucon2 で優勝する気満々で「優勝後のブログ記事はどんなこと書くか」とかそんな話題で盛り上がっていました。
そう、あんなことが起きるまでは・・・(つづく)
ログからは見えてこない高負荷サイトのボトルネック
ちょうど1年前に「高負荷サイトのボトルネックを見つけるには」という記事を掲載していますが、この手のトラブルシューティングって結構大変で悩ましいですよね。はじめまして、新入りの@pandax381です。
ログからは見えてこないもの
「サイトの応答が遅い」という問題が発生した場合、その原因はどこにあるでしょうか。
- Webアプリケーションの処理に時間が掛かっている
- DBサーバに投げたクエリーの応答が遅い
- サーバの処理能力を超えている
などなど、いくつもの可能性があります。通常、上に挙げているような問題は、アプリケーションやサーバのログを調査することで、原因を突き止めることができます。
一方で、こういったログの調査だけでは、その原因にたどり着くことができなかったり、相当な苦労が伴うケースもあります。
あるサイトのある日の出来事
つい先日のことですが、KLabの運営している某ソーシャルゲームにて、サイトが重くなるという問題が発生しました。具体的にはHTTPのタイムアウトが頻発していて、クライアントにエラーが返されてしまっているというものです
DSASでは、HTTPサーバがフロント側とバックエンド側の2段構成になっていて、バックエンド側のHTTPサーバが5秒以内に応答を返せない場合、フロント側のHTTPサーバがエラーを返す仕組みになっています。そのため、このケースで真っ先に疑われるのは、バックエンド側のHTTPサーバとそれに紐づくDBサーバやmemcachedサーバです。
各サーバのリソース状態を集約しているリソースモニタを見てみると、確かにバックエンド側のHTTPサーバのレスポンスタイムが大幅に悪化していることがわかりました。こうなると怪しいのはDBサーバです。アクセス集中によってDBサーバが高負荷状態になり、レスポンスに時間が掛かっている可能性があります。
ところが、DBサーバのリソースを確認しても負荷が掛かっているような様子はなく、レスポンスタイムにも問題ありませんでした。同様にmemcachedサーバを確認したところ、こちらもリソース使用率などに問題はありませんでした。
ここで疑いはアプリケーションレイヤからネットワークレイヤへと変わることになります・・・
早速パケットキャプチャ(使うツールは tcpdump ではなく、もちろん miruo です!)してみると、出るわ出るわ、バックエンド側のHTTPサーバからTCPのSYNセグメント再送が大量に発生していました。
SYNセグメントの再送が発生しているということは、TCPの接続に時間が掛かっているということになります。SYNセグメントの再送は、通常 3秒・6秒・12秒・・・という間隔で行われるため、1回目の再送をしてからすぐにSYN/ACKを受け取れればよいのですが、そうでなければ5秒を経過してしまい、フロント側のHTTPサーバから強制切断されてしまいます。
実際にネットワークを流れるパケットを確認して、ようやく原因に一歩近づきました。そして、パケットの情報から対向はmemcachedサーバということもわかりました。
しかし、memcachedサーバは先に確認して問題なかったはず・・・。念のためリソースモニタでもう一度memcachedサーバの状況を確認してみるものの、やはり問題はなさそう。CPUもほどんど使われていないし、トラフィックも100Mbps程度でまだまだ余裕があります。
っと、ここで妙な違和感が。トラフィックのグラフが奇麗に100Mbpsに張り付いているのです。ちなみに、各サーバのNICは全て1000BASE-Tのものが使われています。まさかと思い、memcachedサーバのNICの接続状況を確認するとリンクが100Mになっている・・なぜ?という思いを抱きながら更に調査を進めた結果、今回の問題の原因が判明しました。
memcachedサーバを接続しているスイッチが故障して、そのポートが100Mでリンクされてしまっていたのです。
結局、問題のスイッチを対処してこの問題は解消されましたが、ネットワーク絡みの問題は原因にたどり着くまでに手間が掛かって大変です。
もっと切り分けの手間を減らしたい
上記のようなケースでは、ネットワーク周りの情報、それもTCPレイヤで再送が発生しているかどうかの情報を早い段階で得ていれば、解決までの道のりはずっと容易だったと思います。
実際にはその情報だけでは意味を成さないこともあるでしょうが、その他の情報や起きている現象と照らし合わせて、問題の切り分けや原因を推測するには大いに役立つはずです。
というわけで作りました!
前置きが長くなりましたが、TCPレイヤで発生した再送を検出するプログラムを作りました。
tcpeek(てぃーしーぴーく)は、TCPのセッション確立(3wayハンドシェイク)時に発生するエラーを監視・集計するネットワークモニタです。libpcapを用いたパケットキャプチャ型のプログラムで、tcpdumpなどと同様に監視したいホスト上で起動させるだけで簡単に使えます。
使い方は付属のREADMEを見てください。ちなみに、tcpeekを使うとこんなことができます。
- エラー検出:RSTやICMPの応答を受け接続に失敗したりタイムアウトしたTCPセッションを集計できます
- 再送検出:SYNおよびSYN/ACKセグメントの再送が発生したTCPセッションを集計できます
- フィルタ:通信方向・IPアドレス・ポート番号の組合わせで複数のフィルタが指定でき、フィルタ毎に個別集計できます
- データ出力:集計したデータをUNIXドメインソケット経由で出力します。スクリプトで扱いやすいJSON形式で出力します
- gmetric経由でrrdを出力するためのスクリプト(tcpeekstat)が付属しています
- プロミスキャスモードでも使えます
- などなど
tcpeekを実行すると、標準エラーへTCPセッションの情報がリアルタイムで出力されます。
$ sudo ./tcpeek -i eth0 listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes TIME(s) | TIMESTAMP | SRC IP:PORT DST IP:PORT | RESULTS | DUP SYN DUP S/A ---------------------------------------------------------------------------------------------------------------------- 0.002 | 12-07-06 16:39:02.552 | 192.168.2.227:48967 192.168.2.202:80 | success | 0 0 0.002 | 12-07-06 16:39:02.559 | 192.168.2.227:48968 192.168.2.202:80 | success | 0 0 0.002 | 12-07-06 16:39:11.219 | 192.168.2.227:42031 192.168.2.202:443 | success | 0 0 0.002 | 12-07-06 16:39:11.273 | 192.168.2.227:48970 192.168.2.202:80 | success | 0 0 0.002 | 12-07-06 16:39:11.279 | 192.168.2.227:42033 192.168.2.202:443 | success | 0 0 0.002 | 12-07-06 16:39:11.309 | 192.168.2.227:48972 192.168.2.202:80 | success | 0 0 0.002 | 12-07-06 16:39:11.323 | 192.168.2.227:42035 192.168.2.202:443 | success | 0 0 0.001 | 12-07-06 16:39:11.354 | 192.168.2.227:42036 192.168.2.202:443 | success | 0 0 0.002 | 12-07-06 16:39:11.385 | 192.168.2.227:42037 192.168.2.202:443 | success | 0 0 0.001 | 12-07-06 16:39:36.254 | 192.168.2.228:62876 192.168.2.227:80 | failure (reject) | 0 0 0.000 | 12-07-06 16:39:38.160 | 192.168.2.228:62877 192.168.2.227:80 | failure (reject) | 0 0 0.000 | 12-07-06 16:39:44.689 | 192.168.2.227:56371 192.168.2.228:8080 | failure (reject) | 0 0 39.947 | 12-07-06 16:41:29.723 | 192.168.2.227:58376 192.168.2.207:8080 | failure (timeout) | 2 0
項目 | 説明 |
---|---|
TIME(s) | TCPセッションの確立(3wayハンドシェイク)に掛かった時間(秒) |
TIMESTAMP | TCPセッションが開始された時刻 |
SRC IP:PORT | TCPセッションの始端(クライアント)のIPアドレスとポート番号 |
DST IP:PORT | TCPセッションの終端(サーバ)のIPアドレスとポート番号 |
RESULTS | TCPセッションの確立可否 |
DUP SYN | SYNセグメントが再送された回数(再送が発生していなければ 0) |
DUP S/A | SYN/ACKセグメントが再送された回数(再送が発生していなければ 0) |
出力したrrdを元に作成したグラフのイメージです
おまけ
実は、上で書いているトラブルが起きる前から「TCPの再送検知できると、こういうトラブル起きたときに早く気づけていいよね」という話が出ていました。tcpeekもその頃から作り出していて、ほぼ完成していたのですが、まだ試験中で、本番環境では動かしていなかったのです。そしてこのようなトラブルが実際に起り「tcpeekを動かしていればすぐにわかったのに!」ということになり、早々に本番環境へ投入される流れとなりそうです。
トラフィックが急増! ボトルネックを退治しよう〜 【設定編】
DSR構成のレシピ
まずは、設定項目をおさらいしておきましょう。次の6つでした。- LVS の負荷分散の設定をDSRに変更する(ipvs の設定)
- Webサーバが、DSRなリクエストパケットを扱えるようにする(iptables の設定)
- Webサーバを、outer VLAN に参加させる(L2 スイッチの設定)
- Webサーバが、outer VLAN において通信できるように設定する(VLAN 用インタフェースの追加)
- Webサーバにおいて、上位ルータへの通信経路を設定する(ルーティングと iptablesの設定)
- Webサーバが、上位ルータに対して応答パケットを送信できるように設定する(ARP エントリの設定)
LVS の負荷分散の設定をDSRに変更する
ipvs で負荷分散する場合、LVS マシンに到達したクライアントからのパケットを Webサーバへと転送する方法には、3種類あります。1つ目は LVS 上で NATして転送する方法、2つ目は届いた IPパケットをカプセル化して転送する方法、3つ目が DSRです。 ipvs の仮想サービスの設定で DSR を指定するには、は、ipvsadm
コマンドで設定する場合でしたら --masquerading (-m)
(= NAT)や --ipip (-i)
(= カプセル化)の代わりに --gatewaying (-g)
を指定します。 keepalived
を使っているのでしたら、仮想サービスの設定で lb_kind
オプションに DR
を指定します。
Webサーバが、DSRなリクエストパケットを扱えるようにする
DSR 構成を採った場合、LVS から Webサーバに転送されてくるパケットは前回説明したように、送信先IPアドレスが LVSの仮想サービス用のアドレス(図では A.B.C.D)のままになっています。このパケットを、Webサーバの OSが受け取ったときに自分宛のパケットだと認識させるためには、追加の設定が必要になります。方法はいくつかありますが、DSAS for Social では Webサーバ上で NAT する方法を使っています。つまり、送信先IPアドレスが A.B.C.D
であるパケットを受け採った場合、Webサーバ自身がそのパケットの送信先アドレスを自分のアドレス(ss.tt.uu.vv
)に書き換えるのです。そうすれば、そのパケットは Webサーバの OSが自分宛のパケットだと理解して、Apacheなどのアプリケーションに渡してくれるようになります。
設定方法は、OSに依って変わりますが、Linux の場合、次のようにするのが簡単です。
# iptables -t nat -A PREROUTING -p tcp --dport 80 -d A.B.C.D -j REDIRECTこれで、クライアントから LVSを経由して Webサーバに届いた宛先アドレス=
A.B.C.D
なパケットが、Webサーバ自身で処理されるようになります。
Webサーバを、outer VLAN に参加させる
ここからは、Webサーバが直接上位ルータに対して応答パケットを渡すために必要となる設定になります。そのためには、まずは Webサーバが "outer VLAN" に参加しないと始まりません。その際 "inner VLAN" と "outer VLAN" が混ざらないように、どちらかの VLAN には tag VLAN として参加させる必要があります。DSAS for Social では、"outer VLAN" 側を、tag VLAN にして参加させています。
Webサーバを "outer VLAN" に属させるためには、L2 スイッチの設定が必要になります。どのような設定になるのかはスイッチによって変わってきますが、DSAS for Social で主に使っている hp社の Procurve シリーズでは、例えば次のようにします。
# config (config)# vlan 4 (vlan-4)# tagged 10-20これは、"outer VLAN" の VLAN番号が 4、Webサーバ(群)が接続されているポートのポート番号が 10番〜20番ポートの場合の例です。
Webサーバが、outer VLAN において通信できるように設定する
Webサーバが "outer VLAN" に対して通信するには、Webサーバ側でも追加設定が必要になってきます。というのも、先ほどのスイッチの設定において、 Webサーバは tag VLAN で "outer VLAN" に参加するようにしたので、Webサーバがスイッチと "outer VLAN" 向けのパケットをやりとりするときは、VLAN tag をつけてるようにしなければならないのです。Linux において tag VLAN を扱うためには、VLAN ID に対応した仮想的なネットワークインタフェース(NIC)を作成してやります。この仮想的な NIC は物理的な NIC にひもづけられます。仮想 NIC はどの物理 NIC に対してひもづけるかと言うと、当然ながら先ほどスイッチの設定で "outer VLAN" に参加させたポートに接続している NIC に対してになります。
Linux において、tag VLAN 用の仮想 NIC を使うためには、8021q
ドライバモジュールが必要となります。カーネルを手元でコンパイルする際にこのドライバを含めるには
CONFIG_VLAN_8021Q CONFIG_VLAN_8021Q_GVRPの2つのオプションを有効にしてください(モジュールにしても組み込みにしても、どちらでも構いません)。
物理NICに紐づいた tag VLAN 用の仮想 NIC を作成するには、vconfig コマンドを使います。
# vconfig add eth0 4 # ip link set eth0.4 upこれで、Webサーバが "outer VLAN" 上でパケットをやりとりする準備が整いました。tcpdump などで eth0.4 を観察すれば、broadcast パケットなどが流れる様子が観察できるはずです。
Webサーバにおいて、上位ルータへの通信経路を設定する
Web サーバが直接上位ルータとパケットをやりとりするための環境は整いました。しかしこれだけでは OS は Webサーバの応答パケットを上位ルータに渡してはくれません。ルーティングを設定する必要があります。そして、前回の要件であげたように、default gateway を変更すること無く、DSRしたいパケットだけ上位ルータに対してルーティングするように設定する必要があります。
これを実現するためには、Linux のポリシールーティング機能を使います。これは、様々な条件に基づいてルーティング設定を切り替える機能で、Linux の他の機能の例に漏れず非常に柔軟な設定をすることができます。今回は Netfilter の mangle テーブル上で DSRしたいパケットに対してマーキングを行い、そのマーキングしたパケットに対してのみ適用する DSR専用のルーティングテーブルを、通常のルーティングテーブルとは別に作成しすることで、DSRを実現します。
Netfilter の mangle テーブルと、ポリシールーティング機能を使うためには、それぞれカーネルの機能を有効にする必要があります。mangle テーブルを使うには、モジュールがすでにある場合は iptable_mangle
モジュールを読み込みます(iptables コマンドで mangleテーブルを触れば自動的にロードされます)。無ければ CONFIG_IP_NF_MANGLE
を有効にして、カーネルを作り直してください。ポリシールーティング機能はカーネルモジュールにはできず、組込みにしなければなりません。お使いのカーネルの config を見て、CONFIG_IP_MULTIPLE_TABLES
が y
になっているか、確認してください。y
になってなければ、カーネルを作り直す必要があります。
カーネルの機能を有効にしたら設定していきましょう。まずは、DSRしたいパケットに対してマーキングをします。
# iptables -t mangle -A OUTPUT -s A.B.C.D -j MARK --set-mark 4
-s
オプションで DSRしたいパケット=ソースアドレスが A.B.C.D
のものを指定します。そして -j MARK --set-mark 4
でマーキングを施します。最後の 4 は DSRしたいパケットにつける識別ID になります。この後に設定するポリシールーティングにおいて、どのパケットに対して DSR用のルーティングテーブルを適用するのかを、この IDを使って指定します。
ポリシールーティングのための設定は、次の様になります。
# ip route add H.I.J.K/32 dev eth0.4 table 100 # ip route add default via H.I.J.K table 100 # ip rule add prio 100 fwmark 4 table 100 # ip route add H.I.J.K/32 dev eth0.4まず、デフォルトのルーティングテーブルとは別の、DSR用のルーティングテーブルを作成します。このテーブルの名前をここでは "100" にしています。最初の2行はこの "100" というルーティングテーブルに対して、ルーティング情報を追加しています。つまり、上位ルータ(アドレス=H.I.J.K)とは eth0.4 という NICを通じてやりとりできることを示し(1行目)、インターネットに対してパケットを送信するときは、その上位ルータを中継すればよいことを指定(2行目)します。
3行目は、先ほど Netfilter の mangle テーブルにて DSRしたいパケットに対してマーキングした 4
というIDを手がかりにして(fwmark 4 の部分です)、新しく作った "100" というルーティングテーブルを参照するよう、指示しています。つまり、mangle テーブルで 4 という ID を付与されたパケットは、3行目の指示にしたがって、1,2行目で新しく作った特別なルーティングテーブルを参照して、行き先が決定されるようになります。
Webサーバが、上位ルータに対して応答パケットを送信できるように設定する
さて最後に、上位ルータと Webサーバがパケットをやりとりする上で必要な、ちょっとした設定を追加します。先ほどは、IP上での(L3的)上位ルータとのやりとりする経路の設定をしました。通常はこれだけで問題なく上位ルータと Webサーバはやりとりができるのですが、今回の設定例では Webサーバの VLAN インタフェースに IPアドレスを振らなかったため、ちょっとした小細工が必要になります。どういうことかというと、上位ルータの MACアドレス=Ethrnetのアドレスが、このままでは Webサーバには分からないのです。通常は通信相手の MACアドレスは、 ARPプロトコルを使って自動的に取得されるのですが、Webサーバは上位ルータと直接通信するために必要な、同一のサブネットに属したIPアドレスを持っていないので、ARPプロトコルが使えないのです。しかたがないので、上位ルータの MAC アドレスは、人間が手動で与えてやることにします。やり方は、上位ルータの MACアドレスが hh:ii:jj:kk:ll:mm だとすれば
# arp -s -i eth0.4 H.I.J.K hh:ii:jj:kk:ll:mmとなります。
以上で、Webサーバが DSRするための設定が完成しました。これでもう負荷分散機がボトルネックになることはありません。
トラフィックが急増! ボトルネックを退治しよう〜
このシリーズも、初めは専らアプリケーション寄りの話題でしたが、ここ二回ほどはインフラ寄りの話題でした。今日はさらに(OSIの7階層モデルにあてると)下寄りの話題になります…。できるだけ分かりやすく書くつもりですので、お付き合い頂ければと思います。
負荷分散機がボトルネック
さて、DSAS for Social ではいくつかのアプリケーションが動いているわけですが、では1つのアプリケーションがピーク時に使う帯域はどれくらいになるか、皆さん想像がつきますでしょうか。答えはもちろんアプリケーションによって全く変わるのですが、今まで記録した中での最大値は、2Gbps を越えました。これは、サーバに搭載されている NIC の能力を越えています。もちろん、1台の web サーバでこのトラフィックを全て捌いたわけじゃないのですが、実は DSAS の構成上、どうしてもこのトラフィックが集中する箇所があります。それは、負荷分散機の部分です。
なぜ負荷分散機がボトルネックになるのか
DSAS for Social では、負荷分散機に、Linux 上に実装された L4 負荷分散システムである、IPVS を使っています(以下、この IPVS が動作するマシンのことを、LVSマシンと呼びます)。そして LVS マシンは、DSAS for Social におけるルータの役割を兼用しています。つまりどういうことかというと、DSAS for Social における外部との相互通信は、全て LVS マシンを経由する、ということです。図で書くと、次のようになります。
LVS マシンは、負荷分散機だからと言って、特別なマシンを使っているわけではありません。Webサーバと同じものを使っています。これは LVS マシンがこわれた場合、隣に並んでいる Webサーバを使って LVS マシンの代わりをさせることができるように、という考えに基づいてこうしています。ですので、LVS マシンに搭載されている NIC(Network Interface Card) も、普通の 1Gbps のものです。DSAS for Social 以前の DSAS では、LVS が捌かなければならない帯域が 1Gbps を越えることは無く問題にはなりませんでした。しかしながら冒頭にも書いたように、DSAS for Social では 1つのアプリケーションが 2Gbps の帯域を消費するケースも出てきます。そのため、LVS がネットワーク通信に置いてボトルネックになってしまいました。
ボトルネックを解消するには
このボトルネックを解消するには、いくつかの方法があります。例えば NIC をもっと広帯域のものに変える、あるいは複数の NIC を束ねて帯域を太くする、などです。しかし、いずれの方法でもハードウェア的な変更が必要となるため、LVS用マシンが特別なマシンとなり、故障時に web サーバを代わりに充てる、ということが手軽にできなくなってしまいます。ということで、ハードウェアを拡張すること無く、どうにかできる方法を考えてみました。
DSAS for Social における L2ネットワークは、どうなっているのか
さて先ほどの図は DSAS for Social の IP ネットワーク = L3 ネットワークの図でした。これがどのような Ethernet のネットワーク = L2 ネットワークの上に載っているかというと、次のようになっています。
インターネットの回線=上位ルータ(データセンタが管理している)は、直接 LVS に接続せずに一旦 L2 スイッチに収容しています(これも、Webサーバを LVSマシンの代替機にし得るようにするための工夫の1つです)。このスイッチには Webサーバや DBサーバも接続されているため、内部のネットワークと外部のネットワークが混在しないように、VLANを使って分離しています。LVS は両方の VLAN に接続し、それぞれの間を IP レベル(L3レベル)で橋渡し(ルーティング)しています。
クライアントからの Webサーバに対する通信は、次のような経路を通ることになります。
つまり、外部のネットワークから送られてくるパケットは、図にある outer のVLANを通って LVS に到達し、LVS が Web サーバへと渡します。Webサーバからの応答パケットは、この逆順をたどります。
この図を見ていると、何となく、Webサーバからの応答パケットが LVS を経由してクライアントへと送信されいていることが、無駄なように思えてきます。なぜわざわざ LVS を経由させているかというと、外部との通信が全て LVS を経由するこの形であれば、通信でトラブルが発生した場合でも、LVS 上で全ての状況を観察できるため、運用上都合がよいからです。
どうすれば、ボトルネックを解消できるか
しかしながら、どうしても LVSの部分が通信のボトルネックになってしまいます。ですので DSAS for Social では、トラフィックの多い一部のアプリケーションに関しては、次の図のように、Webサーバからの応答パケットが LVS を経由しないような通信経路に切り替えることにしました。いわゆる、DSR(Direct Server Return)構成というやつです。
これでもまだクライアントからのリクエストパケットは、LVSを経由しているので、ここがボトルネックになるんじゃないかと思われるかもしれません。しかしながら Webアプリケーションにおいて、そのトラフィックのほとんどは Webサーバからの応答パケットによるものです。クライアントからのリクエストパケットに要する帯域は応答パケットのものに比べると微々たるものです。冒頭で紹介した最大 2Gbpsを記録したトラフィックも、応答パケットのものです。ですので、今回の問題はこの構成で解決できるのです。
解決策のまとめ
以上、長い前フリが終わったところで、要件をまとめておきましょう。
- ipvs の設定と Webサーバの動作を、DSR 構成にする
- HTTP リクエスト以外の通信に関しては、これまでどおり LVS を経由して通信する
DSAS for Social におけるDSR構成の作り方
では要件が明確になったところで、次に設定するべき項目をあげていきましょう。
- LVS の負荷分散の設定をDSRに変更する(ipvs の設定)
- Webサーバが、DSRなリクエストパケットを扱えるようにする(iptables の設定)
- Webサーバを、outer VLAN に参加させる(L2 スイッチの設定)
- Webサーバが、outer VLAN において通信できるように設定する(VLAN 用インタフェースの追加)
- Webサーバにおいて、上位ルータへの通信経路を設定する(ルーティングと iptablesの設定)
- Webサーバが、上位ルータに対して応答パケットを送信できるように設定する(ARP エントリの設定)
それぞれどういうことか、簡単に説明していきましょう。
DSR 構成をとるためには
まず DSR な負荷分散構成を採る場合、Webサーバに届くリクエストパケットは、負荷分散機などにおいて何も手を加えられてないものになります。どういうことかというと、(Webサーバに届いた)リクエストパケットに記述されている送信先IPアドレスは、いわゆる仮想サービス用のアドレスになります。つまり、LVS へとパケットが届けられたときに送信先アドレスとして使われたグローバルアドレスそのままです。これはWebサーバに割り振られた(プライベート)IPアドレスとは別物です。Webサーバはこのパケットを受け入れて、かつ、応答を返信する際は送信元アドレスとして同じグローバルアドレスを記述して送信しなければいけません。これを解決するのが (b) の設定になります。
(c) と (d) に関しては特に難しい問題は孕んでいません。ちょっと変わっている点は、(d) で作る VLAN 用のインタフェースに、IPアドレスを設定しないことくらいです。しかし、IPアドレスを振らずにすませるために1つ小細工が必要になります。それが (f) です。
(e) は、要件の 2 を実現するための設定です。今回 Webサーバのルーティングテーブルに設定するデフォルトゲートウェイのアドレスは、LVS のIPアドレスのまま変更しません。その上で DSR の応答パケットのみルーティングを切り替えて、上位ルータに渡すようにしたいのです。そのための小細工が必要になります。
さて、ではいよいよ設定です… といきたいところですが、紙面がつきました(笑)ので、続きは明日に…
DSAS環境でのDNS活用法2 〜tinydns-get活用術〜
KLab Advent Calendar 2011 「DSAS for Social を支える技術」 の13日目です。
先週に引き続き、DSAS 環境での DNS 活用法を紹介します。
スクリプト中でのゾーン参照方法
DSAS で使用している各種スクリプト内で、DNS の情報を参照する際に使ってるコードを紹介します。
# 設定情報用のゾーン(.dsas)を検索 mzone () { R="$1"; ( cd $INTERNALZONE 2> /dev/null; _zone TXT $R.dsas ) } # 名前解決を行う # tinydnsのゾーンファイルのコピーがあればtinydns-getを使用 # ゾーンファイルがなければDNS問い合わせを行う _zone () { if [ -r data.cdb ]; then DNSCMD="_zonedjb"; else DNSCMD="_zonedig"; fi; if [ -z "$2" ]; then Q="A"; R="$1"; else Q="$1"; R="$2"; fi; $DNSCMD $Q $R } _zonedig () { dig +short $1 $2 | sed 's/^\"\(.*\)\"$/\1/' } # tinydns-getを使い、カレントディレクトリのdata.cdbを検索 _zonedjb () { tinydns-get $1 $2 }
mzone() と呼んでいる関数では、前回紹介した TXT レコードによる設定データを参照します。
この他に、通常の内部ホスト名の名前解決を行う pzone() や、そのグローバル版の gzone() 等を定義しています。
各サーバでは、起動スクリプト中でこれらの関数等を使い、設定ファイルを生成しています。
下記に抜粋したコードでは、この関数を使い、keepalived.conf の VRRP に関する設定を生成しています。
lan=$(mzone net.private) lan=$(pzone lvs)/${lan#*/} wan=$(mzone net.link) wan=$(mzone lvs.link)/${wan#*/} cat <<EOF vrrp_instance VI { state BACKUP interface bond0 garp_master_delay 5 virtual_router_id $(mzone lvs.vrid) priority ${PRIO} nopreempt y advert_int 1 authentication { auth_type PASS auth_pass ******** } virtual_ipaddress { ${lan} dev bond0 ${wan} dev bond0 } } EOF
パラメータ部分を DNS に逃すことで、生成スクリプトの環境依存部分を極力減らせるほか、複数のスクリプトから参照している設定情報を1箇所で管理することができるようになります。
オフライン時の内部ゾーン参照
DSAS 環境では、多くの設定ファイルを同様の方法で起動時に生成しています。
ネットワークインタフェースはもちろん、resolv.confや、全サーバのローカルで動いているDNSキャッシュサーバの設定ファイルも起動時に生成しているため、これらの生成スクリプトは、ネットワークが使えない状態でも動作出来る必要があります。
ネットワーク設定の生成にネットワークアクセス(DNS参照)が必要という矛盾した状況ですが、とても単純な方法で解決しています。
そもそも DNS 上の情報は、それほど頻繁に更新されるものではないため、サーバの起動時にゾーンファイルを転送してローカルで検索を行い、起動が完了したら DNS の参照に切り替えてやればいいのです。
先ほど紹介した、検索用の関数でも、ローカルにゾーンファイルの一時コピー(data.cdb)が存在するかをキーに動作が切り替わるようになっています。
この方法では、ゾーンファイルの一時コピーを消し忘れると、いつまでも古い情報を参照してしまう恐れがあるので、起動処理の一番最後で必ずゾーンファイルを削除するようにします。
tinydns-getの使い方
tinydns-get はカレントディレクトリの data.cdb をゾーンファイルとして読み込むため、ゾーンファイルの置かれてるディレクトリに移動して、下記のように実行します。
$ tinydns-get A www.klab.jp 1 www.klab.jp: 189 bytes, 1+1+4+4 records, response, authoritative, noerror query: 1 www.klab.jp answer: www.klab.jp 86400 A 211.13.209.202 authority: klab.jp 259200 NS ns1.klab.org authority: klab.jp 259200 NS ns2.klab.org authority: klab.jp 259200 NS ns8.klab.org authority: klab.jp 259200 NS ns9.klab.org additional: ns1.klab.org 14400 A 61.195.64.249 additional: ns2.klab.org 14400 A 61.195.64.247 additional: ns8.klab.org 86400 A 211.13.207.96 additional: ns9.klab.org 86400 A 211.13.207.97
「answer:」の行がクエリの結果です。
tinydns-get では、dig のような「+short」オプションがなく、自分で出力を加工する必要がある上に、TXT レコードの処理に問題があります。
'test.dsas.jp:TEST answer: test.dsas.jp 86400 16 \004TEST 'test.dsas.jp:TEST5678901234567890123456789TEST answer: test.dsas.jp 86400 16 !TEST5678901234567890123456789TEST 'test.dsas.jp:TEST5678901234567890123456789012345678901234567890 12345678901234567890123456789012345678901234567890 123456789012345678901234567TEST answer: test.dsas.jp 86400 16 \177TEST56789012345678901234567890 1234567890123456789012345678901234 5678901234567890123456789012345678 90123456789012345678901234567\004T EST
上記は、上段がバイナリ変換前のゾーンファイルでのレコード、下段が tinydns-get コマンドを使って検索した結果を示します。
各レコードは、1桁目がレコードの種類(TXTレコードは「'」)、続いてコロン区切りでFQDNとレコードの値が続きます。
answer行の各カラムは、検索したFQDN、TTL、レコードのタイプ値(TXTは16)、レコードの値となっています。
ご覧のとおり、レコードの値に「\004」、「!」など、元のゾーンにない文字列が混ざってしまっています。
なんと tinydns-get は、TXT レコードに対応しておらず、data.cdb ファイルに格納されている 「1バイトの文字列長 + 最大127バイトのデータ値」の繰り返しによるデータ列をそのまま表示してしまっているのです。
データにより、「\xxx」や「!」など、表示され方に差があるのは、出力時に ASCII Printable であればそのまま、制御コードなどであれば8進数表記に変換して表示しているためです。
tinydns-get-suppress-msgs.patch
上記のパッチを当てると、tinydns-get が TXT レコードに対応し、余分な文字列を出力しなくなります。
また、少々手荒に書いたパッチですが、2つ目のパッチを当てると、検索結果に関する余分な情報が出力されなくなり、スクリプト等で使いやすくなると思います。
DSAS環境でのDNS活用法 〜ネットワーク設定の格納にDNSを使う〜
KLab Advent Calendar 2011 「DSAS for Social を支える技術」 の12日目です。
昨日までの apache の話題からガラリとかわって DNS についてお話します。
DSAS 内では、サービスに用いるドメインに関する権威サーバのほかに、システム内部の各サーバのホスト名や DB、memcache などの役割に応じた名前を登録した内部向けドメインの権威サーバやキャッシュサーバを運用しています。
システム内に、内部向けの権威サーバや、キャッシュサーバを設置するのは、珍しい構成ではありませんが、DSAS では、内部向け DNS サーバに、システムの設定情報を一部格納しています。
今回は、DSAS 環境で DNS サーバに設定情報を格納している理由や運用方法を紹介します。
ネットブートと設定ファイルの動的生成
DNS の話題に入る前に、DSAS の特徴を 1 つ紹介します。
DSAS では、マスタとなる冗長化された1組のサーバ以外、全てのサーバはネットブートによって起動します。
マスタサーバには、ロードバランサ用・Webサーバ用・DBサーバ用等の役割のサーバを構成するファイル一式が圧縮されたアーカイブが置かれていて、これをOSイメージと呼んでいます。
PXEブートにより、カーネルと initramfs がロードされて起動処理が開始されると、マスタサーバから自分の役割に応じたOSイメージ等をダウンロードして、tmpfs に展開します。
この OS イメージは、さすがに個々のサーバごとに個別に用意するわけにはいかないため、機能毎の共通ファイルとなっています。
そこで問題となるのが、個々のサーバに個別のパラメータを記述する必要がある設定ファイル類の扱いです。
DB サーバの MySQL で使用する server-id、ロードバランサやmemcacheの冗長化に使っている VRRP のルータID 等の各種デーモンの設定ファイルはもちろん、自身のネットワークインタフェースに割り当てるIPアドレスすら、起動処理内で決定する必要があります。
DSAS では、これらの設定ファイル類は、いくつかのタネになる情報を元にして、起動スクリプト内で動的に生成する仕組みを導入しています。
各デーモンや、インタフェースの設定を行う rc スクリプトの前に、設定ファイルを生成するための rc スクリプトを実行するという方法です。
そして、設定ファイルを生成するためのタネ情報を仕込む場所の1つが DNS サーバなのです。
DNSに格納するパラメータの例
DSAS 環境で DNS に登録するパラメータの一例を紹介します。
主に、ネットワークの設定を生成するために必要な情報が中心になります。
パラメータを登録するドメインとして、とあるゾーンを定義して、下記のようにTXTレコードで値を登録しています。
なお、実際の DSAS 環境では、権威サーバの実装に tinydns を使用しているため、tinydns-data フォーマットで記述しています。
private IN TXT "dsas.jp" net.private IN TXT "192.168.0.0/20" global IN TXT "example.klab.jp" net.global IN TXT "XXX.XXX.XXX.XXX/xx" net.link IN TYT "YYY.YYY.YYY.0/29" lv1.link IN TYT "YYY.YYY.YYY.4" lv2.link IN TYT "YYY.YYY.YYY.5" lvs.link IN TYT "YYY.YYY.YYY.6" gw.link IN TYT "YYY.YYY.YYY.3" lv1.vlanid.link IN TXT "1" lv2.vlanid.link IN TXT "1" lvs.vrid.link IN TXT "100"
private/global/net.private/net.globalは、それぞれ内部用・外部用のドメイン名、プライベートアドレス、サービス用グローバルIPアドレスのレンジになっています。
真ん中の *.link 系のレコードは、ロードバランサが自身のネットワーク設定を行うための情報です。
データセンタ側の上位ネットワークと DSAS をつなぐセグメントのアドレスや、その中で使用するIPアドレスなどが定義されています。
( lv1、lv2というのは、冗長化された2台のロードバランサを指し、lvs は VRRP で使用する仮想IPアドレスを指します)
VRRP のルータIDや、インタフェースに設定する VLAN I Dなども登録されているので、ロードバランサのインタフェース設定に必要な情報は、殆ど DNS 内の情報を編集するだけで調整できます。
このロードバランサを含め、ほとんどのサーバが起動時に DNS 上の情報を参照して、自身のネットワーク設定を行なっています。
すると、ネットワーク設定のために DNS を参照する必要があって、それにはネットワークが必要という、鶏が先か卵が先かという問題が発生してしまいます。
次週は、この問題を解決するための方法や、使用しているdjbdnsのツールを紹介したいと思います。
高負荷サイトのボトルネックを見つけるには
はじめに
アクセスが急増すると、応答時間が著しく悪化するサイトはありませんか?
普段は200ミリ秒以内で安定してアクセスをさばいているのに、イベントやらキャンペーンやらを開始した瞬間から、普段の2倍や3倍のアクセスが殺到し、その結果、レスポンスタイムが3秒とか9秒とかかかるようになってしまうことってありますよね。
あるサイトの実状
つい先日まで、そんなサイトが私の目の前にもありました。自社で運営している某ソーシャル系のサイトなんですが、イベント開始時刻と同時にアクセス数が急増するのです。とはいえ、所詮は普段の2倍とか3倍程度の数なのだから、少なくとも1秒以内にレスポンスを返せるくらいの性能は維持したいものです。
しかし実際は困ったことに、応答に3秒以上もかかってしまう処理が大量に発生してしまう状況に陥ってしまっていました。これはきっと、どこかにボトルネックがあるに違いありません。
仮説を立ててみる
当初は「DBでどっかロック待ちしてるんじゃね?」と、アプリケーションレイヤに原因があると思っていましたが、コードを読み返しても、show statusを叩いても、information_schemaを覗いても、ロックを待っている形跡はみつかりませんでした。
参考記事)
SH2の日記:MySQL InnoDBにおけるロック競合の解析手順
大変解りやすく参考になる記事が多くて、とても助かっています。
この場を借りてお礼申し上げます。
なんでかなあ、なんでかなあ、とApacheのアクセスログを眺めながら頭を抱える日々が続いていましたが、ふとあることに気づきました。
アクセスが増えると、どういうわけか3秒台のレスポンスと9秒台のレスポンスの割合が急増しているのです。3と9って、なんかだよく聞く数字だなあと冷静に考えてみると、これってTCPのSYNの再送間隔ではありませんか!
ということは、WebサーバはDBのロックを待っているわけじゃなくて、どっかのサーバとの接続に時間がかかっている可能性の方が高そうです。
実態を調査する
というわけで、早速tcpdumpで調査・・・をしたくなるのが人情(?)ですが、Webサーバに流れているパケットは膨大な量です。その中から再送パケットを見つけ出すのは、それなりに手間と時間と気力が必要です。しかも、問題が発生するのはイベント時などアクセスが集中する時なので、できればWebサーバにも余計な負荷をかけずにどうにかしたいところです。
というわけで、こんなツールを作ってみました
「みるお」と発音してください。いつもながら変な名前でごめんなさい(><)
miruoの詳細は、また後日紹介させていただくつもりですが、ざっくり言うと「異常と思われるパケットだけを表示してくれる tcpdumpみたいなツール」といったところでしょうか。
問題を切り分ける
Webサーバ上でmiruoを実行してみると、以下のような結果が表示されました。
これは、Webサーバがmemcachedへ接続しにいっている様子ですが、どうやら、SYNが再送されている模様です。つまり、memcachedが、なんらかの理由で新規の接続を受け入れられなくなっているのかもしれません。
原因を追求する
今度は、memcachedサーバに入ってnetstatで状況を確認してみました。
$ netstat -s (------ 略 -----) TcpExt: 96 resets received for embryonic SYN_RECV sockets 19626994 TCP sockets finished time wait in fast timer 621 delayed acks sent Quick ack mode was activated 4268 times 107708 times the listen queue of a socket overflowed 107708 SYNs to LISTEN sockets dropped 97529 packets directly queued to recvmsg prequeue. 22246 bytes directly in process context from backlog 76082834 bytes directly received in process context from prequeue 1761768710 packet headers predicted (------ 略 -----)
なんと、このサーバは接続を受け入れきれずに、SYNを取りこぼしてしまっているみたいです。memcachedのbacklogはデフォルト値を使用しているので1024になっているはずです。
$ memcached -h | grep backlog -b Set the backlog queue limit (default 1024)
これって、本当に1024で足りなくなっているのでしょうか。単純にバックログを増やせば解決する問題かなと期待しつつも、どうも釈然としなかったので 、man listen をじっくりと読み返していたところ、
backlog 引き数が /proc/sys/net/core/somaxconn の値よりも大きければ、backlog の値は暗黙のうちにこの値に切り詰められる。このファイルのデフォルト 値 は128 である。バージョン 2.4.5 以前のカーネルでは、この上限値はコード埋め込みの固定値 SOMAXCONN であり、その値は 128 であった。
いやーな予感がしたので、おもむろにコマンドを叩いてみると・・・・・
# sysctl net.core.somaxconn net.core.somaxconn = 128
あちゃあ!
実際はこれ、128になっていたわけですね!
# sysctl -w net.core.somaxconn=1024
としてmemcachedを再起動したところ、大幅に性能が改善されました。
まとめ
MySQLなどをチューニングする時なども、性能を上げるためにbacklogを値を増やすことは多いと思いますが、net.core.somaxconnの存在を忘れ去っていると、せっかく設定しても高負荷時のアクセスを処理しきれなくなってしまう可能性があります。特に今回のケースだと、平常時は何の問題もでていなかったのでさらに厄介です。
もし、高負荷時にのみ極端に性能が落ちるサーバがあれば、このあたりの設定を確認してみるとよいかもしれません。
OpenSSL-1.0.0 でのハッシュリンク使用は要注意
先日(03/29)、OpenSSL のメジャーバージョンアップ版 1.0.0 がリリースされました。
ちょうど1年ほど前から beta リリースが出ていましたが、ついに正式にリリースとなりました。
早速ソースコードをダウンロードして試してみたところ、気になるところを見つけたので記事にします。
サブジェクトのハッシュ
OpenSSLには、証明書や CRL に含まれるサブジェクトや発行者名を 8文字で表現されるハッシュ値に変換する機能があります。
このハッシュ値は、OpenSSL のコマンドラインや SSL/TLS の実装として OpenSSL をリンクしているアプリケーションに対して、個々の CA証明書や CRL のファイルをディレクトリごと指定する際にファイル名として使用します。
$ openssl x509 -noout -hash -in hoge.pem
このようなコマンドラインで、サブジェクト名に対するハッシュ値が計算できます。
得られたハッシュ値を元に「 1a2b3c4d.0 (ハッシュ値が衝突した場合は 1a2b3c4d.1、1a2b3c4d.2 と連番をふる)」といったファイル名で証明書を保存して使います。
しかし、実際のファイル名がハッシュ値だと、ファイル名のみから元の証明書を見分けるのは難しいので、わかりやすいファイル名を付けた元ファイルに対するシンボリックリンク(ハッシュリンク)として作成しておく場合も多いと思います。
(ちなみに、このハッシュリンクを一気に簡単に作る補助スクリプトとして c_rehash という Perlスクリプトが添付されています。)
1.0.0 からハッシュアルゴリズムが変更に
このハッシュリンクを作成したディレクトリ、OpenSSLのバージョンを更新する際でも再作成せずにそのまま使用される方も多いと思いますが、OpenSSL-1.0.0 からは再作成したほうがよさそうです。
公式サイトのアナウンスで出ている主な変更点リストを見てもわからないのですが、ハッシュ計算のアルゴリズムが変更になっています。
例えば、こんな感じで 0.9.8系までと 1.0.0系で違うハッシュ値が計算されています。
$ openssl x509 -noout -subject -in equifax.pem subject= /C=US/O=Equifax/OU=Equifax Secure Certificate Authority $ /usr/bin/openssl version OpenSSL 0.9.8k 25 Mar 2009 $ /usr/bin/openssl x509 -noout -hash -in equifax.pem 594f1775 $ /opt/openssl-1.0.0/bin/openssl version OpenSSL 1.0.0 29 Mar 2010 $ /opt/openssl-1.0.0/bin/openssl x509 -noout -hash -in equifax.pem 578d5c04
具体的には、旧アルゴリズムでは MD5ハッシュを元にして計算していたものが、SHA1ハッシュを元にしたものに変更されています。
OpenSSLのコマンドラインオプションでは、-subject_hash、-issuer_hash ともに新アルゴリズムを使用するものになっていて、旧アルゴリズムを使用する場合にはそれぞれ -subject_hash_old と -issuer_hash_old を使用するようになっています。
-hash を指定した場合には新アルゴリズムを使う -subject_hash が呼ばれます。
OpenSSLコマンドライン内部で実際に呼ばれているライブラリの関数名も X509_subject_name_hash と X509_issuer_name_hash に対して X509_subject_name_hash_old と X509_issuer_name_hash_old が定義されているようです。
最後に
私も、まさかハッシュ計算アルゴリズムが変更になることはないだろうと思い、0.9.8ベースで作ったハッシュリンクを 1.0.0 で使おうとして、verifyが通らずにハマりました。
メジャーバージョンアップ時には、面白い機能の追加や改良以外にも、今まであった機能への変更点についても注意しないといけないですね。
keepalivedの運用ノウハウお見せします 〜 割当管理を簡単にしたい
virtual_server_group SITE1 {
a.b.c.d 80
}
virtual_server group SITE1 {
delay_loop 3
lb_algo wlc
lb_kind DR
nat_mask 255.255.252.0
protocol TCP
persistence_timeout 0
real_server 192.168.8.1 80 {
weight 1
inhibit_on_failure
HTTP_GET {
url {
path /s/health.jsp
status_code 200
}
connect_port 80
connect_timeout 5
nb_get_retry 1
delay_before_retry 2
}
real_server 192.168.8.2 80 {
weight 1
inhibit_on_failure
HTTP_GET {
url {
path /s/health.jsp
status_code 200
}
connect_port 80
connect_timeout 5
nb_get_retry 1
delay_before_retry 2
}
}
RealServerが10台以上もあるような大きなサイトだと、ひとつのサーバグループの設定だけでも1画面に収まり切らないほどの量になってしまうんです。こんな巨大な設定ファイルを人間が書き換えなければならないなんてとてもとても我慢できません。サイトを増やそうとしてどっかの設定をコピペしたままうっかり書き換え忘れるような事故が起きることは容易に想像がつきます。
DSASの特徴のひとつとして、「サーバの増減が容易である」というものがあります。
その実体が 「keepalived.confの編集」だと「どこが容易やねん!」という突っ込みが入ることは目に見えていますよね(笑
DSASでは、設定ファイルの編集ミスによる事故を防止するためと、管理者の負担を軽くするために、様々な工夫をしています。
続きを読む
高トラフィックに対応できるLinuxロードバランサを目指して 〜 LVSをNATからDSRへ
こんなに簡単! Linuxでロードバランサ (1) 〜 LVS + NATで負荷分散をしてみよう
こんなに簡単! Linuxでロードバランサ (2) 〜 keepalivedでWebサーバのヘルスチェック
こんなに簡単! Linuxでロードバランサ (3) 〜 VRRPでロードバランサを無停止にする
こんな流れでNATによる負荷分散システムを構築してきました。
今回はこれを DSR(Direct Server Return) 方式に変更してみます。
「DSRとはなんぞや?」という方は、
ロードバランサの運用.DSRって知ってますか?
L4スイッチはDSR構成にすべし
こちらでわかりやすく説明されていますので参考にしてみてください。
続きを読む
こんなに簡単! Linuxでロードバランサ (1)
安価、というか、ハードウエア以外は金銭的コストがゼロなので、一般のクライアントからのアクセスを受ける外部ロードバランサのほかに、内部サービス用のロードバランサも配置しています。それぞれactive, backupで2台ずつあるので合計で4台もロードバランサがあることになります。(こんな構成を製品を使って組んだら数千万円すっとびますね)
また、ネットワークブートでディスクレスな構成にしているので、ハードディスが壊れてロードバランサがダウンした、なんてこともありません。
ですので「ロードバランサは高くてなかなか導入できない」という話を耳にする度にLVSをお勧めしているのですが、どうも、
- なんか難しそう
- ちゃんと動くか不安
- 性能が出ないんじゃないか
等々の不安の声も聞きます。
そこでこれから数回に渡って、How toスタイルでLinuxでロードバランサを作ってみたいと思います。
今回は初回ということで、ごくごく基本的な
- 複数のWebサーバにロードバランスする
というところまでやってみたいと思います。
では、はじまりはじまり〜
続きを読む
届いたメールの料理の仕方 (1) 〜qmail編〜
今回は、dot-qmailを使う上で知っておきたいことをTIPSを交えて紹介したいと思います。
続きを読む
ネットワークパケットを覗いちゃえ
開眼すると「パケットの流れが目で見える」そうですが、私などはまだまだ修行が足りず裸眼では見えないので、tcpdump というツールを使って見ています。
tcpdump はその名前があまりよくないと私は思っていて、"tcp" だけでなく udp やICMP や ethernet フレームまで覗ける超強力ツールです。
というわけで tcpdump の簡単な説明を書いてみます。
tcpdumpについて
tcpdump は UNIX 系の OS で使えるツールなのですが、Windows 用にはWinDumpという CUI のツールがあり、tcpdump と同じように使えます。
GUI がお好みの方は、後でも紹介するEtherealがおすすめです。UNIX 系でも Windows でも使えます。
Ethereal には CUI 版の tethereal というのもあります。
ちなみに tcpdump のほかには sniffit というがあったり、Solaris には snoop というのがついてきたります。
あと、tcpdump を使うには root 権限が必要です。
まずはオプション
- -i
パケットを拾うネットワークインターフェースをデバイス名 (Linux だと eth0 とか) で指定します。Linux の kernel 2.2 以上の場合は、"any" で全てのインターフェースのパケットをキャプチャできます。
windump の場合は少し違っていて、まず、windump -D で有効なデバイスの一覧を表示させて、キャプチャするデバイスを確認します。デバイスの指定は tcpdump と同じように -i オプションなのですが、windump -D で表示された番号か名前で指定します。名前は長いので番号で指定した方がよいでしょう。
- -n
IP アドレスやポート番号を名前に変換せずに数値のまま表示します。このオプションを指定しないと、いちいち IP アドレスを DNS に問い合わせるので表示が遅くなります。
- -x
パケットの中身を 16 進で表示してくれます。
- -X
パケットの中身で ASCII で表示可能なものを ASCII で表示してくれます。
- -s 長さ
-x や -X を指定した場合、パケットの先頭数十バイトだけが表示されますが、-s オプションでダンプ表示する長さを指定できます。ethernet 上でパケットダンプする際は -s 1600 で十分でしょう。
- -e
ethernet フレームのヘッダを表示します。
典型的なオプションの組み合わせとしては、パケットの流れを追えればよいときは -n か -ne 、パケットの中身 (HTTP のリクエスト/レスポンスとか) まで見たいときは -nxXs 1600 といったところでしょうか。
こんなときどーする?
続いてよく使いそうなレシピ集です。
Debin GNU/Linux (sarge) の tcpdump (tcpdump-3.8.3+libpcap-0.8.3) での動作です。
HTTP を見たい
tcpdump -i eth0 -n tcp port 80
これで HTTP リクエストとレスポンスのパケットが見られます。
特定のホストとのやりとりだけ見たい場合は、
tcpdump -i eth0 -n host 210.253.244.195 and tcp port 80
のようにします。
ポート番号 (80) を変えれば、他のプロトコルにも適用できます。例えば、DNS のやり取りを見たい場合は、
tcpdump -i eth0 -nxXs 1600 port 53
で再帰問い合わせの様子を眺めたりできます。
TCP のハンドシェイクを見たい
TCP は 3way ハンドシェイクを経てコネクションが確立するわけですが、その流れをみてみましょう。
tcpdump -i eth0 -n host 10.10.0.2 and port 80
とした状態で、
telnet 10.10.0.2 80
してみると、
SYN パケットを相手に送って、 ↓ココの "S" が SYN
IP 10.10.2.4.56708 > 10.10.0.2.80: S 3693760150:3693760150(0) win 5840
SYN+ACK が返ってきて、 ↓ココの "S" と ココの "ack"↓
IP 10.10.0.2.80 > 10.10.2.4.56708: S 3033576079:3033576079(0) ack 3693760151 win 5792
最後に ACK を返している ↓ココの "ack"
IP 10.10.2.4.56708 > 10.10.0.2.80: . ack 1 win 1460
のがわかります。
ネットワーク組んだのになぜか TCP が通らない場合は、こうやってどの段階までハンドシェイクが行われているか確認したりします。
icmp を見たい
tcpdump -i eth0 -n icmp
or
tcpdump -i eth0 -n ip proto \\icmp
arp を見たい
tcpdump -i eth0 -n arp
or
tcpdump -i eth0 -n ether proto \\arp
or
tcpdump -i eth0 -ne arp
vrrp を見たい
このへんから古い tcpdump だとエラーになったりするかもしれません。
tcpdump -i eth0 -n vrrp
or
tcpdump -i eth0 -n ip proto \\vrrp
or
tcpdump -i eth0 -n ip proto 112
マルチキャストを見たい
tcpdump -i eth0 -n ip multicast
tcpdump -i eth0 -n multicast だと、ether multicast つまり IPv4 のブロードキャストなどもダンプされちゃいます。
stp を見たい
tcpdump -i eth0 -n stp
IPv6を見たい
tcpdump -i eth0 -n ip6
ポートを限定するときは、"and"で条件を連結します。
tcpdump -i eth0 -n ip6 and port 80
やっぱり GUI が好き
冒頭でも紹介したEtherealがおすすめです。Windows でも使えます。
表示するフィルタのルールがメニューから選択できて必要なものだけ見られますし、ポート番号、プロトコル種別、MAC の OUI (メーカ固有番号) を名前で表示してくれたりもします。あと、パケットダンプを ethernet ヘッダ、IPヘッダなどでハイライトしてくれるのも見やすいですね。
Ethereal 単体でもパケットキャプチャはできますが、tcpdump でキャプチャしたデータを Ethereal で読み込んで見ることもできます。
例えば、
# tcpdump -i eth0 -n -s 1600 -w ~/test.dmp
で適当なころあいで ^C で tcpdump を終了して、できた test.dmp をEthereal で開きます。もちろん、tcpdump -w するときにフィルタルールを指定して、
# tcpdump -i eth0 -n -s 1600 -w ~/test.dmp port 80
などとすることもできます。
この方法は、遠隔のサーバで tcpdump した結果を、手元の PC でじっくり鑑賞するときなどに便利でしょう。
(ひ)