kernel

2016年04月01日

Thundering herd 対策の本命、 EPOLLEXCLUSIVE を試してみた

はてなブックマークに登録

epoll を使った prefork 型アプリケーションサーバーにおける Thundering herd 対策の決定版として注目されていた EPOLLEXCLUSIVE が、 3/13 にリリースされた Linux 4.5 で導入されました。

昨年 SO_REUSEPORT というソケットオプションが登場して、 Thundering herd 対策として話題になったものの、ワーカーごとに listen キューが作られるため graceful restart するときに listen キューに入ってるリクエストを取りこぼす可能性があり利用するのが難しい状況でした。

参考: epoll の thundering herd 問題について解説しているサイト

一方、 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

songofacandy at 15:58|この記事のURLComments(0)TrackBack(0)
2015年11月13日

Raspberry Pi 2 で NAT64 箱をつくってみた

はてなブックマークに登録

最近の VirtualBox の記事の人気っぷりに嫉妬している @pandax381 です。

OSX 10.11(El Capitan)がリリースされ、NAT64 の話題をチラホラ見かけるようになってきましたね。弊社内でも例に漏れず話題に上がってきまして、なぜかデスクの上にラズパイが大量に転がっていたので、そこから1台使って NAT64 箱を作ってみました。

COR0iyKUcAAIqRV-1

準備するもの

とりあえず 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.64eth1 などに読みかえてください。

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_netjoolns_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_devicelinux/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_tnet/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_netpossible_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 *skbskb->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 として正式リリースされています。みなさんは何も気にせず、最新バージョンを使ってください。おしまい。

klab_gijutsu2 at 12:48|この記事のURLComments(0)TrackBack(0)
2012年11月06日

#isucon2 に向けて、かなり間違った方向に本気出してみた(recaro 誕生秘話)

はてなブックマークに登録

先日、NHNさん主催の #isucon2@methane と参加してきたので、事前準備や当日の状況などを数回に分けてレポートしようと思います。#isucon2 が終わって少し体調を崩していた @pandax381 です。

すべてはここから始まった

社内のIRCチャンネルで #isucon2 の開催が話題になっていて、隣の席の @methane が真っ先に参加を表明し、パートナーを募集していました。僕はというと、面白そうだなぁと思いつつも、WebアプリとかDBとよくわかんないし戦力にならんだろうと「椅子投げコンテストw」とか言ってスルーしていたんですが、@methane から「一緒に出ようぜ!」とルフィばりの熱い誘いを受け、参加を決意することになりました。ちょうど #isucon2 開催1ヶ月前の話です。

L7未満は全部なんとかしてくれ!

そんなこんなで #isucon2 への参加が決まり、準備とか何すっかなーと考えてたところ、ペアの @methane が驚愕のツイートをしてるのを発見。

15

ちょっwwwこれはヤバい。草とか生やしてる場合じゃない。「きっと @methane が全部なんとかしてくれる!」とか考えてたら完全に先を越された。まぁ、役割分担的に @methane がアプリを全部 Python で書き下ろしたり、SQLのチューニングをやるだろうから、僕はネットワーク周りとかロードバランサ・Webサーバあたりをやるのかなと思っていたけど。そんなわけで LVS で DSR 構成組んだり nginx をチューニングしたり、sysctl でネットワーク周りのパラメータ調整したりと、ベタな内容で社内にテスト環境を構築して ab でベンチマーク比較しながら最速の構成を考えてました。

最速のWebサーバを求めて

「やっぱり最速は nginx かねー」とか「meinheld を更にいじって nginx より早くしたったw」とか最速Webサーバ話に花を咲かせていたら、どこからともなく「つ khttpd・・」という声が。「khttpdとかオワコンw kernel 2.6系じゃ動かないし」と軽くスルーしてたんですが、またもや @methane から凶悪ツイートががが・・・

41

まぁ、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 で優勝する気満々で「優勝後のブログ記事はどんなこと書くか」とかそんな話題で盛り上がっていました。

