阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

为什么Nginx的性能要比Apache高很多?

180次阅读
没有评论

共计 4089 个字符,预计需要花费 11 分钟才能阅读完成。

为什么 Nginx 的性能要比 Apache 高很多?这得益于 Nginx 使用了最新的 epoll(Linux 2.6 内核)和 kqueue(freebsd)网络 I / O 模型,而 Apache 则使用的是传统的 select 模型。

目前 Linux 下能够承受高并发访问的 Squid、Memcached 都采用的是 epoll 网络 I / O 模型。

处理大量的连接的读写,Apache 所采用的 select 网络 I / O 模型非常低效。

下面用一个比喻来解析 Apache 采用的 select 模型和 Nginx 采用的 epoll 模型进行之间的区别:

假设你在大学读书,住的宿舍楼有很多间房间,你的朋友要来找你。

select 版宿管大妈就会带着你的朋友挨个房间去找,直到找到你为止。

而 epoll 版宿管大妈会先记下每位同学的房间号,

你的朋友来时,只需告诉你的朋友你住在哪个房间即可,不用亲自带着你的朋友满大楼找人。

如果来了 10000 个人,都要找自己住这栋楼的同学时,select 版和 epoll 版宿管大妈,谁的效率更高,不言自明。

同理,在高并发服务器中,轮询 I / O 是最耗时间的操作之一,select 和 epoll 的性能谁的性能更高,同样十分明了。

?
1
epoll - I/O event notification facility

在 linux 的网络编程中,很长的时间都在使用 select 来做事件触发。

在 linux 新的内核中,有了一种替换它的机制,就是 epoll。

相比于 select,epoll 最大的好处在于它不会随着监听 fd 数目的增长而降低效率。

因为在内核中的 select 实现中,它是采用轮询来处理的,轮询的 fd 数目越多,自然耗时越多。

并且,在 linux/posix_types.h 头文件有这样的声明:

1
#define __FD_SETSIZE    1024

表示 select 最多同时监听 1024 个 fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。

epoll 的接口非常简单,一共就三个函数:

1. int epoll_create(int size);

创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共有多大。

这个参数不同于 select()中的第一个参数,给出最大监听的 fd+ 1 的值。

需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/ 进程 id/fd/,

是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close()关闭,否则可能导致 fd 被耗尽。

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll 的事件注册函数,它不同与 select()是在监听事件时告诉内核要监听什么类型的事件,

而是在这里先注册要监听的事件类型。第一个参数是 epoll_create()的返回值,

第二个参数表示动作,用三个宏来表示:

EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;

EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;

EPOLL_CTL_DEL:从 epfd 中删除一个 fd;

第三个参数是需要监听的 fd,第四个参数是告诉内核需要监听什么事,struct epoll_event 结构如下:

1
2
3
4
5
6
7
8
9
10
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

events 可以是以下几个宏的集合:

EPOLLIN:表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET:将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是相对于水平触发 (Level Triggered) 来说的。

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,

如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

等待事件的产生,类似于 select()调用。

参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创建 epoll_create()时的 size,参数 timeout 是超时时间(毫秒,0 会立即返回,- 1 将不确定,也有说法说是永久阻塞)。

该函数返回需要处理的事件数目,如返回 0 表示已超时。

4、关于 ET、LT 两种工作模式:

可以得出这样的结论:

ET 模式仅当状态发生变化的时候才获得通知, 这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说, 如果要采用 ET 模式, 需要一直 read/write 直到出错为止,很多人反映为什么采用 ET 模式只接收了一部分数据就再也得不到通知了,大多因为这样;而 LT 模式是只要有数据没有处理就会一直通知下去的。

那么究竟如何来使用 epoll 呢?其实非常简单。

通过在包含一个头文件 #include <sys/epoll.h> 以及几个简单的 API 将可以大大的提高你的网络服务器的支持人数。

首先通过 create_epoll(int maxfds)来创建一个 epoll 的句柄,其中 maxfds 为你 epoll 所支持的最大句柄数。

这个函数会返回一个新的 epoll 句柄,之后的所有操作将通过这个句柄来进行操作。

在用完之后,记得用 close()来关闭这个创建出来的 epoll 句柄。

之后在你的网络主循环里面,每一帧的调用 epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为:

nfds = epoll_wait(kdpfd, events, maxevents, -1);

其中 kdpfd 为用 epoll_create 创建之后的句柄,events 是一个 epoll_event* 的指针,当 epoll_wait 这个函数操作成功之后,epoll_events 里面将储存所有的读写事件。

max_events 是当前需要监听的所有 socket 句柄数。最后一个 timeout 是 epoll_wait 的超时,为 0 的时候表示马上返回,为 - 1 的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则范围。一般如果网络主循环是单独的线程的话,可以用 - 1 来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用 0 来保证主循环的效率。

epoll_wait 范围之后应该是一个循环,遍利所有的事件。

几乎所有的 epoll 程序都使用下面的框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
for(; ;)
    {
        nfds = epoll_wait(epfd,events,20,500);
        for(i=0;i<nfds;++i)
        {
            if(events[i].data.fd==listenfd) //有新的连接
            {
                connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
                ev.data.fd=connfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的 fd 添加到 epoll 的监听队列中
            }
            else if(events[i].events&EPOLLIN ) //接收到数据,读 socket
            {
                n = read(sockfd, line, MAXLINE)) < 0    //
                ev.data.ptr = md;    //md为自定义类型,添加数据
                ev.events=EPOLLOUT|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
            }
            else if(events[i].events&EPOLLOUT) //有数据待发送,写 socket
            {
                struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取数据
                sockfd = md->fd;
                send(sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //发送数据
                ev.data.fd=sockfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
            }
            else
            {
                //其他的处理
            }
        }
    }

本文永久更新链接地址:http://www.linuxidc.com/Linux/2015-05/117797.htm

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-20发表,共计4089字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中