Files
go-admin/common/middleware/sentinel.go
T
zhangwenjian cd8edfa5d4 fix🐛: reject rate-limited requests with 429 and make the threshold configurable
A rejected request answered 200 with the failure only in the body, so every
layer that reads the status line counted it as served: load balancers, metrics,
client-side retry. A load test against this reported the limiter's own
rejections as successful traffic and overstated throughput more than tenfold.

The threshold was a constant in the middleware, which made 200 QPS the ceiling
of every deployment with nothing in the configuration to reveal it. It now
reads extend.rateLimit.inboundQPS; an absent value keeps 200, so an upgrade
changes nothing, and zero disables the limiter for a deployment behind its own
gateway.

Also drops Strategy: system.BBR. Reading sentinel's source, the adaptive
strategy is consulted only for Load and CpuUsage - for InboundQPS the trigger
count is compared directly - so it read as if the limit adapted to the machine
when it never did.
2026-08-28 19:42:21 +08:00

56 lines
1.7 KiB
Go

package middleware
import (
"net/http"
"github.com/alibaba/sentinel-golang/core/system"
sentinel "github.com/alibaba/sentinel-golang/pkg/adapters/gin"
"github.com/gin-gonic/gin"
log "github.com/go-admin-team/go-admin-core/v2/logger"
"go-admin/config"
)
// Sentinel 限流
//
// The threshold comes from extend.ratelimit.inboundqps; see config.RateLimit
// for the values it accepts.
func Sentinel() gin.HandlerFunc {
qps := config.ExtConfig.RateLimit.Threshold()
if qps <= 0 {
log.Info("rate limit disabled by extend.ratelimit.inboundqps")
return func(c *gin.Context) { c.Next() }
}
if _, err := system.LoadRules([]*system.Rule{
{
MetricType: system.InboundQPS,
TriggerCount: qps,
// InboundQPS is compared against TriggerCount directly - the
// adaptive strategy is only consulted for Load and CpuUsage. BBR
// stood here and read as if the limit adapted to the machine, which
// it never did.
Strategy: system.NoAdaptive,
},
}); err != nil {
log.Fatalf("Unexpected error: %+v", err)
}
log.Infof("rate limit: %.0f inbound req/s", qps)
return sentinel.SentinelMiddleware(
sentinel.WithBlockFallback(func(ctx *gin.Context) {
// 429, not 200. Everything that reads the status line rather than
// the body counts a 200 as served: load balancers, metrics,
// client-side retry, and load tests - a benchmark against the old
// behaviour reported the limiter's own rejections as successful
// traffic and overstated throughput by more than tenfold.
ctx.AbortWithStatusJSON(http.StatusTooManyRequests, map[string]interface{}{
"msg": "too many request; the quota used up!",
"code": http.StatusTooManyRequests,
})
}),
)
}