標籤:

想讓伺服器跑得快,並不是換個編程語言那麼簡單

最近一個讀者問我:程序君,我是一個經常被你黑的phper,我想學一門新的語言,做伺服器開發,看你好像用過好多語言,能推薦一個么?最好是開發效率高,支持並發,性能又好的。

看了這留言,程序君滿臉黑線。真是冤枉啊,我和你萍水相逢,何來經常黑你?我只是偶爾調侃一下PHP而已,不敢黑任何一個phper,更不敢黑這「世界上最好的語言」呢。:)

言歸正傳。推薦一個開發效率高,並行性能好的語言/框架做伺服器開發,這個問題有點太寬泛了:nodejs,go,elixir(erlang/otp),clojure,tornado(python),… 都不違背開發效率高,並行性能好的前提。可是:想讓伺服器跑得快,編程語言(及其虛擬機)本身真的能起決定性的作用么?

我們看看最基本的web server開發,畢竟,做互聯網的,沒人離得開web server。然而,我想大部分所謂的web程序員,沒幾個懂web server的。拜library/framework所賜,在他們眼裡,web server就是十行代碼搞定的玩意兒:

(nodejs的高逼格hello world:寫一個web server)

library/framework是好東西,可以讓你不要在意各種細節,大大降低了開發的門檻。但入了門之後,成了熟練工種,最好反過來仔細研究一下被封裝掉的那些細節,否則層次永遠停留在碼工的水平。

Web server和任何TCP server一樣,都是處理網路流量的,因此網路處理的效率便是web server效率的關鍵。除此之外:

  • HTTP Request報文的處理,尤其是header的處理(一個高效的parser)

  • URL dispatch(一個高效的pattern matching engine)

  • HTTP Response的封裝(一個高效的string builder)

也是容不得半點馬虎的效率殺手。我們主要看網路相關的,這個是最大頭。

網路處理

首先需要考慮的是使用什麼並發模型:threading? single event-loop? multiple event-loop?

早期的web server使用OS提供的thread/process(以下講thread,並不區分thread/process),一個connection,accept下來之後,就交給一個thread去處理,thread內部可以做任意的blocking I/O,並發性能靠thread的數量來保證,這就是threading模型。在實際使用中,一般會預先創建(pre-fork)一個thread pool來重複利用空閑的thread,減少thread創建/終止的系統損耗。Apache就是典型的threading模型。

Threading模型的好處是代碼很直觀,符合我們思考問題解決問題的常用模式。操作系統會處理線程的調度,使其適應多核的場景。壞處是每個request的處理都涉及上下文切換(context switch),系統損耗大,同時thread的內存消耗(memory footprint)不小,很難創建成千上萬個thread來有效的提供更多並發能力。

為了彌補threading模型的弱點,如今大多數web server採用的是event driven模型。很多人誤以為event loop是nodejs首創,實際lighthttpd/nginx很早就採用了event driven模型。

event driven主要有兩種使用場景:single event loop和multiple event loop。

所謂single event loop是指使用一個thread承載event loop,處理non blocking I/O。如圖:

lighthttpd/nodejs/tornado採用這種方式。單線程模型簡單,不必考慮跨線程同步(往往是locking),因為web server主要的處理瓶頸在網路的I/O,使用event handler來處理non blocking I/O後,單線程效率都相當高。不過,event handler的問題在於代碼很不直觀,原本順序的函數調用如今都變成了一個個callback,邏輯一複雜,就遇上callback hell。

multiple event loop一般是在每個物理core上運行一個event loop,以最大程度壓榨CPU資源。如圖:

nginx/go 採用此種並發模型。其缺點也是代碼不夠直觀,還要仔細處理跨線程同步。

在網路編程中,選取什麼樣的並發模型直接影響系統最終的效率。比如說,要達到每秒上百k請求的處理能力,threading模型肯定是無能為力的,用什麼語言都沒戲。

即便我們選取了看上去最好的mutilple event loop模型,在系統層面,需要優化的東西也不少:

  • 避免使用lock

  • 優化系統調用

