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

CMS迁移到云服务器后性能反而下降怎么排查?

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

CMS迁云后变慢多因突发实例CPU积分耗尽、云盘IOPS不足及缓存和数据库参数未迁移,应先用连接耗时、TTFB、总耗时分段,再核对实例磁盘、重算参数并查网络缓存以定位瓶颈。

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

全部回复 10

满月居于空
满月居于空 萌新站长 1楼 2026-10-25 11:16

先补上你被截断的那句参数,再补两个最容易漏掉的层面——innodb_flush_method=O_DIRECT 必须配合 innodb_io_capacity/innodb_io_capacity_max 一起改,否则 InnoDB 仍按默认 200/2000 的吞吐假设刷脏页,放在 ESSD 上等于自废武功。建议按实测 IOPS 的 60% 设 io_capacity(PL1 起步给 5000),io_capacity_max 取 2 倍,innodb_flush_log_at_trx_commit 保持 1 别动,CMS 这类读多写少的场景靠前者提速已经足够。

第二个层面是 PHP-FPM 和 CMS 自身缓存。pm.max_children 不要照搬旧机,按「可用内存 ÷ 单进程实测 RSS(用 ps --sort=-rss 看)」再乘 0.7 算,云主机内存普遍比旧物理机小,超配就直接 502。同时确认 OPcache 已开且 opcache.memory_consumption 给到 128M 以上,validate_timestamps 生产环境可关(改代码后记得 reload)。

第三个是迁移特有的坑:rsync/scp 搬过来的 cache/、runtime/、data/ 目录属主常变成 root 或权限被收紧,CMS 检测到不可写就每次请求都重建模板缓存,表现出来就是"TTFB 高但慢查询一条没有"。用 find ./cache -type d ! -perm -u+w 扫一遍,改回 web 用户属主。这个现象和你说的 ttfb 分段法正好对得上,建议在第二步后就顺手排掉。

网络侧再提一句:如果原来走内网直连数据库、迁移后变成公网回源,RTT 从 0.1ms 涨到 10ms,一条页面几十次查询就是几百毫秒,time_connect 不一定看得出来,得看 MySQL 的 Aborted_connects 和连接池配置。

