秒間500件以上のLOGフローアーキテクチャ

今年の9月、社内でLOGのアーキテクチャの改善を計るチームに参加する事になりました。
その際に経験させていただいた、知見をここでまとめてみようと思います。

# 背景
チームが作られた経緯としては、社内のLOGアーキテクチャを利用している際、問題が発生したためです。
問題発生した際の簡単なLOGアーキテクチャ図です。

![AWS Design (1).png](https://qiita-image-store.s3.amazonaws.com/0/98305/1dd76775-9c36-d5a9-c660-a35302f5688f.png)


* backendのmongodbがLOGの流量に耐えきれず落ちた。
* log dispatcherまで派生し、他のbackend_serviceへのLOG流入にまで影響を及ぼした。
* 各種log dispatcherサーバの設定時、backend毎に流量の微調整を行わなくては行けないので障害発生時やサーバ構築時の対応にてインフラチームの人員コストが増大していた。

# 今回の目標
上記問題をふまえて、今回のLOGアーキテクチャ検討チームの目標は

* 秒間500件以上を各backendまで流し込むアーキテクチャ
    * 流量に応じて各種サービスのスケールアウトを行えば、数倍以上の速度を実現。
* 各backendサービス毎に影響範囲を分けたアーキテクチャ
    * 一つのbackendが落ちても他のbackendへのフローに可能な限り影響を及ぼさない。
* インフラ側のコスト削減
    * インフラチームの人員コスト（log dispatcherサーバ構築やtd-agentへの流量調整等の煩雑な作業）を削減。


# LOGアーキテクチャ案 - 1st　サーバレスアーキテクチャ -
上記の問題を解決するために検討された、LOGアーキテクチャ案がこちらです。

## アーキテクチャ図
![AWS Design.png](https://qiita-image-store.s3.amazonaws.com/0/98305/aafdd751-665f-6370-30b2-03207eec5574.png)


## アーキテクチャ概要
* 前提条件
    * LOG １recordの平均容量は1KB。
    * 検証対象となる秒間件数は「総秒間処理レコード数[^1]」。
    * 検証範囲としてはkinesis〜backendサービスの間つまりlambdaが担当するフロー部分全てを範囲とする。
    * kinesisの流入性能は目標秒間件数以上の性能を満たしているため検証範囲から外す。
* LOGについて
    * 流入されるLOGはplayerの行動ログ。
    * LOG内にはplayerのidが入っている事を前提とする。
        * また、そのplayer_idは32文字の16進数文字列とする。
* app serverについて
    * EC2インスタンスのm4.2xlargeを1つ利用しtd-agentサーバからkinesisへLOGを流し込む。
    * 「td-agent fluent-plugin-kinesis」のKPLを利用してkinesisに流し込む。
* kinesisについて
    * Kinesis Streamsは1シャードに秒間1000recordもしくは秒間1MBまで受け付けられる。
    * kinesis Streamsのshard数は2つで検証を行う。
        * ただ、計測結果の内容は1lambda containerでの処理内容を計測する。
* lambdaについて
    * 使用言語はpythonを使用。
    * lambdaの使用メモリは256MB。
    * kinesis streamsよりrecordsを取得するblueprint「kinesis-process-record-python」を利用して実装。
        * kinesisのシーケンス番号を独自に管理してくれる。
        * lambda内でエラーが発生した場合シーケンス番号を進めずリトライしてくれる機能も備わっている。
        * shard数を増やすとlambdaもshard数にあわせてlambda containerをスケールして起動する。
* フロー解説
    1. app serverのtd-agentからkinesisへLOGを流し込む。
    2. lambdaが定期的にKinesisよりrecordを取得して各backendへ流し込む。
        * S3には取得したrecordをgzで圧縮して1起動lambda毎に1ファイル書き出す。
        * Bigqueryにはplayer_id毎のtable作成しplayerのLOGを流し込む。
            * player_id毎のテーブルには検索容量を削減し料金を低減させる目的がある。
            * 弊社各アプリのplayer情報全てを1テーブルに格納して検索するとコスト面がかかりすぎてしまうため分割する。
            * Bigqueryのdate-partitionedを利用して、teble内では日付毎にpartitionされている。
            * テーブルがplayer数だけ生成されるため、GCP社様に確認を行い「問題ない」旨の返答を頂いた。
* 撤去したbackend
    * mongoDBでは、コストに見合ったインスタンスでは流量に耐えられないため撤去。



## 計測結果
実行lambdaのメモリ量は全て256MBです。

| backend形式 |最大メモリ|総平均時間[^1]|平均時間[^2]|平均間隔時間[^3]|平均レコード数[^4]|秒間処理レコード数[^5]|総秒間処理レコード数[^6]|エラー発生率(%)[^7]|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|Bigquery|119MB|868.86秒|72.22秒|32.79秒|38件|3.10件/秒|0.04件/秒|85.7%|
|S3|156MB|6.35秒|6.08秒|0.46秒|4389件|721.24件/秒| 691.11件/秒|0%|


## 達成内容
* S3への流入について1lambda（256MB）で約秒間500件以上を達成。

## 問題点
* Bigqueryへ投げるリクエストに時間がかかっている。
* Bigqueryへのcreate_tableが正常に終了した後数分間はinsertを行っても「Not found Table」のエラーが発生。

## 問題点の考察
> Bigqueryへ投げるリクエストに時間がかかっている。

1lambda内で複数リクエストを投げているが、その部分は直列で処理を実行しており、並列で処理を実行する事でスループットをあげられる可能性があります。

> Bigqueryへのcreate_tableが正常に終了した後数分間はinsertを行っても「Not found Table」のエラーが発生。

「create tableの性能があまり良くない」 or 「table生成にラグが発生する仕様」だと思われるため、tableを無制限に作成する方式では限界があると考えられます。テーブルの作成数を最適化する方法でこの問題に対処します。


# LOGアーキテクチャ案 - 2nd　gevent並列処理 -
「1st案」の問題点を元に続いての案を実行してみました。

## アーキテクチャ図
![Untitled.png](https://qiita-image-store.s3.amazonaws.com/0/98305/2ed2f14e-d415-8188-c469-a885654d9815.png)

## アーキテクチャ概要
* geventを使ってBigqueryへのリクエストを並列処理。
    * 並列処理でリクエストを投げる事でlambda containerのパフォーマンスを最大限利用。
* テーブルの仕様を変更。
    * player_id毎のテーブルでは無く、player_idを元に16分割したテーブルへデータを流し込む。

## 計測結果
実行lambdaのメモリ量は全て256MBです。

| backend形式 |最大メモリ|総平均時間[^1]|平均時間[^2]|平均間隔時間[^3]|平均レコード数[^4]|秒間処理レコード数[^5]|総秒間処理レコード数[^6]|エラー発生率(%)[^7]|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|Bigquery|256MB|24.63秒|20.69秒|3.23秒|4078件|197.09件/秒|165.56件/秒|3.8%|

## 達成内容
* 1stよりも高速化を実現。

## 問題点
* 秒間500件以上は未達成。

## 問題点の考察
> 秒間500件以上は未達成。

256MBのlambdaでpythonを利用し高速化を計る場合は、これが限界だと考えられます。
ただ、lambda container上で実行出来る実行ファイルもしくは共有ライブラリを利用する事で高速化を実現出来るかもしれません。

# LOGアーキテクチャ案 - 3rd Golang共有ライブラリ -

## アーキテクチャ図
![Untitled.png](https://qiita-image-store.s3.amazonaws.com/0/98305/40163c09-7e19-6288-83bc-393285b3ac8d.png)

## アーキテクチャ概要
* golangを利用してbigqueryへrequestを投げる共有ライブラリを作成し利用。
    * インタプリタ言語とは異なるコンパイル言語であるgolangで作成した共有ライブラリを利用。
    * goroutineを利用して並列処理を行う。

## 計測結果
実行lambdaのメモリ量は全て256MBです。

| backend形式 |最大メモリ|総平均時間[^1]|平均時間[^2]|平均間隔時間[^3]|平均レコード数[^4]|秒間処理レコード数[^5]|総秒間処理レコード数[^6]|エラー発生率(%)[^7]|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|Bigquery|158MB|11.11秒|10.50秒|0.57秒|4711件|448.25件/秒|424.01件/秒|0.3%|

256MBでは秒間500件に後少しの所で達成出来ませんでした。

ただ、kinesis shard数を2本から１本へ変更すると

| backend形式 |最大メモリ|総平均時間[^1]|平均時間[^2]|平均間隔時間[^3]|平均レコード数[^4]|秒間処理レコード数[^5]|総秒間処理レコード数[^6]|エラー発生率(%)[^7]|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|Bigquery|146MB|7.62秒|6.86秒|0.86秒|4640件|676.11件/秒|608.63件/秒|1.3%|

目標秒間件数を満たす事が出来ました。
なぜこのような事が発生するのかは、別で記事にしたいと思います。

## 達成結果
* 共有ライブラリを利用する事で高速化を実現し、限定的な条件ではあるモノの目標秒間件数を達成。
* サーバレスアーキテクチャのため、インフラチームの運用コストも削減を実現した。


# あとがき
この構成に関しては下記資料を参考にさせて頂きましたが、AWS以外のbackendサービスへの流入についての記事は見当たらなかったため、検証し記事にさせて頂きました。

今回の記事関連で聞いてみたい事がありましたら、何なりとコメント頂ければわかる範囲でお答えします。それでは、この記事を最後まで読んで頂きありがとうございました。

# 参考資料
https://speakerdeck.com/kanny/miao-jian-shu-mo-falseroguwoiigan-zinisuruakitekutiya

# 注釈
[^1]: (lambda総時間 + error総時間　+ lambda実行間隔総時間) / lambda成功処理回数
[^2]: 1起動lambdaあたり処理する平均処理時間
[^3]: lambdaとlambdaの間の実行間隔の平均時間
[^4]: 1起動lambdaあたり処理する平均record数
[^5]: 平均レコード数 / lambda平均時間
[^6]: 平均レコード数 / 総平均時間
[^7]: エラー終了lambda回数 / 総実行lambda回数