国内最大級のアウトドア情報アプリ「ソトシル」を支えるインフラ改善
sotoshiru(ソトシル)は、株式会社スペースキーが運営する国内最大級のアウトドア情報・実例写真の共有サイトです。
Da Vinci Studio はソトシルのアプリ開発やインフラ改善などを任されています。 今回はインフラに着目し、ユーザーの増加に伴って発生した課題と、それにどう対応してきたかを担当者に聞きました。
ユーザー数とともに増大してきたインフラ課題
―― 馬場薗さんは、カワスイ 川崎水族館に引き続きのインタビューですね。
Da Vinci Studio ではソトシルのアプリ開発も担っていますが、今回はインフラにフォーカスしたいと思います。
インタビュイーは一人だけなので、その分、存分に語ってください(笑)。
それでは、自己紹介からよろしくお願いします。
馬場薗 Da Vinci Studio インフラ基盤部 SRE の馬場薗です。インフラの新規構築から保守運用、DevOps の整備に至るまで幅広く担当しています。
最近はコンテナで環境を構築することがほとんどなので、AWS であれば ECS(Amazon Elastic Container Service)や EKS(Amazon Elastic Kubernetes Service)をよく触っています。モニタリングはプロジェクト次第ですが、Prometheus や Grafana、New Relic あたりを使うケースが多いですね。
今日は、よろしくお願いします。
―― よろしくお願いします。馬場薗さんは2回目の登場となります。関わっているプロジェクトの数が多いだけではなく、インフラだけではなくアプリケーションの実装まで手がけていたりと、活躍の幅が広いですよね。
馬場薗 いえいえ。でも、そうですね。パフォーマンスの劣化や、障害に対応する時にコードを見て修正するということもよくやっています。
―― ソトシルは、そうした馬場薗さんの業務のカバー範囲の広さを最大限に発揮しているプロジェクトのように感じていました。今日はあらためて、お話を聞かせてください。
まずはざっくりと、ソトシルで進めていた業務を教えてください。
馬場薗 大きなトピックだと、バッチや API のパフォーマンス改善に始まり、増え続けるストレージコストへの対応、安定性の向上、そしてインフラの Docker への移行あたりでしょうか。
―― フルコースですね(笑)。どこから取り組んでいったんですか?
馬場薗 プッシュ通知にまつわるところです。当時、事業上の観点からも大きな問題になっていました。
プッシュ通知の処理時間を 75%改善!
馬場薗 特に課題だったのがプッシュ通知の処理に非常に時間がかかっていたことでした。プッシュ通知を対象ユーザーに送り始めて完了するまでの処理時間は、当初は 15 分程度でした。ところがユーザーが増えるにつれてどんどん伸びてしまい、私が関わった頃には 40〜50 分もかかっていました。
ソトシルはサービスの特性上、プッシュ通知の重要度が非常に高いんです。そのため、意図したタイミングで通知できないことは、優先して解決すべき問題でした。
―― 一時間近くもかかっちゃうのか。それは大きな問題ですね。どのあたりがボトルネックになっていたんですか?
馬場薗 データベースへの書き込みです。通知はユーザーごとに内容が異なるため、個々に通知データを作成したうえでデータベースに保存し、最終的にまとめて通知を実行するというプロセスでした。このうち、ユーザーごとの通知データを書き込む処理に時間がかかっていました。
冒頭で話したストレージ容量の問題も、これが起因していました。
―― なるほど。個々に通知データを作成していたのなら、ユーザー数が増えれば増えるほど時間もかかるし容量も大きくなるということですね。
なかなかやりがいがありそうですね(笑)。どうやって対応したんですか?
馬場薗 いくつかの段階に分けて対応を進めていきました。まず、手を付けたのはデータ構造の見直しです。実は、すべてのユーザーに異なるコンテンツを通知していたわけではなく、グループごとに同じ内容を送っていたんです。
そこで通知の中身そのものは切り出し、ユーザー個別には、どのコンテンツを配信するかという紐付けだけを持つようにしました。
次に、通知データを保持するストレージも切り替えました。このデータは通知プロセスが終われば不要なので、Amazon Aurora MySQL から ElastiCache Redis に変更しています。
最後に、ユーザーごとに 1 件ずつ書き込んでいた処理を、同じ内容を配信するユーザー分はまとめて書き込むようにして、トランザクションサイズを大きくしました。
―― データ構造の無駄を省いて書き込み IO とストレージサイズを小さくしつつ、さらにストレージと書き込み方法を工夫して IO を減らしたってことですね。素晴らしい。
これでどのぐらい効果があったんですか?
馬場薗 この一連のデータベースまわりの改善だけで、バッチ処理の実行時間は半分になったと思います。コストは 10%ほど削減できました。
―― おーすごい。かなり改善しましたね。
馬場薗 副次的な効果もいくつかありました。例えば、ユーザーが通知を見てアプリを開くと呼ばれる API のレスポンスが改善できました。
あとは、Aurora から Redis に切り替えたことで、通知データは保持期間が過ぎれば自動で削除するようになったため、定期的に古いデータを削除するプロセスもなくせました。
―― APIのレスポンスまであわせて改善できたのは素晴らしいですね。ちゃんと考えられてる。
そして、「データベースの改善だけで」ということは、それだけじゃないってことですね?
馬場薗 はい。次に通知を送信するユーザーを抽出する処理に着目しました。ユーザーのグループごとに Aurora から検索して処理していくのですが、この抽出のためのクエリも重く、時間がかかっていました。
グループごとに条件が異なっていたので、ここは地道にそれぞれのクエリの最適化を実施しました。インデックスを追加したり、うまく機能していないインデックスを最適化したり。 アプリケーションが Ruby on Rails で実装されていたのですが、ActiveRecord オブジェクトとしてロードしていたためにメモリをかなり使っていたので、必要なデータだけを参照するようにしたり。
また書き込み時と同様に、一度に処理するバッチサイズも調整していきました。
―― 地道ではあるものの王道な対応ですね。これ、言葉で話すと簡単に終わっちゃうんですが、大変な作業ですよね。
それこそバッチサイズの調整なんて、トライアンドエラーだったんじゃないですか?
馬場薗 仰るとおりです。地道にパラメータを調整しては計測して……の繰り返しでしたね。インデックスの張り方も実行計画を見ながら試行錯誤しました。
―― ですよね。お疲れ様でした……。実際の効果のほどは、どうだったんでしょう?
馬場薗 速くはなったんですが、データベースの対応に比べると劇的というわけではなかったですね。この前段階で 15〜20 分ほどだった処理時間が、10〜15 分になったという感じでしょうか。
―― 20〜30%も改善したってことじゃないですか、十分すごいですよ(笑)。
当初からすると 1/4〜1/5 になったってことですね。
馬場薗 はい。通知周り以外の課題もあったのと、これくらいの処理時間であれば十分、許容範囲になったので、一旦の対応はここまでにしました。
処理を並列化しようかというのもアイデアとしてはあったのですが、まだ実現には至っていません。
―― まだ手はあるぞということですね(笑)。要求を満たすラインで、あまり複雑化させないのも大事だと思います。
バッチの実行時間が 2 倍になる原因を綿密な調査で発見
馬場薗 実行時間とともに、バッチの安定性も問題視されていました。例えば実行時間が前日に比べて倍に伸びてしまうことがしばしばありました。たいていは次の日には元の時間に戻るんですが、連日、続く場合もありました。
―― それは何か異常なことが起きていそうですね。通知を送るユーザーの数が大きく変動しているわけではないんですよね?
馬場薗 実際に処理した数や結果を比較したものの、異常はなかったんです。 当然、データベースやサーバーの負荷も確認しましたが、余力がある状態でした。ただ、実行時間だけが伸びている状況で、最初は本当に原因が分からなかったですね。
―― なんだか気持ち悪い状況ですね……。
馬場薗 そうなんです。手探りで原因を調べていったところ、結果的には標準出力への IO が原因でした。ログはバッチジョブから標準出力に吐き出し、AWS CloudWatch Logs に送っていました。
CloudWatch Logs の調子が悪いときにレスポンスタイムが悪化すると、それがバッチジョブの標準出力をブロックしてしまっていたんです。
―― それはなかなか分からない(笑)。どうやって調べていったんですか?
馬場薗 実行時にサーバーに余力がありすぎるのが怪しいと思いました。平時、バッチが稼働しているより負荷が下がっていて、特に CPU がまったく仕事していませんでした。
そこで、実際に処理が長くなる現象が発生したタイミングでプロセスの動きを調べていくと、数百ループに一度ぐらいの頻度でプロセスがスリープしてしまうことが分かりました。
最終的には strace でどこで止まっているかを調べていって、標準出力への書き込みを待っていることを突き止めたんです。
実際は、ここまで辿り着くまでにかなり試行錯誤してはいます。
―― 地道な努力の結実ですね。原因が分かって良かったです。
馬場薗 この一連の調査は、かなり勉強になりました。
ユーザー体験に直結する API のレスポンス改善
―― API レスポンスのパフォーマンスも改善したんですよね?
馬場薗 はい。まさに先ほど話に出たように、プッシュ通知を見てアプリを起動するユーザーが多いので、そのアクセスの集中に耐えきれずレスポンスタイムが悪化するエンドポイントがあったんです。
具体的には、ユーザーに対しての新着情報を返すエンドポイントなどですね。
―― せっかくプッシュ通知を改善したのに、アクセス自体が遅いとユーザー体験としては良くないですよね……。こちらは、どうやって対応を進めていったんですか?
馬場薗 New Relic APM を利用して、対象のエンドポイントを洗い出して個別に対応していきました。
バッチと同じようにループ回数が不用意に多かったり、ActiveRecord オブジェクトそのものをキャッシュしていて遅くなっていたりしました。
実装に起因しないものだと、データベースの問題もありましたね。例えば API とバッチが参照する Aurora の Reader が同じで、バッチの実行タイミングと被るとデータベースのレスポンスが悪化してしまっていました。それを回避するため、Reader を分ける対応も入れました。
―― すごい。王道といえば王道ですけど……それぞれ実際にやるのは大変だったでしょう。
馬場薗 そうですね(笑)。当然、それぞれの箇所で実装は異なります。ただ、その分、自分の引き出しも増えました。
パフォーマンスとコストにこだわった Docker 化
―― Docker 化はどういった経緯で始まったんですか?
馬場薗 もともとソトシルのインフラは AWS の EC2 で構築してありました。ただ、コード化には至っておらず、過去に他のアプリケーションが同居していたりと構成管理がブラックボックスになっていたんです。
―― ブラックボックスというと、ドキュメントがなくてバージョンが把握できない状態だったんでしょうか?
馬場薗 それもありますね。困ったのは、過去に使われていたものの今は不要だったりや、検証用に入れたりしたモジュールも残っていたことです。
―― なるほど。どれが必要でどれが不要なのか判断できないのか。下手に削除できないですよね……サーバーの増設は、どうしていたんですか?
馬場薗 既存のインスタンスのイメージから複製していました。複製後、必要な部分を修正する形です。
―― おお、温もりのある手作業……。オペレーションミスが怖いですね。
馬場薗 はい。あまりドキュメントもなくて……ここまで業務を進める上でもしばしば問題になっていましたし、何よりサンドボックス環境が作りづらく、大規模な検証が困難でした。
優先度の高いパフォーマンス上の課題を解決したので、その次に言語やフレームワークのアップデートなど整備を進めていくにあたり、まずはこの部分を解決しようということになったんです。
―― 確かに。検証もですけど、切り戻しも考慮すると、その方が絶対に良いですね。アップデート作業を念頭に置いていたので、コンテナ環境の方がやりやすそうってことか。
馬場薗 はい。既存環境に残っているインスタンスを精査するのは厳しいのでゼロベースで構築しつつ、OS も入れ替えたかったので、もう Docker に移行しちゃいましょうと提案しました。
―― Docker 化は、どんな風に進めていったんですか?
馬場薗 まずはローカル環境を Docker 化しました。Ruby on Rails アプリケーションの依存関係は把握できていたので、Dockerfile の作成はスムーズでした。
ただ、挙動の確認以上にパフォーマンスとコストまわりをかなり綿密に検証しました。
―― パフォーマンス改善のために、かなり頑張ってきましたもんね。Docker 化してパフォーマンスが下がったら、元も子もない(笑)。
馬場薗 今回、構築した Amazon ECS の環境と、既存の EC2 の環境では特性も異なるので、そこの差を意識して同等のパフォーマンスが出せることと、パフォーマンスが同じでもコストが膨れ上がらないよう比較しながら見積もりました。
―― そうですね。Docker にすることで運用上のメリットは大きいけれど、それでコストが倍増ってなると困りますもんね。
馬場薗 最終的には ALB(Application Load Balancer)の荷重ルーティングを利用して徐々に切り替えていきました。
動作確認としては、テストがない部分もあったので、事前にスペースキーの方や Da Vinci Studio のアプリチームも巻き込んで、かなり細かく実施しました。 結果として大きな問題もなく切り替えられて、良かったです。
―― 素晴らしい。しっかり検証できてたんですね。Docker 化するにあたって構成を組み替えたところはありましたか?あるいは、ハマったところなど。
馬場薗 ログまわりが大変でした。分析用のログは、もともと EC2 上で動く fluentd を経由して収集していました。
Docker 環境化では Sidecar にしたことで構成が変わったため、互換性に配慮しました。具体的にはローテーションのタイミングや、タイムゾーンの設定を合わせています。 これらも環境を構築し、実際に結果を見ながら修正していきました。
―― タイムゾーン!9時間分が微妙にずれるってやつですね。これ、漏れると気づきにくいですよね。
馬場薗 あとは、アラートの通知の集約と整理も大変でした。それまでは Zabbix や Mackerel など複数のツールでモニタリングし、通知先もバラバラでした。これを CWLogs と New Relic に集約して通知先もアラートレベルに応じてまとめました。
―― 素晴らしい。バラバラになっていると、優先度が分かりづらくて「割れ窓」になりがちですしね。
広い知識で難易度が高いボトルネックを発見できるエンジニアに
―― 結構、盛りだくさんですね。あらためて振り返ってみてどうでしたか。
馬場薗 期間でいうと約2年ですかね。ソトシルのプロジェクトだけやっていたわけではないんですが、振り返ると本当にいろいろなことをやりましたね。関与し始めたころは、今日の話にもあったようにプッシュ通知など不安定でよく対応していたのですが、そういったところも落ち着きましたね。個人的に初めての業務も多く、全体として非常に良い経験になりました。
―― 良かったです。ソトシルで得られた知見は他のプロジェクトにも活きていますか?
馬場薗 そうですね。特にパフォーマンス関連では、調査や対応の引き出しが増えたので、レスポンスが遅いぞとなっても素早く打ち手が出せるようになりました。プロセスの安定性に関する問題は、なかなかお目にかかれない内容だったこともあり、そもそもの調べ方などかなり勉強になりました。
―― 実際に問題に直面して解決していくのが技術力としては一番、身につきますよね。
馬場薗 そうですね。書籍を読んだり勉強会に行ったりして知識として持ってはいても、それだけだといざというときに紐付けられないんですよね。実際に自分で対応することで、本当の意味でスキルになった感じがします。
―― 今日の話にもありましたけど、データベースのインデックスの最適化と一言で言っても、実際の作業は試行錯誤ですしね。
馬場薗 そうなんですよ(笑)。インデックスの改善って基本的な考え方はあるものの、実際のコードとなると大体イレギュラーな要素が含まれるし、インデックスのみならず実装の改善など打ち手も無数にあるので悩むんですよね。
―― ちなみに、似たようなプロジェクトがあったらまたやりたいですか?
馬場薗 こういうパフォーマンス改善系の業務は、ぜひやりたいですね。さらに言うなら、ソトシルではフロントエンド面まではあまり手を入れられていないので、機会があったらやりたいです。
―― フロントエンドというのは JavaScript とかってことですか?
馬場薗 はい。SRE の職域からは少し外れてしまうかもしれませんが、しっかりデータを取って、ボトルネックを特定して改善するというところですね。
データのデリバリーをまとめたり、細分化したり、いろいろやりようがあるので、経験を活かしたいです。
―― パフォーマンス改善なら任せろ!といった感じですね。頼もしいです。
今日はどうもありがとうございました!
ソトシル アプリはこちらからダウンロード!
ソトシル - キャンプや釣り、登山などのアウトドア情報アプリ
関連メディア
【CAMP HACK】日本最大級のキャンプ・アウトドアマガジン - キャンプハック
【YAMA HACK】日本最大級の登山マガジン - ヤマハック
【TSURI HACK】日本最大級の釣りマガジン - 釣りハック
【CYCLE HACK】自転車が楽しくなるマガジン - サイクルハック