分享
 
 
 

Apache 性能最优化分析(下)

王朝system·作者佚名  2006-12-16
窄屏简体版  字體: |||超大  

有两种实现这个特性的办法:一是socket的SO_LINGER选项。但似乎是命中注定,在多数TCP/IP协议栈中它从来不能正确地实现。即使是在提供了正确实现的平台(即Linux 2.0.31)上,这种方法也要比第二种方法代价(指CPU时间)高得多。

大多数情况下,Apache在一个叫lingering_close的函数中实现了它(在 http_main.c)。这个函数大致如下所示:

void lingering_close (int s)

{

char junk_buffer[2048];

/* shutdown the sending side */

shutdown (s, 1);

signal (SIGALRM, lingering_death);

alarm (30);

for (;;) {

select (s for reading, 2 second timeout);

if (error) break;

if (s is ready for reading) {

read (s, junk_buffer, sizeof (junk_buffer));

/* just toss away whatever is here */

}

}

close (s);

}

这自然增加了连接结束时的开销,但它是可靠的实现所必需的。随着HTTP/1.1的日益盛行,所有连接都是持久的,这种开销将被众多的连接请求抵消。如果您想冒险禁止这一特性的话,可以定义宏NO_LINGCLOSE,但这显然是不被推荐的。实际上,由于在HTTP/1.0中持久的管道式连接越来越普遍,lingering_close几乎是必须的选择。(管道式连接非常高效,所以您还是希望支持它的吧)

5) 记分板文件

Apache利用一种叫做记分板(scoreboard)的技术在父、子进程间通讯。它的理想实现是在共享内存中。有的操作系统允许我们直接访问共享内存,或者提供它们的确切端口。在这些系统中的典型实现就是共享内存记分板。其他的系统则将磁盘上的文件作为缺省实现。磁盘文件不仅低效而且不稳定(又没有什么优势)。请为您的操作系统仔细阅读src/main/conf.h文件,并在其中寻找USE_MMAP_SCOREBOARD或者USE_SHMGET_SCOREBOARD。定义它们之一(以及相应的HAVE_MMAP和HAVE_SHMGET)将允许Apache使用共享内存。如果您系统的内存共享机制与众不同,请编辑src/main/http_main.c并增加Apache所需的挂钩函数(同时请把补丁寄给我们)

注:直到1.2版,Apache的Linux版才开始使用共享内存。这一疏忽使得以前版本的Apache在Linux上表现得很不理想。

DYNAMIC_MODULE_LIMIT

如果您不打算支持动态加载模块的话(准备榨出最后一滴性能的您可能希望如此),编译服务器时请设定参数-DDYNAMIC_MODULE_LIMIT=0。这将节省出为动态加载模块而分配的内存。

附录:对某次跟踪状况的详细分析

本附录描述了运行在Linux上的Apache 1.3系统调用的跟踪情况。运行时(run-time)配置文件中除了必要的缺省选项外还增加了:

AllowOverride none

Options FollowSymLinks

被请求的文件是一个6K的静态网页,其中不包含特殊内容。对于非静态或者伴随有内容协商的请求,跟踪结果将有明显的不同(一些情况下会十分晦涩)。我们将首先列出完整的跟踪结果,然后逐条进行分析。(它是由strace跟踪程序生成的,其他类似的程序包括truss、ktrace和par)

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

||||||accept串行化:

flock(18, LOCK_UN) = 0

...

flock(18, LOCK_EX) = 0

这两个调用可以被上文提到的宏SINGLE_LISTEN_UNSERIALIZED_ACCEPT去掉。

对信号SIGUSR1的处理:

sigaction(SIGUSR1, {SIG_IGN}, {0x8059954, [], SA_INTERRUPT}) = 0

...

sigaction(SIGUSR1, {SIG_IGN}, {SIG_IGN}) = 0

...

sigaction(SIGUSR1, {0x8059954, [], SA_INTERRUPT}, {SIG_IGN}) = 0

