標籤:

軟體性能調優:看數據,還是看概念?

上周寫了「想讓伺服器跑得快,並不是換個編程語言那麼簡單」,很多朋友的留言歪了樓:論性能,C語言甩Python數倍到數十倍,你說和編程語言沒關?拜託,程序君只是說,不是換個編程語言那麼簡單。這種留言,我都是建議看了原文再說。還有朋友留言說她轉到朋友圈裡炸了鍋,大家眾口一詞:不談zero copy,談什麼伺服器性能!優化系統調用有個毛用!

對此,我只能說,buzz word荼毒太深,以至於大家陷進去,遇到問題,先擺個偉光正的大道理出來。我想,很多開口閉口zero copy的主,也許連zero copy是何物都沒搞清楚。

我們看wikipeidia上的定義:

"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network.

很清楚,zero-copy是當數據需要從內存的A點拷到B的時候,CPU不參與這個過程,也就是不通過調用 memcpy(),實現內存拷貝。這個過程一般是device driver來完成,通過DMA(direct memory access)繞過CPU在外設(一般是NIC,也就是網卡)和主存中做乾坤大挪移。放一張圖:

至於DMA是怎麼工作的,怎麼發起,怎麼通知CPU,cache coherence怎麼處理,這就不是本文要講的內容。

當然,這是狹義的zero copy,一般更廣義的zero copy還包括在DMA結束後的整個數據的生存周期里,數據儘可能地少拷貝(絕對不是不拷貝哦)。

我們看Kernel如何收包(簡化版):

  • 驅動初始化時,為NIC分配ring buffer。

  • NIC收到報文後,找到下一個空閑的buffer,將其DMA到buffer指定的內存里,同時發送interrupt,告知Kernel。(第一次zero copy)

  • Kernel處理這個interrupt,將這個buffer據為己有做後續處理,然後新分配一個buffer,還給driver。(第二次zero copy)。

  • 接下來交給TCP stack處理(因為咱們講的是伺服器開發,一般是TCP伺服器)。TCP是個stream based protocol,在這裡,報文的排序,組包(把TCP頭去掉,payload連接起來供application使用)等等,根據實現的不同,也許需要copy至少一次。另外,如果自己作為發送端,由於需要考慮潛在的retransmit,一般也會copy一份到retransmit queue里。(網路設備專門優化不在此討論範圍之內)

  • 之後交給application處理,這涉及到kernel space到user space的切換。將application的buffer和kernel的buffer映射起來不是一件簡單的事情,所以這裡一般也會有一次copy。

當然,現在有跳過kernel的stack,從NIC直接DMA到user space的技術,但那一般是網路廠商乾的勾當。我覺得咱們做一個伺服器軟體,還是不要搶人家TCP stack的生意,否則你會把自己玩死。

基本上,這些動作都發生在application無法控制的kernel里。好處是:當你 recv() 的時候,你拿到了一個只包含有 TCP payload 的 重組好的,可以線性閱讀的buffer。你只需要關心application level的邏輯。你能控制的,也只有之後,儘可能少的copy這個buffer。我們兜了這麼一大圈,終於回到之前的問題:不談zero copy,談什麼伺服器性能,談什麼優化系統調用!

無奈的是,我們只能控制application level的zero copy。

假設我們寫了一個 HTTP server。大多數HTTP請求頭都不會太大,除非是post一個文件,或者應用伺服器設置了巨大的cookie。我們假設平均而言整個請求大小是4k(這個假設沒有價值,只是為了比對),而我們的 server 稍稍2b一些,除了正常處理中的copy之外,還會把整個請求 memmcpy() 一遍。為公平起見,我們不和那些佔用時間比較長的系統調用比較,就和 gettimeofday() 比一比。主體代碼如下:

帶著gprof編譯運行,然後查看看結果:

(注意:這個結果你最好自己寫代碼測一下,不同的環境可能不一樣,我用的是digital ocean最小的instance,ubuntu 14.04)

喲,貌似 memcpy() 沒有想像的那麼慢嘛,比 gettimeofday() 慢一點點而已(至少不是量級上的差距)。

那問題來了,你是費盡心思去優化散落在各處小小的,基本上不可避免的copy呢,還是1s調用一次 gettimeofday(),而不是來一個包就調用一次,省卻99.99%的調用呢?

你是會把發送response時分別兩次發送header和body的兩個 write() 合併成一個,來減少幾十ms級的網路round trip的延遲,還是費盡心思去優化散落的copy呢?

你是會通過使用 strace,gprof,以及 systemtap 等各種工具,追溯到真正性能所在的瓶頸,然後對症下藥,還是不假思索地跳將出來:一切不談zero-copy而論performance的伺服器軟體都是耍流氓!

當你看不起系統調用帶來的損耗時,你是否又知道,當你苦苦追尋zero-copy的時候,kernel已經儘力在提供各種擴充的系統調用來儘可能讓某些應用場景快起來?比如 sendfile()?如果你的response是個靜態文件,你可以通過這個系統調用輕鬆實現zero-copy?

寫這麼些,不是證明我有多對,我的知識也有可能是錯的。只是當我們遇到問題的時候,是真正測量還是人云亦云,吐幾個buzz word就自認為解決問題了呢?

至少,我寫上一篇文章的時候,我還拿strace親測了一下nginx在首次訪問和再次訪問同一URI下系統調用的不同呢?

不管你怎麼看待題圖中的人物,但我喜歡:「我不是為了輸贏,我只是認真」這句話。performance是認認真真不斷測量和調整打造出來的,不是拍腦門想出來的。

如果您覺得這篇文章不錯,請點贊。多謝!

歡迎訂閱公眾號『程序人生』(搜索微信號 programmer_life)。每篇文章都力求原汁原味,北京時間中午12點左右,美西時間下午8點左右與您相會。


推薦閱讀:

為什麼這麼多人把「Wikipedia」稱作「wiki」?
等待還是放下?
為什麼春天和秋天的溫度差不多,但春天給人感覺暖和,秋天感覺涼爽?
有哪些你覺得可信或難以駁倒的陰謀論?

TAG: | 迷思 |