Kitchen」が開催されました。第20回となる今回のテーマは「クックパッドのマイクロサービスプラットフォーム現状」。クックパッドが開発を行っているマイクロサービスプラットフォームの今と、その仕組みについて解説します。安定運用のために」に登壇したのは、鈴木康平氏。ECSの運用事例と工夫していることについて解説します。ECSの安定運用」というタイトルで発表したいと思います。今回のアウトラインとしては、「ECSをどう使うか」みたいな話ではなくて、そのECSを運用していく上でこんなことやっていますよということを話していければなと思います。内容としては、コンテナインスタンスをどう管理してるのかと、あと各アプリケーションログをどう配送して保存してるのかと、最後にモニタリングについて話そうと思っています。最初に規模感を共有しておこうと思います。現在クックパッドでは、ほとんどのアプリケーションがECS上で動いています。「hako」という自作ツールを使ってアプリケーションのデプロイとか、あるいはバッチジョブの起動を行っています。ECSクラスタ数はいま全部で40くらいあって、ECSのサービス数で言うと500くらいです。1日当たりのRunTask数、だいたいバッチジョブの起動回数だと思っていいはずですが、それが8万くらいという規模感で運用をしています。コンテナインスタンスの管理について話していきます。コンテナインスタンスとしては、オンデマンドインスタンスとスポットインスタンスの両方を使っています。Fleetを使っています。いまオンデマンドとスポット比はだいたい全体だと1:4くらいになっています。これは海外のサービスとかも含めてこうなっているんですけど、日本のサービスに限るとオンデマンドの割合が10パーセントを切るくらいの割合になってます。Fargateを使うケース、使わないケース「コンテナインスタンスの管理」というと、おそらく「Fargateを使ったらいいんじゃない?」みたいな話が出てくると思いますし、実際にFargateを使っているところはあるのですが、基本的にいまは自前管理のインスタンスを使っています。いうのも、主な理由としては2つあります。1つはスポットインスタンスを使ったほうがなんだかんだ安いということ。あとは、Fargateが出てきたばかりなのでしょうがないところもあるかもしれませんが、起動が遅かったり、起動までの時間が安定しないという問題があることです。Webアプリではそんなに頻繁に起動するわけじゃないので困らないのですが、そこそこの頻度で起動するバッチジョブでこういうことが起こるとけっこうつらいです。積極的には採用してません。起動後にENIをアタッチするまでに、5分くらい待ってタイムアウトして終わる、みたいなのがたまに発生していて、そういうのがあるとけっこうつらいので基本的にFargateは使っていません。逆にFargateを使っているケースはどういうのかというと、ECSクラスタ自体を操作するジョブですね。この後に話しますけど、例えば、オートスケールを行うようなジョブ。ECSクラスタ自体のオートスケールを行うようなジョブはFargateを使って動かしていますし、特別に大きなCPUとかメモリリソースを必要とするジョブは、それだけのためにクラスタ作るのもめんどくさいだけなのでFargateでサクッと動かすというふうな使い方はしています。オンデマンドクラスタの管理オンデマンドクラスタのほうはどういうふうに管理してるかというと、全クラスタで共通のAMIから起動するようにしています。Group自体に「CloudWatchと連携してオートスケールする機能」があって、それは一切使っていません。全部自前でオートスケールしています。capacityというアトリビュートを上下することによってECSクラスタの数が増減できる状態にするところと、EC2のインスタンス障害が普通にあるので勝手に代わりのインスタンスを立ててくれるといったオートヒールとして使っています。ECSのコンテナインスタンスを管理するときにスケールアウトするのは簡単です。新しいインスタンスを起動してECSのエージェントを起動するだけでいいんですけれど、スケールインするときには事前にサービスアウトをしておく必要があります。hookを進める前にサービスアウトしきってからTerminateさせています。GroupでECSクラスタを作るときはだいたいどこでも同じようなことになるじゃないかなと思っています。Fleetで管理していて、こちらにもCloudWatchと連携してオートスケールする機能はありますが、それは一切使わずに自分でオートスケールをしています。capacityというアトリビュートを上下するだけでECSクラスタの増減ができるようにすることですね。FleetというAWSのサービスを利用しています。Fleetでのサービスアウトこちらのサービスアウトも同じようにできればいいのですが、スポットインスタンス特有の問題があります。スポット価格の上昇で勝手にTerminateされてしまうんですよね。noticeという通知がくるので、それを利用してサービスアウトを行っています。noticeを受け取る方法はいくつかあります。Eventsから受け取って、SQSキューにそれを通知して、デーモンがそのメッセージを受け取ってサービスアウトの処理を行うという流れにしています。それと、スポットインスタンス特有の問題として、通知が来てから2分以内にサービスアウトをする必要があるんですね。hookのように「サービスアウトするまで待ってくれ」みたいなことが一切できないのでそこをどうにかする必要があります。API、つまりアプリケーションにSIGTERMを送って止めています。スポットインスタンスでバッチジョブを動かすときは、アプリケーション側に冪等性を要求するというデザインにしました。普通のWebアプリをサービスアウトするときは、コンテナインスタンスをDRAINING状態にすればサービスアウトはされるんですが、それだけだと2分以内という時間制限に引っかかることがあるんですね。そこを解決するために「Terminateされるよ」という通知が来たインスタンスをELBから先に自分で外してしまうようにしています。その後に、一定時間が経ってからStopTaskでそのタスクを止めるようにしています。いうことをすると当然、突然一部のタスクが停止しているように見えるので、キャパシティが一時的に足りなくなってしまうんですね。なってもちゃんと要求キャパシティを満たせるように、ふだんからやや過剰気味なキャパシティを常に確保しておくという戦略でやっています。するとタスク数が増えてインスタンス数も増えてしまいますが、そうなったとしてもスポットインスタンスのほうがずっと安いので、こういう方針にしています。簡単な図で説明するとこんな感じですね。Eventsから通知されるので、それをTerminatorと呼ばれるデーモンが受け取ります。バッチジョブの場合はまずDRAINING状態にして、新しいタスクが起動してこないようにしてから、StopTaskでそのタスクを殺すようにしています。ELBがあった場合は間に処理が入ります。最初にDRAINING状態にして新規のタスクが来ないようにするところまでは一緒ですが、その後に自分でさっさとELBから外してしまって、ELBから外れたことを確認したら、StopTaskでタスクを止めてしまうようにしています。オートスケーリングはどうやっているか?こんな感じでクラスタをオートスケールするために、スケールイン時にこういった処理を行ってるんですが、そのオートスケーリング自体はどうやってやってるかっていうのをここから話していこうと思います。オートスケーリングはオンデマンドクラスタとスポットクラスタの2つあるんですけども、その間にとくに戦略の差はありません。同じような仕組みでオートスケールを自前で実現しています。capacityという値を調整するかどうかの違いがあるだけで、戦略自体はどちらも一緒です。それで、3種類のスケールアウトと1種類のスケールインを組み合わせて行っているので、それについて話していこうと思います。スケートアウトのほうですね。スケールアウトのほうは、この3つを行っています。それぞれについて詳しく見ていこうと思います。これは非常に単純で、CloudWatchにCPUReservationとかMemoryReservationという、「ECSクラスタをどれだけ使っているか」のメトリクスが最初から存在しています。それを定期的にチェックして、ある閾値を超えていたらスケールアウト。80パーセント超えてたらもうちょっと増やしておくみたいなことをやっています。スポットインスタンスの場合は、スポット価格の上昇によって勝手に死んでしまうことがあります。スポットクラスタの場合はオンデマンドより閾値をやや低くして、常にインスタンスが少しだけ余っている状態を維持しています。各ECSのサービスの、desired_countとかそのへんの値を定期的にチェックして、足りていなさそうだったらスケールアウトするというのをやっています。ECSの場合は、ふだんだったらそのdesired_countという「これぐらいタスクを起動してほしい」値と、running_countという「実際に動いてるタスクの数」が一致しています。一方で、「ECSサービスがオートスケールしてちょっとタスク数が増えた」、つまりdesired_countが上がったときに、ふだんだったらpending_count、つまり「まだタスクは起動してないけど既にコンテナインスタンスにはアサインされている」状態の値が上がります。コンテナインスタンスが足りていない場合はpending_countも増えず、そういった状況になっていたら、コンテナインスタンスが足りない可能性があるので、さっさとスケールアウトしてキャパシティを確保しにいきます。3つ目は、主にバッチジョブ用のクラスターのオートスケールの話です。oneshotでバッチジョブを起動したときに、リソース不足でそのAPIが失敗したときにはSNSトピックに「リソース不足だよ」と通知するようにしているので、その通知を受け取って必要に応じてスケールアウトしています。図にするとこんな感じです。最初にhakoっていうコマンドラインツールがバッチジョブを起動するためにRunTaskというAPIを呼んで、これが失敗したとします。その場合は、まずSNSに失敗を通知して、それをAutoScalerという社内で動いているデーモンが受け取ります。Fleetを操作したりします。APIを定期的にリトライし続けます。その間にAutoScalerがおそらくスケールアウトして、新しいリソースが確保できるので、そうなったらようやくRunTaskが通って、無事バッチジョブが起動できるというかたちにしています。ここまではスケールアウトのほうで、スケールインのほうは単純に1つだけやっています。スケールアウトのときも出てきましたが、CPUReservationとかMemoryReservationというメトリクスがふつうに最初からあるので、一定の閾値を下回ったらスケールインというふうにしています。こっちの場合も、スポットインスタンスの場合は同じように突然停止する可能性があります。オンデマンドクラスタよりも閾値を低くして、なるべくあんまりスケールインが起きないようにしています。ログの取扱についてここまでがコンテナインスタンスの話でした。ここから先は、各アプリケーションから出たログをどうしてるかという話をしようと思います。Athenaという「S3上に置いたファイルにSQLで問い合わせができる」サービスがあって、それを使って簡易検索ができるようにしています。これについて、ここから詳しく話していこうと思います。Logsかなと思います。Logsを使えば完全にマネージドな環境でログの保存や検索、閲覧ができるんですね。Logsを使っていました。Logsにリアルタイムに入りきらないという問題があったので、仕方なくここから別の方法を探すことになりました。Logsはログのストレージ料金だけではなく、ログを入れるときにも料金がかかったりします。Logsに入れて、その後に古いログをアーカイブするだけでは解決できそうになかったので完全に別の方法を取ることにしました。Logsが便利だったところはやや犠牲にしつつも、スケーラブルで安価なS3をログストレージとして使うことにしました。全体の流れはこんな感じになっています。最初の部分について話します。driverの設定でいったん送信します。そこからコンテナインスタンスのfluentdが受け取ったログは、そのままfluentdの集約用のノードに転送します。v1.0からは複数プロセスでログを処理できるようになっているので、それを利用してがんばって処理しきるようにしています。いまはピークタイムにあわせておそらく20並列ほどでがんばって処理していますね。この集約ノードが受け取ったログは次にS3に置くことになります。この集約ノードのfluentdが1分ごとにservice-logsというバケットに、サービスとコンテナ名と日付をプレフィックスにして、ログは一旦ここに置きます。することによって、あるサービスのこのコンテナのログは特定のプレフィックス以下を見れば全部集まっている状態になるので、最初はこういうかたちでログを置いています。Eventの通知を経由して、ecs-logs-routerに「新しいログが来た」という通知が行きます。こいつは何をやっているかっていうと、このservice-logsというバケットに置かれたログをタスクIDごとに分解して、また別のバケットにサービス名とコンテナ名と、あとタスクIDで切ったところに置いています。こっちはタスク単位で閲覧するためのログで、例えば「あるタスクが異常終了したので原因を調査したい」といったことがよく発生すると思いますが、そういったときはこっちのログを見に行けば、最初からタスクIDごとに分解された状態でログを見れるようになっています。ログ検索の仕組みログを置くだけだとあんまり役に立ちません。grepで検索できるようにならないとなかなかログを扱うのが難しいと思います。Athenaで検索できるようにしています。このサービスを使うと、日付でパーティションを切りつつもSQLを使って検索ができるようになります。「my-awesome-app」というアプリケーションのappコンテナの今日のログから、「ERROR」という行を探したいときには、こんな感じでselect文を書いてAthenaに投げれば結果が返ってくるようになっています。Consoleからこのあたりを全部やるとさすがに厳しいです。先ほどの発表でも何度か出てきたと思いますが、hako-consoleという社内で作ったコンソールからS3に置いてあるログを閲覧できたり、あるいはバックエンドのAthena経由でログを検索できてその結果を見たりができるような環境を整えています。モニタリングについて最後はモニタリングについて話します。CloudWatchのほうにサービス単位のメトリクスは存在してるんですけれども、タスク単位や、あるいはコンテナ単位というメトリクスが存在していない問題があります。これはサービスのメトリクスなので、RunTaskで起動したバッチジョブの場合は一切メトリクスがないという状況になっています。サービス単位で全部まとまってしまうと、もう1つ問題があります。アプリケーション開発者にとって、主に見たいのはアプリケーションコンテナのメトリクスだけなんですよね。ECSで動かすときはサイドカーとしてfluentdを起動したり、あるいは最初の発表であったようなEnvoyを起動したりしますが、そういったコンテナとメトリクスが混ざった状態で見られてもあんまり嬉しくないという問題があるんです。このあたりをなんとかするために、自分たちでモニタリング用のメトリクスを取るようにしました。言っても、実際にはcAdvisorというツールが既に存在しているので、これでメトリクスを取得します。あとは社内に既にPrometheusの環境があったので、PrometheusにcAdvisorから取ったメトリクスを入れて、Grafanaのダッシュボードで可視化するようにしています。schedulingという各コンテナインスタンスに1台起動するオプションが追加されているので、それで各コンテナインスタンスに配備して、Prometheusからそのメトリクスを取りにいくというかたちにしています。するとだいたい、Grafanaで見るとこんな感じになっています。いま何台起動してるのかとか、あるいは1秒間あたりのCPUtimeをどれだけ使っているとかがcAdvisor経由で取れたりします。あとよく見るのはメモリですかね。メモリがリミットに対してどれくらい使われているかとか、そういったものをタスク単位・サービスのコンテナ単位で見られるようにしています。まとめと今後についてというわけで、最後にまとめです。ECSでさまざまなサービスを動かすためにやっていることの一部を紹介してきました。今回、時間の関係で話せなかったんですけど、ECSのレートリミットを回避するための工夫とか、あるいはコンテナインスタンス側の問題を調査するためにいろんなログを入れてたりしています。このあたりに興味あったら、後のQAとかで聞いてもらえばなと思います。最後、今後についてです。いまちょうどre:Inventの期間なので、AWSにさまざまな新サービスや新機能が発表されているところで、センシティブな値を環境変数に入れる機能がようやく来ました。これを使うように移行していきたいと思っています。Fleetがかなり扱いが面倒くさくて、いろいろ機能が足りてなかったり、名前をつけることすらできなかったりしました。Groupでサポートされたっぽいので、こっちを使うようにしていきたいなと思っています。以上です、ありがとうございました。資料を公開しているので、よろしければご覧ください。Kitchenクックパッドオフィスのキッチンで、開発に関する知見や学びを共有する勉強会です。