资讯动态

Shell脚本自动化部署Java项目:常用命令速查与排障实战

发布时间:2026/10/9 17:16:12 来源:尧图企业网站定制
2. 脚本常用命令速查搞不定的先查表下面这张表是我在日常脚本里使用频率最高的命令每条都标注了实际用途和踩坑提醒建议收藏。命令批次用途我的提醒set -euxo pipefail脚本头部统一开启严格模式-x会打印每条命令生产环境建议去掉否则日志太吵date %Y%m%d%H%M%S生成时间戳用于版本号和备份目录不要在 Windows 上跑date 语法不兼容git rev-parse --short HEAD取当前提交短哈希拼进包名先确认在 git 仓库内执行不然报错mvn -q clean package -DskipTests跳过测试快速打包-q静默模式能少刷一大半日志scp file userhost:/path推送构建产物到目标机注意目标路径的写权限否则报 Permission deniedssh userhost command远程执行命令双引号里的$会被本地 shell 吃掉需要转义rsync -avz --delete增量同步文件目录--delete慎用会删目标端多余文件ln -sfn new old软链接原子切换版本-n参数很关键避免链接套链接nohup java -jar app.jar app.log 21 后台启动 Java 应用一定要重定向日志否则 stdout 会把终端撑爆curl -s -o /dev/null -w %{http_code} http://localhost:8080/actuator/health健康检查应用要暴露健康检查端点才有意义4.4 启动脚本与健康检查启动成功不等于可用部署链路里最容易混过去的一步就是启动。很多人在脚本里写nohup java -jar xxx.jar 就觉得完事了结果应用 JVM 起来了端口没监听或者 Spring 容器还没初始化完成流量已经切过来了。这种假启动在生产环境是要出大事的。我的启动脚本里有一个固定单元分三步走# 4.1 启动应用 nohup java -Xms512m -Xmx1024m -jar $APP_HOME/$APP_NAME $LOG_DIR/app.log 21 echo $! $APP_HOME/app.pid # 4.2 等待端口就绪最多 60s for i in $(seq 1 30); do if ss -tln | grep -q :$SERVER_PORT; then echo 端口监听正常 break fi sleep 2 done # 4.3 等待健康检查通过最多 120s for i in $(seq 1 30); do HTTP_CODE$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:$SERVER_PORT$HEALTH_PATH) if [ $HTTP_CODE 200 ]; then echo 健康检查通过 exit 0 fi sleep 4 done echo 健康检查超时部署失败 exit 1端口检测用ss而不是netstat新系统都自带 ss性能更好。健康检查推荐用 Spring Boot Actuator 的/actuator/health端点返回 200 才代表应用真的可用。注意这里有个细节健康检查是返回 200 了但可能是 Liveness 探针而不是 Readiness 探针如果服务还没完全准备好对外提供服务建议检查 readiness 状态。我自己踩过一次坑健康检查配的是 LivenessSpring 容器还在初始化 Bean 时端口就监听了恰好返回 200脚本认为部署成功结果上游服务疯狂报错。后来改成 Readiness 的/actuator/health/readiness才彻底解决。5. 常见坑位与排障实录说真的Shell 自动化脚本写起来容易但真正考验功底的是排障。我在这里把实际踩过的高频坑整理出来分门别类讲清楚原因和解决方案这些几乎都是网上搜不到的实战经验。5.1 Maven 构建内存溢出不是你代码的问题mvn package的时候突然报java.lang.OutOfMemoryError: PermGen space或者GC overhead limit exceeded大多数人第一反应是查代码、调 JVM 参数但你得先搞清楚Maven 本身跑在 JVM 里Maven 进程的内存配置和你的应用 JVM 完全不相干。Maven 默认的堆内存太小构建大型项目多模块、大量依赖时很容易触顶。解决方案是在构建命令里显式指定 MAVEN_OPTSexport MAVEN_OPTS-Xms512m -Xmx2048m -XX:MaxMetaspaceSize512m mvn clean package -DskipTests建议把这段写在构建脚本的头部而不是每个执行构建的终端手动配。如果你用 Jenkins 这类 CI 工具也要在全局配置里加 MAVEN_OPTS否则每次构建都吃默认值。另外一个隐蔽但是极其常见的坑clean阶段会把target目录整个删掉如果构建过程中某个模块失败前面已经编译好的模块产物也被清了导致排查问题的时候找不到现场。我习惯在构建脚本里把target/classes目录做一次快照备份少走很多冤枉路。5.2 中文乱码问题一次发布事故的复盘有次我在服务器上部署了一个新版本启动之后业务方反馈中文全是乱码。我把锅甩给了运维运维把锅甩给了开发最后查下来问题出在打包机上的环境变量。Maven 编译时用的源码编码由project.build.sourceEncoding决定这属于 pome.xml 范畴但运行时 JVM 读取文件的编码由file.encoding决定而这个值受操作系统 locale 影响。我在构建脚本里加了一行export LANGen_US.UTF-8 export LC_ALLen_US.UTF-8这能保证file.encoding是 UTF-8但还有个更稳妥的做法在启动脚本的 Java 参数里显式加-Dfile.encodingUTF-8。这两个地方都堵住中文乱码的坑基本就杜绝了。还有个容易忽略的点如果你用 GitHub Actions 或者其他云构建平台构建容器的默认 locale 五花八门一定要在构建脚本里统一设置 locale不然本地没问题的代码一上 CI 就乱码。5.3set -e的副作用脚本突然诡异退出set -e是一个非常好的命令表示任何命令返回非零退出码就立即退出。但它有个隐患在多管道场景下比如mvn package | tee build.logtee的退出码是 0即使 mvn 失败了整条管道的退出码也可能是 0set -e就失效了脚本继续往下走部署了一个残缺的包。解决方案有两个# 方案一关闭管道退出码掩盖 set -o pipefail set -e # 方案二关键步骤不用管道用临时文件 mvn clean package build.log 21 BUILD_EXIT_CODE$? if [ $BUILD_EXIT_CODE -ne 0 ]; then echo 构建失败退出码 $BUILD_EXIT_CODE exit $BUILD_EXIT_CODE fi我实际生产环境用的是方案一加方案二混合全局开set -euo pipefail在极端重要的步骤构建、部署额外检查退出码。这样既不漏错又能精准定位出错位置。5.4 权限与安全脚本不要裸奔在服务器上部署脚本一般要执行rm、scp、ssh这类高权限命令很多人直接把整个脚本放在 root 下跑或者将私钥文件 644 权限裸放在目录里这都是非常危险的事。我建议三步脚本归属专用部署账号不要用 root。如果是测试环境图省事至少把rm -rf这类命令限定在明确的目录路径里绝对禁止变量未展开时执行rm -rf $APP_HOME/如果变量为空这条命令会变成rm -rf /血泪教训。SSH 私钥权限设为 600放在~/.ssh目录并加上 passphrase配合 ssh-agent 使用。脚本里的敏感信息数据库密码、API Key不要硬编码改用环境变量或外部配置文件同时将配置文件加入.gitignore。另外一个小细节把脚本中所有绝对路径统一用变量定义在脚本头部用readonly修饰防止中途被其他脚本或误操作改写。6. 进阶玩法多环境、多模块、通知机制到这里基础链路已经通了但真实项目往往没那么简单。你可能有 dev、test、prod 三套环境可能是多模块项目还可能有十几个人在等部署结果。接下来讲三个高频进阶需求。6.1 多环境切换的优雅做法一个脚本通吃不要在脚本里写死服务器 IP 和目录用环境参数控制#!/bin/bash ENV${1:-dev} case $ENV in dev) SERVER_HOST192.168.1.10 SERVER_USERdeploy APP_HOME/opt/app/dev ;; test) SERVER_HOST192.168.1.20 SERVER_USERdeploy APP_HOME/opt/app/test ;; prod) SERVER_HOSTyour-prod-host SERVER_USERdeploy APP_HOME/opt/app/prod ;; *) echo 未知环境: $ENV exit 1 ;; esac调用方式是./deploy.sh prod整个脚本的环境相关配置集中在一个 case 分支里清晰好维护。如果有条件不同环境的连接信息也可以放在独立配置文件中用source引入但我更喜欢上面这个方式少一层文件管理负担。6.2 多模块项目构建找准 Reactor 顺序Maven 多模块项目父 pom 加多个子模块构建时模块之间是有依赖顺序的依赖关系不对会直接编译失败。用mvn package会按 Reactor 自动排序一般没问题。但有一个细节容易踩如果只想构建某个子模块及其依赖需要加-pl和-am参数mvn clean package -pl gateway-service -am -DskipTests-pl指定要构建的模块列表-am表示同时构建该模块依赖的其他模块。实际场景中微服务项目往往只有一两个模块需要发布没必要全部重新编译能省几分钟构建时间。打包完以后每个子模块的 jar 在各自的target目录下收集时注意别漏for module in gateway-service user-service order-service; do find $module/target -name *.jar -newer $module/target/classes -exec cp {} dist/ \; done更稳妥的方式是直接用 Maven 的maven-assembly-plugin或者配置好finalName让产物输出到统一目录避免脚本里写死模块名。6.3 部署完成后的通知别让人盯着屏幕等发布脚本跑完如果大家只能蹲在终端等结果这个体验太原始了。建议加一步通知机制用最轻量的方案钉钉机器人 Webhook。NOTICE_URLhttps://oapi.dingtalk.com/robot/send?access_tokenYOUR_TOKEN curl -s $NOTICE_URL -H Content-Type: application/json -d { msgtype: text, text: { content: [$ENV] 部署完成$APP_NAME 版本 $VERSION构建耗时 $COST_TIMEs } }类似的也可以用企业微信、Slack、飞书原理都一样无非是换个接口和 JSON 格式。关键点是在脚本的关键节点都埋入通知开始构建、构建完成、部署中、部署成功、部署失败不要只在最后发一条。部署失败时的告警信息里带上日志文件路径和最近 20 行日志能省掉不少沟通成本。6.4 把脚本交给定时任务或 CI人机分离的最后一步脚本本身很好但如果每次部署还要人肉登录服务器执行那自动化的价值就少了一半。我最后会加一层把脚本挂到 Jenkins 或 GitLab CI当发布分支有 tag 推送时自动触发构建部署。你可以在本地触发模拟比如在 GitLab CI 中配置deploy-to-prod: stage: deploy script: - chmod x deploy.sh - ./deploy.sh prod only: - tags本地调试脚本时一定要记得在开发机上跑通全链路后再切入 CI否则 CI 环境没有你本地的私钥、环境变量脚本跑了半天才发现全部权限错误。这里我把最有价值的建议放在最后永远不要直接修改线上在生产跑的脚本。先复制一份带.test后缀的副本在测试环境验证确认无问题后再替换线上脚本。这是我从一次事故得到的教训线上脚本里手一抖改错一个路径变量部署直接切到了旧版本的软链接目录上20 分钟后才发现问题。7. 实操总结一套可以直接用的完整模板我把整套方案整合成两份脚本模板一份是主控脚本build_deploy.sh一份是远程目标机上的启动脚本startup.sh。生产环境我上线的版本比这个略复杂但核心骨架完全一致你可以在这上面按照自己的项目情况加功能。这两份脚本我建议直接复制保存遇到问题再参照本文的排障章节逐项排查。7.1 主控脚本模板本机执行#!/bin/bash # build_deploy.sh - Java 项目自动化编译打包部署脚本 set -euo pipefail # 配置区 ENV${1:-dev} GIT_URLgitgithub.com:yourorg/yourproject.git GIT_BRANCHmain MODULEScommon,service-api,service-impl,web APP_NAMEyour-app.jar DIST_DIRdist BACKUP_DIRbackup SERVER_HOST SERVER_USERdeploy SERVER_PATH/opt/app/${ENV}/releases APP_HOME/opt/app/${ENV}/current # 环境配置映射 case $ENV in dev) SERVER_HOST192.168.1.10 ;; test) SERVER_HOST192.168.1.20 ;; prod) SERVER_HOSTyour-prod-host ;; *) echo Unknown env: $ENV; exit 1 ;; esac # 准备工作 TIMESTAMP$(date %Y%m%d%H%M%S) VERSION${GIT_BRANCH}-${TIMESTAMP} WORK_DIRbuild_${TIMESTAMP} mkdir -p $WORK_DIR $DIST_DIR $BACKUP_DIR # 拉取代码 cd $WORK_DIR if [ -d .git ]; then git fetch origin else git clone $GIT_URL . fi git checkout $GIT_BRANCH git pull origin $GIT_BRANCH GIT_COMMIT$(git rev-parse --short HEAD) echo 代码版本: $GIT_BRANCH $GIT_COMMIT cd .. # 编译打包 export MAVEN_OPTS-Xms512m -Xmx2048m -XX:MaxMetaspaceSize512m mvn -f $WORK_DIR/pom.xml clean package -DskipTests | tee $BACKUP_DIR/build_${VERSION}.log BUILD_EXIT${PIPESTATUS[0]} if [ $BUILD_EXIT -ne 0 ]; then echo 构建失败退出码 $BUILD_EXIT请查看日志 $BACKUP_DIR/build_${VERSION}.log exit $BUILD_EXIT fi echo 构建成功开始收集产物 # 收集产物 # 按模块收集支持多模块项目 IFS, read -ra MODULE_ARRAY $MODULES for module in ${MODULE_ARRAY[]}; do jar_path$WORK_DIR/$module/target/*.jar # 排除 sources.jar 和 javadoc.jar cp $jar_path $DIST_DIR/ 2/dev/null || true done # 兼容单模块项目 if [ ! $(ls -A $DIST_DIR) ]; then cp $WORK_DIR/target/*.jar $DIST_DIR/ 2/dev/null || true fi # 将 app 包复制为标准的 APP_NAME find $DIST_DIR -name *.jar ! -name *-sources.jar ! -name *-javadoc.jar | head -1 | xargs -I{} cp {} $DIST_DIR/$APP_NAME # 推送产物 ssh ${SERVER_USER}${SERVER_HOST} mkdir -p ${SERVER_PATH}/${VERSION} scp $DIST_DIR/$APP_NAME ${SERVER_USER}${SERVER_HOST}:${SERVER_PATH}/${VERSION}/${APP_NAME} scp $WORK_DIR/startup.sh ${SERVER_USER}${SERVER_HOST}:${SERVER_PATH}/${VERSION}/startup.sh 2/dev/null || true echo 产物已推送到 ${SERVER_HOST}:${SERVER_PATH}/${VERSION} # 远程部署 ssh ${SERVER_USER}${SERVER_HOST} set -e # 备份当前版本 if [ -L ${APP_HOME} ]; then CURRENT_VERSION\$(readlink ${APP_HOME}) cp -a \$CURRENT_VERSION ${SERVER_PATH}/backup-${VERSION} || true fi # 切换软链接原子操作 ln -sfn ${SERVER_PATH}/${VERSION} ${APP_HOME} # 重启应用 if [ -f ${APP_HOME}/app.pid ]; then kill \$(cat ${APP_HOME}/app.pid) || true sleep 3 fi # 启动新版本 nohup java -jar -Xms512m -Xmx1024m ${APP_HOME}/${APP_NAME} ${APP_HOME}/app.log 21 echo \$! ${APP_HOME}/app.pid echo 部署脚本执行完成开始健康检查 # 健康检查 # 简单远程健康检查请按实际端口和路径修改 sleep 10 HTTP_CODE$(ssh ${SERVER_USER}${SERVER_HOST} curl -s -o /dev/null -w %{http_code} http://127.0.0.1:8080/actuator/health) if [ $HTTP_CODE 200 ]; then echo 健康检查通过部署成功 else echo 健康检查失败HTTP_CODE$HTTP_CODE echo 请登录服务器查看 ${APP_HOME}/app.log 排查问题 exit 1 fi7.2 目标机启动脚本模板startup.sh随产物一起推送#!/bin/bash # startup.sh - 目标机服务的启动/停止/重启控制脚本 set -euo pipefail APP_NAMEyour-app.jar APP_HOME/opt/app/${APP_NAME%.jar}/current LOG_DIR/opt/app/${APP_NAME%.jar} SERVER_PORT8080 HEALTH_PATH/actuator/health start() { echo 启动 ${APP_NAME}... nohup java -Xms512m -Xmx1024m -jar ${APP_HOME}/${APP_NAME} ${LOG_DIR}/app.log 21 echo $! ${APP_HOME}/app.pid echo PID: $(cat ${APP_HOME}/app.pid) } stop() { if [ -f ${APP_HOME}/app.pid ]; then PID$(cat ${APP_HOME}/app.pid) echo 停止进程 ${PID}... kill $PID || true rm -f ${APP_HOME}/app.pid else echo PID 文件不存在尝试用 pkill 停止 pkill -f ${APP_NAME} || true fi } status() { if [ -f ${APP_HOME}/app.pid ]; then PID$(cat ${APP_HOME}/app.pid) if ps -p $PID /dev/null; then echo 运行中 (PID: $PID) return 0 fi fi echo 未运行 return 1 } healthcheck() { for i in $(seq 1 30); do CODE$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:${SERVER_PORT}${HEALTH_PATH} || true) if [ $CODE 200 ]; then echo 健康检查通过 return 0 fi sleep 2 done echo 健康检查失败 return 1 } case ${1:-} in start) start ;; stop) stop ;; restart) stop sleep 2 start ;; status) status ;; health) healthcheck ;; *) echo 用法: $0 {start|stop|restart|status|health} exit 1 ;; esac7.3 脚本使用注意事项过来人的经验这套模板我用了很长一段时间整体稳定但有几个坑你得提前知道一是主控脚本中的IFS, read -ra拆分模块列表如果某个模块名称里包含空格会被意外拆分。实际问题里很少遇到但如果你在 Windows 上编辑脚本再传到 Linux 执行务必确认脚本是 LF 换行而不是 CRLF否则 bash 会报错$\r: command not found。用sed -i s/\r$// script.sh一键处理。二是scp $WORK_DIR/startup.sh这行如果你本地的 startup.sh 路径和远程不一致或没有这个文件scp 会直接报错中断整条链路。我在模板里已经加了|| true兜底同时建议你最好把 startup.sh 放在项目仓库里统一维护随代码一起走版本控制。三是健康检查只检查了一次 200 就认为部署成功。真实场景里建议在部署后连续检查 3 次间隔 2 秒并且把/actuator/health换成更严格的接口。如果你用的不是 Spring Boot请把健康检查地址改成你项目实际的接口。四是最重要的一点——这套流程看似把部署这件大事简化成了敲一条命令但每一步背后都是有事可查、有日志可追溯的。无论你把它接到 CI还是人肉执行都要保证出现任何一个非预期结果时你能快速找到日志和版本信息。这比脚本本身写得多优雅更重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