CA Reward

Tech Blog

Tech Blogトップに戻る

Go言語のLogger「zap」は何故高速に構造化されたログを出力する事が出来るのか

2016.06.15

  • このエントリーをはてなブックマークに追加
  • Pocket
daich
daichエンジニア

こんにちは。開発部の平田です。

今回は、PHP製のWeb APIをGoに移植するプロジェクトでアプリケーションの情報やエラーを出力する為のLoggerを検討した際に、uber-go/zapというライブラリが公表しているパフォーマンスがその他ライブラリと比べて大分良かったので、どこでパフォーマンスの差を出しているのか、そのアプローチを簡単に紹介したいと思います。

Zap

初めに、簡単にzapの紹介をしておくと今年の2月にUberから公開されたまだ比較的新しいプロダクトです。その為開発ステータスはBetaの段階で出力もJSONしか対応していませんが、Github上で800以上のスターが付いており注目されているプロジェクトとなっています。

「Fast, structured, leveled logging in Go」とあるように、構造化されたログを出力するためのライブラリで、標準のlogのようなprintfスタイルの出力は提供していません。代わりにkeyとvalueに対応する構造体を渡します。この辺はlogrusなどでも同じ様な感じだったりしますね。

簡単な使い方を抜粋します。

可能な限りリフレクションと文字列のフォーマットを行わないという方針で設計されているので、fieldを指定する際には1つ1つ zap.String("string", "1") のようなヘルパーメソッド経由で渡す必要があります。この辺の手間はパフォーマンスとのトレードオフという所でしょうか。

その他のデータ型としては以下が用意されています。

パフォーマンス

それでは、実際にどのようにしてパフォーマンスを出しているかを見ていきしょう。

zapが公表しているベンチマークでは、README.mdにあるように、その他のライブラリと比べてかなり高速に動作しているようです。ベンチマークのコードはbenchmarks以下にあります。

その他のライブラリとのパフォーマンス面での最大の違いとして、「reflection-free, zero-allocation JSON encoder」と公式に謳っています。reflection-freeに関しては上の例でもあるように、渡すデータを構造体でラップして型情報をLoggerに渡すことでinterface{}でデータを受けることを回避しています。ではzero-allocationの部分に関してはどの様に実現しているのでしょう。

答えから先に言ってしまうと、JSONへのEncoderとBufferをFreeListで管理することで、ランタイムのアロケーションとGCを極力行わないような戦略となっています。FreeListの実装にはsync.Poolが使われています。

最初の例を元に流れを確認してみましょう。

zap.NewJSON()でLoggerを生成した際に、sync.Poolから取得したEncoderをencにセットします。

ちょっと話が逸れてしまいますが、newJSONEncoderで取得できるEncoderは基本的に再利用される前提なのでenc.bytesには以前にencodeしたデータが残っています。その為、再利用する際にはenc.bytesを初期化する必要があります。ここではもう一度[]byteをアロケーティングしたりゼロクリアなどは行わずにfiltered sliceを用いて初期化しています。この辺のテクニックはgo/wikiにもあるので、まだ見ていない人は一度目を通しておくと良いかもしれません。

引き続き、書き込みの部分を見ていきます。

jsonLogger.logの中で、最初に確保したEncoderをCloneしてもう一つのEncoderを確保しています。これはjl.encがWithで添付された共通のフィールドを保持している為です。AddFieldsに関しては単純に、enc.bytesに、JSONに対応する値をappendしたり文字のエスケープを行っている関数です。簡単な例だけ紹介しておくと、

このように、各データ型に対してJSON文字列をどんどん詰めていきます。

WriteMessageで最終的な出力用のJSONを構築するためのEncoderを取得し、JSONを組み立ててio.Writerに書き込みます。使用済みのEncoderはFreeでFreeListに返します。

結構処理を省いていますが、ログを出力する部分の基本的な流れはこんな所でしょうか。

まとめ

簡単に駆け足ですが、zapが以下の戦略でパフォーマンスを向上させている部分を見ました。

  • リフレクションやフォーマット文字列のパースを行わない
  • アロケートした領域を使い回すことによって、全体的なアロケーションとGCの発生頻度を減らす

これらはLoggerを作る場合だけではなく、Goで書く大抵のプログラムにも応用できる部分ですね。また、sync.Poolを活用しているわかりやすいサンプルだと思うので、最適化の一つとして、覚えておいて損は無いと思います。