一、先说清楚要做什么
本站跑在阿里云 2 核 1.8G 的 ECS 上,WordPress + MySQL 8.4 容器服役两年。这篇文章记录一次不停机的生产迁移:把数据层从 MySQL 容器换成 SQLite 单文件。目标只有一个——用实测数据回答”对小博客,这刀值不值得动”。
“生产”这个约束会让每个决策变重:不能停机、不能丢一篇内容、回滚必须分钟级可行。所以这次迁移的验收标准不是”能跑”,而是下面这张表:
| 指标 | 目标 | 怎么测 |
|---|---|---|
| 迁移期间博客可用性 | 全程在线 | curl 首页状态码监控 |
| 内容一致性 | 203 篇逐篇 MD5 归零 | 双库全量比对脚本 |
| 常驻内存 | MySQL 容器(~360MB)归零 | docker stats |
| 冷查询延迟 | 不劣化(实际反超一个量级) | 同容器 PHP 基准 300 次 |
| 恢复演练 | 备份能真的恢复出全量内容 | 解压快照 + 只读校验 |
二、整体方案:三条路线里选一
WordPress 迁 SQLite 有三条路,选错第一条后面全是补丁:
| 方案 | 做法 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| A. 官方插件原位切换 | sqlite-database-integration 插件导入,生成 db.php dropin 接管 $wpdb |
WordPress 官方参与维护;wp-cli、REST、所有 WP API 语义原样保留 | 依赖插件版本跟进 WP 版本 | 本站选择 |
| B. dump 手工转换 | mysqldump 导出,手工改写 DDL 后灌入 sqlite3 | 不依赖插件 | 类型映射、转义、索引全要手工核对,改漏一处就是隐性数据损坏 | 深度定制站 |
| C. 双写灰度切流 | 期间两库双写,逐步切读 | 大站最稳 | 复杂度爆炸,个人博客杀鸡用牛刀 | 高流量站 |
反模式警告:把 mysqldump 的输出直接灌进 sqlite3 命令行。MySQL 方言里 ENGINE=InnoDB、反引号、AUTO_INCREMENT 计数都会让导入静默失败或丢索引——错了不报错,最贵。
切换前后的拓扑变化就一句话:数据从”隔壁容器里的服务”变成”PHP 进程里的一个库文件”:
# 迁移前:一次查询 = TCP + 协议 + 鉴权 + 查询
wp_xiwei (PHP) ──3306──> mysql 容器 (mysqld, ~360MB 常驻)
1. 迁移后:一次查询 = 函数调用
wp_xiwei (PHP) ──直接──> wp-content/database/wp.sqlite (8.4MB 文件)
三、一致性验证:切换成功 ≠ 迁移成功
导入完成那一刻什么都证明不了。验证用的是双库全量比对:mysqli 连 MySQL、SQLite3 只读打开新库,203 篇逐篇比 MD5。
结果是 203/203 篇都在,其中 9 篇字节级不一致。差异不等于错误——判定用的是三件套:
- 结构指标:表格单元格数、代码块数量双侧对比(66 = 66 ✓)
- 关键内容标记:新写入的小节、修复补丁在不在
- 损伤标记:空表格、坏标签双侧扫描
最终 9 篇全部判定为等价变换:迁移器做了转义规范化(& 与 & 这类),以及 wp_update_post 的内容过滤。语义零差异。
反模式警告:只比”文章数一致”就宣布迁移成功。行数一致但正文被过滤坏半篇,是完全可能发生的事故。
本案例的真实插曲:迁移快照恰好落在一次全站内容修复的收尾窗口之间——6 篇刚改完的文章没进快照。补救方式是用 wp_update_post 从源库把最终内容写穿到新库(走 WP API 会自动触发缓存失效)。教训:要么迁移前冻结写入,要么迁移后立刻做一次增量 diff,二选一。
四、最大的坑:三层缓存对”裸操作”完全不感知
这个站的缓存架构有三层,全部不知道”数据库换了”这件事:
请求 → W3TC 页面缓存(磁盘 file_generic)
→ W3TC dbcache(Redis,缓存 SQL 结果)
→ redis-cache 对象缓存(Redis,缓存对象)
→ 数据库(这次换了 SQLite)
迁移当天踩的最深的坑:绕过 WP 直接改数据库后,前台和 REST 迟迟”不生效”。排查链是清一层、验一次、还剩一层的窒息循环——因为两个细节:
- 页面缓存连带查询串的 REST URL 都会缓存,你以为加个随机参数就绕开了,其实没有
- 验证通道本身被缓存污染:看 REST 判断生效与否是不可靠的,要看前台页面 HTML
正确姿势两条,从这次之后固化为规矩:
- 改内容一律走 WP API(wp post update / wp_update_post / REST 写入),写穿路径自动失效三层缓存
- 必须裸写时,事后三层全清:
rm -rf wp-content/cache+redis-cli FLUSHDB(对应库)+apachectl graceful
另一个迁移日专属小坑:wp-cli 的 wp post term set 只认分类名不认数字 ID——传 90 进去会创建一个名字叫”90″的新分类,文章全挂错。操作密集的迁移日,这类语义坑专门咬熟手。
五、WAL 与写并发:博客负载刚好合适
SQLite 最常被质疑的是写并发。先确认模式:
PRAGMA journal_mode; -- wal(插件已默认开启)
WAL 的语义:读不阻塞写、写不阻塞读、写与写串行。对博客负载画像——203 篇内容、读多写少的极端比例、三层缓存之下数据库只服务冷路径——单写者绰绰有余。唯一要留意的信号是 database is locked,出现时优先查 busy_timeout 而不是急着换库。
六、运维配套:三件必须跟着换的东西
库换了,围绕库的运维件不同步换,等于埋新雷:
- 备份反模式:裸拷贝 wp.sqlite 文件——WAL 模式下写入进行时的裸拷贝可能拿到撕裂副本。正确做法(每日 cron):
// PHP:源库只读打开,backup() 产生一致性副本
$s = new SQLite3("/var/www/html/wp-content/database/wp.sqlite",
SQLITE3_OPEN_READONLY);
$s->backup(new SQLite3("/tmp/db-backup.sqlite"));
- 恢复演练
- 开机自检PRAGMA integrity_check 返回 ok。
七、真实数字总账
| 项 | MySQL 时代 | SQLite 时代 |
|---|---|---|
| 常驻内存 | ~360MB(独立容器,InnoDB buffer pool) | 0(库嵌入 PHP 进程) |
| 全机可用内存 | 257Mi | 479Mi(+86%) |
| 数据体积 | 数据目录 + redo/binlog(转储 1.8MB gz) | 8.4MB 单文件(快照 1.5MB gz) |
| 冷查询(主键取正文) | ~0.3–1ms 级(TCP+协议+鉴权;通用参考值,迁移后已无同机对照) | 0.016ms 实测(300 次均值) |
| 恢复全库 | 导入 dump,分钟级 | 解压一个文件,秒级 |
诚实结论三条:
- 性能上这是一次“免费升级”而不是”性能升级”——三层缓存下数据库只服务冷路径,0.02ms 还是 0.5ms 用户毫无感知。真正的收益是内存和运维简化。
- 什么情况我会劝你别迁:高并发写(热文评论区爆量、WooCommerce 多写场景)、依赖 MySQL 特有 SQL 的插件、计划做多机读写分离。
- 单文件单机是 SQLite 的天花板,也是它的地板——数据跟着一个文件走,备份、迁移、恢复从未如此简单。
八、这次迁移教会了什么
- 迁移的完成时刻不是切换时刻,是全量 diff 归零的时刻
- 缓存架构决定运维姿势:多层缓存的世界里,WP API 是唯一正门,裸 SQL 是自找的迷雾
- 单文件数据库的备份必须用一致性 API,裸拷贝是定时炸弹
- 资源受限的小机器上,”少一个 360MB 常驻进程”远比”快几毫秒”值钱
- 迁移日操作密度高,命令行工具的语义坑(比如 term set 认名不认 ID)专门咬熟手——重大操作前先在草稿环境过一遍
延伸阅读:《MySQL 连接数打满的定位方法》——MySQL 时代排障的代表作,如今成了时代的眼泪;《Docker 容器无限重启的排查路径》——容器化数据层的通用排障思路。
本文数字全部来自 2026-11-05 迁移当日的实测;负载不同结论会不同,但判断框架(一致性三件套、缓存认知、配套先行)是通用的。