高性能網絡編程技術

高性能網絡編程技術

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


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


1. 網絡編程模型

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


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。web

在Reactor中,事件分離器負責等待文件描述符或socket爲讀寫操做準備就緒,而後將就緒事件傳遞給對應的處理器,最後由處理器負責完成實際的讀寫工做。redis

而在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的接口很簡單,只有三個函數,十分易用。 

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的接口很簡單,只有三個函數,十分易用。 

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

相關文章
相關標籤/搜索