APIレートリミット技術が高度化 多様な制御方式と実装例が業界で注目集める

APIレートリミットの基本概念をわかりやすく解説

[# 429番台を乗りこなすには:Django REST FrameworkのAPIスロットリング小話

こんにちは、開発者の皆さん。写真は[ファイサル]さん撮影(Unsplashより)。今日はあまり注目されないけど、API設計において割と大事かもしれない「レート制限」について触れてみるつもりです。信号機って道路でそこそこ役立っていますが、それと似たような感じで、APIにも時々ルールが必要になることがありますね。そういう仕組みのおかげで、動作が重くなったり、不公平になったり、ちょっとした悪用につながるのを防ぎやすくなる…そんな印象です。

さて、「レートリミット」って言葉を耳にしたことがある人もいるでしょうし、全然知らない方もいるかもしれません。例えば公開APIを運営していた場合―どこかの誰かがメッセージ投稿できたりするサービスだとして―突然大量にアクセスされちゃうことだってなくはありません。大体の場合、一度に押し寄せるリクエスト数は七十件とかそれ以上なんてことも珍しくなくて…。システム的には負担になるし、他の利用者からしたら理不尽さを感じてしまうケースも出てきます。この辺りについては色んな考え方がありますが、何となくトラフィック整理の一部…そんな風にも例えられる気がします。

実際には細かな手法や設定の方法など幾つかあって、その中身を全部覚える必要はないんですが、Django REST Frameworkでは「APIベース」「モデルベース」どちらでも対応できるっぽいです。それぞれ長所短所はいろいろ語られるんですが、とりあえず基本だけでも知っておけば困らないかもしれません。全部きっちり理解しなくても、おおよそこんなものかな?くらいの感覚でいいと思いますよ。

サーバーを守る5つのレート制限戦略を比較

どこかの誰かがスクリプトを作って、APIに対して一秒間に千回近くアクセスし始めたとする。そんなことが起きると、もし制限とか何もなければサーバーはすぐにパンクしそうだし、ほかの人たちが投稿したいと思ってもできなくなる場合もあるみたい。レート制限という考え方は、一人の利用者やクライアントが短い時間内に送れるリクエスト数を決めておく仕組みらしい。例えば、ほんの数分で百回程度までとか、そのくらい曖昧な感じ。

もしこの上限を越えてしまうと、「429 Too Many Requests」というHTTPレスポンスで返されることが多い。でも実際には、状況によっては違う反応になる時もあるようだ。

こうした仕組みによってAPIへのアクセスは無理なく調整されていて、大勢のユーザーが公平に使えるようになりやすいと言われている。ただし絶対的な効果とは言い切れず、ケースバイケースで役立つ面もあれば課題も残るらしい。サーバーへの負荷もある程度抑えられて、結果的にパフォーマンス面や拡張性にも良い影響が出ることが観察されている。まあ、ときどき想定外の動きも起こるので油断は禁物かもしれない。

Comparison Table:
トピック内容効果
APIリクエスト制限トークンバケット方式や漏れバケツ方式を使用することで、リクエストの処理を効率化し、負荷を分散させる。急激なアクセス増加によるサーバー負荷の軽減。
Django REST Framework (DRF)DEFAULT_THROTTLE_CLASSESを設定し、匿名ユーザーや持続的利用者向けに異なるスロットリングポリシーを適用する。公平なアクセス管理とサービスの安定性が向上。
カスタムスロットルクラスBurstRateThrottleやSustainedRateThrottleなどで細かな制御が可能になる。特定のビジネスニーズに応じた柔軟なレート制限が実現できる。
ユーザーごとの操作制限saveメソッドで特定の条件に基づきレート制限を設けることができる仕組みがある。データ変更操作への過剰アクセス防止とビジネスロジックの保護。
予防策としての重要性急激なアクセス増加は予測困難なので、事前に対策を講じておくことが推奨される。本番環境での混乱回避と安定したサービス提供につながる。

サーバーを守る5つのレート制限戦略を比較

固定ウィンドウ方式でシンプルにリクエスト数を管理

「どうやって導入するか」といった話題って、実際に体験したことがある人じゃなくても、何となく想像できる部分もあると思うんです。たとえば、使う人数が七~八人程度のサービスだとして――いや、それより多いかもしれないですが――ユーザーごとにリクエスト回数を制限したい場合。おそらく「一分間で五回くらいまでならOK」みたいなルールを設けることになるでしょう。ただ、五という数字自体はあまり厳密ではなくて、「少し多めのリクエスト」でブロックされる感じです。

もし仮に、その時間内で誰かが許容されている以上のアクセスを送ってきた場合は、その後の通信が拒否される仕組みになります。でも実際には、「API呼び出している人」と言われてもピンとこない方もいるかもしれませんね。一応ここでは『クライアント』という単語でまとめていますけど、これは大抵の場合ログイン済みユーザーだったりします。

さて、こういう制御方法について調べてみると、多分五つ前後の代表的な戦略が挙げられるようです。その中でも最初に思いつくやつ――「固定ウィンドウカウンター」という方式があります。ざっくり言えば、一分単位でユーザーごとのリクエスト数を数えておいて、新しい区切り(例えば新しい一分)が始まった瞬間にそのカウンターをゼロに戻す形ですね。それ以外にも色々手法はありますが、とりあえずこの辺から試してみても損はなさそうです。

スライディングウィンドウログで精密なトラッキング

時計の針が進むたび、だいたい一分くらいの幅で何かしら制限を設けたいとき、誰かが昔から色々なやり方を考えてきたようだ。今も、ユーザーごとに大体五回前後のリクエストまで許可する仕組みが使われていることが多い気がする。

ある人はこんな話をしていた。時刻をざっくり区切って、その区間ごとにキーを作る方法。キャッシュとかメモリのどこかに「ユーザーID:その瞬間の窓」みたいな文字列で記録しておくんだって。ただ、この方式では細かなタイミングまでは見れないから、多少多めに通過しちゃう場合もあるとかないとか。

他にもね、「最近一分くらい」のログだけ残すっていう方法も耳にしたことある。つまり、IDごとに直近の履歴(それも数件くらい)だけメモしておいて、新しいアクセスが来たら古いものを消して、まだ上限より少ないなら追加。もし溢れてたら…まあ、その時は断るしかないという感じ。

もうひとつ、小さく区切ったバケツみたいなもの(時間的な箱?)でカウントする手法も聞いた覚えがある。でも細かい部分はあまり知られてなくて、実際どうやって動いてるかまでは曖昧だったり。

全体的には、「絶対こうすれば大丈夫」というより、その場その場で調整しながら使われている印象かなぁ。状況によっては違うアプローチも選ばれることが多そうだし、一つの正解があるわけじゃなさそうと思ったりする。

スライディングウィンドウログで精密なトラッキング

バケット分割で柔軟なカウント処理を実現

時間をざっくり十秒くらいで区切って、それぞれのかたまりごとにリクエスト数をカウントしていくみたいなやり方があるらしい。六十秒分くらい、つまり六回分の区間をさかのぼって合計を出すことになるんだけど、実際には一つずつ確認しながら足していく感じだろうか。ユーザーごとにキーを作る仕組みになっていて、「今この瞬間」みたいな秒数で区切った番号が振られているっぽい。ただ、この方法だと制限数はせいぜい片手で数えられる程度だった気がする。

それから、どうも現在のバケット、つまり今進行中の区間にも新しいリクエストが入った時点で増やす形になってるっぽい。何というか、全体的に厳密さより大まかな管理に近い印象。ちなみに「トークンバケット」と呼ばれている方式もあって、そちらはバケツみたいなものにトークン(たぶん五個前後?)をためておき、一回使うごとに一枚減るようなイメージだったと思う。

でもこれ、本当に最適なのかどうかはまだよくわからない部分も多そうだし、人によっては違うやり方を選ぶこともあるとか聞いた記憶がある。まあ環境によって向き不向きとか出てきそうなので、一概には言えないところかな。

トークンバケット方式で突発トラフィックに対応

何となく、トークンが少しずつ増えていく仕組みがあった気がする。例えば十二秒くらい経つと、ほんの一個分ぐらいチャージされるような感じで。全部で五つ前後までしか貯まらないみたいだったけど、その上限に達するともうそれ以上は増えないらしい。

時々、「今って使っていいタイミングなのかな」とか考えながら、時計をチラ見したりもするんだけど…まあ、大体の場合はちょっと待てばまた使えるようになる。いつも正確に覚えてるわけじゃないけど、おおよそそんな印象。

リクエストの処理についても似たような話があるみたいで、とにかく順番待ちの列みたいなものに詰め込まれて、それからゆっくり、一件ずつ漏れ出していくイメージかな? だいたい十数秒ごとに一件くらい外へ流れるっていう話を聞いたことがある。でも、その数字自体には多少幅があるかもしれないし、状況によって前後することもあるとか。

こういう仕組みのおかげで、急激な負荷を和らげたりできる場合もありそうだけど、絶対的な効果とは言い切れない場面もありそうだね。実際は他にも色んな要素や設定次第で挙動が変わるっぽいし、一概には語れないのかもしれない。

トークンバケット方式で突発トラフィックに対応

リーキーバケットで安定した処理速度を確保

キューがいっぱいになったら、新しいリクエストはしばらく止めてしまう。なんとなく、この仕組みは五つ前後の枠を持っているような感覚がある。漏れバケツ方式、というやつだろうか。ただ、秒数にして十ちょっとくらいの間隔で古いものが消えるんじゃないかなと、そんな印象も残る。

それでね、APIの利用制御って色々あるけど、人によっては固定ウィンドウで十分な場面も見かけることがある。アクセスが多くない公開系のAPIとかなら、それで大きな問題にはならない場合もちらほら。一方で、公平性をその場その場できちんと保ちたい時は、スライディングログとかスライディングウィンドウカウンターみたいな手法を選ぶ人もいるようだ。

スマートフォンアプリだったり、一度にドンとアクセスが集中しやすいところではトークンバケット方式を取り入れるケースも耳にしたことがある。安定して流量をコントロールしたい、と考えるなら漏れバケツが合う場合もありそう。もちろん、状況次第で話は変わるので、「これだけやれば完璧」というよりは、その都度検討されている感じかなぁ。

Django REST Frameworkでの実装方法を公開

Django REST Framework(略してDRF)でAPIのリクエスト制限を仕込む話、まあ何かの機会にちょっと見たことがある人もいると思う。設定自体はそこまで複雑じゃなくて、「DEFAULT_THROTTLE_CLASSES」っていうキーにクラスを並べたりするやつ。例えば匿名ユーザー用とか、急激なアクセス防止みたいなものとか、持続的な利用者向けのやつ…だいたい三種類くらい組み合わせるケースが多いらしい。

細かい数字で語るのは苦手なんだけど、例えば一分あたり十回にも満たない頻度で匿名さんが叩く場合は特に問題ない感じ。それよりちょっと多め、一分間に二十回前後なら「バースト」っていう扱いになるし、もう少し長期的な制御として「一日に百回近く」みたいな枠組みもある(もちろん正確には違うかもしれないけど、大体そんなイメージ)。

実際、この仕組みではスコープごとにレート制限ができて、もし上限を超えちゃうとHTTP429番台エラー(Too Many Requests)が返されるんだとか。ちなみに、そのままじゃ飽き足りない開発者もいて、「モデル単位でもっと細かく管理したい」と思ったら別のアプローチもあるっぽい。

カスタムスロットルクラスという名前のものが出てきたりもする。「BurstRateThrottle」とか「SustainedRateThrottle」って響き、専門書っぽさあるけど、中身は意外とシンプル。スコープ名だけ変えて使う感じかな?よく似たコードが転がってる印象。

まあ全部DRFのお作法通り進めば、おおよそ困ることは少ない気がする。ただ、本当に微妙な調整や独自仕様まで踏み込みたい場面では別方法を選ぶ人もいるらしい――そんな話を聞いた覚えもあるし、自分で検証したわけじゃないから断言はできない。でも、ときどきこういう工夫を見ると、設計次第で色々柔軟にできそうだなあとぼんやり思ったことがあったような…。

Django REST Frameworkでの実装方法を公開

モデルレベルできめ細かい制御を行う方法

たとえば、APIのコール回数だけじゃなくて、本当にデータが変わる操作――つまり「作ったり」「編集したり」みたいな動き――に対しても、ユーザーごとに制限をかけたい時ってあるんだろうな。どうやら、そのための仕組みとしてこんなPythonコードが出回っているらしい。

中身をざっと見ると、「20回ぐらいで1分くらい」とか、「約10回程度で1分間」とか、そんな感覚で“何分以内に何回までOK”という設定ができるみたい。正確な数値ではないけど、おおまかにはそれくらい。

この仕組みはsaveメソッドのところでチェックする設計になっていて、もしその決められた上限っぽいものを越えてしまうと、エラーっぽい例外(429番台あたりかな?)が返されるようになっている。キャッシュ機構も使われていて、多分Django標準のやつなんじゃないかな。ユーザーIDごとにカウントして記録している感じがする。ただ、そのユーザー識別方法はモデルによって違うから、get_thottle_user_pk()みたいなメソッドで上書きできる余地が残されていたり。

ちなみに、このMixinクラス自体は、それ単独よりもモデル側で継承させて使う想定なのかも。具体的には、ExampleModelとかいう名前のクラス例が示されていて、その中でレート制限の設定を書き換えたりもできる様子。でも実際どこまで複雑な条件を付け足せるかはよくわからない。

全体的には、“特定のビジネスロジックだけ守りたい”というニーズ向けかな。API全体じゃなく、一部だけ緩めたり締めたりしたい場合にも応用しやすそう。ただし絶対安全とは言えず…本当にこれだけですべて守れる保証はちょっと見当たらない気がする。

思えばこういう工夫は昔からあった気もするけど、今だと内部サービスでも外部公開APIでも、ときどき必要になる場面があるっぽい。その意味では「一つの防御策」と考える人もいそう。ただし過信せず、他の手段とも併用したほうが安心なのかもしれないね。

本番環境で後悔しないためのベストプラクティス

DRFでのスロットリング、まあまずは標準で備わってるやつを使ってみるといいかもしれません。そこから、もっと細かい制御が必要かな?と思ったら、モデルに直接絡めてミックスインとか組み合わせても悪くない感じです。どこかで七十回以上は同じような話を聞いた気もしますが、本番環境が混乱してから対策するより先にやっとく方が落ち着きます。

……あれ、これ前にも誰か言ってたような。でも実際、急激なアクセス増加とかって予測しづらいんですよね。ほぼ半分くらいのケースでは何事もなく終わりますけど、ごく一部で想定外の負荷になったという話も耳にします。

さて、この辺りで一旦区切ります。また気になることあれば、その時にでも。何となくですが、今日はこのへんで失礼します。

Related to this topic:

Comments