☰
ProxySQL读写分离实战:从主从复制到流量路由与故障切换
2026/9/28 7:01:56 网站建设 项目流程

读写分离系列写到了第十篇。前面聊过主从复制原理、从库搭建、半同步复制、延迟监控,这一篇终于要把流量真正“分开”的关键组件摆上台面:ProxySQL。这几年我处理过不少读写分离做不下去的线上项目,共同点几乎一样——不是不会搭主从,而是应用连上来之后,连接、路由、故障切换全都挤在应用代码里,每加一台从库就要发一次版本,主库一挂,应用还在傻乎乎地把读请求打到不可用的实例上。ProxySQL的价值就是把这些事从应用层抽离出来,在SQL入口直接分拣流量。

它适合谁?适合已经搭好MySQL主从、但不想改代码接入数据源切换的团队,也适合业务多语言共存、由DBA或运维统一接入的场景。如果是单机或并发很低的项目,没必要上这么重的中间件,这一点后面也会说到。这一篇会从架构、安装、配置、路由规则、故障转移和排查六个方面把ProxySQL完整过一遍,所有配置命令都可以直接复制到你的环境里,当手册用。

1. 架构设计:什么场景才需要ProxySQL,以及为什么它更合适

1.1 写多读少与高并发读场景的瓶颈

一个典型的电商网站,读写比差不多是3:7甚至1:9,下单少、翻页多。单库扛住读压力之后,大家想到的是加从库。但加了两三台之后,连接数、延迟、各从库负载不均等问题就会冒出来。如果靠应用代码去维护一组DataSource,每个服务都要写一遍,很快你会发现最重的不是SQL,而是这个“路由层”本身。

拿快递分拣打个比方。每个包裹(SQL)送到分拣中心后,分拣员根据面单(路由规则)决定送哪个片区(hostgroup),而不会让每个快递员自己记全城线路。ProxySQL就是这样一个中间层,它在MySQL协议层做分发,应用看到的还是一个普通的MySQL地址,完全无感知。

1.2 为什么不用代码层动态切换数据源

很多人第一反应是用开发框架自带的读写分离能力,或者自己写一套AbstractRoutingDataSource。这类方案表面很轻,但有几个硬伤:

  • 主从切换时,连接池里的连接还牢牢绑定着旧实例,清理不准就会把写请求发到从库;
  • 每增加一个从库,配置和开发要同步改,Java、Go、PHP各写一套,规则很难统一;
  • 一旦有服务绕过ORM直接执行SQL,路由规则就失效了。

ProxySQL在数据库前端做透明代理,应用连上来之后,所有路由、健康检查、连接池都在这一层解决。多语言、多服务接入的场景下,这套方案的可维护性明显更好。

1.3 ProxySQL的配置三层结构有助于让配置热生效

ProxySQL最值得关注的设计是它的配置体系:admin层的配置表、内存里的runtime、磁盘上的持久化。你可以直接用SQL语句INSERT/UPDATE配置表,然后LOAD到runtime让规则立刻生效;再SAVE to disk存入SQLite,防止进程重启丢配置。这套机制就像不重启路由器就能改路由表一样,对线上环境非常友好。

后面所有操作都会围绕这三层展开。先记住一个概念:admin里改的是“草稿”,LOAD到runtime才生效,SAVE to disk才持久化。

2. 环境准备与基础配置:把ProxySQL先跑起来

2.1 安装与启动

我这边基本都是RHEL/CentOS体系的服务器,直接装官方RPM包最快。

yum install -y libaio yum install -y http://repo.proxysql.com/ProxySQL/proxysql-2.4.4-1.1.x86_64.rpm systemctl start proxysql systemctl enable proxysql

注意版本差异:ProxySQL 2.x和1.x在部分变量名上有区别,比如monitor_read_only_interval这类参数是1.4之后才有的。另外旧版安装时如果缺libaio,进程会启动不起来,先装依赖再装主程序,这个顺序别颠倒。

2.2 登录Admin管理接口

ProxySQL默认管理端口是6032,MySQL协议兼容,直接用mysql客户端登录:

mysql -uadmin -padmin -h127.0.0.1 -P6032 --prompt='Admin> '

登录后执行SHOW TABLES,看到的表可以大致分成三类:mysql_开头是核心配置表,stats_开头是运行统计表,runtime_开头是当前实际生效的配置。后面所有操作基本就围绕这三类表展开。6032是管理口,业务流量走6033,这两个端口一开始就要分清楚,排查问题的时候能少走很多弯路。

2.3 配置文件proxysql.cnf里的关键变量

除了用SQL初始化,也可以在/etc/proxysql.cnf里预先设置admin账号、监控账号、线程数和监控间隔。我习惯把基础项写进配置,具体主机和规则再用admin接口操作,这样不会把配置文件弄得又长又乱。

