eddycjy / go-programming-tour-book-comments

go-programming-tour-book-comments
1 stars 1 forks source link

应用中间件 | Go 语言编程之旅 #16

Open utterances-bot opened 3 years ago

utterances-bot commented 3 years ago

应用中间件 | Go 语言编程之旅

2.9 应用中间件 完成了接口的访问控制后,心中的一块大石终于落地了,你在开发服务器上将这个项目运行了起来,等着另外一位同事和你对接你所编写的后端接口后便愉快的先下班了。 但结果第二天你一来,该同事非常苦恼的和你说,你的接口,怎么调一下就出问题了,你大为震惊,详细的咨询了是几时调用的接口,调用的接口是哪个,入参又是什么

https://golang2.eddycjy.com/posts/ch2/09-app-middleware/

willnotlazy commented 3 years ago

建议被依赖的结构体声明放到后面,避免在手动练习过程中报红

willnotlazy commented 3 years ago

LimiterIface 接口的AddBuckets() 按照源码是不应有返回值的, 所以上诉的 AddBuckets是错误的.

0RAJA commented 2 years ago

时间控制感觉没有用唉....我设置时间后并没有监听我的c.Done() 之后就算到时间结束了也不会有什么操作啊,不太清楚,,,

TBXark commented 2 years ago
type AccessLogWriter struct {
    gin.ResponseWriter
    body *bytes.Buffer
}

这个做法body不就有两份了吗,如果这个body比较大的话,感觉还是挺占内存的

wqcstrong commented 2 years ago

文中关于限流的中间件代码是有问题的: GetBuckets 通过 key 获取 bucket 的时候,key 的获取方式是自定义的逻辑(文中是 uri),但 AddBuckets 注册的时候又是写死的,这就会导致中间件获取 bucket 的时候几乎永远都是 false(除非走了狗屎运😒),导致限流中间件没有效果。

下面是我这边调整后的:

// middleware/limiter.go
var (
    IPLimiter = RateLimiter(limiter.NewIPLimiter(), limiter.BucketRule{
        FillInterval: time.Minute,
        Capacity:     1000,
        Quantum:      1000,
    })

    RouteLimiter = RateLimiter(limiter.NewRouteLimiter(), limiter.BucketRule{
        FillInterval: time.Minute,
        Capacity:     1000,
        Quantum:      1000,
    })
)

func RateLimiter(l limiter.LimiterIface, rule limiter.BucketRule) gin.HandlerFunc {
    return func(c *gin.Context) {
        key := l.GetKey(c)
        bucket, ok := l.GetBuckets(key)
        if !ok {
            rule.Key = key
            l.AddBuckets(rule)
            bucket, _ = l.GetBuckets(key)
        }

        count := bucket.TakeAvailable(1)
        if count == 0 {
            response := app.NewResponse(c)
            response.ToErrorResponse(errcode.TooManyRequest)
            c.Abort()
            return
        }
        c.Next()
    }
}

// pkg/limiter/ip-limiter.go

type IPLimiter struct {
    Limiter *Limiter
}

func NewIPLimiter() *IPLimiter {
    l := &IPLimiter{
        Limiter: &Limiter{
            Bucket: make(map[string]*ratelimit.Bucket),
        },
    }
    return l
}

func (l *IPLimiter) GetKey(c *gin.Context) string {
    ip := c.ClientIP()
    return ip
}

func (l *IPLimiter) GetBuckets(key string) (*ratelimit.Bucket, bool) {
    bucket, ok := l.Limiter.Bucket[key]
    return bucket, ok
}

func (l *IPLimiter) AddBuckets(rules ...BucketRule) {
    for _, rule := range rules {
        l.Limiter.Bucket[rule.Key] = ratelimit.NewBucketWithQuantum(rule.FillInterval, rule.Capacity, rule.Quantum)
    }
}

我上面定义的 RouteLimiterIPLimiter 实现方式差不多,供参考。

forthespada commented 2 years ago

限流中间件的方法是没问题的。 煎鱼做的是需要针对某些路由方法来进行限流,而不是对全部的请求进行限流;评论区的那个代码做法会对全部的请求都做到限流。

wqcstrong commented 2 years ago

@forthespada 我们理解的有一丢丢的偏差,我指出的问题在于:「GetBuckets 是通过 key 获取 bucket,AddBuckets 时直接写入 /auth」,如果应用中没有注册这个 /auth 路由的话,那这个中间件则没有效果。

另外我上面的调整也不会对全部请求都做限流,下面我一并贴一下实现和使用:

  1. 实现路由限流接口
    
    // pkg/limiter/route-limiter.go
    type RouteLimiter struct {
    Limiter *Limiter
    }

