Linux(muduo網絡庫):20---muduo簡介之(muduo性能測評:吞吐量、事件處理效率、延遲)

一、muduo與Boost.Asio、libevent2的吞吐量對比

  • 陳碩先生在編寫muduo的時候並沒有以高併發、高吞吐爲主要目標。但出乎意料,ping pong測試表明,muduo的吞吐量比Boost.Asio高15% 以上;比libevent2高18%以上,個別情況甚至達到70%

測試對象

測試代碼

測試環境

  • 硬件:DELL 490工作站,雙路Intel四核Xeon E5320 CPU,共8核, 主頻1.86GHz,內存16GiB。
  • 軟件:操作系統爲Ubuntu Linux Server 10.04.1 LTS x86_64,編譯器 是g++ 4.4.3。

測試方法

  • 依據asio性能測試18的辦法,用ping pong協議來測試muduo、asio、 libevent2在單機上的吞吐量
  • 簡單地說,ping pong協議是客戶端和服務器都實現echo協議。當 TCP連接建立時,客戶端向服務器發送一些數據,服務器會echo回這些 數據,然後客戶端再echo回服務器。這些數據就會像乒乓球一樣在客戶 端和服務器之間來回傳送,直到有一方斷開連接爲止。這是用來測試吞 吐量的常用辦法。注意數據是無格式的,雙方都是收到多少數據就反射 回去多少數據,並不拆包,這與後面的ZeroMQ延遲測試不同
  • 主要做了兩項測試:
    • 單線程測試。客戶端與服務器運行在同一臺機器,均爲單線程, 測試併發連接數爲1/10/100/1000/10000時的吞吐量
    • ·多線程測試。併發連接數爲100或1000,服務器和客戶端的線程數 同時設爲1/2/3/4。(由於我家裏只有一臺8核機器,而且服務器和客戶 端運行在同一臺機器上,線程數大於4沒有意義。)
  • 在所有測試中,ping pong消息的大小均爲16KiB。測試用的shell腳本可從http://gist.github.com/564985下載
  • 在同一臺機器測試吞吐量的原因如下:
    • 現在的CPU很快,即便是單線程單TCP連接也能把千兆以太網的帶 寬跑滿。如果用兩臺機器,所有的吞吐量測試結果都將是110MiB/s,失 去了對比的意義。(用Python也能跑出同樣的吞吐量,或許可以對比哪 個庫佔的CPU少。)
    • 在同一臺機器上測試,可以在CPU資源相同的情況下,單純對比網 絡庫的效率。也就是說在單線程下,服務端和客戶端各佔滿1個CPU, 比較哪個庫的吞吐量高。

測試結果

  • 單線程測試的結果(如下圖所示),數字越大越好。

  • 以上結果讓人大跌眼鏡,muduo居然比libevent2快70%!跟蹤 libevent2的源代碼發現,它每次最多從socket讀取4096字節的數據(證據在buffer.c的evbuffer_ read()函數),怪不得吞吐量比muduo小很多。 因爲在這一測試中,muduo每次讀取16384字節,系統調用的性價比較 高
  • 爲了公平起見,我再測了一次,這回兩個庫都發送4096字節的消息(如下圖所示):

  • 測試結果表明muduo的吞吐量平均比libevent2高18%以上
  • 多線程測試的結果(如下圖所示),數字越大越好

  • 試結果表明muduo的吞吐量平均比asio高15%以上

討論

  • muduo出乎意料地比asio性能優越,我想主要得益於其簡單的設計 和簡潔的代碼。asio在多線程測試中表現不佳,我猜測其主要原因是測 試代碼只使用了一個io_service,如果改用“io_service per CPU”的話,其 性能應該有所提高。我對asio的瞭解程度僅限於能讀懂其代碼,希望能 有asio高手編寫“io_service per CPU”的ping pong測試,以便與muduo做一 個公平的比較
  • 由於libevent2每次最多從網絡讀取4096字節,這大大限制了它的吞 吐量。
  • ping pong測試很容易實現,歡迎其他網絡庫(ACE、POCO、 libevent等)也能加入到對比中來,期待這些庫的高手出馬。