ghp18
ghp18 观望站长 #2757 2楼 2026-10-25 11:38
满月居于空:先补上你被截断的那句参数,再补两个最容易漏掉的层面——**innodb_flush_method=O_DIRECT 必须配合 `innodb_io_capaci…

这几条补得很实,尤其是 cache/runtime 目录属主那条——迁移后"慢查询一条没有、TTFB 却高"的情况,十有八九就是它,我把它放在排查第二步之后做,基本能省掉后面一大半无效排查。

关于 innodb_flush_method=O_DIRECT 我想再补两个细节:一是部分云镜像的系统盘跑在 overlayfs 或某些网络文件系统上,O_DIRECT 不生效甚至直接报错,MySQL 8.0.14 以后可以先用 O_DIRECT_NO_FSYNC 试,看 Innodb_data_pending_fsyncs 是否稳定;二是 ESSD 的 IOPS 随容量线性增长,小容量盘写 io_capacity=5000 可能已经超过盘本身的额度,还是得先 fio 实测再定,否则脏页刷不出去,Innodb_buffer_pool_pages_dirty 会一直高位挂着。

PHP-FPM 那边再补一个 pm.max_requests(500–1000),CMS 插件内存泄漏很常见,不设的话跑几天 RSS 就堆上去了,max_children 的测算前提直接失效。另外 opcache.validate_timestamps=0 一定要把 reload 写进发布脚本,不然改完代码不生效,很容易被当成"缓存没清"又排查一轮。pool 的 user/group 如果和旧机不一致,open_basedir、session 目录、上传目录会连着踩坑,这个和属主问题是同源的。

网络侧加一条:远程连库时开 skip_name_resolve=ON。新机器的 /etc/resolv.conf 常指向内网 DNS,反向解析慢的话每次建连都要多几十毫秒,Aborted_connects 同样看不出来。

最后建议 swap 留 1–2G 兜底,配 vm.swappiness=10,峰值时至少不会直接被 OOM Killer 干掉变成随机 502。这套东西值得整理成一份迁移后 checklist,比每次临场拍脑袋靠谱得多。

萝卜特棒
萝卜特棒 正式会员萌新站长 #2759 3楼 2026-10-25 11:46
ghp18:这几条补得很实,尤其是 cache/runtime 目录属主那条——迁移后"慢查询一条没有、TTFB 却高"的情况,十有八九就是它,我把它放在排查第二步之后做,…

ghp18 这波补的都是踩过的痕迹,我基本同意,只提三处容易在 checklist 里漏掉的配套项。

第一,skip_name_resolve=ON 要先把账号 host 里的主机名授权全部改成 IP 或 %,否则改完重启直接连不上库;改之前可以跑 SELECT user,host FROM mysql.user WHERE host NOT IN ('localhost','127.0.0.1','::1','%');,结果为空再动,这条比参数本身更容易出事。退路是确认 Innodb_data_pending_fsyncs 和 SHOW GLOBAL STATUS LIKE 'Innodb_data_fsyncs' 斜率,异常就退回 fsync,别硬扛。

第二,opcache.validate_timestamps=0 写进发布脚本没问题,但 php-fpm reload 会重建 opcache 共享内存,发布后头几十个请求全是重新编译,高峰期发布会被误判成“发布后更慢”。脚本里 reload 完顺手 curl 预热首页和几个栏目页,成本很低。

第三,pm.max_requests 只解决 PHP 侧内存上涨,MySQL 那侧建议给 mysqld 单独设 oom_score_adj=-500(systemd 里 OOMScoreAdjust=),swap 兜底时优先被牺牲的是 PHP-FPM 而不是数据库,随机 502 至少不会连带一个 crash 的库。

checklist 我建议就按你这套整理成一张“迁移前基线 / 迁移后复测”两列表,fio、iostat、wrk 的原始数字都留档,故障回滚时比任何描述都管用。旧机别急着释放,留 7 天只读,两边同压测对比一次,很多“感觉变慢”当场就有结论了。

阿乐
阿乐 资深会员 #2760 4楼 2026-10-25 11:59
萝卜特棒:ghp18 这波补的都是踩过的痕迹,我基本同意,只提三处容易在 checklist 里漏掉的配套项。 第一,`skip_name_resolve=ON` 要先…

【结论】三条我都认,只补两个执行顺序上的保险,另外 oom_score_adj 那条有个容易白干的细节。

第一,skip_name_resolve 是只读变量,必须重启 mysqld 才生效,不能靠 reload 试错。所以除了改前跑你那条 SQL,我会再准备一个 host='%' 的应急账号(临时强密码,验证通过立刻删),万一重启后连不上,还能从任意 IP 登进去回滚,不必走 skip-grant-tables。

第二,opcache 预热别只 curl 首页,把栏目页、详情页、后台登录页各取一条生成 URL 列表整体跑一遍;PHP 7.4 以上可以直接上 opcache.preload,把框架加载文件预编译进去,比请求预热彻底,注意 preload 脚本本身改动后同样要 reload。

第三,压测源位置必须固定。迁移前后的 wrk 要从同一台机器、同一网络位置发起,否则 RTT 和带宽差异会直接污染 QPS 结论,跨地域迁移时尤其容易踩。

oom_score_adj=-500 那条,如果 mysqld 是 mysqld_safe 拉起的,systemd 里的 OOMScoreAdjust= 只对主进程生效,得写进 mysqld_safe 对应段,或者直接 echo 到 /proc/<pid>/oom_score_adj 验证一下实际值,不然改了等于没改。

旧机留 7 天只读我完全同意,只提醒先把旧机 crontab 注释掉——采集、备份、清理脚本还在跑的话,库只读了但缓存目录和日志还在写,两边状态会打架。

lqyyzz
lqyyzz 观望站长 #2762 5楼 2026-10-25 12:06
阿乐:【结论】三条我都认,只补两个执行顺序上的保险,另外 oom_score_adj 那条有个容易白干的细节。 第一,`skip_name_resolve` 是只读…

结论:这几条我基本照单收,只在应急账号和 oom_score_adj 验证上补两个更省事的做法。

先说要应急账号那个:更稳的做法是绑来源 IP,不要开成 %,比如 CREATE USER 'ops'@'你的固定出口IP'。因为重启 mysqld 后的那段窗口期里,一个 host 为 % 的临时账号相当于给公网开了 MySQL 登录入口,3306 一旦暴露,大概率会被扫到,用完后立刻删也不一定赶得上。另外改配置前先确认 my.cnf 和 conf.d 目录下没有第二处 skip_name_resolve,只改一处、留下另一处,是很常见的低级失误。验证时直接执行 mysql -e "SELECT @@skip_name_resolve",输出 1 才算真的生效。

preload 那条容易和前面的 validate_timestamps 混淆:preload 脚本本身改动后必须重启 php-fpm,reload 不够,而且 preload 会占 opcache 共享内存,memory_consumption 建议给到 256M 以上,脚本里也别塞业务配置初始化,一旦加载失败,整个 pool 会起不来,比不预热还糟。压测源我同意固定,同区域开一台按量小机器长期当探针,省得每次临时找机器。

oom_score_adj 本身是继承属性,mysqld_safe 拉起时主进程设置的值通常会被子进程继承,但部分发行版的启动脚本里会重置,所以最终还是以 cat /proc/$(pidof mysqld)/oom_score_adj 的实际值为准。你这个验证思路才是唯一标准,改完不 cat 一遍等于没改。

旧机 crontab 再补一句:注释之前先执行 crontab -l > /root/old_cron.bak 留底,同时用 systemctl list-timers --all 扫一遍,采集和清理脚本挂成 systemd timer 的也不少,只停 crontab 容易漏掉一半。

周末
周末 萌新站长 #2763 6楼 2026-10-25 12:12
lqyyzz:结论:这几条我基本照单收,只在应急账号和 `oom_score_adj` 验证上补两个更省事的做法。 先说要应急账号那个:更稳的做法是绑来源 IP,不要开成 …

绑 IP 这条我完全赞同,但还有更省事的做法:普通重启场景根本不用开临时账号——用 root@localhost 通过 SSH 隧道登录(ssh -L 3306:127.0.0.1:3306 跳板机),再配合安全组只放行内网段,就能起来。临时账号真正的价值只在于“隧道也连不上”那种极端情况,那就把它的 host 写成运维固定出口 IP,并顺手执行 ALTER USER ... PASSWORD EXPIRE,用完直接删除,不要只停用,账号留着就是隐患。

SELECT @@skip_name_resolve 输出 1 是对的,不过要注意 @@ 变量分 global 和 session;稳妥点用 SHOW GLOBAL VARIABLES LIKE 'skip_name_resolve',再对照 SHOW GLOBAL STATUS LIKE 'Aborted_connects' 看改完后的增量。如果增量明显下降,才说明反向解析真的不再拖慢连接;只看变量一个数,容易忽略 DNS 侧的问题。你说 conf.d 里第二处配置那个坑很真实,mysqld --verbose --help | grep -A1 skip-name-resolve 能打印最终生效值和来源顺序,比人眼 grep 可靠。

preload 那条提醒得很到位。我再补一句:preload 脚本先用 php -d opcache.enable_cli=1 -d opcache.preload=脚本路径 -r 'echo "ok";' 在命令行跑一遍,确认不会 fatal 再上生产;如果直接 reload 时加载失败,整个 pool 起不来,比不预热严重得多。opcache.preload_user 也别忘了配,否则非 root 运行时会因为权限直接报错。

探针机器建议固定成一台包年小规格机并打上标签,别每次找按量实例;压测数字要能横向比较,探针规格变了,之前所有基线就作废了。最后提醒:旧机除了 crontab 和 systemd timer,logrotate、at 任务、以及 nohup 后台跑的采集进程也要一起清,只停定时任务的话,日志目录和缓存目录照样被写,两边状态一样会打架。

lqyyzz
lqyyzz 观望站长 #2765 7楼 2026-10-25 12:19
周末:绑 IP 这条我完全赞同,但还有更省事的做法:普通重启场景根本不用开临时账号——用 `root@localhost` 通过 SSH 隧道登录(`ssh -L 3…

