背景
前几天看到一篇博文,是写关于自动化部署脚本的,而后被一句座右铭吸引了,“偷懒”是人类进步的第一推动力。想到自己也有同样的经历,也有些许时间没写博客了,于是乎,我将八月份写的两个脚本文件写进本篇博文。
想法
当初在做的时候,想的是如何在windows上将单体jar包从本地一键部署到线上centos环境,自己不用再做其它操作。包括:打包,上传jar,运行三个步骤。除了以上步骤,我还考虑了如何让用户无感知你在部署。于是我引入了备用端口,在nginx上通过配置两个端口负载以及故障迁移实现在同一台主机上滚动更新,达到用户在访问网页时无感知你的部署。
实现
以下操作需要使用到 plink 以及 pscp 工具,需要的可到putty下载安装
run.sh 启动脚本,也可以直接使用该脚本在centos环境完成部署 主要通过保存程序运行的pid、端口切换、端口被占用判断项目是否启动完成(刚开始的做法是通过统计日志启动成功标识,由于太过于繁杂去除了)实现,支持不负载实现,只需要将备用端口留空即可。步骤都有详细注释,这里就不再赘述。
#!/bin/sh
# 该脚本为Linux下启动java程序的脚本
# author: Cpz
# since: 2023-08-28
# 根据实际情况来修改以下配置信息 ##################################
# 主端口,默认
SERVER_PORT=8081
# 备份端口,为空时则不进行端口切换(有感知)
SERVER_PORT_BACKUP=8082
# jar存放路径
JAR_PATH='/usr/local/test'
# jar包名称,只填写前缀,后续根据后置获取最新jar包部署
JAR_NAME=test-server
# 启动jar包后进程编号存放文件
JAR_PID="${JAR_NAME}_${SERVER_PORT}.pid"
# 启动jar包后需要关闭的进程编号存放文件
kill_JAR_PID="${JAR_NAME}_${SERVER_PORT_BACKUP}.pid"
# 日志存放路径,info为正常日志,error为异常日志
LOG_FILE='logs'
LOG_INFO_FILE="${LOG_FILE}/info"
LOG_ERROR_FILE="${LOG_FILE}/error"
# java虚拟机启动参数
JAVA_OPTS="-Xss512k -Xms2048M -Xmx2048M -Xmn1024M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelInitialMarkEnabled -XX:+CMSScavengeBeforeRemark -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=utf-8 -Xloggc:$LOG_FILE/gc/gclog.log -XX:HeapDumpPath=$LOG_FILE/gc/HeapDump.hprof"
# 根据实际情况来修改以上配置信息 ###################################
# 判断日志文件夹是否存在,不存在则创建
if [ ! -d "$LOG_FILE" ]; then
mkdir "$LOG_FILE"
echo "Directory created: $LOG_FILE"
fi
# 检查程序是否处于运行状态:$1 为 pid 文件名称,先查询 pid 然后通过 pid 检查程序是否真正在运行中,返回 1 表示程序运行中,返回 0 表示程序未运行
is_exist() {
# 查询应用服务相应的pid
if [ -z "$1" ] ;then
pid=$(ps -ef|grep $JAR_NAME|grep -v grep|awk '{print $2}' )
elif [ ! -f "$1" ]; then
pid=""
else
pid=$(cat "$1")
fi
if [ -z "${pid}" ]; then
return 0
fi
PID_EXIST=$(ps aux | awk '{print $2}' | grep -w $pid)
if [ -z "${PID_EXIST}" ]; then
return 0
else
return 1
fi
}
# 服务启动方法
start() {
# 不存在备用端口号则直接停止服务,否则切换端口号和pid文件
if [ -z "$SERVER_PORT_BACKUP" ]; then
stop
else
# 通过端口号是否被占用实现
process=$(netstat -lnp|grep $SERVER_PORT)
if [ -z "$process" ]; then
kill_JAR_PID="${JAR_NAME}_${SERVER_PORT_BACKUP}.pid"
else
kill_JAR_PID="${JAR_NAME}_${SERVER_PORT}.pid"
SERVER_PORT=$SERVER_PORT_BACKUP
fi
JAR_PID="${JAR_NAME}_${SERVER_PORT}.pid"
fi
# 根据名称排序获取最大jar包名称
max_jar_name=$(ls -v | grep -E "$JAR_NAME.*.jar" | tail -n 1)
echo "current port $SERVER_PORT"
# 启动 jar 并将 pid 编号设置到相应文件中
nohup java $JAVA_OPTS -jar $JAR_PATH/"$max_jar_name" --server.port=$SERVER_PORT > ./$LOG_FILE/run.log 2>&1 &
echo $! > $JAR_PID
echo "start $JAR_NAME pid is $! "
sleep 2
# 启动检查:每秒检查端口是否被占用
while true; do
# 端口进程
process=$(netstat -lnp|grep $SERVER_PORT)
if [ -n "$process" ]; then
break;
fi
echo -n "-"
sleep 1
done
echo " 100% "
# 存在备用端口则关闭相应的进程,通过 pid 文件实现,结合 nginx 达到无感知
if [ -n "$SERVER_PORT_BACKUP" ]; then
stop $kill_JAR_PID
fi
}
# 服务停止方法
stop() {
# 判断服务进程是否存在
is_exist "$1"
# 存在则则关闭并循环检查是否已经关闭,超时:60s
if [ $? -eq "1" ]; then
echo "kill the old"
kill -15 "$pid"
for ((i=1;i<=60;i++)) ; do
is_exist "$1"
if [ $? -eq "1" ]; then
echo -n "-"
sleep 1
else
echo " 100% "
break
fi
# 重试 15s 后强制关闭
if [ "$i" -eq "60" ]; then
echo "force kill the old"
kill -9 $pid
fi
done
fi
}
# 服务运行状态查看方法
status() {
is_exist
if [ $? -eq "1" ]; then
echo "$JAR_NAME is running,pid is ${pid}"
else
echo "$JAR_NAME is not running!"
fi
}
# 重启服务方法
restart() {
# 调用服务停止命令
stop
# 调用服务启动命令
start
}
# 回滚为旧版本,通过删除最大名称jar实现
rollback() {
max_jar_name=$(ls -v | grep -E "$JAR_NAME.*.jar" | tail -n 1)
echo "delete $max_jar_name"
rm -rf "$max_jar_name"
start
}
### 查看日志
info(){
NEW_INFO_FILE_NAME=$(ls -t $LOG_INFO_FILE/ | grep .log | awk 'NR==1{print $1}')
tail -f $LOG_INFO_FILE/"$NEW_INFO_FILE_NAME" -n 100
}
error(){
NEW_ERROR_FILE_NAME=$(ls -t $LOG_ERROR_FILE/ | grep .log | awk 'NR==1{print $1}')
tail -f $LOG_ERROR_FILE/"$NEW_ERROR_FILE_NAME" -n 100
}
# 帮助说明,用于提示输入参数信息
usage() {
echo "usage: sh start.sh [ start | stop | restart | rollback | status | info | error ]"
exit 1
}
###################################
# 读取脚本的第一个参数($1),进行判断
# 参数取值范围:{ start | stop | restart | rollback | status | info | error }
# 如参数不在指定范围之内,则打印帮助信息
###################################
#根据输入参数,选择执行对应方法,不输入则执行使用说明
case "$1" in
'start')
start
;;
'stop')
stop
;;
'restart')
restart
;;
'rollback')
rollback
;;
'status')
status
;;
'info')
info
;;
'error')
error
;;
*)
usage
;;
esac
exit 0
step.sh 流程脚本,完成windows到centos环境的文件上传操作以及远程执行命令操作。步骤都有详细注释,这里就不再赘述。
### 环境配置
server_path=/usr/local/test
server_ip=xxx.xxx.xxx.xxx
server_user=xxxx
server_pw=xxxx
# 无需追加版本号,详见 upload fn
jar_name=test-server
# 检查代码是否处于 prod 环境
check(){
spring_profiles_active=$(grep "active" ./src/main/resources/application.yml)
if [[ ! $spring_profiles_active =~ "prod" ]]; then
echo ">>> spring.profiles.active doesn't contain prod"
exits
fi
}
# maven 打包操作
maven(){
echo ">>> package in directory[$(pwd)]"
mvn clean package -DskipTests=true
echo ">>> package completed!"
}
# 上传 jar 包到 centos 环境
upload(){
jar_name=$(ls | grep $jar_name | awk 'NR==1{print $1}')
if [ -e "$jar_name" ]
then
echo ">>> start upload ${jar_name} to remote[IP:${server_ip}][Path:${server_path}]"
pscp -P 22 -l ${server_user} -pw ${server_pw} "${jar_name}" ${server_user}@${server_ip}:${server_path}
echo ">>> uploaded!"
else
echo ">>> jar not found!"
fi
}
# 上传运行脚本并运行jar包
run(){
pscp -P 22 -l ${server_user} -pw ${server_pw} "run.sh" ${server_user}@${server_ip}:${server_path}
echo ">>> run ${jar_name}"
plink ${server_user}@${server_ip} -ssh -pw ${server_pw} <<< "cd ${server_path} && chmod 777 run.sh && ./run.sh start && exit"
echo ">>> run successfully!"
}
# 回滚到老版本
old(){
plink ${server_user}@${server_ip} -ssh -pw ${server_pw} <<< "cd ${server_path} && ./run.sh rollback && exit"
}
# 查看错误日志
error(){
plink ${server_user}@${server_ip} -ssh -pw ${server_pw} <<< "cd ${server_path} && ./run.sh error"
}
# 查看正常日志
info(){
plink ${server_user}@${server_ip} -ssh -pw ${server_pw} <<< "cd ${server_path} && ./run.sh info"
}
# 停止正在运行的jar
stop(){
plink ${server_user}@${server_ip} -ssh -pw ${server_pw} <<< "cd ${server_path} && ./run.sh stop && exit"
exits
}
# 创建部署目录,第一次部署时使用
directory(){
plink ${server_user}@${server_ip} -ssh -pw ${server_pw} <<< bash "
if [ ! -d ${server_path} ]; then
mkdir -p ${server_path}
echo "Directory created: ${server_path}"
fi
" bash
exit
}
# 退出运行命令框
exits(){
echo "press any key to exit"
read -n 1 -s -r -p ""
exit
}
#### 执行步骤,可以按需配置 ####
# 发布新版本
new(){
# check
cd ..
maven
cd "$jar_name/target" || exit
upload
cd ..
run
exits
}
# 回滚旧版本
old(){
rollback
exits
}
# directory 新建存放路径 | new 发布新版本 | old 回滚到旧版本 | error 错误日志 | info 常规日志 | stop 停止服务
#directory
new
#old
#stop
#error
#info
总结
如果你和我一样,想偷个懒,请尝试下这款自动部署化脚本。一次实现,终身受用。 如果存在不足或需要改进的地方,欢迎下方留言交流,不胜感激。
原创不易,请点赞,留言,关注,转载,感谢。
参考资料
- Linux 环境如何使用 kill 命令优雅停止 Java 服务
- SSH远程免密登录的两种方式 区别于本文通过用户名密码实现。