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

ScalableNetwork Programming

Or: TheQuest For A Good Web Server (That Survives Slashdot)

ScalableNetwork Programming

Felix vonLeitner

felix-linuxkongress@fefe.de

qyb@eyou.net 翻译 2003-10-16

 

简述

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

怎样编写能维持10000个连接的网络服务器  ?

瓶颈在哪里怎么避免它们呢 ?

 

Why care about high performance network code

大多数情况下Apache已经足够用了

但如果你经常上Slashdot,你会发现链接的站点经常无法访问,因为它们无法处理这么

大的负载Slashdot使用8台P3/600,1G内存,1万转的SCSI硬盘

www.heise.de

使用4台P3/650,1G内存(译者注:从下文看似乎这是德国的大网站)

ftp.fu-berlin.de是一台SGI Origin200,包括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:

 

charbuf[4096];//4 x 1024

int len;

 

intfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);

 

structsockaddr_in si;

si.sin_family=PF_INET;

inet_aton("127.0.0.1",&si.sin_addr);

si.sin_port=htons(80);

 

connect(fd,(structsockaddr*)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!

 

intcfd,fd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP);

 

structsockaddr_in si;

si.sin_family=PF_INET;

inet_aton("127.0.0.1",&si.sin_addr);

si.sin_port=htons(80);

 

bind(fd,(structsockaddr*)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,"200OK 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,"200OK 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;i<4000;++i)

