広告技術部のUT(@mocyuto)です。
最近またポケモンGOをちょっとやり始めてしまいました。
今回はオンラインの広告サービスをSpotInstanceを利用したECSで構築し、2ヶ月ほど運用した話を紹介したいと思います。
はじめに
今まで広告の部署では、コンテナの本番運用はバッチのみでした。
ECS上でdigdagを運用しているものを以前紹介しましたが、オンラインでの大きなトラフィックが流入するものをECSに構築するのは初めてです。
今回、新しいサービスを作成するタイミングだったのでECS*1上にサービスを構築することにしました。
アーキテクチャ設計
今回は単一のECS Clusterに管理画面のサービスと配信系のサービスを同居させ、コスト効率をあげるようにしました。 管理画面は単純なので割愛しますが、配信側の構成は以下になります。
広告配信
広告配信におけるサービス間の通信はgRPCで行っています。
通常のRESTのロードバランシングであればサービス間にELBを挟めばいいのですが、gRPCではできません。 gRPCは弊社のサービス間通信ではよく使われていますが、ELBではgRPCのロードバランシングができないので、既存サービスはそこに困っている部分もありました。
そこで、今回はコンテナのサイドカーにEnvoyを乗せることで通信をロードバランシングさせるようにしました。
サイドカーパターンに関しては、下記が詳しいので読むことをオススメします。
サイドカー パターン - Azure Architecture Center | Microsoft Learn
Envoyを利用するメリットとしては、以下があげられます。
- gRPCのロードバランシングができる
- アプリケーション作成時は全てlocalhostとの通信を考えるだけでよい
- 簡単にトラフィックのミラーリング、シフティングができる
また最近ではCNCFのGuraduated Projectにもなったので、デファクトとなっていく可能性が高いですね。
Graduated and Incubating Projects | Cloud Native Computing Foundation
コンテナの外部との通信は、配信側だけでなく管理画面もEnvoyを経由させています。
ログ設計
広告システムのため、ログの欠損はそのまま収益につながるので、欠損はなるべく減らしたいと考えています。 ただ、ECSなどのコンテナの場合、Stateを持つこと自体が大変です。
今回、ログをコンテナから送るにあたり、直接Kinesis、S3、cloudwatch logsの検討を行いました。
結論から言うと、fluentdから直接S3にアップロードを選択しました。
Kinesisやcloudwatch logsの場合、ネットワーク障害が起こるとそのままログが消失します。 また、cloudwatch logsは価格が高めというのもネックでした。
今回はECSインスタンスにEBSをマウントして、そのEBSにログを吐き、DAEMONコンテナのfluentdにそのログを回収させるという方法を取りました。 ネットワーク障害よりもEBS障害のほうが影響が低いと考えているためです。
Spotインスタンスは急なタイミングでインスタンスがシャットダウンするため、EBSとの組み合わせを運用するのはチャレンジングでした。 最近出たEFSも検討しましたが、仕組みはNFSなのでfluentdなどの細切れな大量のファイルはパフォーマンスが出ない*2ので、却下としました。
EBSのログ運用
では具体的にどのような運用を行っているかを紹介します。
オンデマンドの場合、fluentのbufferが捌けるまで待つことができますが、spotインスタンスは2分までしか待ってくれないからです。 そこで以下のような運用をしています。
- EBSをインスタンスにmountさせるが、delete with terminateはOFFにする
- ECSにはDAEMONでfluentコンテナとfluent buffer 監視コンテナを建てる
- terminateを検知したら、fluent bufferをflushさせる
- bufferのflushが終わったらEBSにtagを付与
- 新しくインスタンスを立ち上げるとき、tagのついていないEBSがあれば別のマウントポイントにそのEBSをつける
- tagがついているEBSはバッチで削除する
と、このような運用をしています。
基本的にflushしきらないEBSはないので、tagがついていないEBSがあれば通知させ、手動でインスタンスを増やし回収させます。
デプロイ設計
今回もデプロイにはhakoを利用しています。前回から引き続きの利用です。
Envoyのデプロイに関してはコントロールプレインを置かず、直接ファイルをS3に置き、dockerのentorypointでS3からenvoyのymlをダウンロードしてEnvoyを起動する方式にしています。
デプロイ時のsyntaxチェックは以下のように実行しています
# jsonnetのsyntaxチェック $ find . -name "*.jsonnet" | xargs -n1 jsonnet # envoy ファイルのsyntaxチェック $ envoy --mode validate -c envoy/envoy.yaml
canaryデプロイ
canaryデプロイの仕組みを作成しています。
現状ではコントロールプレインを利用していないので、トラフィックシフティングは行っていませんが、ミラーリングを行うことでcanaryの仕組みを実現しています。
istioなどのServiceMeshのコントロールプレインを導入していないので、動的にEnvoyのルーティングを変えることが難しいです。 そのため、常時canaryのサービスを立ち上げておき、トラフィックを常に流しておきます。 こうしておくと、masterにマージしたらcanaryにデプロイされ、tagを切ると本番にリリースされるという運用が可能です。
ただし、この運用は配信APIのようにwriteがない場合に限ります。
以上のように簡単にトラフィックを増やせるのもEnvoy導入の利点でした。
まとめ
Opsworks運用はインスタンス起動後chefのレシピが走るため実際にサービスが稼働するまでがおそすぎました。 しかし、ECSの場合はインスタンスの起動とコンテナの起動は同タイミングではないのでインスタンスの起動時間が抑えられ、素早い起動ができるようになりました。
また、最近ENI Trunkingという機能が使えるようになり、1インスタンス内でのENIの数が大幅に増えたため、更にコスト効率が上がりました。
コンテナという選択だと、FargateやEKSなどもありますが、今回はECSの紹介をしました。 EKSでのコンテナ運用も始めており、コンテナをガシガシ運用したい方はぜひ一緒に働きましょう!