【原創】高性能網絡編程技術



高性能網絡編程技術

做者:jmz (360電商技術組)html


怎樣使網絡服務器可以處理數以萬計的客戶端鏈接。這個問題被稱爲C10K Problem。在很是多系統中,網絡框架的性能直接決定了系統的整體性能。所以研究解決高性能網絡編程框架問題具備十分重要的意義。node


1. 網絡編程模型

C10K Problem中,給出了一些常見的解決大量併發鏈接的方案和模型,在此依據本身理解去除了一些不實際的方案,並作了一些整理。nginx


1.一、PPC/TPC模型

典型的Apache模型(Process Per Connection,簡稱PPC)。TPCThread Per Connection)模型,這兩種模型思想類似,就是讓每個到來的鏈接都一邊本身作事直到完畢。僅僅是PPC是爲每個鏈接開了一個進程,而TPC開了一個線程。web

可是當鏈接多了以後,如此多的進程/線程切換需要大量的開銷;這類模型能接受的最大鏈接數都不會高,通常在幾百個左右。redis


1.二、異步網絡編程模型

異步網絡編程模型都依賴於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端使用


一、啓動流程

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等待線程結束



二、處理流程

當鏈接到來時觸發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. 網絡流量本身主動負載均衡。充分發揮多核性能

一、在鏈接到來時,正在listen的IO線程接受鏈接。將其增長本線程的鏈接隊列中,以後主動喚醒下一個線程運行listen。經過切換listen線程來使每個線程上處理的鏈接數大體一樣。

二、每個鏈接上的流量是不一樣的,所以在每次有讀寫請求。計算該線程上近一段時間內請求速率,觸發負載均衡,將該鏈接移動到其餘線程上。使每個線程處理的IO請求數大體一樣。


6. encodedecode接口暴露給應用層,實現網絡編程框架與協議的分離

Libeasy將網絡數據包打包解包接口暴露給應用層,由用戶定義數據包內容的格式。實現了網絡編程框架與協議的分離,可以支持http等其餘協議類型。格式更改更加方便。


7. 底層採用libev,對於事件的註冊和更改速度更快

 



參考資料

一、 C10K Problem

二、 Unix環境高級編程

三、 Unix網絡編程

四、 Nginx、Libevent

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

六、 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.一、PPC/TPC模型

典型的Apache模型(Process Per Connection。簡稱PPC),TPCThread Per Connection)模型,這兩種模型思想類似,就是讓每個到來的鏈接都一邊本身作事直到完畢。僅僅是PPC是爲每個鏈接開了一個進程,而TPC開了一個線程。可是當鏈接多了以後。如此多的進程/線程切換需要大量的開銷。這類模型能接受的最大鏈接數都不會高。通常在幾百個左右。


1.二、異步網絡編程模型

異步網絡編程模型都依賴於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端使用


一、啓動流程

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等待線程結束



二、處理流程

當鏈接到來時觸發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. 網絡流量本身主動負載均衡。充分發揮多核性能

一、在鏈接到來時,正在listen的IO線程接受鏈接,將其增長本線程的鏈接隊列中。以後主動喚醒下一個線程運行listen。經過切換listen線程來使每個線程上處理的鏈接數大體一樣。

二、每個鏈接上的流量是不一樣的。所以在每次有讀寫請求。計算該線程上近一段時間內請求速率,觸發負載均衡。將該鏈接移動到其餘線程上,使每個線程處理的IO請求數大體一樣。


6. encodedecode接口暴露給應用層,實現網絡編程框架與協議的分離

Libeasy將網絡數據包打包解包接口暴露給應用層,由用戶定義數據包內容的格式,實現了網絡編程框架與協議的分離,可以支持http等其餘協議類型,格式更改更加方便。


7. 底層採用libev。對於事件的註冊和更改速度更快

 



參考資料

一、 C10K Problem

二、 Unix環境高級編程

三、 Unix網絡編程

四、 Nginx、Libevent

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

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




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

黑夜路人。一個關注開源技術、樂於學習、喜歡分享的程序猿


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

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

微信:heiyeluren2012  

想獲取不少其餘IT開源技術相關信息。歡迎關注微信!

微信二維碼掃描高速關注本號碼:


相關文章
相關標籤/搜索