目录
一、引言
1.1、什么是限流
保护高并发系统的三把利器是缓存、降级、限流。缓存之前讲过了,内存读取,提升相应速率,对高并发请求做暂存缓冲。熔断降级暂时也不做过多讲述,本篇主要讲限流。
前面聊过网关流量控制(限流),熔断降级功能。那么限流是如何实现控制的呢?
限流分瞬时流量控制和时间窗口流量;被限流的端口仍然可以提交任务,只是任务被阻塞,缓存在内存,会有OOM的风险;是对下游服务的一种保护;
1.2、限流和熔断降级的区别
熔断降级是直接拒绝服务;客户端口提交的请求直接非阻塞的被拒绝并给与请求失败的信息返回;
1.3、常用的限流方式和场景
- 限制总并发数(比如数据库连接池、线程池)
- 限制瞬时并发数(如nginx的limitconn模块,用来限制瞬时并发连接数,Java的Semaphore也可以实现)
- 限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limitreq模块,限制每秒的平均速率)
- 其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
二、限流算法:
2.1、固定窗口计数限流
- 在一个时间周期内每来一次请求就将计数器+1
- 如果计数器超过了限制数量, 则拒绝服务
- 时间达到下一个时间窗口, 计数器重置
这种算法很好实现, 但是会出现限流不准确问题。比如每秒通过 5 个请求,时间窗口的大小为 1 秒,当前时间窗口周期内的后半秒正常通过了 5 个请求,下一个时间窗口周期内的前半秒正常通过了 5 个请求,在这两个窗口内都没有超过限制。但是在这两个窗口的中间那一秒实际上通过了 10 个请求,显然不满足每秒 5 个请求的限制。
2.2、滑动窗口计数限流
- 将时间周期设置为滑动窗口大小
- 当有新的请求来临时将窗口滑动到改请求来临的时刻
- 判断窗口内的请求数是否超过了限制, 超过则拒绝服务, 否则请求通过
- 丢弃滑动窗口以外的请求
这种算法解决了固定窗口计数器出现的通过请求数是限制数两倍的缺陷,但是滑动窗口时间记录会比较麻烦(上一次和下一次请求的时间间隔)。
2.3、漏桶限流
- 将进来的请求流量视为水滴放入桶里
- 水从桶的底部以固定速率匀速流出, 相当于匀速请求
- 当漏桶的水满时(超过限流阈值)则拒绝服务
- 这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)
- 漏桶算法对请求令牌是一个缓冲池一样的作用,将突发的请求缓存到桶中,桶的容量就是任一时刻允许的请求量,超过这个阈值burst,请求就会被阻塞,或者直接拒绝请求,并非阻塞式的返回结果。相当于可以控制任务请求启动执行速率。
- 但是需要注意桶的最大容量的选取,当请求量超过阈值的处理方式最好选用非阻塞的直接拒绝,以免OOM,保护下游的同时也保护该服务端。
- 控制流量流速绝对均匀, 适合流量比较平滑的场景(如数据库), 分布式的实现难度较滑动窗口来说复杂一些。
2.4、令牌桶限流
- 按照一定的速率生产令牌并放入令牌桶中
- 如果桶中令牌已满,则丢弃令牌
- 请求过来时先到桶中拿令牌,拿到令牌则放行通过,否则拒绝请求。
- 固定窗口计数算法简单易实现,其缺陷是可能在中间的某一秒内通过的请求数是限流阈值的两倍,该算法仅适用于对限流准确度要求不高的应用场景。
- 滑动窗口计数算法解决了固定窗口计数算法的缺陷,但是该算法较难实现,因为要记录每次请求所以可能出现比较占用内存比较多的情况。
- 漏桶算法可以做到均匀平滑的限制请求,Ngixn 热 limit_req 模块也是采用此种算法。因为匀速处理请求的缘故所以该算法应对限流阈值内的突发请求无法及时处理。
- 令牌桶算法解决了以上三个算法的所有缺陷,是一种相对比较完美的限流算法,也是限流场景中应用最为广泛的算法。使用 Redis + Lua 脚本的方式可以简单的实现;
2.5、常用的限流工具API
- 限流工具类如Java的Semaphore,Guava的RateLimiter的非阻塞API方法如tryAquire,达到限制计数阈值后直接拒绝返回,也可以实现熔断降级的功能;
- 而线程池的最大线程数的配置也可以作为限制瞬时并发数(任务队列长度设置为0,最大线程数就是瞬时最大并发数)的实现工具;但是线程池可能会被不同的业务,不同端口共用的,不能精细的控制每个业务端口的流量,所以线程池的控制粒度不够细。
- 而流量控制API则可以有更好的控制粒度。
- Java的Semaphore是没有什么算法实现的,就是直接的一个信号量的控制。但是限制时间窗口内平均速率却是有多种算法实现的。
RateLimiter 与 Semaphore区别
RateLimiter限制的是速率,默认的create方法的时间单位是每秒
Semaphore限制的是并发数
Semaphore
- 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,
- 注意下面的实现中线程数和数据库连接数是相等的
@Slf4j(topic = "limit")
public class LimitPool {
// 1. 连接池大小
private final int poolSize;
// 2. 连接对象数组
private Connection[] connections;
// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;
private Semaphore semaphore;
//3. 获取数据库连接对象
// Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db3", "root", "root");
// 4. 构造方法初始化
public LimitPool(int poolSize) throws SQLException {
this.poolSize = poolSize;
// 让许可数与资源数一致
this.semaphore = new Semaphore(poolSize);
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = DriverManager.getConnection(String.valueOf(i++));
}
}
// 5. 借连接
public Connection borrow() {// t1, t2, t3
// 获取许可
try {
semaphore.acquire(); // 没有许可的线程,在此等待
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if(states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 不会执行到这里
return null;
}
// 6. 归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
log.debug("free {}", conn);
semaphore.release();
break;
}
}
}
}
RateLimiter
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便。
@Slf4j(topic = "rateLimit")
public class LimitedTest {
@Resource
private NettyTcpClient client;
private RateLimiter pushLimiter;
@PostConstruct
public void initLimiter() {
pushLimiter = RateLimiter.create(50);
}
public JSONArray push(JSONArray jsonArray) throws Exception {
if (jsonArray.isEmpty()) {
return new JSONArray();
}
orderLimiter.acquire(jsonArray.size());
return client.sendMsg(jsonArray.toString);
}
}
/**
* Created by wenrong on 2019/11/22.
*/
@Component
public class NettyTcpClient {
private static final Logger log = LoggerFactory.getLogger(NettyTcpClient.class);
@Value(("${netty.tcp.server.host}"))
String HOST;
@Value("${netty.tcp.server.port}")
int PORT;
@Autowired
ClientChannelInitializer clientChannelInitializer;
//与服务端建立连接后得到的通道对象
private Channel channel;
/**
* 初始化 `Bootstrap` 客户端引导程序