引起它们的原因是"温和重启"(graceful restart --用SIGUSR1而不是SIGHUP使Apache重新启动。这种方式为Apache留出了自行处理的余地--译者注)。当父进程收到SIGUSR1后,它将把这一信号传递给所有子进程(同时递增在共享内存中更新计数器的值"generation counter")。所有空闲的子进程(即在两次连接之间的子进程)收到信号后将立即终止。所有处于持续连接(keep-alive)但在两次请求之间的子进程也将立即终止。但处于连接中并等待第一次请求的子进程将不会立即终止。

为了说明它的必要性,请考虑一个浏览器对已关闭连接的处理。如果已关闭的连接是持续连接,而且下一个请求不是该连接的第一个请求,浏览器将不动声色地建立另一个连接并重新发出请求。由于服务器任何时候都可能关闭一个持续连接(可能因为超时或者超过了最大请求数目),这样处理是有必要的。但是,如果在回应第一个请求前连接就被关闭,浏览器通常会显?quot;文档中无数据"的对话框(或者显示被折断的图片)。这是在假设服务器非正常终止(或者太忙)的情况下作出的反应。因此,Apache力图避免在作出任何回应前就关闭连接。这就是处理SIGUSR1的原因。

尽管在理论上避免那三个调用是可行的,但在粗略的测试中这样的改进是微不足道的。

为了实现虚拟主机,Apache需要用本地socket地址接受连接:

getsockname(3, {sin_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0

在许多情况下都有可能去掉这个调用(比如没有虚拟主机,或者在Listen命令中的地址不含通配符)。但目前尚未对此进行优化。

Apache关掉了Nagle算法:

setsockopt(3, IPPROTO_TCP1, [1], 4) = 0

考虑到John Heidemann的论文中提及的问题,请关注两个time调用:

time(NULL) = 873959960

...

time(NULL) = 873959960

第一个出现在请求的开始,另一个用于写入日志。前者是正确实现HTTP协议所必须的,后者的出现是因为通用日志格式指定了记录中包含时间戳。自定义日志模块能够去掉这个调用。或者您可以用某些方法把时间移到共享内存中。参见补丁一节。

正如前文描述的那样,ExtendedStatus On将引发两次gettimeofday调用和一次times调用:

gettimeofday({873959960, 404935}, NULL) = 0

...

gettimeofday({873959960, 417742}, NULL) = 0

times({tms_utime=5, tms_stime=0, tms_cutime=0, tms_cstime=0}) = 446747

ExtendedStatus Off (即缺省值)将避免这些调用。

这个调用看起来也许有些奇怪:

stat("/home/dgaudet/ap/apachen/htdocs/6k", {st_mode=S_IFREG|0644, st_size=6144, ...}) = 0

它用于生成CGI程序所需的PATH_INFO环境变量。事实上,对于指向/cgi-bin/printenv/foobar的某个请求,stat将被调用两次。第一次调用查询/home/dgaudet/ap/apachen/cgi-bin/printenv/foobar,但它并不存在;第二次调用查询/home/dgaudet/ap/apachen/cgi-bin/printenv,它是存在的。无论如何,对于静态请求而言,至少需要一次调用。文件尺寸和修改时间被用来生成HTTP头标(比如Content-Length和Last-Modified)并用来实现协议的特色部分(比如If-Modified-Since)。一个聪明的服务器能够对非静态的请求避免调用stat,但是这样的实现对于模块化的Apache来说太难了。

||||||所有静态文件使用mmap:

mmap(0, 6144, PROT_READ, MAP_PRIVATE, 4, 0) = 0x400ee000

...

munmap(0x400ee000, 6144) = 0

在一些系统上mmap小文件的效率不如直接读取该文件。宏MMAP_THRESHOLD用来设置应用mmap时的最小文件尺寸。缺省值是0(但在SunOS4上的缺省值是8129。实验证明这个值在该系统上比较理想)类似lmbench的工具可以帮助您在您的系统上进行优化设置。

您也许乐意在MMAP_SEGMENT_SIZE上做个实验(缺省值32768)。它决定了被mmap的文件将以一次多少个字节写出。Apache只在每次write之间重置客户的超时时间,因此把这个值设得过大容易把带宽较窄的用户拒之门外--除非同时增加Timeout。

您的系统有可能根本不用mmap。如果是这样的话,定义USE_MMAP_FILES和HAVE_MMAP也许会奏效(如果它真的有效请告诉我们)。

Apache尽全力避免在内存中拷贝数据。对任何请求的首次写出都将借助writev合并头标及第一块数据:

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

当进行HTTP/1.1块状编码时,Apache将生成最多为4个元素的writev。它的目标是将字节拷贝至内核,这是典型情况下必须做的事情(为了组装网络数据包)。2.0.31之前的Linux并不进行合并,而是为每个元素生成一个数据包。因此升级系统是一个好主意。定义NO_WRITEV将阻止这种合并,但将使得块状编码的性能很差。

日志文件的写入工作

write(17, "127.0.0.1 - - [10/Sep/1997:23:39"..., 71] = 71

能够被宏定义BUFFERED_LOGS推迟。这种情况下,在真正写入文件之前,最多PIPE_BUF个字节(POSIX标准定义的常量)的日志信息将被缓存。由于写入条目不是atomic的(就是说来自不同子进程的信息将混合在一起),因此跨越PIPE_BUF边界的条目不会被分割。当子进程终止时,Apache用出色的方式将缓存排空。

延迟关闭引发了四个系统调用:

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

这些在前文已经提及。

当我 们使用了-DSINGLE_LISTEN_UNSERIALIZED_ACCEPT、-DBUFFERED_LOGS 和 ExtendedStatus Off 的优化参数后,最终得到的跟踪结果如下:

accept(15, {sin_family=AF_INET, sin_port=htons(22286), sin_addr=inet_addr("127.0.0.1")}, [16]) = 3

sigaction(SIGUSR1, {SIG_IGN}, {0x8058c98, [], 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) = 873961916

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) = 0x400e3000

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) = 873961916

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, {0x8058c98, [], SA_INTERRUPT}, {SIG_IGN}) = 0

