Java 版 Fluentd サーバである、Influent のベンチマーク第二弾です。
MessagePack デシリアライズ処理を改善したので、その効果を測定します。
テスト概要
http://blog.okumin.com/entry/2017/01/24/002953#テスト概要
Disclaimer
http://blog.okumin.com/entry/2017/01/24/002953#Disclaimer
テスト環境
インスタンス
送信側、受信側共に Google Compute Engine asia-northeast1 リージョンの n1-highcpu-32 を用いました。
送信側は二台用意し、それぞれ20プロセス、合計40プロセスの Fluentd が並列にイベントを送信します。
バージョン
環境構築は influent-benchmark の a4565f882c5964bdb1a5370112890605bbb3bf3b で行いました。
Fluentd のバージョンは 0.12.31、Influent は前回のベンチマークに使用したものと、MessagePack デシリアライズ処理を最適化したものの2バージョンを用意しました。
プロトコル
Fluentd 0.12 を用いて、at-most-once(require_ack_response なし)プロトコルでイベントを送信しました。
改善ポイント
これまで msgpack-java に委譲していた MessagePack デシリアライズ処理の一部を自前で実装し、性能向上を試みました。
msgpack-java はかなりチューニングされており、普通に使う分には高速です。ただしネットワークプログラミングと組み合わせると、オーバーヘッドが避けられない API となっています。
Fluentd Forward Protocol の wire format
Forward Protocol Specification v1 · fluent/fluentd Wiki · GitHub に書かれているように、Fluentd の forward プラグインは通常 MessagePack を用いてデータをやり取りします。シリアライズされた MessagePack をそのまま wire format として使用しているということです。
またプロトコル上、単一のコネクションを用いて複数のイベントを送信してよいことになっています。そのため、送信されるバイナリは、シリアライズされた MessagePack を敷き詰めたものとなります。各 MessagePack オブジェクトの境界は事前にわからないため、バイナリの頭から1オブジェクトずつデシリアライズしていく必要があります。
msgpack-java の問題点
msgpack-java のデシリアライズ API は、基本シリアライズされたデータが完全に揃った状態で呼び出す必要があります。多分。
一応チャネル経由でデータを受け渡す API もありますが、NONBLOCK モードのコネクションを扱える実装ではありません。
このため、これまでの Influent は次の流れでリクエストを処理していました。
- データを受信すると、バッファにデータを追記する
- バッファ全体を msgpack-java に渡し、デシリアライズしてみる
- バッファにまだ完全な MessagePack オブジェクトが揃っていない場合、「1」に戻る
- MessagePack オブジェクトがデシリアライズできた場合、バッファを消化し、ユーザーが記述したコールバック処理を実行する
「バッファにまだ完全な MessagePack オブジェクトが揃っていない場合」、次回データ受信時にバッファの頭から再度デシリアライズを試みる必要があります。
Fluentd はイベントをバッファリングしながら送信するため、一つ一つの MessagePack オブジェクトは必然的に大きくなります。より大きなデータはより多くのパケットに分割されるため、デシリアライズに余分なコストを払う必要がありました。
Influent で行った最適化
MessagePack は頭から順にデシリアライズできるフォーマットであるため、すべてのデータが揃わなくても少しずつパーズしていくことができます。
Ruby や C++ であればそれを実現するためのストリームデシリアライザが提供されているようですが、msgpack-java にはなぜかそのような機能が見当たりませんでした。なので、ストリームデシリアライザを自前で実装しました。
テスト結果
自作ストリームデシリアライザを導入することで、10~20%程度のスループット向上を観測できました。
Fluentd | Influent(最適化無) | Influent(最適化有) | |||||
---|---|---|---|---|---|---|---|
送信イベント数(秒) | 受信イベント数(秒) | CPU (%) | 受信イベント数(秒) | CPU (%) | 受信イベント数(秒) | CPU (%) | 備考 |
400000 | 400000 | 40% | 400000 | 30~45% | 400000 | 25~45% | |
600000 | 600000 | 60% | 600000 | 45~55% | 600000 | 30~45% | |
800000 | 800000 | 80~85% | 800000 | 60~70% | 800000 | 40~50% | |
1000000 | 900000~1000000 | 100% | 1000000 | 70~85% | 1000000 | 50~65% | |
1200000 | 900000~1000000 | 100% | 1200000 | 85~105% | 1200000 | 60~75% | |
1400000 | N/A | N/A | 1400000 | 115~130% | 1400000 | 75~90% | |
1600000 | N/A | N/A | 1600000 | 130~155% | 1600000 | 90~105% | |
1800000 | N/A | N/A | 1800000 | 120~170% | 1800000 | 95~120% | |
2000000 | N/A | N/A | 2000000 | 120~145% | 2000000 | 110~130% | Influent(最適化無)のヒープリサイズ(456.0M->1824.0M) |
2200000 | N/A | N/A | 2000000 | 120~125% | 2200000 | 115~130% | |
2400000 | N/A | N/A | N/A | N/A | 2300000~2400000 | 125~175% | Influent(最適化有)のヒープリサイズ(456.0M->1824.0M) |
2600000 | N/A | N/A | N/A | N/A | 2300000~2400000 | 125~130% |
雑感
前回取ったプロファイル結果によると、一番重いと思われるのは MessagePackEventStream
の entries
をデシリアライズする処理でした。その部分は今回いじった部分と関係ないので、こんなものかなという感じがしています。
やむを得ずストリームデシリアライザを自作してしまいましたが、これは本来 msgpack-java 側に備わっているべき機能だと思います。msgpack-java に爆速ストリームデシリアライザが実装されることを強く望んでいます。
次やりたいこと
性能へのインパクトが大きいのはマルチスレッド化ですが、その前にできるだけシングルスレッド性能を向上させておこうと思っています。一つ考えているのは、バッファ確保処理の改善です。
負荷テスト中、それなりの数の GC が発生していました。ヒープダンプを見ると ByteBuffer が大量にヒープを使用しているようです。バッファの確保はかなり雑な実装になっているため、並列化を試す前にそのあたりの見直しをしたいと思っています。