読者です 読者をやめる 読者になる 読者になる

おくみん公式ブログ

おくみん公式ブログ

Influent ベンチマーク - Part 2 #fluentd

f:id:okumin:20170124002659p:plain

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オブジェクトずつデシリアライズしていく必要があります。

f:id:okumin:20170402221205p:plain

msgpack-java の問題点

msgpack-java のデシリアライズ API は、基本シリアライズされたデータが完全に揃った状態で呼び出す必要があります。多分。

一応チャネル経由でデータを受け渡す API もありますが、NONBLOCK モードのコネクションを扱える実装ではありません。

このため、これまでの Influent は次の流れでリクエストを処理していました。

  1. データを受信すると、バッファにデータを追記する
  2. バッファ全体を 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%

雑感

前回取ったプロファイル結果によると、一番重いと思われるのは MessagePackEventStreamentries をデシリアライズする処理でした。その部分は今回いじった部分と関係ないので、こんなものかなという感じがしています。

やむを得ずストリームデシリアライザを自作してしまいましたが、これは本来 msgpack-java 側に備わっているべき機能だと思います。msgpack-java に爆速ストリームデシリアライザが実装されることを強く望んでいます。

次やりたいこと

性能へのインパクトが大きいのはマルチスレッド化ですが、その前にできるだけシングルスレッド性能を向上させておこうと思っています。一つ考えているのは、バッファ確保処理の改善です。

負荷テスト中、それなりの数の GC が発生していました。ヒープダンプを見ると ByteBuffer が大量にヒープを使用しているようです。バッファの確保はかなり雑な実装になっているため、並列化を試す前にそのあたりの見直しをしたいと思っています。

関連リンク