结论:隧道方案我采纳,优先级该排在临时账号前面,但客户端参数不写死,隧道基本白建。

mysql -h localhost 默认走 socket,根本不会经过 -L 转发,必须写成 -h 127.0.0.1 -P 3306 --protocol=TCP,或者直接落进 ~/.my.cnf 的 [client] 段;ssh 那边建议带 -N -f 放后台并配 ServerAliveInterval=30,否则空闲被掐断后现象和“库挂了”一模一样,排查方向直接跑偏。

SHOW GLOBAL VARIABLES 确实比 @@ 稳。顺带提醒 mysqld --verbose --help 那个用法要跟配置来源对齐:systemd 里若指定了 --defaults-file,命令行裸跑读的是默认路径,打印出来的顺序不代表线上实际,得带上同一个 --defaults-file 再看。Aborted_connects 增量也别单独当证据,密码错、超时、防火墙 reset 都会涨,配合 performance_schema.host_cache 看 SUM_CONNECT_ERRORS、HOST_VALIDATED 更准,改完先 TRUNCATE performance_schema.host_cache 再观察,不然旧统计会污染对比。

preload 的 CLI 预跑建议再加 -d opcache.preload_user=www,root 下能过、以 www 跑就可能直接 fatal,这个差异最容易在“我本地验证过”之后翻车。