{

gettimeofday(&a,0);

If(fork()>0)

{

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页图表,作者比较了Linux2.4,NetBSD 1.6.1,Linux 2.6,FreeBSD 5.1,OpenBSD 3.4

 

这个图表有一个地方很让人迷惑,就是Linux2.4相关的颜色有两条线(可以参考

http://bulk.fefe.de/scalability/的说明),一条线表明它是性能最好的系统,另一条表明在进程数少的时候Linux2.4性能比FreeBSD 5.1强,但超过1000个进程以后,FreeBSD 5.1表现就比它要好.Linux 2.6比FreeBSD 5.1性能更好.这两个操作系统fork新进程的延迟时间不会随着进程数目的增大而有所变化(scalewell),OpenBSD的性能不是一般的差,NetBSD次之,但还算马马虎虎。

 

fork: dynamic linking versus static linking

fork性能的一个重要因素是看fork过程中的工作量多少有硬件内存管理单元(memorymanagement unit)的现代平台上,fork仅仅拷贝页表的映射动态链接将对共享库的ELF段,Global OffsetTable,以及其它东东创建大量的页表映射基准测试表明程序静态链接的时候性能确实显著提高了.

译者注:参考原pdf第12页,Linux2.6 static > FreeBSD 5.1 static > Linux 2.6

dynamic> FreeBSD 5.1 dynamic Just to make sure you understood these numbers在我的Linux2.6笔记本上,fork-and-do-something的延迟大约是200微秒

就是说我的笔记本一秒可以创建出5000个进程,或者说我的笔记本每月可以fork出130亿次

我的AthlonXP 2000+台式机则可以每秒fork 10000次,或者说每月260亿次!而HeiseOnline,德国最大的站点,2003年9月份也不过1.18亿次页面访问

 

Scheduling

为什么在我的fork基准测试代码中包括了读管道操作(译者注:为了父子进程同步)

这是因为随着进程越来越多不仅仅创建新进程越来越困难,而且挑选一个进程来执行也

越来越困难操作系统中决定该执行哪个进程的模块被称之为scheduler(下面开始scheduler将翻译为调度器)操作系统上通常都有好几十个进程,但同一时刻真正在运行的只有那么一两个,SchedulingLinux每百分之一秒中断一次当前进程(在Alpha系统上是千分之一秒;Linux/Unix上 这个值传统上被称之为HZ,而且是一个compile timeconstant.译者注:记得在 kerneltrap上看过一个2.6的1/1000秒HZ patch),来给其它进程以机会执行当一个进程被阻塞的时候(比如IO等待)也可能引发调度器执行scheduler的工作是选择一个进程执行,公平肯定是一个准则:所有的进程应该平等共享CPU.但做到这一点非常困难

很明显会存在两条链表:一条是可以进入执行状态的进程,另一条是被阻塞的进程

 

Scheduling

Unix有一个机制让交互式的进程比批处理作业进程拥有更高的优先权,内核会每秒都给

每个进程计算一个称之为nice的值如果系统有上万个进程,这个操作会搞定二级缓存

对于SMP来说更是致命的,因为进程表必须通过自旋锁这样的机制来被保护,那么如果

一个CPU开始计算nice,其它的CPU也无法做任何进程切换

商业Unix系统的一个典型解决方案就是每个CPU一个runqueue,更进一步的优化包括

对runqueue排序,for example with a heap(译者注:作者这里的意思应该是基于

nice做堆排序),甚至每个优先级一个runqueue.

 

The Linux 2.4 Scheduler

Linux2.4的调度器采取的是所有的可运行进程放在一个未排序的run queue里面,以

及所有的睡眠和僵尸进程放在一个任务列表里面

对于这个runqueue的所有操作都通过spinlock保护

Linux2.4也有好几个experimental的调度器,包括heap based priority queue或者

多个runqueue什么的,但其中最让人惊异的就是Ingo Molnar的O(1)调度器

O(1)调度器是目前Linux2.6的缺省调度器

The Linux2.6 Scheduler

O(1)调度器对每个CPU都保持两个数组,对于每个可能的优先级数组都有一个对应条

目,该条目的值指向该优先级的链表

链表内包括了所有该优先级的任务,因为每条链表内的任务优先权相同,将一个指定优

先级的进程加入到这个数据结构里面的时间总是相同的

两个数组之一就是当前的runqueue,所有的进程轮流依次运行,当它们执行完了分配

给它们的时间片后就被移动到另外一个数组里面去,当前的runqueue空了以后,两个

数组被交换位置后继续执行上述过程

那些被调度器认为是批处理作业的进程会被中断并被惩罚,这样比提高交互式进程的优

先级更为合理

 

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.一旦一个进程写了一个页面,该页面将被复制(译者这里也不明白...)

这很好,但大多数人没有意识howmuch their programs write all over the main memory

举个例子,调用malloc或者free都可能导致链表的重新结合,或者树的平衡操作

 

Other problems with running many process

另外一个例子就是动态链接器,它会保持GlobalOffset Table,但缺省它工作得太懒

惰了,仅当函数被第一次调用的时候,该表内的相应偏移量才会被更新

就是说如果你fork1000个子进程,而一个函数会在fork后才第一次被进程调用,那么

就会浪费1000个4k的页面

当然可以通过设置$LD_BIND_NOW环境变量,来通知动态链接器在启动后就更新所有的偏

移量

请注意更新1000个进程的GlobalOffset Table通常比fork 1000个进程还要慢,而且

在这个过程中测试系统是不可能有任何响应的

 

Memory Consumption

在Unix上度量内存占用比较困难

尽管有一个getrusage系统调用可以做这件事情,但是用它来检查内存使用的话,

Linux总是返回0,FreeBSD仅仅对动态链接的程序返回数据

在Linux系统上,可以通过/proc/self/statm来取得这些数据

%../show/memusage-glibc-c

1164Ktotal size, 324K resident, 1132K shared

%../show/memusage-glibc-static-c

364Ktotal size, 112K resident, 348K shared

%../show/memusage-diet-c

32K totalsize, 32K resident, 16K shared

译者注:diet是原作者写的一个mini的libc实现

 

The one-process-per-connection model

这种模式工作还算有效,甚至有许多标准工具来专门提供此类服务

Unix系统都包括一个叫inetd的服务程序,它不仅仅是对每个连接fork新的进程,同时

还去执行一个指定程序来提供服务

很不幸,inetd有一些缺陷给它(以及这种模式)带来了恶名,一些人开始编写他们自

己的替代程序,包括tcpserver,xinet,ipsvd

这些替代品变化并不大,但一个显著的特征是包括了IP访问控制的机制

我写的第一个web server——fnord,就是使用这种模式,现在www.fefe.de仍然在使

用它

 

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页,基于Linux2.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基准测试

Thebottom line is that bloat benefits all of us. We just need to make sure

thatthere 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_setrfd;

structtimeval 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处理这个问题的方法是manuallykeeping the descriptors

below 15free(with dup2).(译者注:不大理解这段话,是指apache一启动就通过

调用12次dup2来打开文件3-14,然后需要调用DNS的时候就临时close一个,然

后调用库,返回后再继续dup2占个位置否则什么叫"通过dup2手动保证低于

15的文件描述符都是空闲的")

 

Timeout handling with poll()

poll是select的一个变种(poll自1986年在SystemV R3上出现)

 

structpollfd 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:MacOSX

 

Disadvantages of poll()

The wholearray is unnecessarily copied around between user and kernel

space.The kernel then finds out about the events and sets the correspond-

ingrevents.(译者注:不大明白这段的意思,看起来似乎和下一段话有关系)

现代CPU在内存等待上花费了太多时间.而poll会随着管理的文件描述符的增长导致内

存复制的线性增长(译者注:应该就是上一段的意思,但内存操作真那么慢么)

实际情况并不那么糟糕,如果poll运行的时间比较长,事件也不会丢失,内核会保持该

事件队列,直到下一次poll调用再来通知.

另一方面,在Linux和FreeBSD上的实际例子表明,基于fork的webserver比基于

poll的更好(译者注:所谓基于poll的应该是单进程的服务模型,不知道该模型是否使

用了线程,需要我们自己实际测试了)

 

Linux 2.4: SIGIO

Linux2.4能像信号处理那样通知一个poll事件

 

intsigio_add_fd(int fd)

{

staticconst int signum=SIGRTMIN+1;

staticpid_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);

}

 

intsigio_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不是这样.Thepoll way is called level triggered, the

SIGIO wayis 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");

 

caseSIGIO:

puts("SIGIOqueue 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 isperfectly superfluous.(译者注:实在不明白,难道是反话)

 

/dev/poll

几年前Sun在solaris里面增加了一种类似poll的新API.打开/dev/poll,向该设备

写希望获得事件的描述符们,然后通过ioctl获得事件.

ioctl返回有多少个可返回的事件,然后这些事件可以通过/dev/poll设备读取出来,设

备将只返回符合期望的描述符而不是所有的数组.

这意味着无需再扫描整个数组得到所需要的事件了

有几个补丁在Linux内核中引入了类似的设备,但都没能被进入正式的内核.

没错,这些补丁在高负载下是不稳定的.

 

/dev/epoll

有一个patch是在2.4内核中增加了一个/dev/epoll的设备

 

intepollfd=open("/dev/misc/eventpoll",O_RDWR);

ioctl(epollfd,EP_ALLOC,maxfds);/*hint: number of descriptors */

 

char*map;

map=mmap(0,EP_MAP_SIZE(maxfds),PROT_READ,MAP_PRIVATE,epollfd,0);

 

如果希望捕获事件就向该设备写pollfd(结构体),如果希望撤销对一个事件的捕

获仍然写pollfd,只不过events的为0

 

/dev/epoll

事件通过ioctl捕获到

 

structevpoll evp;

for (;;)

{

int n;

evp.ep_timeout=1000;

evp.ep_resoff=0;

n=ioctl(e.fd,EP_POLL,&evp);

pfds=(structpollfd*)(e.map+evp.ep_resoff);

/* nowyou have n pollfds with events in pfds */

}

 

由于使用了mmap,在内核空间和用户空间之间没有做任何内存复制

 

/dev/epoll

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

他说既然我们已经可以在内核中分配系统调用,如果想加什么新应用,那就增加一个新

系统调用,而不是通过新的设备和ioctl

于是/dev/epoll的作者通过系统调用重新实现了一遍,该API最终进入了Linux2.5

(从2.5.51开始就有文档描述了)

在linux2.6中,推荐使用它完成事件通知功能

 

epoll

intepollfd=epoll_create(maxfds);

 

structepoll_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...等常量的值是一样的,但作者希望keepall

optionopen.epoll开始缺省是edge triggered但现在是level triggered的(译

者注:回忆一下SIGIO),通过|EPOLLET还能切换到edgetriggered模式

 

epoll

下面是取得事件的代码:

for (;;)

{

structepoll_event x[100];

intn=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的早期实现就根本没有考虑leveltriggering

epoll和kqueue的性能基本类似

kqueue也已经在OpenBSD上实现,但NetBSD还没有(译者注:原文档很老了,不

晓得现在是否还是这样)

 

FreeBSD: kqueue

怎样来请求一个事件通知或取消它:

#include <sys/type.h>

#include <sys/event.h>

#include <sys/time.h>

 

intkq=kqueue();

 

structtimespec 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

下面是取得事件的代码:

structkevent ev[100];

structtimespec 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 < n; ++i)

{

if(ev.filter=EVFILT_READ)

can_read(ev.ident);

 

if(ev.filter=EVFILT_WRITE)

can_write(ev.ident);

}

 

Windoze: Completion Ports

既便是微软也得在这方面做一些东西

它的方案就是线程池,每个线程通过一个类似SIGIO的机制活动

该方案也同时组合了线程的不足和SIGIO的不足.让人惊讶的是微软的市场部门把这个

当作一个史诗般的发明来歌颂.(译者注:本人没有任何发言权,但CSDN上的确有很

多人相当推崇"完成端口"的机制的,不晓得他们怎么看待原作者的这条评论)

I foundthis quote in the documentation: "First of all, threads are system re-

sourcesthat are neither unlimited nor cheap."

Huh NotCheap I thought that was the reason for their existance!

(译者注:原作者对微软应该是极度鄙视的,实在不知道怎么翻译这段内容)

 

Other reasons for high latency

POSIXAPI规定,每当新打开一个文件,内核必须使用最小的,未被使用的描述符.

(比如接受一个网络连接,创建一个socket都是这样)

很明显该算法不可能是O(1)的,Linux使用的是位向量.

Linux内核邮件列表上也曾有相关的热烈讨论——给open系统调用增加新的标志位,

可以让行为不遵守POSIX标准,返回一个可用的描述符就可以了.但从来没有出现这样

的功能.而且即使open有了这样的功能,socket调用也无法从中获益.

在Linux和所有的BSD上,socket系统调用倒还能算是scaleswell的(译者注:请

参考原pdf第55页)

 

Other reasons for high latency

对客户端应用或者proxy服务器也有同样的问题

如果你打开一个socket去连接服务器,并且没有明确指定端口号的话,内核将选择一个

可用的端口来使用

并没有标准要求内核必须返回一个最小的可用port,但通常都是这么实现的

测试表明不论是IPv4还是IPv6性能曲线都差不多,因此只给出IPv4的图表(译者

注:参考原pdf第57页,看起来FreeBSD5.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-gatherI/O.即从kernel buffer取

得包头,而包内容来自buffercache(note:csum_partial_copy_from_user)

该技术就是所谓的ZeroCopy TCP

Linux和FreeBSD都有sendfile系统调用(但有些不同)

NetBSD和OpenBSD没有sendfile调用

 

Memory Mapped I/O

如果文件只读,仅仅执行把文件内容读入到内存中的动作,那么一个替代方案是把文件

映射到用户空间中

该系统调用称之为mmap,对于设计可伸缩性网络I/O而言它非常重要,因为它避免了

buffer(译者注:类似上述的sendfile,它也避免了一次内存复制)

操作系统可以把节省的内存用于buffercache

然而,维护页表的工作却不能scaleproperly.对于64位系统尤其如此

译者注:参考原pdf第62页,mmap系统调用性能最好是Linux2.6,其次是

OpenBSD3.4,再次是Linux 2.4,随后是NetBSD 1.6.1,FreeBSD 5.1最糟糕.

参考原pdf第63,64,65的图表,随着mmap页面数目的增多,从mmap的新页

面中读取性能最好的是FreeBSD5.1,其次是Linux 2.6/2.4,NetBSD 1.6.1差劲的

不成比例,OpenBSD3.4简直表现得一塌糊涂

 

File system  latency

如今文件系统延时应该已经不成问题,这里提一个罕见的例子

我曾经维护局域网上的一台FTP服务器,它允许incoming

有一个人决定把他所有的色情图片全部放上来

当他上载了50000个文件后我发现了这个用户并立刻踢他下线,因为此时服务器已经缓

慢异常了

操作系统几乎100%的CPU都在内核态,以反复调整这个目录下的文件在内存中的列

表.

今天我们处理同一目录下大量文件已经有了类似XFS,reiserfs这样的文件系统,而

ext3或FreeBSDUFS也能支持目录哈希

 

Asynchronous I/O

POSIX提供了异步I/O的规范,不幸的是几乎任何人都没有实现它,Linux也不例外

(译者注:这里有些迷惑,2.6明明有AIO的,不过好像只支持磁盘IO的说)

libc hasa emulation that creates one thread for each request. This is much

worsethan not having the API in the first place, so nobody uses the API.(译

者注:这里说的是不是就是2.6AIO )

异步IO的思路就是给读请求排队,可以询问操作系统该请求是否已经完成,或者当请求

完成的时候得到一个信号

问题在于:当你收到一个信号的时候,你无法知道是哪个请求完成了

 

Asynchronous I/O

AsyncI/O仅对文件有意义,而不是socket.如果希望从一个巨大的数据库里面读取

1000个块,而使用了lseek和read来完成,那就意味着让操作系统通过指定的顺序读

取文件,而不论这些块是在磁盘上如何排列的.

通过asyncI/O,操作系统可以基于这些块在磁盘上的排列重新调整顺序,然后磁头一

次性读取这些数目而无需来回移动寻道.理论上,非常美好;实际应用表明作用不大

(译者注:记得Ora的某个版本AIO支持有问题,导致启用AIO后性能急剧下降)

Solaris有它自己的专有asyncI/O API实现,可同样没有提供POSIX规范API,调用

它们会返回ENOSYS

提供asyncI/O的最重要的操作系统就是FreeBSD了

 

writev, TCP_CORK and TCP_NOPUSH

HTTP服务器写回客户端一个HTTP头标,然后是文件的内容.调用write(或者

sendfile),内核首先发送一个包含头标的TCP包,然后创建后继的TCP包,传送文

件内容

如果文件内容很小,比如只有100字节,那么一个TCP包就足以同时容纳头标和内容了

一个很明显的解决方案是准备一个缓冲,把头标和内容都复制到缓冲内,然后只执行一

次write...且慢!我们还需要使用zero-copyTCP的,还记得吗

这里有4个解决方案:writev,TCP_CORK(Linux),TCP_NOPUSH(BSD),以及

FreeBSD的sendfile

 

writev

writev相当于批处理写,给定一组指针和长度的集合,然后它一次性把它们写出去

Thedifference is too small to notice normally, except for TCP connections.

 

structiovec 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

 

intnull=0, eins=1;

 

/* putcork in bottle */

setsockopt(sock,IPPROTO_TCP,TCP_CORK,&eins,sizeof(eins));

 

write(sock,header,strlen(header));

write(sock,mapped_file,filesize);

 

/* pullcork 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并不总是那么快!在Linux2.6上,测试程序静态链接了自己编写的dietlibc

库,vfork就比fork慢——250msvs. 180ms,如果是链接glibc的话则是250ms

vs. 320ms

 

So which OS is the best one

我的推荐是Linux2.6,在所有的基准测试中都表现了O(1)的水准

FreeBSD5.1其次,除了mmap外,都是O(1)

在多进程和mmap测试中Linux2.4表现不好,换成2.6吧

NetBSD没有kqueue和sendfile,仅仅只有poll.但还能算得上高性能操作系统

OpenBSD让人大跌眼镜,磁盘性能烂透了......(文字的东西就不再翻译了)

原pdf第75页:从Linux2.6执行connect测试服务器的连接延时情况,NetBSD

poll的性能比OpenBSD的kqueue要强

第76/77页:测试服务器处理HTTP请求的能力,NetBSDpoll的性能就比OpenBSD

的kqueue差了,Linux2.6 epoll的表现本来比FreeBSD kqueue要强,但当连接数

超过4000后,FreeBSDkqueue的性能猛然有一个跃升,竟然超过了自己在低连接数

下的性能表现,当然同时也超过了Linux2.6,非常可疑,像是在作弊一样,呵呵

 

Questions

Thanksfor sitting through this with me in this ungodly hour!

You canget the source code for all benchmarks via anonymous cvs:

% cvs-d:pserver:cvs@cvs.fefe.de:/cvs -z9 co libowfat

% cvs-d:pserver:cvs@cvs.fefe.de:/cvs -z9 co gatling

My web page is at http://www.fefe.de/

You can email me at felix-linuxkongress@fefe.de

 

以上内容就无需翻译了,感谢Felix的精彩讲解,某种程度上可以和R.Stevens的《网

络编程》媲美了

 

原文在:http://bulk.fefe.de/scalable-networking.pdf

图表说明:http://bulk.fefe.de/scalability/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,在Unity中使用高性能的Socket代码,你可以使用Unity的System.Net.Sockets命名空间。下面是一个简单的例子,展示了如何使用Socket连接到远程服务器并发送和接收数据: ``` using UnityEngine; using System.Net.Sockets; using System.Text; public class SocketExample : MonoBehaviour { private Socket _socket; private byte[] _buffer = new byte[1024]; void Start() { // 创建Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 连接到远程服务器 _socket.Connect("www.example.com", 80); // 发送数据 string data = "Hello World!"; byte[] buffer = Encoding.UTF8.GetBytes(data); _socket.Send(buffer); // 接收数据 int recv = _socket.Receive(_buffer); string recvData = Encoding.UTF8.GetString(_buffer, 0, recv); Debug.Log("Received: " + recvData); } void OnApplicationQuit() { // 关闭Socket _socket.Close(); } } ``` 希望这个例子能帮到你! ### 回答2: 使用Unity编写高性能的socket代码需要注意以下几点: 1. 选择合适的库:Unity自带支持socket通信的API,但是性能不高。可以选择使用第三方库如DotNetty,它是C#的高性能异步网络编程框架,在Unity中也可以使用。 2. 使用异步编程模型:使用async/await关键字可以让代码以异步的方式执行,充分利用IO等待时间。在处理网络数据时,可以使用这种模型来提高性能。 3. 采用事件驱动的方式:使用事件驱动模型可以减少线程等待时间,提高并发性能。可以使用事件回调的方式处理网络数据的接收和发送。 4. 使用TCP连接池:为了减少频繁创建和销毁的开销,可以使用TCP连接池来管理连接。连接池可以复用已经建立的连接,减少建立连接的时间和资源消耗。 5. 使用数据缓冲区:在处理网络数据时,可以使用缓冲区来存储接收和发送的数据,避免频繁的内存分配和复制操作。 6. 设置合理的超时时间:在建立连接、接收和发送数据时,可以设置合理的超时时间,避免因为网络不稳定或者异常情况导致程序阻塞。 7. 合理处理异常:在网络通信中,可能会遇到各种异常情况,如连接断开、超时等。合理处理这些异常可以保证程序的稳定性和可靠性。 使用这些技巧,可以编写高性能的socket代码。但是需要根据具体的业务需求和环境来进行优化和调整,以获得最佳的性能表现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值