二、擊鼓傳花:對比muduo與libevent2的事件處理效率

  • 前面我們比較了muduo和libevent2的吞吐量,得到的結論是muduo 比libevent2快18%.有人會說,libevent2並不是爲高吞吐量的應用場景而 設計的,這樣的比較不公平,勝之不武。爲了公平起見,這回我們用 libevent2自帶的性能測試程序(擊鼓傳花)來對比muduo和libevent2在 高併發情況下的IO事件處理效率。
  • 測試用的軟硬件環境與前一小節相同,另外我還在自己的DELL E6400筆記本電腦上運行了測試,結果也附在後面。
  • 測試的場景是:有1000個人圍成一圈,玩擊鼓傳花的遊戲,一開始 第1個人手裏有花,他把花傳給右手邊的人,那個人再繼續把花傳給右 手邊的人,當花轉手100次之後遊戲停止,記錄從開始到結束的時間
  • 用程序表達是,有1000個網絡連接(socketpair()或pipe()),數據 在這些連接中順次傳遞,一開始往第1個連接裏寫1個字節,然後從這個 連接的另一頭讀出這1個字節,再寫入第2個連接,然後讀出來繼續寫到 第3個連接,直到一共寫了100次之後程序停止,記錄所用的時間
  • 以上是隻有一個活動連接的場景,我們實際測試的是100個或1000 個活動連接(即100朵花或1000朵花,均勻分散在人羣手中),而連接 總數(即併發數)從100~100000(10萬)。注意每個連接是兩個文件 描述符,爲了運行測試,需要調高每個進程能打開的文件數,比如設爲 256000。
  • libevent2的測試代碼位於test/bench.c,我修復了2.0.6-rc版裏的一個 小bug。修正後的代碼見已經提交給libevent2作者,現在下載的最新版本是正確的
  • muduo的測試代碼爲https://github.com/dongyusheng/csdn-code/blob/master/muduo/examples/pingpong/bench.cc

