从物理机到 CVM 全流程落地:部署脚本的 8 个云化改造点(附代码对比)
分类:云计算 / 运维
标签:腾讯云、CVM、Shell、上云迁移、systemd、自动化部署
摘要:上云不是把脚本里的 IP 换一换。本文把物理机部署脚本迁移到 CVM 时的改造点拆成 8 类,每类给出改造前/后代码对比、踩坑记录和验收方法,可作为迁移 checklist 直接使用。
前言:为什么"改改 IP"一定会翻车
物理机时代,一台机器一个角色,脚本怎么写都能跑。上云之后,环境变成可复制、可扩缩、可重建的,脚本里所有"隐式依赖本机状态"的假设都会失效。
我把它归纳成 8 类改造点。每一类都是真实踩过的坑,不是理论推演。
改造点 1:主机标识 ------ 从硬编码到标签化
改造前
bash
# 靠主机名和 IP 段判断环境
hostname=$(hostname) # 例如 node-01.prod.bj
if [[ "$hostname" == *".prod."* ]]; then
ENV=prod
else
ENV=test
fi
DB_HOST=10.10.20.31 # 硬编码
改造后
bash
# 从实例元数据 + 标签获取,不依赖命名规范
get_instance_meta() {
local key="$1"
curl -s -m 3 "http://metadata.tencentyun.com/latest/meta-data/${key}"
}
INSTANCE_ID=$(get_instance_meta instance-id) # ins-xxxxxxxx
REGION=$(get_instance_meta placement/region)
ZONE=$(get_instance_meta placement/zone)
# 环境从标签读取(由基础设施层保证)
ENV_TAG=$(tccli cvm DescribeInstances \
--InstanceIds "[\"${INSTANCE_ID}\"]" \
--query 'InstanceSet[0].Tags[?Key==`env`].Value | [0]' -o text)
if [ -z "${ENV_TAG}" ] || [ "${ENV_TAG}" = "None" ]; then
echo "FATAL: 实例 ${INSTANCE_ID} 缺少 env 标签,拒绝部署"
exit 1
fi
ENV="${ENV_TAG}"
要点:
- 用元数据服务而不是
hostname。CVM 重建后主机名会变,元数据里的 instance-id 才是稳定标识。 - 环境未知时直接退出,不要有"默认值兜底"。默认值是把测试流量打到生产的经典路径。
腾讯云元数据服务地址是
http://metadata.tencentyun.com/latest/meta-data/,只在本机可访问,天然安全。
改造点 2:制品分发 ------ 从本地目录到对象存储
改造前
bash
# 假设包已经在本地,靠运维 scp
tar -xzf /data/pkg/app.tar.gz -C /data/app
改造后
bash
# COS 拉取 + MD5 校验 + 本地缓存
fetch_pkg() {
local pkg="$1" version="$2"
local cos_key="releases/${ENV}/${pkg}-${version}.tar.gz"
local cache="/var/cache/deploy/${pkg}-${version}.tar.gz"
if [ -f "${cache}" ] && md5sum -c "${cache}.md5" &>/dev/null; then
log "命中本地缓存,跳过下载"
else
coscmd download -f "cos://${BUCKET}/${cos_key}" "${cache}"
coscmd download -f "cos://${BUCKET}/${cos_key}.md5" "${cache}.md5"
(cd "$(dirname "${cache}")" && md5sum -c "$(basename "${cache}").md5") \
|| { echo "FATAL: 制品校验失败"; rm -f "${cache}"; exit 1; }
fi
echo "${cache}"
}
踩坑记录 :一定要校验 MD5,并且校验失败要删除坏文件。我们遇到过 COS 下载中断产生半截文件,脚本解包出一个"能启动但功能残缺"的应用,比直接失败还难排查。
改造点 3:依赖管理 ------ 从"机器上装好了"到显式声明
物理机上依赖是历史遗留的:有人装过 JDK 8,有人装过 11,脚本不管。上云后机器是新建的,必须显式检查:
bash
REQUIRED_CMDS=(java coscmd systemctl curl mysql)
REQUIRED_JAVA_MAJOR=8
check_deps() {
local missing=()
for c in "${REQUIRED_CMDS[@]}"; do
command -v "$c" >/dev/null 2>&1 || missing+=("$c")
done
if [ ${#missing[@]} -gt 0 ]; then
echo "FATAL: 缺少依赖: ${missing[*]}"
echo "提示:请在 CVM 初始化脚本(UserData)中安装,而非在部署脚本中临时 yum install"
exit 1
fi
# 校验 Java 大版本,避免 JDK 版本漂移导致启动失败
local java_ver
java_ver=$(java -version 2>&1 | head -n1 | grep -oP '(?<=version ")[0-9]+')
if [ "${java_ver}" != "${REQUIRED_JAVA_MAJOR}" ]; then
echo "FATAL: Java 版本不匹配,期望 ${REQUIRED_JAVA_MAJOR},实际 ${java_ver}"
exit 1
fi
echo "依赖检查通过"
}
原则 :部署脚本只校验、不安装 。安装属于镜像/UserData 职责。让部署脚本去 yum install 会导致:部署变慢、行为不确定、依赖网络、每次发布都在改系统。
改造点 4:进程管理 ------ 从 nohup 到 systemd
改造前
bash
kill -9 $(cat app.pid)
nohup java -jar app.jar > /dev/null 2>&1 &
echo $! > app.pid
问题:不等待退出、日志丢弃、崩溃不重启、开机不自启、停服务靠 kill 运气。
改造后(systemd unit 模板)
ini
[Unit]
Description=biz-web (${ENV})
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=admin
Group=admin
WorkingDirectory=/data/app/biz-web/current
Environment="JAVA_OPTS=-Xms2g -Xmx2g -XX:+UseG1GC"
EnvironmentFile=-/data/app/biz-web/shared/env
ExecStart=/usr/bin/java $JAVA_OPTS -jar /data/app/biz-web/current/biz-web.jar --spring.profiles.active=${ENV}
SuccessExitStatus=143
TimeoutStopSec=60
KillMode=mixed
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
StandardOutput=append:/data/app/biz-web/shared/logs/stdout.log
StandardError=append:/data/app/biz-web/shared/logs/stderr.log
[Install]
WantedBy=multi-user.target
关键字段解释:
| 字段 | 作用 | 为什么必须有 |
|---|---|---|
SuccessExitStatus=143 |
SIGTERM 退出视为正常 | 否则 systemctl stop 后状态是 failed |
TimeoutStopSec=60 |
优雅停机超时 | 配合应用的 graceful shutdown |
KillMode=mixed |
先 TERM 主进程 | 默认 control-group 会杀子进程,可能丢数据 |
LimitNOFILE |
文件句柄上限 | 默认 1024 在高并发下会 OOM 式报错 |
EnvironmentFile |
外部化配置 | 环境相关参数不进 unit,避免 diff 噪音 |
Restart=on-failure |
崩溃自愈 | 云上无人值守的基本盘 |
配置外部化示例(shared/env,由部署脚本按环境生成,不随版本包走):
bash
export DB_HOST=biz-db.prod.internal
export DB_NAME=biz
export DB_USER=biz_app
export REDIS_HOST=biz-redis.prod.internal
改造点 5:网络与安全组 ------ 从"内网互通"到显式放行
物理机内网通常全通,CVM 默认拒绝。踩过的坑:应用起来了、进程活着、端口在听,但调用方连不上。
bash
verify_connectivity() {
# 1. 本机监听
ss -lntp | grep -q ":${PORT} " || { echo "FATAL: 未监听 ${PORT}"; exit 1; }
# 2. 从本机走内网 IP 访问(验证绑定地址不是 127.0.0.1 only)
curl -s -m 5 "http://$(hostname -I | awk '{print $1}'):${PORT}/actuator/health" \
|| { echo "WARN: 未绑定到内网 IP,可能只监听 127.0.0.1"; }
# 3. 从目标调用方验证(下游依赖,独立检查)
for dep in "${DOWNSTREAM_DB}" "${DOWNSTREAM_REDIS}"; do
timeout 3 bash -c "echo > /dev/tcp/${dep%:*}/${dep#*:}" 2>/dev/null \
&& echo " [PASS] 可达 ${dep}" || echo " [FAIL] 不可达 ${dep}"
done
}
验收标准 :部署脚本必须能验证"从调用方视角可达 ",而不是只看本机 ss。上云后 90% 的"部署成功但业务不可用"都是安全组/子网策略问题。
改造点 6:日志 ------ 从文件到可采集
物理机时代日志在 /data/logs,上云后如果不改,接不上 CLS,排障全靠 ssh。
bash
setup_log_rotation() {
cat > /etc/logrotate.d/biz-web <<'EOF'
/data/app/biz-web/shared/logs/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
}
EOF
# 接入 CLS 采集(机器组由标签自动匹配)
if [ ! -f /etc/rsyslog.d/99-cls.conf ]; then
cat > /etc/rsyslog.d/99-cls.conf <<'EOF'
module(load="imfile")
template(name="cls" type="string"
string="%timegenerated% %hostname% %msg%\n")
input(type="imfile" File="/data/app/biz-web/shared/logs/*.log"
Tag="biz-web" Ruleset="clsRuleset" addmetadata="on")
ruleset(name="clsRuleset") { action(type="omfwd" Target="cls-proxy.internal" Port="514") }
EOF
fi
}
注意 copytruncate :应用以 append 模式持有日志文件句柄,用默认的 create 方式轮转会导致应用继续往已重命名的文件写。这个坑很隐蔽------日志"看起来在轮转",但新文件永远是空的。
改造点 7:时间与时区 ------ 从"机器对就行"到强制统一
分布式系统里时间不一致会导致:日志时序错乱、JWT 校验失败、订单时间倒挂。CVM 默认 UTC,物理机可能是 CST。
bash
ensure_time_sync() {
timedatectl set-timezone Asia/Shanghai
timedatectl set-ntp true
local offset
offset=$(chronyc tracking 2>/dev/null | awk '/System time/{print $4}')
echo "时钟偏移: ${offset:-unknown}s"
# 应用侧也要确认
# systemd unit 里显式设置:Environment="TZ=Asia/Shanghai"
# JVM 侧显式设置:-Duser.timezone=Asia/Shanghai
}
双保险 :系统时区 + -Duser.timezone。只用系统时区,容器化后会被覆盖;只用 JVM 参数,日志文件时间戳仍然错。
改造点 8:幂等与回滚 ------ 从"人工重试"到"可重复执行"
幂等的三条铁律
bash
# 铁律 1:写文件前判断内容,避免无意义的 daemon-reload
write_if_changed() {
local target="$1" content="$2"
local tmp; tmp=$(mktemp)
echo "${content}" > "${tmp}"
if [ -f "${target}" ] && diff -q "${tmp}" "${target}" &>/dev/null; then
rm -f "${tmp}"; return 0 # 无变化
fi
mv "${tmp}" "${target}"; return 1 # 有变化
}
# 铁律 2:先建后切,切换只做软链
ln -sfn "${RELEASE_PATH}" "${CURRENT_LINK}"
# 铁律 3:所有 sed/append 操作都要能重复执行
# 反例:echo "export X=1" >> /etc/profile (执行 3 次就有 3 行)
# 正例:sed -i '/^export X=/d' /etc/profile && echo "export X=1" >> /etc/profile
回滚:软链 + 健康校验自动切回
bash
rollback() {
local target_version="$1"
local target="${RELEASE_DIR}/${target_version}"
[ -d "${target}" ] || { echo "FATAL: 版本 ${target_version} 不存在,无法回滚"; exit 1; }
echo ">>> 回滚到 ${target_version}"
ln -sfn "${target}" "${CURRENT_LINK}"
systemctl restart "${APP_NAME}"
bash health_check.sh -e "${ENV}" -w 60 \
&& echo "回滚成功" \
|| { echo "FATAL: 回滚后健康检查仍失败,需人工介入"; exit 1; }
}
回滚版本的可得性 是前提。所以部署脚本必须保留最近 N 个 release 目录(KEEP_RELEASES=3),否则回滚就是空谈。
汇总 checklist
部署脚本上线前,逐条打勾:
| # | 改造点 | 验收方法 |
|---|---|---|
| 1 | 主机标识标签化 | 删除主机名,脚本仍能正确识别环境 |
| 2 | 制品 COS + MD5 | 手工破坏一次包,脚本必须报错退出 |
| 3 | 依赖显式校验 | 卸载 java 后执行,必须给出明确报错 |
| 4 | systemd 管理进程 | systemctl stop 后状态为 inactive 而非 failed |
| 5 | 连通性从调用方验证 | 关闭安全组规则,脚本必须报告不可达 |
| 6 | 日志可采集 | CLS 控制台能看到新写入日志 |
| 7 | 时区统一 | date 与 JVM 日志时间一致,且为 CST |
| 8 | 幂等 + 可回滚 | 连续执行 3 次无异常;手工回滚一次成功 |
最后一条最重要的验收方式:把脚本交给一个没参与开发的同事,让他在一台全新 CVM 上执行。他卡在哪一步,就是脚本的问题,不是他的问题。
结语
上云迁移中,真正花时间的从来不是"把 IP 改掉",而是这 8 类隐式假设的显式化。每一条都对应一个生产环境里的深夜。
如果你正准备做迁移,建议顺序是:先做 1、2、4(主机标识、制品、进程管理),这三条做完,脚本才算"能在云上跑";再做 3、5、6、7(依赖、网络、日志、时间),这些决定"跑得稳不稳";最后补 8(幂等回滚),决定"出事了救不救得回来"。
相关阅读:《传统物理机业务上云:腾讯云助手改写 Shell 脚本,一键生成 CVM 部署与业务校验脚本》
下一篇:《上云迁移验收:CVM 健康校验用例 + 数据库迁移比对报告》