そう、あんなことが起きるまでは・・・(つづく)


@pandax381
klab_gijutsu2 at 15:01|この記事のURLComments(0)TrackBack(0)
2011年06月20日

「crashdmesg」の仕組み 〜vmcoreからリングバッファ領域を取得する方法〜

はてなブックマークに登録

前回公開した「crashdmesg」の仕組みを順を追ってご紹介します。

vmcoreを覗いてみる

kexec+kdumpを使って取得したvmcoreの解析には、gdbを拡張したcrashコマンドを利用します。

crashコマンドは、デバッグシンボル付きのカーネルを準備して、次のように実行します。

$ crash -s vmlinux.debug vmcore

ダンプの調査をする場合には、このままcrashコマンドを使って作業を進めるのですが、今回はvmcoreファイルそのものに注目してみます。

このvmcoreファイル、正体はELF形式のコアファイルです。
readelfコマンドを使うとELFフォーマットのヘッダに書かれた情報を表示できます。vmcoreをreadelfで開いてみると、次のような情報を見ることができます。

$ readelf -a vmcore
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              CORE (Core file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          0 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         5
  Size of section headers:           0 (bytes)
  Number of section headers:         0
  Section header string table index: 0

There are no sections in this file.

There are no sections in this file.

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  NOTE           0x0000000000000158 0x0000000000000000 0x0000000000000000
                 0x000000000000083c 0x000000000000083c         0
  LOAD           0x0000000000000994 0xffffffff81000000 0x0000000001000000
                 0x0000000002125000 0x0000000002125000  RWE    0
  LOAD           0x0000000002125994 0xffff880000000000 0x0000000000000000
                 0x00000000000a0000 0x00000000000a0000  RWE    0
  LOAD           0x00000000021c5994 0xffff880000100000 0x0000000000100000
                 0x0000000018f00000 0x0000000018f00000  RWE    0
  LOAD           0x000000001b0c5994 0xffff88001f000000 0x000000001f000000
                 0x0000000000ffd000 0x0000000000ffd000  RWE    0

There is no dynamic section in this file.

There are no relocations in this file.

There are no unwind sections in this file.

No version information found in this file.

Notes at offset 0x00000158 with length 0x0000083c:
  Owner         Data size       Description
  CORE          0x00000150      NT_PRSTATUS (prstatus structure)
  CORE          0x00000150      NT_PRSTATUS (prstatus structure)
  VMCOREINFO    0x0000055a      Unknown note type: (0x00000000)

LOADセグメント内に、メモリ内容がダンプされています。
/proc/iomemやカーネルドキュメント(Documentation/x86/x86_64/mm.txt)と見比べてみると、カーネル本体がロードされている領域、メモリの先頭から640KBまでの領域、残りのメモリのうちセカンドカーネルが使用する領域を除いた部分に分割して格納されているようです。

ELFプログラムヘッダを解析すれば、各LOADセグメントがどの物理・仮想アドレスに対応するかと、vmcoreファイル中でのオフセット位置 がわかります。シンボル情報等から目的の値が格納されている仮想アドレスがわかれば、vmcoreから内容を取得することができそうです。

カーネルログ領域(リングバッファ)を探す

今回の目的である、カーネルのリングバッファ領域がどこにあるのか探してみましょう

リングバッファの定義や、アクセスする関数は kernel/printk.c に書かれています。
(以降のカーネルソースの引用で、行頭に書かれている行番号は、バージョン 2.6.37 時点での番号です。)

kernel/printk.c:
  58 #define __LOG_BUF_LEN   (1 << CONFIG_LOG_BUF_SHIFT)
 146 static char __log_buf[__LOG_BUF_LEN];
 147 static char *log_buf = __log_buf;
 148 static int log_buf_len = __LOG_BUF_LEN;

バッファのサイズはカーネルビルド時にCONFIG_LOG_BUF_SHIFTで指定します。 カーネル上でもlog_buf_len変数にバイト単位で格納されていて、バッファ本体はそのサイズ分の配列として__log_bufと定義されています。
バッファサイズlog_buf_lenとバッファ本体__log_buf(もしくはlog_buf)が分かればバッファ領域を取得することができそうです。

リングバッファが循環する仕組みを探る

運用中のサーバのカーネルメッセージをdmesgコマンドで表示したことがある人は、カーネルメッセージの量が多くなってくると古いログが消えていくのを見たことがあると思います。
カーネルメッセージは、リングバッファという仕組みで、一定のサイズ内で循環するように書きこむことで古いログを捨てながら保存されていきます。 この仕組を探ってみましょう。

kernel/printk.c:
 109 #define LOG_BUF_MASK (log_buf_len-1)
 110 #define LOG_BUF(idx) (log_buf[(idx) & LOG_BUF_MASK])
 116 static unsigned log_start;  /* Index into log_buf: next char to be read by syslog() */
 117 static unsigned con_start;  /* Index into log_buf: next char to be sent to consoles */
 118 static unsigned log_end;    /* Index into log_buf: most-recently-written-char + 1 */
 149 static unsigned logged_chars; /* Number of chars produced since last read+clear operation */
 
 548 static void emit_log_char(char c)
 549 {
 550     LOG_BUF(log_end) = c;
 551     log_end++;
 552     if (log_end - log_start > log_buf_len)
 553         log_start = log_end - log_buf_len;
 554     if (log_end - con_start > log_buf_len)
 555         con_start = log_end - log_buf_len;
 556     if (logged_chars < log_buf_len)
 557         logged_chars++;
 558 }

バッファへの書き込みを循環させるロジックのキモは、LOG_BUF(idx)というマクロです。
LOG_BUF(idx) マクロを使用してログメッセージを書きこむ際、単純に log_buf[idx] に書くのではなく、インデックスに log_buf_len - 1とのAND演算を行っています。
バッファサイズは、2のべき乗バイトに制限されているので、LOG_BUF_MASK はちょうど log_buf のインデックスの範囲(0 〜 log_buf_len-1)を選択するマスクになるため、バッファサイズを超えた部分の桁を無視することができます。
こうして、idx がバッファサイズを越えても、オーバーフローした桁を無視するので先頭に戻ってくるのです。

printk等からのバッファへの書き込みはemit_log_char()関数が使われますが、その中で特にバッファ領域のダンプに必要な情報として、log_endとlogged_charsが更新されます。
バッファに書きこむたびに、log_endは単純増加しますが、logged_charsはlog_buf_lenまで到達すると、それ以後の書き込みでは増えません。

実際に、バッファ領域をダンプするには、logged_charsの値をもとに、バッファがまだ一杯ではない状態と、バッファが一杯で循環して書き込まれている状態の2パターンを考える必要があります。

  • バッファが一杯でない場合:
    バッファ領域の先頭から、logged_charsまでの範囲
  • バッファが一杯で循環書き込みしている場合:
    前半(バッファ領域的には後半)部分、log_end & (log_buf_len - 1) から log_buf_len - 1 までの範囲と、
    後半部分、バッファの先頭から log_end & (log_buf_len - 1) - 1までの範囲

シンボル情報を探す

log_buf、log_buf_len、logged_chars、log_endの4つの値が分かればリングバッファを正しい順序でダンプできそうなので、この4つの値が格納されている仮想アドレスを探してみましょう。

カーネルビルド時に生成されるSystem.mapからシンボル情報をさがす方法と、同様の情報をカーネル本体(vmlinux)から探す方法の2つを試してみます。

■ System.mapから探す方法

$ egrep " (log_buf|log_buf_len|logged_chars|log_end)$" /boot/System.map
ffffffff82c16b60 d log_buf_len
ffffffff82c16b68 d log_buf
ffffffff82d2a080 d log_buf
ffffffff82edb300 b log_end
ffffffff82edb3e0 b logged_chars

■ vmlinuxをobjdumpする方法

$ objdump -t /boot/vmlinux |egrep " (log_buf|log_buf_len|logged_chars|log_end)$"
ffffffff82c16b60 l     O .data  0000000000000004 log_buf_len
ffffffff82c16b68 l     O .data  0000000000000008 log_buf
ffffffff82edb300 l     O .bss   0000000000000004 log_end
ffffffff82edb3e0 l     O .bss   0000000000000004 logged_chars
ffffffff82d2a080 l     O .data  0000000000000020 log_buf

(サイズの違うlog_bufが2個見つかっていますが、0xffffffff82d2a080にあるlog_bufはリングバッファとは無関係なネットワーク関連の構造体のようです。)

これで、各変数の格納されている仮想アドレスが判明したので、vmcore内でこの変数が格納されているLOADセグメントを探して読み出せば、リングバッファのダンプが可能になります。

VMCOREINFOからシンボル情報を取得する

リングバッファをダンプするのに必要な情報は集まりましたが、ダンプするにはSystem.mapかvmlinuxが必要です。
crashdmesgは、クラッシュカーネル上で実行することを前提に考えているので、クラッシュしたカーネルのSystem.mapやvmlinuxが読めるとは限りません。

ここで、先程、readelfで読み取ったvmcoreの情報を見直してみます。
NOTEセグメントの中に、VMCOREINFOという意味深なデータが入っています。

  VMCOREINFO    0x0000055a      Unknown note type: (0x00000000)

ダンプしてみると、このような内容で、CRASHTIMEを除き、"NAME=num\n"もしくは"TYPE(name)=num\n"という形式のテキストで、いくつかの情報が出力されています。
log_buf等の情報もばっちり記録されています。

VMCOREINFOの内容は、カーネルソースの各所からVMCOREINFO_SYMBOL(name)等のマクロを呼ぶことで記録しています。 ちなみに、CRASHTIMEの値だけは、panic関数から呼ばれるcrash_kexec関数の処理内で追記されるため、カーネルパニックが起こった時間の正確な記録として使えそうです。


crashdmesgでは、vmcoreのNOTEセグメントに保存されているVMCOREINFOのシンボル情報を利用することで、vmcoreファイル以外の追加情報を読み込むこと無く、リングバッファをダンプできるようになっています。

次回は、セカンドカーネル上でこのcrashdmesgを利用するためのinitramfsの作り方等をご紹介しようと思います。


#dSn
klab_gijutsu2 at 14:39|この記事のURLComments(0)TrackBack(0)
2011年06月10日

クラッシュダンプからカーネルメッセージを取り出すツール「crashdmesg」を作りました

はてなブックマークに登録

Linuxカーネルには、カーネルパニック時にkexecを使ってダンプ取得用のカーネル(セカンドカーネル)を起動する仕組みがあります。
このセカンドカーネルは予めリザーブされたメモリ内で起動するため、クラッシュしたカーネルが処理していたメモリの内容はそのまま残っていて、procファイルシステム経由でクラッシュダンプを取得する事ができます。

このDSASブログでも、以前「Linuxでクラッシュダンプを採取(1) 〜 kexec + kdump を使ってみる 〜」と言うタイトルでクラッシュダンプの取得方法をご紹介しました。

「crashdmesg」は、kexec+kdumpで保存したクラッシュダンプから、カーネルメッセージの内容を取り出すツールです。
デバッガと比べてはるかに軽量なため、セカンドカーネル上で直接/proc/vmcoreからカーネルメッセージを取り出すこともできます。

最近のクラッシュダンプ事情

クラッシュダンプにはサーバ上のメモリの内容がほとんど保存されているため、カーネルメッセージのほか、クラッシュ時に稼働していたプロセスやネットワークの状態など、多くの情報を取得することができます。
カーネルパニックでサーバがダウンした場合、syslog等でのカーネルメッセージの転送は間に合わず、ログが全くない状態で原因究明を行わなければならないことが多く、クラッシュダンプを取得する仕組みは、カーネルに起因するトラブル解析の強い味方です。

しかし、クラッシュダンプを取得する方法にはデメリットもあります。
サーバの搭載メモリのほぼ全てをダンプするため、クラッシュダンプのサイズも搭載メモリに合わせて大きくなり、保存に時間がかかるのです。
例えば弊社では、メモリを72GB搭載して運用しているサーバがあり、この場合では72GB近いダンプファイルをローカルディスクもしくはネットワーク上の他のサーバに転送する必要があります。
転送が終わるまでは、サーバの再起動や調査を行うことができず、復旧が遅れてしまいます。

最小限のダウンタイムで情報集め

そもそも、巨大なダンプファイルを転送する時間がないのであれば、セカンドカーネル上でクラッシュダンプを解析してしまい、結果だけを転送するという方法を思いつきました。
クラッシュダンプを解析するcrashコマンドに、予めカーネルメッセージのダンプや、プロセス、ネットワーク状態等の状況を出力するスクリプトを実行させて、結果のテキストデータだけを転送するという作戦です。
パニックのメッセージやスタックトレースの出力に応じたデバッグはできなくなりますが、カーネルのバグフィックス情報を調べる手がかりぐらいは読み取れるのではないかと考えたのです。

しかし、crashコマンドを実行するには、100MB近いサイズになるデバッグシンボル付きのカーネルが必要になるほか、crashコマンドの実行自体もかなりのメモリを必要とします。
セカンドカーネルは予めリザーブした領域上で動くため、非常時にcrashコマンドを実行するためだけに多量のメモリをリザーブするわけにもいかず断念しました。

crashdmesg

そこで思い切って、取得する情報をカーネルメッセージのバッファ領域だけに絞り、カーネルダンプファイルから直接情報を取得することができないか調べてみました。
すると、いくつかのシンボル情報が取得出来れば、クラッシュダンプからカーネルメッセージのバッファ領域を拾うことができそうだと分かり、「crashdmesg」というツールを作ってみました。

その名のとおり、dmesgコマンドのクラッシュダンプ版をイメージしていて、/proc/vmcoreもしくは引数に指定したダンプファイルからカーネルメッセージをダンプして標準出力に出力します。
軽量なプログラムですので、セカンドカーネル上の僅かなメモリの中でも使うことができます。
しかも、ダンプに必要なシンボル情報をvmcore内から読み取るため、デバッグシンボル付きのカーネルやSystem.mapを用意する必要がありません。

最後に

crashdmesgのソースコードは、githubで公開しています。

https://github.com/hiro-dSn/crashdmesg

crashdmesgの仕組み解説や、kexec+kdump使用のコツなども、追ってこのDSASブログに掲載したいと思います。


#dSn
klab_gijutsu2 at 19:13|この記事のURLComments(0)TrackBack(0)
2008年07月25日

linux のシステムコールをフックする

はてなブックマークに登録

最近、とあるクローズドソースなデバイス管理ツールの挙動が気になり、その動作について解析してみることにしました。

プログラムをデバッグしたり解析したい時、どんなシステムコールが呼ばれ、どのような引数が渡されているかを、調べることができる strace は非常に有用です。

しかし、strace では ioctl で渡される複雑なデータ構造を表示することはできないため、システムコールをフックして引数を表示するという手段を取ることにしました。

そんな訳で linux でシステムコールをフックする方法について調べて見たところ、意外といろいろな方法が有ることを知りましたので、試してみた方法を幾つか紹介したいと思います。

注)今回の実験に使用した linux kernel のバージョンは 2.6.25.11 です。異なるバージョンではこの実験通りにはならない場合があります。