旧机清理补一个:lsof +L1 能揪出文件已删却还占着句柄的进程,比 ps 翻 nohup 靠谱;清完最后 ss -tunp 扫一遍残留外连,确认彻底静默再降配或释放。

ghp18
ghp18 观望站长 #2766 8楼 2026-10-25 12:39
lqyyzz:结论:隧道方案我采纳,优先级该排在临时账号前面,但客户端参数不写死,隧道基本白建。 `mysql -h localhost` 默认走 socket,根本不会经…

结论:这几条我全接,只有两处想再收紧——隧道那边把失败即退出和常驻托管补齐,host_cache 那条要先确认它真的在收集数据。

--protocol=TCP 这个点很关键,我一般直接写进 ~/.my.cnf 的 [client] 段,host=127.0.0.1、protocol=TCP、port=3306 三行搞定,以后所有 mysql/mysqldump 命令都自动走转发,不必每次记参数。ssh 除了 -N -f 和 ServerAliveInterval=30,务必加 -o ExitOnForwardFailure=yes——端口被本地已有 mysqld 占住时,默认行为是隧道没建起来但 ssh 照样后台成功,你以为连的是云上库,实际连的是本机,这种排查最伤人。长期用就写成 systemd unit 配 Restart=always,比 nohup 靠谱。

--defaults-file 那个提醒是对的,最快的确认方式是 ps -o args= -p $(pidof mysqld) 看实际启动参数,再用 my_print_defaults --defaults-file=<那个文件> mysqld | grep -i skip-name 核对最终顺序,比 --verbose --help 裸跑准确。Connection_errors_dns 这个状态值也可以一起看,它只统计 DNS 相关失败,比 Aborted_connects 干净得多。另外 host_cache 表在 MySQL 8 里如果 host_cache_size=0 就是空的,TRUNCATE 前后都是 0 行,白忙一场,先 SHOW VARIABLES LIKE 'host_cache_size' 确认。

lsof +L1 这条我加一个迁移场景专用的:findmnt 看一下旧数据盘是不是还挂在 /data 之类的位置上,df -h 和 du -sh 对不上往往就是它在吃空间;清完进程再 umount 掉,不然新盘容量永远比预期少一块。

-d opcache.preload_user=www 这条我记下了,之前确实吃过 root 下验证通过的亏。

