单点是如何变成事故的
绝大多数线上事故的起点都很朴素:某个环节只有一台机器。这台机器半夜磁盘写满、进程被 OOM 杀掉、或者宿主机物理重启,整个服务就跟着挂了。它不是「会不会出事」的问题,只是「什么时候出事」的问题。
高可用要做的事,就是把这些单点一个个找出来、用「不止一份」的方式兜底。它不是上一套很贵的系统,而是一连串朴素决策的叠加:
- 每一层都至少两份:应用多实例、数据库主从、负载均衡器双机。
- 坏了要自动发现:靠健康检查把坏节点摘出去,别等用户来报。
- 挂了要自动切换:流量自动绕开故障节点,不依赖人工半夜登机器。
高可用的目标不是「永不出错」,而是「出错时用户几乎感觉不到」。
- 负载均衡把流量摊到多实例,是高可用最外层的那道闸门。
- 健康检查自动探测坏节点并摘除,是「自动切换」的眼睛。
- 无状态实例之间不存会话,才能随便扩缩容、随便摘挂。
- 故障演练主动杀实例、断网络,验证你的高可用是真的还是纸面上的。
负载均衡:流量怎么分发
负载均衡站在所有应用实例前面,替你把进来的请求分出去。它既做「分摊压力」,也做「摘坏兜底」。常见分发策略各有适用场景:
| 策略 | 做法 | 适用 |
|---|---|---|
| 轮询 | 按顺序一个接一个分发 | 实例配置相近、请求耗时均匀 |
| 加权轮询 | 按机器性能分配不同权重 | 实例规格不一时 |
| 最少连接 | 发给当前连接数最少的实例 | 请求耗时长短差异大时 |
| 一致性哈希 | 同一用户固定落到同一实例 | 有本地会话、需会话粘滞时 |
以 Nginx 为例,一个最基础的反向代理加负载均衡配置长这样:
upstream backend {
least_conn;
server 10.0.0.11:8080 max_fails=2 fail_timeout=10s;
server 10.0.0.12:8080 max_fails=2 fail_timeout=10s;
server 10.0.0.13:8080 max_fails=2 fail_timeout=10s;
}
server {
listen 80;
location / {
proxy_pass http://backend;
}
}
注意 max_fails 与 fail_timeout:它的意思是 10 秒内失败 2 次就把这台实例暂时标记为不可用,一段时间后再试探恢复。这就是下一节要讲的健康检查的雏形。
健康检查:自动摘掉坏节点
一台实例进程还在但已经不服务了——连接池打满、线程死锁、后端依赖全挂——这种「半死不活」最坑。靠请求失败计数去摘节点太慢,正确做法是主动健康检查:负载均衡器定期主动请求一个轻量探测接口,连续失败就摘,恢复了就加回来。
- 探测什么:一个独立的
/health接口,只检查本进程是否活着,不要顺带查数据库——否则数据库一抖,所有实例全被误摘。 - 探活频率:间隔几秒一次,失败连续两三次才摘,别因一次抖动就误伤。
- 恢复探测:摘掉后还要定期试探,一旦恢复自动加回,别摘了就再也回不来。
这里有个经典反模式值得单独说:健康检查接口去依赖下游服务。你的 /health 里如果调了数据库和缓存,那么下游一抖动,负载均衡会把你整池实例都摘掉——本可以局部降级的故障,被你放大成了全站不可用。健康检查只回答「我自己起没起得来」,不回答「世界好不好」。
无状态:水平扩容的前提
负载均衡把流量分多台实例,有个隐含前提:任何一台实例都能处理任何一个请求。如果某台机器本地存着用户登录会话,下次请求被分到另一台,用户就被踢下线了——这就是「有状态」挡在高可用前面的那道墙。
无状态化的几条实操:
- 会话外置:登录态放 Redis,而不是应用内存;任何实例拿到 token 都能验。
- 本地文件外置:上传文件进对象存储,别落单机磁盘,否则这台机器一摘文件就丢。
- 缓存谨慎:进程内缓存要能接受「这台没有就回源」,别把关键数据只放单机内存。
做到无状态之后,扩容缩容才变成「加台机器、健康检查过了就接流量」这么简单,摘挂也才真正安全。
熔断、限流与降级三件套
多实例兜的是「机器挂了」,但还有一类故障是「下游慢了、流量爆了」。这时靠的是三件应用层的防身术:
- 熔断:调用下游连续失败超过阈值,就暂时停止调用它,直接走兜底逻辑,等一段时间再试探恢复。防止被慢下游拖垮自己的线程池。
- 限流:给自己设一个处理能力上限,超过的请求快速拒绝或排队,宁可少接也不被压垮。保护自己也保护整个系统。
- 降级:非核心功能在压力大时主动关掉——推荐位不返回、评论区暂存——保住核心交易链路。
三者的共同思想是:局部故障别扩散成全站雪崩。一个下游服务变慢时,正确反应不是无限等待把自己拖死,而是快速失败、走兜底、把资源留给还能救的请求。
混沌演练:主动注入故障
「我们有三副本,高可用没问题」——这句话在没杀过机器之前都只是假设。混沌演练就是主动把故障制造出来,验证你的高可用到底是真的还是纸糊的。
# 最朴素的演练:随机挑一台 web 实例,直接停掉它
# 观察:负载均衡是否在几秒内把它摘除?
# 剩余实例能否扛住全部流量?
# 监控和告警有没有第一时间报出来?
# 几分钟后重启,它能否自动加回?
常见的演练动作:随机杀一个实例、给某台机器打满 CPU、断开一段网络、让某个依赖接口超时。从非核心服务、业务低峰期开始,逐步加难度。每次演练后记录:故障多久被发现、多久被摘除、用户是否感知、哪一步暴露了预案缺口。
高可用不是搭出来就结束的,是一次次演练打补丁打出来的。没演练过的多副本,等于赌事故当天不会被检验。
一份高可用架构自查清单
把上面所有原则压成一张可以逐项打勾的清单,定期对照检查:
| 检查项 | 合格标准 |
|---|---|
| 应用实例数 | 核心服务至少 2 个实例,跨物理机部署 |
| 负载均衡 | 入口有分发器,且自身也有主备 |
| 健康检查 | 有独立轻量探测接口,坏节点自动摘除与恢复 |
| 会话与文件 | 登录态、上传文件均已外置,实例可随便摘挂 |
| 下游保护 | 对慢依赖有熔断、限流,非核心功能可降级 |
| 数据库 | 主从复制,主库故障有明确切换预案 |
| 故障演练 | 每季度至少一次杀实例演练,并有复盘记录 |
自查时最容易漏的是最后两条——保护自己的应用层策略和定期演练。把这两件事补上,你的系统才真的从「单点赌命」走到「出了事也扛得住」。高可用没有银弹,它就是把每一个单点都问一遍「它挂了会怎样」,然后逐个给出答案。