[admin] admin_variables= { admin_credentials="admin:admin;monitor:monitorpass", mysql_ifaces="0.0.0.0:6032" } mysql_variables= { monitor_username="monitor", monitor_password="monitorpass", monitor_interval=2000, monitor_read_only_interval=1500, threads=4 }

这里几个参数的意义:

  • admin_credentials用分号隔开多组账号密码,我额外加了monitor账号,给健康检查用,避免滥用admin权限;
  • mysql_ifaces改成0.0.0.0是因为要允许其他运维机器来管理,生产环境建议只放内网IP;
  • monitor_interval单位是毫秒,2000就是每2秒检查一次实例存活;
  • threads是ProxySQL自身的工作线程数,一般等于CPU核数或略低,不要盲目调大。

2.4 启动后的基础验证

启动后先登录admin,查一下几张关键表是否正常。最直接的一条验证命令是查连接池:

SELECT * FROM stats_mysql_connection_pool;

如果没有任何连接池数据,说明应用还没把流量导过来。这一步别急着往下配规则,先把ProxySQL当透明代理用,确认应用能连通,再上路由规则。否则后面排查时,很难定位问题到底出在MySQL实例还是代理层。

3. 核心配置逐项拆解:账号、主机组、健康检查

3.1 mysql_users:应用账号的接入层

应用连ProxySQL时用的账号和连MySQL的账号可以一致,也可以单独建。我的做法是:后端MySQL账号权限收得很紧,ProxySQL里再配置对应账号。一般只需要两条SQL:

INSERT INTO mysql_users(username, password, default_hostgroup, transaction_persistent, max_connections) VALUES ('app', 'app_pass', 0, 1, 200); LOAD MYSQL USERS TO RUNTIME; SAVE MYSQL USERS TO DISK;

default_hostgroup为0,表示登录后默认归属主库组;如果一条SQL没有命中任何查询规则,就会去这个组。transaction_persistent=1是这里的关键,它表示一旦开启事务,这个连接后面所有语句都固定到事务第一条语句所在的hostgroup,避免事务内一半走主、一半走从。

一个经常踩的坑:修改mysql_users之后只执行了SAVE,没有LOAD到runtime,导致当前运行环境没变,而磁盘上的配置变了,重启后配置跳变。改配置时LOAD和SAVE的顺序一定要配合好。

3.2 mysql_servers:hostgroup分组与权重

先用hostgroup把读写两套逻辑分好。我习惯用hostgroup 0表示主库,hostgroup 1表示从库。

INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight, max_connections) VALUES (0, '10.0.0.11', 3306, 100, 200), (1, '10.0.0.12', 3306, 100, 200), (1, '10.0.0.13', 3306, 120, 150);

同一hostgroup内部按weight比例分流量。10.0.0.13机器性能更好,weight给120,ProxySQL就会根据总权重占比给它分配更多连接。如果实例规格完全一致,weight保持一致最省心。

除了手动配置分组,还可以用mysql_replication_hostgroups让ProxySQL自动感知主从。

INSERT INTO mysql_replication_hostgroups(writer_hostgroup, reader_hostgroup, check_type) VALUES(0, 1, 'read_only');

配置之后,ProxySQL默认会定期读取各实例的read_only变量,自动把read_only=0的实例放进writer组,read_only=1的放进reader组。对后面做故障切换非常有价值。

3.3 MySQL_Monitor:健康检查与状态收集

健康检查依赖后端MySQL里的一个低权限账号。在每个后端实例上执行:

CREATE USER 'monitor'@'%' IDENTIFIED BY 'monitorpass'; GRANT SELECT, REPLICATION CLIENT ON *.* TO 'monitor'@'%';

监控账号必须和业务账号分开。REPLICATION CLIENT权限是用来读复制状态的,后续做延迟检测会用到。

Monitor检查的内容有两个核心:实例是否能连上,以及read_only值的变化。检查太频繁会消耗后端库资源,一般监控间隔设置2000ms以上;如果从库数量多、业务压力大,可以适当调大到3000-5000ms,但故障感知速度也会变慢。这种权衡没有标准答案,要结合自己业务的容忍度。

3.4 配置持久化:save、load、runtime三层关系

刚入门的同学最容易问:我执行了INSERT,为什么应用流量不对?因为修改只写进了admin内存表,没有加载到runtime。

  • MEMORY层:通过SQL改的配置,存在admin内存中,还没生效;
  • RUNTIME层:真正运行中生效的配置;
  • DISK层:持久化到SQLite文件,重启后自动加载。

