【原创】高性能网络编程技术



高性能网络编程技术

作者:jmz (360电商技术组)


怎样使网络服务器能够处理数以万计的客户端连接。这个问题被称为C10K Problem。在非常多系统中,网络框架的性能直接决定了系统的总体性能。因此研究解决高性能网络编程框架问题具有十分重要的意义。


1. 网络编程模型

C10K Problem中,给出了一些常见的解决大量并发连接的方案和模型,在此依据自己理解去除了一些不实际的方案,并做了一些整理。


1.1、PPC/TPC模型

典型的Apache模型(Process Per Connection,简称PPC)。TPCThread Per Connection)模型,这两种模型思想相似,就是让每一个到来的连接都一边自己做事直到完毕。仅仅是PPC是为每一个连接开了一个进程,而TPC开了一个线程。

但是当连接多了之后,如此多的进程/线程切换须要大量的开销;这类模型能接受的最大连接数都不会高,一般在几百个左右。


1.2、异步网络编程模型

异步网络编程模型都依赖于I/O多路复用模式

一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来。并分发到相应的read/write事件处理器(Event Handler)。开发者预先注冊须要处理的事件及其事件处理器(或回调函数)。事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式採用同步IO,而Proactor採用异步IO。

在Reactor中,事件分离器负责等待文件描写叙述符或socket为读写操作准备就绪,然后将就绪事件传递给相应的处理器,最后由处理器负责完毕实际的读写工作。

而在Proactor模式中。处理器--或者兼任处理器的事件分离器,仅仅负责发起异步读写操作。IO操作本身由操作系统来完毕

传递给操作系统的參数须要包含用户定义的数据缓冲区地址和数据大小,操作系统才干从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完毕事件。然后将事件传递给相应处理器。


在Reactor中实现读:

注冊读就绪事件和相应的事件处理器

事件分离器等待事件

事件到来,激活分离器。分离器调用事件相应的处理器

事件处理器完毕实际的读操作,处理读到的数据。注冊新事件,然后返还控制权


在Proactor中实现读:

处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这样的情况下,处理器无视IO就绪事件,它关注的是完毕事件。

事件分离器等待操作完毕事件

在分离器等待过程中,操作系统利用并行的内核线程运行实际的读操作,并将结果数据存入用户自己定义缓冲区,最后通知事件分离器读操作完毕。

事件分离器呼唤处理器。

事件处理器处理用户自己定义缓冲区中的数据,然后启动一个新的异步操作。并将控制权返回事件分离器。


能够看出,两个模式的同样点。都是对某个IO事件的事件通知(即告诉某个模块。这个IO操作能够进行或已经完毕)。

在结构上。两者也有同样点:demultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时。就回调handler;不同点在于,异步情况下(Proactor)。当回调handler时,表示IO操作已经完毕;同步情况下(Reactor),回调handler时。表示IO设备能够进行某个操作(can read or can write)。


1.2.1 Reactor模式框架

使用Proactor模式须要操作系统支持异步接口。因此在日常中比較常见的是Reactor模式的系统调用接口。使用Reactor模型,必备的几个组件:事件源、Reactor框架、多路复用机制和事件处理程序,先来看看Reactor模型的总体框架,接下来再对每一个组件做逐一说明。


 


事件源

Linux上是文件描写叙述符,Windows上就是Socket或者Handle了,这里统一称为“句柄集”;程序在指定的句柄上注冊关心的事件,比方I/O事件。


event demultiplexer——事件多路分发机制

Ø 由操作系统提供的I/O多路复用机制。比方select和epoll。

Ø 程序首先将其关心的句柄(事件源)及其事件注冊到event demultiplexer上;

Ø 当有事件到达时。event demultiplexer会发出通知“在已经注冊的句柄集中。一个或多个句柄的事件已经就绪”。

Ø 程序收到通知后。就能够在非堵塞的情况下对事件进行处理了。


Reactor——反应器

Reactor,是事件管理的接口,内部使用event demultiplexer注冊、注销事件;并运行事件循环,当有事件进入“就绪”状态时,调用注冊事件的回调函数处理事件。

一个典型的Reactor声明方式


class Reactor {  

public:  

    int register_handler(Event_Handler *pHandler, int event);  

    int remove_handler(Event_Handler *pHandler, int event);  

    void handle_events(timeval *ptv);  

    // ...  

};  


Event Handler——事件处理程序

事件处理程序提供了一组接口。每一个接口相应了一种类型的事件,供Reactor在相应的事件发生时调用,运行相应的事件处理。

通常它会绑定一个有效的句柄。

以下是两种典型的Event Handler类声明方式。二者互有优缺点。


class Event_Handler {  

public:  

    virtual void handle_read() = 0;  

    virtual void handle_write() = 0;  

    virtual void handle_timeout() = 0;  

    virtual void handle_close() = 0;  

    virtual HANDLE get_handle() = 0;  

    // ...  

};  

class Event_Handler {  

public:  

    // events maybe read/write/timeout/close .etc  

    virtual void handle_events(int events) = 0;  

    virtual HANDLE get_handle() = 0;  

    // ...

};  


1.2.2 Reactor事件处理流程


前面说过Reactor将事件流“逆置”了,使用Reactor模式后,事件控制流能够參见以下的序列图

 


1.3 Select,pollepoll

在Linux环境中。比較常见的I/O多路复用机制就是Select。poll和epoll,以下对这三种机制进行分析和比較,并对epoll的使用进行介绍。


1.3.1 select模型

1. 最大并发数限制。由于一个进程所打开的FD(文件描写叙述符)是有限制的,由FD_SETSIZE设置,默认值是1024/2048,因此Select模型的最大并发数就被相应限制了。

2. 效率问题,select每次调用都会线性扫描全部的FD集合。这样效率就会呈现线性下降,把FD_SETSIZE改大的后果就是全部FD处理都慢慢来

3. 内核/用户空间 内存拷贝问题。怎样让内核把FD消息通知给用户空间呢?在这个问题上select採取了内存拷贝方法。


int res = select(maxfd+1, &readfds, NULL, NULL, 120);

if (res > 0) {

    for (int i = 0; i < MAX_CONNECTION; i++) {

        if (FD_ISSET(allConnection[i],&readfds)) {

            handleEvent(allConnection[i]);

        }

    }

}


1.3.2 poll模型

基本上效率和select是同样的,select缺点的23都没有改掉。


1.3.3 epoll模型

