阅读本文之前可先阅读:高可用服务设计概述[1],该文主要介绍了负载均衡和隔离。
在开发高并发系统时,有很多手段保护系统,比如缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统处理能力,可谓是抗高并发的银弹。而降级是当服务出问题或者影响到核心流程的性能,需要暂时屏蔽掉,待高峰过去或者问题解决后再打开的场景。而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页)等。因此,需要有一种手段来限制这些场景下的并发/请求量,这种手段就是限流。
限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限速速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或者默认数据,如商品详情页库存默认有货)。在压测时,我们能找出每个系统的处理峰值,然后通过设定峰值阈值,当系统过载时,通过拒绝过载的请求来保障系统可用。另外,也可以根据系统的吞吐量、响应时间、可用率来动态调整限流阈值。
一般开发高并发系统场景的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如Nginx的limit_conn模块,用来限制瞬间并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、Nginx的limit_req模块,用来限制每秒的平均速率),以及限制远程接口调用速率、限制MQ的消费速率等。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。
常见的限流算法有:令牌桶、漏桶。计数器也可以用来进行粗暴限流实现。
令牌桶算法,是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:
漏桶作为计量工具时,可以用于流量整形和流量控制,漏桶算法的描述如下:
对于一个应用系统来说,一定会有极限并发/请求数,即总有一个TPS/QPS阈值,如果超了阈值,则系统就会不影响用户请求或响应得非常慢。因此,我们最好进行过载保护,以防止大量请求涌入击垮系统。
如MQ(max_connections)、Redis(tcp-backlog)都会有类似的限制连接数的配置。
如果有的资源是稀缺资源(如数据库连接、线程),而且可能有多个系统都会去使用它,那么需要加以限制。可以使用池化技术来限制总资源数,如连接池、线程池。假设分配给每个应用的数据库连接是100,那么本应用最多可以使用100个资源,超出则可以等待或者抛异常。
如果接口可能会有并发流量,但又担心访问量太大造成奔溃,那么久需要限制这个接口的总并发/请求数了。因为粒度比较细,可以为每个接口设置相应的阈值。可以使用Java中的AtomicLong或者Semaphore进行限流。Hystrix在信号量模式下也使用Semaphore限制每个接口的总请求数。
一种实现方式如下:
try {
if (atomic.incrementAndGet() > 限流数) {
//拒绝请求
}
//处理请求
} finally {
atomic.decrementAndGet();
}
即限制某个接口/服务每秒/每分钟/每天的请求数/调用量。一种实现方式如下:
LoadingCache counter =
CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader() {
@Override
public AtomicLong load(Long aLong) throws Exception {
return new AtomicLong(0);
}
});
long limit = 1000;
while (true) {
long currentSeconds = System.currentTimeMillis() / 1000;
if (counter.get(currentSeconds).incrementAndGet() > limit) {
logger.info("被限流了:{}", currentSeconds);
continue;
}
//业务处理
}
Guava RateLimiter提供的令牌桶算法可用于平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使用Redis+Lua或者Nginx+Lua技术进行实现,通过这两种技术可以实现高并发和高性能。
有时候我们想在特定时间窗口内对重复的相同事件最多只处理一次,或者想限制多个连续相同事件最小执行时间间隔,那么可使用节流(Throttle)实现,其防止多个相同事件连续重复执行。节流主要有如下几种用法:throttleFirst、throttleLast、throttleWithTimeout。
当访问量剧增、服务出现问题(如响应时间长或者不响应)或非核心服务影响到核心服务的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键参数进行自动降级,也可以配合开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。降级也需要根据系统的吞吐量、响应时间、可用率等条件进行手工降级或自动降级。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保车,从而梳理出哪些必须誓死保护,哪些可以降级。比如,可以参考日志级别设置预案:
降级的功能点主要从服务器端链路考虑,即根据用户访问的服务调用链路来梳理哪里需要降级。
自动降级是根据系统负载、资源使用情况、SLA等指标进行降级。
比如,上线新功能时进行灰度测试,当新服务有问题时通过开关切换回老服务。
如果应用不设置超时,可能会导致请求响应慢,慢请求累积导致连锁效应,甚至造成应用雪崩。而有些中间件或框架在超时后会进行重试(如设置超时自动重试两次),读服务天然适合重试,但写服务大多不能重试(如写订单,如果写服务是幂等的,则重试是允许的),重试次数太多会导致多倍请求流量,即模拟了DDoS攻击,后果可能是灾难。因此,务必设置合理的重试机制,并且应该和熔断、快速失败机制配合。在进行代码Review时,一定记得Review超时与重试机制。
对于非幂等写服务应避免重试,或者考虑提前生成唯一流水号来保证写服务操作通过判断流水号来实现幂等操作。
在进行数据库/缓存服务器操作时,要经常检查慢查询,慢查询通常是引起服务出问题的罪魁祸首。也要考虑在超时严重时,直接将该服务降级,待该服务修复后再取消降级。
回滚是指当程序或数据出错时,将程序或数据恢复到最近的一个正确版本的行为。通过回滚机制可保证系统在某些场景下的高可用。常见的回滚如下:
在大促来临之前,研发人员需要对现有系统进行梳理,发现系统瓶颈和问题,然后进行系统调优来提升系统的健壮性和处理能力。一般通过系统压测来发现系统瓶颈和问题,然后进行系统优化和容灾(系统参数调整、单机房容灾、多机房容灾等)。
压测一般是指性能压力测试,用来评估系统的稳定性和性能,通过压测数据进行系统容量评估,从而决定是否需要进行扩容或缩容。
压测之前要有压测方案(如压测接口、并发量、压测策略[突发、逐步加压、并发量]、压测指标[机器负载、QPS/TPS]、响应时间[平均、最小、最大]、成功率、相关参数[JVM参数、压缩参数]等),最后根据压测报告分析的结果进行系统优化和容灾。
参考来源:
[1] 亿级流量网站架构核心技术.张开涛著