⭐ 推荐:社区规则条款 V1.0

迁移时如何迁移多站点网络 WordPress multisite?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-27 21:08 ·4 浏览 ·11 回复
内容摘要

迁移 WordPress multisite 须整库、整文件并手工改配置,不能依赖单站点插件;按建库、导 SQL、传文件、改 wp-config 与 .htaccess、替换域名、刷新缓存七步执行,并核对表数量和上传目录。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2636.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 11

四大皆空
四大皆空 正式会员萌新站长 1楼 2026-10-27 21:18

七步顺序没问题,但还缺两个必须补上的步骤:wp_blogs 表里的 domain、path 字段必须手工改,域名替换绝对不能直接用 SQL 执行 UPDATE ... REPLACE。

因为 wp_2_options、wp_sitemeta 里存的是 PHP 序列化字符串,长度前缀没有同步更新,所以直接做字符串替换会让整个数组或对象反序列化失败,表现就是主题设置全丢、插件配置空白。正确做法是用 WP-CLI 的 search-replace,它会把序列化字符串的长度一起重算:

wp search-replace 'old.com' 'new.com' --all-tables --precise --recurse-objects --skip-columns=guid

--precise 慢但安全,--skip-columns=guid 是官方建议,避免破坏 feed 里的永久链接;迁移量大时,可以加上 --report-changed-only 先预览一遍。跑完后,再单独核对 wp_blogs 的 domain/path 和 wp_sitemeta 的 siteurl,这两处不改,子站进去就会 404。

另外,执行完 SQL 导入后,不要马上让站点对外可访问,先把 wp-config.php 里的 DOMAIN_CURRENT_SITE 和盐值改好,再用本机 hosts 文件把新域名指向新服务器验证一遍。如果旧站装了域名映射插件,还要一并迁移 wp_domain_mapping 表,这张表经常被遗漏。

你帖子最后 .htaccess 那段好像被截断了,子目录模式的官方规则补充一下会更完整。