1. Epoll没有最大并发连接的限制,上限是最大能够打开文件的数目,这个数字一般远大于2048, 一般来说这个数目和系统内存关系非常大,具体数目能够cat /proc/sys/fs/file-max察看。

2. 效率提升。Epoll最大的长处就在于它仅仅管你“活跃”的连接。而跟连接总数无关,应用程序就能直接定位到事件,而不必遍历整个FD集合。因此在实际的网络环境中,Epoll的效率就会远远高于selectpoll


int res = epoll_wait(epfd, events, 20, 120);

for(int i = 0; i < res; i++) {

    handleEvent(events[n]);

}


3. 内存拷贝。Epoll在这点上使用了“共享内存”,这个内存拷贝也省略了。

 


1.3.4 使用epoll

Epoll的接口非常easy。仅仅有三个函数。十分易用。 

int epoll_create(int size);

生成一个epoll专用的文件描写叙述符,事实上是申请一个内核空间。用来存放你想关注的socket fd上是否发生以及发生了什么事件。

size就是你在这个Epoll fd上能关注的最大socket fd数,大小自定,仅仅要内存足够。

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

控制某个Epoll文件描写叙述符上的事件:注冊、改动、删除。当中參数epfdepoll_create()创建Epoll专用的文件描写叙述符。相对于select模型中的FD_SETFD_CLR宏。

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

等待I/O事件的发生;參数说明:

Ø epfd:epoll_create()生成的Epoll专用的文件描写叙述符。

Ø epoll_event:用于回传代处理事件的数组。

Ø maxevents:每次能处理的事件数。

Ø timeout:等待I/O事件发生的超时值;

Ø 返回发生事件数。

上面讲到了Reactor的基本概念、框架和处理流程,基于Reactor模型的select。poll和epoll进行了比較分析后,再来对照看网络编程框架就会更容易理解了


 

2. Libeasy网络编程框架

Libeasy底层使用的是Libev事件库,在分析Libeasy代码前。首先对Libev有相关了解。


2.1 Libev简单介绍

Libev是什么?

Libev is an event loop: you register interest in certain events (such as a file descriptor being readable or a timeout occurring), and it will manage these event sources and provide your program with events.

Libev是一个event loop:向libev注冊感兴趣的events,比方Socket可读事件。libev会对所注冊的事件的源进行管理,并在事件发生时触发相应的程序。

通过event watcher来注冊事件


libev定义的watcher类型

Ø ev_io  // io 读写类型watcher

Ø ev_timer  // 定时器 类watcher

Ø ev_periodic

Ø ev_signal

Ø ev_child

Ø ev_stat

Ø ev_idle

Ø ev_prepare

Ø ev_check

Ø ev_embed

Ø ev_fork

Ø ev_cleanup

Ø ev_async  // 线程同步信号watcher

在libev中watcher还能支持优先级 


2.1.1 libev使用

以下以一个简单样例程序说明libev的使用。

这段程序实现从标准输入异步读取数据。5.5秒内没有数据到来则超时的功能。


#include <ev.h>

#include <stdio.h> 

ev_io stdin_watcher;

ev_timer timeout_watcher;

// all watcher callbacks have a similar signature

// this callback is called when data is readable on stdin

static void stdin_cb (EV_P_ ev_io *w, int revents) {

  puts ("stdin ready");

  // for one-shot events, one must manually stop the watcher

  // with its corresponding stop function.

  ev_io_stop (EV_A_ w);

  // this causes all nested ev_run's to stop iterating

  ev_break (EV_A_ EVBREAK_ALL);

}

// another callback, this time for a time-out

static void timeout_cb (EV_P_ ev_timer *w, int revents) {

  puts ("timeout");

  // this causes the innermost ev_run to stop iterating

  ev_break (EV_A_ EVBREAK_ONE);

}

int main (void) {

   // use the default event loop unless you have special needs

   struct ev_loop *loop = EV_DEFAULT;

   // initialise an io watcher, then start it

   // this one will watch for stdin to become readable

   ev_io_init (&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ);

   ev_io_start (loop, &stdin_watcher);

   // initialise a timer watcher, then start it

   // simple non-repeating 5.5 second timeout

   ev_timer_init (&timeout_watcher, timeout_cb, 5.5, 0.);

   ev_timer_start (loop, &timeout_watcher);

   // now wait for events to arrive

   ev_run (loop, 0);

   // break was called, so exit

   return 0;

}


2.1.2  Libev和Libevent比較


libevent和libev架构近似同样,对于非定时器类型。libevent使用双向链表管理。而libev则是使用数组来管理。

如我们所知,新的fd总是系统可用的最小fd,所以这个长度能够进行限制大小的,我们用一个连续的数组来存储fd/watch 信息。

例如以下图,我们用anfd[fd]就能够找到相应的fd/watcher 信息,当然可能遇到anfd超出我们的buffer长度情形,这是我们用相似relloc 的函数来做数组迁移、扩大容量。但这样的概率是非常小的,所以其对系统性能的影响能够忽略不计。


 


我们用anfd[fd]找到的结构体中。有一个指向io_watch_list的头指针,以epoll为例。当epoll_wait返回一个fd_event时 。我们就能够直接定位到相应fd的watch_list,这个watch_list的长度一般不会超过3 ,fd_event会有一个导致触发的事件,我们用这个事件依次和各个watch注冊的event做 “&” 操作, 假设不为0,则把相应的watch增加到待处理队列pendings中(当我们启用watcher优先级模式时,pendings是个2维数组,此时仅考虑普通模式)所以我们能够看到,这个操作是非常非常快。


 


再看增加watch的场景。把watch插入到相应的链表中,这个操作也是直接定位,然后在fdchange队列中,增加相应的fd(假设这个fd已经被增加过,则不会发生这一步。我们通过anfd[fd]中一个bool 值来推断)


注意。假如我们在某个fd上已经有个watch 注冊了read事件,这时我们又再增加一个watch,还是read 事件,但是不同的回调函数,在此种情况下,我们不应该调用epoll_ctrl 之类的系统调用。由于我们的events集合是没有改变的,所以为了达到这个目的。anfd[fd]结构体中,另一个events事件,它是原先的全部watcher的事件的“|操作。向系统的epoll从新增加描写叙述符的操作是在下次事件迭代開始前进行的,当我们依次扫描fdchangs。找到相应的anfd结构,假设发现先前的events与当前全部的watcher的“|操作结果不等,则表示我们须要调用epoll_ctrl之类的函数来进行更改,反之不做操作,作为一条原则,在调用系统调用前。我们已经做了充分的检查。确保不进行多余的系统调用。


