محدودیت نرخ جهانی
بیایید آرام آرام پیش برویم و با ایجاد یک محدودکننده نرخ جهانی برای برنامه خود شروع کنیم. این محدودکننده تمام درخواستهایی که API ما دریافت میکند را در نظر میگیرد (به جای اینکه برای هر کلاینت به صورت جداگانه محدودکننده نرخ داشته باشیم).
به جای نوشتن منطق محدودیت نرخ از صفر — که کار نسبتاً پیچیده و زمانبری است — میتوانیم از پکیج x/time/rate استفاده کنیم. این پکیج یک پیادهسازی آزمایش شده از محدودکننده نرخ سطل توکنی را ارائه میدهد.
اگر قصد دارید همراه با ما پیش بروید، لطفاً آخرین نسخه این پکیج را با دستور زیر دانلود کنید:
$ go get golang.org/x/time/rate@latest go: downloading golang.org/x/time v0.12.0 go: added golang.org/x/time v0.12.0
پیش از شروع نوشتن کد، بیایید لحظهای صرف توضیح نحوه کار محدودکنندههای نرخ سطل توکنی کنیم. مستندات رسمی x/time/rate میگوید:
یک Limiter فرکانس وقوع رویدادها را کنترل میکند. این یک «سطل توکنی» با اندازه
bپیادهسازی میکند که در ابتدا پر است و با سرعتrتوکن در ثانیه پر میشود.
این را در زمینه برنامه API خودمان در نظر بگیریم...
- یک سطل خواهیم داشت که با
bتوکن شروع میشود. - هر بار که یک درخواست HTTP دریافت میکنیم، یک توکن از سطل خارج میکنیم.
- هر
1/rثانیه، یک توکن به سطل اضافه میشود — تا حداکثرbتوکن در مجموع. - اگر درخواست HTTP دریافت کنیم و سطل خالی باشد، باید پاسخ
429 Too Many Requestsبرگردانیم.
در عمل، این به این معناست که برنامه ما اجازه میدهد حداکثر b درخواست HTTP به صورت پشت سر هم ارسال شوند، اما در درازمدت به طور میانگین r درخواست در ثانیه مجاز خواهد بود.
برای ایجاد یک محدودکننده نرخ سطل توکنی از x/time/rate، باید از تابع NewLimiter() استفاده کنیم. امضای تابع به این شکل است:
// Note that the Limit type is an 'alias' for float64. func NewLimiter(r Limit, b int) *Limiter
پس اگر بخواهیم محدودکننده نرخی ایجاد کنیم که به طور میانگین ۲ درخواست در ثانیه مجاز باشد و حداکثر ۴ درخواست در یک «انفجار» مجاز باشد، میتوانیم با کد زیر این کار را انجام دهیم:
// Allow 2 requests per second, with a maximum of 4 requests in a burst. limiter := rate.NewLimiter(2, 4)
اعمال محدودیت نرخ جهانی
خب، حالا که توضیحات کلی را پشت سر گذاشتیم، بیایید وارد کدها شویم و ببینیم در عمل چگونه کار میکند.
یکی از مزایای الگوی مiddleware که استفاده میکنیم این است که به راحتی میتوان کد «مقداردهی اولیه» را اضافه کرد که فقط یک بار زمانی اجرا میشود که چیزی را با middleware بپیچانیم، به جای اینکه برای هر درخواستی که middleware مدیریت میکند اجرا شود.
func (app *application) exampleMiddleware(next http.Handler) http.Handler { // Any code here will run only once, when we wrap something with the middleware. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Any code here will run for every request that the middleware handles. next.ServeHTTP(w, r) }) }
در مورد ما، یک متد middleware جدید rateLimit() ایجاد میکنیم که یک محدودکننده نرخ جدید را به عنوان بخشی از کد «مقداردهی اولیه» میسازد و سپس از این محدودکننده نرخ برای هر درخواستی که بعداً مدیریت میکند استفاده میکند.
اگر قصد دارید همراه با ما پیش بروید، فایل cmd/api/middleware.go را باز کنید و middleware را به این شکل ایجاد کنید:
package main import ( "fmt" "net/http" "golang.org/x/time/rate" // New import ) ... func (app *application) rateLimit(next http.Handler) http.Handler { // Initialize a new rate limiter which allows an average of 2 requests per second, // with a maximum of 4 requests in a single 'burst'. limiter := rate.NewLimiter(2, 4) // The function we are returning is a closure, which 'closes over' the limiter // variable. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Call limiter.Allow() to see if the request is permitted, and if it's not, // then we call the rateLimitExceededResponse() helper to return a 429 Too Many // Requests response (we will create this helper in a minute). if !limiter.Allow() { app.rateLimitExceededResponse(w, r) return } next.ServeHTTP(w, r) }) }
در این کد، هر بار که متد Allow() را روی محدودکننده نرخ فراخوانی میکنیم، دقیقاً یک توکن از سطل مصرف میشود. اگر توکنی در سطل باقی نمانده باشد، Allow() مقدار false برمیگرداند و این به عنوان محرکی عمل میکند تا پاسخ 429 Too Many Requests را به کلاینت ارسال کنیم.
همچنین مهم است توجه داشته باشید که کد پشت متد Allow() توسط mutex محافظت شده و برای استفاده همزمان ایمن است.
حالا بیایید به فایل cmd/api/errors.go برویم و تابع کمکی rateLimitExceededResponse() را ایجاد کنیم. به این شکل:
package main ... func (app *application) rateLimitExceededResponse(w http.ResponseWriter, r *http.Request) { message := "rate limit exceeded" app.errorResponse(w, r, http.StatusTooManyRequests, message) }
در نهایت، در فایل cmd/api/routes.go باید middleware rateLimit() را به زنجیره middleware خود اضافه کنیم. این باید بعد از middleware بازیابی از panic بیاید (تا هرگونه panic در rateLimit() بازیابی شود)، اما در غیر این صورت میخواهیم هرچه زودتر استفاده شود تا از کار غیرضروری برای سرور جلوگیری کنیم.
فایل را مطابقاً به روز کنید:
package main ... func (app *application) routes() http.Handler { router := httprouter.New() router.NotFound = http.HandlerFunc(app.notFoundResponse) router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler) router.HandlerFunc(http.MethodGet, "/v1/movies", app.listMoviesHandler) router.HandlerFunc(http.MethodPost, "/v1/movies", app.createMovieHandler) router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.showMovieHandler) router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.updateMovieHandler) router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.deleteMovieHandler) // Wrap the router with the rateLimit() middleware. return app.recoverPanic(app.rateLimit(router)) }
حالا باید آماده باشیم این را آزمایش کنیم!
API را ریاستارت کنید، سپس در یک پنجره ترمینال دیگر دستور زیر را اجرا کنید تا ۶ درخواست متوالی به endpoint GET /v1/healthcheck خود ارسال کنید. باید پاسخهایی به شکل زیر دریافت کنید:
$ 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"
}
}
{
"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"
}
میتوانیم ببینیم که ۴ درخواست اول موفق هستند، زیرا محدودکننده ما به گونهای تنظیم شده که اجازه «انفجار» ۴ درخواست متوالی را بدهد. اما وقتی آن ۴ درخواست مصرف شدند، توکنهای سطل تمام شدند و API ما شروع به برگرداندن پاسخ خطای "rate limit exceeded" کرد.
اگر یک ثانیه صبر کنید و دوباره این دستور را اجرا کنید، باید ببینید که برخی درخواستها در دسته دوم دوباره موفق میشوند، زیرا سطل توکنی با سرعت دو توکن در ثانیه پر میشود.