Skip to content

27|限流与降级

网关是流量入口,后端服务的容量有限。突发流量、爬虫攻击或缓存击穿都可能导致后端被打满,响应变慢甚至崩溃。限流控制进入系统的请求速率,降级在资源不足时关闭非核心功能,保证核心链路可用。go-gateway 在代理层实现令牌桶限流,在管理 API 实现接口级限流。

一、限流算法

算法原理特点
固定窗口每秒允许 N 个请求简单,但窗口边界可能突发 2N 请求
滑动窗口按时间滑动统计更平滑,实现稍复杂
令牌桶以固定速率生成令牌,请求消耗令牌允许一定突发,最常用
漏桶请求进入桶,以固定速率漏出强制平滑,不支持突发

令牌桶是生产环境最常用的算法,它允许短时间内消耗积累的令牌(突发),但长期速率不超过生成速率。

二、单机限流

Go 标准库扩展 golang.org/x/time/rate 提供了令牌桶实现:

bash
go get golang.org/x/time/rate
go
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"golang.org/x/time/rate"
)

var limiter = rate.NewLimiter(rate.Limit(10), 20) // 每秒10个,桶容量20

func rateLimitMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !limiter.Allow() {
			http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
			return
		}
		next.ServeHTTP(w, r)
	})
}

rate.NewLimiter(10, 20) 表示每秒生成 10 个令牌,桶最多存 20 个。突发时最多一次处理 20 个请求,之后按每秒 10 个的速率放行。

等待令牌(而不是直接拒绝):

go
ctx, cancel := context.WithTimeout(r.Context(), 100*time.Millisecond)
defer cancel()
if err := limiter.Wait(ctx); err != nil {
	http.Error(w, "timeout", http.StatusTooManyRequests)
	return
}

Wait 会阻塞直到拿到令牌或 context 超时。适合对延迟不敏感、希望尽量处理请求的场景。

三、接口级限流

不同接口的容量不同。/api/users 可能能扛 1000 QPS,/api/report 只能扛 10 QPS。按路径分别限流:

go
type RateLimiter struct {
	limiters map[string]*rate.Limiter
	mu       sync.RWMutex
}

func (rl *RateLimiter) GetLimiter(path string, r rate.Limit, burst int) *rate.Limiter {
	rl.mu.RLock()
	lim, ok := rl.limiters[path]
	rl.mu.RUnlock()
	if ok {
		return lim
	}

	rl.mu.Lock()
	defer rl.mu.Unlock()
	lim, ok = rl.limiters[path]
	if ok {
		return lim
	}
	lim = rate.NewLimiter(r, burst)
	rl.limiters[path] = lim
	return lim
}

Gin 路由组中间件按路径限流:

go
admin.Use(func(c *gin.Context) {
	path := c.Request.URL.Path
	lim := limiterManager.GetLimiter(path, 10, 20)
	if !lim.Allow() {
		c.AbortWithStatusJSON(429, gin.H{"error": "rate limit exceeded"})
		return
	}
	c.Next()
})

四、熔断与降级

限流是从入口控制流量。熔断是从出口保护——后端服务连续出错时,停止向其转发请求,避免把后端拖垮。

熔断器有三种状态:

状态行为
Closed正常转发请求
Open后端异常,直接返回错误,不再转发
Half-Open等待一段时间后,放少量请求试探后端是否恢复

go-gateway 的简易熔断实现:

go
type CircuitBreaker struct {
	failures    int
	threshold   int
	lastFailure time.Time
	cooldown    time.Duration
	state       string // closed, open, half-open
	mu          sync.Mutex
}

func (cb *CircuitBreaker) Call(fn func() error) error {
	cb.mu.Lock()
	defer cb.mu.Unlock()

	if cb.state == "open" {
		if time.Since(cb.lastFailure) > cb.cooldown {
			cb.state = "half-open"
		} else {
			return fmt.Errorf("circuit breaker open")
		}
	}

	err := fn()
	if err != nil {
		cb.failures++
		cb.lastFailure = time.Now()
		if cb.failures >= cb.threshold {
			cb.state = "open"
		}
		return err
	}

	cb.failures = 0
	cb.state = "closed"
	return nil
}

生产环境用成熟的库如 github.com/sony/gobreakergithub.com/afex/hystrix-go

五、降级响应

后端不可用时,网关返回预定义的降级响应,而不是把错误透传给客户端:

go
func fallbackResponse(w http.ResponseWriter, path string) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusServiceUnavailable)
	json.NewEncoder(w).Encode(map[string]interface{}{
		"error": "service temporarily unavailable",
		"path":  path,
	})
}

六、常见错误

限流阈值设置过高

限流阈值设成后端容量的 10 倍,等于没限流。阈值应该通过压测确定,留 20%-30% 余量。

全局一个限流器

所有接口共用一个限流器,某个接口被刷时,其他接口也受影响。应该按接口、按用户、按 IP 多维度限流。

不处理限流器的并发安全

go
var limiter = rate.NewLimiter(10, 20) // 这个本身是并发安全的

rate.Limiter 内部有锁,是并发安全的。但自定义的限流管理器(如上面的 RateLimiter)需要自己加锁保护 map。