再来看删除和更新一个watcher造作,基于以上分析,这个操作也是近乎O(1) 的,当然。假设events事件更改,可能会发生一次系统调用。


所以我们对io watcher的操作,在我们的用户层面上,差点儿总是是O(1)的复杂度。当然假设牵涉到epoll 文件结构的更新,我们的系统调用 epoll_ctrl 在内核中还是 O(lgn)的复杂度,但我们已经在我们所能掌控的范围内做到最好了。



2.1.3  性能測试对照


 


结论:The cost for setting up or changing event watchers is clearly much higher for libevent than for libev,具体性能对照測试參考这http://libev.schmorp.de/bench.html



2.2 libeasy

2.2.2 Server端使用


1、启动流程

eio_ = easy_eio_create(eio_, io_thread_count_);

easy_eio_create(eio_, io_thread_count_)做了例如以下几件事:


1. 分配一个easy_pool_t的内存区。存放easy_io_t对象 

2. 设置一些tcp參数,比方tcp_nodelay(tcp_cork),cpu亲核性等參数

3. 分配线程池的内存区并初始化

4. 对每一个线程构建client_listclient_array, 初始化双向链表conn_list session_list request_list

5. 设置listen watcher的ev回调函数为easy_connection_on_listen

6. 调用easy_baseth_init初始化io线程


easy_listen_t* listen = easy_connection_add_listen(eio_, NULL, port_, &handler_);

1. 从eio->pool中为easy_listen_t和listen watcher(在这里listen的watcher数默觉得2个)分配空间

2. 開始监听某个地址

3. 初始化每一个read_watcher

4. 关注listen fd的读事件,设置其回调函数easy_connection_on_accep在这里仅仅是初始化read_watcher, 还没有激活,激活在每一个IO线程启动调用easy_io_on_thread_start的时候做。一旦激活后,当有连接到来的时候。触发easy_connection_on_accept


rc = easy_eio_start(eio_);

1. 调用pthread_create启动每一个io线程。线程运行函数easy_io_on_thread_start。在easy_io_on_thread_start

a) 设置io线程的cpu亲核性sched_setaffinity

b) 假设不是listen_all或者仅仅有一个线程,则发出ev_async_send唤醒一个线程的listen_watcher(实现连接请求的负载均衡)

2. 线程运行ev_run


easy_eio_wait(eio_);

调用pthead_join等待线程结束



2、处理流程

当连接到来时触发easy_connection_on_accept

1. 调用accept获得连接fd。构建connection(easy_connection_new)。设置非堵塞,初始化connection參数和read、write、timeout的watcher

2. 切换listen线程,从自己切换到下一个io线程,调用ev_async_send激活下一个io线程的listen_watcher,实现负载均衡

3. 将connection增加到线程的connected_list线程列表中,并开启该连接上的read、write、timeout的watcher

 

当数据包到来时触发easy_connection_on_readable回调函数


1. 检查当前IO线程同一时候正在处理的请求是否超过EASY_IOTH_DOING_REQ_CNT(8192)。当前连接上的请求数是否超过EASY_CONN_DOING_REQ_CNT(1024),假设超过。则调用easy_connection_destroy(c)将连接销毁掉, 提供了一种负载保护机制


2. 构建message空间


3. 调用read读取socket数据


4. 作为服务端调用easy_connection_do_request

    a) 从message中解包

    b) 调用easy_connection_recycle_message看是否须要释放老的message,构建新的message空间

    c) 调用hanler的process处理数据包,假设返回easy_ok则调用easy_connection_request_done

    d) 对发送数据进行打包

    e) 对返回码是EASY_AGAIN的request将其放入session_list中

    f) 对返回码是EASY_OK的request将其放入request_done_list中。更新统计计数

    g) 统计计数更新  

    h) 调用easy_connection_write_socket发送数据包

    i) 调用easy_connection_evio_start中ev_io_start(c->loop, &c->read_watcher);开启该连接的读watcher

    j) 调用easy_connection_redispatch_thread进行负载均衡


假设负载均衡被禁或者该连接的message_list和output不为空,则直接返回,否则调用easy_thread_pool_rr从线程池中选择一个io线程。将该连接从原来io线程上移除(停止读写timeout 的watcher),将该连接增加到新的io线程中的conn_list中,调用ev_async_send唤醒新的io线程,在easy_connection_on_wakeup中调用easy_connection_evio_start将该连接的read、write、timeou的watcher再打开。

 

当socket可写时触发easy_connection_on_writable回调函数:

1. 调用easy_connection_write_socket写数据

2. 假设没有数据可写,将该连接的write_watcher停掉

 

2.2.3 客户端使用

libeasy作为客户端时。将每一个发往libeasy服务器端的请求包封装成一个session(easy_session_t),客户端将这个session放入连接的队列中然后返回,随后收到包后,将相应的session从连接的发送队列中删除。具体流程例如以下:


easy_session_t *easy_session_create(int64_t asize)

这个函数主要就做了一件事分配一个内存池easy_pool_t。在内存池头部放置一个easy_session_t,剩下部分存放实际的数据包Packet,然后将session的type设置为EASY_TYPE_SESSION。


异步请求

int easy_client_dispatch(easy_io_t *eio, easy_addr_t addr, easy_session_t *s)

1. 依据socket addr从线程池中选择一个线程,将session增加该线程的session_list。然后将该线程唤醒

2. 线程唤醒后调用easy_connection_send_session_list

     a)  当中首先调用easy_connection_do_client,这里首先在该线程的client_list中查找该addr的client,假设没找到,则新建一个client,初始化将其增加client_list。假设该client的connect未建立。调用easy_connection_do_connect建立该连接,然后返回该连接

    b) easy_connection_do_connect中首先创建一个新的connection结构,和一个socket。设置非堵塞,并调用connect进行连接,初始化该连接的read、write、timeout watcher(连接建立前是write,建立后是read)

    c) 调用easy_connection_session_build,当中调用encode函数对数据包进行打包,调用easy_hash_dlist_add(c->send_queue, s->packet_id, &s->send_queue_hash, &s->send_queue_list)将这个session增加到连接的发送队列中。这个函数将session增加到发送队列的同一时候。同一时候将相应的项增加到hash表的相应的bucket的链表头

    d) 开启timeout watcher     

    e) 调用easy_connection_write_socket发送数据包 

 

当回复数据包到达触发easy_connection_on_readable回调函数

1. 初始化一个easy_message_t存放数据包