測試結果

  • 第一輪,分別用100個活動連接和1000個活動連接,無超時,讀寫 100次,測試一次遊戲的總時間(包含初始化)和事件處理的時間(不 包含註冊event watcher)隨連接數(併發數)變化的情況。具體解釋見 libev的性能測試文檔(http://libev.schmorp.de/bench.html),不同之處在於我們不比較timer event的性能, 只比較IO event的性能。對每個併發數,程序循環25次,刨去第一次的 熱身數據,後24次算平均值。測試用的腳本(https://github.com/chenshuo/recipes/blob/master/pingpong/libevent/run_bench.sh)是libev的作者Marc Lehmann寫的,我略做改用,用於測試muduo和libevent2。
  • 第一輪的結果(見下圖),請先只看“+”線(實線)和“×”線(粗 虛線)。“×”線是libevent2用的時間,“+”線是muduo用的時間。數字越 小越好。注意這個圖的橫座標是對數的,每一個數量級的取值點爲1, 2,3,4,5,6,7.5,10

  • 從兩條線的對比可以看出:
    • 1.libevent2在初始化event watcher方面比muduo快20%(左邊的兩 個圖)
    • 2.在事件處理方面(右邊的兩個圖)
      • a.在100個活動連接的情況下, 當總連接數(併發數)小於1000或大於30000時,二者性能差不 多; 當總連接數大於1000或小於30000時,libevent2明顯領先
      • b.在1000個活動連接的情況下, 當併發數小於10000時,libevent2和muduo得分接近; 當併發數大於10000時,muduo明顯佔優
  • 這裏有兩個問題值得探討:
    • 1.爲什麼muduo花在初始化上的時間比較多?
    • 2.爲什麼在一些情況下它比libevent2慢很多?

再次測試

  • 我仔細分析了其中的原因,並參考了libev的作者Marc Lehmann的觀點(http://list.schmorp.de/pipermail/libev/2010q2/001041.html),結論是:在第一輪初始化時,libevent2和muduo都是用epoll_ctl(fd, EPOLL_CTL_ ADD, ...)來添加文件描述符的event watcher。不同之處在 於,在後面24輪中,muduo使用了epoll_ctl(fd, EPOLL_CTL_MOD, ...)來 更新已有的event watcher;然而libevent2繼續調用epoll_ctl(fd, EPOLL_CTL_ADD, ...)來重複添加fd,並忽略返回的錯誤碼 EEXIST(File exists)。在這種重複添加的情況下,EPOLL_CTL_ADD 將會快速地返回錯誤,而EPOLL_CTL_MOD會做更多的工作,花的時 間也更長。於是libevent2撿了個便宜。
  • 爲了驗證這個結論,我改動了muduo,讓它每次都用 EPOLL_CTL_ADD方式初始化和更新event watcher,並忽略返回的錯 誤
  • 第二輪測試結果見上圖的細虛線,可見改動之後的muduo的初始化 性能比libevent2更好,事件處理的耗時也有所降低(我推測是kernel內 部的原因)。
  • 這個改動只是爲了驗證想法,我並沒有把它放到muduo最終的代碼中去,這或許可以留作日後優化的餘地。(具體的改動是https://github.com/dongyusheng/csdn-code/blob/master/muduo/muduo/net/poller/EPollPoller.cc的第138和173行,讀者可自行驗證
  • 同樣的測試在雙核筆記本電腦上運行了一次,結果如下圖所示(我的筆記本電腦的CPU主頻是2.4GHz,高於臺式機的1.86GHz,所以用時較少)

  • 結論:在事件處理效率方面,muduo與libevent2總體比較接近,各 擅勝場。在併發量特別大的情況下(大於10000),muduo略微佔優

三、muduo與Nginx的吞吐量對比

測試環境

測試方法

  • 爲了公平起見,Nginx和muduo都沒有訪問文件,而是 直接返回內存中的數據。畢竟我們想比較的是程序的網絡性能,而不是 機器的磁盤性能。另外,這裏客戶機的性能優於服務機,因爲我們要給 服務端HTTP server施壓,試圖使其飽和,而不是測試HTTP client的性能
  • muduo HTTP測試服務器的主要代碼:
// 詳細代碼參閱: https://github.com/dongyusheng/csdn-code/blob/master/muduo/muduo/net/http/tests/HttpServer_test.cc
void onRequest(const HttpRequest& req, HttpResponse* resp)
{
  //...
  if (req.path() == "/")
  {
    //...
  }
  else if (req.path() == "/favicon.ico")
  {
    resp->setStatusCode(HttpResponse::k200Ok);
    resp->setStatusMessage("OK");
    resp->setContentType("image/png");
    resp->setBody(string(favicon, sizeof favicon));
  }
  else if (req.path() == "/hello")
  {
    resp->setStatusCode(HttpResponse::k200Ok);
    resp->setStatusMessage("OK");
    resp->setContentType("text/plain");
    resp->addHeader("Server", "Muduo");
    resp->setBody("hello, world!\n");
  }
  else
  {
    resp->setStatusCode(HttpResponse::k404NotFound);
    resp->setStatusMessage("Not Found");
    resp->setCloseConnection(true);
  }
}

int main(int argc, char* argv[])
{
  int numThreads = 0;
  if (argc > 1)
  {
    benchmark = true;
    Logger::setLogLevel(Logger::WARN);
    numThreads = atoi(argv[1]);
  }
  EventLoop loop;
  HttpServer server(&loop, InetAddress(8000), "dummy");
  server.setHttpCallback(onRequest);
  server.setThreadNum(numThreads);
  server.start();
  loop.loop();
}

  • 客戶端運行以下命令來獲取/hello的內容,服務端返回字符串"hello, world!"

  • 先測試單線程的性能(見下圖),橫軸是併發連接數,縱軸爲每 秒完成的HTTP請求響應數目,下同。在測試期間,ab的CPU使用率低 於70%,客戶端遊刃有餘

  • 再對比muduo 4線程和Nginx 4工作進程的性能(見下圖)。當連接 數大於20時,top(1)顯示ab的CPU使用率達到85%,已經飽和,因此換 用weighttp(雙線程)來完成其餘測試

  • CPU使用率對比(百分比是top(1)顯示的數值):
    • 10000併發連接,4 workers/threads,muduo是4×83%,Nginx是 4×75%
    • 1000併發連接,4 workers/threads,muduo是4×85%,Nginx是 4×78%
  • 初看起來Nginx的CPU使用率略低,但是實際上二者都已經把CPU 資源耗盡了。與CPU benchmark不同,涉及IO的benchmark在滿負載下的 CPU使用率不會達到100%,因爲內核要佔用一部分時間處理IO。這裏 的數值差異說明muduo和Nginx在滿負荷的情況下,用戶態和內核態的 比重略有區別
  • 測試結果顯示muduo多數情況下略快,Nginx和muduo在合適的條件 下qps(每秒請求數)都能超過10萬。值得說明的是,muduo沒有實現完 整的HTTP服務器,而只是實現了滿足最基本要求的HTTP協議,因此這 個測試結果並不是說明muduo比Nginx更適合用做httpd,而是說明muduo 在性能方面沒有犯低級錯誤

四、muduo與ZeroMQ的延遲對比

  • 本節我們用ZeroMQ自帶的延遲和吞吐量測試(http://wiki.zeromq.org/results:perf-howto)與muduo做一對比, muduo代碼位於https://github.com/dongyusheng/csdn-code/tree/master/muduo/examples/zeromq。測試的內容很簡單,可以認爲是 §6.5.1 ping pong測試的翻版,不同之處在於這裏的消息的長度是固定的,收到完整的消息再echo回發送方,如此往復
  • 測試結果如下圖所示,橫軸爲消息的長度,縱軸爲單程延遲(微秒)。可見在消息長度小於16KiB時,muduo的延遲穩定地低於ZeroMQ。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章