fluent-plugin-bigquery の設定

fluentdとfluent-plugin-bigqueryを使ってBigQueryにデータを投入する場合の設定について。

## BigQueryの制限

2015/04/16に**制限の変更がありました**（[公式発表](http://googlecloudplatform.blogspot.jp/2015/04/take-your-big-data-to-new-places-with-Google-BigQuery.html), [日本語のまとめ情報](
http://qiita.com/hakobera/items/61a0decfe6946813b240)）。これにより、本記事の内容も影響を受けますので、新しい制限値を参考の上、適宜調整してください。


- 1行20KBまで
- 1リクエスト500行まで
- 1リクエスト1MBまで
- 1秒10,000行まで (申請により100,000まで)
- 1秒10MBまで

参照: https://cloud.google.com/bigquery/quota-policy#streaminginserts

1リクエスト1MBを10リクエストで10MB、1リクエスト500行を20リクエストで1万行。

このどちらかが1秒あたりのリクエスト数の上限となる。

例えばサーバのアクセスログであれば1レコードのサイズは小さいため、10MB制限より先に1万レコードの制限に達すると思われる。よって、秒間のリクエストが20を超えないようにしておくとよい。

## バッファの設定

- buffer_chunk_records_limit
- buffer_chunk_limit
- buffer_queue_limit

fluent-plugin-bigqueryはチャンクを1リクエストとしてBigQueryのInsert APIを呼び出す。よって、BigQuery側の制限に違反しないようにプラグイン側を適切に設定する必要がある。

### buffer_chunk_records_limit

チャンクごとの最大レコード数。

```
buffer_chunk_records_limit 500
```

BigQuery側で「1リクエスト500行まで」という制限があるので500とする。

> 500ちょうどにしておくとエラーになることがあるという情報があるので、300程度にしておくとよいかもしれない(コメント欄参照)。


### buffer_chunk_limit

チャンクごとの最大バイト数。

```
buffer_chunk_limit 1000000
```

BigQuery側で「1リクエスト1MBまで」という制限があるので1000000とする。

> 1MBちょうどにしておくとエラーになることがあるという情報があるので、768k程度にしておくとよいかもしれない(コメント欄参照)。

### buffer_queue_limit

プラグイン側でいくつのチャンクをメモリに保持しておくか設定できる。

デフォルト値は1024となっている。

小さすぎるとキューが溢れてエラーとなってしまうので、メモリ量と相談して大きめの値にしておくとよい。

1チャンクの最大サイズが1MBなので、デフォルトの1024だと最大1GBを使うことになる。ただ、`buffer_chunk_records_limit` が500と小さいので、実際には1チャンクが1MBになるわけではない。

例えばnginxなどのアクセスログで1レコードが 0.2 - 0.4KB 程度になる場合だと、1チャンクは `0.4KB * 500 = 200KB` となる。1GBをバッファに使えるのであれば5000チャンクほどにできる。


## 送信の設定

- flush_interval
- try_flush_interval
- queued_chunk_flush_interval
- num_threads

### flush_interval

バッファに溜まったレコードをflushする間隔。

デフォルト値は0.25となっている。

これは`buffer_chunk_records_limit`や`buffer_chunk_limit`に達していないバッファ中のレコードをflushする間隔となる。

高レートの場合は`buffer_chunk_records_limit`や`buffer_chunk_limit`の制限に先に達してflushされるので、この値の影響は小さい。

低レートの場合だと、これが送信の間隔になる。レコードをBigQuery側で即座にクエリしたい用途では短くする意味がある。

BigQueryは、リアルタイムにクエリというよりはバッチ処理の用途で用いられるため、1秒あるいはもっと長い値でも多くのケースで問題ない。

### try_flush_interval

送信待ちとなったチャンクを、実際に送信する間隔。
言い換えると、送信待ちチャンクが存在するかどうかを確認する間隔。

デフォルト値は0.05となっている。

`flush_interval`は「バッファに溜まったレコードを送信待ちに移す間隔」、`try_flush_interval`は「送信待ちのチャンクの有無を確認する間隔」となる。

`buffer_chunk_records_limit`や`buffer_chunk_limit`に達したチャンクは送信待ちになるが、これらのチャンクが送信されるまでに最大で`try_flush_interval`秒かかることになる。

ただ、実際にはBigQueryへのHTTP POSTのほうが時間がかかることで待たされるため、`try_flush_interval`の影響を受けないケースも多い。

例えばBigQueryで0.5秒かかるとすると、その処理が終わった時は`try_flush_interval`の0.05秒はすでに過ぎているため、即座に次の送信が始まることになる。

### queued_chunk_flush_interval

複数のチャンクが存在する場合に、あるチャンクの送信の後に次のチャンクの送信までの待ち時間。

デフォルト値は1となっている。

### num_threads

バッファを処理するスレッド数。

デフォルト値は1となっている。

BigQueryのAPIの呼び出しは、外部サービスへのHTTP POSTのため一定の時間がかかる。このため複数のスレッドで実行すると効率がよい。


## 設定例

BigQueryにレコードを送信する量より、レコードを受信する量が多いと、バッファが溢れてしまうので、これが発生しないようにする。

ピーク時で秒間1000行が送られてくる場合を考えてみる。

1秒で1000行なので、1秒で2チャンクが溜まることになる。

HTTP POSTに0.5秒かかるとして、`queued_chunk_flush_interval`がデフォルトの1のままだと、2チャンクの送信には2秒かかる。2チャンクだとHTTP POSTだけで1秒以上かかることもあるので、`queued_chunk_flush_interval`が0でもバッファ溢れの可能性は残る。

そこで`num_threads`を2以上にする。

例えば`num_threads`が2で、2チャンクがキューに溜まっている場合、1スレッドが1チャンクを処理すれば0.5秒で送信できる。これであればバッファは溢れない。

BigQuery APIが遅い場合も考慮して、大きめの値にしておく。

```
buffer_chunk_records_limit   500        # BigQuery上限
buffer_chunk_limit           1000000    # BigQuery上限
buffer_queue_limit           5000       # 1GBくらい
flush_interval               1          # 暇な時間帯は1秒おき
try_flush_interval           0.05       # チャンクが溜まったら早めに送信
num_threads                  4          # HTTP POSTが遅いので複数スレッド
queued_chunk_flush_interval  0.01       # チャンクが溜まった場合は待ち時間短めで連続送信
```

## エラーとリトライ

`buffer_chunk_records_limit`と`buffer_chunk_limit`をBigQuery側の制限を超えない値にしていても`413 Request Too Large`が発生することがある(らしい)。

また、リクエストが正当であっても、一時的に500系のエラーも発生する。

fluentdのアウトプットプラグインはリトライ機能を備えているので、エラー時は送信がリトライされる。

### insertId

リトライ時にレコードが重複してBigQueryに登録されるのを防ぐためには`insertId`を使う。BigQueryは`insertId`を少なくとも1分間は覚えていて、ベストエフォートで重複を防いでくれる。

fluent-plugin-bigqueryは`insert_id_field`で指定されたフィールドを`insertId`としてBigQueryに送信するので、レコード中にユニークな値があるのであれば、そのフィールド名を`insert_id_field`に設定する。

nginxやApacheのアクセスログだと難しいが、アプリケーションからレコードを送信する場合など自分でコントロール出来るのであれば`insertId`を入れるようにしておくと良い。