本文由 Shaw 發表在 ScalaCool 團隊部落格。
上一篇
1.2 重新思考對計算機資源的利用
要理解「為什麼要採用響應式應用」以及「響應式應用是怎樣的」,我們首先需要快速瞭解一下計算機方面的知識。計算機在過去的幾十年中已經進步了許多。特別是在CPU的時鐘速度(MHz到GHz)和記憶體(千位元組到千兆位元組)方面。
然而,CPU 核心的數量在不斷變化才是過去幾年中發生的最大變化,儘管單個 CPU 的時鐘速度沒有增加。在撰寫本文時,大多數計算機至少有4個 CPU 核心,並且已經有供應商能夠提供具有1024個核心的 CPU 了。
另一方面,計算機的整體架構以及程式的執行機制尚未放生重大變化。因此,這種架構的一些侷限性(比如馮·諾依曼的瓶頸)現在已成為一個問題。要了解這種演變是如何影響 Web 應用程式開發的,我們需要先來看看兩個最受歡迎的 Web 伺服器體系結構。
1.2.1 基於執行緒或者事件的 web 應用伺服器
大致來說,有兩種可以用來實現 web 伺服器的程式設計模型,分別是執行緒模型以及事件模型。線上程模型中,大量的執行緒負責處理傳入的請求。在事件模型中,少量的請求處理執行緒通過訊息傳遞彼此進行通訊。響應式 web 應用伺服器採用的是事件模型。
基於執行緒模型的伺服器
基於執行緒的伺服器(比如 Apache Tomcat),可以看作是一個有很多個月臺的火車站,站長(接收者執行緒)決定哪列火車(HTTP 請求)到達哪個月臺(請求處理執行緒)。不難看出,有多少個月臺,就可以同時接納多少列火車。下圖說明了基於執行緒的伺服器是如何處理 HTTP 請求的。
顧名思義,基於執行緒的 web 伺服器依賴於在佇列中儘可能多地使用執行緒, 關於火車和執行緒伺服器之間的比較如表1.2所示。
基於事件模型的伺服器
為了解釋事件伺服器的工作原理,我們舉一個餐廳服務員的例子。
服務員可以從幾個顧客那裡接收訂單,然後將這些訂單交給廚房裡的多位廚師。服務員將他們的時間分配給手頭上的不同任務,而不用花太多時間在某一個任務上面。他們不需要一次性地處理整個訂單,比如:先上酒,然後冷盤,接下來主菜,最後是甜點和濃咖啡。因此,服務員可以高效地一次性服務多張桌子。
在我寫這本書的時候,Play 是基於 Netty 而構建的。當開發人員用 Play 構建應用的時候,開發人員只需要實現廚師“烹飪”響應的行為,而不用去實現服務員的行為,因為這些 Play 已經幫我們實現了。
基於事件模型的 Web 伺服器的機制如下圖所示。
在事件伺服器中,傳入的請求被分割成多個事件,這些事件代表了處理整個請求所涉及到的各種較小的任務。例如解析請求主體,從磁碟檢索檔案或者呼叫另外一個 web 服務。分割操作是由事件處理程式來完成的,這可能會觸發 I/O 操作,進而產生新的事件。
例如,你想傳送一個請求來獲取伺服器上檔案的大小。在此種情況下,事件處理程式在處理這個請求的時候將會對磁碟進行非同步呼叫。當作業系統計算出檔案大小的時候,會發出一箇中斷,這個中斷也就相當於一個新的事件。當輪到這個新事件執行的時候,你就得到了該請求響應的結果——檔案的大小。當作業系統在計算檔案大小的時候,事件迴圈程式可以處理佇列中的其他事件。
這種程式設計模型的一個重要意義就是,在任務上花費的時間應該是很小的。當服務員要上菜的時候,而廚師卻堅持要將某一個訂單上的全部菜品做完才讓服務員去上菜。那麼一旦服務員最後從廚房出來,將會看到顧客們一張張生氣的面孔。
當整個流水線,比如命令或者 HTTP 請求是非同步的時候,事件模型才會起作用,她可以使整個流水線能夠不被阻塞地去執行。非阻塞 I/O 通常指的是輸入-輸出操作,這些操作在執行工作時不會佔用當前的執行執行緒,而是在工作完成時傳送通知。
在事件模型伺服器以及執行緒模型伺服器上記憶體的利用率
與執行緒模型相比,基於事件模型的伺服器對硬體資源的利用率要高的多。事件伺服器的工作執行緒只需要幾個“服務員”執行緒就可以處理大量的請求而不用再像執行緒伺服器那樣,生成成千上萬的“火車軌道”了。使用較少數量的執行緒有兩個好處,分別是減少記憶體佔用和改善效能,因為這樣做減少了上下文切換、執行緒管理的時間以及排程開銷。
在 JVM 上建立的每個執行緒都有自己的堆疊空間,預設為 1MB。Apache Tomcat 的預設執行緒池大小為200,這意味著要啟動 Apache Tomcat,需要分配超過 200MB 的記憶體。但是,你僅僅只需要 16MB 就可以將一個 Play 程式跑起來。
雖然 200MB 在現在看來不算是一個很大的記憶體,但是不要忘了,這意味著要想同時處理200條請求就需要 200MB 的記憶體(這裡我們先不考慮在處理這些請求的時候可能會有額外的任務會佔用記憶體的情況),但是如果你想同時滿足10000個請求,那麼你將需要大量的記憶體。這種情況並不總是能夠滿足的,因為依賴於可用的記憶體,所以執行緒模型在面對大規模的併發情況時很難去做擴充套件。並且,執行緒模型除了使用大量記憶體之外,還會使 CPU 的效率降低。