Ruby拡張を書くことによる高速化を試みた一例

Ruby拡張を書くことによって、高速化を試みたのでその顛末を書く。尚、Ruby拡張と話の本筋はあまり関係がない。

モチベーション

パケットキャプチャの結果からヘッダ情報を取り出すためのプログラムを、pcaprubライブラリを用いて今まで書いていた。

このpcaprubは、キャプチャのためのライブラリであるlibpcapのための薄いラッパーであり、具体的にはキャプチャ結果であるpcapファイルから1パケットずつを切り離した状態で取り出すことが出来るのみである。このパケット中から各プロトコルのヘッダ情報(例えばIPヘッダやTCPヘッダ)を取り出すためには、Ruby上でバイナリを解析して情報を切り分けて行く必要がある。Ruby上でバイナリからヘッダ情報を取り出すライブラリの例としてpacketfuがある。

packetfuでは十分ではなかったので、Ruby上でバイナリからヘッダ情報を取り出すライブラリを自作していた。やがて、キャプチャ時間の増大に従って、このヘッダ情報の取り出しに遅さが感じられるようになってきた。処理するたびにCPUは100%に張り付く。不満が大きくなってきた。

そこで、pcaprubライブラリの置き換えとして、直接libpcapを叩き、ヘッダ情報の解析を行うプログラムを自作した。1日半かかった。

速度が出ない

Ruby上で行なっていたバイナリからヘッダ情報を取り出すコードをCに書き換えたRuby拡張を作成した。結果としてはrubypcapというライブラリが行なっていることをそのまま行い、必要なプロトコルのための処理を追加した形になっている。

速度改善の効果を大きくするために、不必要なペイロード部分のデータをRubyクラスにコピーしないようにもした。

常にtimeコマンドを使い、速度が出ているかどうかのチェックを行うようにした。結果としては期待するよりは下回ったが、2倍程度の速度が出るようだった。

改善後
real    0m5.902s
user    0m5.692s
sys     0m0.196s

改善前
real    0m13.363s
user    0m13.041s
sys     0m0.280s

しかし、測定のタイミングによっては遅くなる時がある。おそらくdisk cacheのせいだろうとは思っていたが、気には留めていなかった。最後にbenchmark用のスクリプトを書き、disk cache無しの状態の測定を行うことにした。

disk cacheのクリアには、sysctl -w vm.drop_caches=1を用いた。

改善後
vm.drop_caches = 1
real    0m15.106s
user    0m6.768s
sys     0m0.344s

改善前
vm.drop_caches = 1
real    0m16.626s
user    0m13.497s
sys     0m0.436s

disk cacheが効いている状態でなければ速度が出ない、という結果に終わった。(なんだ、俺は1秒を縮めるために1日半も時間をかけてしまったのか。ショックだ。)

この速度が出ない状態のCPU使用率を見ても50%程度であり、wait等に時間が取られている。前述の結果のrealとuserの値を比べても分かる。

disk cacheが効いている状態では2倍早いのは救いだが、このプログラムの利用上、disk cacheが効く状態、すなわち、一度pcapを読み込んだ状態で使うことは頻度としては少ない。少ないので苦労は報われない。

圧縮・伸張を使う

さて、どうにかできないものかと考えた。

先の結果のrealとuserを比較しても半分程はディスクアクセスにかけていそうな時間である。仮にメモリ上から直接読み込んで処理できたら早くなるのではないか。

このpcapファイルの中身、特にペイロード(データ)部分はパターン化されたものであり、特に圧縮が効く。gzip圧縮した場合、1/50になる。元々pcap生成時にgzip圧縮後に伸張して、このプログラムを使用している。このことから、ディスク速度がネックになるのであれば、1/50の状態のファイルから直接メモリ上に展開して処理できないものか、と考えた。

残念なことに、libpcapライブラリのAPIを用いてpcapファイル内データのメモリ上からの読み込みを行うことは簡単にはできない。

しばらく考え、メモリ上だがファイルシステムとして見えるRAMディスクを用いることを考えた。使い勝手がよさそうだったのは、特に何もしなくてもマウントされている、/dev/shmを使うことだった。RAMディスクを使った流れを示す。

  1. shm上にgz内のファイルを展開する
    • gzip -dc file.pcap.gz > /dev/shm/file.pcap
  2. shmに展開したファイルを読み込む
    • ruby_program /dev/shm/file.pcap

shmを使った状態の、改善後と改善前の双方のプログラムを用いた結果を示す。

双方ともにshm上のファイルにアクセス

改善後
vm.drop_caches = 1
real    0m6.194s
user    0m5.580s
sys     0m0.240s

改善前
vm.drop_caches = 1
real    0m14.173s
user    0m13.185s
sys     0m0.244s

2倍程度は早くなった。改善前のプログラムでは2秒程度しか速度が早くなっていないが、改善後のプログラムでは1/2程度に早くなっている。realとuserのバランスを見ても、問題なさそうだ。

gzip伸張する時間分は必要だが、これはどちらにせよ行う作業であり、disk上に伸長するのか、ram上に伸長するのかの違いしかないため、問題はない。ギリギリで速度改善に成功した例だった。

思う所

このような速度改善をおこなう場合、ディスクアクセスがネックになると早期に気づくためには何をすればよかっただろうか、と思うに至る。少なくとも、sysctl -w vm.drop_caches=1をtimeコマンドと共に利用すべきだった。

今回はHDD上だったので速度の問題が出たが、仮にSSD上であれば、問題は出なかったかもしれない。プロファイラーを使って詳細に解析していれば、分かった問題だったのだろうか。

ともかく、俺の1日が無駄にならなくて良かった。これに尽きる。

追記

別の種類ファイルで実験した所、改善前後で大きく性能が違った。

改善後(RAMディスク未使用)
vm.drop_caches = 1
real    0m16.852s
user    0m9.185s
sys     0m0.496s

改善後(RAMディスク使用)
vm.drop_caches = 1
real    0m8.845s
user    0m8.061s
sys     0m0.240s

改善前(RAMディスク未使用)
vm.drop_caches = 1
real    0m38.786s
user    0m36.110s
sys     0m0.556s

改善前(RAMディスク使用)
vm.drop_caches = 1
real    0m37.264s
user    0m35.850s
sys     0m0.388s

38.8秒程度かかっていたのが、8.8秒で終わっている。これは早い。

2種類のファイルともプログラムでの処理を毎回行うので、改善前(RAMディスク未使用)16.6+38.7=55.3秒かかっていたのが、改善後(RAMディスク使用)6.2+8.8=15.0秒 程度で済むことになる。3,4倍は早くなっているので、これでいいか、という気持ちになる。