2. 从内核缓冲区读入数据到应用层输入缓冲区中然后调用easy_connection_do_response进行处理

    a) 先解包,将该packet_id数据包从发包队列中删除,更新统计信息。停止timeout watcher。

    b) 假设是同步请求。则调用session的process函数,从而调用easy_client_wait_process函数,唤醒客户端接收数据包


当超时时间到还没有收到回复数据包时触发easy_connection_on_timeout_mesg回调函数

1. 从发送队列中删除请求数据包

2. 调用session的process函数从而调用easy_client_wait_process函数,唤醒客户端接

3. 释放此连接


同步请求

void *easy_client_send(easy_io_t *eio, easy_addr_t addr, easy_session_t *s)

同步请求是通过异步请求实现的easy_client_send方法封装了异步请求接口easy_client_dispatch

1. easy_client_send将session的process置为easy_client_wait_process方法

2. 初始化一个easy_client_wait_t wobj

3. 调用easy_client_dispatch方法发送异步请求

4. 客户端调用wait在wobj包装的信号量上等待

5. 当这个请求收到包的时候触发session的process函数回调easy_client_wait_process方法,当中会给wobj发送信号唤醒客户端,返回session封装的请求的ipacket

 


2.2.4  特性总结

1. 多个IO线程/epoll,大大提升了数据包处理性能,特别是处理小数据包的性能

针对多核处理器,libeasy使用多个IO线程来充分发挥处理器性能。提升IO处理能力。特别是针对小数据包IO处理请求数较多的情况下。性能提升十分明显。

2. 短任务和长任务区分,处理短任务更加高效(编码了内存拷贝,线程切换)

同步处理

对于短任务而言,调用用户process回调函数返回EASY_OK的数据包直接被增加该连接的发送队列,发送给客户端,这样避免了数据包的内存拷贝和线程切换开销。


异步处理


对于耗时较长的长任务而言。假设放在网络库的IO线程内运行,可能会堵塞住IO线程,所以须要异步处理。


 

3. 应用线程CPU亲核性,避免线程调度开销,提升处理性能

开启亲核特性将线程与指定CPU核进行绑定。避免了线程迁移导致的CPU cache失效,同一时候它同意我们精确控制线程和cpu核的关系,从而依据须要划分CPU核的使用。


sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)  


该函数设置进程为pid的这个进程,让它运行在mask所设定的CPU上.假设pid的值为0,则表示指定的是当前进程,使当前进程运行在mask所设定的那些CPU上.第二个參数cpusetsize是mask所指定的数的长度.通常设定为sizeof(cpu_set_t).假设当前pid所指定的进程此时没有运行在mask所指定的随意一个CPU上,则该指定的进程会从其他CPU上迁移到mask的指定的一个CPU上运行. 



4. 内存管理,降低小内存申请开销,避免内存碎片化

Libeasy的内存管理和nginx一致,有兴趣的能够去学习下,以下大致介绍其思想。

1) 创建一个内存池

2) 分配小块内存(size <= max)

小块内存分配模型:

 


上图这个内存池模型是由上3个小内存池构成的,由于第一个内存池上剩余的内存不够分配了,于是就创建了第二个新的内存池,第三个内存池是由于前面两个内存池的剩余部分都不够分配,所以创建了第三个内存池来满足用户的需求。由图可见:全部的小内存池是由一个单向链表维护在一起的。这里还有两个字段须要关注,failed和current字段。failed表示的是当前这个内存池的剩余可用内存不能满足用户分配请求的次数,假设下一个内存池也不能满足。那么它的failed也会加1,直到满足请求为止(假设没有现成的内存池来满足,会再创建一个新的内存池)。current字段会随着failed的增加而发生改变,假设current指向的内存池的failed达到了一个阈值。current就指向下一个内存池了。


3)、大块内存的分配(size > max)

大块内存的分配请求不会直接在内存池上分配内存来满足。而是直接向操作系统申请这么一块内存(就像直接使用malloc分配内存一样),然后将这块内存挂到内存池头部的large字段下。内存池的作用在于解决小块内存池的频繁申请问题。对于这样的大块内存,是能够忍受直接申请的。同样,用图形展示大块内存申请模型:


 


4)、内存释放

nginx利用了web server应用的特殊场景来完毕。一个web server总是不停的接受connection和request,所以nginx就将内存池分了不同的等级,有进程级的内存池、connection级的内存池、request级的内存池。也就是说,创建好一个worker进程的时候,同一时候为这个worker进程创建一个内存池,待有新的连接到来后,就在worker进程的内存池上为该连接创建起一个内存池;连接上到来一个request后,又在连接的内存池上为request创建起一个内存池。这样,在request被处理完后。就会释放request的整个内存池。连接断开后,就会释放连接的内存池。


5)、总结

通过内存的分配和释放能够看出。nginx仅仅是将小块内存的申请聚集到一起申请(内存池)。然后一起释放。避免了频繁申请小内存,降低内存碎片的产生等问题。


5. 网络流量自己主动负载均衡。充分发挥多核性能

1、在连接到来时,正在listen的IO线程接受连接。将其增加本线程的连接队列中,之后主动唤醒下一个线程运行listen。通过切换listen线程来使每一个线程上处理的连接数大致同样。

2、每一个连接上的流量是不同的,因此在每次有读写请求。计算该线程上近一段时间内请求速率,触发负载均衡,将该连接移动到其他线程上。使每一个线程处理的IO请求数大致同样。


6. encodedecode接口暴露给应用层,实现网络编程框架与协议的分离

Libeasy将网络数据包打包解包接口暴露给应用层,由用户定义数据包内容的格式。实现了网络编程框架与协议的分离,能够支持http等其他协议类型。格式更改更加方便。


7. 底层採用libev,对于事件的注冊和更改速度更快

 



參考资料

1、 C10K Problem

2、 Unix环境高级编程

3、 Unix网络编程

4、 Nginx、Libevent

5、 Libevhttp://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#WHAT_TO_READ_WHEN_IN_A_HURRY

6、 Libeasy源代码分析等http://www.cnblogs.com/foxmailed/archive/2013/02/17/2908180.html




-------------------------------------------------------------------------------------

黑夜路人。一个关注开源技术、乐于学习、喜欢分享的程序猿


博客:http://blog.csdn.net/heiyeshuwu

微博:http://weibo.com/heiyeluren

微信:heiyeluren2012  

想获取很多其他IT开源技术相关信息。欢迎关注微信!

微信二维码扫描高速关注本号码:




高性能网络编程技术

作者:jmz (360电商技术组)