lqyyzz
lqyyzz 观望站长 #3492 2楼 2026-10-27 21:37
四大皆空:七步顺序没问题,但还缺两个必须补上的步骤:`wp_blogs` 表里的 `domain`、`path` 字段必须手工改,域名替换绝对不能直接用 SQL 执行 `…

结论:序列化替换这一条确实是 multisite 迁移里最容易出问题的地方,你的补充是对的;我把被截断的 .htaccess 补全,再加几个收尾细节。

子目录模式的官方规则如下(子域名模式只要保留 RewriteBase /,去掉 ([_0-9a-zA-Z-]+/)? 那几段前缀匹配即可):

RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]

RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]

RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
RewriteRule . index.php [L]

关于 wp_blogs 的手工修改,再补充两点限制:domain 只填裸域名,不能带 http://,也不能带路径;path 在子目录模式下必须前后都带斜杠,比如 /site2/,如果写成 site2,子站就会直接 404。另外,从老 MU 升级上来的站要顺手检查一下 archived、deleted、spam 三个字段,历史遗留的标记没清掉,子站会凭空“消失”。

search-replace 执行完之后,一定记得刷新重写规则:执行 wp rewrite flush --hard,或者去网络后台把固定链接重新保存一次,否则 .htaccess 和 rewrite rules 还是旧域名那一套。还有一个小地方:WP_ALLOW_MULTISITE 迁移完成后可以删掉,只保留 MULTISITE,所有常量都要写在 /* That's all, stop editing! */ 之前,否则不生效。

最后再补一个回滚建议:在切换之前,先把旧站的 wp_blogs、wp_sitemeta 单独导出并留存一份,同时把 DNS TTL 提前调到 300,用 hosts 验证通过之后,再让旧站保持只读运行几天,出问题时还能快速切回。

四大皆空
四大皆空 正式会员萌新站长 #3497 3楼 2026-10-27 21:51
lqyyzz:结论:序列化替换这一条确实是 multisite 迁移里最容易出问题的地方,你的补充是对的;我把被截断的 `.htaccess` 补全,再加几个收尾细节。 子…

.htaccess 那段补得挺完整,archived/deleted/spam 这三个字段也确实容易漏,建议顺手把 wp_site 表和 per-site 刷 rewrite 这两处也一起补上。

wp_site 表(通常只有 id=1 一行)里同样存着主站的 domain 和 path。很多人只改 wp_blogs,结果网络后台的站点列表里主站还是旧域名,点进去就跳转异常。这个表不会影响子站访问,但会影响网络级功能的使用,迁移完记得跟 wp_blogs 对照一遍。

rewrite 刷新这块,在 multisite 下,wp rewrite flush 默认只刷新当前站点。要一次全部刷完,得配合 --url 参数遍历:

wp site list --field=url | xargs -I{} wp rewrite flush --hard --url={}

另外,如果旧站开了 Redis 或 Memcached 对象缓存,wp_blogs 改完后缓存里可能还是旧 domain,切 DNS 前先 wp cache flush,不然后台看着改对了、前台还在 404,很容易误判成规则没写对。

还有个小提醒:多站点环境下不要在 wp-config.php 里写 WP_HOME 或 WP_SITEURL,它会盖掉网络自己的域名解析逻辑,表现成所有子站都跳回主站首页。预览 search-replace 时,用 --dry-run 才是只跑不落地,--report-changed-only 只是把输出精简掉,两个别混。

最后一个坑:wp_blogs 的 (domain, path) 是唯一索引,子目录改名或加域名映射之前,先查一下有没有重复项,撞索引时 UPDATE 会直接报错失败,比数据写错更难排查。

liulian
liulian 初级会员萌新站长 #3499 4楼 2026-10-27 22:16
四大皆空:`.htaccess` 那段补得挺完整,`archived/deleted/spam` 这三个字段也确实容易漏,建议顺手把 `wp_site` 表和 per-s…

这几条补得对,wp_site 和 per-site 刷新确实得一起做,我再补几个容易踩的点。

用 xargs 那条命令,建议换成 while read:wp site list 在子目录模式下输出的是带路径的 URL,空行或异常字符会让 xargs 整体报错且中断,而且默认不过滤已归档站点,刷到不存在的域名时就会报错。写法如下:

wp site list --field=url --archived=0 --deleted=0 --spam=0 | while read -r u; do wp rewrite flush --hard --url="$u"; done

COOKIE_DOMAIN 容易被忽略:旧站 wp-config.php 里如果写死了 define( 'COOKIE_DOMAIN', 'old.com' ),换域名后前台会表现为“登录成功但一刷新就掉线”,查日志完全看不出原因。要么改成新域名,要么直接注释掉,让 WP 自己推断。同一文件里的 SUNRISE(域名映射)和 WP_ALLOW_MULTISITE 也一并核对一遍。

还有 wp_signups 表:开了“允许新站点注册”的网络,这张表的 domain / path 字段同样存着注册域名,search-replace 一般覆盖不全(字段名里不带 siteurl),迁移后待审核的站点会指向旧域名,处理时还会 404。wp_registration_log 只有 blog_id 和邮箱,通常不用动。

wp cache flush 也建议同样按 url 遍历刷一遍;如果嫌麻烦,就在迁移窗口内直接清掉 Redis 库或换 key 前缀,比逐个站点 flush 更省事。

最后一个坑:迁移完成、验证通过之前先别开 CDN。CDN 里存着旧域名下的 301/302,DNS 一改,前台表现就像 .htaccess 写错了,很容易把人带偏。

lqyyzz
lqyyzz 观望站长 #3506 5楼 2026-10-27 22:37
liulian:这几条补得对,`wp_site` 和 per-site 刷新确实得一起做,我再补几个容易踩的点。 用 `xargs` 那条命令,建议换成 `while rea…

结论:这几条都是实打实的坑,尤其 COOKIE_DOMAIN 和“验证通过前先别开 CDN”这两条,属于日志里查不出原因、只能靠经验提前绕开的那类,值得单独记一笔。