避免使用lock是一個比較大的話題,多線程下內存分配都會觸及locking(nginx由於是多進程,這點上就要優於一般而言是單進程多線程的go web server)。locking會把並發處理降級成單線程處理,極大損傷系統的速度。就分配內存而言,可以採取memory pool預分配的方式,由每個thread的event loop自己管理自己的memory poll里的內存的使用,這樣就避免了大多內存分配的lock。其他用到lock的地方還有不少,不展開。

系統調用是另外一個很大的性能殺手。系統調用涉及用戶態到內核態的切換,非常耗時,有些系統調用還涉及到locking,總之,能少觸發系統調用就少觸發。我們看看在瀏覽器請求 GET / HTTP/1.1 後,strace(linux下的system call/signal監控軟體)監控到的nginx的系統調用:

我們看到,一個 GET / 觸發了:

  • gettimeofday

  • accept4

  • epoll_ctl

  • epoll_wait

  • gettimeofday

  • recvfrom

  • stat

  • open

  • fstat

  • pread

  • writev

  • write

  • close

這麼多系統調用。具體它們是幹什麼的就不解釋了,感興趣的可以自己研究。nginx是web server裡面優化得很好的系統,一個簡單的網頁請求,竟然還要這麼多系統調用。

正常我們accept一個connection,會使用 accept(),而nginx使用了 accept4(),這就是為了避免兩個額外的 fctrl() 而做的優化。

發送文件為什麼不用 sendfile(),send(),而是 writev(),大概也有類似的考慮。[補充] 今天仔細看了一下這幾個syscall的區別,其實sendfile()效率是最高的,因為你只要提供一個文件句柄,讀取文件,寫入socket這些事情都由kernel完成,其中,寫入還是直接DMA,zero copy,完全不用消耗CPU時間。不過sendfile()只適用於發送靜態文件;如果nginx要做reverse proxy,從application server接受response,則沒法使用。

有的web server的實現,把http header和http body分開發,這樣不但會多一個write的系統調用,還多了網路中的round trip(header, ack) - 要知道,一般的網路中,一個round trip,幾十ms就過去了,這雖然在高並發下並不是太大的問題,只是處理的latency增加了而已,但在低並發下,performance就大打折扣。

如果不是做streaming(transfer-coding: chunked),則http header和http body分開發送沒有任何必要。

除此之外,pread() 從磁碟上讀取整個文件損耗不小,第一次讀取後,應該緩存起來,下次相同用戶再請求(瀏覽器一般會帶If-Modified頭),只要沒改變,伺服器應該返回 304 NOT MODIFIED;而其他用戶請求這個文件,應該從內存中調用而非從磁碟讀取等等。我們看同一個用戶再訪問這個文件時,nginx都做了什麼:

最後,還有不少細節值得研究:我們知道,在http頭裡,伺服器要返回當前時間。獲取時間 gettimeofday() 這個系統調用消耗也不小,如果你的伺服器每秒要處理10k個請求,每次都獲取,意味著10k次系統調用。如果我們每秒定期調用一次 gettimeofday(),把結果緩存起來,可以減少99.999%的浪費。

不要小看這些優化,高並發情況下,即便多做一個動作要100us,10k乘上去,就是1s,嚇人吧?

語言的影響

寫一個web server這件事,時至今日,已經不用我們這些程序員去做了,因為,市面上已經存在太多太好的web server。但是作為一個伺服器開發者,總還需要寫點這樣那樣的TCP/UDP server。要能夠高效處理,如同上文所述,需要的是對架構的熟悉水平,對操作系統的了解程度,對協議的掌握。這些事關鍵。語言只是給你一個更好的發揮空間。當然,如果能夠做到對系統了如指掌,又對各個語言的優劣有有個基本了解,那就再好不過了。

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

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


推薦閱讀:

黑客馬拉松續 - 旅遊極客開發大賽
創新不是運動,而是文化
軟體性能調優:看數據,還是看概念?
[職場] 設定目標
閑扯設備廠商的轉型

TAG:迷思 |