工具大全
开发者工具作者:Coocon2026年8月22日15 次阅读约 6 分钟阅读

跨境网站页面从 2.7s 降到 0.8s:MySQL binlog 免费搭只读副本 + Prisma 读写分离全记录

业务问题:同一个网站,两种速度

我们的站点按访问者地域做 DNS 分流:大陆用户解析到阿里云(北京),海外用户解析到洛杉矶的 VPS。应用是无状态的 Next.js,两边跑同一份镜像——但数据库只有一个:阿里云 RDS MySQL,在北京。

结果就是一个很典型的"分了流量、没分数据"的架构:

  • 大陆用户:app 和数据库同地域,单条查询几毫秒,页面百毫秒级
  • 海外用户:app 在洛杉矶,每条 SQL 都要跨太平洋往返一次,RTT 约 150-200ms

SSR 页面通常要串行跑多条查询(文章 + 相关推荐 + 标签 + 分类),延迟被逐条放大。实测同一个文章页:国内机 0.1-0.5 秒,海外机 2.7-4.2 秒。用户的直观感受就是"这网站在国外卡得没法用"。

选型:为什么是原生 binlog 复制

写流量很小(内容站,读写比悬殊),要解决的只是"读"的物理距离。备选方案对比:

方案 成本 问题
云厂商只读实例 按实例付费 只能建在同厂商地域内,出不了境,解决不了跨洋
DTS / 数据同步服务 按量付费 为 30MB 的库付订阅费不划算
应用层缓存(Redis) 新组件 缓存失效逻辑侵入业务,且首查仍跨洋
MySQL 原生 binlog/GTID 复制 0 元 需要自己踩运维的坑(本文价值所在)

MySQL 复制是标准能力,RDS 作为源端不收任何费用。副本落在与海外 app 同机房的一台 2C2G 小 VPS 上(Docker 跑 MySQL 8),读延迟从 150ms 变成本机房 1ms 级。

动手前先对源库做前提核查,五个必查项(在 RDS 上执行):

SELECT @@version;          -- 副本版本必须 ≥ 源库(我们是 8.0.36)
SELECT @@gtid_mode;        -- 必须 ON,才能用 SOURCE_AUTO_POSITION
SELECT @@binlog_format;    -- 期望 ROW
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';  -- 断链重建窗口(我们是 30 天)
SHOW GRANTS FOR CURRENT_USER;  -- 有没有 REPLICATION SLAVE 权限

意外收获:阿里云 RDS 的高权限账号默认自带 REPLICATION SLAVE,不需要单独建复制账号。

架构:写回北京,读在本地

阿里云 RDS 北京(主库,唯一写入点)
    │  binlog/GTID 原生复制,SSL 加密,SOURCE_AUTO_POSITION=1
    ▼
洛杉矶 VPS:MySQL 8 只读副本(Docker,与海外 app 同机房)
    ▲  读 ~1ms
海外 app ──写───────────────────→ 仍直写北京主库
国内 app ──读写──→ 北京主库(完全不变)

应用层用 Prisma 官方扩展 @prisma/extension-read-replicas 做路由,核心是双导出

// src/lib/db.ts(简化)
export const prismaPrimary = new PrismaClient({ /* 主库 */ });

export const prisma = process.env.DATABASE_REPLICA_URL
  ? prismaPrimary.$extends(readReplicas({ url: process.env.DATABASE_REPLICA_URL }))
      as unknown as PrismaClient   // cast 保型,全站 200+ 调用点零改动
  : prismaPrimary;                 // 没配副本 = 同一实例,行为与改造前一字不差

扩展的路由规则:find*/count/aggregate/groupBy 走副本;所有写、$transaction$queryRaw 自动走主库。绝大多数代码不用动,但有两类场景必须手工固定主库(import prismaPrimary):

  1. 登录会话:NextAuth 的 database session 是"写入 session 后下一个请求立刻回读",副本哪怕只有半秒延迟,用户登录就会闪断
  2. 写后立读create 之后马上 findMany 同一张表的接口,复制延迟会让刚写的记录"消失"

反过来,「update 的返回值当读结果」这种模式天然安全——update 本身走主库,返回的就是主库数据。

只想让一台机器启用:主机白名单门禁