LD_PRELOAD を使ってフックする

まず一番手っ取り早そうなのは LD_PRELOAD を使用して hook する方法です。

参考: ウノウラボ Unoh Labs: LD_PRELOADを使って任意の関数呼び出しにフックしてみる

しかし、今回私が解析したいプログラムは libc をスタティックリンクした上に strip しているというバイナリでしたので、残念ながらこの方法では hook することが出来ませんでした。

kernel module でフックする

次に、kernel module を使用したフックに挑戦してみました。

参考: Linux Kernel Hacking

こちらを参考にして以下のような open をフックする kernel モジュール を書いてみたのですが、うまくコンパイルできません。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/syscalls.h>
#include <linux/utsname.h>

MODULE_LICENSE("GPL");

asmlinkage int (*orig_open)(const char *pathname, int flags);
asmlinkage static int hook_open(const char *pathname, int flags)
{
    printk(KERN_INFO "hook_open(\"%s\", %d)\n", pathname, flags);
    return orig_open(pathname, flags);
}

static int hook_init(void){
  printk(KERN_INFO "hook_init\n");
  orig_open = sys_call_table[__NR_open];
  sys_call_table[__NR_open] = hook_open;
  return 0;
}

static void hook_exit(void){
  printk(KERN_INFO "hook_exit\n");
  sys_call_table[__NR_open] = orig_open;
}

