稳定性平台的设计与实现( 二 )
(熔断本就是单节点视角的 , 无此问题 。 )
系统设计方面 , 我们决定:
一、管理后台与 SDK 完全独立 。 二者只约定好熔断、限流配置的存储方式即可(我们将配置保存在 etcd 中) 。
二、SDK 划分为核心 API 与外围 API 。 核心 API 包括熔断器、限流器的核心逻辑 , 而外围 API 则是对核心 API 封装 , 比如添加了从 etcd 中加载配置及监听配置更新等功能 。 外围 API 使得 SDK 与服务框架的整合更加简单 , 只需短短十来行代码即可 。 同时还保证了稳定性平台的独立性 。 而核心 API 则可用于其他特定场景 , 比如其中的 SlidingWindow 就被其他项目用于实现资源限额功能 。
具体实现上 , 核心 API 的行为基本上都有良好的定义 , 重点注意并发读写问题及性能即可 。 另外 , 我们支持监听配置更新 , 这就需要注意 , 配置的更新与当前内部状态的相互影响 。
熔断器熔断器持有一个滑动窗口 , 及其他一些内部状态 。 滑动窗口拥有 10 个 cell , 每个 cell 负责 1s 时长 , 熔断器以这 10s 的统计数据 , 以及每个请求的成败 , 驱动其状态迁移 。
由于访问下游服务的时间是不确定 , 请求完成时 , 熔断器可能已经发生了状态迁移 , 需要特别关照一下 。 比如 , 对下游的某次请求非常慢 , 发起时熔断器状态为 closed , 而请求完成时熔断器已经变为 open 甚至 half_open 状态了 。 对于 open 状态 , 直接忽略即可 。 而对于 half_open 状态 , 这个请求是不可用于探测下游服务是否恢复(称为 probe 请求)的 。 为了应对这种情况 , 就需要给每个请求标记是否为 probe 。
阻塞式限流器对于阻塞式限流器 , 我们选择通过漏桶算法来实现 。 需要注意的是 , 一不小心的话 , 实现的漏桶算法可能等价于令牌桶算法 , 维基百科 Leaky bucket 中也有提到 。 这样的话 , 漏桶能够避免突发流量的优点就没有了 。
我们的实现逻辑是这样的 。 每个 LeakyBucketRateLimiter 持有一个 channel , 并有一个 leak() 方法完成「漏」的动作 。leak() 运行在单独的 go routine 中 , 它根据 LeakyBucketRateLimiter 的 qps 阈值 , 计算 tick , 在每个 tick 的时间间隔里 , 漏掉“一滴水” 。 所谓漏掉“一滴水” , 实际上是 channel 中读出一个元素而已 。 而读出的元素的类型也是 channel ,leak() 将其读出之后 , 会往其中写入一个值 , 以唤醒可能处于阻塞中的调用方 。
而调用方调用的方法为 Limit(), 它创建一个 channel , 并将之写入到 LeakyBucketRateLimiter 的 channel 中 。 写入后 , 调用方会读取其创建的 channel 。 这里调用方可能阻塞在两个地方 。 一个是往 LeakyBucketRateLimiter 的 channel 写入时 , 如果已满 , 将会阻塞在写入操作上 , 只有当 leak() 从 LeakyBucketRateLimiter 的 channel 读出数据从而使之不满时 , 才会解除阻塞 。 第二个阻塞的地方是 , 调用方读取其创建的 channel 时 , 如果写入 LeakyBucketRateLimiter 的 channel 已成功 , 但 leak() 尚未处理写入的 channel 时 , 调用方将阻塞 , 直接 leak() 处理完毕 。
通过以上逻辑 , 我们确保调用 Limit(), 不会超过 QPS 阈值 , 若有可能超出则将之阻塞直到不超 。
为了应对配置变更的情况 , 需要一些额外处理 。 当配置的 qps 阈值变化后 ,leak() 的 tick 会发生变化 。 我们的做法是增加一个 change channel , 当 qps 阈值发生变化之后 , 立即向 change channel 写入数据 。 而 leake() 通过 select..case 同时读取多个 channel , 一旦发现 change channel 有数据 , 便重新计算 tick 。
另外 ,leak() 运行在单独 go routine 之中 。 我们希望限流器使用完毕之后 , 能够释放所有资源 , 包括这个 go routine 。 因此增加了一个 close channel , 调用 Close() 方法时即向这个 channel 写入一个值 ,leak() 读取后立即退出 , 从而释放掉其所在的 go routine 。
最后 ,leak() 会在每个 tick 时「漏掉“一滴水”」 , 当 qps 阈值很大时 , tick 会很小 。 此时 timer 的精度和性能开销可能会变得突出(我们使用的是 time.Tick() ) 。 tick 非常小的时候 , timer 的精度可能不足 , 并且限流器本身的性能开销变得不可忽略 , 从而影响限流的效果 。 当然 , 限流的场景下 , 定时器精度不是关注的重点 , 此时需要注意的是性能问题 。 要优化掉这个问题 , 也不麻烦 。 只需要将「每个 tick 漏掉一滴水」改为「每个 tick 漏掉 N 滴水」即可 。 我们可以选择 timer 支持的 tick(越小越好) , 并计算这个 tick 时间段内应漏出的水滴数量 。 这本质上是精度与性能的权衡 。
核心代码如下:
- 智能手机市场|华为再拿第一!27%的份额领跑全行业,苹果8%排在第四名!
- 会员|美容院使用会员管理软件给顾客更好的消费体验!
- 行业|现在行业内客服托管费用是怎么算的
- 人民币|天猫国际新增“服务大类”,知舟集团提醒入驻这些类目的要注意
- 国外|坐拥77件专利,打破国外的垄断,造出中国最先进的家电芯片
- 信息|澜湄合作机制开通水资源合作信息共享平台
- 技术|做“视频”绿厂是专业的,这项技术获人民日报评论点赞
- 互联网|苏宁跳出“零售商”重组互联网平台业务 融资60亿只是第一步
- 面临|“熟悉的陌生人”不该被边缘化
- 中国|浅谈5G移动通信技术的前世和今生