有关Apache的性能分析
分析对象
适用于UNIX系统的Apache1.3
分析内容
Apache服务器的运行流程()以及有可能进行的优化
分析的目标
记录Apache运行过程中各个部分消耗的时间记录,提出优化的方案
开始部分:
由于没有什么可以具体借鉴的文档,我们从分析Apache的运行流程入手,一下内容是我从网上查到的有关Apache运行记录(被请求的对象是一个6k左右的静态网页):
accept(15, {sin_family=AF_INET, sin_port=htons(22283), sin_addr=inet_addr("127.0.0.1")}, [16]) = 3
flock(18, LOCK_UN) = 0
sigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0
getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
setsockopt(3, IPPROTO_TCP1, [1], 4) = 0
read(3, "GET /6k HTTP/1.0\r\nUser-Agent: "..., 4096) = 60
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
time(NULL) = 873959960
gettimeofday({873959960, 404935}, NULL) = 0
stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
open("/home/dgaudet/ap/apachen/htdocs/6k", O_RDONLY) = 4
mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000
writev(3, [{"HTTP/1.1 200 OK\r\nDate: Thu, 11"..., 245}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 6144}], 2) = 6389
close(4) = 0
time(NULL) = 873959960
write(17, "127.0.0.1 - - [10/Sep/1997:23:39"..., 71) = 71
gettimeofday({873959960, 417742}, NULL) = 0
times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747
shutdown(3, 1 /* send */) = 0
oldselect(4, [3], NULL, [3], {2, 0}) = 1 (in [3], left {2, 0})
read(3, "", 2048) = 0
close(3) = 0
sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0
munmap(0x400ee000, 6144) = 0
flock(18, LOCK_EX) = 0
分类:
我们可以大致的对以上内容进行分类
1:socket请求接收部分,很清楚,应该是
accept(15, {sin_family=AF_INET, sin_port=htons(22283), sin_addr=inet_addr("127.0.0.1")}, [16]) = 3
getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
setsockopt(3, IPPROTO_TCP1, [1], 4) = 0
read(3, "GET /6k HTTP/1.0\r\nUser-Agent: "..., 4096) = 60
2:中断信号的处理部分:
sigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0
sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0
sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0
3:读取请求的静态网页并返回:
stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0
open("/home/dgaudet/ap/apachen/htdocs/6k", O_RDONLY) = 4
mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000
writev(3, [{"HTTP/1.1 200 OK\r\nDate: Thu, 11"..., 245}, {"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 6144}], 2) = 6389
close(4) = 0
4:日志记录:
time(NULL) = 873959960
write(17, "127.0.0.1 - - [10/Sep/1997:23:39"..., 71) = 71
gettimeofday({873959960, 417742}, NULL) = 0
times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747
5:关闭连接:
shutdown(3, 1 /* send */) = 0
oldselect(4, [3], NULL, [3], {2, 0}) = 1 (in [3], left {2, 0})
read(3, "", 2048) = 0
close(3) = 0
6:删除mmap文件链接
munmap(0x400ee000, 6144) = 0
分析:
子进程调度
Unix上的Apache是应用了预分支模型的服务器。父进程的责任仅在于繁衍子进程,它从不响应来自socket的任何请求。真正处理连接的是子进程,每个子进程在终止之前会(逐一地)为多个连接服务。父进程根据服务器负载的变化(通过监视记分板,记分板由子进程负责保持同步)生成新的或者杀掉旧的子进程。
在以上的分类过程中,有两句话
flock(18, LOCK_UN) = 0
flock(18, LOCK_EX) = 0
没有明确的含义,我们从这里开始。我在Apache代码http_main.c中找到了这个调用,它很明显是一段互斥作用的实现,它根据操作系统的不同,最终这组互斥调用都被封装到一组叫做
accept_mutex_off
accept_mutex_on
的函数中。
到这里就很清楚了,这部分是Apache针对不同的操作系统对于由哪个子进程接收请求的调度模块,在这里Apache采取了如下的实现模式,在任意时间只能有一个空闲的子进程拥有接收请求的权限,这种权限的分配方式由系统提供的互斥量方法解决,这是一种串行化接收请求的解决方案,简化后的代码如下
for (;;)
{
for (;;)
{
fd_set accept_fds;
FD_ZERO (&accept_fds);
for (i = first_socket; i <= last_socket; ++i)
{
FD_SET (i, &accept_fds);
}
rc = select (last_socket+1, &accept_fds, NULL, NULL, NULL);
if (rc < 1) continue;
new_connection = -1;
for (i = first_socket; i <= last_socket; ++i)
{
if (FD_ISSET (i, &accept_fds))
{
new_connection = accept (i, NULL, NULL);
if (new_connection != -1)
break;
}
}
if (new_connection != -1)
break;
}
process the new_connection;
}
Apache提供了如下的宏定义以改变互斥实现的方式ap_config.h
HAVE_USLOCK_SERIALIZED_ACCEPT
HAVE_PTHREAD_SERIALIZED_ACCEPT
HAVE_SYSVSEM_SERIALIZED_ACCEPT
HAVE_FCNTL_SERIALIZED_ACCEPT
HAVE_FLOCK_SERIALIZED_ACCEPT
HAVE_OS2SEM_SERIALIZED_ACCEPT
HAVE_TPF_CORE_SERIALIZED_ACCEPT
HAVE_BEOS_SERIALIZED_ACCEPT
HAVE_NONE_SERIALIZED_ACCEPT
我们可以在编译之前增加编译选现来实现不同的互斥调用以获得更高的性能,由于时间限制,我只找到了少部分这方面编译的资料
USE_SYSVSEM_SERIALIZED_ACCEPT (1.3版及以后)
此方法借助SysV的信号量(semaphores)实现互斥。但不巧的是SysV信号量有一些负面作用。一是Apache可能在清除信号量之前非正常终止;二是在使用信号量API时需要考虑到任何与服务器UID相同的CGI程序可以进行拒绝服务攻击(就是说所有的CGI程序都可以这样做,除非使用suexec或cgiwrapper之类的方法)。所以,这种方法并不被IRIX之外的系统广泛采纳(由于大多数IRIX系统上,使用前两种方法的代价太大)。
USE_USLOCK_SERIALIZED_ACCEPT
(1.3版及以后)此方法仅在IRIX上可用。它调用usconfig(2)创建互斥量。虽然这种方法避免了对SysV信号量的种种争议,但它不是IRIX的缺省方案。这是由于在单处理器的IRIX系统 (5.3或6.2)上,uslock代码比SysV信号量慢两个数量级;但在多处理器的IRIX中前者比后者快一个数量级。这无非使问题复杂化了。所以在多处理器IRIX系统上,我们需要用如下的附加参数编译Apache:
在EXTRA_CFLAGS中添加-DUSE_USLOCK_SERIALIZED_ACCEPT
USE_PTHREAD_SERIALIZED_ACCEPT
(1.3版及以后)此方法实现了POSIX标准互斥量。它理应可以工作在任何实现了全部POSIX线程规范的系统上,但事实是只有在Solaris 2.5或以上的系统及特定的配置中才能工作。如果尝试这种方法的话,需要小心服务器挂起或者没有响应。服务器在只输出静态网页的情况下运行得很好。
以上言及的方案对多socket服务器是相当不错的,但只有一个socket的情况又如何呢?理论上,由于在连接请求到来之前所有子进程将阻塞在accept中,单个socket不会产生上述种种问题。但实际上,上述非阻塞解决方案所带来的"回旋(spinning)"问题在这里只不过被掩盖起来了。在绝大多数TCP协议栈的实现中,一个接请求到来时内核将唤醒所有阻塞在accept中的进程。它们之一将得到此请求并返回用户空间,其余的进程将返回内核重新休眠。这将带来与多socket非阻塞解决方案相同的资源浪费。
由于这点原因,我们发现如果为socket串行化,许多系统表现得更"友好"--即使是一个socket的情况。这是单个socket串行化作为绝大多数情况的缺省配置的原因。在Linux上不甚精确的(Linux 2.0.30 / 双Pentium Pro 166 w / 128Mb内存)实验表明,对每次请求而言,串行化的单个socket仅比没有串行化的socket损失不到3%的性能。但未串行化的socket显示出每次连接请求100毫秒的延时。这也可能仅仅由于过长的通讯距离造成的。如果您不想串行化单个socket,可以定义宏SINGLE_LISTEN_UNSERIALIZED_ACCEPT。这样,仅有一个socket的服务器将不会串行化。
共享内存的使用
Apache利用一种叫做记分板(scoreboard)的技术在父、子进程间通讯。它的理想实现是在共享内存中。有的操作系统允许我们直接访问共享内存,或者提供它们的确切端口。在这些系统中的典型实现就是共享内存记分板。其他的系统则将磁盘上的文件作为缺省实现。磁盘文件不仅低效而且不稳定(又没有什么优势)。请为您的操作系统仔细阅读src/main/conf.h文件,并在其中寻找USE_MMAP_SCOREBOARD或者USE_SHMGET_SCOREBOARD。定义它们之一(以及相应的HAVE_MMAP和HAVE_SHMGET)将允许Apache使用共享内存。
读取静态网页
我们发现在在静态网页的读取过程中,Apache采用了mmap方式来映射文件信息,我们知道在对于小文件的读取这种方式并没有多少好处,Apache通过一个叫做MMAP_THRESHOLD的宏定义来确定是否采用mmap的文件映射方式,对于大于MMAP_THRESHOLD的文件Apache才采用mmap方式,在http_core.c中这个我们可以找到这个宏定义,它的默认值是8096,资料显示这一数值是对于SunOS4,我们可以针对不同的操作系统修改这一数值来提高性能,这依赖于大量的测试
有关性能的配置文件httpd.conf
在过程中出现了多次的时间函数函数gettimeofday,times的调用,这些调用是由于在编译Apache的过程中包含了mod_status并且将ExtendedStatus设置为On造成的,这些调用都是为了在报告中含有时间戳,我们可以将ExtendedStatus设置为OFF来去掉这些调用。
除此之外,HostnameLookups设定也是导致性能下降的罪魁祸首。DNS解析消耗了大量的时间,除此之外,如果使用了任何allow from domain或deny from domain命令,所付出的代价将是两次DNS查询带来的延时(在一次逆向查询后跟着一次正向查询,以保证前者得到的结果是真实的)。因此为了得到最理想的性能应避免使用HostnameLookups
其它有关性能编译选项
如果我们不打算支持动态加载模块的话,编译服务器时请设定参数-DDYNAMIC_MODULE_LIMIT=0。这将节省出为动态加载模块而分配的内存。
性能监控模块
mod_status和mod_info to模块
在Apache服务器中的 mod_status 和 mod_info to 来告诉我们目前服务器的工作情况
mod_status:
使用 mod_status,允许我们使用URL:http://servername/server-status来通过mod_status生成并报告服务器状态信息。我们可以知道谁在你的服务器上看些什么东西,以及有多少人连在Web 服务器上,每个子进程的运行状态等等信息
mod_info 和 mod_status
这两个模块可以提供十分有用的信息,而且十分方便。
mod_status 能准确地告诉你,你的服务器正在“想”什么。你可以知道有哪些人在浏览您的网站,有多少子进 程在运行,以及这些进程在干吗。系统自从上次启动以来已经运行了多少时间。
当然我们还可以得到更多的信息,修改http.conf
ExtendedStatus on
我们还可以得到一张每一 个子进程及其所作工作的列表。 对于每一个子进程而言,信息包括它的PID ,以及它占用的CPU 时间和已经运行的时间。对于服务器而言,信息包括得到服务器启动以后的合计点击数,CPU的利用率以及每分钟点击数,还有传输给客户端的总计字节数。
启用方式,修改httpd.conf文件,增加以下信息
SetHandler server-status
Order deny,allow
Deny from all
Allow from .your_domain.com
mod_info
启用mod_info,允许使用URL:http://servername/server-info来远程报告服务器配置信息,mpd-info 是一个分类的扩展模块,需要需要mod_info.c支持。
确认安装之后,修改httpd.conf,增加以下信息
SetHandler server-info
Order deny,allow
Deny from all
Allow from .your-domain.com
URL:http://servername/server-info显示的启示就是你编译到Apache 里面的东西的列表以及其他针对服务器的各种特性。如果你输入:http://your.server/server-info/ 就可以看到服务器内置的模块列表或者通过DSO 加载的模块列表。
这对于安装和配置特定的服务器来说是十分有用的。特别是用来对错误的配置文件查找问题时。
结论以及目标
1:Apache运行过程时间的记录
通过修改http:.conf配置文件,
LogFormat "%h %l %u %t \"%r\" %>s %b" common(默认)
增加%D(毫秒级,仅对于Apache2适用),%T(秒级)参数,增加对每个请求的时间过程记录
LogFormat "%h %l %u %t \"%r\" %>s %b %D" common(默认)
2:加载mod_status和mod_info to模块
虽然加载mod_status和mod_info to模块会带来一定的性能损失,但是一个完善的监控机制还是有必要的,方便我们对Apache的参数调整后的检测,这要比查看单条的请求记录来得更直观和有效
3:进一步的希望
我们觉得以上的信息仍然不够,需要更加量化的信息,比如我们需要每个请求的数据接收,处理,日志的时间记录,我们可能需要去修改Apache的源代码,以达到以上的目标