怎样使网络服务器能够处理数以万计的客户端连接,这个问题被称为C10K Problem。在非常多系统中,网络框架的性能直接决定了系统的总体性能。因此研究解决高性能网络编程框架问题具有十分重要的意义。


1. 网络编程模型

C10K Problem中,给出了一些常见的解决大量并发连接的方案和模型。在此依据自己理解去除了一些不实际的方案。并做了一些整理。


1.1、PPC/TPC模型

典型的Apache模型(Process Per Connection。简称PPC),TPCThread Per Connection)模型,这两种模型思想相似,就是让每一个到来的连接都一边自己做事直到完毕。仅仅是PPC是为每一个连接开了一个进程,而TPC开了一个线程。但是当连接多了之后。如此多的进程/线程切换须要大量的开销。这类模型能接受的最大连接数都不会高。一般在几百个左右。


1.2、异步网络编程模型

异步网络编程模型都依赖于I/O多路复用模式一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。

分离器对象可将来自事件源的I/O事件分离出来,并分发到相应的read/write事件处理器(Event Handler)。开发者预先注冊须要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。

两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式採用同步IO,而Proactor採用异步IO。

在Reactor中,事件分离器负责等待文件描写叙述符或socket为读写操作准备就绪,然后将就绪事件传递给相应的处理器,最后由处理器负责完毕实际的读写工作。

而在Proactor模式中,处理器--或者兼任处理器的事件分离器,仅仅负责发起异步读写操作。IO操作本身由操作系统来完毕。传递给操作系统的參数须要包含用户定义的数据缓冲区地址和数据大小。操作系统才干从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完毕事件。然后将事件传递给相应处理器。


在Reactor中实现读:

注冊读就绪事件和相应的事件处理器

事件分离器等待事件

事件到来,激活分离器,分离器调用事件相应的处理器

事件处理器完毕实际的读操作,处理读到的数据,注冊新事件,然后返还控制权


在Proactor中实现读:

处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这样的情况下,处理器无视IO就绪事件,它关注的是完毕事件。

事件分离器等待操作完毕事件

在分离器等待过程中,操作系统利用并行的内核线程运行实际的读操作。并将结果数据存入用户自己定义缓冲区,最后通知事件分离器读操作完毕。

事件分离器呼唤处理器。

事件处理器处理用户自己定义缓冲区中的数据。然后启动一个新的异步操作。并将控制权返回事件分离器。


能够看出,两个模式的同样点。都是对某个IO事件的事件通知(即告诉某个模块。这个IO操作能够进行或已经完毕)。

在结构上。两者也有同样点:demultiplexor负责提交IO操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调handler;不同点在于,异步情况下(Proactor),当回调handler时。表示IO操作已经完毕;同步情况下(Reactor)。回调handler时。表示IO设备能够进行某个操作(can read or can write)。


1.2.1 Reactor模式框架

使用Proactor模式须要操作系统支持异步接口,因此在日常中比較常见的是Reactor模式的系统调用接口。使用Reactor模型,必备的几个组件:事件源、Reactor框架、多路复用机制和事件处理程序。先来看看Reactor模型的总体框架。接下来再对每一个组件做逐一说明。


 


事件源

Linux上是文件描写叙述符,Windows上就是Socket或者Handle了,这里统一称为“句柄集”;程序在指定的句柄上注冊关心的事件,比方I/O事件。


event demultiplexer——事件多路分发机制

Ø 由操作系统提供的I/O多路复用机制。比方select和epoll。

Ø 程序首先将其关心的句柄(事件源)及其事件注冊到event demultiplexer上;

Ø 当有事件到达时。event demultiplexer会发出通知“在已经注冊的句柄集中,一个或多个句柄的事件已经就绪”;

Ø 程序收到通知后,就能够在非堵塞的情况下对事件进行处理了。


Reactor——反应器

Reactor,是事件管理的接口,内部使用event demultiplexer注冊、注销事件;并运行事件循环。当有事件进入“就绪”状态时。调用注冊事件的回调函数处理事件。

一个典型的Reactor声明方式


class Reactor {  

public:  

    int register_handler(Event_Handler *pHandler, int event);  

    int remove_handler(Event_Handler *pHandler, int event);  

    void handle_events(timeval *ptv);  

    // ...  

};  


Event Handler——事件处理程序

事件处理程序提供了一组接口。每一个接口相应了一种类型的事件。供Reactor在相应的事件发生时调用,运行相应的事件处理。

通常它会绑定一个有效的句柄。

以下是两种典型的Event Handler类声明方式,二者互有优缺点。


class Event_Handler {  

public:  

    virtual void handle_read() = 0;  

    virtual void handle_write() = 0;  

    virtual void handle_timeout() = 0;  

    virtual void handle_close() = 0;  

    virtual HANDLE get_handle() = 0;  

    // ...  

};  

class Event_Handler {  

public:  

    // events maybe read/write/timeout/close .etc  

    virtual void handle_events(int events) = 0;  

    virtual HANDLE get_handle() = 0;  

    // ...

};  


1.2.2 Reactor事件处理流程


前面说过Reactor将事件流“逆置”了。使用Reactor模式后。事件控制流能够參见以下的序列图

 


1.3 Select,pollepoll

在Linux环境中。比較常见的I/O多路复用机制就是Select,poll和epoll,以下对这三种机制进行分析和比較,并对epoll的使用进行介绍。


1.3.1 select模型

1. 最大并发数限制。由于一个进程所打开的FD(文件描写叙述符)是有限制的。由FD_SETSIZE设置。默认值是1024/2048。因此Select模型的最大并发数就被相应限制了。

2. 效率问题,select每次调用都会线性扫描全部的FD集合,这样效率就会呈现线性下降,把FD_SETSIZE改大的后果就是全部FD处理都慢慢来

3. 内核/用户空间 内存拷贝问题,怎样让内核把FD消息通知给用户空间呢?在这个问题上select採取了内存拷贝方法。


int res = select(maxfd+1, &readfds, NULL, NULL, 120);

if (res > 0) {

    for (int i = 0; i < MAX_CONNECTION; i++) {

        if (FD_ISSET(allConnection[i],&readfds)) {

            handleEvent(allConnection[i]);

        }

    }

}


1.3.2 poll模型

基本上效率和select是同样的。select缺点的23都没有改掉。


1.3.3 epoll模型

1. Epoll没有最大并发连接的限制,上限是最大能够打开文件的数目。这个数字一般远大于2048, 一般来说这个数目和系统内存关系非常大,具体数目能够cat /proc/sys/fs/file-max察看。

