服务高可用实践总结
服务100%可用是不可能的, 我们只能追求尽量接近100%的可用时间, 通常的衡量标准是几个9:
| 标准 | 年度停机上限 | 场景 | 说明 |
|---|---|---|---|
| 99% | 87.6小时 | 辅助服务 | 可用性不敏感 |
| 99.9% | 8.8小时 | 常规服务 | 大部分的常规服务追求3个9就够了 |
| 99.99% | 53分钟 | 敏感服务 | 标准较高, 关键服务需要满足此要求 |
| 99.999% | 5分钟 | 核心服务 | 例如特殊场景下的核心服务: 金融交易, 硬件 |
| 99.9999% | 31秒 | 重大关键基础服务 | 关键公共基础服务, 如电力系统, 通讯系统等 |
普通互联网企业的服务基本在追求99.9%, 99.99%, 本文总结部分互联网服务实现高可用目标的常见措施. 这些措施基本围绕如下方面提高可用性:
避免发生 -> 快速发现 -> 控制影响 -> 快速恢复
避免故障发生
| 方案 | 解释 |
|---|---|
| 服务集群化, 避免单点 | 物理机虽然稳定, 但是如果服务仅部署在一台机器上, 则不可避免的会遇到: 1. 服务升级; 2. 机器故障, 如磁盘, 网络, 电力. 因此现代服务都是由多个实例组成的集群, 单个实例故障不影响整体的可用性. |
| 故障隔离, 避免雪崩 | 需要有措施隔离下游问题服务, 避免影响蔓延. 例如配置短路器, 在探测到下游故障时, 打开短路器, 并按照指数回退的方式重试, 以便在依赖方重新上线后快速恢复. |
| 快速失败 | 快速失败分为本服务故障和依赖方故障. 如果启动条件不满足, 则应该避免服务启动, 并打印日志, 报告错误; 如果是依赖方故障, 则要配置超时, 不能一直等待返回, 也就是需要和依赖方商量sla, 不满足sla时及早返回. |
| 模块/服务解耦 | 解耦可以带来很多好处, 例如独立升级/修改/回滚等, 也带来了维护的复杂性, 毕竟需要维护更多的模块/服务. 解耦的方式是多样的, 例如异步调用可以用异步api, 多mq. 同步调用可以拆分服务(微服务), 共享内存(游戏行业常见)等. |
| 无状态服务 | 将服务本身配置为无状态, 状态信息存储在独立的系统(如mysql cluster, redis cluster)可以带来很多好处, 例如可以快速启动, 随时迁移, 方便服务调度/部署等, 我遇到的大多数服务都是/可以配置为无状态的. |
| 方案选择: 简单可依赖 | 在做技术决策时, 一个重要的原则是奥卡姆剃刀原则. 如果多种备选之间优缺点难分伯仲, 则优先选择简单的方案, 简单往往意味着可靠性高. |
快速发现故障
按照故障根因(root cause)发生位置, 基本可以分成:
调用方故障 -> 本服务故障 -> 被依赖方故障.
| 方案 | 解释 |
|---|---|
| 服务可监控 | 基本上所有的服务都应该做到可监控, 最常见的方式是打点(metrics), 日志(log), 并将实时状态可视化的展示出来, 常见的工具有, elk家族, grafana/prometheus, opentracing, zipkin, datadog等. |
| 服务可测试 | 按照面向对象方式编写的代码比较容易单元/集成/压力/回归测试, 每个版本在上线之前需要跑一遍回归测试, 有条件的可以常态化压力测试, 以最大限度的在上线前发现问题. 如果条件允许, 还可以在发布时开启小流量测试, 例如采用灰度/金丝雀/蓝绿/AB部署的方式, 让一小部分流量先使用新版服务, 观察一段时候之后再全量发布. |
控制影响范围
如果是调用方故障:
| 方案 | 解释 |
|---|---|
| 配额限流 | 采用自适应/启发式规则, 对调用方进行限流, 常见的实现方式是令牌桶/漏斗等, 实现上可以使用redis计数, key采用caller+时间戳, value是调用计数, 超过上限则限流生效. 防止上游的洪峰击垮自己. |
| 异步返回 | 解耦之后, 服务间的调用可以采取异步的方式进行返回, 最常见的如红包, 热点账户, 秒杀等, 都可以使用mq削峰. 请求暂时先放到mq中, 服务方按照自己的容量, 逐一处理mq的请求. 异步处理容易出错, 需要谨慎使用. |
| 重复请求幂等处理 | 如果服务自身不允许重复调用, 例如转账操作, 则应该强制要求幂等, 具体方式是调用时传入唯一id, 多次重复调用保证使用唯一id做去重. 还可以利用底层存储的唯一索引做检查, 尝试插入重复的唯一索引会报错. |
| 并发请求限制并发度 | 并发控制和配额比较类似, 区别在于并发控制在于限制同时发生的请求数量, 配额是限制时间段内的请求总数. 并发控制常见的方案是加锁, 例如限制并发为100, 则在处理请求前先抢锁, 成功后才能处理请求, 否则自旋等待. |
| 请求积压丢弃 | 对于实时性要求比较高的场景, 如果出现请求挤压, 例如jetty的等待队列超过阈值, 则可以配置丢弃规则, 直接返回预定义的错误码. |
本服务故障:
| 方案 | 解释 |
|---|---|
| 发布可回滚 | 服务在上线之前尽可能地想到回滚预案, 一旦发现异常, 最常见的操作就是回滚更改. |
| 灰度/蓝绿发布 | 见上. |
| 服务/数据单元化. | 单元化部署也是隔离故障的一种方式, 数据/服务均进行水平拆分, 效果上类似使用sticky session+小流量测试. 单元化成本较高, 非大厂不会采用此方案 |
| 隔离/禁用故障机器 | 有时我们会遇到非代码故障, 例如环境, 机器, 系统, 配置, 网络延迟等造成的故障, 则可以适时将机器摘除, 待恢复/重启后再加入集群. 这在分布式存储中比较常见, 例如后台进程检测脑裂/双主的发生. |
被依赖方故障: 例如相应超时, 返回错误码
| 方案 | 解释 |
|---|---|
| 服务可降级 | 非强/可禁用依赖在出问题时, 可以通过动态下发的配置暂时关闭访问, 待恢复时在动态打开. 多个依赖可以按照优先级进行梯度降级, 问题最严重时仅保留业务核心依赖, 其余非核心依赖的访问全部关掉. 可以暂时显著提高系统服务能力, 但是用户体验可能造成损失 |
| 依赖可替换 | 按照容灾的思路, 强依赖有备份时可以随时切换到备选方案, 例如切换通知mq, 切备库等. |
快速恢复服务
| 方案 | 解释 |
|---|---|
| 保留扩/缩容能力 | 在遇到突发流量, 系统资源吃紧的情况下, 可以通过紧急扩/缩容的方式缓解系统压力. 例如服务实例太少, 导致cpu打满则可以扩容. 下游连接数吃紧, 则可以上游缩容, 减少连接数. |
| 保留迁移实例能力 | 无状态服务都可以比较容易的迁移, 所以只要有足够灵活的检测/迁移机制, 则迁移发疯的实例比较容易, 这个操作其实和电脑的重启有异曲同工之妙 |
以上是我所经历过的微服务高可用方案总结, 部分参考了网上的一些文章.