https://github.com/throttled/throttled と同じ GCRA アルゴリズムを実装(移植)した https://github.com/viafintech/gcra-ruby/tree/master には正式な mem_store 実装はないけどテストコードの中に実質 mem_store な実装がある。しかしそれを使ってレートリミットを実装してみると、時間が経過しても回復しない。
class TestStore attr_accessor :now attr_accessor :fail_sets def initialize @now = 0 @data = {} @fail_sets = false end def get_with_time(key) return @data[key], @now end def set_if_not_exists_with_ttl(key, value, ttl) return false if fail_sets if @data.has_key?(key) return false end @data[key] = value return true end def compare_and_set_with_ttl(key, old_value, new_value, ttl) return false if fail_sets if @data[key] != old_value return false end @data[key] = new_value return true end end
#!/usr/bin/env ruby require 'gcra/rate_limiter' require_relative 'test_store' store = TestStore.new rate_period = 0.5 # Two requests per second max_burst = 10 # Allow 10 additional requests as a burst limiter = GCRA::RateLimiter.new(store, rate_period, max_burst) p limiter x = 0 loop do exceeded, info = limiter.limit('foo', 1) pp [exceeded, info] if exceeded x += 1 end if x > 50 break end sleep(rand(0.01..0.02)) end
❯ ruby mem.rb #<GCRA::RateLimiter:0x000000010a8091d8 @store=#<TestStore:0x000000010a8092a0 @now=0, @data={}, @fail_sets=false>, @emission_interval=500000000, @delay_variation_tolerance=5500000000, @limit=11> [false, #<struct GCRA::RateLimitInfo limit=11, remaining=10, reset_after=0.5, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=9, reset_after=1.0, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=8, reset_after=1.5, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=7, reset_after=2.0, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=6, reset_after=2.5, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=5, reset_after=3.0, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=4, reset_after=3.5, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=3, reset_after=4.0, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=2, reset_after=4.5, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=1, reset_after=5.0, retry_after=nil>] [false, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=nil>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>] [true, #<struct GCRA::RateLimitInfo limit=11, remaining=0, reset_after=5.5, retry_after=0.5>]
TestStore のなかで ttl を無視してるからだと思うが、不思議なのは Go 実装の方の mem_store も ttl は無視しているのに、それを使ったレートリミットの実装ではちゃんと回復するところ。
https://github.com/throttled/throttled/blob/master/store/memstore/memstore.go
↓このコードは GOでGCRAレートリミット - aptpod Tech Blog から拝借したものを改変した。
package main import ( "flag" "fmt" "log" "time" "github.com/throttled/throttled/v2" "github.com/throttled/throttled/v2/store/memstore" ) func main() { var ( maxRate int burst int interval time.Duration ) flag.IntVar(&maxRate, "r", 100, "") flag.IntVar(&burst, "b", 2, "") flag.DurationVar(&interval, "i", time.Second, "") flag.Parse() store, err := memstore.New(65536) if err != nil { log.Fatal(err) } quota := throttled.RateQuota{ MaxRate: throttled.PerSec(maxRate), MaxBurst: burst, } rateLimiter, err := throttled.NewGCRARateLimiter(store, quota) if err != nil { log.Fatal(err) } ticker := time.NewTicker(interval / 100) defer ticker.Stop() for i := 0; i < 100; i++ { ng, res, err := rateLimiter.RateLimit("sample", 1) if err != nil { log.Fatal(err) } if ng { fmt.Printf("NG res = %+v\n", res) continue } fmt.Printf("OK res = %+v\n", res) } }
go mod init go mod tidy go run main.go
❯ go run main.go | head OK res = {Limit:3 Remaining:2 ResetAfter:10ms RetryAfter:-1ns} OK res = {Limit:3 Remaining:1 ResetAfter:19.95ms RetryAfter:-1ns} OK res = {Limit:3 Remaining:0 ResetAfter:29.947ms RetryAfter:-1ns} NG res = {Limit:3 Remaining:0 ResetAfter:29.942ms RetryAfter:9.942ms} NG res = {Limit:3 Remaining:0 ResetAfter:29.94ms RetryAfter:9.94ms} NG res = {Limit:3 Remaining:0 ResetAfter:29.938ms RetryAfter:9.938ms} NG res = {Limit:3 Remaining:0 ResetAfter:29.936ms RetryAfter:9.936ms} NG res = {Limit:3 Remaining:0 ResetAfter:29.934ms RetryAfter:9.934ms} NG res = {Limit:3 Remaining:0 ResetAfter:29.932ms RetryAfter:9.932ms} NG res = {Limit:3 Remaining:0 ResetAfter:29.931ms RetryAfter:9.931ms}
こちらは一応ちゃんと ResetAfter と RetryAfter の値が変化しているのでちゃんと動いているように見える。
どこに違いがあるのか、Go の mem_store のどこが Ruby の TestStore と意味的に違うのかがわからない。