supervisor#01#基本使用方法教程

Supervisor is a client/server system that allows its users to monitor and control a number of processes on UNIX-like operating systems.

Supervisor是一个在类UNIX系统上,监控控制大量进程的客户端/服务端系统。其主要功能包括程序自动启动、程序奔溃后自动重启,指定进程数目等等。本文是Supervisor安装、配置和使用的简单教程。

安装软件

在Ubuntu系统上,我们使用apt安装supervisor,命令如下:

1
$ sudo apt install supervisor

安装成功之后,systemd依据随软件一起安装的supervisor.service,自动启动supervisord,如下图所示:

使用示例

成功安装以及正确启动supervisor之后,我们使用一个简单的例子来介绍其使用方法:使用python3内置的http.server为例,让supervisor来负责http.server的启动、停止、重启以及随机启动等操作。

第一步:创建被管理程序的配置文件

首先,我们需要让supervisord知道其待管理程序的基本信息,比如该程序在系统中的标识、启动方法、停止方法、进程数量等属性,所以需要给被管理程序做个配置文件,然后通知supervisord读取该配置文件,supervisord依据该配置文件对程序在系统内部进行进一步的管理。

我们在 /etc/supervisor/conf.d 目录下创建配置文件,文件名称自定义即可,一般我保持配置文件名字和服务名字一致,如本例为httpserver.conf。然后在配置文件中输入以下内容:

1
2
[program:httpserver]
command=python3 -m http.server 20080

新建配置文件之后,其成品如下图所示:

第二步:通知supervisord加载配置文件,并启动httpserver

使用如下命令加载配置文件:

1
2
3
4
5
$ sudo supervisorctl update
# 或者
$ sudp supervisorctl update httpserver
# 或者
$ sudo supervisorctl reload

实际操作过程,如下图所示:

从上图我们可以看出,当supervisord重新加载配置文件之后,httpserver被系统成功启动。

好了,我们目前已经可以确认supervisord会帮我们启动程序了。下面我们再做一些其他的测试:

A.程序崩溃(我们使用kill -9模拟)之后,系统自动重启httpserver

B.我们主动停止httpserver的方法

主动停止使用如下命令:

1
$ sudo supervisorctl stop httpserver

其执行过程如下图所示:

C.我们主动停止httpserver后,手工重启的方法

主动停止使用如下命令:

1
2
3
$ sudo supervisorctl start httpserver
# 后者
$ sudo supervisorctl restart httpserver

其执行过程如下图所示:

D.被管理程序配置文件修改之后,重新加载的方法
修改被管理程序配置文件,改变了该程序运行的一些属性,比如当前httpserver监听在20080端口上,我们修改配置文件让其监听在20081端口上。如果仅使用sudo supervisorctl restart httpserver,你会发现重启之后的程序依然监听在20080端口上,也就是说在supervisord不重启或者不通知supervisord重新加载配置文件的情况下,使用restart命令是不会改变已在supervisord内存中的属性。
所以,修改配置文件之后要么让supervisord重启,要么让supervisord重新配置文件(重新加载配置文件,发现与内存属性不一致时,会重启程序)。

主动加载配置文件,使用如下命令:

1
$ sudo supervisorctl update

其执行过程如下图所示:

E.停止supervisord服务,以及重新启动

停止supervisord服务,使用如下命令:

1
2
3
$ sudo supervisorctl shutdown
# 或者
$ sudo systemctl stop supervisor.service

supervisord服务停止之后,使用如下命令重新启动:

1
$ sudo systemctl start supervisor.service

其执行过程如下图所示:

通过上述操作过程我们可知:

  • 执行 sudo supervisorctl shutdown 之后,会将当前其所管理的所有程序都停止掉
  • 执行 sudo systemctl start supervisor.service 之后,会将当前其所管理的所有可随supervisord重启而重启的程序都重启

supervisorctl命令介绍

使用 supervisorctl help 命令获取 supervisorctl 支持的命令列表,如下图所示:

0x00:查看进程运行状态

1
$ sudo supervisorctl status <arguments>

1
2
3
4
5
6
mancode@manos:~$ supervisorctl help status
status <name> Get status for a single process
status <gname>:* Get status for all processes in a group
status <name> <name> Get status for multiple named processes
status Get all process status info
mancode@manos:~$

0x01:启动进程

1
$ sudo supervisorctl start <arguments>

