• 编写一个可复用的SpringBoot应用运维脚本


    前提

    作为Java开发者,很多场景下会使用SpringBoot开发Web应用,目前微服务主流SpringCloud全家桶也是基于SpringBoot搭建的。SpringBoot应用部署到服务器上,需要编写运维管理脚本。本文尝试基于经验,总结之前生产使用的Shell脚本,编写一个可以复用的SpringBoot应用运维脚本,从而极大减轻SpringBoot应用启动、状态、重启等管理的工作量。本文的Shell脚本在CentOS7中正常运行,其他操作系统不一定适合。如果对一些基础或者原理不感兴趣可以拖到最后,直接拷贝脚本使用。

    依赖到的Shell相关的知识

    编写SpringBoot应用运维脚本除了基本的Shell语法要相对熟练之外,还需要解决两个比较重要的问题(笔者个人认为):

    • 正确获取目标应用程序的进程ID,也就是获取Process ID(下面称PID)的问题。
    • kill命令的正确使用姿势。
    • 命令nohup的正确使用方式。

    获取PID

    一般而言,如果通过应用名称能够成功获取PID,则可以确定应用进程正在运行,否则应用进程不处于运行状态。应用进程的运行状态是基于PID判断的,因此在应用进程管理脚本中会多次调用获取PID的命令。通常情况下会使用grep命令去查找PID,例如下面的命令是查询Redis服务的PID

    ps -ef |grep redis |grep -v grep |awk '{print $2}'
    

    其实这是一个复合命令,每个|后面都是一个完整独立的命令,其中:

    • ps -efps命令加上-ef参数,ps命令主要用于查看进程的相关状态,-e代表显示所有进程,而-f代表完整输出显示进程之间的父子关系,例如下面是笔者的虚拟机中的CentOS 7执行ps -ef后的结果:

    • grep XXX其实就是grep对应的目标参数,用于搜索目标参数的结果,复合命令中会从前一个命令的结果中进行搜索。
    • grep -v grep就是grep命令执行时候忽略grep自身的进程。
    • awk '{print $2}'就是对处理的结果取出第二列。

    ps -ef |grep redis |grep -v grep |awk '{print $2}'复合命令执行过程就是:

    • <1>通过ps -ef获取系统进程状态。
    • <2>通过grep redis<1>中的结果搜索redis关键字,得出redis进程信息。
    • <3>通过grep -v grep<2>中的结果过滤掉grep自身的进程。
    • <4>通过awk '{print $2}'<3>中的结果获取第二列。

    Shell脚本中,可以使用这种方式获取PID

    PID=`ps -ef |grep redis-server |grep -v grep |awk '{print $2}'`
    echo $PID
    

    但是这样会存在一个问题,就是每次想获取PID都必须使用这串非常长的命令,显得有些笨拙。可以使用eval简化这个过程:

    PID_CMD="ps -ef |grep docker |grep -v grep |awk '{print $2}'"
    PID=$(eval $PID_CMD)
    echo $PID
    

    获取PID的问题解决,然后可以基于PID是否存在,决定一下步怎么操作。

    理解kill命令

    kill命令的一般形式是kill -N PID,本质功能是向对应PID的进程发送一个信号,然后对应的进程需要对这个信号作出响应,信号的编号就是N,这个N的可选值如下(系统是CentOS 7):

    其中开发者常见的就是9) SIGKILL15) SIGTERM,它们的一般描述如下:

    信号编号 信号名称 描述 功能 影响
    15 SIGTERM Termination (ANSI) 系统向对应的进程发送一个SIGTERM信号 进程立即停止,或者释放资源后停止,或者由于等待IO继续处于运行状态,也就是一般会有一个阻塞过程,或者换一个角度来说就是进程可以阻塞、处理或者忽略SIGTERM信号
    9 SIGKILL Kill(can't be caught or ignored) (POSIX) 系统向对应的进程发送一个SIGKILL信号 SIGKILL信号不能被忽略,一般表现为进程立即停止(当然也有额外的情况)

    不带-N参数的kill命令默认就是kill -15。一般而言,kill -9 PID是进程的必杀手段,但是它很有可能影响进程结束前释放资源的过程或者中止I/O操作造成数据异常丢失等问题。

    nohup命令

    如果希望在退出账号或者关闭终端后应用进程不退出,可以使用nohup命令运行对应的进程。

    nohup就是no hang up的缩写,翻译过来就是"不挂起"的意思,nohup的作用就是不挂起地运行命令。

    nohup命令的格式是:nohup Command [Arg...] [&],功能是:基于命令Command和可选的附加参数Arg运行命令,忽略所有kill命令中的挂断信号SIGHUP&符号表示命令需要在后台运行。

    这里注意一点,操作系统中有三种常用的标准流:
    0:标准输入流STDIN
    1:标准输出流STDOUT
    2:标准错误流STDERR

    直接运行nohup Command &的话,所有的标准输出流和错误输出流都会输出到当前目录nohup.out文件,时间长了有可能导致占用大量磁盘空间,所以一般需要把标准输出流STDOUT和标准错误流STDERR重定向到其他文件,例如nohup Command 1>server.log 2>server.log &。但是由于标准错误流STDERR没有缓冲区,所以这样做会导致server.log会被打开两次,导致标准输出和错误输出的内容会相互竞争和覆盖,因此一般会把标准错误流STDERR重定向到已经打开的标准输出流STDOUT中,也就是经常见到的2>&1,而标准输出流STDOUT可以省略>前面的1,所以:

    nohup Command 1>server.log 2>server.log &修改为nohup Command >server.log 2>&1 &

    然而,更多时候部署Java应用的时候,应用会专门把日志打印到磁盘特定的目录中便于ELK收集,如笔者前公司的运维规定日志必须打印在/data/log-center/${serverName}目录下,那么这个时候必须把nohup的标准输出流STDOUT和标准错误流STDERR完全忽略。一个比较可行的做法就是把这两个标准流全部重定向到"黑洞/dev/null"中。例如:

    nohup Command >/dev/null 2>&1 &

    编写SpringBoot应用运维脚本

    SpringBoot应用本质就是一个Java应用,但是会有可能添加特定的SpringBoot允许的参数,下面会一步一步分析怎么编写一个可复用的运维脚本。

    全局变量

    考虑到尽可能复用变量和提高脚本的简洁性,这里先提取可复用的全局变量。先是定义JDK的位置JDK_HOME

    JDK_HOME="/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64/bin/java"
    

    接着定义应用的位置APP_LOCATION

    APP_LOCATION="/data/shell/app.jar"
    

    接着定义应用名称APP_NAME(主要用于搜索和展示):

    APP_NAME="app"
    

    然后定义获取PID的命令临时变量PID_CMD,用于后面获取PID的临时变量:

    PID_CMD="ps -ef |grep $APP_LOCATION |grep -v grep |awk '{print $2}'"
    // PID = $(eval $PID_CMD)
    

    定义虚拟机属性VM_OPTS

    VM_OPTS="-Xms2048m -Xmx2048m"
    

    定义SpringBoot属性SPB_OPTS(一般用于配置启动端口、应用Profile或者注册中心地址等等):

    SPB_OPTS="--spring.profiles.active=dev"
    

    主要是这些参数,具体可以按照实际的场景修改或者添加。

    编写核心方法

    例如脚本的文件是server.sh,那么最后需要使用sh server.sh Command执行,其中Command列表如下:

    • start:启动服务。
    • info:打印信息,主要是共享变量的内容。
    • status:打印服务状态,用于判断服务是否正在运行。
    • stop:停止服务进程。
    • restart:重启服务。
    • help:帮助指南。

    这里通过case关键字和命令执行时输入的第一个参数确定具体的调用方法。

    start() {
     echo "start: start server"
    }
    
    stop() {
     echo "stop: shutdown server"
    }
    
    restart() {
     echo "restart: restart server"
    }
    
    status() {
     echo "status: display status of server"
    }
    
    info() {
     echo "help: help info"
    }
    
    help() {
       echo "start: start server"
       echo "stop: shutdown server"
       echo "restart: restart server"
       echo "status: display status of server"
       echo "info: display info of server"
       echo "help: help info"
    }
    
    case $1 in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        restart
        ;;
    status)
        status
        ;;
    info)
        info
        ;;
    help)
        help
        ;;
    *)
        help
        ;;
    esac
    exit $?
    

    测试一下:

    [root@localhost shell]# sh server.sh 
    start: start server
    stop: shutdown server
    restart: restart server
    status: display status of server
    info: display info of server
    help: help info
    ......
    [root@localhost shell]# sh c.sh start
    start: start server
    

    接着需要编写对应的方法实现。

    info方法

    info()主要用于打印当前服务的环境变量和服务的信息等等。

    info() {
      echo "=============================info=============================="
      echo "APP_LOCATION: $APP_LOCATION"
      echo "APP_NAME: $APP_NAME"
      echo "JDK_HOME: $JDK_HOME"
      echo "VM_OPTS: $VM_OPTS"
      echo "SPB_OPTS: $SPB_OPTS"
      echo "=============================info=============================="
    }
    

    status方法

    status()方法主要用于展示服务的运行状态。

    status() {
      echo "=============================status==============================" 
      PID=$(eval $PID_CMD)
      if [[ -n $PID ]]; then
           echo "$APP_NAME is running,PID is $PID"
      else
           echo "$APP_NAME is not running!!!"
      fi
      echo "=============================status=============================="
    }
    

    start方法

    start()方法主要用于启动服务,需要用到JDKnohup等相关命令。

    start() {
     echo "=============================start=============================="
     PID=$(eval $PID_CMD)
     if [[ -n $PID ]]; then
        echo "$APP_NAME is already running,PID is $PID"
     else
        nohup $JDK_HOME $VM_OPTS -jar $APP_LOCATION $SPB_OPTS >/dev/null 2>$1 &
        echo "nohup $JDK_HOME $VM_OPTS -jar $APP_LOCATION $SPB_OPTS >/dev/null 2>$1 &"
        PID=$(eval $PID_CMD)
        if [[ -n $PID ]]; then
           echo "Start $APP_NAME successfully,PID is $PID"
        else
           echo "Failed to start $APP_NAME !!!"
        fi
     fi  
     echo "=============================start=============================="
    }
    
    • 先判断应用是否已经运行,如果已经能获取到应用进程PID,那么直接返回。
    • 使用nohup命令结合java -jar命令启动应用程序jar包,基于PID判断是否启动成功。

    stop方法

    stop()方法用于终止应用程序进程,这里为了相对安全和优雅地kill掉进程,先采用kill -15方式,确定kill -15无法杀掉进程,再使用kill -9

    stop() {
     echo "=============================stop=============================="
     PID=$(eval $PID_CMD)
     if [[ -n $PID ]]; then
        kill -15 $PID
        sleep 5
        PID=$(eval $PID_CMD)
        if [[ -n $PID ]]; then
          echo "Stop $APP_NAME failed by kill -15 $PID,begin to kill -9 $PID"
          kill -9 $PID
          sleep 2
          echo "Stop $APP_NAME successfully by kill -9 $PID"
        else 
          echo "Stop $APP_NAME successfully by kill -15 $PID"
        fi 
     else
        echo "$APP_NAME is not running!!!"
     fi
     echo "=============================stop=============================="
    }
    

    restart方法

    其实就是先stop(),再start()

    restart() {
      echo "=============================restart=============================="
      stop
      start
      echo "=============================restart=============================="
    }
    

    测试

    笔者已经基于SpringBoot依赖只引入spring-boot-starter-web最简依赖,打了一个Jarapp.jar放在虚拟机的/data/shell目录下,同时上传脚本server.sh/data/shell目录下:

    /data/shell
      - app.jar
      - server.sh
    

    某一次测试结果如下:

    [root@localhost shell]# sh server.sh info
    =============================info==============================
    APP_LOCATION: /data/shell/app.jar
    APP_NAME: app
    JDK_HOME: /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64/bin/java
    VM_OPTS: -Xms2048m -Xmx2048m
    SPB_OPTS: --spring.profiles.active=dev
    =============================info==============================
    ......
    [root@localhost shell]# sh server.sh start
    =============================start==============================
    app is already running,PID is 26950
    =============================start==============================
    ......
    [root@localhost shell]# sh server.sh stop
    =============================stop==============================
    Stop app successfully by kill -15 
    =============================stop==============================
    ......
    [root@localhost shell]# sh server.sh restart
    =============================restart==============================
    =============================stop==============================
    app is not running!!!
    =============================stop==============================
    =============================start==============================
    Start app successfully,PID is 27559
    =============================start==============================
    =============================restart==============================
    ......
    [root@localhost shell]# curl http://localhost:9091/ping -s
    [root@localhost shell]# pong
    

    测试脚本确认执行的结果是正确的。其中的=================是笔者故意加入,如果觉得碍眼可以去掉。

    小结

    SpringBoot是目前或者将来一段很长时间Web服务中的主流框架,笔者花了一点时间学习Shell相关的语法,结合nohuppsLinux命令编写了一个可复用的应用运维脚本,目前已经应用在测试和生产环境中,在一定程度上节省了运维成本。

    参考资料:

    附录

    下面是server.sh脚本的所有内容:

    #!/bin/bash
    JDK_HOME="/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.242.b08-0.el7_7.x86_64/bin/java"
    VM_OPTS="-Xms2048m -Xmx2048m"
    SPB_OPTS="--spring.profiles.active=dev"
    APP_LOCATION="/data/shell/app.jar"
    APP_NAME="app"
    PID_CMD="ps -ef |grep $APP_NAME |grep -v grep |awk '{print $2}'"
    
    start() {
     echo "=============================start=============================="
     PID=$(eval $PID_CMD)
     if [[ -n $PID ]]; then
        echo "$APP_NAME is already running,PID is $PID"
     else
        nohup $JDK_HOME $VM_OPTS -jar $APP_LOCATION $SPB_OPTS >/dev/null 2>$1 &
        echo "nohup $JDK_HOME $VM_OPTS -jar $APP_LOCATION $SPB_OPTS >/dev/null 2>$1 &"
        PID=$(eval $PID_CMD)
        if [[ -n $PID ]]; then
           echo "Start $APP_NAME successfully,PID is $PID"
        else
           echo "Failed to start $APP_NAME !!!"
        fi
     fi  
     echo "=============================start=============================="
    }
    
    stop() {
     echo "=============================stop=============================="
     PID=$(eval $PID_CMD)
     if [[ -n $PID ]]; then
        kill -15 $PID
        sleep 5
        PID=$(eval $PID_CMD)
        if [[ -n $PID ]]; then
          echo "Stop $APP_NAME failed by kill -15 $PID,begin to kill -9 $PID"
          kill -9 $PID
          sleep 2
          echo "Stop $APP_NAME successfully by kill -9 $PID"
        else 
          echo "Stop $APP_NAME successfully by kill -15 $PID"
        fi 
     else
        echo "$APP_NAME is not running!!!"
     fi
     echo "=============================stop=============================="
    }
    
    restart() {
      echo "=============================restart=============================="
      stop
      start
      echo "=============================restart=============================="
    }
    
    status() {
      echo "=============================status==============================" 
      PID=$(eval $PID_CMD)
      if [[ -n $PID ]]; then
           echo "$APP_NAME is running,PID is $PID"
      else
           echo "$APP_NAME is not running!!!"
      fi
      echo "=============================status=============================="
    }
    
    info() {
      echo "=============================info=============================="
      echo "APP_LOCATION: $APP_LOCATION"
      echo "APP_NAME: $APP_NAME"
      echo "JDK_HOME: $JDK_HOME"
      echo "VM_OPTS: $VM_OPTS"
      echo "SPB_OPTS: $SPB_OPTS"
      echo "=============================info=============================="
    }
    
    help() {
       echo "start: start server"
       echo "stop: shutdown server"
       echo "restart: restart server"
       echo "status: display status of server"
       echo "info: display info of server"
       echo "help: help info"
    }
    
    case $1 in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart)
        restart
        ;;
    status)
        status
        ;;
    info)
        info
        ;;
    help)
        help
        ;;
    *)
        help
        ;;
    esac
    exit $?
    

    个人博客

    不定时更新,只写原创,偏向于架构、并发。

    (本文完 c-2-d e-a-2020-03-01)

    技术公众号(《Throwable文摘》),不定期推送笔者原创技术文章(绝不抄袭或者转载):

    娱乐公众号(《天天沙雕》),甄选奇趣沙雕图文和视频不定期推送,缓解生活工作压力:

  • 相关阅读:
    GeoProcessor执行工具参数设置
    matlab为影像制作散点图
    无法连接SVN
    STM32例程之USB HID双向数据传输(源码下载)【转】
    USB仪器控制教程
    STemWin5.22移植笔记(flyheart)
    STemWin5.22移植笔记【转】
    [STemWin教程入门篇]第二期:emWin5.xx的详细移植步骤
    [STemWin教程入门篇] 第一期:emWin介绍
    TI推出一款强大模拟设计与仿真工具TINA-TI 9.
  • 原文地址:https://www.cnblogs.com/throwable/p/12392497.html
Copyright © 2020-2023  润新知