func NewRouteLimiter() RouteLimiter { return &RouteLimiter{ Limiter: &Limiter{ Bucket: make(map[string]ratelimit.Bucket), }, } } func (l RouteLimiter) GetKey(c gin.Context) string { uri := c.Request.RequestURI index := strings.Index(uri, "?") if index == -1 { return uri } return uri[:index] } func (l RouteLimiter) GetBuckets(key string) (ratelimit.Bucket, bool) { // ... 和 ip-limiter 实现一样 } func (l *RouteLimiter) AddBuckets(rules ...BucketRule) { // ... 和 ip-limiter 实现一样 }


2. 路由限流中间件的使用
```go
// 2.1 实例化中间件
// middleware/limiter.go
RouteLimiter = RateLimiter(limiter.NewRouteLimiter(), limiter.BucketRule{
  FillInterval: time.Minute,
  Capacity:     5,
  Quantum:      1000,
})

// 2.2 在路由中注册中间件
v1 := r.Group("/v1")
v1.GET("/verify/captcha", middleware.RouteLimiter, api.GetAuthCaptcha)

// 2.3 日志输出查看效果
2022-04-21 08:59:22     DEBUG   middleware/logger.go:65 HTTP Access Log {"status": 200, "request": "GET  /v1/verify/captcha", "query": "", "ip": "127.0.0.1", "time": "2.916ms"}
2022-04-21 08:59:25     DEBUG   middleware/logger.go:65 HTTP Access Log {"status": 200, "request": "GET  /v1/verify/captcha", "query": "", "ip": "127.0.0.1", "time": "2.299ms"}
2022-04-21 08:59:26     DEBUG   middleware/logger.go:65 HTTP Access Log {"status": 200, "request": "GET  /v1/verify/captcha", "query": "", "ip": "127.0.0.1", "time": "1.987ms"}
2022-04-21 08:59:27     DEBUG   middleware/logger.go:65 HTTP Access Log {"status": 200, "request": "GET  /v1/verify/captcha", "query": "", "ip": "127.0.0.1", "time": "2.123ms"}
2022-04-21 08:59:28     DEBUG   middleware/logger.go:65 HTTP Access Log {"status": 200, "request": "GET  /v1/verify/captcha", "query": "", "ip": "127.0.0.1", "time": "2.234ms"}
2022-04-21 08:59:28     WARN    middleware/logger.go:63 HTTP Warn 429   {"status": 429, "request": "GET  /v1/verify/captcha", "query": "", "ip": "127.0.0.1", "time": "0.173ms"}
stephenzhang0713 commented 2 years ago

在限流那块,MethodLimiter结构体里面可嵌套一个LimiterIface

type MethodLimiter struct {
    *Limiter
    LimiterIface
}
oYto commented 1 year ago

要是买了书,就能看完整版电子档内容就好了

blkcor commented 1 year ago
func RateLimiter(l limiter.LimiterIface) gin.HandlerFunc {
    return func(c *gin.Context) {
        key := l.Key(c)
        //如果buckets中存在这个key -> 请求已经发生过了
        if bucket, ok := l.GetBucket(key); ok {
            //try take available token
            count := bucket.TakeAvailable(1)
            //no available token -> refuse request
            if count == 0 {
                response := app.NewResponse(c)
                response.ToErrorResponse(errcode.TooManyRequests)
                c.Abort()
                return
            }
        } else {
            l.AddBuckets(limiter.LimiterBucketRule{
                Key:          key,
                FillInterval: time.Second,
                Capacity:     10,
                Quantum:      10,
            })
        }
        c.Next()
    }
}

这里加上key不存在的逻辑就不用在router.go中使用中间件的时候吧key写死了

githublister commented 1 year ago

defer func() { if err := recover(); err != nil { global.Logger.WithCallersFrames().Errorf(c, "panic recover err: %v", err)

            err := defailtMailer.SendMail(
                global.EmailSetting.To,
                fmt.Sprintf("异常抛出,发生时间: %d", time.Now().Unix()),
                fmt.Sprintf("错误信息: %v", err),
            )
            if err != nil {
                global.Logger.Panicf(c, "mail.SendMail err: %v", err)
            }

            app.NewResponse(c).ToErrorResponse(errcode.ServerError)
            c.Abort()
        }
    }()

Cannot convert 'nil' to type 'any' 第一行的nil 报错啊 IDE

wyming175533 commented 10 months ago

@wqcstrong 其实限流这里本身就可以再追加多个限流规则的,具体的key写死却是不合适,通常可以加入到配置文件中,针对不同的key,做不同的限流规则,在以前开发java项目也是这样做的 var methodLimiters = limiter.NewMethodLimiter().AddBuckets( limiter.LimiterBucketRule{ Key: "/auth", FillInterval: time.Second, Capacity: 10, Quantum: 10, }, limiter.LimiterBucketRule{ Key: "/api/v1", FillInterval: time.Second, Capacity: 10, Quantum: 10, }, ) 甚至它是支持链式调用的 var methodLimiters = limiter.NewMethodLimiter().AddBuckets(limiter.LimiterBucketRule{ Key: "/auth", FillInterval: time.Second, Capacity: 10, Quantum: 10, }, limiter.LimiterBucketRule{ Key: "/api/v1/tags", FillInterval: time.Second, Capacity: 10, Quantum: 55, }, ).AddBuckets( limiter.LimiterBucketRule{ Key: "/...", FillInterval: time.Second, Capacity: 99, Quantum: 10, }, limiter.LimiterBucketRule{ Key: "/...", FillInterval: time.Second, Capacity: 100, Quantum: 1000, }, ) 接口限流通常只针对部分接口,生成key的时候也可以生成多个,比如/api/v1/tags,你就可以生成一个api/v1,和api/v1/tags,再根据不同的给出不同规则