如何编写高性能的网络服务器

(转贴)如何编写高性能的网络服务器


from : [url]http://team.eyou.com/[/url]

Scalable Network Programming
Or: The Quest For A Good Web Server (That Survives Slashdot)
Scalable Network Programming
Felix von Leitner
[email][email protected][/email]
[email][email protected][/email] 翻译
2003-10-16
简述
如何编写高性能的网络服务器
怎样编写能维持10000个连接的网络服务器
瓶颈在哪里 怎么避免它们呢

About me
我在一家安全咨询公司供职(公司名是Code Blau)
我喜欢编写高效但简短的代码
除了编写web server,我也曾写过一个ftp server,一个非常快的LDAP server(现
在还仅是只读的,如果有人愿意掏钱我会继续完成它),以及一个微型的libc实现(我
的所有程序都是依赖它编译的)
我对开源社区的贡献包括移植djbdns让它支持IPv6,编写了mutt邮件客户端的FAQ,
在Linux kernel邮件列表里面发表一些bug报告并成为热门话题

Why care about high performance network code
大多数情况下Apache已经足够用了
但如果你经常上Slashdot,你会发现链接的站点经常无法访问,因为它们无法处理这么
大的负载
Slashdot使用8台P3/600,1G内存,1万转的SCSI硬盘
[url]www.heise.de[/url]使用4台P3/650,1G内存(译者注:从下文看似乎这是德国的大网站)
ftp.fu-berlin.de是一台SGI Origin 200,包括2个R10k/225,1G内存,1万转
SCSI硬盘(译者注:似乎是原作者维护的一台FTP)
当然可以通过购买硬件获得更高的性能,但我们如何才能确认软件不是瓶颈呢

Why is it important to handle many connections
我曾经受理过一起网络玩具店的求助,那是12月份的一个雨天
圣诞期间的销售量比平时要大得多
据他们说web服务器受到了攻击,某种类型的分布式攻击产生了10万个web连接
结果导致了每台机器出现了2万个apache进程,性能急剧恶化
此时不可能再销售任何商品
我估计他们担心会因此破产

First, let's write a web client
char buf[4096];
int len;
int fd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in si;
si.sin_family=PF_INET;
inet_aton("127.0.0.1",&si.sin_addr);
si.sin_port=htons(80);
connect(fd,(struct sockaddr*)si,sizeof(si));
write(fd,"GET / HTTP/1.0/r/n/r/n");
len=read(fd,buf,sizeof(buf));
close(fd);

That's it
当然,上述代码还不够完善
1. 我没有#include任何头文件
2. 缺乏错误处理
3. 客户端仅仅读取4k数据
4. 并没有请求真实的URL
除了这几个地方,这段代码可说是一个相当精致的程序

