互联网大厂的微服务架构系统应对超大流量解决方案
常见的限流方式有:
限制总并发数(数据库连接池、线程池等等)
限制瞬时并发数(如Nginx的limit_conn模块)
限制时间窗口的平均速率(如Guava的RateLimiter、Nginx的limit_req模块)
限制远程接口的调用速率、限制消息系统的消费速率
1.1 接入层限流
抗疫项目中,一般ISV会把Nginx作为业务的接入层,通过Nginx将请求分发到后端的应用集群上。接入层(流量层)是整个系统的咽喉入口, 越早挡掉请求越好,扛不住的流量越往后端传递,对后端的压力越大。
1.1.1 Nginx限流
官方提供两个模块:
limit_conn模块 (限制瞬时并发数)
limit_req模块 (限制单位时间内的请求数,速率限制,采用的漏桶算法)
limit_req
某口罩预约项目NG限流配置:
http {
......
# $binary_remote_addr 通过remote_addr这个标识来做限制
# zone=xxx:10m 生成一个大小为10M,名字为xxx的内存区域,用来存储访问的频次信息,64位可以存放163840个IP地址。
# rate=xx r/s 限制相同标识的客户端的访问频次
limit_req_zone $binary_remote_addr zone=req_perip:10m rate=30r/m; #30r/m: 限制两秒钟一个请求
limit_req_zone $binary_remote_addr zone=req_query:10m rate=5r/s; #5r/m: 限制每秒五个请求
}
server {
location /mask-appointment-service/appointment/ {
......
limit_req zone=req_perip burst=4; # 通过req_perip区域来限制
limit_req_status 507; # 限流返回值507
......
}
location /mask-appointment-service/query/ {
......
limit_req zone=req_query; # 通过req_query区域来限制
limit_req_status 509; # 限流返回值507
......
}
}
上述Nginx配置分别配置了 limit_req 两个规则,预约 和 查询。
预约: 针对某个唯一的来源IP做速率的控制,速率为30r/m(每2秒1个请求)。
查询: 针对某个唯一的来源IP做速率的控制,速率为5r/s (每1秒5个请求)。
特别注意:
rate=30r/m 很多时候会让人理解成每分钟30个请求, 其实并不是。官网的解释:
速率以每秒请求数r/s的形式指定。如果希望速率小于每秒一个请求,可以按每分钟请求数 r/m 来指定。例如,每秒半个请求为: 30r/m。
The rate is specified in requests per second (r/s). If a rate of less than one request per second is desired, it is specified in request per minute (r/m). For example, half-request per second is 30r/m.
几个写法含义:
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=10r/m;
代表单ip每6秒1个请求(60/10=6)
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=20r/m;
代表单ip每3秒1个请求(60/20=3)
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=30r/m;
代表单ip每2秒1个请求(60/30=2)(官方文档叫half-request per second 1秒半个请求)
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=1r/s;
代表单ip每秒1个请求
limit_req_zone $binary_remote_addr zone=limit_login:10m rate=2r/s;
代表单ip每秒2个请求
PS:
size ?该如何设置呢?
一个二进制的IP地址在64位机器上占用63个字节,设置10M的话: 10x1024x1024/64 = 163840,64位可以存放163840个IP地址。
limit_conn
limit_conn 这里不详细说了,有兴趣同学可以自行了解一下。
Nginx限流总结
Nginx属于接入层面,不能解决所有问题,比如内部调用的一些接口无法保证是否有限流控制,同时也只能挡掉http请求,比如rpc请求无法限制,所以在应用层实现限流还是很有必要的。上述Nginx限流都是对于单NG的,如果行业合作伙伴的接入层有多个Nginx,该怎么办呢?
解决方案1: Nginx前面部署负载均衡,通过一致性哈希按照限流的key把请求转发到接入层的NG(相同key的请求打到同一台Nginx上)
解决方案2: 通过 Nginx+Lua(OpenResty)调用分布式限流逻辑实现。
更多信息可以查看官方文档
[limit_req - Nginx] http://nginx.org/en/docs/http/ngx_http_limit_req_module.html
[limit_conn - Nginx] http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html
1.1.2 Openresty
有需要动态和精细控制的需求,可以通过Openresty做接入,OpenResty提供了lua-resty-limit-traffic 的Lua限流模块,基于Nginx的limit.conn和limit.req实现,通过可编程的方式进行复杂场景限流设计。比如变化限流速率,变化桶的大小等动态特性,可以按照更复杂的业务逻辑进行限流处理。
lua-resty-limit-traffic
resty.limit.req 模块
示例代码
local limit_req = require "resty,limit.req"
local lim, err = limit_req.new("mylimit", 5, 9)
local delay, err = lim:incoming(ngx.var.binary_remote_addr, true)
if not delay then
if err == "rejected" then
return ngx.exit(503)
end
return ngx.exit(500)
end
if delay >= 0.001 then
ngx.sleep(delay)
end
resty.limit.req 和 Nginx 的 limit_req 实现的效果和功能一样,但是这里用 Lua 来表达限速逻辑,可以在别的代码里面去引入。
更多其它模块和用法可以查看官方文档:
[lua-resty-limit-traffic - Openresty] https://github.com/openresty/lua-resty-limit-traffic
1.1.3 四层限流
OSI 四层限流可以通过SLB、WAF等四层接入进行流量的限流,也可以通过F5等硬负载四层设备来实现对流量控制。四层流量控制异常设计的面比较大,一般情况下慎用。
1.1.4 API路由网关
在微服务架构中,API路由网关作为内部系统的入口,非常适合做API限流操作,网关层的限流可以简单地针对不同业务的接口进行限流。业界较有名的Zuul、Dubbo都可以对限流进行较好的设计,当然还有我们的 Sentinel开源方案,Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。这里就不详细说了,有需要查阅相关资料。
1.2 应用层限流
1.2.1 单机单应用限流
我们可以在服务器内部通过编写一些算法进行限流,也可以调用一些类库中存在的API,比如Google Guava类库的RateLimiter,Guava RateLimiter 是 Google Guava 提供的生产级别的限流工具,RateLimiter 基于令牌桶流控算法,可以有效控制 单 JVM 下某个逻辑操作的频率。
Guava 的 RateLimiter 提供了以下核心方法 create()acquire()tryAcquire(),其中 create() 创建令牌桶,acquire() 获取令牌,支持同时获取 N 个令牌,tryAcquire(long timeout, TimeUint unit)支持在限定时间内获取令牌,如果获取不到将返回 false。
示例代码如下:
import com.google.common.util.concurrent.RateLimiter;
public class GuavaRateLimiterDemo {
// 每秒 100 个令牌
private final RateLimiter rateLimiter = RateLimiter.create(100.0);
void doBusiness() {
rateLimiter.acquire();
// 继续操作用户业务
}
}
由于 acquire() 会一直等待,并且 Guava 不保证公平分发,所以会出现线程持续等待情况,需要考虑超时情况下面请求直接结束。
示例代码如下:
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.TimeUnit;
public class GuavaRateLimiterDemo {
// 每秒 100 个令牌
private final RateLimiter rateLimiter = RateLimiter.create(100.0);
void doBusinessWithTryAcquire() {
boolean isPermit = rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!isPermit) {
throw new RuntimeException("business overheated.");
}
// 继续操作用户业务
}
}
RateLimiter 不仅实现了令牌桶,还做了不少优化,可以满足不同场景的需求。
它支持「预热」warmupPeriod,tryAcquire 操作还支持立即计算返回,避免无效等待。
更多信息可以查看官方文档
[RateLimiter (Guava: Google Core Libraries for Java 19.0 API)]) https://guava.dev/releases/19.0/api/docs/index.html?com/google/common/util/concurrent/RateLimiter.html
缺点:
Guava RateLimiter(以及其他基于令牌桶算法的实现)缺点是只能针对单机,无法在分布式环境下面共享流量数据, 不适用于分布式服务。如果想使用的话,需要提前计算好每个示例负载均衡之后的流量阈值。
1.2.2 分布式限流
这一类的限流策略跟上面 API 路由网关模式的限流相似,同样是依赖配置中心管理,限流逻辑会配套服务化的框架完成。
抗击疫情-某口罩预约项目限流实战
2.1 挑战
在抗疫期间,我们联合政府机构,外部的合作伙伴紧急上线了口罩预约的服务。由于在疫情期间,口罩预约是个大热点,在开放预约入口1分钟内,预约系统面临着每秒峰值XX w+的并发压力。针对这种秒杀预约的场景,联合ISV对业务系统进行了一系列的改造优化, 这里我们重点来看看该项目是怎么通过不断消减请求和限流来保障流量突增时系统的稳定性的。
2.2 实战
2.2.1 第一层: 合法性限流拦截
在口罩预约开放时间段内,用户执行的第一个动作是填写预约信息,并获取手机验证码进行提交。我们可以在这一层就将不合法的请求给拦截掉(刷单行为、是否已登录,预约渠道是否合法), 允许合法的用户访问到服务器。
- 预约时使用手机验证码,用来拉长用户访问时间。(输入验证码到提交预约整个过程可能需要五秒时间,降低访问峰值)
- 防火墙进行IP限制策略配置,防止不合法刷单行为。
- 非预约时间段内隐藏口罩预约入口页面。
2.2.2 第二层: 负载限流与服务限流
合法性限流只能限制无效、非法的请求。但对于口罩预约场景来说,仍然会有大量的合法有效请求进入系统。在项目上线前,协助ISV进行如下的分流和限流的优化。
- 查询和预约读写分离,分流数据库的压力。
- 静态资源请求上CDN,分担了总请求处理的压力。
- 扩容了Nginx代理层和后台服务集群,共同来负载请求压力。
- Nginx入层进行了第二层的限流熔断。(详细配置请参考上述文档中的Nginx限流方案)。
- 获取手机验证码会调用三方短信服务网关,在应用接口层面单独进行了限流。
通过上面介绍,相信大家对限流算法有了一定理解,那么像大厂在大促期间如何能抗住那么大的并发的呢? 大家别着急请关注我们今晚八点(2021/06/23)直播课程<抗住亿级高并发流量的微服务架构解决方案> 在本次课程中有深入讲解如何能抗住亿级高并发流量的系统设计.
直播地址:https://ke.qq.com/course/3454153?taid=11561948885071049&tuin=4ea50e8f
资料下载:http://106.75.108.26:8001/index?empid=5
资料共享群:141703812