限流方案
安全:如何设计高吞吐和大流量分布式集群的限流方案?
消息队列系统安全的第二部分:数据自我保护和服务自我保护。
- 数据维度的自我保护主要指如何保证服务端数据的安全,不被窃取。
- 服务维度的自我保护主要指集群的限流机制,通过限制客户端的流量、请求、连接等保护自身不被击垮。
集群中的数据加密
当客户端发送消息 A(比如 hello world)到 Broker,Broker 保存到磁盘的数据是一串内容 A 加密后的字符串,当消费端消费数据时,Broker 将加密后的字符串解密成消息 A 返回给消费端。
实现这个效果一般有以下两种方案:
- 由服务端自动化做好加密解密。工作量全在服务端,客户端没有任何工作量。
- 客户端在生产的时候自助做好加密逻辑,在消费的时候自助做好解密操作。好处在于消息队列服务端没有任何工作量,坏处在于工作量全部在客户端,所有的客户端和消费端都需要感知加密的逻辑,在编码和协调各方的成本方面较高。
不过目前业界主流开源消息产品都是不支持消息加密的,我们无法用第一个方案,但一些闭源的商业化消息队列产品具备这个能力
如果需要第一种方案,技术上要怎么实现呢?
主要分为三步
- 首先,把接收到的数据解析为生产端发送出来的原始消息格式。需要解析是因为客户端可能设置了批量发送、压缩等行为,导致服务端收到的格式和原始的消息不一致。
- 其次,对这些消息进行加密,并存储到磁盘。
- 最后,在客户端消费数据时,把加密的消息解密成为原始接收到的消息格式返回给客户端。
因为消息队列需要较高的性能,并且数据的加解密都是在服务端内核完成的,安全性较高。所以我们一般选择对称加密算法,比如 AES。
如何保护好集群的服务不会因为外力原因而崩溃呢
服务限流、服务降级。
消息队列限流机制思考
业界在限流方面的理论结构和开源组件也都非常丰富。限流主要包含两个部分,限流算法和实现机制。我们先了解一下不同机制的具体实现和优缺点,最后会针对具体案例,实战设计限流方案。
在业务系统中,这四种算法都比较常用。因为消息队列经常需要应对突发流量,需要尽量平滑的限流机制,所以从实现上来看会比较推荐令牌桶算法。
限流的实现机制主要分为两种:单机限流和全局限流。
单机限流就好理解了,限流配额发送到单机维度,在内存中完成计数、比对、限流决策。它的优点是在单机内存内完成限流逻辑的闭环,几乎不影响主流程的耗时。缺点是集群部署时,无法在多台节点之间共享集群信息,从而导致无法进行集群维度的限流。
全局限流,一般基于第三方的集中式服务来实现分布式的多机限流。在集中式服务中完成配额的记录、限流判断等行为,各个服务节点,通过对中心服务的上报和访问完成限流。在第三方组件的选择上,主要有 Redis、Sentinel、ASAS、PolarisMesh 等。优缺点跟单机限流刚好相反,能在多节点之间完成限流信息的共享,但是在限流操作上的耗时较高。
当前主流消息队列产品,主要选择还是单机限流机制,比如 Kafka、Pulsar。思路是:放弃集群维度的精准限流,将集群总的配额根据节点数据量均分到每个节点,在每个节点内部完成单机限流。
这种方案的好处就是限流的逻辑对耗时无影响,另外主流程不会依赖第三方服务,不会因为第三方服务的稳定性问题导致主流程不可用。
缺点是当写入、读取出现倾斜时,会出现单机维度达到限流值,集群维度却没有达到限流值的问题。这个问题,我们可以通过不断地动态调节单机限流配额的方式,尽量提高限流精度,但是这也无法根本解决毛刺、限流不精准等问题。
限流方案的选择,我会建议优先使用全局限流的机制,支持临时开启关闭限流能力、支持限流策略降级结合的机制, 让某些延时敏感的客户或者限流 Server 异常的时候,支持关闭限流或者降级为单机限流。
对哪些资源和维度进行限流
从消息队列的特性上来看,主要对流量、连接数、请求数三类资源进行限流,有些消息队列还会对 CPU 和内存进行限制。限制的维度一般包括:集群、节点、租户 / Namespace、Topic、Partition、Group/Subscribe 六个维度。
流量限制指对生产、消费的流量限制,是消息队列的核心限流指标。因为很多问题都是流量波动引起的,限制好集群的流量,很大程度上能保证集群的稳定。所以你会在各款消息队列里看到对流量的限制。一般会对集群、节点、租户 / Namespace、Topic、Group/Subscribe 这几个维度配置。
连接数限制指对客户端连接到服务端的 TCP 连接数量进行限制。因为 TCP 连接的建立和关闭需要消耗 CPU、内存等资源,限制是为了保护服务端不会因为连接数太多,耗尽资源,导致服务不可用。虽然现在技术上的网络编程有异步 IO、多路复用等技术,但是连接太多还是会出现问题。所以 RabbitMQ 在连接的基础上设计了 Channel 信道,避免 TCP 连接频繁建立关闭、TCP 连接数太多。
请求数限制指对单个接口的访问频次进行限制,来保护集群自身的可用性。比如消息队列中的获取元数据(Lookup、寻址)接口,这个接口一般需要返回所有 Topic、分区、节点的数据,需要做很多获取、组合、聚合的操作,很消耗 CPU。在客户端很多的情况下,如果客户端同时更新元数据,很容易把服务端的 CPU 耗完,导致集群生产消费异常。请求限流就能起很好的保护作用。连接数的限制一般在集群、租户 / Namespace 两个维度配置。
发生限流后怎么处理
按正常的逻辑,限流发生后,肯定是拒绝请求或者流量了。但注意,在消息队列里,我们是需要分情况来考虑的。
消息队列本身的功能是削峰填谷,在有突发流量的时候,流量很容易超过配额。此时,机器层面一般是有能力处理流量的,如果直接拒绝流量,就会导致消息投递失败,客户端请求异常。所以,在限流后,我们一般有两种处理形式。
- 返回超额错误,拒绝请求或流量。
- 延时回包,通过加大单次请求的耗时,整体上降低集群的吞吐。因为正常状态下,客户端和服务端的连接数是稳定的,如果提升单次处理请求的耗时,集群整体流量就会相应下降。
Kafka 的主要处理机制是延时回包。延时回包的优点是可以承载突发流量,当有突发流量时,不会对客户端造成严重影响,缺点是无法精准限制流量。
延时回包的实现比较复杂,收到流量后,需要根据当前的延时回包算法,计算一个延时回包的时长,然后把回包信息放入到延时回包队列(延时回包队列的实现一般是使用时间轮的方案),等过了延时回包的时间,再给客户端回包
消息队列全局限流设计
了解了各种机制的优缺点和具体实现思路,接下来我们来实操综合运用一下,如何为一个消息队列集群设计实现单机限流方案和全局限流方案。
- 计算单节点的配额。
- 存储每个节点的配额信息,以免节点重启后配额信息丢失,比如 Kafka 和 Pulsar 都是存储在 ZooKeeper 上。
- 为每个节点下发变更配额信息,节点在重启的时候加载配额信息。
- 当生产消费的时候,在内存存储计算流量,并和配额数据进行比较,确认是否限流。
单机限流还有一个小优化,我们可以实时监控每台节点的限流情况,动态修改每台节点的配额。通过判断,给流量较高的节点分配较多的配额,给流量较少的节点分配较少的配额,从而在流量倾斜的时候,也能够做到较为精准的限流。
全局限流方案
全局限流方案思考的核心有三点。
- 对当前主流程不能影响或者影响极低。
- 限流的精度需要仔细权衡,需要考虑限流是否足够精准,是否会有倾斜。
- 需要设计好回退措施,即限流组件抖动时,不能影响主流程。
步骤
- 首先选择一个集中式的限流 Server,你可以选择业界的全局限流的组件,比如 Sentinel 或 PolarisMesh,也可以是消息队列内置实现的一个全局限流的组件,简单点也可以是 MySQL 或者 Redis。
- 然后把组件中写入限流配额。
- 在生产和消费时,向限流 Server 记录配额信息,获取限流状态,判断是否进行限流。
- 同时根据单机限流的方案,在本地缓存一份均分的配额数据,当限流 Server 异常时,直接使用本地缓存的配额数据进行计算限流。
- 同时提供开关,在某些情况下可以关闭限流。
消息队列的服务降级
因为一些环境因素的影响,比如节点故障、机房故障或某些异常的攻击行为。可能导致一些限流策略无法生效,就会导致集群在一段时间内的负载很高或无法正常提供服务。此时我们需要进行服务降级,通过拒绝流量或者拒绝连接的方式,完成自我保护,以保证消息队列核心链路的功能正常使用。
常见的降级策略
- 配置 Broker 的 CPU 或内存的使用率额度,当使用率到达配额时,通过拒绝生产或消费流量的形式来保证服务的部分正常。通常会优先拒绝生产流量,因为大部分集群过载是生产流量过大引起的。此时禁止生产流量的写入,可以保证消费的正常,服务不至于崩溃,消费端可以及时消费掉积压的数据。RabbitMQ 就内置了这个策略。
- 配置磁盘保护机制,可以保护消费不会有异常。当真实的磁盘使用率使用达到一定的程度时,就禁止流量写入。因为在消息队列中,磁盘较容易被打满,打满的话如果还允许写入服务程序就会有异常,从而影响消费。
- 判断异常自动重启 Broker,通过自动判断服务的运行情况,决定是否重启 Broker。比如当发现频繁发生 Full GC 的时候,就自动重启自身服务,以达到回收资源的目的。这种方式用得比较少,因为比较危险,可能会导致集群中的所有 Broker 频繁重启。一般需要依赖第三方组件的多维度判断,以降低误重启的风险。
地址
此文章为9月day12 学习笔记,内容来源于极客时间《https://time.geekbang.org/column/article/684256》