module_init(hook_init);
module_exit(hook_exit);

どうやら上記の方法で hook 出きるのは kernel 2.4 の頃までで、 2.6 からはセキュリティ上の理由から sys_call_table シンボルが export されなくなったようです。

kernel 変更してのフック

どうにか 2.6.x でも hook する方法は無いのかな、と調べてみると以下の方法を見つけました。

Re: using sys_mknod in init_module

メールの内容と若干異なりますが。x86 32bit アーキテクチャの場合 arch/x86/kernel/i386_ksyms_32.c へ以下の2行を追記し、

extern void* sys_call_table[];
EXPORT_SYMBOL(sys_call_table);

kernel module に以下の宣言を追記することで無事ビルドして insmod 出来るようになりました。

extern void *sys_call_table[];

早速 insmod して

# dmesg -c
# cat /etc/passwd > /dev/null
# dmesg
hook_open("/dev/null", 33345)
hook_open("/etc/ld.so.cache", 0)
hook_open("/lib/libc.so.6", 0)
hook_open("/etc/passwd", 32768)
hook_open("/etc/ld.so.cache", 0)
hook_open("/lib/libc.so.6", 0)

ばっちり hook することが出来ました。

kernel を変更しないでフックする

rootkit の危険性は理解出来るのですが kernel を変更しなければ hook 出来ないというのも大変なので、kernel を変更を加えず、kernel module のみで hook 出きるかどうか調べてみました。

  1. sys_call_table をハードコーディングする
  2. sys_call_table が export されなくともアドレスを予め調べておいてハードコードしてやればよさそうです。未圧縮の vmlinux が残っていれば sys_call_table のアドレスを簡単に調べることが出来ます。(/proc/kallsyms でも調べられるようです)

    nm vmlinux|grep sys_call_table
    c02bc838 R sys_call_table
    

    問題解析の用途ではこの方法で十分かもしれませんね。

  3. sys_call_table を探索する
  4. ハードコードするのはちょっとなぁという場合は module の init 時に sys_call_table を探索するという手がある様です。

    Finding the Linux System Call table in 2.6 series kernels | Subversity

    こちらに sys_call_table を探索するコードが紹介されているのですが、unlock_kernel シンボルが見当たらないためビルドできませんでした。

    unlock_kernel の代わりに、程よく sys_call_table の前方にあるシンボルを眺めていたところ strstr というシンボルがちょうど良さそうなので、これを使用した sys_call_table の探索コードを紹介します。

    unsigned long **find_sys_call_table(void)
    {
       unsigned long **sctable;
       unsigned long ptr;
             
       sctable = NULL;
       for (ptr = (unsigned long)&strstr;
            ptr < (unsigned long)&boot_cpu_data;
            ptr += sizeof(void *))
       {
          unsigned long *p;
          p = (unsigned long *)ptr;
          if (p[__NR_close] == (unsigned long) sys_close)
          {  
             sctable = (unsigned long **)p;
             return &sctable[0];
          }
       }
       return NULL;
    }
    