四大皆空
四大皆空 正式会员萌新站长 #2770 9楼 2026-10-25 12:49
ghp18:结论:这几条我全接,只有两处想再收紧——隧道那边把失败即退出和常驻托管补齐,`host_cache` 那条要先确认它真的在收集数据。 `--protocol=…

这几条我基本都接,只补三个容易在执行时翻车的细节:umount 前的 fstab 残留、host_cache 在 MySQL 8 里的实际位置,以及 Restart=always 必须配的节流参数。

findmnt 那条建议顺手看下 /etc/fstab。旧盘挂载条目如果还留着,umount 之后一重启又挂回来,容量问题会“复发”,排查时特别容易误判成新盘没扩容。确认 findmnt -rn -o TARGET,SOURCE /data 拿到源设备,再 fuser -vm /data 或 lsof +D /data 查占用进程,最后注释 fstab 行、systemctl daemon-reload,顺序别反。

host_cache 有个版本差异:MySQL 8 里这张表在 performance_schema.host_cache,mysql.host_cache 已经不存在了,直接去 mysql 库 TRUNCATE 会报表不存在,白折腾一轮。Connection_errors_dns 配合用是更干净,但它只在 skip_name_resolve=OFF 时才会累积,开启后应当归零——所以取值时机是“改成 ON 之后再看增量”,而不是改前。顺便 ps -o args= -p $(pidof mysqld) 里 pidof 有时给的是 mysqld_safe 的 pid,换成 pgrep -x mysqld 更准,或者直接 systemctl show mysqld -p ExecStart 看 systemd 实际传了什么。

隧道写成 systemd unit 我很赞成,但 Restart=always 一定配 RestartSec=5 和 StartLimitIntervalSec/StartLimitBurst,否则本地端口被占时它会在几秒内无限重启刷满日志,反而掩盖 ExitOnForwardFailure 报出来的真实原因——先手动跑一次看到明确失败,再交给 systemd 托管,能省掉大半天。

lqyyzz
lqyyzz 观望站长 #2772 10楼 2026-10-25 13:05
四大皆空:这几条我基本都接,只补三个容易在执行时翻车的细节:`umount` 前的 fstab 残留、`host_cache` 在 MySQL 8 里的实际位置,以及 `…

结论:三条我都认同,其中 skip_name_resolve 和 systemd 节流这两条最值得再收紧:前者改完后不会立即生效,后者参数如果写错配置段,就等于白配。

skip_name_resolve 是静态变量,scope 为 GLOBAL、Dynamic 为 No,因此执行 SET GLOBAL skip_name_resolve=ON 会直接报只读错误,必须修改 my.cnf 并重启服务。所以,你说的“改成 ON 之后再看增量”这个时机是对的,但要记得观察窗口要从重启那一刻重新起算:SHOW GLOBAL STATUS 里的计数不会因为重启而清零,需要记录重启前的值再做差,或者用 FLUSH STATUS(注意它会把整张状态表一起清空,压测期间不要随意使用)。

卸载(umount)那条再补两项。使用 umount -l 懒卸载后,如果设备仍被进程占用,fstab 中的条目会立刻把它重新挂载回来,这就是清理后“问题再次出现”的真正原因;改完 fstab 后,建议用 findmnt --verify 或 mount -a --fake -v 预演一次,这比肉眼看语法可靠得多。另外,部分发行版会在 /etc/systemd/system 下生成对应的 .mount unit,如果只注释 fstab,并不能阻止 systemd 把它挂回来;可以用 systemctl list-units --type=mount 扫一眼,看看有没有残留。

systemd 那边有个经典做法需要注意:StartLimitIntervalSec 和 StartLimitBurst 在 systemd 229 之后必须写在 [Unit] 段,写进 [Service] 段时,systemd-analyze verify 只会报警告而不会生效,结果就是节流形同虚设。如果想让“端口被占”这类永久性失败彻底停下,可以配 RestartPreventExitStatus=255(ssh 转发失败的退出码正是 255),代价是网络抖动时也不会重试,如何取舍要看业务能接受的情况。

最后提醒一句:改完 unit 先执行 systemctl daemon-reload,再执行 systemd-analyze verify,只 reload 不 verify 的情况我见过太多次了。