編寫一個可複用的SpringBoot應用運維腳本

前提

做爲Java開發者,不少場景下會使用SpringBoot開發Web應用,目前微服務主流SpringCloud全家桶也是基於SpringBoot搭建的。SpringBoot應用部署到服務器上,須要編寫運維管理腳本。本文嘗試基於經驗,總結以前生產使用的Shell腳本,編寫一個能夠複用的SpringBoot應用運維腳本,從而極大減輕SpringBoot應用啓動、狀態、重啓等管理的工做量。本文的Shell腳本在CentOS7中正常運行,其餘操做系統不必定適合。若是對一些基礎或者原理不感興趣能夠拖到最後,直接拷貝腳本使用。html

依賴到的Shell相關的知識

編寫SpringBoot應用運維腳本除了基本的Shell語法要相對熟練以外,還須要解決兩個比較重要的問題(筆者我的認爲):java

  • 正確獲取目標應用程序的進程ID,也就是獲取Process ID(下面稱PID)的問題。
  • kill命令的正確使用姿式。
  • 命令nohup的正確使用方式。

獲取PID

通常而言,若是經過應用名稱可以成功獲取PID,則能夠肯定應用進程正在運行,不然應用進程不處於運行狀態。應用進程的運行狀態是基於PID判斷的,所以在應用進程管理腳本中會屢次調用獲取PID的命令。一般狀況下會使用grep命令去查找PID,例以下面的命令是查詢Redis服務的PIDnode

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

其實這是一個複合命令,每一個|後面都是一個完整獨立的命令,其中:web

  • 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}'複合命令執行過程就是:redis

  • <1>經過ps -ef獲取系統進程狀態。
  • <2>經過grep redis<1>中的結果搜索redis關鍵字,得出redis進程信息。
  • <3>經過grep -v grep<2>中的結果過濾掉grep自身的進程。
  • <4>經過awk '{print $2}'<3>中的結果獲取第二列。

Shell腳本中,可使用這種方式獲取PIDspring

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

可是這樣會存在一個問題,就是每次想獲取PID都必須使用這串很是長的命令,顯得有些笨拙。可使用eval簡化這個過程:docker

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

獲取PID的問題解決,而後能夠基於PID是否存在,決定一下步怎麼操做。shell

理解kill命令

kill命令的通常形式是kill -N PID,本質功能是向對應PID的進程發送一個信號,而後對應的進程須要對這個信號做出響應,信號的編號就是N,這個N的可選值以下(系統是CentOS 7):安全

其中開發者常見的就是9) SIGKILL15) SIGTERM,它們的通常描述以下:springboot

信號編號 信號名稱 描述 功能 影響
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)

相關文章
相關標籤/搜索