1. 重新理解高可用:三件事,一个目标
做后端开发和架构设计这些年,我被问得最多的问题之一就是:你的系统到底怎么保证高可用?"高可用"这三个字,在简历里人人都会写,但在真实的生产环境里,真正经得起故障演练、扛得住突发流量、能在凌晨三点数据库宕机时依然不丢请求的系统,我见过的不算多。
先说一个我自己的观点:高可用不是某个组件、某条命令、某个配置文件能解决的,它本质上是三件事的协同设计——无状态化、水平扩展和故障转移。这三件事单拆开看,每一件都不算难,网上资料一大把;难的是把它们放在同一个系统里,让它们互为前提、互相兜底。很多高可用方案做失败,不是因为某一件事没做,而是因为三件事之间出现了断层——比如应用做了无状态化,但数据库还是单点;比如水平扩展做了,但扩展完发现 session 还黏在原来的节点上;比如故障转移做了,但切换之后应用连接池里的旧连接全废了。
这篇文章我就把这"三件事"从头到尾拆一遍,包括每一件背后的原理、常见的落地方式、以及三者如何协同设计。我还会结合我实际接触过的场景——Web 服务集群、MySQL 高可用、SQL Server 高可用同步、集群故障转移——把具体的配置思路和踩坑经验也一并分享出来。适合正在做架构设计、运维保障、或者准备把单机系统改造为高可用集群的同学参考。
1.1 高可用的本质是"把不可用时间压到最低"
我们聊高可用,其实聊的是一个非常朴素的指标:系统一年到头能正常服务的时间占比,也就是常说的"几个九"。99%(两个九)意味着一年大概宕机 3.65 天,99.9%(三个九)是 8.76 小时,99.99%(四个九)是 52.6 分钟。很多业务其实连三个九都做不到,不是因为技术不行,而是因为架构层面压根没把高可用当成一个系统工程来做。
要把不可用时间压到最低,光靠"买好机器""用好硬件"是没有出路的,因为任何硬件都会坏,任何软件都会有 bug,任何机房都可能出事故。真正靠谱的思路只有一个:让系统在部分组件失效的情况下,仍然能对外提供服务。这就是无状态化、水平扩展和故障转移共同指向的那个目标。无状态化解决的是"任何一个节点都可以被替换",水平扩展解决的是"有足够多的节点可以分担和接替",故障转移解决的是"节点失效后,流量能自动切换到健康节点上"。
1.2 三件事不是三个独立方案,而是环环相扣
我见过不少团队,把这三件事当成三个独立的优化项来推进:先做无状态化,做完就以为高可用搞定了;后来又加负载均衡做水平扩展,扩展完发现故障转移靠人工盯告警;再后来配了故障转移,结果切换时才发现应用层根本没准备好。这三件事其实是环环相扣的——
- 无状态化是水平扩展的前提。如果应用节点持有本地状态(比如 session 存在本地内存),你扩到十个节点,用户的下一个请求被负载均衡分到另一台机器,状态就丢了,扩展不仅没提升可用性,反而制造了新故障。
- 水平扩展是故障转移的基础。如果只有一台应用节点,故障转移做得再漂亮也无处可转;只有集群里存在多个健康节点,故障转移才有意义。
- 故障转移是无状态化和水平扩展的闭环。无状态化和水平扩展解决了"正常情况下如何分摊流量"的问题,故障转移解决的是"异常情况下如何把流量交给健康节点"的问题。没有故障转移,前面两件事做得再好,遇到单点硬件故障时系统还是会停摆。
所以设计高可用架构时,千万不要一个一个地做,而是要把这三件事放一起通盘考虑。下面我分别拆开讲,最后再讲怎么协同设计。
2. 无状态化:把"记忆"赶出业务节点
2.1 什么是状态,为什么状态是高可用的天敌
先明确一个概念。在分布式系统里,"状态"指的是一份数据在某个节点上被持久化地保存,并且后续请求的处理依赖这份数据。最常见也最典型的状态就是用户会话(session)。一个用户登录之后,服务器在本地内存里记下他的登录信息,后续请求带着 cookie 或 token 过来,服务器从本地内存里找到对应的 session,识别出"这是同一个用户"。听起来很自然,对吧?
但问题来了:如果这台服务器挂了,对于那些 session 存在它内存里的用户来说,他们的登录状态就丢了,请求被负载均衡转发到另一台服务器,另一台服务器查不到这个 session,会直接把用户踢回登录页。用户侧的感受就是"系统突然要我重新登录",这在生产环境里就是不可用。更麻烦的是,如果你把负载均衡策略配成"按用户 IP 哈希"或者"按 session ID 哈希",强行把同一用户的请求都打到同一台机器上,那这台机器的负载就会变成一个热点,其他机器闲着,这台机器累死,所谓水平扩展变成了"伪扩展"。
除了 session,以下这些也属于"状态"的范畴:应用节点本地磁盘上的临时文件、进程内存里的业务计数器、节点本地缓存等。凡是"只有某一台节点拥有、其他节点读不到、并且业务依赖它的数据",都是高可用的天敌。
2.2 把状态"踢出去"的三种手段
无状态化的核心思路一句话就能概括:让业务节点变成"无记忆"的纯计算节点,所有需要保留的数据都放到外部共享存储里。具体来说,有三种常见手段:
**第一,Session 外置。**把原本存在内存里的 session 挪到 Redis、Memcached 或数据库里。所有应用节点都去同一个 Redis 集群读写 session,任何一台应用节点处理请求都能拿到完整的会话数据。这样任意节点宕机,用户请求被转发到其他节点后,session 还能从 Redis 里取回来,登录状态不丢。我当时改造过一个老项目,session 从本地内存迁到 Redis,代码改动其实很小,引入一个 session 管理库,配置连接信息,再改一下 session 存取路径,半天就完成了,但可用性提升是立竿见影的。
**第二,本地文件外置。**业务产生的临时文件、上传的图片、导出的报表,不要往应用节点的本地磁盘写。本地磁盘有两个问题:一是节点扩容时,文件不会自动同步到新节点;二是节点宕机被替换时,本地文件随机器一起消失。正确做法是把文件放到对象存储(比如 MinIO、云上的 OSS/S3)或者分布式文件系统中,应用节点只负责处理文件的读写请求,不持有文件本体。
**第三,业务数据集中化。**应用节点内的本地缓存,能不用就不用,非要用也要做好"缓存失效广播"的机制,否则一旦节点数多了,缓存一致性会变成一场噩梦。正式的缓存应该放到独立的缓存集群(Redis Cluster 或者 Hazelcast 这类分布式缓存)里,由集群自身去保证数据分布和副本。
提示:无状态化并不是说系统里不允许有状态,而是说状态要收敛到专门的、具备自身高可用能力的组件上。Redis、数据库这些组件本来就是有状态的,它们的高可用靠后面要讲的"故障转移"来保证。无状态化说的是"业务节点这一层"不要持有状态。
2.3 无状态化改造的实操要点
无状态化改造看似简单,但实际操作中容易踩坑。我列几个自己亲身踩过的:
**连接池的管理。**应用节点把状态外置之后,每一个节点都会同享地连接 Redis 或数据库,连接池的连接数上限、空闲超时、最大等待时间这些参数要提前规划。10 个节点和 1 个节点,对后端连接数的压力完全不同。我记得有一次改造完 session 外置后,Redis 的连接数一夜之间涨了 8 倍,Redis 默认的 maxclients 直接被打满,反而引发了新的故障。
**配置的收敛。**无状态化不只是数据层面的,配置也算一种"状态"。每台应用节点上散落的配置文件(比如数据库地址、第三方服务的密钥)要收敛到配置中心(比如 Nacos、Consul、etcd + confd)里统一管理。否则节点扩容时,新拉起一台机器,还得手动改配置文件,一旦配错,新节点一上线就带着错误配置加入集群,危害更大。
**随机性和本地性操作的排查。**改造完之后,要专门做一轮代码审查,检查有没有"往本地磁盘写东西""用了本机 IP 拼 URL""依赖了本机时间戳做业务主键"之类的隐性状态。特别是"本机 IP 拼 URL"这种操作,单机部署时没啥问题,一旦水平扩展,回调地址全打到同一台机器上,轻则功能异常,重则雪崩。
无状态化做完之后,你会得到一个非常清爽的结论:**应用层任意一台机器随时可以宕机、随时可以被替换,业务不受影响。**这是后面所有高可用设计的地基。
3. 水平扩展:从"一台机器扛所有"到"集群分摊压力"
3.1 垂直扩展的天花板与水平扩展的逻辑
无状态化做完之后,紧接着的问题是:一台机器的处理能力是有上限的,怎么突破?
很多团队的第一反应是"加配置",也就是垂直扩展——把 CPU 从 4 核升到 32 核,内存从 8G 升到 64G,磁盘从 SATA 换成 NVMe。垂直扩展的好处是简单,不改造代码,业务毫无感知。但它的天花板很明显:硬件是有上限的,双路 CPU 的服务器最多就那么多核,内存插满也就那么大的容量;而且越往上走,成本不是线性增长而是指数级增长,一台顶配服务器能买好几台中端配置的机器。
水平扩展的思路完全反过来:**不追求单台机器更强,而是让一堆普通机器一起工作,每台机器分摊一部分流量。**核心机制就两个:负载均衡把请求分发到多台节点上;节点之间无状态、可互换。我拿一个非常直观的生活例子来类比:一家餐厅只有一个厨师,客人一多就只能排队,这是垂直扩展思路(给厨师加薪让他干活更快,但一天就那么多时间);水平扩展是再请几个厨师,一起在厨房做菜,来多少客人都能接得住。要让多个厨师协作不出乱子,前提是他们之间不需要共享"记忆"——比如不能出现"这个菜只有张厨师知道怎么做"的情况,这就是无状态化。
3.2 扩展的艺术:分层扩展与容量规划
水平扩展不是"把应用节点复制 N 份"那么简单。一套完整的系统通常分好几层:负载均衡层、应用层、缓存层、数据层。每一层的扩展方式各不相同:
- 负载均衡层:部署多台 Nginx/HAProxy,前端再挂虚拟 IP(VIP)或者 DNS 轮询,保证负载均衡器本身也不成为单点。
- 应用层:这是最方便扩展的,因为经过无状态化改造之后,应用节点就像"即插即用"的积木,新起一个节点、注册到服务发现或负载均衡池里,就能立刻接流量。
- 缓存层:单机 Redis 有容量上限,可以升级为 Redis Cluster,把数据自动分片到多个主节点上,每个主节点再挂从节点。分片数量可以随着数据量增长而扩展。
- 数据层:MySQL 可以选择业务拆分(拆库)、读写分离(一主多从)、或者分库分表(ShardingSphere、MyCat)。SQL Server 则更多依赖 Always On 可用性组实现读写分离与高可用同步。
做水平扩展时,有个特别关键的考量:**先搞清楚瓶颈在哪里,再决定扩哪一层。**我见过不少团队,应用层 CPU 才用到 30%,就慌忙加应用节点,结果数据库连接被打满,整体性能反而更差了。正确做法是先压测、 先监控,确认瓶颈是 CPU、内存、IO、连接数还是数据库的查询慢,然后针对瓶颈层做扩展。
3.3 常见扩展方案的选型对比
我把自己实际用过的几种方案整理一下,供大家选型时参考:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| DNS 轮询 + 多应用节点 | 简单 Web 服务 | 配置简单,几乎零成本 | DNS 缓存导致切换不实时,故障时无法自动摘除 |
| LVS/HAProxy + 多应用节点 | 中等规模集群 | 调度能力强,健康检查灵活,故障节点自动摘除 | 负载均衡器自身需要做主备部署 |
| Nginx + 应用节点 | 大部分 Web 场景 | 配置简单、自带健康检查和重试机制 | 单台 Nginx 是潜在单点,需要前置 LVS 或 DNS 冗余 |
| Redis Cluster | 缓存量大、吞吐高的场景 | 自动分片、自动故障转移,扩展方便 | 客户端需要适配 Cluster 协议 |
| MySQL 主从 + MHA/MGR | 数据库高可用 | 主从切换相对成熟,读写分离效果好 | 切换有秒级延迟,主库故障时仍有短暂不可用 |
| SQL Server Always On | 微软技术栈、强一致性要求 | 同步提交模式下数据零丢失,可自动故障转移 | 对网络要求高,配置复杂度大 |
表格里这几个方案,其实正好对应了无状态化、水平扩展和故障转移这三件事在不同层次上的展开。接下来重点讲第三件事:故障转移。
4. 故障转移:让系统具备"自我修复"的能力
4.1 故障转移的两种模式:主动切换与被动发现
无状态化和水平扩展解决的是"正常情况下多节点分摊流量",但高可用最核心的临门一脚是:**某台节点突然挂了之后,系统能不能自动把它的流量交给别人。**这就是故障转移。
故障转移有两种常见模式,我分别说一下。
**主动切换模式(也叫做主备切换)。**这种模式下,系统里有两个角色:主节点(Active)和备节点(Standby)。正常时所有流量都进主节点,备节点处于待命状态,持续从主节点同步数据或配置。主节点出现故障时,监控组件(比如 Keepalived 的 VRRP、MySQL MHA 的 manager)检测到心跳丢失,就会把 VIP(虚拟 IP)从主节点摘下来,绑定到备节点上,同时对备节点执行"升级"动作。应用层完全感知不到 IP 变化,因为访问的始终是那个 VIP。这种模式适合数据库这类有状态组件。
**被动发现模式(也叫服务发现+健康检查)。**应用节点们把自己注册到注册中心(Nacos、Consul、etcd),负载均衡器定期对所有节点发起健康检查(HTTP 探活、TCP 探测、执行自定义脚本)。节点挂了,健康检查失败,负载均衡自动把它从可用节点列表里摘除;新节点上线,健康检查通过,自动加回列表。这种模式适合无状态应用层,因为无状态节点之间没有"主备"关系,所有节点都是平等的,任何一个挂掉,只需把它从分发列表里剔除即可。
无论是哪种模式,故障转移都包含三个关键动作:检测到故障、作出切换决策、执行切换动作。三个动作任何一个慢了或者错了,故障转移就不会成功。
4.2 数据库层的故障转移:MySQL 主从切换与 SQL Server 高可用同步
数据库是所有系统里最"有状态"的组件,也是故障转移最难做的部分。原因很简单:应用节点挂了,数据在别处,没啥影响;数据库挂了,数据就在它身上,切换不只是把流量转走,还要保证数据完整。我在实际工作中接触过的两个主流场景值得重点说说。
MySQL 高可用的常见落地。
MySQL 的高可用方案很成熟,网上资料也多,但落地时细节极其繁琐。从简单到复杂,大致有三个层次:
第一层:主从复制 + 手动切换。一个主库,一个或多个从库,开启 binlog,从库通过 IO 线程拉取主库的 binlog,写入自己的 relay log,再由 SQL 线程回放。这个方案的优点是完全免费、配置简单、对业务代码透明;缺点是主库宕机时,需要运维人员手动把从库提升为主库,再修改应用连接配置,整个过程动不动就是十几分钟到半小时,而且容易出错——比如提升之前忘了检查从库的回放进度,导致提升后丢失最近几秒的数据。
第二层:使用 MHA(Master High Availability)自动切换。MHA 是 MySQL 生态里老牌的高可用管理工具,它监控主库的状态,如果主库挂了,MHA manager 会在候选从库中选择一个数据最完整的节点,自动补全缺失的 binlog 事件,然后提升为新主库,并把其他从库重新指向新主库。使用 MHA 时,有一个重要细节:网络分区或者主库假死的情况下,MHA 可能会把"实际上还活着的旧主库"误判为宕机,触发切换,这就可能造成"双主"的脑裂问题。所以生产环境里,最好在切换脚本里加入一个"旧主库隔离"的步骤——确认旧主库已经被踢出集群之后,才允许执行提升动作。
第三层:使用 MySQL 组复制(MGR)。MGR 是官方提供的多主/单主强一致方案,底层用 Paxos 协议保证数据一致性。相比传统主从复制,MGR 的故障检测由集群内部完成,切换也更自动化、更迅速。MGR 的问题是配置门槛较高,对网络延迟敏感,而且某些 DDL 和事务存在限制(比如不支持一些隐式锁),需要业务侧做适配。
另外,现在很多人还会借助 Orchestrator 这类工具来做 MySQL 故障转移的编排,它比 MHA 更现代,拓扑感知能力更强,支持跨机房场景。工具选型上我的建议是:**不要盲目追求最新最炫的组件,关键是理顺故障转移的语义和操作步骤,然后做好充分的切换演练。**我见过一家公司,MHA 配好了半年没切换过,第一次真实故障时才发现 manager 所在机器和主库在同一台物理机上,主库断电的时候 manager 也连不上了,切换根本没法执行。
SQL Server 高可用同步实践。
微软技术栈里,与其对应的高可用方案就是 Always On 可用性组(Availability Group)。它的核心思路是把一组数据库放进一个"可用性组"里,组内有主副本和若干辅助副本,辅助副本通过同步提交或异步提交模式接收主副本的数据变更。启用"同步提交"时,事务在主副本上提交之前,必须等辅助副本确认已持久化日志,因此理论上可以做到零数据丢失的自动故障转移。
Always On 故障转移的基本流程是:主副本发生故障,可用性组监听器(Availability Group Listener)检测到失联,自动将某个辅助副本切换为主副本;由于监听器提供了固定的虚拟网络名,应用连接串指向监听器即可,切换之后应用不需要修改连接字符串。配置 Always On 时,下面几个坑是很多人都会遇到的:
- 域环境依赖。Always On 的自动故障转移通常依赖 Windows Server 故障转移集群(WSFC),而 WSFC 又依赖域环境。如果公司基础设施里没有域控,配置起来会非常痛苦。这种场景下,有时候只能退而求其次做手动故障转移。
- 同步提交的性能开销。同步提交模式对事务响应时间的损耗是实实在在的,尤其在跨机房的场景下,每次提交都在等网络往返。所以异地灾备副本一般会设置成异步提交,只有同城/同机房的副本才用同步提交。
- 见证(Witness)配置。Always On 做自动故障转移时,需要一个见证节点来防止脑裂。很多人在测试环境里不配见证,结果两节点之间网络抖动一下,集群就不知道应该选谁做主了。生产环境一定记得部署独立的见证节点(云虚拟机或共享文件夹都可以)。
数据库层的故障转移之所以难,归根到底是因为:**它不仅要解决"流量去哪儿"的问题,还要解决"数据不能丢"和"数据不能乱"的问题。**无状态化那一套在这些场景里用不了,数据库本身必须持有状态。应对办法就是依靠数据库内核提供的复制协议、仲裁协议和切换机制,把这些机制理解和配置到位,才能真正做好高可用的最后一环。
4.3 故障转移的边界:脑裂、数据丢失与切换条件
故障转移看起来是"把流量从坏的节点挪到好的节点",但实际执行时常常会遇到两个非常棘手的边界问题。
第一是脑裂(Split-Brain)。所谓脑裂,就是集群里两个节点都认为自己才是主节点,同时对外提供服务,导致数据双写、状态混乱。脑裂的根源往往是网络分区:主节点和备份节点的网络断了,心跳检测失败,备份节点以为主节点死了,于是把自己升级为主节点;但此时主节点其实还活着,还在对外提供服务,两个"主节点"同时存在,业务立刻乱套。解决脑裂的手段主要有两种:仲裁机制(比如引入第三方的 witness 节点投票,少数服从多数)和隔离机制(比如抢占共享锁/STONITH,把旧主节点强制关停)。无论是哪种,都在强调一件事:切换必须经过严格的合法性确认,宁可不切换,也不能双主。
第二是数据丢失风险与切换窗口。故障转移的另一个核心矛盾是:切换越快,越容易丢数据;切换越安全,耗时越长。拿 MySQL 主从来说,异步复制下从库往往落后主库几百毫秒甚至几秒,主库突然断电,从库切换后,最近那几百毫秒的事务就丢了。这种数据丢失对金融、交易类业务是完全不能接受的,所以这类业务通常要求半同步复制或者强一致方案(MGR、Galera),但付出的代价是写入性能下降。故障转移不是免费的,每一个"高可用"的承诺背后,都需要你用一致性和性能去买单。
因此,做故障转移设计时,我给自己定了一个原则:**先明确定义"可用性目标"和"一致性容忍度",再选择对应的复制模式和切换策略。**目标越清楚,方案越不容易做变形。
5. 三者的协同设计:从"单一方案"走向"高可用架构"
5.1 一个典型的协同架构如何搭建
无状态化、水平扩展、故障转移这三件事,拆开讲容易,协同起来才是真正的难点。我举个最常见的 Web 应用从单机演进到高可用集群的例子,把协同设计的完整过程走一遍,大家照着这个思路去套自己的场景就行。
假设最初系统是一台服务器:上面同时跑着 Nginx、应用进程(比如 Spring Boot / Go 写的服务)、Redis、MySQL。整个系统只有一个节点,任何组件出问题,系统就不可用。
第一步,应用层无状态化。把 Spring Boot 的 session 存储从内存切到 Redis,把上传文件从本地磁盘迁到 MinIO 或 OSS,把配置从本地配置文件切到 Nacos。这一步做完,应用进程本身变成了"无状态"的,可以随便复制。
第二步,负载均衡与水平扩展。在前面部署一层 Nginx,把流量分发到 N 个应用节点上。Nginx 配置 upstream,里面列出所有应用节点的 IP 和端口,开启健康检查。应用节点不够了,就新起一台机器,把代码部署上去,然后在 Nginx upstream 里加一条记录,reload 一下 Nginx 就完成扩容。应用层的水平扩展到这里基本就通了。
第三步,缓存和数据层的故障转移。Redis 从单机改为 Redis Cluster(至少三主三从),或者退一步用一个主一从 + Keepalived 的架构,保证缓存层有自动切换能力。MySQL 从单机改为主从复制 + MHA(或 Orchestrator),开启半同步复制,配置自动切换脚本和 VIP 漂移;SQL Server 则配置 Always On 可用性组和监听器,设置好见证节点。
第四步,全局演练与收尾。完整地把这三件事串起来之后,做一次故障演练:杀掉一台应用节点,看 Nginx 是否自动摘除该节点、流量是否平滑转移;杀掉 Redis 主节点,看 Redis Cluster 是否自动选出新主节点;杀掉 MySQL 主库,看 MHA 是否在 10~30 秒内完成切换、应用侧是否只是出现少量失败重试。
这个演进路径,每一步都是在给前面的步骤"填坑":无状态化让应用可以扩展,扩展让故障转移有地方可去,故障转移让整体架构有了自愈能力。三者协同的本质就是这么个逻辑。
5.2 协同设计中的几个关键参数考量
协同设计听起来有点虚,但落到实操上,确实有一些具体参数值得认真打磨。我把自己的经验集中列一下:
**注册中心/健康检查的超时与重试次数。**负载均衡的健康检查间隔建议设在 3~5 秒,失败阈值 2~3 次,成功阈值 2 次。间隔太短会频繁触发探活,对节点造成不必要的压力;太长则故障发现速度慢。Nginx 的proxy_next_upstream要配置为http_502 http_503 http_504,这样单次请求遇到后端异常时,Nginx 可以自动重试下一个节点,应用层看到的就是一次成功响应。
**会话超时与 Redis 连接池。**Session 外置到 Redis 之后,session 超时时间要按业务实际需求设置,不要随手设一个 30 分钟,否则 Redis 里堆积大量无效 session,导致内存膨胀。连接池参数方面,Redis 连接池的maxTotal建议按"单节点预计 QPS × 平均耗时 × 节点数冗余系数"来估算,比如一个节点大约 3000 QPS、平均获取连接耗时 0.5ms,理论上需要约 1.5 个并发连接,但为了留足余量,通常直接按 8~16 配置即可。我没见过几个系统真的需要几百个 Redis 连接,配置过大反而会打爆 Redis 的 fd 限制。
**MySQL 半同步复制的超时。**半同步复制有一个超时参数rpl_semi_sync_master_timeout,如果默认值(比如 10 秒)过大,主库写入在从库迟迟不响应时会一直等待,写入延迟飙升;如果过小,半同步会频繁退化成异步复制,失去"至少一份从库确认"的保障。生产环境我一般设置在 1000~3000ms 之间,性能和数据安全取一个平衡点。
**自动切换的触发条件。**不要一看到主库端口不通就立刻切换。很多时候进程卡死、磁盘接近写满导致心跳超时,并不代表主库彻底没救。合理的触发条件应该是:连续 N 次心跳失败 + 监控组件尝试了 M 次重连 + 排除网络抖动可能性,然后再执行切换。我见过一个团队,因为网络抖动导致误切换,切换后数据回放延迟,引发了一连串连锁故障,比不切还惨。
5.3 协同设计中的常见误区和避坑建议
协同设计做得多了,自然能总结出一些规律性的"坑"。这里我重点说三个:
**误区一:把"状态外置"等同于"用 Redis"。**Redis 本身是单线程模型,虽然快,但它也有自己的高可用问题(主从切换瞬间有秒级卡顿、持久化 RDB fork 时会短暂阻塞)。把 session 放进 Redis 之后,Redis 就成了新的单点。所以做无状态化时,必须同步考虑缓存层自身的高可用方案,比如 Redis Cluster、主从 + Sentinel 等。这一点最容易被忽略,因为大家的注意力都在应用层,很少有人会想到"状态被踢到了 Redis 里,Redis 挂了怎么办"。
**误区二:故障转移做完了就不做回归测试。**故障转移直接影响的就是线上稳定性,但很多团队只在初次配置时测一次,后面业务代码、数据库 schema、网络环境都在变,早就不是当初验证过的状态了。比如应用在代码里引入了对数据库的SELECT ... FOR UPDATE锁,切换之后由于主从延迟,应用可能读到旧数据或者锁等待,表现完全不一样。我自己的习惯是,每季度做一次故障演练,把主库切换、Redis 主从切换、应用节点摘除这几个动作全部实际执行一遍,确认监控告警是否都正常触发。
**误区三:把"高可用"寄托在某一个工具上。**使用 MHA、Orchestrator、Always On 等工具时,最常见的心态是"只要配置好了它,数据库就高可用了"。但工具只是切换的执行者,它做不了业务层面的判断。真正的高可用,需要的是整体机制的设计:监控怎么采集、告警怎么分级、切换后应用的连接池怎么快速重建、下游的 binlog 消费端怎么应对主库切换带来的 binlog 坐标变化……这些细节都是在工具之外的。工具越高级,越要警惕"工具掩盖了机制不足"的问题。
提示:一个实战时很有用的习惯:把所有切换操作写进"切换手册"里,包括检查清单、执行步骤、回滚措施。手册的目的是让一个不熟悉该系统的同事也能在故障时较快地执行切换。别小看这个动作,出故障时大家脑子都是懵的,有手册照着做,状态会稳定非常多。
6. 常见问题与排查技巧实录
协同设计是一个系统工程,在实操过程中遇到问题实在太正常了。我把过去几年里在 MySQL 高可用、SQL Server 高可用同步、集群故障转移这几个方向遇到的典型问题整理成一张速查表,顺便把排查思路写清楚,大家遇到类似情况时可以参考着查。
6.1 典型问题速查表
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 故障转移后,应用一直报连接超时 | 应用连接池里的数据库连接还是指向旧主库 IP,切换后连接未被重建 | 应用侧数据库连接串改为指向 VIP 或使用连接池的自动重建机制;切换脚本里增加一条通知应用的步骤,或者依赖注册中心让连接自动刷新 |
| 主从切换完成后,部分数据查不到 | 异步复制下从库落后主库,切换时丢失了尾部事务 | 开启半同步复制;切换前检查从库的Seconds_Behind_Master;对数据敏感场景使用 MGR/Always On 同步提交 |
| 切换之后出现"双主",两个数据库同时在写 | 脑裂,旧主库未被真正隔离 | 在切换脚本里加入隔离步骤(如SET GLOBAL read_only=ON、关闭旧主库的网络或抢占仲裁);配置好仲裁节点,防止网络抖动导致误判 |
| 应用水平扩展后,用户频繁被踢下线 | session 还在本地内存,未完全外置到 Redis | 检查 session 配置文件和代码中的 session 存取实现;确认应用节点之间共享同一个 Redis 集群 |
| 扩展节点后,数据库连接数暴涨 | 每个新应用节点都会建立一批数据库连接池,池大小配置过大 | 合理设置应用侧数据库连接池的maximum-pool-size(如 10~20),并预留冗余;在数据库侧监控max_connections,必要时调整数据库连接上限 |
| SQL Server 自动故障转移后,辅助副本迟迟不进主 | Always On 的健康检查和仲裁配置有问题,或者辅助副本同步模式是异步且落后太多 | 检查 WSFC 群集事件日志;确认同步提交副本数量是否满足自动故障转移条件;检查见证节点是否在线 |
| Redis Cluster 主节点挂了,但客户端报错重定向失败 | 客户端没有正确支持 Cluster 协议(如使用普通 Jedis 而非 JedisCluster) | 替换为支持 Cluster 协议的客户端;检查cluster-require-full-coverage配置,建议生产环境设为no以提高可用性 |
| 健康检查偶尔失败,应用节点被频繁摘除和加回 | 健康检查路径太敏感(如探活依赖数据库查询),数据库慢查询导致探活超时 | 健康检查改为轻量级探活(如检查进程存活 + 静态页面或内存状态),不要在里面做数据库操作 |
6.2 一些值得收藏的实战经验
仔细讲讲我在实际排障过程中的几个体会。
第一,**故障转移之后最先要做的事情不是恢复业务,而是确认"没有双主"。**很多人一看流量切过去了、系统恢复了,就松了一口气。实际上,如果切换过程有问题,旧主库和新主库同时在接受写入,这种隐患比宕机更可怕——因为数据分裂是慢慢发生的,等之后再发现,已经很难追回。所以我在切换完成后,一定会第一时间检查集群状态、确认旧主库已被降级或隔离。
第二,**所有高可用系统的首要瓶颈,往往不在组件本身,而在"切换后的人工决策"环节。**自动切换脚本能把主库从 30 分钟手忙脚乱压缩到 30 秒自动完成,前提是脚本可靠。但如果监控告警不够精准,每次半夜告警都让人去排查原因,等确认真的需要切换的时候,系统的不可用时间已经过去好几分钟了。所以我会把告警分级做得非常细:哪些是"预警"(手动观察),哪些是"必须立即自动切换"(不可等待)。把决策前移,是高可用升级的着力点。
第三,**尽量让"切换"和"恢复"自动化,但保留"人介入"的开关。**全自动切换不是万能的,比如机房断电、核心交换机故障这类场景,自动切换动作本身可能也是无效的。所以我会设计一个"维护模式"开关——在可预期的大规模变更期间(比如机房迁移、网络割接),把自动切换临时关闭,改为人工决策。等变更结束再重新打开自动切换。
第四,**做高可用方案时,多准备一条"逃生通道"。**比如数据库主库彻底损坏、所有副本都不完整时,还能不能从最近的备份 + binlog 恢复到某个时间点?这听起来很原始,但我见过很多团队把精力都花在主备切换上,备份策略反而荒废了。其实备份和 binlog 归档是最后一道防线,平时的恢复演练比切换演练更重要。真到了最坏的时刻,一道可靠的备份通道,比任何花哨的自动切换机制都让人安心。
这篇文章把我对无状态化、水平扩展和故障转移的多年理解和盘托出了。最后再分享一个小建议:高可用不是一次性的项目,而是一个持续演进的过程。每次故障演练之后,把出现的问题记录下来,逐个补齐,你的系统会一年比一年更稳。配合着这些经验,试着从自己的最小系统开始,先做无状态化,再做扩展,再配故障转移,然后主动搞一次演练——我相信你会有不一样的体会。