:代理层路由选型:ProxySQL、中间件与直连的取舍)
切换完成后谁来通知应用前一篇的自动化切换把数据库内部谁当主管到了底但整个链条还缺一环切换成功的那一刻全公司几百个服务实例、几万个长连接还握着旧主不放。连接与路由这一层有四种活法客户端直连加 DNS、客户端自带多主机插件如 Connector/J 的 failover 模式、地址层VIP/LVS漂移、独立代理层ProxySQL、MySQL Router或数据库中间件。四种方案在高可用里的差别不是能不能连而是改口的速度、读写分离的位置、以及故障瞬间流量的去向。本篇用两个模拟把代理层最值钱也最容易翻车的两件事量化带延迟阈值的读路由和连接复用带来的会话状态问题。三种接法的机制账直连加 DNS/VIP数据库切换 把 VIP 或 DNS 记录指向新主。优点是零额外组件、零额外跳数缺点是改口速度受 TTL 和连接池心跳控制——DNS 缓存 300 秒就意味着 300 秒的错乱窗口且客户端手里的长连接根本不看 DNS必须等到 TCP 断开重连实际窗口比 TTL 还长。VIP 漂移快于 DNS但跨机房就无能为力。客户端插件把拓扑写进连接串jdbc:mysql://primary,secondary1,secondary2/app?failOverReadOnlyfalsesecondsBeforeRetrySource60驱动层完成故障转移与回切。它本质是每人一个迷你代理拓扑知识散落在所有应用里改拓扑要发布应用读扩散read after write之类一致性策略只有驱动作者懂。适合单库主备、连接规模不大的场景。代理层/中间件ProxySQL 这类独立进程持有全部路由知识hostgroup定义写组读组mysql_query_rules按用户名、schema、语句指纹分流mysql_replication_hostgroups用read_only标志自动搬移节点max_replication_lag给从库设延迟熔断线监控模块每秒探活节点失联直接出组。切换系统上一篇的 MHA/Orchestrator只管改节点的read_only和 GTID 身份路由表跟着自动收敛——这就是两者衔接的接口。中间件ShardingSphere、老 MyCat 路线则是库内路由 分库分表的超集功能更多、链路更深。代价同样明确多一跳同机约 100 微秒、跨机亚毫秒代理自身要有集群与 VIP路由规则成为新的分布式配置中心要人养。实验一读路由的延迟熔断是保护伞也是主库的隐形放大器600 条查询按三读一写排列两个从库各有一段延迟窗口R1 撞上 5 秒大报表、R2 有 3 秒补数窗口且与 R1 重叠。对比max_replication_lag设与不设的路由结果。N600# 600 条查询WRITE_EVERY4# 每第 4 条是写: 25% 写deflag_of(node,i):ifnodeR1and200i260:return5.0# R1 撞上定时大报表, 延迟 5sifnodeR2and210i230:return3.0# R2 的批量补数与 R1 窗口重叠 20 条return0.1defroute(threshold):dist{M:0,R1:0,R2:0}stale_reads0fallback_reads0foriinrange(N):if(i1)%WRITE_EVERY0:dist[M]1# 写永远进主库continueok[nfornin(R1,R2)iflag_of(n,i)threshold]ifok:nodemin(ok,keylambdan:lag_of(n,i))# 最健康优先dist[node]1iflag_of(node,i)1.0:stale_reads1else:dist[M]1# 全落后 - 回主库fallback_reads1returndist,stale_reads,fallback_readsforthin(999.0,1.0):dist,stale,fbroute(th)print(max_replication_lag%.0fs: 主库 %d 条 | R1 %d 条 | R2 %d 条%(th,dist[M],dist[R1],dist[R2]))print( 读到 1s 陈旧数据的查询: %d 条; 因全员落后回主的读: %d 条%(stale,fb))dist0,_,_route(999.0)dist1,_,_route(1.0)print(\n阈值收紧的代价: 主库承担读 %d 条(写基线 %d 条 - 现 %d 条, 主库负载 %.0f%%)%(dist1[M]-dist0[M],150,dist1[M],100.0*(dist1[M]-dist0[M])/150))print(重叠窗口 [210,230) 内两条读规则同时失效是风暴源头: 保护读一致性的阈值, 就是主库的隐性流量放大器)运行输出max_replication_lag999s: 主库 150 条 | R1 405 条 | R2 45 条 读到 1s 陈旧数据的查询: 15 条; 因全员落后回主的读: 0 条 max_replication_lag1s: 主库 165 条 | R1 405 条 | R2 30 条 读到 1s 陈旧数据的查询: 0 条; 因全员落后回主的读: 15 条 阈值收紧的代价: 主库承担读 15 条(写基线 150 条 - 现 165 条, 主库负载 10%) 重叠窗口 [210,230) 内两条读规则同时失效是风暴源头: 保护读一致性的阈值, 就是主库的隐性流量放大器熔断阈值本身没有对错它是一台陈旧读→主库过载的汇率转换器。真正的工程要点藏在数字之外延迟窗口的源头往往是从库上跑的定时报表/备份扫描第 1 篇 MTS 栅栏的账所以要么给报表库单独建组、设更高的熔断线要么把批任务调度错开而全员落后回主触发时主库正在承担写洪峰10% 的读可能就是压垮它的最后一根稻草——容量规划时要给这个回退路径留出至少 20% 的主库读余量并给它配独立告警。实验二连接复用的账省下的都是脏会话没占的ProxySQL 的招牌特性之一是多路复用multiplexing应用侧连接与后端服务器连接解耦事务间隙归还后端连接给别的会话用。省连接是真的会话状态污染也是真的——用户变量、SQL_CALC_FOUND_ROWS、临时表、LOCK TABLES都长在后端连接上。用一组交错事件看三种策略的差别。events[(S1,SET,A),# S1: SET flagA(S1,GET,None),# S1: SELECT flag(S1,DONE,None),# S1 结束, 连接归还池(S2,SET,B),# S2: SET flagB(S2,GET,None),# S2: SELECT flag(S2,DONE,None),(S3,GET,None),# S3 新会话, 没 SET 过, 只查(S3,DONE,None),]defsimulate(pin,reset):free,held,made[],{},0dirty_leave0wrong[]forsid,op,valinevents:ifopDONE:cheld.pop(sid,None)ifcisnotNoneandc[state]:ifpin:dirty_leave1# 带会话状态的连接不回池, 等真空闲再回收else:ifreset:c[state]None# COM_RESET_CONNECTION 清用户变量free.append(c)elifcisnotNone:free.append(c)continueifsidnotinheld:cfree.pop(0)iffreeelseNoneifcisNone:made1c{name:sc%d%made,state:None}held[sid]c cheld[sid]ifopSET:c[state]valelse:seenc[state]ifsidS3andseenisnotNone:wrong.append(S3 SELECT flag 读到 %s(上一个会话的残留)%seen)elifsidS2andseen!B:wrong.append(S2 的 SET 之前先读到 %s%seen)returnwrong,dirty_leave,made w0,_,m0simulate(pinFalse,resetFalse)w1,_,m1simulate(pinFalse,resetTrue)w2,dl,m2simulate(pinTrue,resetFalse)print(策略一 裸复用(ProxySQL 关闭 multiplexing 语义错误用法): %s%(w0or无异常))print(策略二 复用 借出前 COM_RESET_CONNECTION: %s%(w1or无异常))print(策略三 复用 脏连接钉住不归还: %s (钉住回收 %d 次, 新建连接 %d)%(w2or无异常,dl,m2))sessions500sticky_ratio0.6print(\n规模账(%d 并发会话, %.0f%% 会话携带用户变量/临时表/显式事务):%(sessions,sticky_ratio*100))print( 直连 1:1: 后端连接 %d, 无污染%sessions)print( 全复用 10:1: 后端连接 %d, 但上面策略一的重演(会话串数据)%(sessions//10))print( 复用 脏会话钉住: 后端连接 %d(钉住) %d(复用) %d, 无污染%(int(sessions*sticky_ratio),sessions//10,int(sessions*sticky_ratio)sessions//10))print(结论: multiplexing 省的是干净会话的连接, 脏会话的连接数省不掉——代理层的连接收益要打折扣)运行输出策略一 裸复用(ProxySQL 关闭 multiplexing 语义错误用法): [S3 SELECT flag 读到 B(上一个会话的残留)] 策略二 复用 借出前 COM_RESET_CONNECTION: 无异常 策略三 复用 脏连接钉住不归还: 无异常 (钉住回收 2 次, 新建连接 3) 规模账(500 并发会话, 60% 会话携带用户变量/临时表/显式事务): 直连 1:1: 后端连接 500, 无污染 全复用 10:1: 后端连接 50, 但上面策略一的重演(会话串数据) 复用 脏会话钉住: 后端连接 300(钉住) 50(复用) 350, 无污染 结论: multiplexing 省的是干净会话的连接, 脏会话的连接数省不掉——代理层的连接收益要打折扣策略一就是真实事故脚本的抽象ORM 里一句SET tenant ?被下一个借用这条后端连接的会话读到轻则数据查串、重则跨租户越权。ProxySQL 的处理是自动失效 显式钉住双轨检测到会话级状态就把该连接踢出复用等价于策略三SET var、GET_LOCK、部分隔离级别语句默认禁复用策略二的COM_RESET_CONNECTION则要求后端支持且状态可枚举临时表、 prepared statement 各有归宿。规模账提醒另一件事如果你的应用会话普遍带粘性大量用户变量或长事务代理层宣称的连接数收益会缩水到和直连同一量级此时上代理层的理由只剩路由与故障摘除。常见陷阱与落地清单代理层只配了一个实例路由层单点故障时数据库健康也没人给流量指路至少两实例 VIP/负载均衡并把代理自身纳入监控告警。读写分离规则用是否包含 select 关键字判断SELECT ... FOR UPDATE、INSERT ... SELECT、存储过程里的读全都被错放去从库按语句指纹加事务上下文判定。写后立读走从库熔断阈值挡不住落后 0.8 秒的窗口订单详情页照样读到旧状态会话内在主库写过N 秒内强制回主ProxySQL 可用事务粘性 自定义规则实现。故障切换没联动代理新主提了、read_only改了但 ProxySQL 写组还指着旧主 IP 探测失败反复进出组震荡切换系统的on_master_failover钩子里要有主动LOAD MYSQL SERVERS TO RUNTIME的一步。DNS TTL 一刀切 300 秒演练时切换 5 分钟才改口全是它贡献的数据库域名用短 TTL30~60 秒并把客户端连接池的 maxLifetime 配得比 DNS TTL 还短。压测只走直连、上线才过代理代理的跳数、复用、规则匹配开销全被藏进生产首周 P99 抖动作废。到这里“写谁读谁、故障时流量怎么改口已经闭环。但从第 1 篇开始就埋着一句话复制只保证传播”不保证可回溯——误删、坏发布、勒索加密代理层一律帮不上。下一篇《数据库高可用与容灾实战6备份体系设计mysqldump、XtraBackup 与快照的边界》开始讲回到过去的能力。参考来源ProxySQL Documentationhttps://proxysql.com/documentation/GitHubsysown/proxysqlhttps://github.com/sysown/proxysqlMySQL Connector/J 8.0 Developer Guidehttps://dev.mysql.com/doc/connector-j/8.0/en/Apache ShardingSphere 文档站https://shardingsphere.apache.org/document/current/en/overview/WikipediaLoad balancing (computing)https://en.wikipedia.org/wiki/Load_balancing_(computing)本系列已结集为免费专栏数据库高可用与容灾实战从主从复制到机房级演练进阶推荐付费专栏Python 自动化接单实战从脚本到第一单限时 ¥9.9首篇免费试读