こんにちは。鹿島(@kashitaka)です。リクルートでサーバーサイドエンジニアをやってます。
今年も色々ありましたが、中でも社内向けのLT会で発表して、バックエンド・フロントエンド・ネイティブ関係なく面白いと好評だった**「ある障害の振り返りと学び」**をこの場にも書こうと思います。
TL; DR
ログイン直後に叩く状態取得系のAPIの設計に気をつけよう
- ログイン直後APIはログインAPIと同じくらい重要
- ログイン直後APIに多くの処理、複雑なロジックは盛り込まない
- ログイン直後APIではトランザクションデータはなるべく扱わない
障害内容
一部ユーザーでアプリが全く利用できない
私が担当しているプロダクトで「あるユーザーがiOSアプリでログインするとフリーズして全く利用できないようだ」という報告を受けました。一部ユーザーとはいえアプリが完全に利用不能な状態というのは緊急事態です。
システムについて
障害が起きたプロダクトは社外向けの有料サービスで、iOS・ブラウザアプリのクライアントアプリとAPIサーバーからなる一般的なアプリです。
コードの品質もよく、障害も軽微なものが年に数回起きる程度だったため「完全に利用できない」レベルの障害は意外でした。
原因の調査
調べると特定のユーザーで**ログイン直後にリクエストされる設定取得API(以下、ログイン直後APIと呼ぶ)**でエラーしていました。
また、このシステムでは予約
というEntityを扱うのですが、障害が発生しているユーザーはその予約
の登録件数が平均的なユーザーの数百倍という使われ方をしていました。
要因①: ログイン直後API
ログイン直後APIは、クライアントアプリの初期化と初期画面をレンダリングするために必要な情報:
- ユーザーの設定状態
- ユーザーの権限
- 現在のユーザーの状態
などを返すAPIです。サーバーでHTMLをレンダリングしていた時代と違い、SPAやネイティブアプリのクライアントアプリとAPIサーバーからなる昨今のシステムでは、ログイン直後に設定取得系のAPIを叩いて情報を取得するのは一般的な作りだと思います。で、このログイン直後APIがコケると初期化処理に失敗して利用不能になるわけです。
これは当然といえば当然なのですが意外と盲点でした。 ログインAPIは絶対にコケちゃいけないという認識があるので入念にテストしますが、ログイン直後APIは他APIと同じ扱いでアグレッシブに変更していました。
APIの名称も「ログイン直後API」などでは当然なく「ユーザー情報取得API」的なものなので、重要度は直感的に理解しにくいものでした。
要因②: 肥大化したログイン直後API
さらに調査のためにこのログイン直後APIの仕様を調べると、レスポンスパラメーターの項目数が30以上。オブジェクトの中の項目も含めると超膨大な数になっていました。
度重なる機能追加で初期画面のレンダリングに必要な情報がどんどん増えてこうなったのだと思いますが、ここまで多くの情報を詰め込んでいると
- 影響範囲がわかりづらい・把握できない
- 多くの情報をかき集めるため、実行するコード行数が多い
となり、どこか1つでもエラーすると全部共倒れしてしまい、クライアントアプリに大きな影響を出してしまいます。
要因③: トランザクションデータを扱う複雑なロジック
また、今回の障害にエラーになった箇所を調べると、上記のパラメーターのうち
-
予約ステータス
がタイプA かつ タイプB
となる予約
Entityの配列
なるものを取得する箇所で予約
レコードを数千件取得しようとしてタイムオーバーしていました。これは上の条件に一致する状態の予約
レコードが大量に作られないと負荷がかからないので、簡単な負荷試験では気が付きにくいと思います。
少なくとも、使い方やレコード数による負荷が予測しにくいトランザクションデータは、ログイン直後APIに入れるべきではありませんでした。
悲しいことに、取得に失敗していたパラメーターは画面では割と目立たない位置に表示する情報で、「これがないとアプリのUXを大きく損なう!」というレベルの重要情報ではありませんでした。
まとめと対策
- ログイン直後APIの重要性を軽んじた
- 情報が多すぎて影響範囲がわかりにくいAPIになっていた
- 使い方次第で負荷が予測できないトランザクションデータを使っていた
という地雷は想定しないレコード数で発動し、アプリ利用不能という重い障害が起きました。
もし、何か1つでもちゃんとやっていれば想定以上のレコード数だとしても軽い機能不備くらいの障害で済んだはずです。悔しい!!
対策としては下記を実施しました。
- アプリで絶対使えて欲しい主要な機能を定義
- その機能を動かすために最低限必要な情報を洗い出し
- ログイン直後APIはその情報のみにスリム化する
- その他の情報は別APIで取得。もしどれかが取得失敗しても主要機能が動くようにフロント実装する
で、見直してみると、主要機能を動かすために必要な最低限の情報って設定系のマスタデータくらいだったわけです。
マスタデータだと基本的にはDBから取得し、多少の加工をして返すだけの簡単な処理になるはずなのでテストも容易だし、レコード数の影響も受けにくくなります。
感想
- あまり知られてない(?)けど重要なAPI設計のアンチパターンを見つけられてよかったです。
- アプリによってはログイン直後APIが一つでなく、複数APIの結果が全て正常に揃わないと初期化できないケースがあるはずです。そのような場合には本当にその設計で良いのか見直した方がいいと思います。
- BFFでAPIをアグリゲーションするトレンドもありますが、アグリゲーションのしすぎは危険。APIの適切な情報量がありそうだなあと思いました。
そんなわけで、この失敗が読んでいただいた皆さんのシステム設計の参考になればと思います。参考になった方は👍を押していただけると。ではでは〜👋