模板说明
守护由linux crontab定时调度,守护程序不负责任务调度(crontab稳定性高,守护程序需要使用循环语法,稳定性无法保证,如进程退出)
守护的验证标准
- 开机能启动
- 正常运行时不守护
- 手动关闭进程,守护启动
- 同时只有一个进程
crontab
crontab -e 与 vim /etc/crontab 的区别
-
crontab -e 系统会检查语法,而vim /etc/crontab不检查语法。
-
crontab -e的写法与vi /etc/crontab也有微小差异,在vim /etc/crontab时,一定要加入用户,否则不会生效;而crontab -e定制定时任务时,则不需要 添加用户,否则也会失效。
crontab -e的格式: * * * * * command vim /etc/crontab的格式: * * * * * user command
以下为 crontab -e 语法说明
# Example of job definition: # .---------------- minute (0 - 59) # | .------------- hour (0 - 23) # | | .---------- day of month (1 - 31) # | | | .------- month (1 - 12) OR jan,feb,mar,apr ... # | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat # | | | | | # * * * * * command to be executed
crontab运行跟踪
查看定时任务是否配置成功可以使用:tail -f /var/log/cron 来进行跟踪任务是否执行
特殊语法
支持特殊关键字实现任务调度,代替 5个时间设置标识符,特殊语法以 @ 符号为前缀
具体请参考:官方文档
语法:@xxx command
@reboot : Run once after reboot. 重启时执行一次,实测比 /etc/rc.d/rc.local 更早执行(提前1-2s),比默认1分钟定时提前30s左右 @yearly : Run once a year, ie. "0 0 1 1 *". 一年执行一次,1月1号0点0分执行 @annually : Run once a year, ie. "0 0 1 1 *". 一年执行一次,1月1号0点0分执行 @monthly : Run once a month, ie. "0 0 1 * *". 一月执行一次,每月1号0点0分执行 @weekly : Run once a week, ie. "0 0 * * 0". 一周执行一次 @daily : Run once a day, ie. "0 0 * * *". 一天执行一次 @hourly : Run once an hour, ie. "0 * * * *". 一小时执行一次
守护程序模板
#! /bin/bash #守护由linux crontab调度,本程序不负责任务调度(crontab稳定性高) #本脚本支持根据 服务名称 或 服务端口 守护,请根据守护的组件特性调用对应方法 #crontab不会自动加载全局环境变量,此处需要主动加载环境变量 source /etc/profile #!!!需要关注的内容!!!# SERVICE_NAME= SERVICE_PORT= #kafka命令中JMX_PORT=9999为赋值语句,而在执行时默认会被识别为命令语句,所以会提示异常:JMX_PORT=9999: 未找到命令 #所以在执行复杂命令时(赋值、一行多条命令等),使用eval语法: eval ${SERVICE_CMD} SERVICE_CMD= MONITOR_LOG= #!!!需要关注的内容!!!# #格式化日期时间函数 function getDatetime(){ local cur=`date "+%Y-%m-%d %H:%M:%S"` echo ${cur} } #根据 服务端口 守护 #此方法适合对外提供服务的程序,监听端口只能被唯一程序占用 function processByPort(){ ... } #根据 服务名称 守护, 使用 `killall 服务名称` 结束进程 #此方法适合进程名与服务名称唯一的程序,不适合java程序(进程名为java,而不是jar包) function processByNameKillByName(){ ... } #根据 服务名称 守护, 使用 `kill pid` 结束进程 #此方法适合java程序 function processByNameKillByPid(){ ... } #!!!需要关注的内容!!!# processByPort #!!!需要关注的内容!!!#
#根据 服务端口 守护方案
#根据 服务端口 守护 #此方法适合对外提供服务的程序,监听端口只能被唯一程序占用 function processByPort(){ #tcp 0 0 0.0.0.0:7379 0.0.0.0:* LISTEN 5593/redis-server 0 #指定 -w 选项全字匹配,避免多个端口(7379,73791)同时匹配,造成误杀 NUM=`netstat -lntup | grep -w ${SERVICE_PORT} | wc -l` #进程数少于1,启动进程 if [ ${NUM} -lt 1 ];then echo `getDatetime` "found 0, start ${SERVICE_NAME}" >> ${MONITOR_LOG} eval $SERVICE_CMD #大于1,杀掉所有进程,重启 elif [ ${NUM} -gt 1 ];then echo `getDatetime` "more than 1 found, killall and start ${SERVICE_NAME}" >> ${MONITOR_LOG} pids=`netstat -lntup | grep -w ${SERVICE_PORT} | awk -F '[ /]+' '{print $7}'` for pid in ${pids} do #此处可以根据实际服务决定是否发送相关进程信号,如kill -9强制停止进程 #默认会通知进程优雅关闭,不会强制停止进程 #有些服务如 MySQL 自身存在守护程序,监听进程信号,此时执行kill、killall、pkill等如果发送信号,MySQL会重置新的进程ID继续运行 #停止 MySQL 可以使用服务自身命令 service mysqld stop 或执行kill、killall、pkill等不发送进程信号 #!!!需要关注的内容!!!# echo "kill ${pid}" >> ${MONITOR_LOG} kill -9 ${pid} #!!!需要关注的内容!!!# done #!!!需要关注的内容!!!# #如果停止进程时没有发送强制停止信号,则此处需要延迟一定时间(单位:秒),待进程优雅退出后才能启动服务,否则服务启动失败(绝大部分服务不允许重复启动) #sleep 5 #!!!需要关注的内容!!!# #启动服务 eval $SERVICE_CMD #else # echo `getDatetime` "found 1, ${SERVICE_NAME} is alive" >> ${MONITOR_LOG} fi }
#根据 服务名称 守护方案
进程名称与服务名称一致,使用 `killall 服务名称` 结束进程
#根据 服务名称 守护, 使用 `killall 服务名称` 结束进程 #此方法适合进程名与服务名称唯一的程序,不适合java程序(进程名为java,而不是jar包) function processByNameKillByName(){ #root 5593 1 0 13:06 ? 00:00:08 /usr/local/redis/redis-5.0.13/bin/redis-server 0.0.0.0:7379 [cluster] #指定 -w 选项全字匹配,避免多个名称(7379,73791)同时匹配,造成误杀 NUM=`ps -ef | grep -w ${SERVICE_NAME} | grep -v grep | wc -l` #进程数少于1,启动进程 if [ ${NUM} -lt 1 ];then echo `getDatetime` "found 0, start ${SERVICE_NAME}" >> ${MONITOR_LOG} eval $SERVICE_CMD #大于1,杀掉所有进程,重启 elif [ ${NUM} -gt 1 ];then echo `getDatetime` "more than 1 found, killall and start ${SERVICE_NAME}" >> ${MONITOR_LOG} #此处可以根据实际服务决定是否发送相关进程信号,如kill -9强制停止进程 #默认会通知进程优雅关闭,不会强制停止进程 #有些服务如 MySQL 自身存在守护程序,监听进程信号,此时执行kill、killall、pkill等如果发送信号,MySQL会重置新的进程ID继续运行 #停止 MySQL 可以使用服务自身命令 service mysqld stop 或执行kill、killall、pkill等不发送进程信号 #!!!需要关注的内容!!!# killall -9 ${SERVICE_NAME} eval $SERVICE_CMD #!!!需要关注的内容!!!# #else #echo `getDatetime` "found 1, ${SERVICE_NAME} is alive" >> ${MONITOR_LOG} fi }
#根据 服务名称 守护方案
进程名称与服务器名称不一致,使用 `kill pid` 结束进程
#此方法适合java程序
#根据 服务名称 守护, 使用 `kill pid` 结束进程 #此方法适合java程序 function processByNameKillByPid(){ #root 5593 1 0 13:06 ? 00:00:08 /usr/local/redis/redis-5.0.13/bin/redis-server 0.0.0.0:7379 [cluster] #指定 -w 选项全字匹配,避免多个名称(7379,73791)同时匹配,造成误杀 NUM=`ps -ef | grep -w ${SERVICE_NAME} | grep -v grep | wc -l` #进程数少于1,启动进程 if [ ${NUM} -lt 1 ];then echo `getDatetime` "found 0, start ${SERVICE_NAME}" >> ${MONITOR_LOG} eval $SERVICE_CMD #大于1,杀掉所有进程,重启 elif [ ${NUM} -gt 1 ];then echo `getDatetime` "more than 1 found, killall and start ${SERVICE_NAME}" >> ${MONITOR_LOG} pids=`ps -ef | grep -w ${SERVICE_NAME} | grep -v grep | awk '{print $2}'` for pid in ${pids} do #此处可以根据实际服务决定是否发送相关进程信号,如kill -9强制停止进程 #默认会通知进程优雅关闭,不会强制停止进程 #有些服务如 MySQL 自身存在守护程序,监听进程信号,此时执行kill、killall、pkill等如果发送信号,MySQL会重置新的进程ID继续运行 #停止 MySQL 可以使用服务自身命令 service mysqld stop 或执行kill、killall、pkill等不发送进程信号 #!!!需要关注的内容!!!# echo "kill ${pid}" >> ${MONITOR_LOG} kill -9 ${pid} #!!!需要关注的内容!!!# done #!!!需要关注的内容!!!# #如果停止进程时没有发送强制停止信号,则此处需要延迟一定时间(单位:秒),待进程优雅退出后才能启动服务,否则服务启动失败(绝大部分服务不允许重复启动) #sleep 5 #!!!需要关注的内容!!!# #启动服务 eval $SERVICE_CMD #else #echo `getDatetime` "found 1, ${SERVICE_NAME} is alive" >> ${MONITOR_LOG} fi }
特别关注:特殊情况
请根据要守护的应用服务组件的实际特性,合理调整守护脚本,适当增加必要的操作,主要为SERVICE_CMD内容,比如先cd到特定目录,先创建特定目录,先删除特定文件,等。
以kafka、kafka-manager为例(详细内容可参考教程kafka-manager安装)
- kafka需要配置特定参数才能被kafka-manager监控,所以启动前需要配置相关环境变量
- kafka-manager重启前需要删除PID文件
特别关注:关于进程定位
不论使用端口守护 还是 进程名称守护,一定要确认唯一性,即通过指定的条件能精准定位到具体进程
如:java程序的进程名为 java,而不是jar包名,所以通过 killall 进程名 是无法精确定位进程的,即如果执行 killall java,则会将其他java进程退出
- 建议1:使用端口号守护方案
- 建议2:进程定位使用进程名,但停止进程使用 kill pid,不能使用killall 进程名 或 pkill 进程名
[root@localhost ~]# ps -ef | grep V2XApiServer.jar root 1754 1 0 3月09 ? 00:15:26 java -jar V2XApiServer.jar [root@localhost ~]# [root@localhost ~]# killall V2XApiServer.jar V2XApiServer.jar: no process found [root@localhost ~]# pgrep -l java 1707 java 1753 java 1754 java 1755 java 1756 java 1757 java
附录:redis守护步骤
1.创建统一数据目录
存放守护程序 与 守护程序执行日志
[root@localhost ~]# mkdir -p /usr/local/daemonProcess
2.编辑守护程序脚本
需要关注:不同守护方案需要关注不同信息
2.1 通过端口守护
redis服务启动后,默认会监听2个主要端口,可以配置任意一个
- 6379,redis对外提供服务的端口,可以在配置文件中指定
- 16379,redis集群各节点通信的端口,在配置的对外端口基础上 +10000
建议使用 netstat -lntup | grep -w ${SERVICE_PORT} | wc -l
[root@localhost ~]# netstat -lntup | grep 7379 tcp 0 0 0.0.0.0:7379 0.0.0.0:* LISTEN 1057/redis-server 0 tcp 0 0 0.0.0.0:17379 0.0.0.0:* LISTEN 1057/redis-server 0 [root@localhost ~]# [root@localhost ~]# netstat -lntup | grep -w 7379 tcp 0 0 0.0.0.0:7379 0.0.0.0:* LISTEN 1057/redis-server 0
2.2 通过进程名守护
redis服务启动后,进程名为 redis-server
[root@localhost ~]# ps -ef | grep -v grep | grep redis root 1057 1 0 3月15 ? 00:05:05 /usr/local/redis/redis-5.0.13/bin/redis-server 0.0.0.0:7379 [cluster]
2.3 完整守护程序脚本建议 daemonProcessRedis.sh
#! /bin/bash #守护由linux crontab调度,本程序不负责任务调度(crontab稳定性高) #本脚本支持根据 服务名称 或 服务端口 守护,请根据守护的组件特性调用对应方法 #crontab不会自动加载全局环境变量,此处需要主动加载环境变量 source /etc/profile #!!!需要关注的内容!!!# SERVICE_NAME=redis-server SERVICE_PORT=7379 #kafka命令中JMX_PORT=9999为赋值语句,而在执行时默认会被识别为命令语句,所以会提示异常:JMX_PORT=9999: 未找到命令 #所以在执行复杂命令时(赋值、一行多条命令等),使用eval语法: eval ${SERVICE_CMD} SERVICE_CMD="/usr/local/redis/bin/redis-server /usr/local/redis/conf/redis.conf" MONITOR_LOG="/usr/local/daemonProcess/daemonProcessRedis.log" #!!!需要关注的内容!!!# #格式化日期时间函数 function getDatetime(){ local cur=`date "+%Y-%m-%d %H:%M:%S"` echo ${cur} } #根据 服务端口 守护 #此方法适合对外提供服务的程序,监听端口只能被唯一程序占用 function processByPort(){ #tcp 0 0 0.0.0.0:7379 0.0.0.0:* LISTEN 5593/redis-server 0 NUM=`netstat -lntup | grep -w ${SERVICE_PORT} | wc -l` #进程数少于1,启动进程 if [ ${NUM} -lt 1 ];then echo `getDatetime` "found 0, start ${SERVICE_NAME}" >> ${MONITOR_LOG} eval $SERVICE_CMD #大于1,杀掉所有进程,重启 elif [ ${NUM} -gt 1 ];then echo `getDatetime` "more than 1 found, killall and start ${SERVICE_NAME}" >> ${MONITOR_LOG} pids=`netstat -lntup | grep -w ${SERVICE_PORT} | awk -F '[ /]+' '{print $7}'` for pid in ${pids} do #此处可以根据实际服务决定是否发送相关进程信号,如kill -9强制停止进程 #默认会通知进程优雅关闭,不会强制停止进程 #有些服务如 MySQL 自身存在守护程序,监听进程信号,此时执行kill、killall、pkill等如果发送信号,MySQL会重置新的进程ID继续运行 #停止 MySQL 可以使用服务自身命令 service mysqld stop 或执行kill、killall、pkill等不发送进程信号 #!!!需要关注的内容!!!# kill -9 ${pid} #!!!需要关注的内容!!!# done #!!!需要关注的内容!!!# #如果停止进程时没有发送强制停止信号,则此处需要延迟一定时间(单位:秒),待进程优雅退出后才能启动服务,否则服务启动失败(绝大部分服务不允许重复启动) #sleep 5 #!!!需要关注的内容!!!# #启动服务 eval $SERVICE_CMD #else # echo `getDatetime` "found 1, ${SERVICE_NAME} is alive" >> ${MONITOR_LOG} fi } #!!!需要关注的内容!!!# processByPort #!!!需要关注的内容!!!#
3.设置守护程序脚本可执行权限
[root@localhost ~]# cd /usr/local/daemonProcess/ [root@localhost daemonProcess]# chmod +x daemonProcessRedis.sh [root@localhost daemonProcess]# ll 总用量 12 -rwxr-xr-x. 1 root root 2087 3月 16 18:16 daemonProcessRedis.sh
4.配置定时任务
[root@localhost daemonProcess]# crontab -e # Example of job definition: # .---------------- minute (0 - 59) # | .------------- hour (0 - 23) # | | .---------- day of month (1 - 31) # | | | .------- month (1 - 12) OR jan,feb,mar,apr ... # | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat # | | | | | # * * * * * command to be executed #开机启动 @reboot /usr/local/daemonProcess/daemonProcessRdis.sh #守护 * * * * * /usr/local/daemonProcess/daemonProcessRedis.sh
5.测试
停止redis进程,模拟服务故障,查看守护程序执行日志 daemonProcessRedis.log,此处略