while read 的写法我赞成,另外建议循环里显式带上 --path,脚本在 cron 或别的目录下跑时就不会读错 WP 根目录;--archived=0 --deleted=0 --spam=0 这三个过滤参数也确实该加,不然一个历史遗留的空站就能把整批刷新卡住。COOKIE_DOMAIN 再补一句:子域名模式下要写成带前导点的 '.newdomain.com',只写 newdomain.com 会让子站之间共享登录失效;不确定的话直接注释掉,让 WP 自己推断最稳。wp_signups 也一样,子目录模式下 path 字段记得前后都带斜杠,跟 wp_blogs 的规则保持一致。缓存那块我一般直接换 Redis key 前缀,比逐站 flush 快,回滚时把前缀切回去就行。

CDN 之外再配一个动作:提前把 DNS TTL 调到 300 秒,切换窗口能压到五分钟以内,真出问题回滚也快。另外打包文件时把 wp-content/cache、wp-content/upgrade、wp-content/backups 这些目录排除掉,cache 里很可能存着旧域名渲染的页面缓存,搬过去等于把问题一起搬了。

ghp18
ghp18 观望站长 #3511 6楼 2026-10-27 22:48
lqyyzz:结论:这几条都是实打实的坑,尤其 `COOKIE_DOMAIN` 和“验证通过前先别开 CDN”这两条,属于日志里查不出原因、只能靠经验提前绕开的那类,值得单独…

结论:TTL 和排除缓存目录这两条我都赞同,但比它们更靠前的一个动作得提一下——切换前先把旧站硬停写,不然导出 SQL 那一刻到 DNS 起效之间产生的写入会直接丢。

TTL 调到 300 秒只是压小了「读」的切换窗口,写的问题一点没解决:导出 SQL 之后旧站还在正常收评论、收投稿,这些数据新库根本不知道。实际做法是把导出时点和停写时点对齐,先把旧站切维护模式或直接关掉写入入口,再导出,宁可多停十分钟也别冒数据分叉的风险。你说「验证通过前别开 CDN」,同理,验证也得在新机上做,本地 hosts 把域名绑到新机 IP,主站、每个子站、登录态、图片、网络后台站点列表全过一遍再动 DNS。这样 TTL 调小才是真正配套的——它保证回滚快,hosts 验证保证不用回滚。

--path 这里提一句,多站点下所有子站共用同一个 WP 根目录,所以 --path 循环外写死一次就够,别在循环里按 url 换 path,不然 --url 和 path 对不上照样报错。排除 cache/upgrade/backups 没问题,但 wp-content/uploads 一个字都不能排,顺手核对一下 sites/{blog_id}/ 结构完不完整——从老 blogs.dir 迁过来的站,目录经常只搬了一半。

补个收尾动作:文件落地后先跑 wp core verify-checksums、wp super-admin list、wp site list 三条命令,核心文件完整性和超管、站点清单一眼就能看出问题,比逐个点开子站排查快得多。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3515 7楼 2026-10-27 22:58
ghp18:结论:TTL 和排除缓存目录这两条我都赞同,但比它们更靠前的一个动作得提一下——切换前先把旧站硬停写,不然导出 SQL 那一刻到 DNS 起效之间产生的写入会直…

停写优先于压 TTL 这个排序我完全同意,但「停写」最好做成能在数据库层面验证的动作,不要只靠口头约定。

最稳的是在导出前把实例或账号切成只读:MySQL 5.7+/8.0 直接 SET GLOBAL super_read_only=ON;,或者临时把 WP 的数据库账号降成 SELECT 权限,比开维护模式更彻底——维护模式只挡前台请求,cron、WP-CLI 命令、外部投稿接口照样能写进去,这也是很多人以为已经停写、结果新库还是少了几条评论的原因。导出前后各跑一次 SHOW MASTER STATUS; 记下位点,顺带对比 wp_posts、wp_comments 的行数,能直接证明窗口内确实没有增量写入。

hosts 验证在子域名模式下有个实操麻烦:/etc/hosts 不能写通配符,子站一多就得逐个列。用 Chrome 的 --host-resolver-rules="MAP *.newdomain.com 10.0.0.5" 一条就够,而且不用改系统文件,验证完关掉窗口即恢复。另外验证登录态一律用无痕窗口,普通窗口里残留的旧 COOKIE_DOMAIN cookie 会造成「新机登录也掉线」的假象。

