Apache中预创建Preforking MPM机制剖析(一)


6.3.1 Leader/Follow模式
在了解Preforking MPM之前有必要首先了解Leader/Follow模型。Preforking模型本质上也属于Leader/Follow模型。通常情况下,L/F可以用下图进行描述:
通常情况下,对于服务器中的进程采用的都是即时创建的策略,即一旦有一个新的客户端请求立即创建一个新的进程或者线程,而当进程或者线程执行完毕后,进程和线程也随之退出。显然这种策略对于小规模的服务器还能接受,但是如果对于大规模的服务器而言,创建进程或者线程的时间将增加,最终会导致响应时间变长,单位时间内请求处理效率降低。L/F模式则不同,它首先一次性创建多个进程或者线程,包括到系统中,这些进程或者线程担任三种不同的角色:侦听者、工作者以及空闲者,其含义分别如下:
1)、侦听者的角色。该线程负责侦听客户端的请求。在L/F模式中它属于Leader的角色。通常情况下只允许一个进程或者线程担当侦听者的角色。
2)、工作者的角色。当侦听者侦听到客户端的请求之后,它将立即转换为工作者角色并开始处理客户端的请求。工作者角色的线程可以有多个。
3)、空闲者的角色。当工作者执行任务完毕后它并不会立即退出,而是转变它的角色为空闲者,并呆在空闲队列中。空闲者出现的原因是因为客户端请求不够多。空闲者们等待变为侦听者。而当侦听者变为工作者之后,空闲者中的每一个都相互竞争,最终将会有一个线程变为侦听者,其余的继续保持空闲者的状态。
4)、几个极端的情况也是可能出现的:所有线程都变为工作者,忙于处理客户端的请求,没有线程担任侦听者的角色,因此此时客户端的请求都被拒绝;没有工作者,如果没有任何请求到达,那么所有的线程都处于空闲状态。
线程的三个角色的相互转换关系可以用上图的红线进行描述。
6.3.2 Preforking MPM概述
UNIX平台上可以使用的第一个MPM就是预先派生(Preforking)MPM,也是默认的MPM。该模型在功能上等同于Apache1.3上的模型,该MPM的示意图如下所示:
该MPM中,存在一个主进程和多个子进程。每个子进程都会为所进行的请求侦听一个套接字。当接受到请求之后,子进程就会接受它并且提供响应。父进程会监控所有的子进程以确保总是可以使用最少数量的进程来处理请求,并且确保等候请求到达的闲置进程不能过少。如果没有足够的空闲进程来处理潜在的请求高峰,那么父进程就会启动新的子进程。如果存在过多的进程,那么父进程会每次终止一个空闲进程,直到服务器回到最大空闲子进程数量之下。通过保持一定数量的空闲子进程来接受所引入的请求,服务器就可以避免在接受到请求时再去启动新进程的开销。
父进程和子进程之间通过记分板进行通信。对于每一个产生的子进程,它的状态信息都写入到记分板中,父进程通过读取记分板可以了解子进程的状态。当需要关闭子进程的时候它将通过终止管道发送终止信息给子进程,另外的一种通知方法就是通过信号。
预先派生模型有一些优点,例如健壮性以及可靠性。 Apache允许使用动态模块将第三方的代码加入到服务器。这意味着如果管理员在服务器中增加了第三方软件,而且模块导致了子进程出现段故障,那么服务器就会丢失一个连接,而且仅仅丢失一个连接。服务器的其余部分还将继续运行,并且可以为请求提供服务。唯一可以注意到问题出现的用户就是不幸地进行了导致问题地请求的用户。另一方面必须要意识到,可能并不是遭遇了故障的请求而导致了问题,可能会是因为有一系列请求而导致了问题的出现。
这个模型的另外一个优点就是可以很容易地编写采用这种方式运行的MPM。如果每个进程每次只需要处理一次请求,那么就没有过多的边界条件需要考虑。例如,如果管理员想平稳重新启动服务器,那么她就要等待所有的子进程完成当前请求,并且适时强制其终止来完成任务,然后父进程就可以启动新的进程来代替旧的进程了。
不过预派生模型也有自己的缺点,比如扩充性。因为预先派生模型是依赖于进程,所以在某些平台上并不能很好的执行,比如在Window平台上则由于进程的代价太高,耗时太长的原因则弃用该方案。当然 Window并不是唯一的遭遇该问题的OS。比如在高负载的情况下AIX也会遇到这个问题。
预先派生模型的另外一个问题就是安全性。许多ISP都会在相同的计算机上使用Apache来为多个公司的Web站点提供需要的Web服务。为了完成这个任务,每个公司都要被赋予一个虚拟主机,但是因为所有的站点都需要访问Web服务器,所以必须通过运行子进程的用户ID来读取所有的页面。大多数Apache用户运行Apache的方式是作为超级用户运行父进程,然后作为专用于Web服务器的用户运行子进程。这样就可以让服务器打开特权端口80,而且还可以确保侦听网络的进程不作为根用户运行,因此就减少了发现安全漏洞所带来的风险。因为所有的虚拟主机都会使用相同的用户ID运行,所以它们的CGI教本也会使用这个ID运行。这意味着任何为这个站点存储信息的数据库都必须要通过这个用户可读。这样就会让任何站点访问其他站点的私有信息。当然,Apache已经针对这个问题提供了解决方案,它可以让站点规定它们的CGI脚本作为哪个用户运行但是这只是解决了实际的CGI教本的问题,并没有解决PHP,Apache模块或者通过mod_perl运行的Perl脚本的问题。
这种设计的最后问题就是它会消弱某些优化。因为每个请求都会在它自己的进程中运行,所以进程之间很难共享任何信息。增加Web服务器性能的常见方式就是缓存最近所发送的所有的页面。但是,如果每个进程都要保存它自己的缓存,那么缓存的作用就会降低。只有多次获取缓存中所缓存的页面缓存才会有作用。大多数情况下,这种方式会随着时间的推移逐渐显示其作用,缓存总是会缓存最常受到请求的页面。这意味着,到缓存起作用的时候,进程就要受到没有缓存的新进程的替换。Apache中的子进程会在由配置文件中的 MaxRequestsPerChild指令所控制的指定时间间隔终止。Apache在指定的请求数量之后强制终止子进程的原因是为了防止内存泄漏。因为 Apache要在相当长的时间内运行,所以很小的内存泄漏也会导致服务器出现问题。而且因为子进程是唯一分配内存的进程——所以强制其退出,并且偶尔对其重新启动——就有可能避免由于内存泄漏而导致的问题。
在下面的部分,我们将描述MPM的实现细节。首先我们分析主进程管理细节,然后分析子进程的工作细节,最后我们分析主进程是如何与子进程进行通信并对其进行管理的。
6.3.3 Preforking MPM实现
6.3.3.1 内部结构
在对Preforking MPM进行深入的分析之前,我们先从整体上了解Preforking MPM的内部结构,从而能够从整体上有一个认识,这样在后面的具体分析中不至于迷失方向。下图给出的则是Preforking MPM的中的内部大概的实现机制:

一个完整的MPM包括下面几个数据流程:
1)、第一次初始化
第一次初始化的时候是分配资源(主要是内存池),读取以及检查配置文件,然后服务器进程将其变为一个后台进程。
2)、重启循环
重启循环是指不关闭Apache而进行的启动。重启循环中主要重新读取配置文件防止配置文件发生变化,创建子进程池并进入服务器主循环。
3)、服务器主进程循环
服务器主循环主要是控制进程池中空闲子进程的数目,具体到细节中则是循环监控记分板,并根据记分板中子进程的状态作出反应。
4)、客户端请求/响应循环
这个循环只适合于子进程。在该循环中,子进程等待自己变为侦听者,然后等待客户端的连接,一旦获取到连接则变为工作者开始处理请求,同时进入Keep-alive循环中。
5)、Keep-alive循环
Keep-alive循环主要是处理客户端的请求,该循环仅仅适合子进程。
6)、在退出之前进行清理工作
 
6.3.3.2 MPM中的定义
预创建MPM所对应的文件为prefork.c。通常情况下,MPM的名称总是和MPM的文件具有相同的名称,这样做可以让配置更合理一些。对于每一个MPM,其遇到的第一件事情就是定义一些全局变量,它们各自的含义描述在右边的注释中:
int ap_threads_per_child=0;                                       /* 每个进程对应的线程数目 */
static apr_proc_mutex_t *accept_mutex;                          /*连接接受互斥锁,用以确保在任何时候只有一个连
接被接受*/
static int ap_daemons_to_start=0;                             /*初始启动的进程数目*/
static int ap_daemons_min_free=0;                                   /*可以接受的空闲进程的最小数目*/
static int ap_daemons_max_free=0;                                  /*允许空闲的进程的最大数目*/
static int ap_daemons_limit=0;                                          /*允许同时运行的进程的最大值*/
static int server_limit = DEFAULT_SERVER_LIMIT;        /**/
static int first_server_limit;
static int changed_limit_at_restart;
static int mpm_state = AP_MPMQ_STARTING;                     /*描述当前*/
static ap_pod_t *pod;
 
int ap_max_daemons_limit = -1;
 
server_rec *ap_server_conf;
static int one_process = 0;
 
static apr_pool_t *pconf;                               /* Pool for config stuff */
static apr_pool_t *pchild;                             /* Pool for httpd child stuff */
 
static pid_t ap_my_pid;                          /* it seems silly to call getpid all the time */
static pid_t parent_pid;
#ifndef MULTITHREAD
static int my_child_num;
#endif
ap_generation_t volatile ap_my_generation=0;
static int die_now = 0;
另外一个所有的MPM都用到的函数就是ap_mpm_query(),外界调用该函数通常是想了解当前MPM的一些私有属性。比如在mod_snake(在Apache进程中嵌入python解释器的模块)中就会使用这个函数来查询给定的MPM是否进行了线程化。如果进行了线程化,那么该模块就必须同步某些python函数,否则就不需要进行同步了。
Apache中关于MPM的状态分类可以归结为两大类:运行状态和内部状态。
#define AP_MPMQ_STARTING              0
#define AP_MPMQ_RUNNING               1
#define AP_MPMQ_STOPPING              2
上面三种属于运行状态,分别表示Apache处理启动、运行和停止状态。
#define AP_MPMQ_MAX_DAEMON_USED       1 /* 所有的进程都已经满了         */
#define AP_MPMQ_IS_THREADED          2 /* MPM 能够支持线程化           */
#define AP_MPMQ_IS_FORKED             3 /* MPM 能够调用fork产生子进程 */
#define AP_MPMQ_HARD_LIMIT_DAEMONS    4 /* The compiled max # daemons   */
#define AP_MPMQ_HARD_LIMIT_THREADS    5 /* The compiled max # threads   */
#define AP_MPMQ_MAX_THREADS           6 /* # of threads/child by config */
#define AP_MPMQ_MIN_SPARE_DAEMONS     7 /* Min # of spare daemons       */
#define AP_MPMQ_MIN_SPARE_THREADS     8 /* Min # of spare threads       */
#define AP_MPMQ_MAX_SPARE_DAEMONS     9 /* Max # of spare daemons       */
#define AP_MPMQ_MAX_SPARE_THREADS    10 /* Max # of spare threads       */
#define AP_MPMQ_MAX_REQUESTS_DAEMON 11 /* Max # of requests per daemon */
#define AP_MPMQ_MAX_DAEMONS          12 /* Max # of daemons by config   */
#define AP_MPMQ_MPM_STATE            13 /* starting, running, stopping */
上面的都属于Apache的内部状态。预创建MPM中的函数定义如下:
AP_DECLARE(apr_status_t) ap_mpm_query(int query_code, int *result)
{
    switch(query_code){
        case AP_MPMQ_MAX_DAEMON_USED:
            *result = ap_daemons_limit;
            return APR_SUCCESS;
        case AP_MPMQ_IS_THREADED:
            *result = AP_MPMQ_NOT_SUPPORTED;
            return APR_SUCCESS;
        case AP_MPMQ_IS_FORKED:
            *result = AP_MPMQ_DYNAMIC;
            return APR_SUCCESS;
        case AP_MPMQ_HARD_LIMIT_DAEMONS:
            *result = server_limit;
            return APR_SUCCESS;
        case AP_MPMQ_HARD_LIMIT_THREADS:
            *result = HARD_THREAD_LIMIT;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_THREADS:
            *result = 0;
            return APR_SUCCESS;
        case AP_MPMQ_MIN_SPARE_DAEMONS:
            *result = ap_daemons_min_free;
            return APR_SUCCESS;
        case AP_MPMQ_MIN_SPARE_THREADS:
            *result = 0;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_SPARE_DAEMONS:
            *result = ap_daemons_max_free;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_SPARE_THREADS:
            *result = 0;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_REQUESTS_DAEMON:
            *result = ap_max_requests_per_child;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_DAEMONS:
            *result = server_limit;
            return APR_SUCCESS;
        case AP_MPMQ_MPM_STATE:
            *result = mpm_state;
            return APR_SUCCESS;
    }
    return APR_ENOTIMPL;
}
这个是ap_mpm_query函数是所有的MPM都要求的函数,它可以让模块发现MPM的运行特性。尽管这个函数在所有的MPM中看起来很相似,但是细节还是十分重要,因为每个MPM都必须实现自己的这个函数。使用这个函数最常见的原因就是要通过web或者管理应用程序报告信息。可以采用很多方式来使用这个函数中的信息,所以应该保证它正确无误。比如mod_snake(在Apache中嵌入python解释器的模块)就会使用这个查询模块来确定MPM是否进行了线程化,如果进行了线程化,那么该模块可能就必须同步某些python函数;否则就不需要进行同步
本文作者:
« 
» 
快速导航

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3