1
2
3
4
5
6
mancode@manos:~$ supervisorctl help start
start <name> Start a process
start <gname>:* Start all processes in a group
start <name> <name> Start multiple processes or groups
start all Start all processes
mancode@manos:~$

0x02:停止进程

1
$ sudo supervisorctl stop <arguments>

1
2
3
4
5
6
mancode@manos:~$ supervisorctl help stop
stop <name> Stop a process
stop <gname>:* Stop all processes in a group
stop <name> <name> Stop multiple processes or groups
stop all Stop all processes
mancode@manos:~$

0x03:重启进程

1
$ sudo supervisorctl restart <arguments>

1
2
3
4
5
6
7
mancode@manos:~$ supervisorctl help restart
restart <name> Restart a process
restart <gname>:* Restart all processes in a group
restart <name> <name> Restart multiple processes or groups
restart all Restart all processes
Note: restart does not reread config files. For that, see reread and update.
mancode@manos:~$

注意:Note: restart does not reread config files. For that, see reread and update.

0x04:重新加载配置文件

1
$ sudo supervisorctl update <arguments>

1
2
3
4
5
mancode@manos:~$ supervisorctl help update
update Reload config and add/remove as necessary, and will restart affected programs
update all Reload config and add/remove as necessary, and will restart affected programs
update <gname> [...] Update specific groups
mancode@manos:~$

注意:

  • 使用update命令,系统会重新读取配置文件。
  • 然后第一会重启配置文件变化过的原已经运行的程序,未变化的子进程保持原样不受影响。
  • 然后第二会启动新增加的子进程服务程序。

0x05:重新加载所有配置文件

1
$ sudo supervisorctl realod

1
2
3
mancode@manos:~$ supervisorctl help reload
reload Restart the remote supervisord.
mancode@manos:~$

注意:

  • 该命令重新加载所有配置文件
  • 该命令不会重启supervisord主进程
  • 该命令重启所有子进程

0x06:关闭supervisord

1
$ sudo supervisorctl shutdown

1
2
3
mancode@manos:~$ supervisorctl help shutdown
shutdown Shut the remote supervisord down.
mancode@manos:~$

0x07:清除日志

1
$ sudo supervisorctl clear

1
2
3
4
5
mancode@manos:~$ supervisorctl help clear
clear <name> Clear a process’s log files.
clear <name> <name> Clear multiple process’ log files
clear all Clear all process’ log files
mancode@manos:~$

supervisor管理平台

supervisor自带一个可视化的管理平台,我们可以通过浏览器访问管理平台,然后通过该管理平台查看进程状态、启动、停止后者重启进程。我们使用apt安装supervisor,默认配置文件中没有打开该项功能。我们在配置文件 /etc/supervisor/supervisord.conf 查找 inet_http_server 配置域,如果没有该配置域,我们在配置文件中添加如下内容:

1
2
3
4
[inet_http_server]
port=0.0.0.0:9001
username=admin
password=123

  • port 管理平台监听的地址和端口,可以自定义
  • username 登录管理平台的账号,可以自定义名称
  • password 登录管理平台的密码,可以自定义名称

修改的配置文件如下图所示:

修改配置文件之后,我们重新启动supervisord,使用如下命令:

1
$ sudo supervisorctl reload

supervisord重启之后,我们在其他主机的浏览器上访问该管理平台,如下图所示:

supervisor不能管理守护进程

我们要特别注意一点,supervisor不能去管理那些后台守护进程!(目前我还未发现解决方法)

关于这种情况,我们编写一个可以进入后台守护进程模式的服务,来进行验证。测试代码文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>
#include<fcntl.h>
#include<string.h>
#include<signal.h>
#include<errno.h>
#include<sys/stat.h>

#define EXIT_WITH_CLEAN(code) do { exit(unlink(Z_PID_FILE) == -1?1:code); } while(0);
#define EXIT_WITH_ERRNO(format, args...) do { \
printf(format":(%d) %s\n", ##args, errno, strerror(errno)); EXIT_WITH_CLEAN(1); \
} while(0)

char Z_CNF_FILE[512]="/etc/sptest/sptest.conf";
char Z_PID_FILE[512]="/var/run/sptest/sptest.pid";
char Z_LOG_FILE[512]="/tmp/sptest.log";
int B_SET_DEAMON =0;

