Blog
Finagle Client のリトライ機能
AI Messenger というチャットボット事業でサーバサイドの開発をしている大野です。
AI Messenger では、非同期 RPC フレームワークの Finagle を使用して開発をしています。
Finagle については 今年4月に弊社内で行った AdTech Scala Meetup の記事をご覧ください ([AdTech Scala Meetup] Finagle | Scala Tech Blog)。Meetup の記事中では「アドテクスタジオではほとんど導入実績が無い」とありますが、最近アドテクスタジオの中でも導入しているプロダクトが増えてきました。
AI Messenger では内部のサービス間や Facebook Messenger 等メッセージングサービス間で Http 通信を行っていますが、様々な要因でリクエストが失敗することがあります。その際に Finagle Client のリトライ機能によって再度リクエストして、リカバリしています。
リトライ処理は、回数や頻度を調整せずに行うと過度にリクエストを発行してしまう危険性があります。そのため、 Finagle Client では Backoff と RetryBudget によってリトライを調整することができます。
Backoff
あるリクエストが失敗してリトライする対象になった時に、リトライする間隔を調整する機能が RetryBackoff です。
Stream[Duration]
で定義を行い、例えば Backoff(start = 1.seconds)(_ * 2)
だと 1.seconds #:: 2.seconds #:: 4.seconds ...
という形のストリームが定義されます。この場合だと、あるリクエストが失敗してリトライする時、はじめのリトライリクエストは1秒後、2度目は2秒後という風に間隔を変えてリトライすることができます。
上記の RetryBackoff でリトライ間隔が調整される Http Client を定義したサンプルコードは以下のようになります。
(本記事のサンプルコード中で使用している finagle-http
のバージョンは 6.35.0
です)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import com.twitter.conversions.time._ import com.twitter.finagle.Http import com.twitter.finagle.http.{Request, Response} import com.twitter.finagle.service.{Backoff, RetryFilter} import com.twitter.finagle.util.DefaultTimer import com.twitter.util._ val shouldRetry: PartialFunction[(Request, Try[Response]), Boolean] = { case (r, Return(rep)) if rep.statusCode >= 500 => true } val backoff = Backoff(start = 1.seconds)(_ * 2) val retryFilter = RetryFilter(backoff)(shouldRetry)(DefaultTimer.twitter) val client = Http.client.newService(host) val service = retryFilter andThen client |
Finagle は OSI 参照モデルのセッション層を扱うため、アプリケーション層の Status Code を失敗と見做すために、(今回は乱暴ですが) Http Status Code が 500 以上の場合をリトライ対象にするように shouldRetry
で定義しています。
定義した Backoff とリトライ対象を示す shouldRetry
から RetryFilter
を作成します。RetryFilter
を Client
として合成して、 Backoff されるリトライを行う Http クライアントを定義することができました。
以下のような定義済みの Backoff のストラテジーも用意されており、例えば Backoff.exponentialJittered
を使用すると、リトライ間隔を揺らぎを持たせながら指数関数的に増加させ、競合を減らしてリトライさせることができます。
- Backoff.linear
- Backoff.exponential
- Backoff.exponentialJittered
- Backoff.equalJittered
- Backoff.decorrelatedJittered
RetryBudget
RetryBudget は Leaky Token Bucket と呼ばれるモデルを使ってリトライの量を調整します。
Leaky Token Bucket は、トークンと呼ばれるデータが入るバケツです。トークンを入れる操作と取り出す操作ができ、ある操作から一定時間経過するとその操作で変動したトークン数が元に戻る動きをします。RetryBudget を定義すると、初期トークン数 reserve とトークンへ操作を戻す期間 ttl が設定された Leaky Token Bucket が内部的に作成されます。RetryBudget はリクエスト発行時にトークンを入れ (deposit) 、リトライ時にトークンを取り出します (withdraw) 。
以下の3つのパラメータから作成され、Bucket の reserve, ttl, 1 度のリトライで取り出されるトークン数 (withdrawAmount) を設定します。
- ttl
- percentCanRetry
- minRetriesPerSec
ttl は bucket の ttl に対応します。
percentCanRetry は depositAmount と withdrawAmount の割合です。例えば percentCanRetry が 0.1 だと、10 リクエストされて貯まるトークンで 1 回リトライができます。1 回のリクエスト発行で入れるトークン数 (depositAmount) は 1000 トークン固定になっているので、 percentCanRetry が 0.1 の場合、 withdrawAmount は 1000 * 10 = 10000 トークンになります。
minRetriesPerSec は bucket の reserve を決めます。ttl 期間にリトライできる最小数 (minRetriesPerSec * ttl) に withdrawAmount をかけた数が reserve になります。minRetriesPerSec が 5 、 ttl が 10 秒、 withdrawAmount が 10000 の場合だと、50 * 10000 になり、新しいリクエストを発行しない (deposit されない) 場合でも 10 秒間に 50 回リトライできることを表します。これでリクエストをしたばかりでも ttl 間にリクエストを多くしていないクライアントでもリトライが行われます。
Backoff と RetryBudget によるリトライを行う Http Client を定義したサンプルコードは以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
import com.twitter.conversions.time._ import com.twitter.finagle.Http import com.twitter.finagle.http.{Request, Response} import com.twitter.finagle.service._ import com.twitter.finagle.stats.NullStatsReceiver import com.twitter.finagle.util.DefaultTimer import com.twitter.util._ val shouldRetry: PartialFunction[(Request, Try[Response]), Boolean] = { case (r, Return(rep)) if rep.statusCode >= 500 => true } val retryBudget = RetryBudget( ttl = 10.seconds, minRetriesPerSec = 10, percentCanRetry = 0.2 ) val backoff = Backoff.equalJittered(5.millis, 200.millis) val retryPolicy = RetryPolicy.backoff(backoff)(shouldRetry) val retryFilter = new RetryFilter( retryPolicy = retryPolicy, timer = DefaultTimer.twitter, statsReceiver = NullStatsReceiver, retryBudget = retryBudget ) val client = Http.client .newService(host) val service = retryFilter andThen client |
RetryPolicy.tries(n, shouldRetry)
で、1クライアントの最大リトライ回数をn回に制限することもできます。
まとめ
Backoff ではリトライ間隔を調整して、RetryBudget ではリクエスト当たりにリトライできる回数、ある時間当たりにリトライできる回数を調整して、過度にリクエスト発行をする危険性を減らすことができます。
本番で活用する場合はサーバ側の性能やリトライ条件に合致する割合を見ながら、適切な Backoff, RetryBudget をチューニングする必要があります。 Backoff に関しては AWS Architecture Blog – Exponential Backoff And Jitter によると Backoff.exponentialJittered か Backoff.decorrelatedJittered アルゴリズムを採択することが良さそうです。
Finagle Client にはリトライ機能以外にも Load Balancer や Circuit Breaker など他にも面白い機能があるので興味のある方は触ってみてください。
Author