Ruby Web 服務器的并發模型與性能

BurtonMacke 8年前發布 | 35K 次閱讀 Ruby 并發 Ruby開發

這是整個 Rack 系列文章的最后一篇了,在之前其實也嘗試寫過很多系列文章,但是到最后都因為各種原因放棄了,最近由于自己對 Ruby 的 webserver 非常感興趣,所以看了下社區中常見 webserver 的實現原理,包括 WEBrick、Thin、Unicorn 和 Puma,雖然在 Ruby 社區中也有一些其他的 webserver 有著比較優異的性能,但是在這有限的文章中也沒有辦法全都介紹一遍。

在這篇文章中,作者想對 Ruby 社區中不同 webserver 的實現原理和并發模型進行簡單的介紹,總結一下前面幾篇文章中的內容。

文中所有的壓力測試都是在內存 16GB、8 CPU、2.6 GHz Intel Core i7 的 macOS 上運行的,如果你想要復現這里的測試可能不會得到完全相同的結果。

WEBrick

WEBrick 是 Ruby 社區中非常古老的 Web 服務器,從 2000 年到現在已經有了將近 20 年的歷史了,雖然 WEBrick 有著非常多的問題,但是迄今為止 WEBrick 也是開發環境中最常用的 Ruby 服務器;它使用了最為簡單、直接的并發模型,運行一個 WEBrick 服務器只會在后臺啟動一個進程,默認監聽來自 9292 端口的請求。

當 WEBrick 通過 .select 方法監聽到來自客戶端的請求之后,會為每一個請求創建一個單獨 Thread 并在新的線程中處理 HTTP 請求。

run Proc.new { |env| ['200', {'Content-Type' => 'text/plain'}, ['get rack\'d']] }

如果我們如果創建一個最簡單的 Rack 應用,直接返回所有的 HTTP 響應,那么使用下面的命令對 WEBrick 的服務器進行測試會得到如下的結果:

