Chinaunix

标题: SPProcPool: Unix/Linux 上的进程池服务器框架(增加类似 apache 的 worker模型) [打印本页]

作者: iunknown    时间: 2007-12-09 11:25
标题: SPProcPool: Unix/Linux 上的进程池服务器框架(增加类似 apache 的 worker模型)
SPProcPool 是一个 linux/unix 平台上的进程池服务器框架,使用 c++ 实现。主要包含了几种不同类型的进程池的实现:
一个基于 Leader/Follower 模式的服务器端进程池(类似 apache 的 prefork 模型);
一个基于文件句柄传递的服务器端进程池;
一个用于非服务器端的,能够在多线程或事件驱动环境下使用的进程池。

主页:http://code.google.com/p/spprocpool/
下载:spprocpool

关于进程池服务器框架,在《unix网络编程(第二版)》的第 27 章,详细地描述了多种不同的实现方式。
基于 Leader/Follower 模式的实现:27.6 TCP 预先派生子进程服务器程序,accept 无上锁保护
基于文件句柄传递:27.9 TCP 预先派生子进程服务器程序,传递描述字

关于非服务器端的,能够在多线程或事件驱动环境下使用的进程池,这里做一个比较详细的说明。

多线程的好处是各个线程能够共享一个地址空间,因此对一些需要全局排序来调度的任务,使用多线程可以比较方便地实现。比如类似 postfix/qmgr 的模块,如果使用多线程的话,那么所有的邮件能够在一个优先队列中排队,按队列顺序进行投递;如果投递失败了,那么重新插入队列。
但另一方面,如果具体的任务处理部分已经有了实现,但不是线程安全的,这种问题要怎么来解决呢?

一个最直观的解决方法是每个任务直接 fork 一次。但是这种做法有几个细节问题需要认真考虑:
1.在子进程中如何把结果返回给调度进程?常见的方法是使用 pipe
2.每个任务 fork 一次,估计很多人的第一反应就是性能会怎么样?
3.如果调度进程中,除了负责 fork 的线程,还有其他的线程存在,那么就存在 fork-with-thread 的问题。
>>具体的内容可以参考:http://www.opengroup.org/onlinepubs/000095399/functions/fork.html

针对上面存在的问题,在每个任务直接 fork 一次的基础上,做了一些改进,就形成了这样的一个进程池实现:
1.在前面的方案中,存在调度进程(Sched)和工作进程(Worker)这两类进程。
>>为了避免 fork-with-thread 的问题,再增加一类管理进程(Manager)。管理进程的主要职责就是负责 fork 工作进程。
2.通常 Manager 是由 Sched fork 出来的,它们之间存在一条通信的 pipe (MgrPipe) 。
>>创建一个新的工作进程的流程如下:Sched 创建一个 pipe (WorkerPipe),把其中的一端用 send_fd 的方法发送给 Manager,
>>然后 Manager fork 一个 Worker 出来,并且把 WorkerPipe 传递给 Worker 。这样就在 Sched 和 Worker 之间建立了一个 Pipe 。
3.Worker 在被 fork 出来之后,通常就阻塞在读 WorkerPipe 上面。Sched 通过 WorkerPipe 发送任务给 Worker 。
>>Worker 完成任务之后,通过 WorkerPipe 发送结果给 Sched 。Worker 可以不断地重复这个过程,这样就达到了一个池的效果。
4.对于使用 libevent 这类事件驱动类型的程序,这个进程池也能方便地被调用。
>>因为 Worker 曝露出来的是一个 PipeFd,能够方便地加入到 libevent 的事件循环中。这类事件驱动类的程序,
>>通常使用单线程实现,当具体的任务处理可能需要耗费比较长时间的时候,就需要使用多线程或者多进程来辅助了。

[ 本帖最后由 iunknown 于 2008-1-4 21:25 编辑 ]
作者: queue    时间: 2007-12-11 18:57
原帖由 iunknown 于 2007-12-9 11:25 发表
SPProcPool 是一个能够在多线程或事件驱动环境下使用的进程池实现。
主页:http://code.google.com/p/spprocpool/
下载:spprocpool

多线程的好处是各个线程能够共享一个地址空间,因此对一些需要全局排序 ...


没有实现类似之前的  SPServer 那样的一个框架吗?在目前的基础上,要实现类似 SPServer 那样的一个框架,应该不会很复杂了。
按目前的结构,继续使用 send_fd 的方式应该是最简单的做法。类似 UNP 的 27.9 TCP 预先派生子进程服务器程序,传递描述字。
不过 UNP 中提到这种方法的性能比起各个子进程直接 accept 差。
作者: iunknown    时间: 2007-12-12 23:17
原帖由 queue 于 2007-12-11 18:57 发表


