本笔记内容是 2018 年 ~ 2020 年的一个实际产物,如今有场景需要再次用到这里的部署方案,就找了下之前的脚本总结成了该文章 该脚本参考资料:

  1. 基础知识:鸟哥私房菜-基础版- 学习笔记
  2. 有了基础知识之后,按照流程逻辑,遇到不会的就需要去百度查询写法,比如 awk 如何提取文本信息成数组显示之类的

推荐一个 IDEA 的 shell 检查插件,Shell Script强烈推荐,idea 官方出品,里面有一个扩展 shellcheck,在你打开 sh 脚本的时候会提示你是否安装它,这个很有用,会告诉你一些写法可能是有问题的,然后可以根据提示(一般有问题的会有黄色的背景)你可以去看看为什么会有提示,要如何修改。这个功能还挺不错的,强烈推荐

该方案前提:

  1. jar 包运行机器和 jar 包打包的机器可以是同一个,也可以是不同的机器(不同机器使用 ssh 命令去执行命令,提前配置好 ssh 免密登录
  2. 需要在打包机器上安装 git、gradle 环境;因为需要使用命令来模拟人工打包部署的流程

功能描述:

  1. 使用 git pull 更新代码
  2. gradle 打包脚本
  3. 将打包好的 jar 包复制到指定位置
  4. 按 jar 包名称查找进程,并使用 kill -9 暴力杀掉进程
  5. 运行 jar 包

java 启动 jar 包命令

  1. nohup java -jar 微服务j.jar --eureka.instance.hostname=内网ip --spring.profiles.active=prod > ./catalina.out 2>&1 &
  2. # 服务注册中心需要额外指定一个参数
  3. --eureka.environment=eureka-client-prod
  4. # 如果有日志框架输出了日志文件,可以将后面控制台输出到 /dev/null 中
  5. nohup java -jar 微服务j.jar > /dev/null 2>&1 &

打包脚本 - v1 基础版

这里分两个操作脚本:打包、杀掉进程和运行
脚本单独存放,比如存放在 /data/packaging/script
build.sh 打包脚本

  1. #!/bin/sh
  2. # 存放 git 代码的分类仓库,有多个项目的话,会放到同一个 目录下
  3. BASE_DIR=/data/packaging/git-code
  4. # git 仓库目录名称,在 BASE_DIR 下的目录名称
  5. PROJECT=app1
  6. # 要部署的 jar 包目录
  7. APP_DIR=/data/service/app1
  8. # 远程服务器的 ip,或则实现配置免密登录的 host 名称
  9. IP=config
  10. echo -e "bootjar ${PROJECT}"
  11. cd $BASE_DIR/${PROJECT}
  12. git pull
  13. # -PbuildProfile=prod 是 gradle 的环境变量方式,没有使用到该方式的可以不用这个
  14. # gradle -PbuildProfile=prod bootJar
  15. gradle bootJar
  16. # 如果目标机器上的部署目录不存在,就创建
  17. # 这里的 IP 如果登录目标机器是指定账户,这可以使用 用户名@IP 的方式执行
  18. if ssh $IP test -e $APP_DIR;
  19. then echo $filePathexists
  20. else ssh $IP "mkdir ${APP_DIR}"
  21. fi
  22. # 将打包好的 jar 包复制到目标机器
  23. scp -r ./build/libs/* $IP:$APP_DIR

kill-run.sh 杀掉进程和启动 jar 包

  1. #!/bin/sh
  2. APP_DIR=/data/service/app1
  3. APP_JAR=app-1.0.0-boot.jar
  4. IP_ADDRESS=192.168.3.76
  5. ssh config "ps -ef | grep root | grep ${APP_JAR} | grep -v grep | cut -c 9-15 | xargs kill -s 9"
  6. ssh config "cd ${APP_DIR}; source /etc/profile; nohup java -jar ${APP_JAR} --eureka.instance.ip-address=${IP_ADDRESS} --spring.profiles.active=prod > /dev/null 2>&1 &"

打包脚本 - v2 版

v1 版本经过一段使用之后,发现了一些不太方便的场景:

  1. 当 jar 包版本号发生改变的时候打包发布变得困难,需要修改发布脚本,手动执行 kill 命令等操作
  2. 多开实例一个 boot.jar 往多个服务器推送的时候,变得困难,需要 copy 多次代码

v2 脚本重构目的:脚本顶部提取变量,其他部分全部一样。维护变得简单

build.sh

  1. #!/bin/sh
  2. BASE_DIR=/data/packaging/git-code
  3. PROJECT=app1
  4. APP_DIR=/data/service/$PROJECT
  5. # 推送目标机器,多个空格隔开
  6. PUSH_TARGET_HOST_NAMES="cloud-04"
  7. echo -e "${PROJECT} ..."
  8. cd $BASE_DIR/${PROJECT}
  9. git pull
  10. rm -rf ./build/libs/*
  11. gradle -PbuildProfile=prod bootJar
  12. # 其他脚本中统一使用一个 jar 名称,防止版本升级脚本改动
  13. mv ./build/libs/*-boot.jar ./build/libs/$PROJECT-boot.jar
  14. for hostName in $PUSH_TARGET_HOST_NAMES ; do
  15. if ssh $hostName test -e $APP_DIR;
  16. then echo $filePathexists
  17. else ssh $hostName "mkdir ${APP_DIR}"
  18. fi
  19. scp -r ./build/libs/*-boot.jar $hostName:$APP_DIR
  20. done

kill.sh

  1. #!/bin/sh
  2. APP_JAR=app-boot.jar
  3. TARGET_HOST_NAME=cloud-04
  4. ssh $TARGET_HOST_NAME "ps -ef | grep root | grep ${APP_JAR} | grep -v grep | cut -c 9-15 | xargs kill -s 9"

kill-run.sh

  1. #!/bin/sh
  2. APP_DIR=/data/service/app1
  3. APP_JAR=app1-boot.jar
  4. IP_ADDRESS=192.168.7.2
  5. TARGET_HOST_NAME=cloud-04
  6. ssh $TARGET_HOST_NAME "ps -ef | grep root | grep ${APP_JAR} | grep -v grep | cut -c 9-15 | xargs kill -s 9"
  7. ssh $TARGET_HOST_NAME "cd ${APP_DIR}; source /etc/profile; nohup java -jar ${APP_JAR} --eureka.instance.ip-address=${IP_ADDRESS} --spring.profiles.active=prod > /dev/null 2>&1 &"

如果有多个实例,kill 与 kill-run 脚本是分开写的。为什么呢?因为往往可能只是想重启其中一台的,而且这种几率非常大

打包脚本 - v3 交互版

该脚本比较复杂,先来看看交互界面,然后再看脚本
image.png
start.sh 交互脚本核心

  1. #!/bin/sh
  2. # Program:
  3. # cloud 微服务统一部署,提供打包推送、kill、run、检查项目是否运行等功能
  4. # History:
  5. # 2020/02/18 mrcode first relese
  6. # 2020/02/28 mrcode 修复多机器打包机器选择逻辑错误,函数内部变量跨域等问题
  7. SH_VERSION="v0.4"
  8. # 打包项目并推送到目标机器
  9. function f_pack() {
  10. local _uiName=$uiName
  11. local _BASE_DIR=$BASE_DIR
  12. local _PROJECT=$PROJECT
  13. local _APP_DIR=$APP_DIR
  14. local _PUSH_TARGET_HOST_NAMES=$PUSH_TARGET_HOST_NAMES
  15. echo -e "\n开始部署项目 ${_PROJECT} ..."
  16. cd ${_BASE_DIR}/${_PROJECT}
  17. git pull
  18. rm -rf ./build/libs/*
  19. gradle -PbuildProfile=prod bootJar
  20. # 其他脚本中统一使用一个 jar 名称,防止版本升级脚本改动
  21. mv ./build/libs/*-boot.jar ./build/libs/${_PROJECT}-boot.jar
  22. for hostName in ${_PUSH_TARGET_HOST_NAMES} ; do
  23. if ssh $hostName test -e ${_APP_DIR}; then
  24. echo $filePathexists
  25. else
  26. ssh $hostName "mkdir ${_APP_DIR}"
  27. fi
  28. scp -r ./build/libs/*-boot.jar $hostName:${_APP_DIR}
  29. done
  30. }
  31. function f_checkIsRun() {
  32. local checkIsRunInfo=$(ssh $TARGET_HOST_NAME "ps -ef | grep root | grep $1")
  33. if [ "$checkIsRunInfo" == "" ]; then
  34. echo "未运行"
  35. else
  36. echo $checkIsRunInfo
  37. fi
  38. }
  39. # 打印 host 待选菜单,并完成选择
  40. function f_selectHostName() {
  41. local index=0
  42. echo -e "\n当前项目有如下多个实例机器需要部署:"
  43. for i in ${hostNames[*]} ; do
  44. index=$((index+1))
  45. echo "$index. $i"
  46. done
  47. echo -e "\n"
  48. read -p "请选择要操作的机器:" hostNameN
  49. TARGET_HOST_NAME=${hostNames[$(($hostNameN-1))]}
  50. echo -e "当前选择操作项目为 $PROJECT $TARGET_HOST_NAME \n"
  51. }
  52. # 检测是否有多台机器
  53. function f_checkSelectHostName() {
  54. local hostNames=($PUSH_TARGET_HOST_NAMES)
  55. if [ "${#hostNames[@]}" -gt 1 ]; then
  56. # 选择哪一台机器
  57. f_selectHostName
  58. fi
  59. }
  60. # kill 掉项目
  61. function f_kill() {
  62. ssh $TARGET_HOST_NAME "ps -ef | grep root | grep ${APP_JAR} | grep -v grep | cut -c 9-15 | xargs kill -s 9"
  63. }
  64. # run 项目
  65. function f_run() {
  66. ssh $TARGET_HOST_NAME "cd ${APP_DIR}; source /etc/profile; nohup java -jar ${APP_JAR} --eureka.instance.ip-address=${IP_ADDRESS} --spring.profiles.active=prod ${LAUNCH_PARAMS} > /dev/null 2>&1 &"
  67. }
  68. # 匹配 IP 地址
  69. function f_matchIp() {
  70. IP_ADDRESS=$(grep ${TARGET_HOST_NAME} ./ip | awk '{printf $2}')
  71. }
  72. echo -e "\n==================== 微服务统一部署脚本 ${SH_VERSION} ==================== \n"
  73. #cat ./projectData | grep -v '^#' | awk -F '\t' '{print NR ". " $1}'
  74. # 一行两列格式化对齐输出
  75. cat ./projectData | grep -v '^#' | awk -F '\t' 'NR%2==1 {printf ("%s. %-20s \t\t",NR,$1)} NR%2==0 {print NR ". " $1}'
  76. echo -e "\n"
  77. read -p "请选择项目编号(输入 0 退出):" projectDataN
  78. if [ "$projectDataN" == 0 ]; then
  79. exit 1
  80. fi
  81. uiName=''
  82. BASE_DIR=''
  83. PROJECT=''
  84. APP_DIR=''
  85. PUSH_TARGET_HOST_NAMES=''
  86. LAUNCH_PARAMS='' # 额外启动参数
  87. # 获取到该项目一行的配置数据
  88. eval $(cat ./projectData | grep -v '^#' | sed -n "${projectDataN}p" | awk -F '\t' '{ printf("uiName=%s;BASE_DIR=%s;PROJECT=%s;APP_DIR=%s;PUSH_TARGET_HOST_NAMES=%s;LAUNCH_PARAMS=%s",$1,$2,$3,$4,$5,$6)}')
  89. echo -e "\n当前操作项目为:${uiName}\n"
  90. echo "1. 打包并推送项目"
  91. echo "2. kill 项目"
  92. echo "3. run 项目"
  93. echo "4. kill & run 项目"
  94. echo "5. 检测项目是否运行"
  95. echo -e "\n"
  96. read -p "请选择:" optNu
  97. APP_JAR="${PROJECT}-boot.jar"
  98. TARGET_HOST_NAME=${PUSH_TARGET_HOST_NAMES}
  99. IP_ADDRESS='' # ip 地址
  100. case $optNu in
  101. 1)
  102. echo "已选择:打包并推送项目"
  103. f_pack
  104. ;;
  105. 2)
  106. echo "已选择:kill 项目"
  107. f_checkSelectHostName
  108. f_matchIp
  109. f_kill
  110. ;;
  111. 3)
  112. echo "已选择:run 项目"
  113. f_checkSelectHostName
  114. f_matchIp
  115. f_run
  116. ;;
  117. 4)
  118. echo "已选择:kill & run 项目"
  119. f_checkSelectHostName
  120. f_matchIp
  121. f_kill
  122. f_run
  123. ;;
  124. 5)
  125. echo "已选择:检测项目是否运行"
  126. f_checkSelectHostName
  127. f_matchIp
  128. f_checkIsRun $APP_JAR
  129. ;;
  130. *)
  131. echo "选择错误,已退出"
  132. esac
  133. # function_print $uiName $BASE_DIR $PROJECT $APP_DIR $PUSH_TARGET_HOST_NAMES

projectData 微服务信息文件

  1. # ui界面显示名称 BASE_DIR源(码目录) PROJECT(项目名称) APP_DIR(部署目录) PUSH_TARGET_HOST_NAMES(要部署的机器名称,多个用逗号隔开) LAUNCH_PARAMS(其他启动参数)
  2. # 建议多实例相同启动参数,直接使用多 PUSH_TARGET_HOST_NAMES ,而不同启动参数,写多行
  3. "mrcode-cloud-sam" "/data/packaging/git-code" "mrcode-cloud-sam" "/mnt/service/$PROJECT" "cloud-05" ""
  4. "mrcode-cloud-sam-agent-1" "/data/packaging/git-code" "mrcode-cloud-sam-agent" "/mnt/service/$PROJECT" "cloud-05" "--spring.cloud.stream.instance-index=0"
  5. "mrcode-cloud-sam-agent-2" "/data/packaging/git-code" "mrcode-cloud-sam-agent" "/mnt/service/$PROJECT" "cloud-04" "--spring.cloud.stream.instance-index=1"
  6. "mrcode-cloud-sem" "/data/packaging/git-code" "mrcode-cloud-sem" "/mnt/service/$PROJECT" "cloud-04" ""
  7. "mrcode-cloud-sem-google" "/data/packaging/git-code" "mrcode-cloud-sem-google" "/mnt/service/$PROJECT" "cloud-05" ""
  8. "mrcode-cloud-sem-facebook" "/data/packaging/git-code" "mrcode-cloud-sem-facebook" "/mnt/service/$PROJECT" "cloud-05" ""
  9. "mrcode-cloud-product" "/data/packaging/git-code" "mrcode-cloud-product" "/mnt/service/$PROJECT" "cloud-05" ""
  10. "mrcode-cloud-feed" "/data/packaging/git-code" "mrcode-cloud-feed" "/mnt/service/$PROJECT" "cloud-05" ""
  11. "mrcode-cloud-site-group" "/data/packaging/git-code" "mrcode-cloud-site-group" "/mnt/service/$PROJECT" "cloud-04" ""
  12. "mrcode-cloud-life-cycle" "/data/packaging/git-code" "mrcode-cloud-life-cycle" "/mnt/service/$PROJECT" "cloud-04 cloud-05" ""
  13. "mrcode-cloud-notice" "/data/packaging/git-code" "mrcode-cloud-notice" "/mnt/service/$PROJECT" "cloud-05" ""
  14. "mrcode-cloud-gateway" "/data/packaging/git-code" "mrcode-cloud-gateway" "/mnt/service/$PROJECT" "cloud-06" ""

ip host 与 ip 映射, 这个其实不太需要,主要是为了传递内网 IP 给 --eureka.instance.ip-address参数

  1. cloud-02 192.168.7.70
  2. cloud-03 192.168.7.71
  3. cloud-04 192.168.7.72
  4. cloud-05 192.168.7.2
  5. cloud-06 192.168.7.73
  6. cloud-07 192.168.7.74