两台服务器共享同一份 .env 文件(同一个 CI Secret 渲染),不能靠"发不同配置"区分。做法是三段式:

  1. 部署时 docker run -e DEPLOY_HOST=<本机IP>,容器知道自己在哪台机器
  2. 入口脚本按白名单 + TCP 探测决定变量去留(fail-safe 方向永远是降级直连主库):
if [ -n "$DATABASE_REPLICA_URL" ]; then
  if host_in_whitelist "$DATABASE_REPLICA_HOST"; then
    node -e "TCP 探测副本 3 秒" || unset DATABASE_REPLICA_URL   # 副本挂了→回主库
  else
    unset DATABASE_REPLICA_URL                                   # 国内机→行为零变化
  fi
fi
  1. 应用代码只看变量存不存在

这个设计同时就是降级开关:副本宕机时重启 app 容器即自动回退主库;彻底摘除只需删 Secret 里两行。

五个真实踩坑(附报错原文)

坑 1:lower_case_table_names 只能在初始化时设置

阿里云 RDS 默认 lower_case_table_names=1,Linux 下 MySQL 默认 0。ORM 建的表名带大写(ArticleUser),两边不一致复制直接断。这个参数只在初始化 datadir 时生效,副本起容器前就必须写进 my.cnf,事后改 = 删数据目录重来。

坑 2:mysqldump 要 RELOAD 权限,而 RDS 心跳让 GTID 永不静止

初始全量同步的标准姿势 mysqldump --set-gtid-purged=ON 直接报错:

mysqldump: Couldn't execute 'FLUSH TABLES': Access denied;
you need (at least one of) the RELOAD or FLUSH_TABLES privilege(s)

RDS 普通账号没有也给不了 RELOAD。退而求"dump 前后各取一次 gtid_executed,一致就采信"——重试 8 次全失败:阿里云 RDS 内部心跳表(mysql.ha_health_check)秒级写 GTID,静止窗口根本不存在

最终解法:--set-gtid-purged=OFF + 手工 SET GLOBAL gtid_purged='<dump前的GTID>',配合响亮失败验证——dump 前的 GTID 与快照点间隙只有 1-2 秒,若业务写恰好落在里面,重放时必触发 1062/1032 报错(响亮、可检测,绝不静默错数据),报错就重建重试。心跳事务则被下一条过滤规则挡掉,重放无害。

坑 3:RDS binlog 携带命名时区,官方镜像时区表是空的

复制刚跑起来就断:

Error 1298: Unknown or incorrect time zone: 'Asia/Shanghai'

RDS 的会话时区是命名时区并写进 binlog 事务头;mysql 官方 Docker 镜像的 mysql.time_zone* 表默认全空。my.cnf 写 +08:00 只能对齐服务器默认值,防不住 binlog 里携带的命名时区。解法一行:

docker exec mysql-replica sh -c "mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -uroot mysql"

灌完时区表无需重建,START REPLICA 原地恢复(失败的事务是原子回滚的)。

顺带一条:副本要配 replicate-wild-do-table=业务库.% 过滤,否则 RDS 内部心跳表的 ROW 事件在副本找不到对应表,同样会打断复制。

坑 4:Docker 发布的端口不走 INPUT 链

限制 3306 只允许 app 机访问,iptables -A INPUT --dport 3306 写完毫无作用——Docker 发布端口的流量走 PREROUTING DNAT → FORWARD,根本不进 INPUT。正确姿势是 DOCKER-USER 链 + 匹配 DNAT 前的原始端口:

iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack \
  --ctorigdstport 3306 --ctdir ORIGINAL ! -s <app机IP> -j DROP

坑 5:MYSQL_ROOT_HOST=localhost 下 root 是空密码

为了不把 root 暴露到公网设了 MYSQL_ROOT_HOST=localhost,结果初始化完 mysql.userroot@localhostauthentication_string空的(无密码 socket 直入)。暴露面虽然只有容器内,但仍然要手工补一刀 ALTER USER 并回查字段非空。

另外两道保险值得写进任何副本的 my.cnf:super_read_only=ON(应用层路由 bug 会直接报错而不是把数据写分叉)和 performance_schema=OFF(2G 小机省几百 MB 内存)。

