+2

Giới hạn tốc độ API Golang bằng Redis

Giới hạn tốc độ (Rate Limiting) là một kỹ thuật giới hạn số lượng yêu cầu mà người dùng hoặc máy khách có thể thực hiện đến API trong một khoảng thời gian nhất định. Bài viết này sẽ hướng dẫn cách tạo một máy chủ HTTP với Golang sử dụng framework Gin và áp dụng chức năng giới hạn tốc độ cho một endpoint bằng Redis.

Nói một cách đơn giản hơn, Giới hạn tốc độ là một kỹ thuật trong đó chúng ta giới hạn số lượng yêu cầu mà người dùng hoặc máy khách có thể thực hiện đến API trong một khoảng thời gian nhất định. Bạn có thể đã gặp phải thông báo "vượt quá giới hạn tốc độ" khi cố gắng truy cập API thời tiết hoặc API truyện cười. Có rất nhiều lý do xoay quanh việc tại sao phải giới hạn tốc độ API, nhưng một số lý do quan trọng là để sử dụng API một cách công bằng, đảm bảo an ninh, bảo vệ tài nguyên khỏi bị quá tải, v.v.

Trong bài viết này, chúng ta sẽ tạo một máy chủ HTTP với Golang sử dụng framework Gin, áp dụng chức năng giới hạn tốc độ cho một endpoint bằng Redis và lưu trữ tổng số yêu cầu được thực hiện bởi một IP đến máy chủ trong một khoảng thời gian. Và nếu vượt quá giới hạn chúng ta đặt, chúng ta sẽ đưa ra một thông báo lỗi.

Trong trường hợp bạn không biết Gin và Redis là gì. Gin là một web framework được viết bằng Golang. Nó giúp tạo một máy chủ đơn giản và nhanh chóng mà không cần viết nhiều code. Redis là một kho dữ liệu trong bộ nhớ và theo cặp key-value, có thể được sử dụng làm cơ sở dữ liệu hoặc cho khả năng lưu trữ cache.

Điều kiện tiên quyết

  • Bạn cần phải quen thuộc cách sử dụng với Golang, Gin và Redis
  • Có sẵn một instance Redis (Chúng ta có thể sử dụng Docker hoặc một máy từ xa)

Bắt đầu

Để khởi tạo dự án, hãy chạy go mod init <đường dẫn github>

Sau đó, hãy tạo một máy chủ HTTP đơn giản với Gin Framework, sau đó chúng ta áp dụng logic để giới hạn tốc độ. Bạn có thể sao chép đoạn code bên dưới. Nó rất cơ bản. Máy chủ sẽ trả lời bằng một thông báo khi chúng ta truy cập endpoint /message.

Sau khi sao chép đoạn code bên dưới, hãy chạy go mod tidy để tự động cài đặt các gói mà chúng ta đã import.

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/message", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "You can make more requests",
        })
    })
    r.Run(":8081") //listen and serve on localhost:8081
} 

Chúng ta có thể chạy máy chủ bằng cách thực thi go run main.go trong terminal và thấy thông báo này trong terminal. image.png

Để kiểm tra, chúng ta có thể truy cập localhost:8081/message, chúng ta sẽ thấy thông báo này trong trình duyệt. image.png

Giờ máy chủ của chúng ta đang chạy, hãy thiết lập chức năng giới hạn tốc độ cho route /message. Chúng ta sẽ sử dụng gói go-redis/redis_rate. Nhờ người tạo ra gói này, chúng ta không cần phải viết logic để xử lý và kiểm tra giới hạn từ đầu. Nó sẽ làm tất cả những việc nặng nhọc cho chúng ta.

Bên dưới là đoạn code hoàn chỉnh sau khi triển khai chức năng giới hạn tốc độ. Chúng ta sẽ tìm hiểu từng chút một. Vừa đưa ra đoạn code hoàn chỉnh sớm để tránh nhầm lẫn và để hiểu cách các phần khác nhau hoạt động cùng nhau.

Sau khi sao chép code, hãy chạy go mod tidy để cài đặt tất cả các gói đã import. Bây giờ hãy tìm hiểu và hiểu đoạn code (Bên dưới đoạn code).

package main

import (
    "context"
    "errors"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/go-redis/redis_rate/v10"
    "github.com/redis/go-redis/v9"
)

func main() {
    r := gin.Default()
    r.GET("/message", func(c *gin.Context) {
        err := rateLimiter(c.ClientIP())
        if err != nil {
            c.JSON(http.StatusTooManyRequests, gin.H{
                "message": "you have hit the limit",
            })
            return
        }
        c.JSON(http.StatusOK, gin.H{
            "message": "You can make more requests",
        })
    })
    r.Run(":8081")
}

func rateLimiter(clientIP string) error {
    ctx := context.Background()
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    limiter := redis_rate.NewLimiter(rdb)
    res, err := limiter.Allow(ctx, clientIP, redis_rate.PerMinute(10))
    if err != nil {
        panic(err)
    }
    if res.Remaining == 0 {
        return errors.New("Rate")
    }

    return nil
}