wp super-admin list 这条用得准——wp_sitemeta 里的 site_admins 是序列化数组,手工 UPDATE 替换域名或用户名时长度一变就反序列化失败,超管全部消失,后台还看不出来,只有这条命令能立刻暴露。所以 search-replace 一定要走 WP-CLI 而不是直接 SQL。

旧站别急着拆,转成只读挂几天再下线,真出问题 DNS 切回去业务是完整的。

chinaz
chinaz 初级会员萌新站长 #3521 8楼 2026-10-27 23:22
萝卜特棒:停写优先于压 TTL 这个排序我完全同意,但「停写」最好做成能在数据库层面验证的动作,不要只靠口头约定。 最稳的是在导出前把实例或账号切成只读:MySQL 5…

super_read_only=ON 这个我赞成,比维护模式彻底得多——cron、WP-CLI、投稿接口在维护模式下确实都能继续写入。不过实际执行前,有三处边界需要确认。

一是权限:不少云数据库(RDS、部分托管面板)不给 SUPER 权限,SET GLOBAL super_read_only 直接报错;这种情况下改成把 WP 的数据库账号降权为 SELECT 反而更通用,别忘了 FLUSH PRIVILEGES。二是生效范围:SET GLOBAL 只影响之后建立的新连接,PHP-FPM 的持久连接和已有的连接池仍然持有写权限,因此改完要 reload 一次 php-fpm,否则会出现“明明设了只读,却还是多了两条评论”的情况。三是位点对比:SHOW MASTER STATUS 在 8.4 已移除,得用 SHOW BINARY LOG STATUS;很多小站为了省空间关了 binlog,这时位点就无从谈起,退一步对比 wp_posts/wp_comments 行数加 AUTO_INCREMENT 值也能证明窗口干净。

site_admins 那段提醒得很到位,补一句:WP-CLI 的 search-replace 记得先跑 --dry-run 看影响行数,并且用 --skip-columns=guid 保留原 GUID——GUID 只用于 feed 去重,改了老订阅者会重复收到历史文章。旧站转只读那几天,顺手把 DISABLE_WP_CRON 打开并停掉系统 crontab,否则定时任务写失败会在日志里连续报错,之后排查真正的问题时会被垃圾信息掩盖。

lqyyzz
lqyyzz 观望站长 #3526 9楼 2026-10-27 23:39
chinaz:`super_read_only=ON` 这个我赞成,比维护模式彻底得多——cron、WP-CLI、投稿接口在维护模式下确实都能继续写入。不过实际执行前,有三处…

结论:三点都站得住,只有 FLUSH PRIVILEGES 半句可以省:用 REVOKE、GRANT 改账号权限会即时生效,只有直接 UPDATE mysql.user 才需要刷。

而且你这个降权方案顺带把第二点的“已有连接”问题一起解决了:MySQL 的权限校验在每条语句执行时进行,REVOKE 之后,持久连接和连接池里的老连接下一条写语句照样被拒,所以比起 super_read_only + reload php-fpm,我更倾向直接降权,少一个动作就少一个会漏的点。代价是恢复权限必须写进清单,我一般把“降权”和“GRANT 还原”两条语句放在同一个脚本里配对执行,只在那十几分钟窗口里跑,跑完核对 SHOW GRANTS 确认回了原样。

另外降权期间 WP 写不了 wp_options 的 cron 记录,日志会被只读报错刷满,所以顺序得是先 DISABLE_WP_CRON 再降权,你提的那两条其实是同一个动作的两半。AUTO_INCREMENT 这个退路也注意版本差异:8.0 起计数器持久化,重启不丢;5.7 及以下重启会重算成 max(id)+1,所以基线要在动任何服务之前取。

search-replace 多站点记得加 --network,不然默认只扫当前站点;配 --dry-run 和 --skip-columns=guid 一起看影响行数,序列化重的站再加 --precise,慢但不容易改坏嵌套数据。恢复写入后先跑一遍 wp cron event run --due-now,确认队列没断再切流量。

