Linux服务进程守护程序模板

发布时间 2023-09-01 17:38:18作者: xiaoyaozhe

Linux系统-部署-运维系列导航

模板说明

守护由linux crontab定时调度,守护程序不负责任务调度(crontab稳定性高,守护程序需要使用循环语法,稳定性无法保证,如进程退出)
 

守护的验证标准

  1. 开机能启动
  2. 正常运行时不守护
  3. 手动关闭进程,守护启动
  4. 同时只有一个进程
 

crontab

crontab -e 与 vim /etc/crontab 的区别

  1. crontab -e 系统会检查语法,而vim /etc/crontab不检查语法。
  2. 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 选项全字匹配,避免多个端口(737973791)同时匹配,造成误杀
    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
}
View Code
 

#根据 服务名称 守护方案

进程名称与服务名称一致,使用 `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 选项全字匹配,避免多个名称(737973791)同时匹配,造成误杀
    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
}
shell Code
 

#根据 服务名称 守护方案

进程名称与服务器名称不一致,使用 `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 选项全字匹配,避免多个名称(737973791)同时匹配,造成误杀
    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
}
shell Code

 

特别关注:特殊情况

请根据要守护的应用服务组件的实际特性,合理调整守护脚本,适当增加必要的操作,主要为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
#!!!需要关注的内容!!!#
shell Code
 

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,此处略