没有实现类似之前的  SPServer 那样的一个框架吗?在目前的基础上,要实现类似 SPServer 那样的一个框架,应该不会很复杂了。
按目前的结构,继续使用 send_fd 的方式应该是最简单的做法。类似 UNP 的 27. ...



正好最近重新看了 UNP 的那几章。也计划实现一个多进程的 Server 框架。初步看了 apache 的做法,好复杂,还没看的出大概。
如果用 send_fd 的方式来做的话,的确比较简单,但是看 UNP 上的说法,慢的不是一点二点,是慢很多啊。
不过也可以试试看,可以对比一下,看相差有多远。
作者: agaric    时间: 2007-12-12 23:18
先顶,后看。
作者: iunknown    时间: 2007-12-14 14:36
原帖由 iunknown 于 2007-12-12 23:17 发表



正好最近重新看了 UNP 的那几章。也计划实现一个多进程的 Server 框架。初步看了 apache 的做法,好复杂,还没看的出大概。
如果用 send_fd 的方式来做的话,的确比较简单,但是看 UNP 上的说法,慢的不 ...


上 google 做了一下查找,找到两份关于 prefork 的文章,写得很深入

Secure Pre-forking - A Pattern for Performance and Security
Multitasking server architectures
作者: iunknown    时间: 2007-12-16 19:58
原帖由 queue 于 2007-12-11 18:57 发表


没有实现类似之前的  SPServer 那样的一个框架吗?在目前的基础上,要实现类似 SPServer 那样的一个框架,应该不会很复杂了。
按目前的结构,继续使用 send_fd 的方式应该是最简单的做法。类似 UNP 的 27. ...



用 send_fd 的方法实现了一个 server 框架。
http://spprocpool.googlecode.com/files/spprocpool-0.2.src.tar.gz

为了与 UNP 27 章的其他类型 server 对比,实现了类似的一个测试案例 testinetserver 。
同时为了方便测试,把 UNP 的测试 client 也放入包里面了,改名为 testinetclient 。
UNP 书上列出的数据是指 server 端通过 getrusage 得到的时间统计,而不是 client 端统计到的响应时间。
为了更直观的观察,在 testinetclient  中加入了响应时间的统计。


  1. class SP_ProcUnpService : public SP_ProcInetService {
  2. public:
  3.   SP_ProcUnpService() {}
  4.   virtual ~SP_ProcUnpService() {}

  5.   virtual void handle( int sockfd ) {
  6.     int ntowrite;
  7.     ssize_t nread;
  8.     char line[MAXLINE], result[MAXN];

  9.     for ( ; ; ) {
  10.       if ( (nread = read(sockfd, line, MAXLINE)) == 0) {
  11.         return;   /* connection closed by other end */
  12.       }                 

  13.       /* line from client specifies #bytes to write back */
  14.       ntowrite = atol(line);
  15.       if ((ntowrite <= 0) || (ntowrite > MAXN)) {
  16.         syslog( LOG_WARNING, "WARN: client request for %d bytes", ntowrite);
  17.         exit( -1 );
  18.       }

  19.       SP_ProcPduUtils::writen(sockfd, result, ntowrite);
  20.     }
  21.   }
  22. };

  23. class SP_ProcUnpServiceFactory : public SP_ProcInetServiceFactory {
  24. public:
  25.   SP_ProcUnpServiceFactory() {}
  26.   virtual ~SP_ProcUnpServiceFactory() {}

  27.   virtual SP_ProcInetService * create() const {
  28.     return new SP_ProcUnpService();
  29.   }
  30. };

  31. void sig_int(int signo)
  32. {      
  33.   SP_ProcPduUtils::print_cpu_time();

  34.   kill( 0, SIGUSR1 );
  35. }   
  36.   
  37. int main( int argc, char * argv[] )
  38. {
  39.   SP_ProcInetServer server( "", 1770, new SP_ProcUnpServiceFactory() );

  40.   signal( SIGINT, sig_int );

  41.   server.runForever();

  42.   return 0;
  43. }
复制代码

[ 本帖最后由 iunknown 于 2007-12-16 19:59 编辑 ]
作者: iunknown    时间: 2007-12-22 16:09
原帖由 iunknown 于 2007-12-12 23:17 发表



