Let's Go Further محدودیت نرخ › پیکربندی محدودکننده‌های نرخ
قبلی · فهرست مطالب · بعدی
فصل ۱۰.۳.

پیکربندی محدودکننده‌های نرخ

در حال حاضر مقادیر درخواست‌های در ثانیه و burst ما به صورت سخت‌افزاری در middleware تابع rateLimit() کدنویسی شده‌اند. این خوب است، اما انعطاف‌پذیرتر می‌شد اگر این مقادیر در زمان اجرا قابل تنظیم بودند. به همین ترتیب، داشتن یک راه آسان برای غیرفعال کردن کامل محدودیت نرخ مفید خواهد بود (برای مثال، ممکن است بخواهید آن را هنگام اجرای بنچمارک یا انجام تست بار غیرفعال کنید و تمام درخواست‌ها از تعداد کمی آدرس IP بیایند).

برای قابل تنظیم کردن این موارد، بیایید به فایل cmd/api/main.go برگردیم و ساختار config و پرچم‌های خط فرمان را به این صورت به‌روزرسانی کنیم:

فایل: cmd/api/main.go
package main

...

type config struct {
    port int
    env  string
    db   struct {
        dsn          string
        maxOpenConns int
        maxIdleConns int
        maxIdleTime  time.Duration
    }
    // Add a new limiter struct containing fields for the requests-per-second and burst
    // values, and a boolean field which we can use to enable/disable rate limiting
    // altogether.
    limiter struct {
        rps     float64
        burst   int
        enabled bool
    }
}
...

func main() {
    var cfg config

    flag.IntVar(&cfg.port, "port", 4000, "API server port")
    flag.StringVar(&cfg.env, "env", "development", "Environment (development|staging|production)")

    flag.StringVar(&cfg.db.dsn, "db-dsn", os.Getenv("GREENLIGHT_DB_DSN"), "PostgreSQL DSN")

    flag.IntVar(&cfg.db.maxOpenConns, "db-max-open-conns", 25, "PostgreSQL max open connections")
    flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections")
    flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time")

    // Create command-line flags to read the settings values into the config struct. 
    // Notice that we use true as the default for the 'enabled' setting.
    flag.Float64Var(&cfg.limiter.rps, "limiter-rps", 2, "Rate limiter maximum requests per second")
    flag.IntVar(&cfg.limiter.burst, "limiter-burst", 4, "Rate limiter maximum burst")
    flag.BoolVar(&cfg.limiter.enabled, "limiter-enabled", true, "Enable rate limiter")

    flag.Parse()

    ...
}

...

و سپس بیایید middleware تابع rateLimit() خود را برای استفاده از این تنظیمات به این صورت به‌روزرسانی کنیم:

فایل: cmd/api/middleware.go
package main

...

func (app *application) rateLimit(next http.Handler) http.Handler {
    // If rate limiting is not enabled, return the next handler in the chain with
    // with no further action.
	if !app.config.limiter.enabled {
		return next
	}

	type client struct {
		limiter  *rate.Limiter
		lastSeen time.Time
	}

	var (
		mu      sync.Mutex
		clients = make(map[string]*client)
	)

	go func() {
		for {
			time.Sleep(time.Minute)

			mu.Lock()

			for ip, client := range clients {
				if time.Since(client.lastSeen) > 3*time.Minute {
					delete(clients, ip)
				}
			}

			mu.Unlock()
		}
	}()

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ip := realip.FromRequest(r)

		mu.Lock()

		if _, found := clients[ip]; !found {
			clients[ip] = &client{
                // Use the requests-per-second and burst values from the config
                // struct.
				limiter: rate.NewLimiter(rate.Limit(app.config.limiter.rps), app.config.limiter.burst),
			}
		}

		clients[ip].lastSeen = time.Now()

		if !clients[ip].limiter.Allow() {
			mu.Unlock()
			app.rateLimitExceededResponse(w, r)
			return
		}

		mu.Unlock()

		next.ServeHTTP(w, r)
	})
}

پس از اتمام کار، بیایید این را با اجرای API با پرچم -limiter-burst و مقدار burst کاهش یافته به ۲ امتحان کنیم:

$ go run ./cmd/api/ -limiter-burst=2
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

اگر دوباره یک دسته شش درخواست را با سرعت زیاد ارسال کنید، باید اکنون متوجه شوید که فقط دو درخواست اول موفق هستند:

$ for i in {1..6}; do curl  http://localhost:4000/v1/healthcheck; done
{
    "status": "available",
    "system_info": {
        "environment": "development",
        "version": "1.0.0"
    }
}
{
    "status": "available",
    "system_info": {
        "environment": "development",
        "version": "1.0.0"
    }
}
{
    "error": "rate limit exceeded"
}
{
    "error": "rate limit exceeded"
}
{
    "error": "rate limit exceeded"
}
{
    "error": "rate limit exceeded"
}

به همین ترتیب، می‌توانید با استفاده از پرچم -limiter-enabled=false محدودکننده نرخ را کلاً غیرفعال کنید:

$ go run ./cmd/api/ -limiter-enabled=false
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established"
time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development

و باید متوجه شوید که اکنون تمام درخواست‌ها با موفقیت تکمیل می‌شوند، هر تعداد که ارسال کنید.

$ for i in {1..6}; do curl http://localhost:4000/v1/healthcheck; done
{
    "status": "available",
    "system_info": {
        "environment": "development",
        "version": "1.0.0"
    }
}
...
{
    "status": "available",
    "system_info": {
        "environment": "development",
        "version": "1.0.0"
    }
}