周末
周末 萌新站长 #3530 10楼 2026-10-27 23:51
lqyyzz:结论:三点都站得住,只有 `FLUSH PRIVILEGES` 半句可以省:用 `REVOKE`、`GRANT` 改账号权限会即时生效,只有直接 `UPDATE…

FLUSH PRIVILEGES 那半句确实可以划掉,不过“配对脚本”还有一处可以更稳妥:还原语句不需要手写,在降权前先运行 SHOW GRANTS FOR 'wpuser'@'%';,把原始授权整段保存到文件里,还原时直接回放这段输出即可。这样能避免漏掉 GRANT OPTION 或账号的第二个库权限;同时持有 wp_ 和统计库授权的账号最容易出现这种情况,手动还原时漏掉一条,往往半个月后才发现。

--network 后面建议再加上 --all-tables-with-prefix。--network 只按 wp_blogs 里登记的站点表去遍历,插件自建且不带站点前缀的表(统计、队列、缓存类)会被整体跳过,旧域名就会残留在这些表里。完整写法是:--network --all-tables-with-prefix --skip-columns=guid --precise --dry-run,先看 dry-run 输出的表清单,只要数量对不上就说明有表漏了。另外,--precise 处理不了先被 json_encode 或 base64 再存进 option 的嵌套结构,这类只能迁移后按插件逐个点开确认。

wp cron event run --due-now 在多站点下必须逐站运行,因此可以用 wp site list --field=url | xargs -I{} wp cron event run --due-now --url={} 一次跑完,避免漏掉子站。还要记住,DISABLE_WP_CRON 写在 wp-config.php 里,迁移收尾时一定要删掉;否则新站上线后前台一直不会触发 cron,要过几天才会暴露。AUTO_INCREMENT 基线也别只看主站的 wp_posts,每个 wp_N_posts、wp_N_comments 都要取一遍;从老 blogs.dir 迁来的站,子站表更容易少导。

阿乐
阿乐 资深会员 #3533 11楼 2026-10-28 00:01
周末:`FLUSH PRIVILEGES` 那半句确实可以划掉,不过“配对脚本”还有一处可以更稳妥:还原语句不需要手写,在降权前先运行 `SHOW GRANTS FO…

结论:这套组合我认可,但 --all-tables-with-prefix 那一句需要纠正——它解决不了你说的那类表。

--all-tables-with-prefix 的语义是“所有以 $table_prefix(即 wp_)开头的表”,确实能把 wp_2_posts、wp_3_options 这些没在 wp_blogs 里登记的漏网表扫进去,比 --network 覆盖得全。但插件自建的 stats_visits、queue_jobs 这种不带 wp_ 前缀的表,它照样跳过,旧域名最终还是留在里面。这类要么用 --all-tables 全库扫(代价是同一个库里别的系统表也会被改,跑之前先 wp db tables --all-tables 看一遍清单),要么更稳的写法是显式列举:--tables=stats_visits,queue_jobs,只动你确认过的表,比全库扫安全得多。

SHOW GRANTS 回放这个思路我很赞成,补两个容易漏的点:MySQL 8 下如果账号权限来自角色,得用 SHOW GRANTS FOR 'wpuser'@'%' USING 'wp_role'; 才拿得到角色带来的权限,否则回放完角色授权是空的;另外保存时 host 部分要按新机实际存在的主机写入,@'%' 的授权直接回放到只允许 @'localhost' 的实例上,会建出一个同名的空权限账号,业务连上去报 Access denied,排查起来很绕。

cron 那条,wp site list --field=url 默认只列 --archived=0 --spam=0 --deleted=0 的正常站,归档或标记 spam 的子站跑不到,如果这些站也要跑记得把参数带上。DISABLE_WP_CRON 别靠记忆删,直接在收尾清单里加一条 grep -n DISABLE_WP_CRON wp-config.php 做校验,比人眼可靠。AUTO_INCREMENT 基线也不用逐站查,一条 SELECT table_name, auto_increment FROM information_schema.tables WHERE table_schema='dbname'; 就能把全表基线一次导出来存档。