はじめに
以前書いたSpringBoot編で触れたとおり、マイクロサービスなシステムを作っていく上で、RateLimitは重要な要素になってくる。
Golangでは標準ライブラリでRateLimitが実装できるものが容易されているので、今回はそれをGinに組み込んで、お手軽に流量制御をやってみよう。
なお、記事執筆時点でGolangのランタイムは1.13を使っている。
RateLimitライブラリ
RateLimitを行うライブラリは、公式ドキュメントを参考にしよう。
このライブラリは、トークンバケットアルゴリズムを用いているので、まずはこの基本を押さえておこう。
time/rateには、Allow()/Reserve()/Wait()という3種類のトークンコンシューマーがあるが、Wait()してしまうとクライアントを待たせてしまうため、HTTPサーバとしての流量制御を実装する場合は、Allow()かReserve()を用いる。細かい制御をしないのであれば、Allow()が良いだろう。
基本のHTTPサーバ
まずは、time/rateを実装する前の基本形を作っておこう。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type body struct {
Detail string `json:"detail"`
}
func example() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, body{
Detail: http.StatusText(http.StatusOK),
})
}
}
func initRouter() *gin.Engine {
router := gin.Default()
router.GET("/example", example())
return router
}
func main() {
router := initRouter()
router.Run(":8080")
}
お手軽!このWebサーバを起動してcurl -i http://localhost:8080/example
すると、
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sat, 09 Apr 2022 09:34:49 GMT
Content-Length: 15
{"detail":"OK"}
という結果を得られる。
さて、ではここにRateLimiterを実装していこう。
コードの修正
ソースコードは以下のように変更しよう。
package main
import (
"net/http"
"time" // ★追加
"github.com/gin-gonic/gin"
"golang.org/x/time/rate" // ★追加
)
type body struct {
Detail string `json:"detail"`
}
var ( // ★追加
interval = 2 // ★追加
burst = 2 // ★追加
limit = rate.NewLimiter(rate.Every(time.Duration(interval)*time.Second), burst) // ★追加
) // ★追加
func example() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, body{
Detail: http.StatusText(http.StatusOK),
})
}
}
func rateLimiter() gin.HandlerFunc { // ★追加
return func(c *gin.Context) { // ★追加
if limit.Allow() == false { // ★追加
c.JSON(http.StatusTooManyRequests, body{ // ★追加
Detail: http.StatusText(http.StatusTooManyRequests), // ★追加
}) // ★追加
c.Abort() // ★追加
} // ★追加
} // ★追加
} // ★追加
func initRouter() *gin.Engine {
router := gin.Default()
router.Use(rateLimiter()) // ★追加
router.GET("/example", example())
return router
}
func main() {
router := initRouter()
router.Run(":8080")
}
ポイントになるのは、router.Use(rateLimiter())
でミドルウェアとして実装するところだ。
Ginのミドルウェアについては、こちらの記事が詳しいので参照していただければ。
実際にプロダクションコードを作る場合、ヘルスチェックのパスがRateLimitの対象になっては困るので、
router.GET("/example", rateLimiter(), example())
と実装したり、上記記事のGroupを用いるのが良いだろう。
あとは、
limit = rate.NewLimiter(rate.Every(time.Duration(interval)*time.Second), burst)
で初期化したRateLimiterを、limit.Allow()
で呼んで判定してあげれば良い。
なお、NewLimiterの引数は、第一引数が「トークンを補給するレート」だ。実際にはトークンバケットアルゴリズムにおける1/rのrを渡すことになる。
分かりにくいので、rate.Every()
を使うことで、rを直接渡すことが可能だ。
今回のコードの例では、interval
が2なので、2秒に1トークンが補給されるが、burst
である2を最大として補給するという意味になる。(つまり、最大までトークンを補給するには4秒かかる)
動かしてみる
このコードでWebサーバを起動して、同様にcurlで投げ込んでみると、以下のようなログを出力する。
[GIN] 2022/04/09 - 18:50:45 | 200 | 69.4µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:45 | 200 | 33.6µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:45 | 429 | 36.5µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:46 | 429 | 35µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:46 | 429 | 37.7µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:46 | 429 | 27.5µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:46 | 429 | 21.5µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:46 | 429 | 22.3µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:46 | 429 | 21.9µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:46 | 429 | 41.3µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:47 | 429 | 22.1µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:47 | 429 | 22.7µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:47 | 429 | 21.2µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:47 | 200 | 57.7µs | 127.0.0.1 | GET "/example"
[GIN] 2022/04/09 - 18:50:47 | 429 | 26.4µs | 127.0.0.1 | GET "/example"
設定した通り、最初の2トランザクションでトークンを使い果たし、2秒後に1件補給されるが、1件正常応答後は補給町の状態になって"Too Many Requests"を応答する。
rate.NewLimiter()
に渡すパラメータを制御することで、柔軟な流量制御が可能になる。