Kyoto.なんか #2 で Rust の実践的な話について発表してきました
タイムラインをみていると流れてきたので、なんか会社休みだし、なんか実家にいるので Kyoto.なんか #2 で インフラエンジニアのための Rust というタイトルで発表してきた。
今回、発表で話しきれなかったことやうまくいかなかったことを、ブログ記事に発表内容をまとめた。
※ 発表資料はあとでリンク貼ります。
モチベーション
趣味で Rust を書いていて、 Rust に対するモチベーションをあげたかった話。
Rust というと一般にシステムプログラミング言語で、変数や式の所有権を付与することで安全性や高速性が謳われている。 Hacker News や Reddit を眺めているとほぼ毎日のように Rust の話があって、みんな OS や DNS サーバを実装したり、知見を共有し合っていて楽しそうだなあと思っていた。 趣味のプロダクトだけではなくて、エンタープライズでも一部のコンポーネントとして使っている事例が CloudFlare や Dropbox からもあがっていて、すごくホットな雰囲気も伝わってくる。 一方で国内の Rust 事情はというと、あまり情報が出回っていなくて、そもそも Rust の実践的な話を聞かないなあと感じていたのが今回の話につながっている。
最近、業務で苦労していることを絡めて Rust で netmap を使ったネットワークパケットの作成、解析として将来を見据えた話題になればなあと思って話をしてきた。
実践的な Rust の使い方を目指して
最近、 AWS を使って運用している案件で高トラフィックな要件の案件があって、負荷試験なんかやるとボトルネックが掴みにくくなっていて困っている。 大抵は EC2 インスタンスのリソースが限界になることが多くて、インスタンスタイプごとの性能がこういった形で共有されているのだけど、帯域はともかく m3.large なんかだと PPS は限界に到達してしまう事案が発生している。
問題に対して、今のところは数の暴力で殴って解決していて落ち着いている。 けれども、それではコストがかさむし、AWS だからといって無限にスケールアウトできるわけでもない。 さらに、パケットの処理能力がボトルネックとなっているほどネットワークが高負荷な場合、パケットキャプチャしようとすればパケットを取りこぼす可能性がある。 パケットキャプチャで取りこぼすようだと、何かネットワークまわりで問題が発生したときに調査がしづらくなるため見過ごせない。
そうした課題は以前から社内でも懸念に上がっていて、 netmap や Intel DPDK といった技術は注目していて情報は追っていた。 Rust で netmap や Intel DPDK を利用して何かできれば実践的な話になるんじゃないかなあということで、Rust から netmap を使ってみた。
Rust で packet を作成 & 解析する
Rust には libpnet という Crate (Rust のライブラリ) があって、汎用的なプロトコルスタックを持っていてパケットを作成 & 解析するのに使える。
標準では扱うレイヤによって OS のネットワークスタックを使うようなんだけど、その気になれば自前でネットワークスタックを libpnet 上で作ることもできる。
リポジトリの examples
にはパケットキャプチャのサンプルプログラムが含まれていて、 lo を指定してパケットキャプチャしてみると以下のような結果を得ることができる。
./target/debug/pnet-study-general lo [lo]: TCP Packet: 127.0.0.1:52472 > 127.0.0.1:6600; length: 39 [lo]: TCP Packet: 127.0.0.1:6600 > 127.0.0.1:52472; length: 269 [lo]: TCP Packet: 127.0.0.1:52472 > 127.0.0.1:6600; length: 32 [lo]: TCP Packet: 127.0.0.1:52472 > 127.0.0.1:6600; length: 44
コードも 200 行程度しかなくて、簡潔な記述でかけることがわかると思う。 パケットキャプチャと逆の要領でパケットを作成することもできて、各レイヤの処理に精通していなくても必要なヘッダやパラメタが分かるところが嬉しい。
さらに libpnet では様々なデータリンク層のインターフェイスがサポートされていて、 以下のリンクから見れる。
https://github.com/libpnet/libpnet/tree/master/src/datalink
libpnet がサポートするデータリンク層のインターフェイスには中には netmap も含まれていて、今回はこれを利用した。
試してみたのは ICMP echo-request と ICMP echo-reply の簡単な例。 netmap の環境を用意するには netmap をサポートしたデバイスを持っていなかったため、docker を利用して veth インターフェイスを作成し環境を切り分けて、 veth 上で netmap を試してみた*1。 ICMP echo-request を送信して、受け取ったパケットを解析して標準出力に出力した後に ICMP echo-reply を返し、返したパケットも解析して標準出力に出力している。
root@6485c836a3ff:/# /netmap-ping eth0 172.18.0.1 64 bytes from 172.18.0.1: icmp_seq=0 ttl=64 time= 64 bytes from 172.18.0.1: icmp_seq=1 ttl=64 time= 64 bytes from 172.18.0.1: icmp_seq=2 ttl=64 time= debug # ./pnet-study-pong vethf90b4d8 [vethf90b4d8]: ICMP echo request 172.17.0.2 -> 172.18.0.1 (seq=0, id=70) [vethf90b4d8]: ICMP echo reply 172.18.0.1 -> 172.17.0.2 (seq=0, id=70) [vethf90b4d8]: ICMP echo request 172.17.0.2 -> 172.18.0.1 (seq=1, id=70) [vethf90b4d8]: ICMP echo reply 172.18.0.1 -> 172.17.0.2 (seq=1, id=70) [vethf90b4d8]: ICMP echo request 172.17.0.2 -> 172.18.0.1 (seq=2, id=70)
OS のネットワークスタックを使うと、 ICMP のヘッダとデータ部分を作成するだけでいいんだろうけれど、 netmap を使うと当然ながら OS のネットワークスタックを使えない。 そのため、今のところパケットを全部自前で作成しないといけないところが少ししんどいところ。
ちなみに、今回利用した、コードは以下のリンクから
- https://github.com/kizkoh/rust/tree/master/netmap-ping
- https://github.com/kizkoh/rust/tree/master/pnet-study-pong
netmap-ping の方は需要ありそうだと思っていて、これからも機能を加えていきたいなぁと思っているので、アップデートしていきたい。
まとめ
発表初めに会場で Rust を書いたことがある人を聞いたら結構、多くて驚いた。 # たぶん、若手エンジニアが多かったからだろうな〜。若手エンジニアが多いからこそ、 Rust について話そうと思ったんだけども。
Rust を使ったモチベーションとして、インフラエンジニア向けな話だったと思うのでそこは刺さりにくいのよなあと思ったので少し反省。 けれども id:y_uuki さんや id:moznion くんには刺さったようで、発表後や懇親会ではずっと喋っていてて、一定手応えはあったと感じることができたので良かった。
コードを書いていて、思ったのが libpnet のプロトコルスタックを使うと簡単にパケットを作成できるので libpnet は優秀だなあと感じた。 使っていて思ったのはパケットを作成するというよりはどちらかというと、受け取ったパケットを一部書換えてキャプチャしたり、転送するのに向いているなと思ったので、ロードバランサやホワイトボックススイッチとかの実装に向いているんじゃないかなあと思った。 そういったこれからも発展していくものに関わっているところで Rust は将来明るい技術だと思うし、学ぶモチベーションが上がるし、プロダクション投入に向けて高めていきたい。
最後になりましたが、こうした発表の機会を設けてくださり、ありがとうございました!! > id:hakobe932, id:hitode909
*1:発表した時点ではうまく疎通できなかったんだけど、 veth だから実際は一つのインタフェイスしかないわけで..普通に考えてコンテナの内側も外側も netmap じゃないとダメだよねといオチだった
Cargo.toml の編集に cargo-edit を使う
この記事は以下の記事の本記事です。 qiita.com
cargo-edit について cargo-edit の README.md をベースに簡単に日本語でまとめています。
※ 執筆時点の cargo-edit のバージョンは cargo-edit-0.1.3
です。
cargo は Rust の強力なツールの一つで、パッケージの取得、ビルドといった機能を持っています。 パッケージの追加や削除といった作業は Cargo.toml を編集して行えます。 管理するパッケージが増えるにつれ、依存解決やバージョンの変更などを行いたくなる場合、ファイルの編集というインターフェースはエディタで開くという手間もあり面倒に感じることが多くなるでしょう。 cargo-edit はそれを解決してくれるツールで、パッケージの追加・削除・確認を行うことができます。
cargo-edit: https://github.com/killercup/cargo-edit
インストール
cargo install
でインストールできます。 cargo はなるべく最新版を利用することが推奨されています。
cargo install cargo-edit
使い方
cargo に以下のサブコマンドをサポートする形でインストールされます。
cargo add
cargo list
cargo rm
各サブコマンドは cargo のサブコマンドと同様に cargo add --help
のようにサブコマンドの後に --help
を指定するとヘルプが出力されます。
cargo add
cargo add
は名前の通り、パッケージを追加します。
# パッケージ名@バージョン でバージョンを指定する $ cargo add regex@0.1.41 # バージョンが指定されない場合は最新のバージョン番号を crates.io から取得する $ cargo add rand --build # crates.io に存在しないパッケージを追加することもできる $ mkdir -pv ./lib/trial-and-error $ cd ./lib/trial-and-error $ cargo init --name local_experiment $ cd ../../ $ cargo add local_experiment --path=lib/trial-and-error/
Cargo.toml はこうなります。
[package] authors = ["kizkoh"] name = "study" version = "0.1.0" [build-dependencies] rand = "0.3.14" [dependencies] regex = "0.1.41" [dependencies.local_experiment] optional = false path = "lib/trial-and-error/"
cargo list
cargo list
は依存パッケージの一覧を出力します。
# cargo build しないと `Your Cargo.toml is missing.` が出力される $ cargo build # --tree オプションで依存木を出力できる $ cargo list --tree ├── rand (0.3.14) │ └── libc (0.2.14) └── regex (0.1.73) ├── aho-corasick (0.5.2) │ └── memchr (0.1.11) │ └── libc (0.2.14) ├── memchr (0.1.11) │ └── libc (0.2.14) ├── regex-syntax (0.3.4) ├── thread_local (0.2.6) │ └── thread-id (2.0.0) │ ├── kernel32-sys (0.2.2) │ │ ├── winapi (0.2.8) │ │ └── winapi-build (0.1.1) │ └── libc (0.2.14) └── utf8-ranges (0.1.3)
cargo rm
cargo rm
は cargo add
の逆の操作です。
# regex の依存を削除する $ cargo rm regex # rand の依存を削除する $ cargo rm rand --build
Cargo.toml はこうなります。
[package] authors = ["kizkoh"] name = "study" version = "0.1.0" [dependencies] [dependencies.local_experiment] optional = false path = "lib/trial-and-error/"
まとめ
ターミナルは常にビルドやテストのため開いていると思います。 cargo-edit を使えばエディタで Cargo.toml を開くことなくコマンド一つでパッケージの追加・削除・確認を行うことができます。
また、記事中では例に取り上げませんでしたが cargo add
で追加したパッケージはアルファベット順にソートされます。 rustfmt を使ってコードはフォーマッティングできても Cargo.toml はフォーマットできません。見栄えを気にする際も便利なツールです。使わない理由はないですね!
Python の faulthandler を uwsgi で指定する
Python で実装した Web アプリケーションを uwsgi 上で動作させて運用しているときのお役立ち Tips 的なやつ。
仕事で uwsgi を運用しているのだけど、先日 uwsgi の worker がずっと busy な状態になっているプロセスが常駐していたので調査した。 strace でプロセスの状態を追うと read() で止まっていてその前後は読めない状態だった。その時は read() で待たされていることが原因だったので read() している fd を特定して、どういったことが原因で待たされているのか調査することで解決した。
けれども、もう少しスマートに解決したいという欲求はあって、 Python の faulthandler [1]なる機能を教えてもらったので調べてみた。
faulthandler を使うと指定したシグナル (SIGSEGV, SIGFPE, SIGABRT, SIGBUS, SIGILL) を受信したときにスタックトレースを標準エラー出力に出力してプロセスを終了する。簡単な WSGI アプリケーションを書いて、環境変数から faulthandler を設定してみた。
#!/usr/bin/env python3.4 from werkzeug import run_simple def app(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain; charset=utf-8')]) yield b'Hello World!\n' run_simple('127.0.0.1', 5000, app)
# exec docker@3b813e88ee88:~$ python3 -V Python 3.4.2 docker@3b813e88ee88:~$ PYTHONFAULTHANDLER=1 python3 ./wsgi.py & [1] 39 docker@3b813e88ee88:~$ * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) docker@3b813e88ee88:~$ curl 127.0.0.1:5000 127.0.0.1 - - [05/Jul/2016 07:28:39] "GET / HTTP/1.1" 200 - Hello World! # send SIGSEGV docker@3b813e88ee88:~$ kill -s SEGV $(pidof python3) Fatal Python error: Segmentation fault Current thread 0x00007f7b6d866700 (most recent call first): File "/usr/lib/python3.4/socketserver.py", line 154docker@d733ec43cf93:~$ in _eintr_retry File "/usr/lib/python3.4/socketserver.py", line 236 in serve_forever File "/home/docker/.local/lib/python3.4/site-packages/werkzeug/serving.py", line 499 in serve_forever File "/home/docker/.local/lib/python3.4/site-packages/werkzeug/serving.py", line 659 in inner File "/home/docker/.local/lib/python3.4/site-packages/werkzeug/serving.py", line 694 in run_simple File "./wsgi.py", line 9 in <module> [1]+ Segmentation fault PYTHONFAULTHANDLER=1 python3 ./wsgi.py
あとは uwsgi を使ってアプリケーションを動かして確認するのだけど気をつけないといけないところがあって、 py-call-osafterfork [2] を有効にしないと uwsgi worker にシグナルを送信してもアプリケーションに伝達されないので注意。
# exec # 今回 uwsgi は emperor モードで実行 # env: PYTHONFAULTHANDLER=1 を vassal の設定ファイルで指定 root@fb755eb211cb:/# uwsgi -y /etc/uwsgi/emperor.yaml # send SIGSEGV root@fb755eb211cb:/# ps auxf USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 127 0.0 0.0 20256 3284 ? Ss 08:38 0:00 /bin/bash root 148 0.0 0.0 17496 2144 ? R+ 08:39 0:00 \_ ps auxf root 10 0.0 0.0 20256 3264 ? Ss 08:31 0:00 /bin/bash root 144 0.0 0.0 48236 4664 ? S+ 08:39 0:00 \_ uwsgi -y /etc/uwsgi/emperor.yaml root 145 0.9 0.1 66480 11172 ? S+ 08:39 0:00 \_ uWSGI master docker 146 0.0 0.1 68692 10476 ? S+ 08:39 0:00 \_ uWSGI worker 1 docker 147 0.0 0.1 68692 10476 ? S+ 08:39 0:00 \_ uWSGI worker 2 docker 1 0.0 0.0 20252 3224 ? Ss+ 08:31 0:00 /bin/bash # worker(cheaper) に SIGSEGV を送る docker@fb755eb211cb:~$ kill -s SEGV 146 Fatal Python error: Segmentation fault Current thread 0x00007fa047204780 (most recent call first): !!! uWSGI process 146 got Segmentation Fault !!! *** backtrace of 146 *** uWSGI worker 1(uwsgi_backtrace+0x30) [0x4635f0] uWSGI worker 1(uwsgi_segfault+0x21) [0x4639b1] /lib/x86_64-linux-gnu/libpthread.so.0(+0xf8d0) [0x7fa046de58d0] /lib/x86_64-linux-gnu/libpthread.so.0(raise+0x2b) [0x7fa046de579b] /lib/x86_64-linux-gnu/libpthread.so.0(+0xf8d0) [0x7fa046de58d0] /lib/x86_64-linux-gnu/libc.so.6(epoll_wait+0x13) [0x7fa044f8ae33] uWSGI worker 1(event_queue_wait+0x33) [0x456ed3] uWSGI worker 1(wsgi_req_accept+0xd2) [0x4175f2] uWSGI worker 1(simple_loop_run+0xb6) [0x45f7a6] uWSGI worker 1(uwsgi_ignition+0x208) [0x463c78] uWSGI worker 1(uwsgi_worker_run+0x25d) [0x466fbd] uWSGI worker 1(uwsgi_run+0x3ae) [0x4674de] uWSGI worker 1(_start+0) [0x41698e] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7fa044ec3b45] uWSGI worker 1() [0x4169b7] *** end of backtrace *** DAMN ! worker 1 (pid: 146) died, killed by signal 11 :( trying respawn ... Respawned uWSGI worker 1 (new pid: 150) ...
同様にスタックトレースの出力が確認できた。標準エラー出力に出力されるため、実際の動作では damontools の multilog や systemd のサービスとして動作させることが必要。
これで、知らないコードを追いかけずにすんで便利。2 台以上の worker でトラブル起こしていたら、1 台試してみるのいいかも。出力できる文字数が 500 文字までとか制限はあるけれども、インフラ担当者が環境変数指定すればいいだけなので、設定しておいて間違いはなさそう。
使った設定とかは下記のリポジトリに置いておく (気まぐれで消すかも)。
参照
[1]: http://docs.python.jp/3/library/faulthandler.html
[2]: https://uwsgi-docs.readthedocs.io/en/latest/Options.html#py-call-osafterfork