首页 / 帮助文档 / 分布式读写分离路由

分布式读写分离路由

分布式读写分离路由的核心不是“怎么把读和写分开”,而是“分开之后,读请求到底该打到哪个库”。很多人以为配个中间件就完事了,结果线上该读主库的请求读到了从库,读到脏数据;或者主库已经故障了,流量还拼命往里打。真正的问题出在路由策略的粒度、延迟感知和故障转移的联动上。

读写分离最原始的形态是代码里硬编码两个数据源,Service层手动调用DAO时选择主库或从库。这种方式在单服务、低并发时能用,一旦进入微服务架构,数据源数量膨胀,业务逻辑和路由逻辑耦合,维护成本会指数级上升。所以需要一层独立的路由层来接管这个决策。

路由的四个核心维度

路由决策不能只靠“这条SQL是SELECT就发从库”这么简单。一个生产级的路由策略至少要覆盖四个维度:SQL类型、事务边界、数据新鲜度要求和Hint强制路由。

SQL类型是最基础的,DML走主库,无状态查询走从库。但SELECT LAST_INSERT_ID()这种必须走主库,因为它依赖会话状态。事务边界更关键,一个事务一旦开启,内部所有操作无论读写都必须绑定到主库,否则会出现事务内读到从库的过期数据,或者从库写入这种更严重的错误。数据新鲜度要求是指某些业务场景虽然只是查询,但刚写入的数据必须立即可见,比如用户下单后立即跳转的订单详情页,这种读请求必须发主库。Hint强制路由是留给开发人员的最后手段,通过在SQL注释或请求上下文中标记,强制指定数据源。

路由中间件的架构选择

目前主流的分布式读写分离路由实现有三种架构:客户端SDK、独立代理层和Sidecar模式。

客户端SDK以Apache ShardingSphere-JDBC为代表,它直接嵌入应用进程,以jar包形式提供能力。优势是零额外部署,延迟极低,因为路由逻辑就在本地内存完成。缺点也很明显,多语言栈不友好,升级SDK需要重启应用,而且连接数无法收敛,每个应用实例都会和所有数据库节点建立连接。当数据库集群规模扩大到几十个节点时,连接数会先撑爆数据库。

独立代理层以ProxySQL、MaxScale为代表,它们作为独立进程部署在应用和数据库之间,对应用完全透明。应用只需要连接代理的单一入口,由代理完成路由、连接池管理和读写分离。这种架构连接数收敛效果极好,几百个应用实例只需要代理层和数据库保持少量连接。但代价是增加了一跳网络延迟,代理本身成为新的单点,需要做高可用部署。

Sidecar模式是云原生场景下的折中方案,每个应用Pod旁挂一个路由代理容器,应用通过localhost访问。这种方式兼顾了低延迟和连接管理,但运维复杂度上升,需要配合服务网格一起使用。

从库延迟是路由策略的最大变量

很多人以为主从复制延迟只是几十毫秒的事,实际上在生产环境,大事务、批量操作、网络抖动、从库资源争抢都可能导致延迟飙到秒级甚至更高。如果路由策略不考虑延迟,用户就会看到“提交成功但刷新后数据消失”的诡异现象。

解决这个问题需要两套机制配合:延迟检测和动态路由调整。延迟检测不能只靠SHOW SLAVE STATUS里的Seconds_Behind_Master,这个值在IO线程正常但SQL线程卡住时可能显示为0,完全不可信。更可靠的做法是在主库定期写入心跳记录,从库通过比对心跳时间戳计算真实延迟。ProxySQL就内置了这种机制,它维护一个monitor模块,持续探测每个从库的延迟,当延迟超过阈值时自动将该节点从读负载池中暂时剔除。

动态路由调整的粒度可以做到连接级别。比如设置三个阈值:延迟小于100ms的从库正常承担全部读流量;延迟在100ms到1s之间的从库只承担非关键业务的读流量;延迟超过1s的从库暂时下线,等延迟恢复后再重新加入。这个切换过程对应用完全无感知,代理层会在连接池层面完成节点的上下线。

多数据中心场景下的路由拓扑

当数据库集群跨地域部署时,读写分离路由要解决的问题从“主从选择”升级为“就近访问与数据一致性之间的平衡”。同城多机房场景下,从库延迟通常在10ms以内,路由策略可以优先选择同机房的从库以减少跨机房网络开销。但在异地多活架构下,跨地域的从库延迟可能达到50ms到200ms,这时候需要引入“可用区亲和性”路由。

具体做法是给每个数据库节点打上地域标签,路由层在启动时感知自身的可用区信息,读请求优先路由到同可用区的从库。如果同可用区从库全部不可用或延迟超标,再降级到跨可用区从库,最后降级到主库。这个降级链路必须明确配置,不能自动无限制降级,否则一个地域的从库故障会把主库压垮。