munmap(0x400e3000, 6144) = 0

只剩下19个系统调用了。其中有四个很容易被移走,但没有什么必要了。

附录:可用的补丁

这里有一些1.3版的性能补丁。但随着1.3.0版的发行它们可能有些过时。对掌握一点C语言的知识的人来说,升级它们并不是难事。特别地:

有一个补丁去掉所有time(2)系统调用。

有一个补丁去掉mod_include中的许多系统调用,它们仅出于向后兼容而被少数站点所使用。

有一个补丁结合了上面两个特性,并通过去掉一些特性使服务器加速。

||||||附录:预分支(Pre-Forking)模型

Unix上的Apache是应用了预分支模型的服务器。父进程的责任仅在于繁衍子进程,它从不响应来自socket的任何请求。真正处理连接的是子进程,每个子进程在终止之前会(逐一地)为多个连接服务。父进程根据服务器负载的变化(通过监视记分板,记分板由子进程负责保持同步)生成新的或者杀掉旧的子进程。

这种模型为服务器提供了其他模型所不具备的健壮。父进程的代码非常简单,它有足够的信心保证在不出现错误的情况下持续运行。子进程就很复杂了,而且当您加入了第三方提供的模块后,将冒segmentation fault和其他崩溃的危险。即便这样的事情发生了,也只会影响到一个连接。父进程将继续为请求服务,并迅速替换掉已经死亡的子进程。

预分支在不同的Unix之间有良好的可移植性。Apache向来将它作为重要的目标之一,并且将保持下去。

但预分支模型由于各种各样的性能问题而饱受批判。主要的因素是分支进程带来的负担、上下文切换带来的负担和多个进程为内存带来的负担。另外它不能为请求提供有效的缓存机制(比如mmap文件池)。另有一些模型。JAWS project的论文对它们进行了详细的分析。实际上,所有这些模型带来的性能损失在不同操作系统上的差别迥异。

Apache的内核代码已经支持多线程。NT上的Apache 1.3就是多线程的。至少有另外两种实验性的多线程Apache:一个基于1.3内核并运行在DCE上;另一个基于1.0内核,它使用了一套自定义的用户级线程库。它们都不是对公众发行的。有一个已经发行了的Apache实验版本:运行于Netscape运行时可移植(Portable Run Time)平台上的1.3版,可以在此下载(如果您准备使用它的话,欢迎您加入new-httpd邮件列表)。被重新设计的Apache2.0将包含抽象化的服务器模型,它使我们可以既支持预分支模型,又支持多种线程模型。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有