编写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

使用eval简化这个过程:

PID_CMD="ps -ef |grep docker |grep -v grep |awk '{print \$2}'"
PID=$(eval $PID_CMD)
echo $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允许的参数。

4.1全局变量

提取可复用的全局变量。先是定义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"

4.2编写核心方法

例如脚本的文件是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

编写对应的方法实现。

4.3Info方法

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=============================="
}

4.4status方法

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=============================="
}

4.5start方法

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判断是否启动成功。

4.6stop方法

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=============================="
}

4.7restart方法

先stop(),再start()。

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

4.8测试

基于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

4.9完整的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 $?

(二)SpringBoot应用运维脚本的更多相关文章

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

    前提 作为Java开发者,很多场景下会使用SpringBoot开发Web应用,目前微服务主流SpringCloud全家桶也是基于SpringBoot搭建的.SpringBoot应用部署到服务器上,需要 ...

  2. python常用运维脚本实例

    转载  file是一个类,使用file('file_name', 'r+')这种方式打开文件,返回一个file对象,以写模式打开文件不存在则会被创建.但是更推荐使用内置函数open()来打开一个文件 ...

  3. 转:python常用运维脚本实例

    python常用运维脚本实例 转载  file是一个类,使用file('file_name', 'r+')这种方式打开文件,返回一个file对象,以写模式打开文件不存在则会被创建.但是更推荐使用内置函 ...

  4. python常用运维脚本实例【转】

    file是一个类,使用file('file_name', 'r+')这种方式打开文件,返回一个file对象,以写模式打开文件不存在则会被创建.但是更推荐使用内置函数open()来打开一个文件 . 首先 ...

  5. sql server自动化运维脚本

    数据库运维中盛传一个小段子,我误删除了数据库,改怎么办?有备份还原备份,没有备份就准备简历!听起来有趣但发生在谁身上,谁都笑不起来.接触了很多的客户发现90%客户的运维策略都不是很完善.本篇就分享一些 ...

  6. Shell编程案例:修改运维脚本输出效果

    1. 需求:每日运维检查脚本dailymonitor.sh显示对服务器测试结果,其中命令 zabbix_get -s 192.168.111.21 -p 10050 -k "net.tcp. ...

  7. 运维脚本while语法

    循环的意思就是让程序重复地执行某些语句; whiler循环就是循环结构的一种,当事先不知道循环该执行多少次,就要用到while循环; while循环语句的运行过程 使用while循环语句时,可以根据特 ...

  8. Python运维脚本整理

    Python 实现的自动化服务器管理 import sys import os import paramiko ssh = paramiko.SSHClient() ssh.set_missing_h ...

  9. 运维脚本-elasticsearch数据迁移python3脚本

    elasticsearch数据迁移python3脚本 #!/usr/bin/python3 #elsearch 数据迁移脚本 #迁移工具路径 import time,os #下面命令是用到了一个go语 ...

随机推荐

  1. Mac Arduino ESP8266 ESP32 搭建开发环境

    目录 1.安装Arduino 2.搭建开发板管理器 3.可能出现的错误 1.安装Arduino Arduino下载. 官方下载地址:Arduino官方网站 Arduino中文社区:下载地址 安装方式: ...

  2. react状态管理器之mobx

    react有几种状态管理器,今天先来整理一下mobx状态管理器,首先了解一下什么是mobx 1.mobx成员: observable action 可以干嘛: MobX 的理念是通过观察者模式对数据做 ...

  3. 通过naa在esxi主机上找到物理磁盘的位置

    因为有一块磁盘告警,需要找到这个块磁盘.通过网络搜索就找到了这个shell脚本. 感谢 Jorluis Perales, VxRail TSE 2 shell脚本: # Script to obtai ...

  4. API企业级网关设计

    前言 摘自--https://github.com/aCoder2013/blog/issues/35 假设你正在开发一个电商网站,那么这里会涉及到很多后端的微服务,比如会员.商品.推荐服务等等. 那 ...

  5. Sqoop(二)常用命令及常数解析

    一.常用命令列举 二.命令及参数详解 1.数据库连接 2.import 3.export 4.hive

  6. 10分钟搞定让你困惑的 Jenkins 环境变量

    前言 Jenkins, DevOps 技术栈的核心之一,CI/CD 离不开编写 Pipeline 脚本,上手 Jenkins ,简单查一下文档,你就应该不会被 agent,stages,step 这类 ...

  7. 2020DevOps状态报告——变更管理

    如果你的公司还没有走向平台化,现在仍然可以是很大的飞跃.您仍然可以通过解决公司的变更管理流程来加快软件交付.在本章中,我们将研究我们在公司内部所学的变更管理模式.我们将向您展示什么是有效的,什么是无效 ...

  8. 借助window.performance实现基本的前端基础性能监控日志

    借助window.performance实现基本的前端基础性能监控日志并二次重写console方法方便日常前端console日志的调试 npm install sn-console

  9. Docker 镜像仓库使用(六)

    阿里云docker 容器镜像服务: www.aliyun.com 1 服务开通 (开通的时候要求创建密码请牢记此密码) 2 创建命名空间 3 创建镜像仓库 4 linux 客户端登录 登录: dock ...

  10. 【System】进程,线程和任务之间的区别是什么?

    任务(task)是最抽象的,是一个一般性的术语,指由软件完成的一个活动.一个任务既可以是一个进程,也可以是一个线程.简而言之,它指的是一系列共同达到某一目的的操作.例如,读取数据并将数据放入内存中.这 ...