まとめ

今回は、問題解析のためにとにかくフックしたかったのでやや強引な方法になってしまいましたが、もっと真っ当な方法でシステムコールをフックする方法がありそうです。

また、今回いろいろな方法を調べてみてシステムコールのフックに興味が出てきたので、また別の方法を知った時には紹介してみたいと思います。

--
hamano
klab_gijutsu2 at 09:00|この記事のURLComments(3)TrackBack(0)
2007年08月16日

並列プログラミング(その3)

はてなブックマークに登録

4.Atomicなメモリ書き換え

その1で、単純なLoad/StoreについてのAtomicについて説明しました。 こんどは、Read-Modify-Writeについてについて考えます。

Read-Modify-Writeを複数のスレッドが実行する際、マルチプロセッサ環境で 同期を取るには、そのプロセッサのメモリモデルに沿った同期機構が必要です。 そのため、大抵のCPUには、AtomicなLoad&Store命令を持っています。LoadとStoreが Atomicであるということは、LoadしてからStoreするまでの間に他のプロセッサが LoadもStoreもできない(=バスロックしている)ということです。

続きを読む
klab_gijutsu2 at 14:18|この記事のURLComments(0)TrackBack(0)
2007年08月15日

並列プログラミング(その2)

はてなブックマークに登録

