本文已收录在Github,关注我,紧跟本系列专栏文章,咱们下篇再续!
- 🚀 魔都架构师 | 全网30W技术追随者
- 🔧 大厂分布式系统/数据中台实战专家
- 🏆 主导交易系统百万级流量调优 & 车联网平台架构
- 🧠 AIGC应用开发先行者 | 区块链落地实践者
- 🌍 以技术驱动创新,我们的征途是改变世界!
- 👉 实战干货:编程严选网
0 高并发三板斧
高服务保护:
- 缓存,提升系统访问速度和增大系统能处理的容量,抗高并发流量的银弹
- 熔断、降级,当服务异常或影响核心流程,需暂时屏蔽,待高峰或问题解决后打开。服务降级,对不怎么重要的服务进行低优先级处理。尽可能把系统资源让给优先级高的服务。资源有限,而请求无限
- 有时不能缓存、降级解决,如稀缺资源、写服务、频繁复杂查询,需限制这些场景的并发/请求量,即限流。对并发访问/请求进行限速或对一个时间窗口内的请求进行限速。一旦达限制速率,则拒绝服务、排队或等待、降级。Guava RateLimiter、lua+Redis、Sentinel方案。
高并发系统限流
限制总并发数、限制瞬时并发数、限制时间窗口内的平均速率、限制远程接口的调用速率、限制MQ的消费速率,或根据网络连接数、网络流量、CPU或内存负载等来限流。
本文讨论分布式限流方案Spring Cloud Gateway限流原理。
限流关键
将限流服务做成原子化,常见限流算法:令牌桶、漏桶等。Spring Cloud Gateway用Redis+Lua实现高并发和高性能限流。
1 令牌桶算法
存放固定容量令牌的桶,按固定速率往桶里添加令牌。
1.1 算法描述
- 假如用户配置的平均速率为r,则每隔1/r秒一个令牌被加入到桶
- 假设桶最多可存b个令牌。若令牌到达时,令牌桶已满,则该令牌丢弃
- 当一个n字节的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上
- 若令牌桶中少于n个令牌,则不会删除令牌,并且认为这个数据包在流量限制之外
算法允许最长b个字节的突发,但长期运行结果看,数据包的速率被限制成常量r。
流量限制外的数据包可以不同方式处理:
- 被丢弃
- 排放在队列中,以便当令牌桶中累积足够多的令牌时再传输
- 继续发送,但要特殊标记,网络过载时将这些特殊标记的包丢弃
2 漏桶算法
漏桶作为计量工具(The Leaky Bucket Algorithm as a Meter)时,可用于:
- 流量整形(Traffic Shaping)
- 流量控制(Traffic Policing)
2.1 算法描述
一个固定容量的漏桶,按固定速率流出水滴。
若桶空,则不需流出水滴;
可以任意速率流入水滴到漏桶;
若流入水滴超出桶容量,则流入的水滴溢出(被丢弃),而漏桶容量不变。
3 实践
SCG默认实现 Redis 限流,如扩展只需实现Ratelimter接口,同时也可通过自定义KeyResolver指定限流K,如需根据用户、IP、URI做限流等,通过exchange对象可获取到请求信息,如:
3.1 用户限流
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
SCG默认提供的 RedisRateLimiter 的核心逻辑为判断是否取到令牌的实现,调用
META-INF/scripts/request_rate_limiter.lua
脚本实现基于令牌桶算法限流:
1: local tokens_key = KEYS1
2: local timestamp_key = KEYS2
3:
4: local rate = tonumber(ARGV1)
5: local capacity = tonumber(ARGV2)
6: local now = tonumber(ARGV3)
7: local requested = tonumber(ARGV4)
8:
9: local fill_time = capacity/rate
10: local ttl = math.floor(fill_time*2)
11:
12: local last_tokens = tonumber(redis.call(“get”, tokens_key))
13: if last_tokens == nil then
14: last_tokens = capacity
15: end
16:
17: local last_refreshed = tonumber(redis.call(“get”, timestamp_key))
18: if last_refreshed == nil then
19: last_refreshed = 0
20: end
21:
22: local delta = math.max(0, now-last_refreshed)
23: local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
24: local allowed = filled_tokens >= requested
25: local new_tokens = filled_tokens
26: local allowed_num = 0
27: if allowed then
28: new_tokens = filled_tokens - requested
29: allowed_num = 1
30: end
31:
32: redis.call(“setex”, tokens_key, ttl, new_tokens)
33: redis.call(“setex”, timestamp_key, ttl, now)
34:
35: return { allowed_num, new_tokens }
KeyResolver
KeyResolver从传入请求中提取路由键(route key)值,以路由到正确服务实例。KeyResolver根据请求中不同属性提取路由键,如请求头、请求参数、请求路径等。
常与Predicate和Filter协作:
- Predicate匹配请求
- Filter处理请求
用KeyResolver,可根据请求的不同属性将请求路由到不同的服务实例,实现LB和高可用性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。