void parse_opts(int argc, char **argv)
{
for (int c = 0; (c = getopt (argc, argv, "dc:p:f:")) != -1; )
{
switch (c)
{
case 'd':
B_SET_DEAMON = 1;
break;
case 'c':
snprintf(Z_CNF_FILE, sizeof(Z_CNF_FILE), "%s", optarg);
break;
case 'f':
snprintf(Z_LOG_FILE, sizeof(Z_LOG_FILE), "%s", optarg);
break;
case 'p':
snprintf(Z_PID_FILE, sizeof(Z_PID_FILE), "%s", optarg);
break;
case '?':
fprintf (stderr, "Unknown option character `\\x%x'.\n", optopt);
return ;
default:
abort();
}
}
}

void write_pidfile(const char *zPidFile, pid_t iPid)
{
char zPidPath[512];
memset(zPidPath, 0, sizeof(zPidPath));

int iLen = strrchr(zPidFile, '/') - zPidFile;

snprintf(zPidPath, sizeof(zPidPath), "%.*s", iLen, zPidFile);

if (access(zPidPath, F_OK) != 0)
{
if (mkdir(zPidPath, S_IRWXU|S_IRWXG|S_IROTH|S_IXOTH) != 0)
{
EXIT_WITH_ERRNO("Can't create dir %s for pidfile", zPidPath);
}
}

FILE *fp;
if ((fp = fopen(Z_PID_FILE, "w")) == NULL)
{
EXIT_WITH_ERRNO("Can't open pidfile %s for writing", Z_PID_FILE);
}

fprintf(fp, "%d\n", iPid);
fclose(fp);
}

void set_daemon(void)
{
pid_t pid;
pid = fork();
if (pid == -1)
EXIT_WITH_ERRNO("fork error");
if (pid > 0)
exit(EXIT_SUCCESS);

if (setsid() == -1)
EXIT_WITH_ERRNO("SETSID ERROR");

chdir("/");

close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY); /* 0 = stdin */
open("/dev/null", O_WRONLY); /* 1 = stdout */
open("/dev/null", O_RDWR); /* 2 = stderr */

umask(0);

return;
}

void sig_handle(int sig)
{
printf("sig_handle:%d\n", sig);

EXIT_WITH_CLEAN(0);
}

int main(int argc, char **argv)
{
parse_opts(argc, argv);

if (B_SET_DEAMON == 1)
{
set_daemon();
}

write_pidfile(Z_PID_FILE, getpid());

signal(SIGUSR1, sig_handle);

{
int iFile = open(Z_LOG_FILE, O_WRONLY|O_CREAT|O_APPEND, 0644);
if (iFile == -1)
{
EXIT_WITH_ERRNO("Can't open logfile %s for writing", Z_LOG_FILE);
}

for(time_t t = time(0); 1; t = time(0))
{
char *zBuff = asctime(localtime(&t));
write(iFile, zBuff, strlen(zBuff));

sleep(60);
}

close(iFile);
}

return 0;
}

使用如下命令编译服务:

1
$ cc -o sptest sptest.c

被管理程序的配置文件,如下所示(其中启动服务所用-d选项表示进入后台守护进程模式):

1
2
[program:sptest]
command=/home/mancode/dev/sp/sptest -d -p /var/run/sptest/sptest.pid -f /tmp/sptest.log

如上工作准备完毕之后,我们使用 sudo supervisorctl update 通知supervisord重新加载配置文件,然后稍后我们查看下服务状态:

上图显示,我们的后台守护进程sptest服务,因为退出太快而被设置为了FATAL状态。我们再看下此时 supervisord 的日志:

看启动日志,我们也能看到supervisor尝试4次启动sptest,但守护进程的特点就是服务启动后会马上fork第二个进程,而第一个进程退出,但supervisor的机制就是监控第一个进程号是否存在来判断服务启动成功与否,以及后续依据第一个进程号来判断进程是否崩溃。可见对于守护进程,目前supervisor还掌控不了,至少我没有找到解决方法。

那这个时候,服务到底启动没有?我们ps下进程看看:

显而易见,服务已经启动,而且被启动了4个实例。

这种情况,我觉得也有办法解决:

  • 假如子进程不是强制进入守护模式,supervisor本来就是以后台进程的方式启动的子进程,那么子进程启动时不要指定使用自己的守护进程模式,让supervisor来负责后台守护即可。
  • 期待官方的改进,比如上述的测试实例,程序内部写了pidfile,那supervisor的配置文件中,加个pidfile的配置项。

    然后supervisor去监控该pidfile中的pid,就能解决上述问题。

参考资料