ja ja ja de ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja eu ja ja ja zh es ja en ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja en ja ja ja ja en nl ja de ja ja nb ja ja nb ja ja nb ja ja en ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja cv es es ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja ja
このような制限をかけることでクライアントに「ちょっとずつAPIを使う」ことを強いることができます。
ではこれをどうやって実装すると良いでしょうか。素朴に実装しようとすると、クライアントごとにバケツの水量を記録しておき、API利用時にそれを増加させ、一定時間ごとに減少させるような形になるでしょう。しかしそれだと「一定時間ごとに減少させる」のコストが高くなってしまいます。
例えば「1分毎に各クライアントの水を100減らす」という実装をしたとします。このときクライアントの数が多くなると、1分ではすべての処理が完了しないかもしれません。つまりこの実装はクライアントが多い場合は現実的ではありません。
同等のアルゴリズムで「一定時間ごとに減少させる」を不要にしたのがGCRA(Generic Cell Rate Algorithm)です
GCRAは計算不要で自動的に増える値である「現在時間」を使って、時間を計算することでAPIの利用可否を判断します。
GCRAは水ではなくセルという名前をつかいますが、アルゴリズム的には同じです。他にもいくつか覚えないといけない用語があります。
理論到着時間(TAT)
Tごとにセルが到着すると仮定したときの到着時間 。APIが実行されるたびにTの分加算される。現在時刻が加算前のTATより大きい場合もしくは初回は現在時刻+TがTATになる
バケットの時間的な最大容量(バケットの最大容量 * T)
この用語でアルゴリズムを説明すると、「TAT-tが現在時刻以下であればAPI利用可能」となります。
具体的な数値をいれないとよくわからないですよね。仮の数値を入れて考えてみましょう。
バケットの最大容量2セル
1分毎に1のセルが放出される
APIをそれぞれ12:00:00, 12:00:01, 12:00:02に実行した
12:00:00のAPI実行時
TATは12:01(現在時刻+T)
TAT - tが12:01 - 2分 = 11:59となり、現在時刻(12:00:00)より前なのでAPI実行可能
12:00:01のAPI実行時
TAT - tが12:02 - 2分 = 12:00となり、現在時刻(12:00:01)より前なのでAPI実行可能
12:00:02のAPI実行時
TAT - tが 12:03 - 2分 = 12:01となり、現在時刻(12:00:02)より後なのでAPI実行不可。実行するには12:01まで待つ必要がある
「なんかいい感じに均等にAPI利用制限できるアルゴリズムがあり、その名前はleakey bucket(もしくはGCRA)である」と覚えておいてください
使ってみたいけどイチから実装するのめんどくさいのでgemないですか
Sidekiq Enterpriseのv2.2から、GCRAのアルゴリズムで制限をかける機能が使えるようになっています。もともとSidekiq Enterpriseの機能として利用制限をかけるものはあったのですが、それにGCRA(leaky bucket)用のアルゴリズムが追加された形です。この機能はsidekiqのワーカ以外でも使うことができます。
次のようなコード(The Leaky Bucket rate limiter | Mike Perhamから引用)をRack Middlewareとして利用すると簡単にAPIサーバにleaky bucketな制限をかけることができます。
class LeakyLimiter def initialize(app) @app = app end def call(env) remote_ip = env["REMOTE_ADDR"].tr(':', '_') begin limiter = Sidekiq::Limiter.leaky("ip-#{remote_ip}", 10, 10, wait_timeout: 0) limiter.within_limit do @app.call(env) end rescue Sidekiq::Limiter::OverLimit => ex [429, { "Content-Type" => "text/plain", "X-Rate-Limited-For" => ex.limiter.next_drip.to_s, }, ["Rate limited"]] end end end
もしこれから外部にAPIを公開していくぞ!となったときにこの話を思い出してみてください。
だいたいこのような感じで毎週なにかしら共有をしています。自分も参加してみたい!という方はこちらのリンクからどうぞ。
*1:毎週ネタをひねり出すのに苦労しています><
willnet 2020-12-11 10:00
Railsアプリケーションのテストが失敗したときにどうしたらいいのか
本エントリはiCARE Advent Calendar 2020の25日目です。 僕はiC…
ginza.rb 第33回で Rails 5 の新機能について学んだ
3/15は ginza.rb の33回目のミートアップでした。 Ginza.rb 第3…
Rails で DB の Time 型を扱う
Rails では、DB の Time 型 を扱うことができます。これは日付…
Rails 4.1.0 で新しく導入された便利メソッド
Rails(ActiveSupport) は標準クラスを拡張した便利メソッド群を…
エラー通知用ウェブサービス airbrake のメモ
エラー通知用のウェブサービス Airbrake の ruby 用プラグイン …
« Railsアプリケーションのテストが失敗した… iCARE Dev Meetup #12 で登壇した »
id:willnet はてなブログPro
べんりで楽しいwebサービスをたくさん作りたい
読者です 読者をやめる 読者になる 読者になる
@netwillnetをフォロー
失敗したテストのログだけを出力するぞ
iCARE Dev Meetup #34で登壇しました
`Thread.current[:hoge]`はスレッドローカル変数を参照していると思いきや実際はファイバーローカル変数だった