来源:https://sourl.cn/7cnEEe 作者:throwable
1
前提
作为Java
开发者,很多场景下会使用SpringBoot
开发Web
应用,目前微服务主流SpringCloud
全家桶也是基于SpringBoot
搭建的。SpringBoot
应用部署到服务器上,需要编写运维管理脚本。本文尝试基于经验,总结之前生产使用的Shell
脚本,编写一个可以复用的SpringBoot
应用运维脚本,从而极大减轻SpringBoot
应用启动、状态、重启等管理的工作量。本文的Shell
脚本在CentOS7
中正常运行,其他操作系统不一定适合。如果对一些基础或者原理不感兴趣可以拖到最后,直接拷贝脚本使用。
2
依赖到的Shell相关的知识
编写SpringBoot
应用运维脚本除了基本的Shell
语法要相对熟练之外,还需要解决两个比较重要的问题(笔者个人认为):
正确获取目标应用程序的进程
ID
,也就是获取Process ID
(下面称PID
)的问题。kill
命令的正确使用姿势。命令
nohup
的正确使用方式。
3
获取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
是否存在,决定一下步怎么操作。
4
理解kill命令
kill
命令的一般形式是kill -N PID
,本质功能是向对应PID
的进程发送一个信号,然后对应的进程需要对这个信号作出响应,信号的编号就是N
,这个N
的可选值如下(系统是CentOS 7
):
不带-N
参数的kill
命令默认就是kill -15
。一般而言,kill -9 PID
是进程的必杀手段,但是它很有可能影响进程结束前释放资源的过程或者中止I/O
操作造成数据异常丢失等问题。
5
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 &
6
编写SpringBoot应用运维脚本
SpringBoot
应用本质就是一个Java
应用,但是会有可能添加特定的SpringBoot
允许的参数,下面会一步一步分析怎么编写一个可复用的运维脚本。
全局变量
考虑到尽可能复用变量和提高脚本的简洁性,这里先提取可复用的全局变量。先是定义JDK
的位置JDK_HOME
:
|
|
接着定义应用的位置APP_LOCATION
:
|
|
接着定义应用名称APP_NAME
(主要用于搜索和展示):
|
|
然后定义获取PID
的命令临时变量PID_CMD
,用于后面获取PID
的临时变量:
PID_CMD="ps -ef |grep $APP_LOCATION |grep -v grep |awk '{print \$2}'"
// PID = $(eval $PID_CMD)
定义虚拟机属性VM_OPTS
:
|
|
定义SpringBoot
属性SPB_OPTS
(一般用于配置启动端口、应用Profile
或者注册中心地址等等):
|
|
主要是这些参数,具体可以按照实际的场景修改或者添加。
7
编写核心方
例如脚本的文件是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 ]]; thenecho "$APP_NAME is running,PID is $PID"elseecho "$APP_NAME is not running!!!"fiecho "=============================status=============================="
}
start方法
start()
方法主要用于启动服务,需要用到JDK
和nohup
等相关命令。
start() {echo "=============================start=============================="PID=$(eval $PID_CMD)if [[ -n $PID ]]; thenecho "$APP_NAME is already running,PID is $PID"elsenohup $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 ]]; thenecho "Start $APP_NAME successfully,PID is $PID"elseecho "Failed to start $APP_NAME !!!"fifi 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 ]]; thenkill -15 $PIDsleep 5PID=$(eval $PID_CMD)if [[ -n $PID ]]; thenecho "Stop $APP_NAME failed by kill -15 $PID,begin to kill -9 $PID"kill -9 $PIDsleep 2echo "Stop $APP_NAME successfully by kill -9 $PID"else echo "Stop $APP_NAME successfully by kill -15 $PID"fi elseecho "$APP_NAME is not running!!!"fiecho "=============================stop=============================="
}
restart方法
其实就是先stop()
,再start()
。
restart() {echo "=============================restart=============================="stopstartecho "=============================restart=============================="
}
测试
笔者已经基于SpringBoot
依赖只引入spring-boot-starter-web
最简依赖,打了一个Jar
包app.jar
放在虚拟机的/data/shell
目录下,同时上传脚本server.sh
到/data/shell
目录下:
|
|
8
总结
SpringBoot
是目前或者将来一段很长时间Web
服务中的主流框架,笔者花了一点时间学习Shell
相关的语法,结合nohup
、ps
等Linux
命令编写了一个可复用的应用运维脚本,目前已经应用在测试和生产环境中,在一定程度上节省了运维成本。
参考资料:
http://www.gnu.org/software/coreutils/manual/html_node/nohup-invocation.html#nohup-invocation
附录
下面是server.sh
脚本的所有内容:
#!/bin/bashJDK_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 instart) start ;;stop) stop ;;restart) restart ;;status) status ;;info) info ;;help) help ;;*) help ;;esacexit $?
看都看完了,还不点这里试试