Opt Technologies Magazine

オプトテクノロジーズ 公式Webマガジン

オプトのログ収集基盤のお金とアーキテクチャの歴史

f:id:shogo807:20190617121530p:plain

この記事では、オプトのログ収集基盤の日々増えていくリクエスト数と、膨れ上がっていくインフラ費用に対し、アーキテクチャがどのように変遷していき、どのような苦労があったのかを簡単に紹介していきたいと思います。

あいさつ

オプトのデータ活用支援ツール Spin App のリードエンジニアの鈴木省吾です(@munaita_)。 Spin Appはクライアント様の広告データを中心に、サービスの価値向上に関わるあらゆるデータを収集、分析、活用するための総合ツールです。この記事では、SpinAppのログ収集基盤のお金とアーキテクチャの歴史についてお話したいと思います。

日々増えるリクエスト数とインフラ費用との戦い

現在、Spin App では、月間30億レコード以上のログを集め、ピーク時のリクエストは4,000qpsに達します。オプトでは、このデータを、多くのお客様へのレポート作成や、マーケティング施策への活用に日々使っています。現在は、非常に安いコストで、多くのリクエストをさばける安定したログ収集基盤として稼働していますが、安定化までは様々な苦労がありました。

このログ収集基盤は2017年前半に一度大幅なリプレイスを行っています。当時、AWSで構築されていたログ収集基盤は、当初の想定を大幅に上回るリクエストとデータ量となり、集計遅延とインフラコストの膨大に悩まされていました。我々はBigQueryを中心とした、GCPのログ収集システムに全面刷新し、より多くのビジネス要求に答えられるようできるようにしました。さらに、この刷新によりインフラ費用は1/8に大幅に削減され、集計スピードも格段に向上しました。

参考資料: Gcpでprivate dmpを作ってみた

しかし、その後もクライアントは増え続け、リクエスト数とインフラ費用は膨らんでいきました。

f:id:shogo807:20190614200529p:plain
リクエスト数とインフラ費用の推移

このグラフは、月ごとのリクエスト数とインフラ費用の推移を表しています。リクエストの増加に対し、インフラ費用が右肩上がり一辺倒ではなく、上がったり下がったりガクガクしているのがわかると思います。インフラ費用の上下動の裏には、アーキテクチャの改善や、その運用のための様々なトライアンドエラーがありました。この記事では、我々がどのような施策を行い、どう乗り切ってきたのかの一部を簡単に紹介していきたいと思います。

バッチシステム期(2017/7 ~ 2018/6)

まずは、GCPの構成にした初期の状態です。この時期は、リクエストは増加していましたが、インフラ費用が大きく増加することもなく、1年近く安定して運用できてた時期でもありました。

f:id:shogo807:20190614142104p:plain
バッチシステムのアーキテクチャ

飛んできたリクエストをApp Engineで受け取り、タスクキューにInsert。その後ろのApp Engineで順次タスクキューにはいったメッセージを取り出しGCSにアップロード。GCSのファイルをDataflowのバッチ方式で加工してBigQueryにInsertするというものです。Dataflowがバッチ方式を採用していたので、バッチシステム期と名付けました。

f:id:shogo807:20190614200923p:plain
リクエスト数とインフラ費用の推移(バッチシステム)

費用のほとんどはApp EngineとBigQueryのInsert費用だったことがわかります。

この時期の運用は、タスクキューやGCSやDataflowなどが不具合を起こしたときに整合性を維持することに結構苦労していました。

例えば、タスクキューから GCS へのアップロードでよくエラーが返っていました。GCSへのアップロードは成功しているにもかかわらず、ステータスコードがエラーを返すことなどがあり、どのタスクキューメッセージがGCSにアップロードされているかどうかを監視するスクリプトを書いて対応する必要がありました。

あとは、Dataflowのバッチも実行失敗することがありました。処理が行われたGCSのファイルのみをアーカイブし、失敗した場合は再実行対象としたかったので、アーカイブロジックに、Dataflowのジョブの成否を噛ませて対応するなどしていました。

また、保存したBigQuery側でログが重複されて保存されてしまう事象がかなり起こっていました。こちらも定期的に重複をチェックし、取り除くようなスケジュールジョブを走らせていました。

さらに、このアーキテクチャだとリクエストがきてからBigQueryにはいるまでに30分 ~ 1時間程度かかっていてリアルタイム性にかけるものでした。一部の分析要求でリアルタイムに見たいというものも挙がってきていました。

このあたりのメンテナンスコストがこの構成では結構面倒だったなと今では思います。これが、後述するアーキテクチャにしてからは、このあたりの面倒な運用からは大幅に開放されていきます。

リクエスト急騰とバッチシステムの限界(2018/7)

f:id:shogo807:20190614200608p:plain
リクエスト数とインフラ費用の推移

2018/7にリクエスト数が急増し、それを上回る勢いでインフラ費用が跳ね上がっています。 大型のお客様の加入があり、想定していた以上の大量のリクエストが送られ、ログ収集システムが詰まるという障害が発生しました。それまでは、リクエストのピークは1,000 ~ 1,500qps程度だったのですが、このときは3,000qpsくらい継続的に飛んできているような状態でした。

ボトルネックになっていたのは、タスクキューからApp Engine が GCSにメッセージをアップロードする処理です。このApp Engineインスタンスをいくら増強してもInsertのペースに間に合わないような状態でした。ビジネス側で連携して、不要なイベントの送信を止めるような設定をいれてもらい、なんとか詰まりは解決できましたが、3,000qpsでこのログ収集システムは限界を迎えるという悲しい現実を知りました。費用が急騰しているのは、アップロード用のApp Engineインスタンスを急増させたことが原因です。