效果:3.3 倍,而且拿到了干净的 A/B

上线后做降级演练时顺手解决了"没有改造前基线"的问题——把副本容器停掉、app 自动回退直连主库,这个状态就是改造前:

状态 同一文章页耗时(海外机本机实测)
直连北京主库(=改造前) 2.7 - 4.2 s
读本地副本(=改造后) 0.81 - 0.95 s

3.3 倍。剩下的 0.8 秒里还包含一次跨洋写(页面浏览计数)和渲染本身,读路径已经不再是瓶颈。

演练同时验证了故障路径:停副本 → app 重启自动降级(站点全程 200 无中断)→ 副本恢复后复制自动续传、延迟归零。日常由 cron 每 5 分钟检查复制线程和延迟,异常发邮件告警。

这套方案适合谁

适合:读写比悬殊的内容站/工具站;数据库不大(我们只有 30MB,断链重建是分钟级);已经做了多地域部署、只差数据这一层。

不适合:写多读少、或对"写后立读"有强一致要求的业务(要么忍受到处标注主库读,要么直接上分布式数据库);库大到全量重建很痛的场景要先算好 binlog 保留窗口。

常见问题 FAQ

副本挂了网站会挂吗?

不会。入口脚本在启动时做 TCP 探测,探测失败自动降级为直连主库;运行中副本宕机则重启一次 app 容器即可回退。fail-safe 的方向永远是"回主库",最坏情况是海外用户变慢,不是不可用。

复制断了怎么办?

先看报错:时区/表结构类问题修复后 START REPLICA 原地续传;无法修复或断链超过 binlog 保留期(我们是 30 天)就删数据目录全量重建——库小的话整个过程只要几分钟。

为什么不用 Redis 缓存解决?

缓存解决的是"热数据重复读",这里的问题是"所有读都要跨洋"。缓存需要失效逻辑侵入业务代码,且冷数据首查仍然慢;副本方案对业务代码几乎透明,一次搭建全量加速。

Prisma 的读写分离扩展稳定吗?

@prisma/extension-read-replicas 是 Prisma 官方扩展,但它依赖 Prisma Client 的内部字段,peer 版本声明比较保守。我们把扩展和 prisma 双双锁死精确版本,并留了一个 7 项断言的路由验证脚本,升级 prisma 前必须重跑。

参考链接

相关文章

语音延迟压进 50 毫秒:体验拐点过了,先拿你的场景跑一遍再说

Qwen3-TTS 把语音合成响应压进 50 毫秒,砍掉传统 TTS 一个数量级。拆解这个数字跨过的感知门槛、为什么关键是「没靠堆算力」、端到端瓶颈会往哪前移,以及为什么官方数字不是你的数字。

developer2026年8月22日9 min
45

零增益的声音:一次 WebAudio 指纹排查,和一个被夸大的结论

有人发现打开速卖通页面后,蓝牙多点耳机就切不回手机了。排查下去是两个隐藏的 AudioContext——增益设成 0,但仍连着系统音频输出,于是浏览器一直在真实处理音频,把蓝牙通道摁住了。这个排查过程值得学。但顺着它传出去的结论「WebAudio 指纹是下一个大威胁」,被 Firefox 指纹防护负责人当场否掉了。

webaudiofirefox+4
developer2026年8月21日8 min
76

86 分钟:Rust 供应链攻击把 yank 机制变成了诱饵

2026 年 8 月 20 日,arrayref 被投毒。真正值得研究的不是恶意代码本身,而是攻击者把 cargo 的 yank 提示变成了社会工程武器——先把好版本全部标记为废弃,再让 cargo 亲口建议你升级到那个恶意版本。整条链路只在线 86 分钟,却踩中了 Rust 依赖模型里最难防的一环:build.rs 在编译期就是任意代码执行。

rustcargo+4
developer2026年8月21日8 min
78

Go 1.27 补上泛型最后一块短板:库作者今天开工,业务代码先别急

Go 1.27 正式发布,泛型方法终于落地——从 1.18 起函数能泛型、方法却不能的四年缺口被补上。拆解标准库怎么用、三个会撞墙的限制、工具链才是真正的升级风险,以及库作者和业务代码分别该做什么。

developer2026年8月21日9 min
100