Concurrency Level:      100
Time taken for tests:   22.519 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      2160000 bytes
HTML transferred:       200000 bytes
Requests per second:    444.07 [#/sec] (mean)
Time per request:       225.189 [ms] (mean)
Time per request:       2.252 [ms] (mean, across all concurrent requests)
Transfer rate:          93.67 [Kbytes/sec] received

在處理 ApacheBench 發出的 10000 個 HTTP 請求時,WEBrick 對于每個請求平均消耗了 225.189ms,每秒處理了 444.07 個請求;除此之外,在處理請求的過程中 WEBrick 進程的 CPU 占用率很快達到了 100%,通過這個測試我們就可以看出為什么不應該在生產環境中使用 WEBrick 作為 Ruby 的應用服務器,在業務邏輯和代碼更加復雜的情況下,WEBrick 的性能想必也不會達到期望。

在 2006 和 2007 兩年,Ruby 社區中發布了兩個至今都非常重要的開源項目,其中一個是 Mongrel,它提供了標準的 HTTP 接口,同時多語言的支持也使得 Mongrel 在當時非常流行,另一個項目就是 Rack 了,它在 Web 應用和 Web 服務器之間建立了一套統一的 標準 ,規定了兩者的協作方式,所有的應用只要遵循 Rack 協議就能夠隨時替換底層的應用服務器。

隨后,在 2009 年出現的 Thin 就站在了巨人的肩膀上,同時遵循了 Rack 協議并使用了 Mongrel 中的解析器,而它也是 Ruby 社區中第一個使用 Reactor 模型的 Web 服務器。

Thin 使用 Reactor 模型處理客戶端的 HTTP 請求,每一個請求都會交由 EventMachine,通過內部對事件的分發,最終執行相應的回調,這種事件驅動的 IO 模型與 node.js 非常相似,使用單進程單線程的并發模型卻能夠快速處理 HTTP 請求;在這里,我們仍然使用 ApacheBench 以及同樣的負載對 Thin 的性能進行簡單的測試。

Concurrency Level:      100
Time taken for tests:   4.221 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      880000 bytes
HTML transferred:       100000 bytes
Requests per second:    2368.90 [#/sec] (mean)
Time per request:       42.214 [ms] (mean)
Time per request:       0.422 [ms] (mean, across all concurrent requests)
Transfer rate:          203.58 [Kbytes/sec] received

對于一個相同的 HTTP 請求,Thin 的吞吐量大約是 WEBrick 的四倍,每秒能夠處理 2368.90 個請求,同時處理的速度也大幅降低到了 42.214ms;在壓力測試的過程中雖然 CPU 占用率有所上升但是在處理的過程中完全沒有超過 90%,可以說 Thin 的性能碾壓了 WEBrick,這可能也是開發者都不會在生產環境中使用 WEBrick 的最重要原因。

但是同樣作為單進程運行的 Thin,由于沒有 master 進程的存在,哪怕當前進程由于各種各樣奇怪的原因被操作系統殺掉,我們也不會收到任何的通知,只能手動重啟應用服務器。

Unicorn

與 Thin 同年發布的 Unicorn 雖然也是 Mongrel 項目的一個 fork,但是使用了完全不同的并發模型,每Unicorn 內部通過多次 fork 創建多個 worker 進程,所有的 worker 進程也都由一個 master 進程管理和控制:

由于 master 進程的存在,當 worker 進程被意外殺掉后會被 master 進程重啟,能夠保證持續對外界提供服務,多個進程的 worker 也能夠很好地壓榨多核 CPU 的性能,盡可能地提高請求的處理速度。

一組由 master 管理的 Unicorn worker 會監聽綁定的兩個 Socket,所有來自客戶端的請求都會通過操作系統內部的負載均衡進行調度,將請求分配到不同的 worker 進程上進行處理。

不過由于 Unicorn 雖然使用了多進程的并發模型,但是每個 worker 進程在處理請求時都是用了阻塞 I/O 的方式,所以如果客戶端非常慢就會大大影響 Unicorn 的性能,不過這個問題就可以通過反向代理來 nginx 解決。

在配置 Unicorn 的 worker 數時,為了最大化的利用 CPU 資源,往往會將進程數設置為 CPU 的數量,同樣我們使用 ApacheBench 以及相同的負載測試一個使用 8 核 CPU 的 Unicorn 服務的處理效率:

Concurrency Level:      100
Time taken for tests:   2.401 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      1110000 bytes
HTML transferred:       100000 bytes
Requests per second:    4164.31 [#/sec] (mean)
Time per request:       24.014 [ms] (mean)
Time per request:       0.240 [ms] (mean, across all concurrent requests)
Transfer rate:          451.41 [Kbytes/sec] received

經過簡單的壓力測試,當前的一組 Unicorn 服務每秒能夠處理 4000 多個請求,每個請求也只消耗了 24ms 的時間,比起使用單進程的 Thin 確實有著比較多的提升,但是并沒有數量級的差距。

除此之外,Unicorn 由于其多進程的實現方式會占用大量的內存,在并行的處理大量請求時你可以看到內存的使用量有比較明顯的上升。

距離 Ruby 社區的第一個 webserver WEBrick 發布的 11 年之后的 2011 年,Puma 正式發布了,它與 Thin 和 Unicorn 一樣都從 Mongrel 中繼承了 HTTP 協議的解析器,不僅如此它還基于 Rack 協議重新對底層進行了實現。

與 Unicorn 不同的是,Puma 是用了多進程加多線程模型,它可以同時在 fork 出來的多個 worker 中創建多個線程來處理請求;不僅如此 Puma 還實現了用于提高并發速度的 Reactor 模塊和線程池能夠在提升吞吐量的同時,降低內存的消耗。

但是由于 MRI 的存在,往往都需要使用 JRuby 才能最大化 Puma 服務器的性能,但是即便如此,使用 MRI 的 Puma 的吞吐量也能夠輕松達到 Unicorn 的兩倍。

Concurrency Level:      100
Time taken for tests:   1.057 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      750000 bytes
HTML transferred:       100000 bytes
Requests per second:    9458.08 [#/sec] (mean)
Time per request:       10.573 [ms] (mean)
Time per request:       0.106 [ms] (mean, across all concurrent requests)
Transfer rate:          692.73 [Kbytes/sec] received

在這里我們創建了 8 個 Puma 的 worker,每個 worker 中都包含 16~32 個用于處理用戶請求的線程,每秒中處理的請求數接近 10000,處理時間也僅為 10.573ms,多進程、多線程以及 Reactor 模式的協作確實能夠非常明顯的增加 Web 服務器的工作性能和吞吐量。

在 Puma 的 官方網站 中,有一張不同 Web 服務器內存消耗的對比圖:

我們可以看到,與 Unicorn 相比 Puma 的內存使用量幾乎可以忽略不計,它明顯解決了多個 worker 占用大量內存的問題;不過使用了多線程模型的 Puma 需要開發者在應用中保證不同的線程不會出現競爭條件的問題,Unicorn 的多進程模型就不需要開發者思考這樣的事情。

上述四種不同的 Web 服務器其實有著比較明顯的性能差異,在使用同一個最簡單的 Web 應用時,不同的服務器表現出了差異巨大的吞吐量:

Puma 和 Unicorn 兩者之間可能還沒有明顯的數量級差距,1 倍的吞吐量差距也可能很容易被環境因素抹平了,但是 WEBrick 可以說是絕對無法與其他三者匹敵的。

上述的不同服務器其實有著截然不同的 I/O 并發模型,因為 MRI 中 GIL 的存在我們很難利用多核 CPU 的計算資源,所以大多數多線程模型在 MRI 上的性能可能只比單線程略好,達不到完全碾壓的效果,但是 JRuby 或者 Rubinius 的使用確實能夠利用多核 CPU 的計算資源,從而增加多線程模型的并發效率。

傳統的 I/O 模型就是在每次接收到客戶端的請求時 fork 出一個新的進程來處理當前的請求或者在服務器啟動時就啟動多個進程,每一個進程在同一時間只能處理一個請求,所以這種并發模型的吞吐量有限,在今天已經幾乎看不到使用 accept & fork 這種方式處理請求的服務器了。

目前最為流行的方式還是混合多種 I/O 模型,同時使用多進程和多線程壓榨 CPU 計算資源,例如 Phusion Passenger 或者 Puma 都支持在單進程和多進程、單線程和多線程之前來回切換,配置的不同會創建不同的并發模型,可以說是 Web 服務器中最好的選擇了。

最后要說的 Thin 其實使用了非常不同的 I/O 模型,也就是事件驅動模型,這種模型在 Ruby 社區其實并沒有那么熱門,主要是因為 Rails 框架以及 Ruby 社區中的大部分項目并沒有按照 Reactor 模型的方式進行設計,默認的文件 I/O 也都是阻塞的,而 Ruby 本身也可以利用多進程和多線程的計算資源,沒有必要使用事件驅動的方式最大化并發量。

Node.js 就完全不同了。Javascript 作為一個所有操作都會阻塞主線程的語言,更加需要事件驅動模型讓主線程只負責接受 HTTP 請求,其余的臟活累活都交給線程池來做了,結果的返回都通過回調的形式通知主線程,這樣才能提高吞吐量。

在這個系列的文章中,我們先后介紹了 Rack 的實現原理以及 Rack 協議,還有四種 webserver 包括 WEBrick、Thin、Unicorn 和 Puma 的實現,除了這四種應用服務器之外,Ruby 社區中還有其他的應用服務器,例如:Rainbows 和 Phusion Passenger,它們都有各自的實現以及優缺點。

從當前的情況來看,還是更推薦開發者使用 Puma 或者 Phusion Passenger 作為應用的服務器,這樣能獲得最佳的效果。

Reference

 

來自:http://draveness.me/ruby-webserver

 

 本文由用戶 BurtonMacke 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!