Trước tiên, hãy trực tiếp đến hàm rateLimiter() và hiểu nó. Hàm này yêu cầu một đối số là địa chỉ IP của yêu cầu mà chúng ta có thể lấy thông qua c.ClientIP() trong hàm main. Và chúng ta trả về lỗi nếu đạt đến giới hạn, ngược lại giữ nó là nil. Hầu hết code là code mẫu mà chúng ta lấy từ repo GitHub chính thức. Chức năng chính cần xem xét kỹ hơn ở đây là hàm limiter.Allow(). Addr: nhận giá trị đường dẫn URL cho instance Redis. Tôi đang sử dụng Docker để chạy nó cục bộ. Bạn có thể sử dụng bất cứ thứ gì, chỉ cần đảm bảo bạn thay thế URL cho phù hợp.

res, err := limiter.Allow(ctx, clientIP, redis_rate.PerMinute(10))

Hàm này nhận ba đối số: đối số đầu tiên là ctx, đối số thứ hai là Key (khóa cho một giá trị) cho Cơ sở dữ liệu Redis và đối số thứ ba là giới hạn. Vì vậy, hàm lưu trữ địa chỉ clientIP làm key và giới hạn mặc định làm value và giảm nó khi một yêu cầu được thực hiện. Lý do cho cấu trúc này là cơ sở dữ liệu Redis cần định danh duy nhất và một key duy nhất để lưu trữ dữ liệu kiểu cặp key-value và mọi địa chỉ IP đều duy nhất theo cách riêng của nó, đây là lý do tại sao chúng ta sử dụng địa chỉ IP thay vì tên người dùng, v.v.

Đối số thứ 3 redis_rate.PerMinute(10) có thể được sửa đổi theo nhu cầu của chúng ta, chúng ta có thể đặt giới hạn PerSecond, PerHour, v.v., và đặt giá trị bên trong ngoặc đơn cho số lượng yêu cầu có thể được thực hiện mỗi phút/giây/giờ. Trong trường hợp của chúng ta, đó là 10 mỗi phút. Vâng, thật đơn giản để thiết lập phải không nào.

Cuối cùng, chúng ta đang kiểm tra xem có hạn ngạch còn lại hay không bằng res.Remaining. Nếu nó bằng 0, chúng ta sẽ trả về lỗi với thông báo, nếu không chúng ta sẽ trả về nil.

Ví dụ: bạn cũng có thể thực hiện res.Limit.Rate để kiểm tra tốc độ giới hạn, v.v. Bạn có thể thử nghiệm và tìm hiểu sâu hơn về điều đó. Một điều cần lưu ý ở đây là, đây chỉ là một ví dụ về cách kết hợp hai phần này lại với nhau, vì chúng ta có một route duy nhất nên chúng ta không sử dụng bất kỳ middleware nào, điều gì sẽ xảy ra khi chúng ta có hàng chục hoặc hàng trăm route?

Bây giờ đến hàm main():

func main() {
    r := gin.Default()
    r.GET("/message", func(c *gin.Context) {
        err := rateLimiter(c.ClientIP())
        if err != nil {
            c.JSON(http.StatusTooManyRequests, gin.H{
                "message": "you have hit the limit",
            })
            return
        }
        c.JSON(http.StatusOK, gin.H{
            "message": "You can make more requests",
        })
    })
    r.Run(":8081")
}

Mọi thứ gần như giống nhau. Trong route /message, bây giờ mỗi khi route bị truy cập, chúng ta gọi hàm rateLimit() và truyền cho nó địa chỉ ClientIP và lưu trữ giá trị trả về (lỗi) trong biến err. Nếu có lỗi, chúng ta sẽ trả về 429, tức là http.StatusTooManyRequests và thông báo "message": "You have hit the limit". Nếu người đó có giới hạn còn lại và rateLimit() không trả về lỗi, nó sẽ hoạt động bình thường, như trước đây và phục vụ yêu cầu.

Đó là tất cả các giải thích trong bài viết này. Bây giờ hãy kiểm tra hoạt động. Chạy lại máy chủ bằng cách thực thi cùng một lệnh. Lần đầu tiên, chúng ta sẽ thấy cùng một thông báo mà chúng ta nhận được trước đó. Bây giờ hãy làm mới trình duyệt của bạn 10 lần (Vì chúng ta đặt giới hạn 10 mỗi phút) và bạn sẽ thấy thông báo lỗi trong trình duyệt. image.png

Chúng ta cũng có thể xác minh điều này bằng cách xem nhật ký trong terminal. Gin cung cấp khả năng ghi nhật ký tuyệt vời ngay lập tức. Sau một phút, nó sẽ khôi phục hạn ngạch giới hạn của chúng ta. image.png

Đây là tất cả những gì mà tôi muốn trình bày với các bạn trong bài viết này. Hy vọng các bạn thấy những thông tin này là hữu ích.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí