从物理机到 CVM 全流程落地:部署脚本的 8 个云化改造点(附代码对比)

从物理机到 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 健康校验用例 + 数据库迁移比对报告》

相关推荐
棣廷2 小时前
初识OpenCV——人脸检测与识别实战
人工智能·opencv·计算机视觉
byte轻骑兵2 小时前
【BlueZ 】util 模块:通用工具函数,源码中高频复用的基础组件
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
lhldsg2 小时前
多商户团购系统源码:技术架构选型与二次开发实战指南
java·小程序·架构
Cicada1282 小时前
分享我一直在用的一套 AI 记忆系统方案
大数据·数据库·人工智能
桃西西呀2 小时前
买房怕买贵?我把真实成交价丢进决策树,看它到底按什么给房子定价
人工智能·机器学习·llm
脉动数据行情12 小时前
Java SpringBoot 国际期货批量采集实践 美原油 / 黄金 / 指数期货定时落库
java·开发语言·spring boot
jimmyleeee2 小时前
大模型安全之十:LLM无界消耗(Unbounded Consumption)
人工智能·安全
晚安日记wanna2 小时前
分布式事务 6 连问从 Seata AT 到本地消息表落地
面试·架构
Spider Cat 蜘蛛猫2 小时前
微服务- 自动审核商家入驻
微服务·云原生·架构