做爲Java
開發者,不少場景下會使用SpringBoot
開發Web
應用,目前微服務主流SpringCloud
全家桶也是基於SpringBoot
搭建的。SpringBoot
應用部署到服務器上,須要編寫運維管理腳本。本文嘗試基於經驗,總結以前生產使用的Shell
腳本,編寫一個能夠複用的SpringBoot
應用運維腳本,從而極大減輕SpringBoot
應用啓動、狀態、重啓等管理的工做量。本文的Shell
腳本在CentOS7
中正常運行,其餘操做系統不必定適合。若是對一些基礎或者原理不感興趣能夠拖到最後,直接拷貝腳本使用。html
編寫SpringBoot
應用運維腳本除了基本的Shell
語法要相對熟練以外,還須要解決兩個比較重要的問題(筆者我的認爲):java
ID
,也就是獲取Process ID
(下面稱PID
)的問題。kill
命令的正確使用姿式。nohup
的正確使用方式。通常而言,若是經過應用名稱可以成功獲取PID
,則能夠肯定應用進程正在運行,不然應用進程不處於運行狀態。應用進程的運行狀態是基於PID
判斷的,所以在應用進程管理腳本中會屢次調用獲取PID
的命令。一般狀況下會使用grep
命令去查找PID
,例以下面的命令是查詢Redis
服務的PID
:node
ps -ef |grep redis |grep -v grep |awk '{print $2}'
其實這是一個複合命令,每一個|
後面都是一個完整獨立的命令,其中:web
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}'
複合命令執行過程就是:redis
<1>
經過ps -ef
獲取系統進程狀態。<2>
經過grep redis
從<1>
中的結果搜索redis
關鍵字,得出redis
進程信息。<3>
經過grep -v grep
從<2>
中的結果過濾掉grep
自身的進程。<4>
經過awk '{print $2}'
從<3>
中的結果獲取第二列。在Shell
腳本中,可使用這種方式獲取PID
:spring
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 -N PID
,本質功能是向對應PID
的進程發送一個信號,而後對應的進程須要對這個信號做出響應,信號的編號就是N
,這個N
的可選值以下(系統是CentOS 7
):安全
其中開發者常見的就是9) SIGKILL
和15) 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就是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
應用本質就是一個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() { 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() { 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()
方法主要用於啓動服務,須要用到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()
方法用於終止應用程序進程,這裏爲了相對安全和優雅地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==============================" }
其實就是先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
命令編寫了一個可複用的應用運維腳本,目前已經應用在測試和生產環境中,在必定程度上節省了運維成本。
參考資料:
下面是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)