首页UC › 服务器开发之 Daemon 和 Keepalive

服务器开发之 Daemon 和 Keepalive

由于业务开发需要,需要对数据库代理进行研究,在研究 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 实现的基本原则。事实上,编写守护进程程序时是存在一些基本规则的,目的是防止产生不需要的交互作用(比如与终端的交互)。规则如下:

  1. 调用 umask 将文件模式创建屏蔽字设置为 0 原因防止继承得来的文件模式创建屏蔽字会拒绝设置某些权限的情况。
  2. 调用 fork ,然后使父进程退出(exit)。原因第一,令启动 daemon 进程的 shell 认为命令已经执行完毕;第二,令产生的子进程不是其所在进程组的组长。
  3. 调用 setsid 以创建一个新会话。原因使调用进程,第一,成为新会话的首进程;第二,成为新进程组的组长进程;第三,没有控制终端(在基于System V 的系统中可以通过 fork 两次来达到防止取得控制终端的效果的,其不再需要下面的规则6)。
  4. 将当前工作目录更改为根目录。原因防止出现不能 umount 的问题。
  5. 关闭不再需要的文件描述符。原因令守护进程不再持有从父进程继承来的某些文件描述符。
  6. 某些守护进程打开 /dev/null 使其具有文件描述符012原因防止守护进程与终端设备相关联。

有了上面的原则,现在对照下 MySQL Proxy 中的代码:

从上面的实现代码中,可以看出以下几点:

  • 代码执行的先后顺序有的是必须的(如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 所在进程组中的所有进程(这里就是为了发送给子进程)。

其次讲解工作子进程中的实现代码,其主要实现的功能为:

通过 libevent 提供的接口设置对 SIGTERM/SIGINT/SIGHUP 三个信号的处理,通过 libevent 的信号处理方式可以做到,将I/O事件、Timer事件和信号事件统一按event-driven方式进行处理的目的,这样,一旦工作子进程检测到相应的信号,就会将控制变量signal_shutdown设置为1,进而令循环终止。

【测试】

经过了上述源码分析,下面进行一些实验对其进行检验。

1.启动带 keepalive 功能的 mysql-proxy。

2.向 daemon进程发送 INT 信号。

3. MySQL Proxy日志显示内容:

可以看出,父子进程均退出。因为其信号处理函数会将全局变量 signal_shutdown 设置为 1,从而导致子进程退出 loop 循环,而处于 waitpid 状态的父进程获得的子进程的退出状态为 child_exit_status = 0 ,进而令父进程也会正常退出执行。

4.重复上述动作,但是改为向子进程发送 INT 信号。

日志内容如下,完全相同。

5. 同样的实验(对子进程和和父进程分别实验一次),只是将信号变为 -TERM ,结果和上面的完全相同(因为代码中对这两个信号的处理方式完全相同)。

6. 同样的实验(对子进程和和父进程分别实验一次),只是将信号变为 -HUP ,结果如下:

上述打印出现在子进程的 HUP 信号处理函数中。该函数仅对日志设置了 rotate_logs = true 标识,并没有设置 signal_shutdown = 1 ,所以子进程不会结束,父进程也不会结束。

7. 同样的实验,将信号变为 -KILL ,向子进程发送:

输出日志如下:

从日志和代码上都可以分析得出原因:由于 -KILL 信号是无法获取或者忽略的,所以当发送该信号给子进程后,子进程将被杀死,退出状态为 died on signal=9 ,此时父进程会执行 restart 子进程的操作。

此时重新查看进程信息:

若向父进程发送 -KILL 信号,那么父进程将被直接杀死,子进程被 init 收留,而 init 进程根本不会理会是否需要 keepalive 子进程的问题,所以此时再向子进程发送 -KILL ,子进程被杀死后,不会重新被启动。

8. 同样的实验,将信号变为-STOP,向子进程发送:

出现上述结果的原因,是信号 -STOP 同样不可捕获和忽略,而进程对该信号的默认处理方式为暂停进程(可以从进程状态标志看出来)。同时在代码中,父进程在获得子进程状态处于暂停时,没有做任何特别处理,只是重新调用 waitpid 继续获取子进程的状态而已。

【总结】
daemon 功能和 keepalive 功能属于服务器程序开发过程中经常要面对到的问题,本文提供了上述功能的一种实现方式。通过学习开源代码,可以有机会接触到一些经典的处理问题的方法,通过对一些问题的深入了解,能够进一步完善自身的知识体系,强化对一些知识的理解。最后引用一位大师的名言:源码面前,了无秘密。祝玩的开心!

====================================

再贴两个 daemonize 的实现进行对比(取自 memcached-1.4.14):

(下面代码取自 Twemproxy)

发表评论