服务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打满则可以扩容. 下游连接数吃紧, 则可以上游缩容, 减少连接数.
保留迁移实例能力 无状态服务都可以比较容易的迁移, 所以只要有足够灵活的检测/迁移机制, 则迁移发疯的实例比较容易, 这个操作其实和电脑的重启有异曲同工之妙

以上是我所经历过的微服务高可用方案总结, 部分参考了网上的一些文章.