由Scalable IO引發的思考

前言

前面我們一起研究了NIO,瞭解了NIO對TCP通信過程做了哪些優化,以及這些優化的實現原理。今天我們一起來看一看在利用NIO進行應用開發時,線程模型的演化。讓我們一起來學習一下Java併發包的作者Doug Lea的Scalable IO in Java。

一起讀一讀

NIO通過操作系統的epoll機制,幫助用戶在通信條件就緒時能夠獲得相應的Event。幫助上層應用準確的調度線程啓動計算。這種模式跟我們接觸過的很多設計都很相似,比如在Java圖形界面編程中AWT通過界面的按鈕觸發相應事件從而完成功能計算。

 

這種事件驅動計算執行的模式就是響應模式(Reactor Pattern)。一個最簡單的響應式線程模型如下:

 

在響應模式中有以下幾個要素: 

  • 事件(Event)。事件是計算條件就緒的信號,他是所有計算行爲的起點。不同的事件會驅動不同的計算執行。事件都是由不同的客戶端行爲生成的。

  • 響應器(Reactor)。響應器是計算的協調和調度者。是事件和計算行爲之間的橋樑。Reactor收集所有的事件,同時根據不同的Event調用不同的處理器進行處理。所以他又相當於任務收集與分發(dispatch)角色。

  • 處理器(Handler)。處理器是具體任務的執行,所有的事件處理邏輯都集成在Handler內部。

從上面的過程描述可以看出在Reactor內部存在這樣一個while循環,不停的探測事件,同時根據不同的事件new出對應的Handler去process事件。對於網絡IO來說事件主要有如下幾種:

  • ACCEPT事件。對應着客戶端的連接請求,TCP三次握手。

  • READ事件。對應着客戶端的數據發送條件據需。(客戶端斷開連接後也會不停觸發READ事件,這個不再贅述)。

  • WRITE事件。對應着服務端向客戶端發送數據條件就緒。

在READ和WRITE之間就是我們的業務處理過程PROCESS,這個不需要事件觸發。通常情況下我們的通信過程運行上面的處理循環中就OK了。到目前爲止,我們的所有處理邏輯運行在一個線程當中。但是現實中我們常常面臨着客戶端的併發壓力,因此我們的模型需要進行改變。解決併發計算問題的慣用手段就是分治(Devide and Conquer),加多線程。這就是我們的業務邏輯多線程模型。

 

加多線程只是解決問題的指導思想,但是具體落地實施時是有一些講究的。多線程應該怎麼加,在哪個粒度加是有講究的。一個請求的處理鏈路可以看作是read,decode,compute,encode,send等多個處理節點(Handler)串聯而成。有些節點在事件響應模式下造成性能問題的可能性是比較小的,而有些節點是重型計算節點。而整個機器的線程計算資源是有限的,我們需要將有限的資源分發給最需要的模塊,這樣才能最大可能的提高整機的吞吐。從上圖中可以看出,我們將多線程提供給了業務邏輯處理節點上。因爲這部分的計算壓力是比較大的。這一個改進的實現比較容易的,只要將業務處理的部分通過線程池提交,同時在線程處理完畢後把通信的SocketChannel重新註冊Send事件,這樣Reactor就能夠重新dispatch send任務了。

同理,如果隨着業務併發的增長,計算瓶頸逐漸轉移到read和send節點上(此時我們的Reactor還是單線程執行)。我們就需要在Reactor上加持多線程資源。這部分的改進稍微麻煩一點,因爲事件是通過註冊和探測實現的。事件必須註冊(綁定)到特定的Selector,後續的響應也由對應的Selector探測分發。因此如果維持一個Selector的狀態,探測分發模塊將無法進行分治。所以需要引入多個Selector,在註冊事件時可以採取隨機或者輪轉的方式進行註冊分治。每個Selector運行在獨立的探測線程中,從而達到分治的目的。這個模型就是主從Reactor的雛形。

 

在Reactor內部創建多個Selector,爲每個Selector創建單獨的運行線程。這種模式也是向重型節點進行資源分配傾斜的分治思路。因爲accept相對read和send來說是一個及其輕量的操作,不會出現性能問題。還是那句話,線程資源是有限的,只有好鋼用在刀刃上才能儘可能的提升整臺機器的吞吐。通過將accept和read,send的Selector分離,實現數據傳輸的異步化和多線程化,這就是主從Reactor模式。負責ACCEPT邏輯的Reactor被稱爲主Reactor,負責read和send的稱爲從Reactor的模式。從Reactor可以是多實例多線程運行的,從而具備可伸縮的擴展能力(而主Reactor是不行的,因爲ServerSocketChannel只有一個只能單線程運行,對於Accept來說也不需要,這個操作很輕量)。主從Reactor線程模型如下:

從上面的演化過程我們可以看出,整個模型的演變和優化過程就是不停的細化拆解每個模塊。不斷將重型節點劃分出來形成獨立運行模塊。尋找計算模型中的性能瓶頸點,進行模塊隔離,進行計算資源傾斜配置。這就是分治法最重要的落地原則。

一些思考

模型認知是重要的知識沉澱。從上面的介紹可以看出React模型是一個優秀的線程模型。在不同的性能場景下都可以方便的進行擴展優化。我常常思考,一個經驗豐富的工程師比一個小鮮肉工程師到底應該強在哪裏。對優秀模型認知理解一定是非常重要的一項。如果我們仔細去研究,一定會發現很多優秀的框架本質的模型抽象是非常驚人的相似(比如TensorFlow的OP流和Flink的算子流非常相似,都是流水線的解題思路)。對這些優秀模型的認識和思考對我們去面對新的問題會有很大的幫助。 

程序設計是一門管理科學。Reactor是一種優秀的IO模型,擴展性非常強。我覺得這更像是一種IO場景下的線程管理模式。我們在程序設計的過程中,一定碰到過當需求發生部分擴散之後,整個架構需要進行翻天覆地的改變才能滿足新的功能需求。在我看來,這是設計者沒有能夠管理好我們的工程,沒有劃分好功能模塊,沒有爲我們面對的需求設計好一套相互獨立,有序配合的執行流程和協作機制。試想一下,如果今天沒有MVC分層思想,讓我們來寫一個Web應用,我想大部分人會苦不堪言。因爲MVC思想本質上是針對Web場景的工程管理思路,他對Web應用設計了明確角色層次,職責劃分和協作依賴準則。這跟管理世界裏的組織架構設定,部門職能分配,人員分工和組織協同配合是一個道理。

歡迎關注個人公衆號討論交流。ScalableIO in Java的原文可以到我的Git上進行下載:https://github.com/SharkWater/blog.git

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