正好最近重新看了 UNP 的那几章。也计划实现一个多进程的 Server 框架。初步看了 apache 的做法,好复杂,还没看的出大概。
如果用 send_fd 的方式来做的话,的确比较简单,但是看 UNP 上的说法,慢的不 ...



模仿 apache 实现了另外一个 TCP Preforked Server 框架。这个框架也可以认为是 Leader/Follower pattern 基于多进程的实现。
对比了传递 socket 句柄和  Leader/Follower 两种做法的性能,结果是 Leader/Follower 模型比传递 socket 句柄快。

采用的测试模型就是《Unix网络编程(第二版)》第27章的模型。对书上提到的 client 测试工具做了一下修改,加上了 client 端的时间测量。
测试的时候,两种框架都预先启动 10 个进程,并且 client 端也只跑 10 个进程,每个进程顺序发起 100 个请求,每次请求 512 字节。
针对每种框架,连续运行 client 10 次。每个 client 进程结束的时候,都输出它的总运行时间。最后要对比的就是进程的平均运行时间。

结果:
Leader/Follower       2635 / 100 = 26.35 (毫秒)
Descriptor Passing   3644 / 100 = 36.44 (毫秒)

简单来说,就是在 Leader/Follower 框架下,一个进程处理 100 个请求需要耗时 26 毫秒,而在另外一个框架下,需要 36 毫秒。

具体的测试程序可以参考:
spprocpool-0.3.src.tar.gz
Descriptor Passing
Leader/Follower

[ 本帖最后由 iunknown 于 2007-12-22 21:05 编辑 ]
作者: cookis    时间: 2007-12-22 18:10
好东西..有机会研究一下..
作者: queue    时间: 2007-12-25 22:57
原帖由 iunknown 于 2007-12-22 16:09 发表



模仿 apache 实现了另外一个 TCP Preforked Server 框架。这个框架也可以认为是 Leader/Follower pattern 基于多进程的实现。
对比了传递 socket 句柄和  Leader/Follower 两种做法的性能,结果是 Leade ...


在 parent 和 child 之间,完全使用 pipe 来同步,比较有新意。通常都会使用 filelock,或者 mutex,或者信号量来同步。
我想主要的好处是在于 child 意外崩溃的时候,parent 能够非常方便地检测出来。
子进程只要一退出,在 pipe 上 select 就会显示可读,然后读出来为 0 字节,就是子进程崩溃了。
但是在性能上会有不小的影响吧?

另外,在处理 MaxIdleProc 上,在 LF 模型中,好像不是严格精确的吧?
当 parent 发送 CHAR_EXIT 给 child 的时候,这个时候,如果 child 阻塞在 accept 中,那么 child 是不会退出的。

在处理 MaxIdleProc 上,最近刚刚看过的 tinyproxy 中的处理方法不错。
tinyproxy 是在 child_main 中处理的,这样就不会发生 child 阻塞在 accept 中的情况。


  1. static void
  2. child_main(struct child_s* ptr)
  3. {
  4. ......
  5.                 SERVER_COUNT_LOCK();
  6.                 if (*servers_waiting > child_config.maxspareservers) {
  7.                         /*
  8.                          * There are too many spare children, kill ourself
  9.                          * off.
  10.                          */
  11.                         log_message(LOG_NOTICE,
  12.                                     "Waiting servers (%d) exceeds MaxSpareServers (%d). Killing child.",
  13.                                     *servers_waiting, child_config.maxspareservers);
  14.                         SERVER_COUNT_UNLOCK();

  15.                         break;
  16.                 } else {
  17.                         SERVER_COUNT_UNLOCK();
  18.                 }

  19. ......
  20. }
复制代码

[ 本帖最后由 queue 于 2007-12-25 22:59 编辑 ]
作者: ChinaOK    时间: 2007-12-26 09:55
关注。
作者: iunknown    时间: 2007-12-26 13:29
原帖由 queue 于 2007-12-25 22:57 发表

另外,在处理 MaxIdleProc 上,在 LF 模型中,好像不是严格精确的吧?
当 parent 发送 CHAR_EXIT 给 child 的时候,这个时候,如果 child 阻塞在 accept 中,那么 child 是不会退出的


嗯,的确有这样的问题。就目前看到的解决办法由两个:
1.就是 tinyproxy 的方法
2.在 parent 和 child 之间用类似 apache 的 POD (pipe of death)。
  POD 和现在 spprocpool 中,parent 和 child 之间的 pipe 不同。不是一个 child 一个 pipe ,是所有 child 共用一个 pipe 。
  这样当 parent 往 pod 写一个 CHAR_EXIT 的时候,是有可能被所有的 child 读到的(仅仅是可能,最终还是只有一个读成功了)。
  这样可以避免上面提到的问题。不过仍然有可能不够精确,在 parent 往 pod 写 CHAR_EXIT 的时候,可能所有的 child 都已经阻塞在 accept 上了。
  按这样来看,目前 apache 的实现应该也会存在这样的问题。不过出现的几率应该不是很高。并且即使出现了,影响也不是很大。

相对来看,比较倾向于用 apache 的 POD 方式。用 pipe 的方式主要是在于“干净”,不需要外在的依赖,也就不会带来冲突。
主要是一旦使用了 filelock 或者 信号量,就引入了一个外在的依赖。filelock 需要有一个文件名,信号量需要有一个 key 。
另外当 child 被强行 kill 的时候,如果使用的同步手段不能够自动恢复,那么就会死锁。
filelock 和 信号量都有进程如果异常退出,那么自动解锁的功能。mutex 就没有。
作者: iunknown    时间: 2007-12-29 21:21
标题: 使用 PipeOfDeath 改进 MaxIdleProc 的控制
0.4 版使用 PipeOfDeath 改进了 MaxIdleProc 的控制。
同时增加了可选的 accept locking 机制:filelock 或者 mutex 。
http://spprocpool.googlecode.com/files/spprocpool-0.4.src.tar.gz
作者: drog1983    时间: 2007-12-29 22:55
先保存,日后再研究!!
作者: iunknown    时间: 2008-01-04 21:24
标题: 增加了类似 apache 的 worker 模型
这个模型在 Leader/Follower 模型的基础上,在每个进程内部使用了一个线程池,可以提高同时并发的连接数。
http://spprocpool.googlecode.com/files/spprocpool-0.5.src.tar.gz
作者: iunknown    时间: 2008-01-05 17:30
标题: Leader/Follower 进程池设计思路
看了 apache 的分析文章之后,觉得里面的图非常好地描述了 apache 的结构。也尝试用 visio 画一下 spprocpool 的结构。

对图中各个部分的说明:
1. MasterServer 通过 Fork 创建 ProcessManager ,ProcessPool 作为 ProcessManager 在 MasterServer 中的存根
2. 在 ProcessPool 和 ProcessManager 之间存在一个 ManagerPipe 管道
3. 当 MasterServer 需要更多的子进程的时候,MasterServer 通过 ProcessPool 对象向 ProcessManager 发起创建 ChildServer 的请求
4. ProcessManager 是创建 ChildServer 的唯一一个地方
5. 在 MasterServer 和 ChildServer 之间存在一个 ChildPipe 管道
6. MasterServer 只负责监控子进程的状态(包括:忙、闲、异常退出),监控子进程的状态完全是通过 ChildPipe 来进行的;MasterServer 使用 select 可以同时监控所有的 ChildPipe 的可读状态;当一个 ChildPipe 可读的时候,MasterServer 读入内容,如果读入的最后一个字节为 BusyChar,设置 Child 的状态为 Busy;如果为 IdleChar ,设置 Child 的状态为 Idle;如果读入 0 字节,那么表示 Child 已经关闭 ChildPipe ,也就表明 Child 已经异常退出了。
7. ChildServer 被创建出来之后,就在 ListenFd 上 accept ,如果 accept 成功就通过 ChildPipe 发送一个 BusyChar,然后开始处理;处理结束之后,再通过 ChildPipe 发送一个 IdleChar;接着检查 PipeOfDeath 是否有内容,如果读到有内容,那么子进程自行退出;

关于几个主要文件句柄的传递说明:
1. ManagerPipe :MasterServer 创建,传递给 ProcessManager(通过 Fork)
2. ListenFd :MasterServer 创建,传递给 ProcessManager(通过 Fork),由 ProcessManager 传递给 ChildServer(通过Fork)
3. PipeOfDeath :MasterServer 创建,传递给 ProcessManager(通过 Fork),由 ProcessManager 传递给 ChildServer(通过 Fork),MasterServer 持可写的一端,各个 ChildServer 持可读的一端;PipeOfDeath 在 MasterServer 只有一个句柄;
4. ChildPipe :MasterServer 创建,传递给 ProcessManager(通过 Send_Fd),由 ProcessManager 传递给 ChildServer(通过 Fork),ChildPipe 在 MasterServer 有多个,每个 ChildServer 有一个 ChildPipe;







欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2