編寫一個可複用的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 -ef
是ps
命令加上-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) SIGKILL
和15) 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()
方法主要用於啟動服務,需要用到JDK
和nohup
等相關命令。
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
最簡依賴,打了一個Jar
包app.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
相關的語法,結合nohup
、ps
等Linux
命令編寫了一個可複用的應用運維指令碼,目前已經應用在測試和生產環境中,在一定程度上節省了運維成本。
參考資料:
- Nohup Invocation
附錄
下面是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 $?
個人部落格
- Throwable's Blog
不定時更新,只寫原創,偏向於架構、併發。
(本文完 c-2-d e-a-2020-03-01