2. 效率提升,Epoll最大的长处就在于它仅仅管你“活跃”的连接,而跟连接总数无关,应用程序就能直接定位到事件,而不必遍历整个FD集合,因此在实际的网络环境中,Epoll的效率就会远远高于selectpoll


int res = epoll_wait(epfd, events, 20, 120);

for(int i = 0; i < res; i++) {

    handleEvent(events[n]);

}


3. 内存拷贝。Epoll在这点上使用了“共享内存”,这个内存拷贝也省略了。 


1.3.4 使用epoll

Epoll的接口非常easy。仅仅有三个函数,十分易用。 

int epoll_create(int size);

生成一个epoll专用的文件描写叙述符,事实上是申请一个内核空间。用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个Epoll fd上能关注的最大socket fd数。大小自定,仅仅要内存足够。

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

控制某个Epoll文件描写叙述符上的事件:注冊、改动、删除。当中參数epfdepoll_create()创建Epoll专用的文件描写叙述符。

相对于select模型中的FD_SETFD_CLR宏。

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

等待I/O事件的发生;參数说明:

Ø epfd:epoll_create()生成的Epoll专用的文件描写叙述符。

Ø epoll_event:用于回传代处理事件的数组;

Ø maxevents:每次能处理的事件数;

Ø timeout:等待I/O事件发生的超时值。

Ø 返回发生事件数。

上面讲到了Reactor的基本概念、框架和处理流程,基于Reactor模型的select,poll和epoll进行了比較分析后,再来对照看网络编程框架就会更容易理解了


 

2. Libeasy网络编程框架

Libeasy底层使用的是Libev事件库,在分析Libeasy代码前,首先对Libev有相关了解。


2.1 Libev简单介绍

Libev是什么?

Libev is an event loop: you register interest in certain events (such as a file descriptor being readable or a timeout occurring), and it will manage these event sources and provide your program with events.

Libev是一个event loop:向libev注冊感兴趣的events,比方Socket可读事件,libev会对所注冊的事件的源进行管理,并在事件发生时触发相应的程序。

通过event watcher来注冊事件


libev定义的watcher类型

Ø ev_io  // io 读写类型watcher

Ø ev_timer  // 定时器 类watcher

Ø ev_periodic

Ø ev_signal

Ø ev_child

Ø ev_stat

Ø ev_idle

Ø ev_prepare

Ø ev_check

Ø ev_embed

Ø ev_fork

Ø ev_cleanup

Ø ev_async  // 线程同步信号watcher

在libev中watcher还能支持优先级 


2.1.1 libev使用

以下以一个简单样例程序说明libev的使用。这段程序实现从标准输入异步读取数据,5.5秒内没有数据到来则超时的功能。


#include <ev.h>

#include <stdio.h> 

ev_io stdin_watcher;

ev_timer timeout_watcher;

// all watcher callbacks have a similar signature

// this callback is called when data is readable on stdin

static void stdin_cb (EV_P_ ev_io *w, int revents) {

  puts ("stdin ready");

  // for one-shot events, one must manually stop the watcher

  // with its corresponding stop function.

  ev_io_stop (EV_A_ w);

  // this causes all nested ev_run's to stop iterating

  ev_break (EV_A_ EVBREAK_ALL);

}

// another callback, this time for a time-out

static void timeout_cb (EV_P_ ev_timer *w, int revents) {

  puts ("timeout");

  // this causes the innermost ev_run to stop iterating

  ev_break (EV_A_ EVBREAK_ONE);

}

int main (void) {

   // use the default event loop unless you have special needs

   struct ev_loop *loop = EV_DEFAULT;

   // initialise an io watcher, then start it

   // this one will watch for stdin to become readable

   ev_io_init (&stdin_watcher, stdin_cb, /*STDIN_FILENO*/ 0, EV_READ);

   ev_io_start (loop, &stdin_watcher);

   // initialise a timer watcher, then start it

   // simple non-repeating 5.5 second timeout

   ev_timer_init (&timeout_watcher, timeout_cb, 5.5, 0.);

   ev_timer_start (loop, &timeout_watcher);

   // now wait for events to arrive

   ev_run (loop, 0);

   // break was called, so exit

   return 0;

}


2.1.2  Libev和Libevent比較


libevent和libev架构近似同样,对于非定时器类型,libevent使用双向链表管理,而libev则是使用数组来管理。如我们所知,新的fd总是系统可用的最小fd。所以这个长度能够进行限制大小的,我们用一个连续的数组来存储fd/watch 信息。例如以下图。我们用anfd[fd]就能够找到相应的fd/watcher 信息,当然可能遇到anfd超出我们的buffer长度情形,这是我们用相似relloc 的函数来做数组迁移、扩大容量。但这样的概率是非常小的,所以其对系统性能的影响能够忽略不计。


 


我们用anfd[fd]找到的结构体中。有一个指向io_watch_list的头指针,以epoll为例,当epoll_wait返回一个fd_event时 。我们就能够直接定位到相应fd的watch_list。这个watch_list的长度一般不会超过3 。fd_event会有一个导致触发的事件。我们用这个事件依次和各个watch注冊的event做 “&” 操作, 假设不为0,则把相应的watch增加到待处理队列pendings中(当我们启用watcher优先级模式时。pendings是个2维数组,此时仅考虑普通模式)所以我们能够看到,这个操作是非常非常快。


 


再看增加watch的场景。把watch插入到相应的链表中。这个操作也是直接定位,然后在fdchange队列中。增加相应的fd(假设这个fd已经被增加过,则不会发生这一步。我们通过anfd[fd]中一个bool 值来推断)


注意,假如我们在某个fd上已经有个watch 注冊了read事件,这时我们又再增加一个watch,还是read 事件,但是不同的回调函数,在此种情况下,我们不应该调用epoll_ctrl 之类的系统调用,由于我们的events集合是没有改变的,所以为了达到这个目的,anfd[fd]结构体中。另一个events事件。它是原先的全部watcher的事件的“|操作。向系统的epoll从新增加描写叙述符的操作是在下次事件迭代開始前进行的,当我们依次扫描fdchangs,找到相应的anfd结构,假设发现先前的events与当前全部的watcher的“|操作结果不等,则表示我们须要调用epoll_ctrl之类的函数来进行更改,反之不做操作。作为一条原则。在调用系统调用前,我们已经做了充分的检查。确保不进行多余的系统调用。


