中间件不适合检测主从延迟,因其在每次HTTP请求中执行,会加剧数据库压力、产生瞬时误判、无法保证连接到同一从库,且异常会中断业务;应改用定时任务采集位点并缓存,中间件仅读取缓存快照做路由决策。

Hyperf 无法靠中间件准确检测主从延迟,位点对比必须脱离 HTTP 生命周期,在独立协程或定时任务中做。
为什么中间件不适合做主从延迟检测
中间件运行在每次 HTTP 请求的生命周期内,而主从延迟是数据库层面的异步状态,受网络、写入压力、从库负载等多因素影响。用中间件去查 SHOW SLAVE STATUS 或比对 Exec_Master_Log_Pos 和 Read_Master_Log_Pos,会带来几个硬伤:
- 每次请求都触发一次查询,放大数据库压力,尤其在高并发下可能拖垮从库监控接口
- 中间件拿到的是「某一瞬」的位点差,无法区分是短暂抖动还是持续延迟,误报率高
- Hyperf 的协程上下文不保证 MySQL 连接复用到同一从库实例(尤其用了读写分离代理时),查到的位点可能来自不同节点,结果无意义
- 若把检测逻辑塞进
process(),还会污染正常请求链路——延迟检测失败不该导致 500,但中间件里抛异常或返回错误响应会直接中断业务流
正确做法:用 Command + 定时任务拉取位点
主从延迟检测本质是运维可观测性问题,应和业务请求解耦。Hyperf 提供了成熟的命令行能力,配合 hyperf/crontab 扩展即可实现稳定轮询:
- 新建命令类
app/Command/CheckReplicationDelayCommand.php,在handle()中用原生 PDO 或Hyperf\DbConnection\Db连接从库执行SHOW SLAVE STATUS - 提取
Seconds_Behind_Master字段值;若为NULL,说明复制已断开,需告警 - 将结果写入 Redis 或本地文件(如
storage/logs/replication_delay.log),避免每次查库 - 在
config/autoload/crontab.php中注册每 10 秒执行一次:['rule' => '*/10 * * * * *', 'command' => 'check:replication-delay']
这样既避开请求链路干扰,又能控制采样频率和失败容忍度。
如果非要“在请求中感知延迟”,该怎么做
某些强一致性场景(如刚写完立刻查)确实需要动态判断是否可读从库。这时中间件不是用来「检测延迟」,而是「读取缓存的最新延迟快照」:
- 前置条件:已有上面的定时命令持续更新
redis->get('replication:delay:seconds') - 在中间件(如
App\Middleware\ReadPreferenceMiddleware)的process()中,仅做一次$this->redis->get('replication:delay:seconds') - 若值 > 300(5 分钟),则强制切换当前协程的 DB 连接为写库连接:
Db::connection('write') - 注意:这个中间件必须放在所有业务中间件之前(比如在
middlewares.php数组最前面),否则可能已经发起了从库查询
关键点在于——中间件只读缓存,不查库;位点采集和判断逻辑完全隔离。
容易被忽略的 MySQL 配置细节
即使位点能取到,Seconds_Behind_Master 也可能不准,尤其在以下情况:
- 从库开启了
slave_parallel_workers > 0:MySQL 5.7+ 并行复制下该字段可能长期为 0,实际延迟藏在relay_log_space或Retrieved_Gtid_Set差值里 - 主库 binlog_format = STATEMENT:某些函数(如
NOW())会导致从库执行时间偏移,位点一致但语义不一致 - 从库设置了
read_only=0:人为写入从库会破坏位点连续性,SHOW SLAVE STATUS不再可信
真正可靠的延迟检测,得结合 GTID、心跳表(如往主库插入带时间戳的记录,从库查该记录时间差)或多源位点交叉验证——这些都不适合放在中间件里跑。


