操作顺序建议是:先LOAD到RUNTIME让当前生效,确认没问题后SAVE到DISK防重启丢配置。常见事故就是只load不save,重启后配置回到旧版;或者一边改一边反复save,状态下混乱时想回退都没办法。

4. 读写分离路由规则:让SELECT走对库

4.1 最简单的SELECT转发规则及其隐患

先给一条看起来很美的规则:

INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) VALUES(1, 1, '^SELECT', 1, 1); LOAD MYSQL QUERY RULES TO RUNTIME; SAVE MYSQL QUERY RULES TO DISK;

它的意思是:以SELECT开头的SQL全部送到hostgroup 1,也就是从库。问题很快就来了:

  • 很多框架自动生成的SQL是select小写开头,正则默认区分大小写,这条规则匹配不到小写语句;
  • 事务中第一条SELECT被送到从库,后续UPDATE却送到主库,同一条连接产生跨库读写,轻则读到旧数据,重则直接出现数据不一致;
  • SELECT ... FOR UPDATE本质是带锁的读,必须走主库;
  • SELECT里如果调用存储过程,执行结果未必适合放到从库。

所以生产环境不要照抄这条规则,至少得加上大小写兼容、FOR UPDATE优先、事务绑定这三点。

4.2 事务内SELECT与FOR UPDATE的正确姿势

前面在mysql_users里设置transaction_persistent=1,这是第一层兜底。但注意,这个参数只保证事务内连接不会随意切换hostgroup,如果事务第一条SELECT先走了从库,那后续写的语句就会被从库拒绝。所以在规则层面,要把带锁的读优先指到主库。

INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) VALUES(10, 1, '^SELECT.*FOR UPDATE', 0, 1); INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) VALUES(20, 1, '(?i)^SELECT', 1, 1);

注意rule_id的顺序,ProxySQL按rule_id从小到大匹配,命中的规则如果apply=1就不再往后看。所以FOR UPDATE这种特殊情况一定放在普通SELECT前面。用(?i)前缀让正则不区分大小写,能覆盖掉大部分框架生成的小写SQL。

4.3 默认分流思路与存储过程处理

很多团队喜欢反着设计规则:除了明确INSERT、UPDATE、DELETE、REPLACE这些写语句之外,其余都走从库。这样更保守,不容易漏掉隐式写法,但也容易让SET、SHOW这类语句跑到从库。SET @var=...这类会话变量如果只设在从库连接上,后面主库连接读不到,会出莫名其妙的问题。

稳妥一点的做法是:写语句走主库,SELECT FOR UPDATE走主库,剩余SELECT走从库,事务内所有SQL跟随第一条语句,靠transaction_persistent兜底。必要时可以加一条规则让SET和SHOW走主库:

INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) VALUES(40, 1, '^(SET|SHOW)\s', 0, 1);

存储过程是个特例。CALL语句本身可以按规则走主库或从库,但存储过程内部的动态SQL,ProxySQL是看不见的。只要存储过程涉及写操作,最安全的做法是CALL一律走主库,宁慢勿错。

4.4 通过SQL注释做精细化分流

ProxySQL支持在SQL中带注释来匹配规则,这是我强烈推荐的手段。某些业务场景,比如支付结果需要在同一事务或写操作后立刻读最新数据,普通SELECT走从库会读到旧数据。这种SQL可以让应用加上/*master*/标记,再配一条注释规则:

INSERT INTO mysql_query_rules(rule_id, active, match_pattern, destination_hostgroup, apply) VALUES(30, 1, '^.*/\*master\*/.*', 0, 1);

应用在SQL前或SELECT关键字后面写入/*master*/,这条SQL就会强制走主库。不需要改数据源,不需要换数据源,只在SQL层做标记,效果立竿见影。很多团队没用这个功能,但在诊断强一致读和慢查询时,它真的能省很多事。

5. 主从切换与故障转移:线上最实用的场景

5.1 手动切换:调整hostgroup的读写状态

读写分离的故障切换,核心不是MySQL数据层面的切换,而是ProxySQL怎么把原本去旧主的流量切走。最直接的方式是在admin里改mysql_servers。

假设10.0.0.12要从从库提升为新主库,旧主10.0.0.11要降为从库:

UPDATE mysql_servers SET hostgroup_id=1 WHERE hostname='10.0.0.11'; UPDATE mysql_servers SET hostgroup_id=0 WHERE hostname='10.0.0.12'; LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;

流量切换是秒级的。但有个细节:改主机组前,最好先在MySQL层面把旧主库的read_only改为1,确保它不再接收新写入。如果没配mysql_replication_hostgroups自动识别,那这一步只能手动做。

5.2 自动切换思路:不要指望ProxySQL做一切

ProxySQL只负责代理流量,它不会去执行MySQL层面的主从切换,选新主、补数据、改GTID这些工作要交给MHA、Orchestrator这类工具。ProxySQL能帮忙的是:检测到某个后端实例不可用后自动SHUN摘除,以及通过read_only字段变化自动调整主机组成员。

一个常见误区是以为配了mysql_replication_hostgroups就等于自动切换。实际上,当真正的主库宕机后,如果MySQL层面没有选主,ProxySQL能看到的只是所有read_only可能都为1,没有节点能被当作新主。完整方案要分两步:先用外部心跳组件完成数据层主从切换,再让ProxySQL感知新主并切换流量。

我在生产环境用Orchestrator配合ProxySQL做过多次切换,整个流程比较灵活,但配置也更重。如果团队没有专门工具,至少先把手动切换流程演练熟,不要一上来就想全自动。

5.3 切换后需要立刻处理的三个问题

第一,连接状态。ProxySQL复用连接时,旧连接指向的MySQL实例地址在切换后可能已经变化,执行LOAD MYSQL SERVERS TO RUNTIME可以刷新连接池,必要时重启ProxySQL让所有连接重建。但重启前一定要确认配置已经SAVE过。

第二,账号同步。新主库上必须提前建好业务账号和监控账号,否则切换后应用连上来直接被拒。这个最容易被忽视,通常在副本上账号已经同步,但手动提升时容易漏掉。

第三,延迟数据。旧主如果没彻底掉线,可能还有少量数据没被从库追上。切换前确认Seconds_Behind_Master接近0,切换后再观察新主复制状态,别急着让大量读流量压到刚提升的主库。

6. 常见问题排查与性能调优实录

6.1 经典的error 2002连接失败排查

应用报ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'这类错,先理清是哪个环节出了问题。

报错位置可能原因快速处理
连接6032管理端口ProxySQL进程没启动或admin未监听systemctl status proxysql,查监听端口
应用连接6033业务端口客户端走了socket而不是TCP去掉socket参数,强制走host和port
后端MySQL实例直接报错MySQL没启动或socket配置路径不对检查mysqld状态和socket路径

排查顺序我是固定的:先看端口,再看进程,再看日志,最后才看配置。很多时候问题出在应用端把socket模式当成TCP在用,跟ProxySQL本身没关系。

6.2 从库延迟导致读脏数据

如果从库复制延迟高,读流量还是照常打过去,业务就会读到旧数据。ProxySQL的mysql_servers表支持max_replication_lag,可以让延迟超过阈值的从库自动被摘除。

UPDATE mysql_servers SET max_replication_lag=5 WHERE hostgroup_id=1; LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;

单位是秒。阈值设为5秒,就意味着从库seconds_behind_master超过5秒时,ProxySQL把这台从库从路由池剔除,等恢复后再加回。这个参数只对从库有意义,主库别设置。

注意,这个功能依赖MySQL Monitor的复制状态采集,如果监控账号没有REPLICATION CLIENT权限,延迟值拿不到,规则就不会生效。

6.3 后端连接池打满与连接复用

ProxySQL最容易被低估的能力是连接复用。很多短连接应用直接连MySQL,后端会出现大量TIME_WAIT;连了ProxySQL之后,多个前端连接可以共享后端连接,只要mysql_users里的max_connections设置合理,后端连接数通常能稳定在几十个以内。

当看到stats_mysql_connection_pool里connERR很高时,优先怀疑后端MySQL的max_connections是不是被写满了,而不是急着调大ProxySQL的max_connections。另外ProxySQL有connection_max_age参数,默认1小时。如果后端MySQL的wait_timeout比较小,旧连接会被MySQL断开但ProxySQL还在复用,建议把这个参数调小,或者统一把后端wait_timeout调大,让连接生命周期对得上。

6.4 性能监控与日常巡检建议

上线之后不是一劳永逸。我最常看的是这两张统计表。

SELECT hostgroup, srv_host, status, ConnFree, ConnOK, ConnERR FROM stats_mysql_connection_pool; SELECT count_star, sum_time, average_time, Digest_text FROM stats_mysql_query_digest ORDER BY count_star DESC LIMIT 20;

第一张看连接池健康度,第二张看哪些SQL消耗最大,方便针对慢查询优化。我还会定期把/var/lib/proxysql/proxysql.db备份走,这个SQLite文件包含全部配置,换机器后直接替换再启动就能复原整个环境。

最后提一个建议:在低峰期做一次完整的主从切换演练。很多时候不是ProxySQL配置错了,而是切换流程里某个环节没跑通,等真出故障时才发现。我在上线新主机组前,都会先把“SLAVE提升+ProxySQL切流+连接回收”这条链路完整过一遍,确认没问题再正式对外提供流量。这种习惯,比临时抱佛脚有用得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询