再来看删除和更新一个watcher造作,基于以上分析,这个操作也是近乎O(1) 的,当然。假设events事件更改,可能会发生一次系统调用。


所以我们对io watcher的操作,在我们的用户层面上,差点儿总是是O(1)的复杂度,当然假设牵涉到epoll 文件结构的更新。我们的系统调用 epoll_ctrl 在内核中还是 O(lgn)的复杂度,但我们已经在我们所能掌控的范围内做到最好了。



2.1.3  性能測试对照


 


结论:The cost for setting up or changing event watchers is clearly much higher for libevent than for libev。具体性能对照測试參考这http://libev.schmorp.de/bench.html



2.2 libeasy

2.2.2 Server端使用


1、启动流程

eio_ = easy_eio_create(eio_, io_thread_count_);

easy_eio_create(eio_, io_thread_count_)做了例如以下几件事:


1. 分配一个easy_pool_t的内存区,存放easy_io_t对象 

2. 设置一些tcp參数,比方tcp_nodelay(tcp_cork),cpu亲核性等參数

3. 分配线程池的内存区并初始化

4. 对每一个线程构建client_listclient_array, 初始化双向链表conn_list session_list request_list

5. 设置listen watcher的ev回调函数为easy_connection_on_listen

6. 调用easy_baseth_init初始化io线程


easy_listen_t* listen = easy_connection_add_listen(eio_, NULL, port_, &handler_);

1. 从eio->pool中为easy_listen_t和listen watcher(在这里listen的watcher数默觉得2个)分配空间

2. 開始监听某个地址

3. 初始化每一个read_watcher

4. 关注listen fd的读事件。设置其回调函数easy_connection_on_accep在这里仅仅是初始化read_watcher, 还没有激活。激活在每一个IO线程启动调用easy_io_on_thread_start的时候做。

一旦激活后,当有连接到来的时候。触发easy_connection_on_accept


rc = easy_eio_start(eio_);

1. 调用pthread_create启动每一个io线程。线程运行函数easy_io_on_thread_start,在easy_io_on_thread_start

a) 设置io线程的cpu亲核性sched_setaffinity

b) 假设不是listen_all或者仅仅有一个线程,则发出ev_async_send唤醒一个线程的listen_watcher(实现连接请求的负载均衡)

2. 线程运行ev_run


easy_eio_wait(eio_);

调用pthead_join等待线程结束



2、处理流程

当连接到来时触发easy_connection_on_accept

1. 调用accept获得连接fd。构建connection(easy_connection_new),设置非堵塞,初始化connection參数和read、write、timeout的watcher

2. 切换listen线程,从自己切换到下一个io线程。调用ev_async_send激活下一个io线程的listen_watcher。实现负载均衡

3. 将connection增加到线程的connected_list线程列表中,并开启该连接上的read、write、timeout的watcher

 

当数据包到来时触发easy_connection_on_readable回调函数


1. 检查当前IO线程同一时候正在处理的请求是否超过EASY_IOTH_DOING_REQ_CNT(8192),当前连接上的请求数是否超过EASY_CONN_DOING_REQ_CNT(1024),假设超过,则调用easy_connection_destroy(c)将连接销毁掉, 提供了一种负载保护机制


2. 构建message空间


3. 调用read读取socket数据


4. 作为服务端调用easy_connection_do_request

    a) 从message中解包

    b) 调用easy_connection_recycle_message看是否须要释放老的message,构建新的message空间

    c) 调用hanler的process处理数据包,假设返回easy_ok则调用easy_connection_request_done

    d) 对发送数据进行打包

    e) 对返回码是EASY_AGAIN的request将其放入session_list中

    f) 对返回码是EASY_OK的request将其放入request_done_list中,更新统计计数

    g) 统计计数更新  

    h) 调用easy_connection_write_socket发送数据包

    i) 调用easy_connection_evio_start中ev_io_start(c->loop, &c->read_watcher);开启该连接的读watcher

    j) 调用easy_connection_redispatch_thread进行负载均衡


假设负载均衡被禁或者该连接的message_list和output不为空,则直接返回。否则调用easy_thread_pool_rr从线程池中选择一个io线程,将该连接从原来io线程上移除(停止读写timeout 的watcher),将该连接增加到新的io线程中的conn_list中,调用ev_async_send唤醒新的io线程,在easy_connection_on_wakeup中调用easy_connection_evio_start将该连接的read、write、timeou的watcher再打开。

 

当socket可写时触发easy_connection_on_writable回调函数:

1. 调用easy_connection_write_socket写数据

2. 假设没有数据可写,将该连接的write_watcher停掉

 

2.2.3 客户端使用

libeasy作为客户端时,将每一个发往libeasy服务器端的请求包封装成一个session(easy_session_t),客户端将这个session放入连接的队列中然后返回,随后收到包后,将相应的session从连接的发送队列中删除。具体流程例如以下:


easy_session_t *easy_session_create(int64_t asize)

这个函数主要就做了一件事分配一个内存池easy_pool_t,在内存池头部放置一个easy_session_t。剩下部分存放实际的数据包Packet,然后将session的type设置为EASY_TYPE_SESSION。


异步请求

int easy_client_dispatch(easy_io_t *eio, easy_addr_t addr, easy_session_t *s)

1. 依据socket addr从线程池中选择一个线程,将session增加该线程的session_list,然后将该线程唤醒

2. 线程唤醒后调用easy_connection_send_session_list

     a)  当中首先调用easy_connection_do_client,这里首先在该线程的client_list中查找该addr的client,假设没找到,则新建一个client,初始化将其增加client_list,假设该client的connect未建立,调用easy_connection_do_connect建立该连接。然后返回该连接

    b) easy_connection_do_connect中首先创建一个新的connection结构,和一个socket。设置非堵塞。并调用connect进行连接,初始化该连接的read、write、timeout watcher(连接建立前是write。建立后是read)

    c) 调用easy_connection_session_build,当中调用encode函数对数据包进行打包,调用easy_hash_dlist_add(c->send_queue, s->packet_id, &s->send_queue_hash, &s->send_queue_list)将这个session增加到连接的发送队列中。这个函数将session增加到发送队列的同一时候,同一时候将相应的项增加到hash表的相应的bucket的链表头

    d) 开启timeout watcher     

    e) 调用easy_connection_write_socket发送数据包 

 

当回复数据包到达触发easy_connection_on_readable回调函数

1. 初始化一个easy_message_t存放数据包

