单机限流指针对单一服务器的情况,通过限制单台服务器在单位时间内处理的请求数量,防止服务器过载。常见的限流算法(固定窗口限流算法、滑动窗口算法、漏桶限流算法)上文已介绍,其优点在于实现简单,效率高,效果明显。
随着微服务架构的普及,系统的服务通常会部署在多台服务器上,此时就需要分布式限流来保证整个系统的稳定性。接下本文会介绍几种常见的分布式限流技术方案:
基于中心化的限流方案
方案原理
通过一个中心化的限流器来控制所有服务器的请求。实现方式:
代码实现
package main
import (
"context"
"fmt"
"go.uber.org/atomic"
"sync"
"git.code.oa.com/pcg-csd/trpc-ext/redis"
)
type RedisClient interface {
Do(ctx context.Context, cmd string, args ...interface{}) (interface{}, error)
}
// Client 数据库
type Client struct {
client RedisClient // redis 操作
script string // lua脚本
}
// NewBucketClient 创建redis令牌桶
func NewBucketClient(redis RedisClient) *Client {
helper := redis
return &Client{
client: helper,
script: `
-- 令牌桶限流脚本
-- KEYS[1]: 桶的名称
-- ARGV[1]: 桶的容量
-- ARGV[2]: 令牌产生速率
local bucket = KEYS[1]
local capacity = tonumber(ARGV[1])
local tokenRate = tonumber(ARGV[2])
local redisTime = redis.call('TIME')
local now = tonumber(redisTime[1])
local tokens, lastRefill = unpack(redis.call('hmget', bucket, 'tokens', 'lastRefill'))
tokens = tonumber(tokens)
lastRefill = tonumber(lastRefill)
if not tokens or not lastRefill then
tokens = capacity
lastRefill = now
else
local intervalsSinceLast = (now - lastRefill) * tokenRate
tokens = math.min(capacity, tokens + intervalsSinceLast)
end
if tokens < 1 then
return 0
else
redis.call('hmset', bucket, 'tokens', tokens - 1, 'lastRefill', now)
return 1
end
`,
}
}
// 获取令牌,获取成功则立即返回true,否则返回false
func (c *Client) isAllowed(ctx context.Context, key string, capacity int64, tokenRate int64) (bool, error) {
result, err := redis.Int(c.client.Do(ctx, "eval", c.script, 1, key, capacity, tokenRate))
if err != nil {
fmt.Println("Redis 执行错误:", err)
return false, err
}
return result == 1, nil
}
// 调用检测
func main() {
c := NewBucketClient(redis.GetPoolByName("redis://127.0.0.1:6379"))
gw := sync.WaitGroup{}
gw.Add(120)
count := atomic.Int64{}
for i := 0; i < 120; i++ {
go func(i int) {
defer gw.Done()
status, err := c.isAllowed(context.Background(), "test", 100, 10)
if status {
count.Add(1)
}
fmt.Printf("go %d status:%v error: %v\n", i, status, err)
}(i)
}
gw.Wait()
fmt.Printf("allow %d\n\n", count.Load())
}
执行结果:
存在的问题
基于负载均衡的分布式限流方案
方案原理
可以看到中心化限流方案的存在较高的单点故障风险,且带宽瓶颈比较严重。在这个基础上本文结合本地缓存单机限流和负载均衡设计了一个新的分布式限流方案。具体方案如下:
存在的问题
基于分布式协调服务的限流
方案原理
使用 ZooKeeper 或者 etcd 等分布式协调服务来实现限流。每台服务器都会向分布式协调服务申请令牌,只有获取到令牌的请求才能被处理。基本方案:
存在的问题
这个方案的优点是可以实现精确的全局限流,并且可以避免单点故障。但是,这个方案的缺点是实现复杂,且对 ZooKeeper 的性能有较高的要求。如果 ZooKeeper 无法处理大量的令牌申请和释放操作,可能会成为系统的瓶颈。
小结
总之,没有最好的方案,只有合适的方案。在选择合适的限流方案时,我们需要考虑多种因素,包括系统的需求、现有的技术栈、系统的负载情况以及底层系统的性能等。理解每种方案的工作原理和特性,以便在实际应用中做出最佳的选择。
限流是保证系统稳定和高效运行的重要手段,但它并不是唯一的解决方案。我们还需要考虑其他的系统设计和优化手段,例如负载均衡、缓存、异步处理等(面对爆量,扩容永远是最好的方式,除了贵!)。这些手段相互配合,才能构建出一个既能应对高并发请求,又能保证服务质量的系统。