3.Memory Ordering

シングルプロセッサのマルチスレッドでは、volatile変数をフラグにして簡単な同期を書くことができました。 例えば、次のような感じです。(コンパイラはvolatile変数へのアクセスの順序を入れ替えないものとします)

volatile int done = 0;
volatile struct { int foo; int bar; } foobar;

void writer(void) {
    foobar.foo = fizz();
    foobar.bar = bazz();
    done = 1;
}
 
void reader(void) {
    int foo, bar;

    while (!done) sleep(1);
    foo = foobar.foo;
    bar = foobar.bar;
}

これは、マルチプロセッサ環境では上手くいかないことがあります。今時のCPUは、命令を順番に実行するとは限らないからです。例えば、メモリ書き込みを後回しにしたり、メモリ読み込みを投機的に(先走って)実行します。

続きを読む
klab_gijutsu2 at 13:00|この記事のURLComments(0)TrackBack(0)
2007年08月14日

並列プログラミング(その1)

はてなブックマークに登録

1.マルチプロセッサ時代の並列プログラミング

Pentium4でHyperThreadingが採用されてから、一般的なPC用のCPUでも並列動作が発生するようになりました。 マルチスレッドプログラムにおいて、シングルプロセッサ環境では問題にならなかった事が、マルチプロセッサ 環境では問題になってきます。

もちろん、pthreadなどを利用して普通にプログラムを書いている場合は、複数のスレッドから同一の メモリにアクセスするところを全てMutexやSemaphoreで同期しておけば問題ありません。が、プロセッサ間の 同期ってどんな問題があってどうやって対処しているのか気になったので、調べてみました。

IntelやAMDのDeveloper's Manualなどを読んで勉強しながら書いているので、間違っている部分が あるかもしれません。間違いに気づかれた方は、宜しければコメントやトラックバックでご指摘 下さい。

続きを読む
klab_gijutsu2 at 13:00|この記事のURLComments(0)TrackBack(0)
2006年09月14日

DBサーバ向けLinuxチューニングを考える 〜 メモリオーバーコミット編

はてなブックマークに登録

Cでプログラムを書いていて大量のメモリを確保したくなったとき、大抵は mallocを使うと思いますが、その際には戻り値がNULLかどうかを判断してエラー処理に飛ばすと思います。しかし、Linux のメモリ管理サブシステムには「メモリ・オーバーコミット」という機構があり、実装されているメモリ以上の領域を確保できてしまいます。

続きを読む
klab_gijutsu2 at 17:13|この記事のURLComments(2)TrackBack(0)
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント