apache: 理论 - 工作模式



0. 环境背景

apache httpd version 2.4.54

1. apache HTTP 的MPMs

apache HTTP被设计为一个可以在不同环境下工作在不同平台的强大且灵活的web服务器。不同环境和不同平台通常需要不同的特性,或者同一个特性需要不同的实现方式。apache HTTP通过模块化的设计特点充分的满足了不同的场景的需求。

apache HTTP 2.0将这个模块化的设计延展到了它本身作为一个web服务器最基础的功能上。它创建了一个特殊模块,MPMs(Multi-Processing Moudles),其负责网络端口监听、接收请求和分发请求给子进程。

通过在基础功能上的模块化,得到了如下好处

在用户视角,MPMs和其他的普通模块没有什么不同,但是不同的是,MPMs同时只能加载一个。

在类UNIX系统平台上,只有三种MPM可选,prefork、worker和event。

  • 当服务器不支持线程,也支持线程安全时,MPMs默认是prefork
  • 当服务器支持线程,但不支持线程安全时,MPMs默认是woker
  • 当服务器支持线程,也支持线程安全时,MPMs默认是event

1.1 prefork

prefork使用多个子进程,每个子进程只有一个线程。每个进程在某个确定的时间只能维持一个连接,效率高,但内存占用量比较大。

这个非线程型的、预派生的web服务器,它适合于没有线程安全库,需要避免线程兼容性问题的系统。它是要求将每个请求相互独立的情况下最好的MPM,这种模式下,一个请求出现问题不会影响到其他请求。

配置重点

prefork这种MPM能很好的自我调节,所以需要很少的配置。重点是MaxRequestWorkers要设置的足够大,可以处理足够多的请求,同时需要设置的足够小,至少保证可以给所有的进程分配足够的物理内存。

工作原理

一个控制进程作为父进程来启动多个子进程,每个子进程负责监听请求连接并处理请求。

apache HTTP会通过相关配置(StartServers,MinSpareServers,MaxSpareServers,MaxRequestWorkers)动态调整进程数量,以保留足够的空闲进程来随时响应请求。

当需要同时处理超过256个请求时,可以调大MaxRequestWorkersServerLimit来增强请求处理能力;当服务器内存紧张时,可以调小MaxRequestWorkers来节省内存。

MaxConnectionsPerChild配置了一个子进程能处理的最大请求数,超过这个数目,该子进程就会停止处理新请求,然后被杀掉,重新创建一个新的子进程(为了有效的避免内存泄漏)。

1.2 worker

worker使用多个子进程,每个子进程有多个线程,每个线程在某个确定的时间只能维持一个连接,内存占用量比较小,适合高流量的http服务器。缺点是假如一个线程崩溃,整个进程就会连同其任何线程一起”死掉”,所以要保证一个进程在运行时必须被系统识别为”每个线程都是安全的”。

这个支持混合多线程多进程的web服务器,由于使用线程来处理请求,所以可以处理海量请求,而系统资源的开销小于基于进程的MPM。但是它也使用了多进程,每个进程又有多个线程,以获得基于进程的稳定性。

配置重点

worker MPM,使用ThreadsPerChild来指定每个子进程的线程数量上限,使用MaxRequestWorkers来指定所有进程的线程数量总和上限。

工作原理

一个控制进程作为父进程来启动多个子进程,每个子进程创建固定数量的处理请求的线程以及一个监听线程,监听线程会监听请求连接,并将它们转交给处理请求的线程来处理。

apache HTTP 会通过相关配置(MinSpareThreads,MaxSpareThreads)来动态调整线程和进程数量,以保留足够的空闲线程来随时响应请求。

使用worker MPM时,最大处理的连接请求数量由MaxRequestWorkers限定;最大子进程数量由MaxRequestWorkers除以ThreadsPerChild的值来限定。

另外,有两个单独的配置来决定子进程和子进程中线程的绝对上限,这个配置只能完全停止apache HTTP进程,然后再启动才可以更改。其中ServerLimit是子进程的绝对上限值,必须要大于或等于MaxRequestWorkers除以ThreadsPerChild的值。ThreadLimit是子进程中线程的绝对上限值,必须要大于或者等于ThreadsPerChild

需要额外注意的是,apache HTTP的子进程包含活动进程和正在关闭中的进程,至少会有一个进程来提供服务,至多有MaxRequestWorkers个正在关闭的进程(实际数量肯定少于这个值)。这种行为的产生原因有两种情况,一种是因为MaxConnectionsPerChild配置了一个子进程能处理的最大请求数,超过这个数目,该子进程就会停止处理新请求,然后被杀掉,重新创建一个新的子进程(为了有效的避免内存泄漏)。另外一种是因为空闲线程的数量超过了MaxSpareThreads配置的数目,触发了自动调节机制。如果希望关闭这种行为,可以将MaxConnectionsPerChild设置为0,然后将MaxSpareThreadsMaxRequestWorkers设置为同样的值。

1.3 event

event MPM旨在通过将一些处理工作传递给侦听器线程从而释放工作线程以服务新请求的方式,来允许同时处理更多请求。

worker的关系

event是基于worker的。一个控制进程作为父进程来启动子进程,每个子进程创建固定数量的处理请求的线程以及一个监听线程,监听线程会监听请求连接,并将它们转交给处理请求的线程来处理。

配置重点

eventworker配置基本一致,只是多了AsyncRequestWorkerFactor

工作原理

event MPM致力于在HTTP中的keepalive问题。

在客户端完成第一次请求后,它可以保留当前的连接而不关闭它,在相同的连接上发送接下来的请求,从而节省了TCP连接的信号交换成本。然而apache HTTP会留出一个完整的进程/线程来等待客户端的请求,这样相当于将其闲置,只为等待。为了解决这个问题,event MPM在每个子进程中使用指定的监听线程来掌控所有的监听sockets、所有处于Keepalive状态的sockets、处理程序和协议过滤器完成工作的sockets和唯一剩下发送给客户端的sockets。

这种新架构,使用了非阻塞sockets和APR提供的现代内核特性(比如linux的epoll)。 单个进程/线程块可以处理的连接总数由AsyncRequestWorkerFactor配置限定。

更多event相关的原理,可详细参照apache httpd event。实践总结apahce(event) <= nginx + apache(worker),event唯一的作用是比worker节省资源,可以用更少的线程处理同样多的请求(超过线程能力的连接被放到队列中)。

2. MPM配置

2.1 常用MPM配置

# prefork MPM
# StartServers: number of server processes to start
# MinSpareServers: minimum number of server processes which are kept spare
# MaxSpareServers: maximum number of server processes which are kept spare
# MaxRequestWorkers: maximum number of server processes allowed to start
# MaxConnectionsPerChild: maximum number of connections a server process serves before terminating
<IfModule mpm_prefork_module>
    StartServers             5
    MinSpareServers          5
    MaxSpareServers         10
    MaxRequestWorkers      250
    MaxConnectionsPerChild   0
</IfModule>

# worker MPM
# StartServers: initial number of server processes to start
# MinSpareThreads: minimum number of worker threads which are kept spare
# MaxSpareThreads: maximum number of worker threads which are kept spare
# ThreadsPerChild: constant number of worker threads in each server process
# MaxRequestWorkers: maximum number of worker threads
# MaxConnectionsPerChild: maximum number of connections a server process serves before terminating
<IfModule mpm_worker_module>
    StartServers             3
    MinSpareThreads         75
    MaxSpareThreads        250 
    ThreadsPerChild         25
    MaxRequestWorkers      400
    MaxConnectionsPerChild   0
</IfModule>

# event MPM
# StartServers: initial number of server processes to start
# MinSpareThreads: minimum number of worker threads which are kept spare
# MaxSpareThreads: maximum number of worker threads which are kept spare
# ThreadsPerChild: constant number of worker threads in each server process
# MaxRequestWorkers: maximum number of worker threads
# MaxConnectionsPerChild: maximum number of connections a server process serves
#                         before terminating
<IfModule mpm_event_module>
    StartServers             3
    MinSpareThreads         75
    MaxSpareThreads        250
    ThreadsPerChild         25
    MaxRequestWorkers      400
    MaxConnectionsPerChild   0
</IfModule>

MaxRequestWorkers 在 apache HTTP 2.3.13 之前被称为 MaxClients MaxConnectionsPerChild 在apache HTTP 2.3.9 之前被称为MaxRequestsPerChild

2.2 配置项详解

StartServers(event, worker, prefork)

指定服务器启动时建立的子进程数量。

preforkMPM下默认为5。

workereventMPM下默认是3.


MinSpareServers(prefork)

指定空闲子进程的最小数量,默认为5。

如果当前空闲子进程数少于MinSpareServers ,那么Apache将创建一个子进程,一秒后,创建两个,一秒后,创建四个,就这样持续增加,最大一次创建32个。就这样创建进程直到达到MinSpareServers指定的数值。


MaxSpareServers(prefork)

设置空闲子进程的最大数量,默认为10。

如果当前有超过MaxSpareServers数量的空闲子进程,那么父进程将杀死多余的子进程。此参数不要设的太大。如果你将该指令的值设置为等于或小于MinSpareServers,Apache将会自动将其修改成MinSpareServers+1


MinSpareThreads(event, worker)

指定空闲子线程的最小数量,默认为75。

workerevent在服务级别来处理空闲子线程,当空闲子线程小于最小值,Apache会创建子进程,直到空闲子线程大于MaxSpareThreads


MaxSpareThreads(event, worker)

指定空闲子线程的最大数量,默认为250。

workerevent在服务级别来处理空闲子线程,当空闲子线程超过了最大值,Apache会杀死子进程,直到空闲子线程小于MaxSpareThreads


ThreadsPerChild(event, worker)

指定每个子进程要创建多少个子线程,默认为25。

ThreadsPerChild不可以超过ThreadLimit


MaxRequestWorkers(event, worker, prefork)

指定同一时间客户端最大接入请求的数量。

preforkMPM下,默认为256。任何超过MaxRequestWorkers限制的请求都将进入等候队列,一旦一个链接被释放,队列中的请求将得到服务。要增大这个值,你必须同时增大ServerLimit

workereventMPM下,默认为16(ServerLimit)*25(ThreadsPerChild)。如果MaxRequestWorkersThreadsPerChild需要的子进程数超过16,同时应该提高ServerLimit


MaxConnectionsPerChild(event, worker, prefork)

每个子进程在其生存期内允许处理的最大请求数量,默认为0,即子进程永远不会结束。

到达MaxRequestsPerChild的限制后,子进程将会结束。将MaxRequestsPerChild设置成非零值有两个好处:

  1. 可以防止(偶然的)内存泄漏无限进行,从而耗尽内存。
  2. 给进程一个有限寿命,从而有助于当服务器负载减轻的时候减少活动进程的数量。

ServerLimit(event, worker, prefork)

preforkMPM下,指定Apache httpd 进程声明周期内MaxRequestWorkers可配置的最大值。

workereventMPM下,和ThreadLimit组合在一起,指定Apache httpd 进程声明周期内MaxRequestWorkers可配置的最大值。

需要重点关注的是,当设定远超过需要的值时,未使用的额外的共享内存也会被分配占用。如果ServerLimitMaxRequestWorkers设定为超过系统可以支撑的值时,Apache httpd不会启动或者会不稳定。

preforkMPM下,只有当MaxRequestWorkers需要设定超过其默认值(256)时,才需要设定ServerLimit。不要将ServerLimit的值设置为高于你可能希望将 MaxRequestWorkers 设置为的值。

workereventMPM下,只有当 MaxRequestWorkers/ThreadsPerChild需要大于默认值(16)时,才需要设定ServerLimit

ServerLimit(prefork)在编译阶段,默认有个最大配置限制200000。如果希望可以配置的更大,则必须修改mpm源码中的MAX_SERVER_LIMIT,然后重新编译apache。


ThreadLimit(event, worker)

指定 ThreadsPerChild可配置的最大值,默认值64。

需要重点关注的是,当设定远超过需要的值时,为使用的额外的共享内存也会被分配占用。如果ThreadLimitThreadsPerChild设定为超过系统可以支撑的值时,Apache httpd不会启动或者会不稳定。

ThreadLimit(worker)在编译阶段,默认有个最大配置限制20000(event 10000)。如果希望可以配置的更大,则必须修改mpm源码中的MAX_THREAD_LIMIT,然后重新编译apache。

3. apache模式的查看

3.1 常看当前模式

如果apache已经安装,我们可以用以下命令查看当前模式。

httpd -l

若找到 prefork.c 则表示当前工作在preforkMPM,同理出现 worker.c 则工作在worker MPM。

如果apache还未安装,我们在编译的时候可以加入 --with-pem=(prefork|worker) 选项决定启用什么模式。

3.2 切换模式

# a. 将当前的prefork模式启动文件改名
mv httpd httpd.prefork

# b. 将worker模式的启动文件改名
mv httpd.worker httpd

# c. 修改Apache配置文件
vi /usr/local/apache2/conf/extra/httpd-mpm.conf
# 找到里边的如下一段,可适当修改负载等参数:
#<IfModule mpm_worker_module>
#StartServers 
#MaxClients 
#MinSpareThreads 
#MaxSpareThreads 
#ThreadsPerChild 
#MaxRequestsPerChild 
#</IfModule>

# d. 重新启动服务
/usr/local/apache2/bin/apachectl restart

4. 总结

4.1 为什么event和nginx都是用了epoll的原理,但是apache性能不如nginx呢?

这是因为apache(event)的thread依赖kernel进行调度,有线程的上下文切换成本。但是nginx使用event驱动本身作为调度器,没有上下文切换成本。这也就是为什么nginx+apache(worker)比使用apache(event)更优的原因

4.2 那么apache和nginx该如何选择呢?