2. 从内核缓冲区读入数据到应用层输入缓冲区中然后调用easy_connection_do_response进行处理

    a) 先解包,将该packet_id数据包从发包队列中删除,更新统计信息,停止timeout watcher,

    b) 假设是同步请求。则调用session的process函数,从而调用easy_client_wait_process函数,唤醒客户端接收数据包


当超时时间到还没有收到回复数据包时触发easy_connection_on_timeout_mesg回调函数

1. 从发送队列中删除请求数据包

2. 调用session的process函数从而调用easy_client_wait_process函数,唤醒客户端接

3. 释放此连接


同步请求

void *easy_client_send(easy_io_t *eio, easy_addr_t addr, easy_session_t *s)

同步请求是通过异步请求实现的easy_client_send方法封装了异步请求接口easy_client_dispatch

1. easy_client_send将session的process置为easy_client_wait_process方法

2. 初始化一个easy_client_wait_t wobj

3. 调用easy_client_dispatch方法发送异步请求

4. 客户端调用wait在wobj包装的信号量上等待

5. 当这个请求收到包的时候触发session的process函数回调easy_client_wait_process方法。当中会给wobj发送信号唤醒客户端,返回session封装的请求的ipacket

 


2.2.4  特性总结

1. 多个IO线程/epoll,大大提升了数据包处理性能,特别是处理小数据包的性能

针对多核处理器,libeasy使用多个IO线程来充分发挥处理器性能,提升IO处理能力。特别是针对小数据包IO处理请求数较多的情况下,性能提升十分明显。

2. 短任务和长任务区分,处理短任务更加高效(编码了内存拷贝,线程切换)

同步处理

对于短任务而言。调用用户process回调函数返回EASY_OK的数据包直接被增加该连接的发送队列,发送给客户端。这样避免了数据包的内存拷贝和线程切换开销。


异步处理


对于耗时较长的长任务而言,假设放在网络库的IO线程内运行,可能会堵塞住IO线程,所以须要异步处理。


 

3. 应用线程CPU亲核性,避免线程调度开销,提升处理性能

开启亲核特性将线程与指定CPU核进行绑定,避免了线程迁移导致的CPU cache失效,同一时候它同意我们精确控制线程和cpu核的关系。从而依据须要划分CPU核的使用。


sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask)  


该函数设置进程为pid的这个进程,让它运行在mask所设定的CPU上.假设pid的值为0,则表示指定的是当前进程,使当前进程运行在mask所设定的那些CPU上.第二个參数cpusetsize是mask所指定的数的长度.通常设定为sizeof(cpu_set_t).假设当前pid所指定的进程此时没有运行在mask所指定的随意一个CPU上,则该指定的进程会从其他CPU上迁移到mask的指定的一个CPU上运行. 



4. 内存管理,降低小内存申请开销。避免内存碎片化

Libeasy的内存管理和nginx一致。有兴趣的能够去学习下。以下大致介绍其思想。

1) 创建一个内存池

2) 分配小块内存(size <= max)

小块内存分配模型:

 


上图这个内存池模型是由上3个小内存池构成的,由于第一个内存池上剩余的内存不够分配了,于是就创建了第二个新的内存池。第三个内存池是由于前面两个内存池的剩余部分都不够分配,所以创建了第三个内存池来满足用户的需求。

由图可见:全部的小内存池是由一个单向链表维护在一起的。这里还有两个字段须要关注。failed和current字段。failed表示的是当前这个内存池的剩余可用内存不能满足用户分配请求的次数,假设下一个内存池也不能满足。那么它的failed也会加1,直到满足请求为止(假设没有现成的内存池来满足,会再创建一个新的内存池)。current字段会随着failed的增加而发生改变,假设current指向的内存池的failed达到了一个阈值,current就指向下一个内存池了。


3)、大块内存的分配(size > max)

大块内存的分配请求不会直接在内存池上分配内存来满足,而是直接向操作系统申请这么一块内存(就像直接使用malloc分配内存一样)。然后将这块内存挂到内存池头部的large字段下。内存池的作用在于解决小块内存池的频繁申请问题,对于这样的大块内存。是能够忍受直接申请的。同样。用图形展示大块内存申请模型:


 


4)、内存释放

nginx利用了web server应用的特殊场景来完毕。一个web server总是不停的接受connection和request,所以nginx就将内存池分了不同的等级,有进程级的内存池、connection级的内存池、request级的内存池。也就是说。创建好一个worker进程的时候,同一时候为这个worker进程创建一个内存池,待有新的连接到来后,就在worker进程的内存池上为该连接创建起一个内存池。连接上到来一个request后,又在连接的内存池上为request创建起一个内存池。

这样,在request被处理完后。就会释放request的整个内存池,连接断开后。就会释放连接的内存池。


5)、总结

通过内存的分配和释放能够看出,nginx仅仅是将小块内存的申请聚集到一起申请(内存池),然后一起释放,避免了频繁申请小内存,降低内存碎片的产生等问题。


5. 网络流量自己主动负载均衡。充分发挥多核性能

1、在连接到来时,正在listen的IO线程接受连接,将其增加本线程的连接队列中。之后主动唤醒下一个线程运行listen。通过切换listen线程来使每一个线程上处理的连接数大致同样。

2、每一个连接上的流量是不同的。因此在每次有读写请求。计算该线程上近一段时间内请求速率,触发负载均衡。将该连接移动到其他线程上,使每一个线程处理的IO请求数大致同样。


6. encodedecode接口暴露给应用层,实现网络编程框架与协议的分离

Libeasy将网络数据包打包解包接口暴露给应用层,由用户定义数据包内容的格式,实现了网络编程框架与协议的分离,能够支持http等其他协议类型,格式更改更加方便。


7. 底层採用libev。对于事件的注冊和更改速度更快

 



參考资料

1、 C10K Problem

2、 Unix环境高级编程

3、 Unix网络编程

4、 Nginx、Libevent

5、 Libevhttp://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#WHAT_TO_READ_WHEN_IN_A_HURRY

6、 Libeasy源代码分析等http://www.cnblogs.com/foxmailed/archive/2013/02/17/2908180.html




-------------------------------------------------------------------------------------

黑夜路人。一个关注开源技术、乐于学习、喜欢分享的程序猿


博客:http://blog.csdn.net/heiyeshuwu

微博:http://weibo.com/heiyeluren

微信:heiyeluren2012  

想获取很多其他IT开源技术相关信息。欢迎关注微信!

微信二维码扫描高速关注本号码:



posted @ 2017-08-08 19:35  jzdwajue  阅读(448)  评论(0编辑  收藏  举报