From: http://blog.sina.com.cn/iyangjian
一,如何节约CPU
二,怎样使用内存
三,减少磁盘I/O
四,优化你的网卡
五,调整内核参数
六,衡量Web Server的性能指标
七,NBA js直播的发展历程
八,新浪财经实时行情系统的历史遗留问题 (7 byte = 10.68w RMB/year)
一,如何节约CPU
1,选择一个好的I/O模型(epoll, kqueue)
3年前,我们还关心c10k问题,随着硬件性能的提升,那已经不成问题,但如果想让PIII 900服务器支撑5w+ connections,还是需要些能耐的。
epoll最擅长的事情是监视大量闲散连接,批量返回可用描述符,这让单机支撑百万connections成为可能。linux 2.6以上开始支持epoll,freebsd上相应的有kqueue,不过我个人偏爱linux,不太关心kqueue。
边缘触发ET 和 水平触发LT 的选择:
早期的文档说ET很高效,但是有些冒进。但事实上LT使用过程中,我苦恼了将近一个月有余,一不留神CPU 利用率99%了,可能是我没处理好。后来zhongying同学帮忙把驱动模式改成了ET模式,ET既高效又稳定。
简单地说,如果你有数据过来了,不去取LT会一直骚扰你,提醒你去取,而ET就告诉你一次,爱取不取,除非有新数据到来,否则不再提醒。
重点说下ET,非阻塞模式,
man手册说,如果ET提示你有数据可读的时候,你应该连续的读一直读到返回 EAGAIN or EWOULDBLOCK 为止,但是在具体实现中,我并没有这样做,而是根据我的应用做了优化。因为现在操作系统绝大多数实现都是最大传输单元值为1500。
HTTP header,不带cookie的话一般只有500+ byte。留512给uri,也基本够用,还有节余。
如果请求的header恰巧比这大是2050字节呢?
会有两种情况发生:1,数据紧挨着同时到达,一次read就搞定。 2,分两个ethernet frame先后到达有一定时间间隔。
我的方法是,用一个比较大的buffer比如1M去读header,如果你很确信你的服务对象请求比1460小,读一次就行。如果请求会很大分几个ethernet frame先后到达,也就是恰巧你刚刚read过,它又来一个新数据包,ET会再次返回,再处理下就是了。
顺便再说下写数据,一般一次可以write十几K数据到内核缓冲区。
所以对于很多小的数据文件服务来说,是没有必要另外为每个connections分配发送缓冲区。
只有当一次发送不完时候才分配一块内存,将数据暂存,待下次返回可写时发送。
这样避免了一次内存copy,而且节约了内存。
选择了epoll并不代表就就拥有了一个好的 I/O模型,用的不好,你还赶不上select,这是实话。
epoll的问题我就说这么多,关于描述符管理方面的细节请参见我早期的一个帖子,epoll模型的使用及其描述符耗尽问题的探讨
/*-------------------------------------------------------------------------------------------------
gcc -o httpd httpd.c -lpthread
author: wyezl
2006.4.28
---------------------------------------------------------------------------------------------------*/
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#define PORT 8888
#define MAXFDS 5000
#define EVENTSIZE 100
#define BUFFER "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\nContent-Type: text/html\r\n\r\nHello"
int epfd;
void *serv_epoll(void *p);
void setnonblocking(int fd)
{
int opts;
opts=fcntl(fd, F_GETFL);
if (opts < 0)
{
fprintf(stderr, "fcntl failed\n");
return;
}
opts = opts | O_NONBLOCK;
if(fcntl(fd, F_SETFL, opts) < 0)
{
fprintf(stderr, "fcntl failed\n");
return;
}
return;
}
int main(int argc, char *argv[])
{
int fd, cfd,opt=1;
struct epoll_event ev;
struct sockaddr_in sin, cin;
socklen_t sin_len = sizeof(struct sockaddr_in);
pthread_t tid;
pthread_attr_t attr;
epfd = epoll_create(MAXFDS);
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) <= 0)
{
fprintf(stderr, "socket failed\n");
return -1;
}
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt));
memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_port = htons((short)(PORT));
sin.sin_addr.s_addr = INADDR_ANY;
if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) != 0)
{
fprintf(stderr, "bind failed\n");
return -1;
}
if (listen(fd, 32) != 0)
{
fprintf(stderr, "listen failed\n");
return -1;
}
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, serv_epoll, NULL) != 0)
{
fprintf(stderr, "pthread_create failed\n");
return -1;
}
while ((cfd = accept(fd, (struct sockaddr *)&cin, &sin_len)) > 0)
{
setnonblocking(cfd);
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
//printf("connect from %s\n",inet_ntoa(cin.sin_addr));
//printf("cfd=%d\n",cfd);
}
if (fd > 0)
close(fd);
return 0;
}
void *serv_epoll(void *p)
{
int i, ret, cfd, nfds;;
struct epoll_event ev,events[EVENTSIZE];
char buffer[512];
while (1)
{
nfds = epoll_wait(epfd, events, EVENTSIZE , -1);
//printf("nfds ........... %d\n",nfds);
for (i=0; i<nfds; i++)
{
if(events[i].events & EPOLLIN)
{
cfd = events[i].data.fd;
ret = recv(cfd, buffer, sizeof(buffer),0);
//printf("read ret..........= %d\n",ret);
ev.data.fd = cfd;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, cfd, &ev);
}
else if(events[i].events & EPOLLOUT)
{
cfd = events[i].data.fd;
ret = send(cfd, BUFFER, strlen(BUFFER), 0);
//printf("send ret...........= %d\n", ret);
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, &ev);
//shutdown(cfd, 1);
close(cfd);
}
}
}
return NULL;
}
另外你要使用多线程,还是多进程,这要看你更熟悉哪个,各有好处。
多进程模式,单个进程crash了,不影响其他进程,而且可以为每个worker分别帮定不同的cpu,让某些cpu单独空出来处理中断和系统事物。多线程,共享数据方便,占用资源更少。进程或线程的个数,应该固定在 (cpu核数-1) ~ 2倍cpu核数间为宜,太多了时间片轮转时会频繁切换,少了,达不到多核并发处理的效果。
还有如何accept也是一门学问,没有最好,只有更适用,你需要做很多实验,确定对自己最高效的方式。有了一个好的I/O框架,你的效率想低也不容易,这是程序实现的大局。
关于更多网络I/O模型的讨论请见
另外,必须强调的是,代码和结构应该简洁高效,一定要具体问题具体分析,没什么法则是万能的,要根据你的服务量身定做。
2,关闭不必要的标准输入和标准输出
close(0);
close(1);
如果你不小心,有了printf输出调试信息,这绝对是一个性能杀手。
一个高性能的服务器不出错是不应该有任何输出的,免得耽误干活。
这样做,至少能为你节约两个描述符资源。
3,避免用锁 (i++ or ++i )
多线程编程用锁是普遍现象,貌似已经成为习惯。
但各线程最好是独立的,不需要同步机制的。
锁会消耗资源,而且造成排队,甚至死锁,尽量想办法避免。
非用不可时候,比如,实时统计各线程的负载情况,多个线程要对全局变量进行写操作。
请用 ++i ,因为它是一个原子操作。
4,减少系统调用
系统调用是很耗的,因为它通常需要钻进内核再钻出来。
我们应该避免用户空间和内核空间的切换。
比如我要为每个请求打个时间戳,以计算超时,我完全可以在返回一批可用描述符前只调用一次time(),而不用每个请求都调用一次。 time()只精确到秒,一批请求处理都是毫秒级,所以也没必要那么做,再说了,计算超时误差那么一秒有什么影响吗?
5, Connection: close vs
谈httpd实现,就不能不提长连接Keep-Alive 。
Keep-Alive是http 1.1中加入的,现在的浏览器99。99%应该都是支持Keep-Alive的。
先说下什么是Keep-Alive:
这是基于tcp的connections说的,也就是一个描述符(fd),它并不代表独立占用一个进程或线程。一个线程用非阻塞模式可以保持成千上万个长连接。
先说一个完整的HTTP 1.0的请求和响应:
建立tcp连接 (syn; ack, syn2; ack2; 三个分组握手完成)
请求
响应
关闭连接 (fin; ack; fin2; ack2
再说HTTP 1.1的请求和响应:
建立tcp连接 (syn; ack, syn2; ack2; 三个分组握手完成)
请求
响应
...
...
请求
响应
关闭连接 (fin; ack; fin2; ack2
如果请求和响应都只有一个分组,那么HTTP 1.0至少要传输11个分组(补充:请求和响应数据还各需要一个ack确认),才拿到一个分组的数据。而长连接可以更充分的利用这个已经建立的连接,避免的频繁的建立和关闭连接,减少网络拥塞。
我做过一个测试,在2cpu*4core服务器上,不停的accept,然后不做处理,直接close掉。一秒最多可以accept
目前唯一的也是最好的选择,就是保持长连接。
比如我们NBA JS直播页面,刚打开就会向我的js服务器发出6个http请求,而且随后平均每10秒会产生两个请求。再比如,我们很多页面都会嵌几个静态池的图片,如果每个请求都是独立的(建立连接然后关闭),那对资源绝对是个浪费。
长连接是个好东西,但是选择 Keep-Alive必须根据你的应用决定。比如NBA JS直播,我肯定10秒内会产生一个请求,所以超时设置为15秒,15秒还没活动,估计是去打酱油了,资源就得被我回收。超时设置过长,光连接都能把你的服务器堆死。
为什么有些apache服务器,负载很高,把Keep-Alive关掉负载就减轻了呢?
apache 有两种工作模式,prefork和worker。apache 1.x只有,prefork。
prefork比较典型,就是个进程池,每次创建一批进程,还有apache是基于select实现的。在用户不是太多的时候,长连接还是很有用的,可以节约分组,提升响应速度,但是一旦超出某个平衡点,由于为了保持很多长连接,创建了太多的进程,导致系统不堪重负,内存不够了,开始换入换出,cpu也被很多进程吃光了,load上去了。这种情况下,对apache来说,每次请求重新建立连接要比保持这么多长连接和进程更划算。
6,预处理 (预压缩,预取lastmodify,mimetype)
预处理,原则就是,能预先知道的结果,我们绝不计算第二次。
预压缩:我们在两三年前就开始使用预压缩技术,以节约CPU,伟大的微软公司在现在的IIS 7中也开始使用了。所谓的预压缩就是,从数据源头提供的就是预先压缩好的数据,IDC同步传输中是压缩状态,直到最后web server输出都是压缩状态,最终被用户浏览器端自动解压。
预取lastmodify:
预取mimetype: mimetype,如果你的文件类型不超过256种,一个字节就可以标识它,然后用数组下标直接输出,而且不是看到一个js文件,然后strcmp()了近百种后缀名后,才知道应该输出Content-Type: application/x-javascript,而且这种方法会随文件类型增加而耗费更多cpu资源。当然也可以写个hash函数来做这事,那也至少需要一次函数调用,做些求值运算,和分配比实际数据大几倍的hash表。
如何更好的使用cpu一级缓存
数据分解
CPU硬亲和力的设置
待补充。。。。
二,怎样使用内存
1,避免内存copy (strcpy,memcpy)
虽然内存速度很快,但是执行频率比较高的核心部分能避免copy的就尽量别使用。如果必须要copy,尽量使用memcpy替代sprintf,strcpy,因为它不关心你是否遇到'\0'; 内存拷贝和http响应又涉及到字符串长度计算。如果能预先知道这个长度最好用中间变量保留,增加多少直接加上去,不要用strlen()去计算,因为它会数数直到遇见'\0'。能用sizeof()的地方就不要用strlen,因为它是个运算符,在预编的时被替换为具体数字,而非执行时计算。
2,避免内核空间和用户进程空间内存copy (sendfile, splice and tee)
sendfile: 它的威力在于,它为大家提供了一种访问当前不断膨胀的Linux网络堆栈的机制。这种机制叫做“零拷贝(zero-copy)”,这种机制可以把“传输控制协议(TCP)”框架直接的从主机存储器中传送到网卡的缓存块(network card buffers)中去,避免了两次上下文切换。详细参见 <使用sendfile()让数据传输得到最优化> 。据同事测试说固态硬盘SSD对于小文件的随机读效率很高,对于更新不是很频繁的图片服务,读却很多,每个文件都不是很大的话,sendfile+SSD应该是绝配。
splice and tee: splice背后的真正概念是暴露给用户空间的“随机内核缓冲区”的概念。“也就是说,splice和tee运行在用户控制的内核缓冲区上,在这个缓冲区中,splice将来自任意文件描述符的数据传送到缓冲区中(或从缓冲区传送到文件描述符),而tee将一个缓冲区中的数据复制到另一个缓冲区中。因此,从一个很真实(而抽象)的意义上讲,splice相当于内核缓冲区的read/write,而tee相当于从内核缓冲区到另一个内核缓冲区的memcpy。”。本人觉得这个技术用来做代理,很合适。因为数据可以直接从一个soket到另一个soket,不需要经用户和内核空间的切换。这是sendfile不支持的。详细参见
3,如何清空一块内存(memset ?)
比如有一个buffer[1024*1024],我们需要把它清空然后strcat(很多情况下可以通过记录写的起始位置+memcpy来代替)追加填充字符串。
其实我们没有必要用memset(buffer,0x00,sizeof(buffer))来清空整个buffer, memset(buffer,0x00,1)就能达到目的。 我平时更喜欢用buffer[0]='\0'; 来替代,省了一次函数调用的开销。
4,内存复用
对于NBA JS服务来说,我们返回的都是压缩数据,99%都不超过15k,基本一次write就全部出去了,是没有必要为每个响应分配内存的,公用一个buffer就够了。如果真的遇到大数据,我先write一次,剩下的再暂存在内存里,等待下次发送。
5,避免频繁动态申请/释放内存(malloc)
这个似乎不用多说,要想一个Server启动后成年累月的跑,就不应该频繁地去动态申请和释放内存。原因很简单一,避免内存泄露。二,避免碎片过多。三,影响效率。一般来说,都是一次申请一大块内存,然后自己写内存分配算法。为http用户分配的缓冲区生命期的特点是,可以随着fd的关闭,而回收,避免漏网。还有Server的编写者应该对自己设计的程序达到最高支撑量的时候所消耗的内存心中有数。
6,字节对齐
先看下面的两个结构体有什么不同:
struct A {
} a ;
struct B {
} b ;
仅仅是一个顺序的变化,结构体B顺序是合理的:
在32bit linux系统上,是按照32/8bit=4byte来对齐的, sizeof(a)=12 ,sizeof(b)=12 。
在64bit linux系统上,是按照64/8bit=8byte来对齐的, sizeof(a)=24 ,sizeof(b)=16 。
32bit机上看到的A和B结果大小是一样的,但是如果把int改成short效果就不一样了。
如果我想强制以2byte对齐,可以这样:
#pragma pack(2)
struct A {
} a ;
#pragma pack()
注意pack()里的参数,只能指定比本机支持的字节对齐标准小,而不能更大。
7,内存安全问题
先举个好玩的例子,不使用a,而给a赋上值:
int main()
{
}
程序输出
这就是典型的溢出,如果是空闲的内存,用点也就罢了,可是把别人地盘上的数据覆盖了,就不好了。
接收的用户数据一定要严格判断,确定不会越界,不是每个人都按规矩办事的,搞不好就挂了。
8,云风的内存管理理论 (sd2c大会所获 blog & ppt)
没有永远不变的原则
大原则变化的慢
没有一劳永逸的解决方案
内存访问很廉价但有代价
减少内存访问的次数是很有意义的
随机访问内存慢于顺序访问内存
请让数据物理上连续
集中内存访问优于分散访问
尽可能的将数据紧密的存放在一起
无关性内存访问优于相关性内存访问
请考虑并行的可能性、即使你的程序本身没有使用并行机制
控制周期性密集访问的数据大小
必要时采用时间换空间的方法
读内存快于写内存
代码也会占用内存,所以、保持代码的简洁
物理法则
晶体管的排列
批量回收内存
不释放内存,留给系统去做
list map
长用字符串做成hash,使用指针访问
直接内存页处理控制
三,减少磁盘I/O
这个其实就是通过尽可能的使用内存达到性能提高和i/o减少。从系统的读写buffer到用户空间自己的cache,都是可以有效减少磁盘i/o的方法。用户可以把数据暂存在自己的缓冲区里,批量读写大块数据。cache的使用是很必要的,可以自己用共享内存的方法实现,也可以用现成的BDB来实现。欢迎访问我的公益站点 berkeleydb.net ,不过我不太欢迎那种问了问题就跑的人。BDB默认的cache只有256K,可以调大这个数字,也可以纯粹使用Mem Only方法。对于预先知道的结果,争取不从磁盘取第二次,这样磁盘基本就被解放出来了。BDB取数据的速度每秒大概是100w条(2CPU*2Core Xeon(R) E5410 @ 2.33GHz环境测试,单条数据几十字节),如果你想取得更高的性能建议自己写。
四,优化你的网卡
首先ethtool ethx 看看你的外网出口是不是Speed: 1000Mb/s 。
对于多核服务器,运行top命令,然后按一下1,就能看到每个核的使用情况。如果发现cpuid=0的那颗使用率明显高于其他核,那就说明id=0的cpu将来也许会成为你的瓶颈。然后可以用mpstat(非默认安装)命令查看系统中断分布,用cat /proc/interrupts 网卡中断分布。
下面这个数据是我们已经做过优化了的服务器中断分布情况:
[yangjian2@D08043466 ~]$ mpstat -P ALL 1
Linux 2.6.18-53.el5PAE (D08043466)
01:51:27 PM
01:51:28 PM
01:51:28 PM
01:51:28 PM
01:51:28 PM