故障转移与路由的联动陷阱

主库故障是读写分离路由面临的最严峻考验。很多系统在发生主库切换后,路由层没有及时更新拓扑信息,导致写请求继续发往已宕机的旧主库,或者读请求发往已被提升为新主库的旧从库,造成数据错乱。

正确的做法是路由层必须与高可用组件紧密联动。以MHA或Orchestrator为例,当它们完成主从切换后,需要回调路由层的API,或者路由层通过监听配置中心的节点变更事件来感知拓扑变化。ShardingSphere-Proxy支持通过治理中心动态刷新数据源配置,切换过程通常在秒级完成。但这里有个细节容易被忽略:切换期间已经在途的事务必须被中断并回滚,新的请求要拿到最新的拓扑后再执行,否则会出现“半切换”状态下的脏写。

一个更稳妥的实践是引入“写禁用的保护窗口”。当路由层检测到主库不可达时,不是立刻将写流量切到候选主库,而是先进入一个短暂的只读保护期,所有写请求直接拒绝并返回明确错误码,等拓扑确认稳定后再恢复写入。这个保护期虽然会牺牲几秒的可用性,但能彻底杜绝脑裂导致的脏数据。

路由策略的配置化与可观测性

硬编码路由规则是运维灾难。一个合格的分布式读写分离路由方案必须支持动态配置下发,而且配置变更不能中断现有连接。常见的做法是把路由规则存储在配置中心,路由层通过长轮询或事件监听机制实时感知变更。

配置的粒度至少要支持到表级别。比如用户表的查询可以走从库,但订单表的查询在特定场景下必须走主库。更精细的配置还可以基于SQL特征匹配,例如包含FOR UPDATE的查询自动路由到主库。

可观测性方面,路由层必须暴露至少三类指标:每个数据源的路由次数和延迟分布、路由规则命中率、以及路由异常次数。这些指标要对接监控系统,当某个从库的路由比例突然下降或错误率上升时能立即告警。日志层面,每条SQL的路由决策过程应该记录在DEBUG级别,包括匹配了哪条规则、最终选择了哪个数据源、以及选择的原因,这样在排查数据不一致问题时才能有据可查。

一个轻量级路由代理的配置示例

以ProxySQL为例,一个生产可用的读写分离路由配置大致如下:

-- 定义后端数据库节点
INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight) VALUES
(10, 'master-db-1', 3306, 1),   -- 写组
(20, 'slave-db-1', 3306, 1),    -- 读组
(20, 'slave-db-2', 3306, 1);

-- 定义读写分离规则
INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) VALUES
(1, 1, '^SELECT.*FOR UPDATE$', 10, 1),
(2, 1, '^SELECT.*', 20, 1),
(3, 1, '^INSERT|^UPDATE|^DELETE|^REPLACE', 10, 1);

-- 配置从库延迟检测
UPDATE global_variables SET variable_value='100' WHERE variable_name='mysql-monitor_slave_lag_when_null';
SET mysql-monitor_username='monitor';
SET mysql-monitor_password='monitor_pass';

-- 加载配置
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;

这套配置定义了写组10和读组20,SELECT FOR UPDATE强制走写组,普通SELECT走读组,其他写操作走写组。同时开启了从库延迟监控,当从库延迟超过100ms时,ProxySQL会自动将其标记为不可用,读流量不再发往该节点。

新硬件的冲击:CXL内存池和NVMe从库

随着CXL(Compute Express Link)内存池化技术的成熟,主从之间的数据同步延迟有望大幅降低。传统主从复制依赖网络传输binlog,而CXL允许主库和从库共享同一块物理内存区域,数据变更几乎实时可见。这意味着读写分离路由的延迟检测阈值可以从毫秒级下探到微秒级,路由策略可以更加激进地将读流量分配到从库。

NVMe存储的普及也在改变游戏规则。过去从库延迟很大程度受限于磁盘IO,NVMe的微秒级延迟让从库回放binlog的速度大幅提升。在NVMe从库上,即使是大事务的回放也能在极短时间内完成,路由层可以更放心地将读流量分配到这些高性能从库上。但这也要求路由层能够感知不同从库的硬件配置差异,在负载均衡时给高性能节点分配更高的权重。

分布式读写分离路由本质上是一个实时决策系统,它的核心价值不在于把读写请求分开,而在于每一次路由决策都能准确反映当前集群的真实状态。延迟感知的精度、故障转移的时效性、配置的灵活度,这三者共同决定了路由层的上限。把这三个点做扎实,读写分离才能真正从“能用”变成“好用”。