libevent 和 libev 提高網絡應用性能
建構現代的伺服器應用程式需要以某種方法同時接收數百、數千甚至數萬個事件,無論它們是内部請求還是網絡連接配接,都要有效地處理它們的操作。有許多解決方 案,但是 libevent 庫和 libev 庫能夠大大提高性能和事件處理能力。在本文中,我們要讨論在 UNIX® 應用程式中使用和部署這些解決方案所用的基本結構和方法。libev 和 libevent 都可以在高性能應用程式中使用,包括部署在 IBM Cloud 或 Amazon EC2 環境中的應用程式,這些應用程式需要支援大量并發用戶端或操作。
簡介
許多伺服器部署(尤其是 web 伺服器部署)面對的最大問題之一是必須能夠處理大量連接配接。無論是通過建構基于雲的服務來處理網絡通信流,還是把應用程式分布在 IBM Amazon EC 執行個體上,還是為網站提供高性能元件,都需要能夠處理大量并發連接配接。
一個好例子是,web 應用程式最近越來越動态了,尤其是使用 AJAX 技術的應用程式。如果要部署的系統允許數千用戶端直接在網頁中更新資訊,比如提供事件或問題實時監視的系統,那麼提供資訊的速度就非常重要了。在網格或雲 環境中,可能有來自數千用戶端的持久連接配接同時打開着,必須能夠處理每個用戶端的請求并做出響應。
在讨論 libevent 和 libev 如何處理多個網絡連接配接之前,我們先簡要回顧一下處理這類連接配接的傳統解決方案。
處理多個用戶端
處理多個連接配接有許多不同的傳統方法,但是在處理大量連接配接時它們往往會産生問題,因為它們使用的記憶體或 CPU 太多,或者達到了某個作業系統限制。
使用的主要方法如下:
- 循環:早期系統使用簡單的循環選擇解決方案,即循環周遊打開的網絡連接配接的清單,判斷是否有要讀取的數 據。這種方法既緩慢(尤其是随着連接配接數量增加越來越慢),又低效(因為在處理目前連接配接時其他連接配接可能正在發送請求并等待響應)。在系統循環周遊每個連接配接 時,其他連接配接不得不等待。如果有 100 個連接配接,其中隻有一個有資料,那麼仍然必須處理其他 99 個連接配接,才能輪到真正需要處理的連接配接。
- poll、epoll 和變體:這是對循環方法的改進,它用一個結構儲存要監視的每個連接配接的數組,當在網絡套接字上發現資料時,通過回調機制調用處理函數。poll 的問題是這個結構會非常大,在清單中添加新的網絡連接配接時,修改結構會增加負載并影響性能。
- 選擇:
函數調用使用一個靜态結構,它事先被寫死為相當小的數量(1024 個連接配接),是以不适用于非常大的部署。select()
在各種平台上還有其他實作(比如 Solaris 上的 /dev/poll 或 FreeBSD/NetBSD 上的 kqueue),它們在各自的 OS 上性能可能更好,但是無法移植,也不一定能夠解決處理請求的高層問題。
上面的所有解決方案都用簡單的循環等待并處理請求,然後把請求分派給另一個函數以處理實際的網絡互動。關鍵在于循環和網絡套接字需要大量管理代碼,這樣才能監聽、更新和控制不同的連接配接和接口。
處理許多連接配接的另一種方法是,利用現代核心中的多線程支援監聽和處理連接配接,為每個連接配接啟動一個新線程。這把責任直接交給作業系統,但是會在 RAM 和 CPU 方面增加相當大的開銷,因為每個線程都需要自己的執行空間。另外,如果每個線程都忙于處理網絡連接配接,線程之間的上下文切換會很頻繁。最後,許多核心并不适 于處理如此大量的活躍線程。
libevent 方法
libevent 庫實際上沒有更換
select()
、
poll()
或其他機制的基礎。而是使用對于每個平台最高效的高性能解決方案在實作外加上一個包裝器。
為了實際處理每個請求,libevent 庫提供一種事件機制,它作為底層網絡後端的包裝器。事件系統讓為連接配接添加處理函數變得非常簡便,同時降低了底層 I/O 複雜性。這是 libevent 系統的核心。
libevent 庫的其他元件提供其他功能,包括緩沖的事件系統(用于緩沖發送到用戶端/從用戶端接收的資料)以及 HTTP、DNS 和 RPC 系統的核心實作。
建立 libevent 伺服器的基本方法是,注冊當發生某一操作(比如接受來自用戶端的連接配接)時應該執行的函數,然後調用主事件循環
event_dispatch()
。執行過程的控制現在由 libevent 系統處理。注冊事件和将調用的函數之後,事件系統開始自治;在應用程式運作時,可以在事件隊列中添加(注冊)或删除(取消注冊)事件。事件注冊非常友善,可以通過它添加新事件以處理新打開的連接配接,進而建構靈活的網絡處理系統。
例如,可以打開一個監聽套接字,然後注冊一個回調函數,每當需要調用
accept()
函數以打開新連接配接時調用這個回調函數,這樣就建立了一個網絡伺服器。清單 1 所示的代碼片段說明基本過程:
清單 1. 打開監聽套接字,注冊一個回調函數(每當需要調用
accept()
函數以打開新連接配接時調用它),由此建立網絡伺服器
|
event_set()
函數建立新的事件結構,
event_add()
在事件隊列機制中添加事件。然後,
event_dispatch()
啟動事件隊列系統,開始監聽(并接受)請求。
清單 2 給出一個更完整的示例,它建構一個非常簡單的回顯伺服器:
清單 2. 建構簡單的回顯伺服器
|
下面讨論各個函數及其操作:
-
:主函數建立用來監聽連接配接的套接字,然後建立main()
的回調函數以便通過事件處理函數處理每個連接配接。accept()
-
:當接受連接配接時,事件系統調用此函數。此函數接受到用戶端的連接配接;添加客 戶端套接字資訊和一個 bufferevent 結構;在事件結構中為用戶端套接字上的讀/寫/錯誤事件添加回調函數;作為參數傳遞用戶端結構(和嵌入的 eventbuffer 和用戶端套接字)。每當對應的用戶端套接字包含讀、寫或錯誤操作時,調用對應的回調函數。accept_callback()
-
:當用戶端套接字有要讀的資料時調用它。作為回顯服務,此函數把 "you said..." 寫回用戶端。套接字仍然打開,可以接受新請求。buf_read_callback()
-
:當有要寫的資料時調用它。在這個簡單的服務中,不需要此函數,是以定義是空的。buf_write_callback()
-
:當出現錯誤時調用它。這包括用戶端中斷連接配接。在出現錯誤的所有場景中,關閉用戶端套接字,從事件清單中删除用戶端套接字的事件條目,釋放用戶端結構的記憶體。buf_error_callback()
-
:設定網絡套接字以開放 I/O。setnonblock()
當用戶端連接配接時,在事件隊列中添加新事件以處理用戶端連接配接;當用戶端中斷連接配接時删除事件。在幕後,libevent 處理網絡套接字,識别需要服務的用戶端,分别調用對應的函數。
為了建構這個應用程式,需要編譯 C 源代碼并添加 libevent 庫:
$ gcc -o basic basic.c -levent
。
從用戶端的角度來看,這個伺服器僅僅把發送給它的任何文本發送回來(見 清單 3)。
清單 3. 伺服器把發送給它的文本發送回來
|
這樣的網絡應用程式非常适合需要處理多個連接配接的大規模分布式部署,比如 IBM Cloud 系統。
很難通過簡單的解決方案觀察處理大量并發連接配接的情況和性能改進。可以使用嵌入的 HTTP 實作幫助了解可伸縮性。
使用内置的 HTTP 伺服器
如果希望建構本機應用程式,可以使用一般的基于網絡的 libevent 接口;但是,越來越常見的場景是開發基于 HTTP 協定的應用程式,以及裝載或動态地重新裝載資訊的網頁。如果使用任何 AJAX 庫,用戶端就需要 HTTP,即使您傳回的資訊是 XML 或 JSON。
libevent 中的 HTTP 實作并不是 Apache HTTP 伺服器的替代品,而是适用于與雲和 web 環境相關聯的大規模動态内容的實用解決方案。例如,可以在 IBM Cloud 或其他解決方案中部署基于 libevent 的接口。因為可以使用 HTTP 進行通信,伺服器可以與其他元件內建。
要想使用 libevent 服務,需要使用與主要網絡事件模型相同的基本結構,但是還必須處理網絡接口,HTTP 包裝器會替您處理。這使整個過程變成四個函數調用(初始化、啟動 HTTP 伺服器、設定 HTTP 回調函數和進入事件循環),再加上發送回資料的回調函數。清單 4 給出一個非常簡單的示例:
清單 4. 使用 libevent 服務的簡單示例
|
應該可以通過前面的示例看出代碼的基本結構,不需要解釋。主要元素是
evhttp_set_gencb()
函數(它設定當收到 HTTP 請求時要使用的回調函數)和
generic_request_handler()
回調函數本身(它用一個表示成功的簡單消息填充響應緩沖區)。
HTTP 包裝器提供許多其他功能。例如,有一個請求解析器,它會從典型的請求中提取出查詢參數(就像處理 CGI 請求一樣)。還可以設定在不同的請求路徑中要觸發的處理函數。通過設定不同的回調函數和處理函數,可以使用路徑 '/db/' 提供到資料庫的接口,或使用 '/memc' 提供到 memcached 的接口。
libevent 工具包的另一個特性是支援通用計時器。可以在指定的時間段之後觸發事件。可以通過結合使用計時器和 HTTP 實作提供輕量的服務,進而自動地提供檔案内容,在修改檔案内容時更新傳回的資料。例如,以前要想在新聞頻發的活動期間提供即時更新服務,前端 web 應用程式就需要定期重新裝載新聞稿,而現在可以輕松地提供内容。整個應用程式(和 web 服務)都在記憶體中,是以響應非常快。
這就是 清單 5 中的示例的主要用途:
清單 5. 使用計時器在新聞頻發的活動期間提供即時更新服務
|
這個伺服器的基本原理與前面的示例相同。首先,腳本設定一個 HTTP 伺服器,它隻響應對基本 URL 主機/端口組合的請求(不處理請求 URI)。第一步是裝載檔案 (
read_file()
)。在裝載最初的檔案時和在計時器觸發回調時都使用此函數。
read_file()
函數使用
stat()
函數調用檢查檔案的修改時間,隻有在上一次裝載之後修改了檔案的情況下,它才重新讀取檔案的内容。此函數通過調用
fread()
裝載檔案資料,把資料複制到另一個結構中,然後使用
strcpy()
把資料從裝載的字元串轉移到全局字元串中。
load_file()
函數是觸發計時器時調用的函數。它通過調用
read_file()
裝載内容,然後使用 RELOAD_TIMEOUT 值設定計時器,作為嘗試裝載檔案之前的秒數。libevent 計時器使用 timeval 結構,允許按秒和毫秒指定計時器。計時器不是周期性的;當觸發計時器事件時設定它,然後從事件隊列中删除事件。
使用與前面的示例相同的格式編譯代碼:
$ gcc -o basichttpfile basichttpfile.c -levent
。
現在,建立作為資料使用的靜态檔案;預設檔案是 sample.html,但是可以通過指令行上的第一個參數指定任何檔案(見 清單 6)。
清單 6. 建立作為資料使用的靜态檔案
|
現在,程式可以接受請求了,重新裝載計時器也啟動了。如果修改 sample.html 的内容,應該會重新裝載此檔案并在日志中記錄一個消息。例如,清單 7 中的輸出顯示初始裝載和兩次重新裝載:
清單 7. 輸出顯示初始裝載和兩次重新裝載
|
注意,要想獲得最大的收益,必須確定環境沒有限制打開的檔案描述符數量。可以使用 ulimit 指令修改限制(需要适當的權限或根通路)。具體的設定取決與您的 OS,但是在 Linux® 上可以用
-n
選項設定打開的檔案描述符(和網絡套接字)的數量:
清單 8. 用
-n
選項設定打開的檔案描述符數量
|
通過指定數字提高限制:
$ ulimit -n 20000
。
可以使用 Apache Bench 2 (ab2) 等性能基準測試應用程式檢查伺服器的性能。可以指定并發查詢的數量以及請求的總數。例如,使用 100,000 個請求運作基準測試,并發請求數量為 1000 個:
$ ab2 -n 100000 -c 1000 http://192.168.0.22:8081/
。
使用伺服器示例中所示的 8K 檔案運作這個示例系統,獲得的結果為大約每秒處理 11,000 個請求。請記住,這個 libevent 伺服器在單一線程中運作,而且單一用戶端不太可能給伺服器造成壓力,因為它還受到打開請求的方法的限制。盡管如此,在交換的文檔大小适中的情況下,這樣的 處理速率對于單線程應用程式來說仍然令人吃驚。
使用其他語言的實作
盡管 C 語言很适合許多系統應用程式,但是在現代環境中不經常使用 C 語言,腳本語言更靈活、更實用。幸運的是,Perl 和 PHP 等大多數腳本語言是用 C 編寫的,是以可以通過擴充子產品使用 libevent 等 C 庫。
例如,清單 9 給出 Perl 網絡伺服器腳本的基本結構。
accept_callback()
函數與 清單 1 所示核心 libevent 示例中的 accept 函數相同。
清單 9. Perl 網絡伺服器腳本的基本結構
|
用這些語言編寫的 libevent 實作通常支援 libevent 系統的核心,但是不一定支援 HTTP 包裝器。是以,對腳本程式設計的應用程式使用這些解決方案會比較複雜。有兩種方法:要麼把腳本語言嵌入到基于 C 的 libevent 應用程式中,要麼使用基于腳本語言環境建構的衆多 HTTP 實作之一。例如,Python 包含功能很強的 HTTP 伺服器類 (httplib/httplib2)。
應該指出一點:在腳本語言中沒有什麼東西是無法用 C 重新實作的。但是,要考慮到開發時間的限制,而且與現有代碼內建可能更重要。
libev 庫
與 libevent 一樣,libev 系統也是基于事件循環的系統,它在
poll()
、
select()
等機制的本機實作的基礎上提供基于事件的循環。到我撰寫本文時,libev 實作的開銷更低,能夠實作更好的基準測試結果。libev API 比較原始,沒有 HTTP 包裝器,但是 libev 支援在實作中内置更多事件類型。例如,一種 evstat 實作可以監視多個檔案的屬性變動,可以在 清單 4 所示的 HTTP 檔案解決方案中使用它。
但是,libevent 和 libev 的基本過程是相同的。建立所需的網絡監聽套接字,注冊在執行期間要調用的事件,然後啟動主事件循環,讓 libev 處理過程的其餘部分。
Libev是一個eventloop:向libev注冊感興趣的events,比如Socket可讀事件,libev會對所注冊的事件的源進行管理,并在事件發生時觸發相應的程式。
To do this, it must take more or less complete control over yourprocess (or thread) by executing the event loop handler, andwill then communicate events via a callback mechanism.
通過event watcher來注冊事件,which are relatively small C structures youinitialise with the details of the event, and then hand it over tolibev by starting the watcher.
先來解釋watcher,libev通過配置設定和注冊watcher對不同類型的事件進行監聽。不同僚件類型的watcher又對應不同的資料類型,watcher的定義模式是structev_TYPE或者ev_TYPE,其中TYPE為具體的類型。目前libev定義了如下類型的watcher:
- ev_io
- ev_timer
- ev_periodic
- ev_signal
- ev_child
- ev_stat
- ev_idle
- ev_prepare and ev_check
- ev_embed
- ev_fork
- ev_cleanup
- ev_async
下面是一個libev使用的例子,通過注冊io類型的watcher來監視STDIN可讀事件的發生:
static void my_cb (structev_loop *loop, ev_io *w, int revents)
{
ev_io_stop (w);
ev_break (loop, EVBREAK_ALL);
}
struct ev_loop *loop =ev_default_loop (0);
ev_io stdin_watcher;,
ev_init(&stdin_watcher, my_cb);
ev_io_set(&stdin_watcher, STDIN_FILENO, EV_READ);
ev_io_start (loop,&stdin_watcher);
ev_run (loop, 0);
上面的示例代碼中用到的與watcher相關的函數有ev_init,ev_io_set,ev_io_start,ev_io_stop。ev_init對一個watcher的與具體類型無關的部分進行初始化。ev_io_set對watcher的與io類型相關的部分進行初始化,顯然如果是TYPE類型那麼相應的函數就是ev_TYPE_set。可以采用ev_TYPE_init函數來替代ev_init和ev_TYPE_set。ev_io_start激活相應的watcher,watcher隻有被激活的時候才能接收事件。ev_io_stop停止已經激活的watcher。
接下來看看event loop的概念。示例程式中的ev_run、ev_break以及ev_loop_default都是eventloop控制函數。event loop定義為struct ev_loop。有兩種類型的eventloop,分别是default類型和dynamicallycreated類型,差別是前者支援子程序事件。ev_default_loop和ev_loop_new函數分别用于建立default類型或者dynamicallycreated類型的event loop。
event_run函數告訴系統應用程式開始對事件進行處理,有事件發生時就調用watchercallbacks。除非調用了ev_break或者不再有active的watcher,否則會一直重複這個過程。
libev程式設計
先來看libev提供的例子。這段代碼等待鍵盤事件的發生,或者逾時,兩個事件都會觸發程式結束。作業系統環境是ubuntu server10.10,libev是下載下傳的源碼,并沒有采用ubuntu server自己提供的版本,源碼的位址是http://dist.schmorp.de/libev/libev-4.04.tar.gz。例子代碼如下:
// 隻需include一個頭檔案
#include <ev.h>
#include <stdio.h> // for puts
// every watcher type has its own typedef'd struct
// with the name ev_TYPE
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, 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;
}
用如下指令編譯:
gcc -lev -o samplesample.c
結束語
libevent 和 libev 都提供靈活且強大的環境,支援為處理伺服器端或用戶端請求實作高性能網絡(和其他 I/O)接口。目标是以高效(CPU/RAM 使用量低)的方式支援數千甚至數萬個連接配接。在本文中,您看到了一些示例,包括 libevent 中内置的 HTTP 服務,可以使用這些技術支援基于 IBM Cloud、EC2 或 AJAX 的 web 應用程式。
本文來自:
http://www.ibm.com/developerworks/cn/aix/library/au-libev/index.html
http://hi.baidu.com/hins_pan/item/d23fee117d997d8889a95678
本文轉自:http://www.cnblogs.com/kunhu/p/3632285.html