高可用服务设计概述[2]

阅读本文之前可先阅读:高可用服务设计概述[1],该文主要介绍了负载均衡和隔离。

3 限流

在开发高并发系统时,有很多手段保护系统,比如缓存降级限流。缓存的目的是提升系统访问速度和增大系统处理能力,可谓是抗高并发的银弹。而降级是当服务出问题或者影响到核心流程的性能,需要暂时屏蔽掉,待高峰过去或者问题解决后再打开的场景。而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀、抢购)、写服务(如评论、下单)、频繁的复杂查询(评论的最后几页)等。因此,需要有一种手段来限制这些场景下的并发/请求量,这种手段就是限流。

限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的请求进行限速来保护系统,一旦达到限速速率则可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或者默认数据,如商品详情页库存默认有货)。在压测时,我们能找出每个系统的处理峰值,然后通过设定峰值阈值,当系统过载时,通过拒绝过载的请求来保障系统可用。另外,也可以根据系统的吞吐量、响应时间、可用率来动态调整限流阈值。

一般开发高并发系统场景的限流有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如Nginx的limit_conn模块,用来限制瞬间并发连接数)、限制时间窗口内的平均速率(如Guava的RateLimiter、Nginx的limit_req模块,用来限制每秒的平均速率),以及限制远程接口调用速率、限制MQ的消费速率等。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。

限流算法

常见的限流算法有:令牌桶、漏桶。计数器也可以用来进行粗暴限流实现。

令牌桶算法

令牌桶算法,是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。令牌桶算法的描述如下:

  • 假设限制2r/s,则按照500毫秒的固定速率往桶内添加令牌。
  • 桶中最多存放b个令牌,当桶满时,新添加的令牌会被丢弃或拒绝。
  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
  • 如果桶中的令牌不足n个,则不会删除令牌,且该数据包被限流(要么丢弃,要么在缓冲区等待)。

漏桶算法

漏桶作为计量工具时,可以用于流量整形和流量控制,漏桶算法的描述如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴。
  • 如果桶是空的,则不需流出水滴。
  • 可以以任意速率流入水滴到漏桶。
  • 如果流入水滴超过了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

令牌桶和漏桶对比

  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时,则拒绝新的请求。
  • 漏桶则是按照常量固定速率流出请求,请求流入速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。
  • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿多个令牌),并允许一定程序的突发流量。
  • 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),从而平滑突发流入速率。
  • 令牌桶允许一定程序的突发,而漏桶主要目的是平滑流入速率。
  • 两个算法实现可以一样,但是方向是相反的,对于相同的参数得到的限流效果是一样的。

应用级限流

限制总并发/连接/请求数

对于一个应用系统来说,一定会有极限并发/请求数,即总有一个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。

4 降级

当访问量剧增、服务出现问题(如响应时间长或者不响应)或非核心服务影响到核心服务的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键参数进行自动降级,也可以配合开关实现人工降级。

降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。降级也需要根据系统的吞吐量、响应时间、可用率等条件进行手工降级或自动降级。

降级预案

在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保车,从而梳理出哪些必须誓死保护,哪些可以降级。比如,可以参考日志级别设置预案:

  • 一般:比如,有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级。
  • 警告:有些服务在一段时间内成功率有波动(如在95%~100%之间),可以自动降级或人工降级并发送告警。
  • 错误:比如,可用率低于90%,或者数据库连接池用完了,或者访问量突然猛增到系统能承受的最大阈值,此时可以根据情况自动降级或人工降级。
  • 严重错误:比如,因为特殊原因数据出现错误,此时需要紧急人工降级。

降级分类

  • 降级按照是否自动化可分为:自动开关降级和人工开关降级。
  • 降级按照功能可分为:读服务降级和写服务降级。
  • 降级按照处于的系统层次可分为:多级降级。

降级的功能点主要从服务器端链路考虑,即根据用户访问的服务调用链路来梳理哪里需要降级。

  • 页面降级。
  • 页面片段降级。
  • 页面异步请求降级。
  • 服务功能降级。
  • 读降级。比如多级缓存模式,如果后端服务有问题,则可以降级为只读缓存,这种方式是用于对读一致性要求不高的场景。
  • 写降级。比如秒杀抢购我们可以只进行Cache的更新,然后异步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
  • 爬虫降级。
  • 风控降级。

自动开关降级

自动降级是根据系统负载、资源使用情况、SLA等指标进行降级。

  • 超时降级
  • 统计失败次数降级
  • 故障降级
  • 限流降级

人工开关降级

比如,上线新功能时进行灰度测试,当新服务有问题时通过开关切换回老服务。

5 超时与重试机制

如果应用不设置超时,可能会导致请求响应慢,慢请求累积导致连锁效应,甚至造成应用雪崩。而有些中间件或框架在超时后会进行重试(如设置超时自动重试两次),读服务天然适合重试,但写服务大多不能重试(如写订单,如果写服务是幂等的,则重试是允许的),重试次数太多会导致多倍请求流量,即模拟了DDoS攻击,后果可能是灾难。因此,务必设置合理的重试机制,并且应该和熔断、快速失败机制配合。在进行代码Review时,一定记得Review超时与重试机制。

对于非幂等写服务应避免重试,或者考虑提前生成唯一流水号来保证写服务操作通过判断流水号来实现幂等操作。

在进行数据库/缓存服务器操作时,要经常检查慢查询,慢查询通常是引起服务出问题的罪魁祸首。也要考虑在超时严重时,直接将该服务降级,待该服务修复后再取消降级。

6 回滚机制

回滚是指当程序或数据出错时,将程序或数据恢复到最近的一个正确版本的行为。通过回滚机制可保证系统在某些场景下的高可用。常见的回滚如下:

  • 事务回滚
  • 代码库回滚
  • 部署版本回滚
  • 数据版本回滚
  • 静态资源版本回滚。

7 压测与预案

在大促来临之前,研发人员需要对现有系统进行梳理,发现系统瓶颈和问题,然后进行系统调优来提升系统的健壮性和处理能力。一般通过系统压测来发现系统瓶颈和问题,然后进行系统优化和容灾(系统参数调整、单机房容灾、多机房容灾等)。

系统压测

压测一般是指性能压力测试,用来评估系统的稳定性和性能,通过压测数据进行系统容量评估,从而决定是否需要进行扩容或缩容。

压测之前要有压测方案(如压测接口、并发量、压测策略[突发、逐步加压、并发量]、压测指标[机器负载、QPS/TPS]、响应时间[平均、最小、最大]、成功率、相关参数[JVM参数、压缩参数]等),最后根据压测报告分析的结果进行系统优化和容灾。

参考来源:
[1] 亿级流量网站架构核心技术.张开涛著

你可能感兴趣的:(高可用,缓存,并发,性能,读书笔记,Just,Coding)