LoginSignup
4
2

More than 1 year has passed since last update.

Golangはじめて物語(第11話: Gin+time/rateでお手軽に流量制御を行う)

Posted at

はじめに

以前書いたSpringBoot編で触れたとおり、マイクロサービスなシステムを作っていく上で、RateLimitは重要な要素になってくる。
Golangでは標準ライブラリでRateLimitが実装できるものが容易されているので、今回はそれをGinに組み込んで、お手軽に流量制御をやってみよう。

なお、記事執筆時点でGolangのランタイムは1.13を使っている。

RateLimitライブラリ

RateLimitを行うライブラリは、公式ドキュメントを参考にしよう。
このライブラリは、トークンバケットアルゴリズムを用いているので、まずはこの基本を押さえておこう。
time/rateには、Allow()/Reserve()/Wait()という3種類のトークンコンシューマーがあるが、Wait()してしまうとクライアントを待たせてしまうため、HTTPサーバとしての流量制御を実装する場合は、Allow()かReserve()を用いる。細かい制御をしないのであれば、Allow()が良いだろう。

基本のHTTPサーバ

まずは、time/rateを実装する前の基本形を作っておこう。

main.go
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を実装していこう。

コードの修正

ソースコードは以下のように変更しよう。

main.go

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()に渡すパラメータを制御することで、柔軟な流量制御が可能になる。

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2