OK, then let's write a web server!
int cfd,fd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in si;
si.sin_family=PF_INET;
inet_aton("127.0.0.1",&si.sin_addr);
si.sin_port=htons(80);
bind(fd,(struct sockaddr*)si,sizeof(si));
listen(fd);
while ((cfd=accept(fd,(struct sockaddr*)si,sizeof(si)) != -1) {
read_request(cfd); /* read(cfd,...) until "/r/n/r/n" */
write(cfd,"200 OK HTTP/1.0/r/n/r/n"
"That's it. You're welcome.",19+27);
close(fd);
}

This server sucks!!!
上述服务器代码除了还没有实现真正的协议以外,尚存在的一个问题就是只能处理一个
客户端请求.下面再修改一下:
while ((cfd=accept(fd,(struct sockaddr*)si,sizeof(si)) != -1) {
if (fork()>0) continue; /* handle connection in a child process */
read_request(cfd); /* read(cfd,...) until "/r/n/r/n" */
write(cfd,"200 OK HTTP/1.0/r/n/r/n"
"That's it. You're welcome.",19+27);
close(fd);
exit(0);
}

One process per connection – is that a good idea
对于每一个客户请求连接创建一个进程处理绝对有可伸缩性(scalability)的问题
实现一个良好的fork()是非常困难的.我们写一个程序来做基准测试:
pipe(pfd);
for(i=0;i0){
write((pfd[1],"+",1); block(); exit(0);
}
read(pfd[0],buf,1);
gettimeofday(&b,0);
printf("%llu/n",difference(&a,&b));
}

latency: forking a process
译者注:请参考原pdf第10页图表,作者比较了Linux 2.4,NetBSD 1.6.1,Linux
2.6,FreeBSD 5.1,OpenBSD 3.4
这个图表有一个地方很让人迷惑,就是Linux 2.4相关的颜色有两条线(可以参考
[url]http://bulk.fefe.de/scalability/[/url]的说明),一条线表明它是性能最好的系统,另一
条表明在进程数少的时候Linux 2.4性能比FreeBSD 5.1强,但超过1000个进程以后,
FreeBSD 5.1表现就比它要好.
Linux 2.6比FreeBSD 5.1性能更好.这两个操作系统fork新进程的延迟时间不会随着
进程数目的增大而有所变化(scale well)
OpenBSD的性能不是一般的差,NetBSD次之,但还算马马虎虎.

fork: dynamic linking versus static linking
fork性能的一个重要因素是看fork过程中的工作量多少
有硬件内存管理单元(memory management unit)的现代平台上,fork仅仅拷贝页表
的映射
动态链接将对共享库的ELF段,Global Offset Table,以及其它东东创建大量的页表
映射
基准测试表明程序静态链接的时候性能确实显著提高了.
译者注:参考原pdf第12页,Linux 2.6 static > FreeBSD 5.1 static > Linux 2.6
dynamic > FreeBSD 5.1 dynamic

Just to make sure you understood these numbers
在我的Linux 2.6笔记本上,fork-and-do-something的延迟大约是200微秒
就是说我的笔记本一秒可以创建出5000个进程
或者说我的笔记本每月可以fork出130亿次
我的Athlon XP 2000+台式机则可以每秒fork 10000次,或者说每月260亿次
Heise Online,德国最大的站点,2003年9月份也不过1.18亿次页面访问

Scheduling
为什么在我的fork基准测试代码中包括了读管道操作 (译者注:为了父子进程同步)
这是因为随着进程越来越多不仅仅创建新进程越来越困难,而且挑选一个进程来执行也
越来越困难
操作系统中决定该执行哪个进程的模块被称之为scheduler(下面开始scheduler将翻
译为调度器)
操作系统上通常都有好几十个进程,但同一时刻真正在运行的只有那么一两个

Scheduling
Linux每百分之一秒中断一次当前进程(在Alpha系统上是千分之一秒;Linux/Unix上
这个值传统上被称之为HZ,而且是一个compile time constant.译者注:记得在
kerneltrap上看过一个2.6的1/1000秒HZ patch),来给其它进程以机会执行
当一个进程被阻塞的时候(比如IO等待)也可能引发调度器执行
scheduler的工作是选择一个进程执行,公平肯定是一个准则:所有的进程应该平等共
享CPU.但做到这一点非常困难
很明显会存在两条链表:一条是可以进入执行状态的进程,另一条是被阻塞的进程

Scheduling
Unix有一个机制让交互式的进程比批处理作业进程拥有更高的优先权,内核会每秒都给
每个进程计算一个称之为nice的值
如果系统有上万个进程,这个操作会搞定二级缓存
对于SMP来说更是致命的,因为进程表必须通过自旋锁这样的机制来被保护,那么如果
一个CPU开始计算nice,其它的CPU也无法做任何进程切换
商业Unix系统的一个典型解决方案就是每个CPU一个run queue,更进一步的优化包括
对run queue排序,for example with a heap(译者注:作者这里的意思应该是基于
nice做堆排序),甚至每个优先级一个run queue.

The Linux 2.4 Scheduler
Linux 2.4的调度器采取的是所有的可运行进程放在一个未排序的run queue里面,以
及所有的睡眠和僵尸进程放在一个任务列表里面
对于这个run queue的所有操作都通过spinlock保护
Linux 2.4也有好几个experimental的调度器,包括heap based priority queue或者
多个run queue什么的,但其中最让人惊异的就是Ingo Molnar的O(1)调度器
O(1)调度器是目前Linux 2.6的缺省调度器

The Linux 2.6 Scheduler
O(1)调度器对每个CPU都保持两个数组,对于每个可能的优先级数组都有一个对应条
目,该条目的值指向该优先级的链表
链表内包括了所有该优先级的任务,因为每条链表内的任务优先权相同,将一个指定优
先级的进程加入到这个数据结构里面的时间总是相同的
两个数组之一就是当前的run queue,所有的进程轮流依次运行,当它们执行完了分配
给它们的时间片后就被移动到另外一个数组里面去,当前的run queue空了以后,两个
数组被交换位置后继续执行上述过程
那些被调度器认为是批处理作业的进程会被中断并被惩罚,这样比提高交互式进程的优
先级更为合理

How important is scheduler performance
每1/100秒就有可能执行调度器,每当一个进程阻塞的时候调度器也有可能执行
调度器执行就是挂起一个进程,转而去执行另外一个进程,然后内核会给一个计数器增

这个"上下文切换"计数器能通过vmstat观察到它的变化
上下文切换的代价有多高是由硬件体系所决定的,基本上来说,寄存器越多,上下文切
换代价越高.从这点考虑x86平台比RISC平台要好得多,特别是对于SPARC或IA64平
台来说更是如此
译者注:参考原pdf第20页,作者究竟想表明什么意见还不是很清楚,但可以看出内核
态的CPU使用时间(sy),同上下文切换次数(cs)相比,几乎是线性的.而内核中断的
次数(in)对内核态运行时间没有什么影响

Other problems with running many process
创建进程数目多了后会引发另外的问题:内存消耗
每个进程都要使用内存,当一个进程fork出子进程, 两个进程的所有页面都会被设置
成copy-on-write.一旦一个进程写了一个页面,该页面将被复制(译者这里也不明
白...)
这很好,但大多数人没有意识how much their programs write all over the main
memory
举个例子,调用malloc或者free都可能导致链表的重新结合,或者树的平衡操作

Other problems with running many process
另外一个例子就是动态链接器,它会保持Global Offset Table,但缺省它工作得太懒
惰了,仅当函数被第一次调用的时候,该表内的相应偏移量才会被更新
就是说如果你fork 1000个子进程,而一个函数会在fork后才第一次被进程调用,那么
就会浪费1000个4k的页面
当然可以通过设置$LD_BIND_NOW环境变量,来通知动态链接器在启动后就更新所有的偏
移量
请注意更新1000个进程的Global Offset Table通常比fork 1000个进程还要慢,而且
在这个过程中测试系统是不可能有任何响应的

Memory Consumption
在Unix上度量内存占用比较困难
尽管有一个getrusage系统调用可以做这件事情,但是用它来检查内存使用的话,
Linux总是返回0,FreeBSD仅仅对动态链接的程序返回数据
在Linux系统上,可以通过/proc/self/statm来取得这些数据
% ../show/memusage-glibc-c
1164K total size, 324K resident, 1132K shared
% ../show/memusage-glibc-static-c
364K total size, 112K resident, 348K shared
% ../show/memusage-diet-c
32K total size, 32K resident, 16K shared
译者注:diet是原作者写的一个mini的libc实现

The one-process-per-connection model
这种模式工作还算有效,甚至有许多标准工具来专门提供此类服务
Unix系统都包括一个叫inetd的服务程序,它不仅仅是对每个连接fork新的进程,同时
还去执行一个指定程序来提供服务
很不幸,inetd有一些缺陷给它(以及这种模式)带来了恶名,一些人开始编写他们自
己的替代程序,包括tcpserver,xinet,ipsvd
这些替代品变化并不大,但一个显著的特征是包括了IP访问控制的机制
我写的第一个web server——fnord,就是使用这种模式,现在[url]www.fefe.de[/url]仍然在使
用它

The one-process-per-connection model
Aapche同样使用这种每个连接一个进程的模型
当然,为了避免fork延迟,Apache使用了pre-fork机制:事先fork子进程,然后接
受请求并指派子进程处理
但等等,还应该看看整个Apache的基准测试图表
译者注:请参考原pdf第29页,看起来似乎每次重新fork子进程的时候,就会出现很
大的延时

Since processes are so slow,why not use threads instead
大家都知道fork是慢的,而可以用thread来替代它
其实真相非常复杂
fork并不总是慢的,就如同前面做的基准测试所表明的.但在一些系统上,fork的确非
常缓慢,包括Solaris
显然因为解决fork问题很困难(也可能他们的所有工程师都去发明Java了),process
的替代品thread被发明了
两年以前(译者注:就是2001年,那个时候已经有Solaris8了),我发现相同硬件下
Solaris上的pthread_create居然比Linux的fork还要慢.现在我已经无法再访问那
台系统了,没法再次做同样的试验
译者注:请参考原pdf第31页,基于Linux 2.6/glibc 2.3.2做的测试,从大概400个
连接后,fork的性能超过了pthread_create的性能

Multithreading Performance
线程的一个巨大障碍就是在一个弱智的操作系统上,创建线程仍然缓慢,Solaris和
Windows上也许创建线程比创建进程要快一些,但仍然太慢了
猜猜他们怎么做的 他们发明了"线程池"
线程池工作方式类似于pre-fork,程序启动之初创建一堆线程,然后让分配任务给不同
的线程
当连接数高于线程数目的时候,新的连接不得不等待,但除了这点以外,的确避免了线
程创建的开销
我不知道你们怎么想的,但我仍然不能称这种方式是可伸缩的

Positive aspects of threading
对Java和线程也有一些好消息:硬件越来越快,内存越来越便宜
对软件也是一样:应用层的无能逼迫操作系统做了不少重大革新.比如因为Lotus
Notes对每个客户端都保持一个打开的连接,Linux上的"一个进程处理10万个连接"
的优化就主要是由IBM完成的
O(1)调度器也是源起于看起来不相干的Java基准测试
The bottom line is that bloat benefits all of us. We just need to make sure
that there are always small and efficient alternatives to all the crapware.
译者注:依译者的观点看,Apache2的worker模型仍然是一个优越的方案——multi-
thread避免了内存消耗,multiprocess绕过了单一进程文件描述符上限的问题
(FreeBSD缺省3000多,Linux缺省是1024,但其它操作系统可能就更少)

How is timeout handling done
对网络服务器来说一个麻烦的问题是:如何检测连接超时
网络服务器需要检测到那个客户端连接上来后什么事情也没有做(而只是消耗服务器的
进程数或线程数)
不论你怎么按照前面所教的去优化,你(以及操作系统)都不得不对每一个创建的连接
进行处理,保持状态,但系统资源应该为真正的活动连接而服务

How is timeout handling done
Unix有一个标准的系统调用可以处理这件事情
alarm(23);/* deliver SIGALRM in 23 seconds */
未经特殊处理的SIGALRM信号将中止当前进程
这样23秒后,内核会杀死处理该连接的进程
由于新的对alarm的调用,会覆盖上一次的alarm设定
因此只需要每次检测到网络活动的时候重新执行一遍alarm就可以

Timeout handling with select()
select可以等待一个或多个文件描述符的时间(select自1983年在4.2BSD上出现)
fd_set rfd;
struct timeval tv;
FD_ZERO(&rfd); FD_SET(0,&rfd);/* fd 0 */
tv.tv_sec=5; tv.tv_usec=0;/* 5 seconds */
if (select(1,&rfd,0,0,&tv)==0)/* read,write,error,timeout */
handle_timeout();
if (FD_ISSET(0,&rfd))
can_read_on_fd_0();
参数里面超时值可以精确到微秒级,其实Unix自己无法提供这么高的精度.第一个参数
是所有fd的最大值再加1. 真恶心(译者注:原文为 *Puke*)

Disadvantages of select()
select无法告诉你等待该事件花了多长时间,必须再执行gettimeofday()来得到.尽
管在Linux的select实现了这个功能,但使用这个特性程序将不具备可移植性
select依赖位向量工作(译者注:思考一下FD_SET系列宏的实现),对文件描述符的
最大值有限制,具体和操作系统相关,如果你足够幸运,最大值能达到1024(比如
Linux系统),但大多数的Unix系统比这个要少(译者注;从上下文看,这里说的是
select第一个参数的上限而不是单进程打开文件数目上限,尽管Linux实际的单进程打
开文件数上限同样是1024)
关于限制有些非常糟糕的例子,比如一些实现的很弱智的DNS库使用select来处理超
时.如果你打开了1024个文件,DNS将立即失效,因为DNS再创建socket将超过
1024(译者注:这里说的意思应该是操作系统可以打开大于1024个文件,但select
无法处理)!Apache处理这个问题的方法是manually keeping the descriptors
below 15 free(with dup2).(译者注:不大理解这段话,是指apache一启动就通过
调用12次dup2来打开文件3-14,然后需要调用DNS的时候就临时close一个,然
后调用库,返回后再继续dup2占个位置 否则什么叫"通过dup2手动保证低于
15的文件描述符都是空闲的")

Timeout handling with poll()
poll是select的一个变种(poll自1986年在System V R3上出现)
struct pollfd pfd[2];
pfd[0].fd=0; pfd[0].events=POLLIN;
pfd[1].fd=1; pfd[1].events=POLLOUT|POLLERR;
if (poll(pfd,2,1000)==0)/* 2 records, 1000 milliseconds timeout */
handle_timeout();
if (pfd[0].revents&POLLIN) can_read_on_fd_0();
if (pfd[1].revents&POLLOUT) can_write_on_fd_1();
优点:对记录数目,文件描述符值没有任何限制
缺点:并不是所有的系统上都实现了poll,目前仅有一个Unix变种没有提供poll:
MacOS X

Disadvantages of poll()
The whole array is unnecessarily copied around between user and kernel
space. The kernel then finds out about the events and sets the correspond-
ing revents.(译者注:不大明白这段的意思,看起来似乎和下一段话有关系)
现代CPU在内存等待上花费了太多时间.而poll会随着管理的文件描述符的增长导致内
存复制的线性增长(译者注:应该就是上一段的意思,但内存操作真那么慢么 )
实际情况并不那么糟糕,如果poll运行的时间比较长,事件也不会丢失,内核会保持该
事件队列,直到下一次poll调用再来通知.
另一方面,在Linux和FreeBSD上的实际例子表明,基于fork的web server比基于
poll的更好(译者注:所谓基于poll的应该是单进程的服务模型,不知道该模型是否使
用了线程,需要我们自己实际测试了)

Linux 2.4: SIGIO
Linux 2.4能像信号处理那样通知一个poll事件
int sigio_add_fd(int fd) {
static const int signum=SIGRTMIN+1;
static pid_t mypid=0;
if (!mypid) mypid=getpid();
fcntl(fd,F_SETOWN,mypid);
fcntl(fd,F_SETSIG,signum);
fcntl(fd,F_SETFL,fcntl(fd,F_GETFL)|O_NONBLOCK|O_ASYNC);
}
int sigio_rm_fd(struct sigio *s, int fd) {
fcntl(fd,F_SETFL,fcntl(fd,F_GETFL)&(~O_ASYNC));
}

Linux 2.4: SIGIO
SIGIO并不是poll的替代品,poll告诉你某个字已经准备好了,而SIGIO告诉你某个
字开始准备好了
举例,如果poll告诉你可以从描述符3读,但你不作任何操作,下一次调用poll它会再
次通知这个状态,SIGIO不是这样.The poll way is called level triggered, the
SIGIO way is called edge triggered.
当针对SIGIO阻塞后取得事件的最佳方法就是sigtimedwait.该操作存储一个同步状
态,避免了对锁定或者可重入函数的需求

Linux 2.4: SIGIO
for (;;) {
timeout.tv_sec=0;
timeout.tv_nsec=10000;
switch (r=sigtimedwait(&s.ss,&info,&timeout)) {
case -1:if (errno!=EAGAIN) error("sigtimedwait");
case SIGIO: puts("SIGIO queue overflow!"); return 1;
}
if (r==signum) handle_io(info.si_fd,info.si_band);
}
info.si_band相当于pollfd.revents

Disadvantages of SIGIO
SIGIO也有其问题,内核会保持一个事件的队列,然后把事件一个个的传递出来,也许
你会接受到一个已经关闭了的字的相关事件
事件队列是有大小限制的,如果队列满了,会得到一个SIGIO信号而无法取得事件
这样只能期望清空当前的队列(通过设置信号处理句柄为SIG_DFL),然后通过poll
来取得事件
该API避免了内存复制,但现在对于每个事件都需要执行一次系统调用,在Linux上尽
管系统调用负载很低,但并不能完全忽略
处理队列溢出也十分烦人,即使可以通过poll补救,但你也不愿意维护一个pollfd数
组.

What's wrong with the pollfd array
考虑实际的例子,poll告诉我们描述符5可以读了,然后read返回该连接已经断开,
我们得close这个字并从pollfd数组中把它去掉
可我们得怎么做呢 我们不能仅仅是把数组中的该记录清空,因为poll会失败在
EBADF,并设置revents为POLLNVAL.我们只能把数组的最后一项拷贝到此位置,
并减小该数组的大小.
可现在在数组中的第5项就不再是描述符5,我们会需要一个额外的索引来在pollfd中
查找一个指定的描述符的情况.这个索引需要额外的维护工作来扩展或收缩,这一切都
非常丑陋而且容易出错(译者注:不明白啊,顺序遍历pollfd不就可以完成这项工作
么 难道原作者以为这种方法不是O(1)的而很不爽么 )
And it is perfectly superfluous.(译者注:实在不明白,难道是反话)

/dev/poll
几年前Sun在solaris里面增加了一种类似poll的新API.打开/dev/poll,向该设备
写希望获得事件的描述符们,然后通过ioctl获得事件.
ioctl返回有多少个可返回的事件,然后这些事件可以通过/dev/poll设备读取出来,设
备将只返回符合期望的描述符而不是所有的数组.
这意味着无需再扫描整个数组得到所需要的事件了
有几个补丁在Linux内核中引入了类似的设备,但都没能被进入正式的内核.
没错,这些补丁在高负载下是不稳定的.

/dev/epoll
有一个patch是在2.4内核中增加了一个/dev/epoll的设备
int epollfd=open("/dev/misc/eventpoll",O_RDWR);
char *map;
ioctl(epollfd,EP_ALLOC,maxfds);/* hint: number of descriptors */
map=mmap(0,EP_MAP_SIZE(maxfds),PROT_READ,MAP_PRIVATE,epollfd,0);
如果希望捕获事件就向该设备写pollfd(结构体 ),如果希望撤销对一个事件的捕
获仍然写pollfd,只不过events的为0

/dev/epoll
事件通过ioctl捕获到
struct evpoll evp;
for (;;) {
int n;
evp.ep_timeout=1000;
evp.ep_resoff=0;
n=ioctl(e.fd,EP_POLL,&evp);
pfds=(struct pollfd*)(e.map+evp.ep_resoff);
/* now you have n pollfds with events in pfds */
由于使用了mmap,在内核空间和用户空间之间没有做任何内存复制

/dev/epoll
/dev/epoll的问题在于它仅仅是一个补丁,Linus不喜欢在内核里面出现一个新的伪设

他说既然我们已经可以在内核中分配系统调用,如果想加什么新应用,那就增加一个新
系统调用,而不是通过新的设备和ioctl
于是/dev/epoll的作者通过系统调用重新实现了一遍,该API最终进入了Linux 2.5
(从2.5.51开始就有文档描述了)
在linux 2.6中,推荐使用它完成事件通知功能

epoll
int epollfd=epoll_create(maxfds);
struct epoll_event x;
x.events=EPOLLIN|EPOLLERR;
x.data.ptr=whatever;/* you can put some cookie here */
/* changing is analogous; */
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&x);
/* deleting -- only x.fd has to be set */
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&x);
EPOLLIN...等常量定义实际上和POLLIN...等常量的值是一样的,但作者希望keep all
option open.epoll开始缺省是edge triggered但现在是level triggered的(译
者注:回忆一下SIGIO),通过|EPOLLET还能切换到edge triggered模式

epoll
下面是取得事件的代码:
for (;;) {
struct epoll_event x[100];
int n=epoll_wait(epollfd,x,100,1000);/* 1000 milliseconds */
/* x[0] ... x[n-1] are the events */
}
注意epoll_event里面并没有包括实际的描述符!
这就是为什么上面的代码里面需要cookie,这里可以存放一个文件描述符值,当然也可
以存放一个指向到整个连接相关信息的结构体指针.

FreeBSD: kqueue
kqueue是一个epoll和SIGIO的交叉,类似epoll可以有edge或level的触发事件,
kqueue也能完成文件和目录的状态通知.问题在于:该API不容易使用并没有很好的
文档.
kqueue比epoll要早.Linux本应该简单的实现kqueue相同的接口就可以了,没有
必要重新发明epoll,但Linux黑客们坚持要把别人已经犯过的错误再犯一次.比如
epoll的早期实现就根本没有考虑level triggering
epoll和kqueue的性能基本类似
kqueue也已经在OpenBSD上实现,但NetBSD还没有(译者注:原文档很老了,不
晓得现在是否还是这样)

FreeBSD: kqueue
怎样来请求一个事件通知或取消它:
#include
#include
#include
int kq=kqueue();
struct timespec ts;
EV_SET(&kev, fd, EVFILT_READ, EV_ADD|EV_ENABLE, 0, 0, 0);
ts.tv_sec=0; ts.tv_nsec=0;
kevent(kq, &kev, 1, 0, 0, &ts);
EV_SET(&kev, fd, EVFILT_READ, EV_DELETE, 0, 0, 0);
ts.tv_sec=0; ts.tv_nsec=0;
kevent(kq, &kev, 1, 0, 0, &ts);

FreeBSD: kqueue
下面是取得事件的代码:
struct kevent ev[100];
struct timespec ts;
ts.tv_sec=millisconds/1000;
ts.tv_nsec=(millisecons%1000)*1000000;
if ((n=kevent(io_master,0,0,y,100,milliseconds!=-1 &ts:0))==-1)
return -1;
for (i=0; i if (ev[i].filter=EVFILT_READ) can_read(ev[i].ident);
if (ev[i].filter=EVFILT_WRITE) can_write(ev[i].ident);
}

Windoze: Completion Ports
既便是微软也得在这方面做一些东西
它的方案就是线程池,每个线程通过一个类似SIGIO的机制活动
该方案也同时组合了线程的不足和SIGIO的不足.让人惊讶的是微软的市场部门把这个
当作一个史诗般的发明来歌颂.(译者注:本人没有任何发言权,但CSDN上的确有很
多人相当推崇"完成端口"的机制的,不晓得他们怎么看待原作者的这条评论)
I found this quote in the documentation: "First of all, threads are system re-
sources that are neither unlimited nor cheap."
Huh Not Cheap I thought that was the reason for their existance!
(译者注:原作者对微软应该是极度鄙视的,实在不知道怎么翻译这段内容)

Other reasons for high latency
POSIX API规定,每当新打开一个文件,内核必须使用最小的,未被使用的描述符.
(比如接受一个网络连接,创建一个socket都是这样)
很明显该算法不可能是O(1)的,Linux使用的是位向量.
Linux内核邮件列表上也曾有相关的热烈讨论——给open系统调用增加新的标志位,
可以让行为不遵守POSIX标准,返回一个可用的描述符就可以了.但从来没有出现这样
的功能.而且即使open有了这样的功能,socket调用也无法从中获益.
在Linux和所有的BSD上,socket系统调用倒还能算是scales well的(译者注:请
参考原pdf第55页)

Other reasons for high latency
对客户端应用或者proxy服务器也有同样的问题
如果你打开一个socket去连接服务器,并且没有明确指定端口号的话,内核将选择一个
可用的端口来使用
并没有标准要求内核必须返回一个最小的可用port,但通常都是这么实现的
测试表明不论是IPv4还是IPv6性能曲线都差不多,因此只给出IPv4的图表(译者
注:参考原pdf第57页,看起来FreeBSD 5.1,Linux 2.4/2.6还可以算scales
well,OpenBSD和NetBSD就差一些)

Fragmentation and seeking
磁盘顺序读是很快的,但需要寻道的时候就会很慢
这就是为什么大型服务器应用都使用10k甚至15k的硬盘
磁盘吞吐量是相当主要考虑的因素
从图表可以看出(译者注:参考原pdf第58/59页,本页不打算继续翻译了...)

sendfile
sendfile系统调用类似write,它直接把文件连接到一个socket上,即不准备buffer
缓冲,也不准备read该文件
这个系统调用避免了把文件数据拷贝到用户空间后,再拷贝回内核空间传递给write
现代网卡(包括所有的千兆网卡)都支持scatter-gather I/O.即从kernel buffer取
得包头,而包内容来自buffer cache(note:csum_partial_copy_from_user)
该技术就是所谓的Zero Copy TCP
Linux和FreeBSD都有sendfile系统调用(但有些不同)
NetBSD和OpenBSD没有sendfile调用

Memory Mapped I/O
如果文件只读,仅仅执行把文件内容读入到内存中的动作,那么一个替代方案是把文件
映射到用户空间中
该系统调用称之为mmap,对于设计可伸缩性网络I/O而言它非常重要,因为它避免了
buffer(译者注:类似上述的sendfile,它也避免了一次内存复制)
操作系统可以把节省的内存用于buffer cache
然而,维护页表的工作却不能scale properly.对于64位系统尤其如此
译者注:参考原pdf第62页,mmap系统调用性能最好是Linux 2.6,其次是
OpenBSD 3.4,再次是Linux 2.4,随后是NetBSD 1.6.1,FreeBSD 5.1最糟糕.
参考原pdf第63,64,65的图表,随着mmap页面数目的增多,从mmap的新页
面中读取性能最好的是FreeBSD 5.1,其次是Linux 2.6/2.4,NetBSD 1.6.1差劲的
不成比例,OpenBSD 3.4简直表现得一塌糊涂

Filesystem latency
如今文件系统延时应该已经不成问题,这里提一个罕见的例子
我曾经维护局域网上的一台FTP服务器,它允许incoming
有一个人决定把他所有的色情图片全部放上来
当他上载了50000个文件后我发现了这个用户并立刻踢他下线,因为此时服务器已经缓
慢异常了
操作系统几乎100%的CPU都在内核态,以反复调整这个目录下的文件在内存中的列
表.
今天我们处理同一目录下大量文件已经有了类似XFS,reiserfs这样的文件系统,而
ext3或FreeBSD UFS也能支持目录哈希

Asynchronous I/O
POSIX提供了异步I/O的规范,不幸的是几乎任何人都没有实现它,Linux也不例外
(译者注:这里有些迷惑,2.6明明有AIO的,不过好像只支持磁盘IO的说)
libc has a emulation that creates one thread for each request. This is much
worse than not having the API in the first place, so nobody uses the API.(译
者注:这里说的是不是就是2.6 AIO )
异步IO的思路就是给读请求排队,可以询问操作系统该请求是否已经完成,或者当请求
完成的时候得到一个信号
问题在于:当你收到一个信号的时候,你无法知道是哪个请求完成了

Asynchronous I/O
Async I/O仅对文件有意义,而不是socket.如果希望从一个巨大的数据库里面读取
1000个块,而使用了lseek和read来完成,那就意味着让操作系统通过指定的顺序读
取文件,而不论这些块是在磁盘上如何排列的.
通过async I/O,操作系统可以基于这些块在磁盘上的排列重新调整顺序,然后磁头一
次性读取这些数目而无需来回移动寻道.理论上,非常美好;实际应用表明作用不大
(译者注:记得Ora的某个版本AIO支持有问题,导致启用AIO后性能急剧下降)
Solaris有它自己的专有async I/O API实现,可同样没有提供POSIX规范API,调用
它们会返回ENOSYS
提供async I/O的最重要的操作系统就是FreeBSD了

writev, TCP_CORK and TCP_NOPUSH
HTTP服务器写回客户端一个HTTP头标,然后是文件的内容.调用write(或者
sendfile),内核首先发送一个包含头标的TCP包,然后创建后继的TCP包,传送文
件内容
如果文件内容很小,比如只有100字节,那么一个TCP包就足以同时容纳头标和内容了
一个很明显的解决方案是准备一个缓冲,把头标和内容都复制到缓冲内,然后只执行一
次write...且慢!我们还需要使用zero-copy TCP的,还记得吗
这里有4个解决方案:writev,TCP_CORK(Linux),TCP_NOPUSH(BSD),以及
FreeBSD的sendfile

writev
writev相当于批处理写,给定一组指针和长度的集合,然后它一次性把它们写出去
The difference is too small to notice normally, except for TCP connections.
struct iovec x[2];
x[0].iov_base=header;x[0].iov_len=strlen(header);
x[1].iov_base=mapped_file; x[1].iov_len=file_size;
writev(sock,x,2);/* return bytes written */

TCP_CORK
int null=0, eins=1;
/* put cork in bottle */
setsockopt(sock,IPPROTO_TCP,TCP_CORK,&eins,sizeof(eins));
write(sock,header,strlen(header));
write(sock,mapped_file,filesize);
/* pull cork out of bottle */
setsockopt(sock,IPPROTO_TCP,TCP_CORK,&null,sizeof(null));
BSD上的TCP_NOPUSH工作类似,但必须在最后一次写之前把标志位设成0.这个逻
辑就有些复杂了,不如TCP_CORK好用
FreeBSD的sendfile和Linux的sendfile实现有些微不同,它的参数里增加了类似
writev那样的向量,一个指向头,一个指向尾.在FreeBSD上就可以不考虑
TCP_NOPUSH了(译者注:感觉这样的sendfile实现纯粹就是为应用而设计的!甚至
咱们MTA的remote就可以考虑这么使用)

vfork
现代Unix上fork是很快的,因为它并不做真正的内容复制,而仅仅是复制页表
当然,这样的代价也还是挺昂贵的,尤其是fork只是为了执行一个CGI程序而言.这就
是为什么Linux和BSD都提供了vfork调用的原因
但vfork并不总是那么快!在Linux 2.6上,测试程序静态链接了自己编写的dietlibc
库,vfork就比fork慢——250ms vs. 180ms,如果是链接glibc的话则是250ms
vs. 320ms

So which OS is the best one
我的推荐是Linux 2.6,在所有的基准测试中都表现了O(1)的水准
FreeBSD 5.1其次,除了mmap外,都是O(1)
在多进程和mmap测试中Linux 2.4表现不好,换成2.6吧
NetBSD没有kqueue和sendfile,仅仅只有poll.但还能算得上高性能操作系统
OpenBSD让人大跌眼镜,磁盘性能烂透了......(文字的东西就不再翻译了)
原pdf第75页:从Linux 2.6执行connect测试服务器的连接延时情况,NetBSD
poll的性能比OpenBSD的kqueue要强
第76/77页:测试服务器处理HTTP请求的能力,NetBSD poll的性能就比OpenBSD
的kqueue差了,Linux 2.6 epoll的表现本来比FreeBSD kqueue要强,但当连接数
超过4000后,FreeBSD kqueue的性能猛然有一个跃升,竟然超过了自己在低连接数
下的性能表现,当然同时也超过了Linux 2.6,非常可疑,像是在作弊一样,呵呵

Questions
Thanks for sitting through this with me in this ungodly hour!
You can get the source code for all benchmarks via anonymous cvs:
% cvs -d:pserver:[email protected]:/cvs -z9 co libowfat
% cvs -d:pserver:[email protected]:/cvs -z9 co gatling
My web page is at [url]http://www.fefe.de/[/url]
You can email me at [email][email protected][/email]
以上内容就无需翻译了,感谢Felix的精彩讲解,某种程度上可以和R. Stevens的《网
络编程》媲美了
原文在:[url]http://bulk.fefe.de/scalable-networking.pdf[/url]
图表说明:[url]http://bulk.fefe.de/scalability/[/url]

你可能感兴趣的:(Socket相关,BDB)