由于业务开发需要,需要对数据库代理进行研究,在研究 MySQL Proxy 实现原理的过程中,对一些功能点进行了分析总结。本文主要讲解下 MySQL Proxy 的 daemon 和 keepalive 功能实现原理。
MySQL Proxy 是数据库代理实现中的一种,提供了 MySQL server 与 MySQL client 之间的通信功能。由于 MySQL Proxy 使用的是 MySQL 网络协议,故其可以在不做任何修改的情况下,配合任何符合该协议的且与 MySQL 兼容的客户端一起使用。在最基本的配置下,MySQL Proxy 仅仅是简单地将自身置于服务器和客户端之间,负责将 query 从客户端传递到服务器,再将来自服务器的应答返回给相应的客户端。在高级配置下,MySQL Proxy 可以用来监视和改变客户端和服务器之间的通信。查询注入(query interception) 功能允许你按需要添加性能分析命令 (profiling) ,且可以通过 Lua 脚本语言对注入的命令进行脚本化控制。
本文不讨论 MySQL Proxy 作为数据库代理在功能上和实践中的优劣,而是着重讲述其源码实现中的两个功能点:daemon 功能和 keepalive 功能。
通过命令行启动 MySQL Proxy 时经常会用到如下两个配置项:--daemon 和 –keepalive 。在其相应的帮助命令中的解释为:
- --daemon Start in daemon-mode
- --keepalive try to restart the proxy if it crashed
keepalive
功能从字面理解为提供保活功能,
daemon
为守护进程。但
daemon
的功能究竟是如何定义的呢?
APUE 上的定义如下:
守护进程也称
daemon 进程,是生存期较长的一种进程,它们常常在系统自举时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是再后台运行的。
【Daemon 功能实现】
首先,讲解下 daemon 实现的基本原则。事实上,编写守护进程程序时是存在一些基本规则的,目的是防止产生不需要的交互作用(比如与终端的交互)。规则如下:
- 调用 umask 将文件模式创建屏蔽字设置为 0 。原因:防止继承得来的文件模式创建屏蔽字会拒绝设置某些权限的情况。
- 调用 fork ,然后使父进程退出(exit)。原因:第一,令启动 daemon 进程的 shell 认为命令已经执行完毕;第二,令产生的子进程不是其所在进程组的组长。
- 调用 setsid 以创建一个新会话。原因:使调用进程,第一,成为新会话的首进程;第二,成为新进程组的组长进程;第三,没有控制终端(在基于 System V 的系统中可以通过 fork 两次来达到防止取得控制终端的效果的,其不再需要下面的规则6)。
- 将当前工作目录更改为根目录。原因:防止出现不能 umount 的问题。
- 关闭不再需要的文件描述符。原因:令守护进程不再持有从父进程继承来的某些文件描述符。
- 某些守护进程打开 /dev/null 使其具有文件描述符0、1和2。原因:防止守护进程与终端设备相关联。
有了上面的原则,现在对照下 MySQL Proxy 中的代码:
02 |
* start the app in the background |
06 |
void chassis_unix_daemonize(void) { |
08 |
g_assert_not_reached(); /* shouldn't be tried to be called on win32 */ |
11 |
signal(SIGTTOU, SIG_IGN); |
14 |
signal(SIGTTIN, SIG_IGN); |
17 |
signal(SIGTSTP, SIG_IGN); |
19 |
if (fork() != 0) exit(0); |
21 |
if (setsid() == -1) exit(0); |
23 |
signal(SIGHUP, SIG_IGN); |
25 |
if (fork() != 0) exit(0); |
从上面的实现代码中,可以看出以下几点:
- 代码执行的先后顺序有的是必须的(如setsid 之前的 fork),有的不是必须的(如 umask 放在最后执行)。
- 实现中使用了两次 fork ,为 System V 中理念。
- 在 setsid 和第二次 fork 之间插入了 signal 处理,用于对 SIGHUP 执行 SIG_IGN 处理。
在上述 6 条 daemon 编程规则中没有提到 signal 处理的问题,那么针对 SIGHUP 的处理代表的是什么意思呢?还是参阅 APUE :
如果终端接口检测到一个连接断开,则将此信号发送给与该中断相关的控制进程(会话首进程)。仅当中断的 CLOCAL 标志没有设置时,上述条件下才产生此信号。
有别于由终端正常产生的信号(如中断、退出和挂起)-- 这些信号总是传递给前台进程组 -- SIGHUP信号可以发送到位于后台运行的会话首进程。SIGHUP 信号的默认处理动作是终止当前进程。通常会使用该信号来通知守护进程,以重新读取它们的配置文件,因为守护进程不会有控制终端,而且通常决不会收到这种信号。
从上面这段文字可以看出,这里增加了 signal 信号处理的原因是,在 setsid 和第二次 fork 之间,当前的子进程仍旧是会话首进程,有可能会在收到 SIGHUP 信号时终止,所以这里通过设置 SIG_IGN 进行忽略。
至此,一个 daemon-mode 的守护进程就启动了。
【Keepalive 功能实现】
下面讲解下 keepalive 功能的实现。简单的说,MySQL Proxy 的服务器编程模型为:1个 daemon 父进程 + 一个工作子进程(在其中可以再启动 n 个工作线程)。而 keepalive 的功能就是要求 daemon 进程在发现工作子进程被异常终结后,能够重新启动该子进程。
首先讲下 daemon 进程中的实现代码,其主要实现的功能为:
- fork 一个工作子进程,并通过 waitpid 阻塞方式获取子进程的退出状态信息,若子进程为正常退出,即 exit-code 为 0 时,则守护进程也正常退出;若信号导致子进程正常退出,则守护进程同样正常退出;若信号导致子进程异常退出,则在延时2s后,由daemon进程重新启动子进程;对于 SIGSTOP 信号按照系统默认处理;
- 将发送给 daemon 进程的 SIGINT/SIGTERM/SIGHUP/SIGUSR1/SIGUSR2 信号通过信号处理函数 chassis_unix_signal_forward() 转发到 daemon 所在进程组中的所有进程(这里就是为了发送给子进程)。
002 |
* forward the signal to the process group, but not us |
004 |
static void chassis_unix_signal_forward(int sig) { |
006 |
g_assert_not_reached(); /* shouldn't be tried to be called on win32 */ |
008 |
signal(sig, SIG_IGN); /* we don't want to create a loop here */ |
015 |
* keep the ourself alive |
017 |
* if we or the child gets a SIGTERM, we quit too |
018 |
* on everything else we restart it |
020 |
int chassis_unix_proc_keepalive(int *child_exit_status) { |
022 |
g_assert_not_reached(); /* shouldn't be tried to be called on win32 */ |
023 |
return 0; /* for VC++, to silence a warning */ |
026 |
pid_t child_pid = -1; |
028 |
/* we ignore SIGINT and SIGTERM and just let it be forwarded to the child instead |
029 |
* as we want to collect its PID before we shutdown too |
031 |
* the child will have to set its own signal handlers for this |
035 |
/* try to start the children */ |
042 |
g_debug("%s: we are the child: %d", |
046 |
} else if (pid < 0) { |
049 |
g_critical("%s: fork() failed: %s (%d)", |
056 |
/* we are the angel, let's see what the child did */ |
057 |
g_message("%s: [angel] we try to keep PID=%d alive", |
061 |
/* forward a few signals that are sent to us to the child instead */ |
062 |
signal(SIGINT, chassis_unix_signal_forward); |
063 |
signal(SIGTERM, chassis_unix_signal_forward); |
064 |
signal(SIGHUP, chassis_unix_signal_forward); |
065 |
signal(SIGUSR1, chassis_unix_signal_forward); |
066 |
signal(SIGUSR2, chassis_unix_signal_forward); |
073 |
if (child_pid != -1) { |
074 |
struct rusage rusage; |
078 |
g_debug("%s: waiting for %d", |
082 |
exit_pid = wait4(child_pid, &exit_status, 0, &rusage); |
084 |
memset(&rusage, 0, sizeof(rusage)); /* make sure everything is zero'ed out */ |
085 |
exit_pid = waitpid(child_pid, &exit_status, 0); |
087 |
g_debug("%s: %d returned: %d", |
092 |
if (exit_pid == child_pid) { |
093 |
/* our child returned, let's see how it went */ |
094 |
if (WIFEXITED(exit_status)) { |
095 |
g_message("%s: [angel] PID=%d exited normally with exit-code = %d (it used %ld kBytes max)", |
098 |
WEXITSTATUS(exit_status), |
099 |
rusage.ru_maxrss / 1024); |
100 |
if (child_exit_status) *child_exit_status = WEXITSTATUS(exit_status); |
102 |
} else if (WIFSIGNALED(exit_status)) { |
104 |
/* our child died on a signal |
106 |
* log it and restart */ |
108 |
g_critical("%s: [angel] PID=%d died on signal=%d (it used %ld kBytes max) ... waiting 3min before restart", |
111 |
WTERMSIG(exit_status), |
112 |
rusage.ru_maxrss / 1024); |
115 |
* to make sure we don't loop as fast as we can, sleep a bit between |
119 |
signal(SIGINT, SIG_DFL); |
120 |
signal(SIGTERM, SIG_DFL); |
121 |
signal(SIGHUP, SIG_DFL); |
122 |
while (time_towait > 0) time_towait = sleep(time_towait); |
126 |
} else if (WIFSTOPPED(exit_status)) { |
128 |
g_assert_not_reached(); |
130 |
} else if (-1 == exit_pid) { |
131 |
/* EINTR is ok, all others bad */ |
132 |
if (EINTR != errno) { |
133 |
/* how can this happen ? */ |
134 |
g_critical("%s: wait4(%d, ...) failed: %s (%d)", |
143 |
g_assert_not_reached(); |
其次讲解工作子进程中的实现代码,其主要实现的功能为:
通过 libevent 提供的接口设置对 SIGTERM/SIGINT/SIGHUP 三个信号的处理,通过 libevent 的信号处理方式可以做到,将I/O事件、Timer事件和信号事件统一按event-driven方式进行处理的目的,这样,一旦工作子进程检测到相应的信号,就会将控制变量signal_shutdown设置为1,进而令循环终止。
01 |
void chassis_set_shutdown_location(const gchar* location) { |
02 |
if (signal_shutdown == 0) g_message("Initiating shutdown, requested from %s", (location != NULL ? location : "signal handler")); |
06 |
gboolean chassis_is_shutdown() { |
07 |
return signal_shutdown == 1; |
10 |
static void sigterm_handler(int G_GNUC_UNUSED fd, short G_GNUC_UNUSED event_type, void G_GNUC_UNUSED *_data) { |
11 |
chassis_set_shutdown_location(NULL); |
14 |
static void sighup_handler(int G_GNUC_UNUSED fd, short G_GNUC_UNUSED event_type, void *_data) { |
15 |
chassis *chas = _data; |
17 |
g_message("received a SIGHUP, closing log file"); /* this should go into the old logfile */ |
19 |
chassis_log_set_logrotate(chas->log); |
21 |
g_message("re-opened log file after SIGHUP"); /* ... and this into the new one */ |
001 |
int chassis_mainloop(void *_chas) { |
002 |
chassis *chas = _chas; |
004 |
struct event ev_sigterm, ev_sigint; |
006 |
struct event ev_sighup; |
008 |
chassis_event_thread_t *mainloop_thread; |
010 |
/* redirect logging from libevent to glib */ |
011 |
event_set_log_callback(event_log_use_glib); |
014 |
/* add a event-handler for the "main" events */ |
015 |
mainloop_thread = chassis_event_thread_new(); |
016 |
chassis_event_threads_init_thread(chas->threads, mainloop_thread, chas); |
017 |
chassis_event_threads_add(chas->threads, mainloop_thread); |
019 |
chas->event_base = mainloop_thread->event_base; /* all global events go to the 1st thread */ |
021 |
g_assert(chas->event_base); |
024 |
/* setup all plugins all plugins */ |
025 |
for (i = 0; i < chas->modules->len; i++) { |
026 |
chassis_plugin *p = chas->modules->pdata[i]; |
028 |
g_assert(p->apply_config); |
029 |
if (0 != p->apply_config(chas, p->config)) { |
030 |
g_critical("%s: applying config of plugin %s failed", |
037 |
* drop root privileges if requested |
041 |
struct passwd *user_info; |
042 |
uid_t user_id= geteuid(); |
044 |
/* Don't bother if we aren't superuser */ |
046 |
g_critical("can only use the --user switch if running as root"); |
050 |
if (NULL == (user_info = getpwnam(chas->user))) { |
051 |
g_critical("unknown user: %s", chas->user); |
055 |
if (chas->log->log_filename) { |
057 |
if (-1 == chown(chas->log->log_filename, user_info->pw_uid, user_info->pw_gid)) { |
058 |
g_critical("%s.%d: chown(%s) failed: %s", |
060 |
chas->log->log_filename, |
067 |
setgid(user_info->pw_gid); |
068 |
setuid(user_info->pw_uid); |
069 |
g_debug("now running as user: %s (%d/%d)", |
076 |
signal_set(&ev_sigterm, SIGTERM, sigterm_handler, NULL); |
077 |
event_base_set(chas->event_base, &ev_sigterm); |
078 |
signal_add(&ev_sigterm, NULL); |
080 |
signal_set(&ev_sigint, SIGINT, sigterm_handler, NULL); |
081 |
event_base_set(chas->event_base, &ev_sigint); |
082 |
signal_add(&ev_sigint, NULL); |
085 |
signal_set(&ev_sighup, SIGHUP, sighup_handler, chas); |
086 |
event_base_set(chas->event_base, &ev_sighup); |
087 |
if (signal_add(&ev_sighup, NULL)) { |
088 |
g_critical("%s: signal_add(SIGHUP) failed", G_STRLOC); |
092 |
if (chas->event_thread_count < 1) chas->event_thread_count = 1; |
094 |
/* create the event-threads |
096 |
* - dup the async-queue-ping-fds |
097 |
* - setup the events notification |
099 |
for (i = 1; i < (guint)chas->event_thread_count; i++) { /* we already have 1 event-thread running, the main-thread */ |
100 |
chassis_event_thread_t *event_thread; |
102 |
event_thread = chassis_event_thread_new(); |
103 |
chassis_event_threads_init_thread(chas->threads, event_thread, chas); |
104 |
chassis_event_threads_add(chas->threads, event_thread); |
107 |
/* start the event threads */ |
108 |
if (chas->event_thread_count > 1) { |
109 |
chassis_event_threads_start(chas->threads); |
113 |
* handle signals and all basic events into the main-thread |
115 |
* block until we are asked to shutdown |
117 |
chassis_event_thread_loop(mainloop_thread); |
119 |
signal_del(&ev_sigterm); |
120 |
signal_del(&ev_sigint); |
122 |
signal_del(&ev_sighup); |
【测试】
经过了上述源码分析,下面进行一些实验对其进行检验。
1.启动带 keepalive 功能的 mysql-proxy。
1 |
[root@Betty data]# mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
3 |
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND |
4 |
1 16766 16765 16765 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
5 |
16766 16767 16765 16765 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
2.向 daemon进程发送 INT 信号。
1 |
[root@Betty ~]# kill -INT 16766 |
3. MySQL Proxy日志显示内容:
1 |
2013-03-19 19:31:38: (message) Initiating shutdown, requested from signal handler |
2 |
2013-03-19 19:31:39: (message) shutting down normally, exit code is: 0 |
3 |
2013-03-19 19:31:39: (debug) chassis-unix-daemon.c:167: 16767 returned: 16767 |
4 |
2013-03-19 19:31:39: (message) chassis-unix-daemon.c:176: [angel] PID=16767 exited normally with exit-code = 0 (it used 1 kBytes max) |
5 |
2013-03-19 19:31:39: (message) Initiating shutdown, requested from mysql-proxy-cli.c:606 |
6 |
2013-03-19 19:31:39: (message) shutting down normally, exit code is: 0 |
可以看出,父子进程均退出。因为其信号处理函数会将全局变量 signal_shutdown 设置为 1,从而导致子进程退出 loop 循环,而处于 waitpid 状态的父进程获得的子进程的退出状态为 child_exit_status = 0,进程令父进程也会正常退出执行。
4.重复上述动作,但是改为向子进程发送 INT 信号。
2 |
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND |
3 |
1 16872 16871 16871 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
4 |
16872 16873 16871 16871 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
6 |
[root@Betty ~]# kill -INT 16873 |
日志内容如下,完全相同。
1 |
2013-03-19 20:03:49: (message) Initiating shutdown, requested from signal handler |
2 |
2013-03-19 20:03:50: (message) shutting down normally, exit code is: 0 |
3 |
2013-03-19 20:03:50: (debug) chassis-unix-daemon.c:167: 16873 returned: 16873 |
4 |
2013-03-19 20:03:50: (message) chassis-unix-daemon.c:176: [angel] PID=16873 exited normally with exit-code = 0 (it used 1 kBytes max) |
5 |
2013-03-19 20:03:50: (message) Initiating shutdown, requested from mysql-proxy-cli.c:606 |
6 |
2013-03-19 20:03:50: (message) shutting down normally, exit code is: 0 |
5. 同样的实验(对子进程和和父进程分别实验一次),只是将信号变为 -TERM ,结果和上面的完全相同(因为代码中对这两个信号的处理方式完全相同)。
6. 同样的实验(对子进程和和父进程分别实验一次),只是将信号变为 -HUP ,结果如下:
1 |
2013-03-19 20:10:03: (message) received a SIGHUP, closing log file |
2 |
2013-03-19 20:10:03: (message) re-opened log file after SIGHUP |
上述打印出现在子进程的 HUP 信号处理函数中。该函数仅对日志设置了 rotate_logs = true 标识,并没有设置 signal_shutdown = 1 ,所以子进程不会结束,父进程也不会结束。
7. 同样的实验,将信号变为 -KILL ,向子进程发送:
2 |
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND |
4 |
1 16902 16901 16901 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
5 |
16902 16903 16901 16901 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
7 |
[root@Betty ~]# kill -KILL 16903 |
输出日志如下:
01 |
2013-03-19 20:09:38: (debug) chassis-unix-daemon.c:121: we are the child: 16903 |
02 |
2013-03-19 20:09:38: (critical) plugin proxy 0.8.3 started |
03 |
2013-03-19 20:09:38: (debug) max open file-descriptors = 1024 |
04 |
2013-03-19 20:09:38: (message) proxy listening on port 172.16.40.60:4040 |
05 |
2013-03-19 20:09:38: (message) added read/write backend: 172.16.40.60:12345 |
06 |
2013-03-19 20:09:38: (message) chassis-unix-daemon.c:136: [angel] we try to keep PID=16903 alive |
07 |
2013-03-19 20:09:38: (debug) chassis-unix-daemon.c:157: waiting for 16903 |
10 |
2013-03-19 20:31:36: (debug) chassis-unix-daemon.c:167: 16903 returned: 16903 |
11 |
2013-03-19 20:31:36: (critical) chassis-unix-daemon.c:189: [angel] PID=16903 died on signal=9 (it used 1 kBytes max) ... waiting 3min before restart |
12 |
2013-03-19 20:31:38: (debug) chassis-unix-daemon.c:121: we are the child: 16947 |
13 |
2013-03-19 20:31:38: (critical) plugin proxy 0.8.3 started |
14 |
2013-03-19 20:31:38: (debug) max open file-descriptors = 1024 |
15 |
2013-03-19 20:31:38: (message) proxy listening on port 172.16.40.60:4040 |
16 |
2013-03-19 20:31:38: (message) added read/write backend: 172.16.40.60:12345 |
17 |
2013-03-19 20:31:38: (message) chassis-unix-daemon.c:136: [angel] we try to keep PID=16947 alive |
18 |
2013-03-19 20:31:38: (debug) chassis-unix-daemon.c:157: waiting for 16947 |
从日志和代码上都可以分析得出原因:由于 -KILL 信号是无法获取或者忽略的,所以当发送该信号给子进程后,子进程将被杀死,退出状态为 died on signal=9 ,此时父进程会执行 restart 子进程的操作。
此时重新查看进程信息:
2 |
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND |
3 |
1 16902 16901 16901 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
4 |
16902 16947 16901 16901 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
若向父进程发送 -KILL 信号,那么父进程将被直接杀死,子进程被 init 收留,而 init 进程根本不会理会是否需要 keepalive 子进程的问题,所以此时再向子进程发送 -KILL ,子进程被杀死后,不会重新被启动。
8. 同样的实验,将信号变为-STOP,向子进程发送:
01 |
[root@Betty ~]# ps ajx |
02 |
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND |
04 |
1 16977 16976 16976 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
05 |
16977 16978 16976 16976 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
07 |
[root@Betty ~]# kill -STOP 16978 |
09 |
1 16977 16976 16976 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
10 |
16977 16978 16976 16976 ? -1 T 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
13 |
[root@Betty ~]# kill -CONT 16978 |
15 |
1 16977 16976 16976 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
16 |
16977 16978 16976 16976 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
18 |
[root@Betty ~]# kill -STOP 16977 |
20 |
1 16977 16976 16976 ? -1 T 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
21 |
16977 16978 16976 16976 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
23 |
[root@Betty ~]# kill -CONT 16977 |
25 |
1 16977 16976 16976 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
26 |
16977 16978 16976 16976 ? -1 S 0 0:00 mysql-proxy --defaults-file=/etc/mysql-proxy.cnf |
出现上述结果的原因,是信号 -STOP 同样不可捕获和忽略,而进程对该信号的默认处理方式为暂停进程(可以从进程状态标志看出来)。同时在代码中,父进程在获得子进程状态处于暂停时,没有做任何特别处理,只是重新调用 waitpid 继续获取子进程的状态而已。
【总结】
daemon 功能和 keepalive 功能属于服务器程序开发过程中经常要面对到的问题,本文提供了上述功能的一种实现方式。通过学习开源代码,可以有机会接触到一些经典的处理问题的方法,通过对一些问题的深入了解,能够进一步完善自身的知识体系,强化对一些知识的理解。最后引用一位大师的名言:源码面前,了无秘密。祝玩的开心!
====================================
再贴一个 daemonize 的实现进行对比(取自 memcached-1.4.14):
01 |
int daemonize(int nochdir, int noclose) |
24 |
if (noclose == 0 && (fd = open("/dev/null", O_RDWR, 0)) != -1) { |
25 |
if(dup2(fd, STDIN_FILENO) < 0) { |
29 |
if(dup2(fd, STDOUT_FILENO) < 0) { |
30 |
perror("dup2 stdout"); |
33 |
if(dup2(fd, STDERR_FILENO) < 0) { |
34 |
perror("dup2 stderr"); |
38 |
if (fd > STDERR_FILENO) { |