こんにちは。Tunnelにてエンジニアをしてます阿南です。
最近は、WebやAPIまわりを中心にやっております。
RoomClipの技術ブログのリリース早々に自分の順番が回ってきたので、拙筆ではありますが、技術ブログを書かせていただきます。
RoomClipではユーザーは投稿した自分の写真に含まれる商品検索して、アイテムタグとしてその写真にタグ付けすることができる機能があります。

この商品検索の際、各ショップが提供しているAPIを利用して商品検索を行っているわけなのですが、外部から提供されているサービスを利用する時って、自分が実装した部分の範疇にないところで問題が起きたり、サービスの定める仕様に左右されたりと何かと問題があるんですよね。
このアイテム検索の機能拡張でも同様で、直面したのは以下の問題でした。
外部のサービスのシステム障害やメンテナンスモードなどにより、自社のサービス(アイテム検索機能)の一部が利用できなくなるケース
外部のサービスが定めるリクエストの制限数に達してしまい、一定時間リクエストが受理されないようなケース
このようなケースの解決策として、サーキット・ブレーカーなるアイデアがあるらしく、それを元にしてアーキテクチャを構成することで、外部APIを利用した際に発生した上記のような問題を、回避・解決した話をしたいと思います。
サーキット・ブレーカーとは?
簡単にまとめますと
クライアントとサーバーの間に入り、クライアント→サーバーへのリクエストや、サーバー→クライアントへのレスポンスを監視。
障害など発生し、一時的に利用ができなくなってしまったサービス(今回の件なら、外部API)を自社のシステムから切り離すことで、自社のサービス全体を停止せずに運用するための構成。
のことを言うそうです。
主に以下のような挙動をしています。
クライアントから外部サーバーへのリクエストや、外部サーバーからクライアントへのレスポンスはサーキット・ブレーカーを介して行なわれる。(closed state)
外部サーバーに対して、リクエストを送る際に、リクエストに失敗してしまうと、サーキット・ブレーカーが落ちる。(open state)
ブレーカーが落ちている間は、クライアントからのすべてのリクエストが、外部サーバーからのレスポンスを待つことなく、サーキット・ブレーカーがすぐにリクエスト失敗のレスポンスをクライアントに返す。
一定時間経過後に、サーキット・ブレーカーは外部サーバーへリクエストが可能な状態になる。(half-open state)
商品検索APIを利用するような今回のケースを例にしてみますと、
- 平常時(closed state)

- リクエスト失敗(この時点で、ブレーカーが落ちる)

- ブレーカーが落ちている間(open state)

- 一定時間経過(half-open state)

のような構成となり、4.の時点で外部のサーバーへのリクエストが成功すれば、1.のclosed stateに戻り、失敗すれば、3.のopen stateに戻ります。
回避・解決方法
今回のケースにおいてサーキット・ブレーカーが、監視するのは以下の2つです。
各外部サーバーに障害が発生していないかの監視
- 使用不可サーバーの判別が常について状態にする目的
- 障害発覚時点で自動的にブレーカーを落とす(open state)
各外部APIの上限リクエスト数が超えないかを監視
- 各外部APIの秒間リクエスト数の上限を超えないようにする目的
- リクエスト制限を超えた時点で自動的にブレーカーを落とす(open state)
また2つの役割を1つのクラスが担うのでなく、別々のクラスがそれぞれの役割を担うような実装してます
"外部サーバーに障害が発生していないかの監視"について
ユーザーからのリクエストとは別に定期的に外部のサーバーへのリクエストを送信。
レスポンス内容を判断(400,500番系のエラー検出や200番で返ってきたときのレスポンス内容で判断)し、外部サーバー側での障害が見られるようなら、各外部サーバーごとにブレーカーを落とす。
その後即座にhalf-openになり、ユーザーからのリクエストとは別に定期的にリクエストを送信し続けるが、ユーザーからのリクエストに対しては、ブレーカーが落ちている状態(open state)になっている。
以下のようなイメージです。
ユーザーからのリクエストに対してはopen stateであるので、

のようにユーザーからは見えていますが、実際はhalf-open stateになっています。

また外部のサーバーごとにブレーカーが落ちたり元に戻ったりしますが、この外部のサーバーごとのブレーカーの状態(すなわち外部サーバーがリクエストを受理できるかどうかの状態)はMySQLで管理しています。
"各外部APIの上限リクエスト数が超えないかを監視"について
RoomClipで利用している外部の商品検索APIのリクエスト制限は、だいたい秒間1~10リクエストでした。
ですので、各APIのごとに1秒間で何回のリクエストがあるのかをカウントするようにします。
以下のように動作します。
1秒で揮発するように設定したRedisが各APIごとに、ユーザーからの叩かれたリクエスト回数をカウントした結果を保持し、カウンターが上限数に達したら、外部サーバーへリクエストを送る前に、APIごとにブレーカーが落ちる。
数秒(2~3秒程度)待機し、half-openに移行。再度同様のリクエストを送信する。

Redisを1秒で揮発するように設定しておくことで1秒経つと、その前の1秒間に送られたリクエスト数がリセットされます。
ちょうどリクエストの上限に引っかかってしまったら数秒待機となってしまうので、商品検索の結果のレスポンスが遅れ得てしまいますが、リクエスト制限に引っかかって検索結果が返ってこないような自体になるよりはよいのではないでしょうか。
こんな感じで僕の初の技術ブログ執筆は終わりですが、
やっぱりなにかを書くのって難しいですね。
今後もTunnelの諸先輩エンジニアの元でたくさん学ばせていただきまして、もっと面白い記事が書ければなと思います。
それでは。
参考

この記事を書いた人:アナン トモロウ
ルームクリップ株式会社でエンジニアしております。 最近はエンジニアリングに限らず いろいろやってます