この障害をふまえ、ベース5,000qps、ピーク15,000qpsはさばけるようにするため、システム改修をすることにしました。 もともと構想していた、Cloud Pub/Subを使ったストリーミング方式のアーキテクチャへの移行をこのとき本格的に検討しはじめました。

ストリーミングへの刷新には開発期間がそれなりに必要でした。まずは、ボトルネックのタスクキューからの吐き出しのパフォーマンスを上げるために、1つだったタスクキューを複数にする対応をすることにしました。

バッチマルチタスクキュー期(2018/8 ~ 2018/10)

f:id:shogo807:20190614142141p:plain
バッチマルチタスクキューのアーキテクチャ

シンプルにタスクキューの本数をふやし、ラウンドロビン方式でリクエストをInsertしていくように変えただけです。

f:id:shogo807:20190614201542p:plain
リクエスト数とインフラ費用の推移(マルチタスクキュー)

タスクキューの本数を増やすことによってある程度のタスクキューからの吐き出しパフォーマンスは上がりました。インフラ費用も元の状態に戻っていることがわかります。

一方、負荷試験を通して、5,000qps程度で、タスクキューからの吐き出し速度は限界に達し、それ以上のリクエストでは追いつかなくなるということもわかっていました。GCPサポートにも問い合わせましたが、タスクキューは一定のトラフィックに以上なるとパフォーマンスが不安定になるという回答をいただきました。ストリーミング方式に移行しなきゃいけないなと思いました。

PubSubのストリーミング前期(2018/11 ~ 2019/1)

f:id:shogo807:20190614142211p:plain

PubSubを取り入れたストリーミングにリプレイスしたことにより、構成はかなりシンプルになりました。 目標としていたベース5,000qpsも、ピーク15,000qpsも余裕でさばけるようになりました。

移行時の苦労した話はこちらに詳しく書きました。

qiita.com

f:id:shogo807:20190614202145p:plain
リクエスト数とインフラ費用の推移(ストリーミング前期)

アーキテクチャを変え、パフォーマンスは良くなった一方、インフラ費用が増加してしまっていることがわかります。 ストリーミング方式にした影響で、PubSubの費用が新たに積まれ、Dataflowの費用も上がっています。 問題は、App Engineの費用が大きく増加してしまっていることです。当初の想定では、タスクキューをアップロードするためのApp Engineクラスタがまるごとなくなったので、App Engineのコストは大幅に下がっているはずでした。

この原因は、リクエストの数に対し、フロントのApp Engineの起動台数が異常に多いことでした。そして、多くのインスタンスがリクエストをほとんど処理していないのに起動し続けていました。

これは実装上の問題だったと思われます。 App Engineには、fetchURLというAPIの呼び出し回数には上限があります。PubSubにPublishするためにこのAPIをコールする必要がありました。この上限に達しないようにするため、App Engineインスタンス上のキャッシュにログを貯めて、バルクでPublishを行うという処理を実装していました。さらに、App Engineがキャッシュに貯めたままインスタンスが落ちてログを消失してしまわないように、App Engineに監視スレッドを立てるような実装もおこなっていました。この監視スレッドが悪さをして、無駄にインスタンスが起動しつづけていた可能性が高いんじゃないかと思っています。

PubSubのストリーミング後期(2019/1 ~ 現在)

2018/10月にApp Engine/Go1.11バージョンのベータ版のリリースがありました。App Engineの料金を下げる目的でバージョンを上げました。

f:id:shogo807:20190614202656p:plain
リクエスト数とインフラ費用の推移(ストリーミング後期)

App Engineの金額が大幅にさがったことがわかります。

このバージョンアップによりfetchURLによる通信回数制限が撤廃され、PubSubへのPublish回数の制限もなくなりました。 ログをバルクでPublishするための処理が必要なくなったので、監視スレッドなどの疑わしい実装を削除でき、使われていないインスタンスが適切に停止されるようになったと思われます。

このアーキテクチャにしたことで運用もかなり楽になりました。バッチシステムでは、マネージドサービス同士のつなぎ目を自作のプログラミングでつなぎこんでいたので、そこのメンテナンスをする必要がありました。このアーキテクチャでは、 PubSub -> Dataflow -> BigQuery はほぼマネージドの状態でつなぎこまれるので、PubSubからBigQueryまでの整合性の管理も気にせずにすみます。結果、リクエストログのロストもほぼなく、BigQueryへの重複保存も減り、安定性も上がりました。

さらに、ストリーミング方式ではほぼリアルタイムにリクエストされたものがBigQueryに保存されるので、ビジネスでの実現できることも広がりました。

まとめ

ログ収集システムのリクエスト数とインフラ費用の増加に対して行ってきた施策と、苦労したことについて簡単に紹介してきました。 特に、アーキテクチャをバッチ方式のものからPubSubを使ったストリーミング方式の構成に刷新したことにより、大きなメリットを得られました。

  • ログ収集のパフォーマンスが飛躍的に向上した
  • 運用が楽になった
  • BigQueryへの重複保存が減った
  • リアルタイム性をもった分析が可能になった

一方、バッチ方式のときよりも多少コストが高くなるというデメリットもありました。 今からログ収集基盤つくるのであれば、ストリーミング方式で作ることををおすすめします。

最後に

株式会社オプトでは、大規模データを使ったプロダクト開発に挑戦できる環境が多くあります。 エンジニアを絶賛大募集中ですので、もし興味をお持ちいただけましたら、こちらよりお声がけください。