在之前的文章中就討論過為什么在高并發(fā)的情況下,程序會(huì)崩潰。主要原因是,在高并發(fā)的情況下,有大量用戶請(qǐng)求需要程序計(jì)算處理,而目前的處理方式是,為每個(gè)用戶請(qǐng)求分配一個(gè)線程,當(dāng)程序內(nèi)部因?yàn)樵L問數(shù)據(jù)庫等原因造成線程阻塞時(shí),線程無法釋放去處理其他請(qǐng)求,這樣就會(huì)造成請(qǐng)求堆積,不斷消耗資源,最終導(dǎo)致程序崩潰。
這是傳統(tǒng)的 Web 應(yīng)用程序運(yùn)行期的線程特性。對(duì)于一個(gè)高并發(fā)的應(yīng)用系統(tǒng)來說,總是同時(shí)有很多個(gè)用戶請(qǐng)求到達(dá)系統(tǒng)的 Web 容器。Web 容器為每個(gè)請(qǐng)求分配一個(gè)線程進(jìn)行處理,線程在處理過程中,如果遇到訪問數(shù)據(jù)庫或者遠(yuǎn)程服務(wù)等操作,就會(huì)進(jìn)入阻塞狀態(tài),這個(gè)時(shí)候,如果數(shù)據(jù)庫或者遠(yuǎn)程服務(wù)響應(yīng)延遲,就會(huì)出現(xiàn)程序內(nèi)的線程無法釋放的情況,而外部的請(qǐng)求不斷進(jìn)來,導(dǎo)致計(jì)算機(jī)資源被快速消耗,最終程序崩潰。
那么有沒有不阻塞線程的編程方法呢?
一、反應(yīng)式編程
答案就是反應(yīng)式編程。反應(yīng)式編程本質(zhì)上是一種異步編程方案,在多線程(協(xié)程)、異步方法調(diào)用、異步 I/O 訪問等技術(shù)基礎(chǔ)之上,提供了一整套與異步調(diào)用相匹配的編程模型,從而實(shí)現(xiàn)程序調(diào)用非阻塞、即時(shí)響應(yīng)等特性,即開發(fā)出一個(gè)反應(yīng)式的系統(tǒng),以應(yīng)對(duì)編程領(lǐng)域越來越高的并發(fā)處理需求。
人們還提出了一個(gè)反應(yīng)式宣言,認(rèn)為反應(yīng)式系統(tǒng)應(yīng)該具備如下特質(zhì):
- 即時(shí)響應(yīng),應(yīng)用的調(diào)用者可以即時(shí)得到響應(yīng),無需等到整個(gè)應(yīng)用程序執(zhí)行完畢。也就是說應(yīng)用調(diào)用是非阻塞的。
- 回彈性,當(dāng)應(yīng)用程序部分功能失效的時(shí)候,應(yīng)用系統(tǒng)本身能夠進(jìn)行自我修復(fù),保證正常運(yùn)行,保證響應(yīng),不會(huì)出現(xiàn)系統(tǒng)崩潰和宕機(jī)的情況。
- 彈性,系統(tǒng)能夠?qū)?yīng)用負(fù)載壓力做出響應(yīng),能夠自動(dòng)伸縮以適應(yīng)應(yīng)用負(fù)載壓力,根據(jù)壓力自動(dòng)調(diào)整自身的處理能力,或者根據(jù)自身的處理能力,調(diào)整進(jìn)入系統(tǒng)中的訪問請(qǐng)求數(shù)量。
- 消息驅(qū)動(dòng),功能模塊之間,服務(wù)之間,通過消息進(jìn)行驅(qū)動(dòng),完成服務(wù)的流程。
目前主流的反應(yīng)式編程框架有 RxJAVA、Reactor 等,它們的主要特點(diǎn)是基于觀察者設(shè)計(jì)模式的異步編程方案,編程模型采用函數(shù)式編程。
觀察者模式和函數(shù)式編程有自己的優(yōu)勢(shì),但是反應(yīng)式編程并不是必須用觀察者模式和函數(shù)式編程。Flower 就是一個(gè)純消息驅(qū)動(dòng),完全異步,支持命令式編程的反應(yīng)式編程框架。
下面我們就看看 Flower 如何實(shí)現(xiàn)異步無阻塞的調(diào)用,以及 Flower 這個(gè)框架設(shè)計(jì)使用了什么樣的設(shè)計(jì)原則與模式。
二、反應(yīng)式編程框架Flower的基本原理
一個(gè)使用 Flower 框架開發(fā)的典型 Web 應(yīng)用的線程特性如下圖所示:
當(dāng)并發(fā)用戶到達(dá)應(yīng)用服務(wù)器的時(shí)候,Web 容器線程不需要執(zhí)行應(yīng)用程序代碼,它只是將用戶的 HTTP 請(qǐng)求變?yōu)檎?qǐng)求對(duì)象,將請(qǐng)求對(duì)象異步交給 Flower 框架的 Service 去處理,自身立刻就返回。因?yàn)槿萜骶€程不做太多的工作,所以只需極少的容器線程就可以滿足高并發(fā)的用戶請(qǐng)求,用戶的請(qǐng)求不會(huì)被阻塞,不會(huì)因?yàn)槿萜骶€程不夠而無法處理。相比傳統(tǒng)的阻塞式編程,Web 容器線程要完成全部的請(qǐng)求處理操作,直到返回響應(yīng)結(jié)果才能釋放線程;使用Flower 框架只需要極少的容器線程就可以處理較多的并發(fā)用戶請(qǐng)求,而且容器線程不會(huì)阻塞。
用戶請(qǐng)求交給基于 Flower 框架開發(fā)的業(yè)務(wù) Service 對(duì)象以后,Service 之間依然是使用異步消息通訊的方式進(jìn)行調(diào)用,不會(huì)直接進(jìn)行阻塞式的調(diào)用。一個(gè) Service 完成業(yè)務(wù)邏輯處理計(jì)算以后,會(huì)返回一個(gè)處理結(jié)果,這個(gè)結(jié)果以消息的方式異步發(fā)送給它的下一個(gè)Service。
傳統(tǒng)編程模型的 Service 之間如果進(jìn)行調(diào)用,被調(diào)用的Service 在返回之前,調(diào)用的 Service 方法只能阻塞等待。而 Flower 的 Service 之間使用了 AKKA Actor 進(jìn)行消息通信,調(diào)用者的 Service 發(fā)送調(diào)用消息后,不需要等待被調(diào)用者返回結(jié)果,就可以處理自己的下一個(gè)消息了。事實(shí)上,這些 Service 可以復(fù)用同一個(gè)線程去處理自己的消息,也就是說,只需要有限的幾個(gè)線程就可以完成大量的 Service 處理和消息傳輸,這些線程不會(huì)阻塞等待。
我們剛才提到,通常 Web 應(yīng)用主要的線程阻塞,是因?yàn)閿?shù)據(jù)庫的訪問導(dǎo)致的線程阻塞。Flower 支持異步數(shù)據(jù)庫驅(qū)動(dòng),用戶請(qǐng)求數(shù)據(jù)庫的時(shí)候,將請(qǐng)求提交給異步數(shù)據(jù)庫驅(qū)動(dòng),立刻就返回,不會(huì)阻塞當(dāng)前線程,異步數(shù)據(jù)庫訪問連接遠(yuǎn)程的數(shù)據(jù)庫,進(jìn)行真正的數(shù)據(jù)庫操作,得到結(jié)果以后,將結(jié)果以異步回調(diào)的方式發(fā)送給 Flower 的 Service 進(jìn)行進(jìn)一步的處理,這個(gè)時(shí)候依然不會(huì)有線程被阻塞。
也就是說,使用 Flower 開發(fā)的系統(tǒng),在一個(gè)典型的 Web 應(yīng)用中,幾乎沒有任何地方會(huì)被阻塞,所有的線程都可以被不斷地復(fù)用,有限的線程就可以完成大量的并發(fā)用戶請(qǐng)求,從而大大地提高了系統(tǒng)的吞吐能力和響應(yīng)時(shí)間,同時(shí),由于線程不會(huì)被阻塞,應(yīng)用就不會(huì)因?yàn)椴l(fā)量太大或者數(shù)據(jù)庫處理緩慢而宕機(jī),從而提高了系統(tǒng)的可用性。
Flower 框架實(shí)現(xiàn)異步無阻塞,一方面是利用了 Web 容器的異步特性,主要是 Servlet3.0以后提供的 AsyncContext,快速釋放容器線程;另一方面是利用了異步的數(shù)據(jù)庫驅(qū)動(dòng)以及異步的網(wǎng)絡(luò)通信,主要是 HttpAsyncClient 等異步通信組件。而 Flower 框架內(nèi),核心的應(yīng)用代碼之間的異步無阻塞調(diào)用,則是利用了 Akka 的 Actor 模型實(shí)現(xiàn)。
Akka Actor 的異步消息驅(qū)動(dòng)實(shí)現(xiàn)如下:
一個(gè) Actor 向另一個(gè) Actor 進(jìn)行通訊的時(shí)候,當(dāng)前 Actor 就是一個(gè)消息的發(fā)送者sender,當(dāng)它想要向另一個(gè) Actor 進(jìn)行通訊的時(shí)候,就需要獲得另一個(gè) Actor 的ActorRef,也就是一個(gè)引用,通過引用進(jìn)行消息通信。而 ActorRef 收到消息以后,會(huì)將這個(gè)消息放入到目標(biāo) Actor 的 Mailbox 里面去,然后就立即返回了。
也就是說一個(gè) Actor 向另一個(gè) Actor 發(fā)送消息的時(shí)候,不需要另一個(gè) Actor 去真正地處理這個(gè)消息,只需要將消息發(fā)送到目標(biāo) Actor 的 Mailbox 里面就可以了。自己不會(huì)被阻塞,可以繼續(xù)執(zhí)行自己的操作,而目標(biāo) Actor 檢查自己的 Mailbox 中是否有消息,如果有消息,Actor 則會(huì)在從 Mailbox 里面去獲取消息,對(duì)消息進(jìn)行異步的處理,而所有的 Actor會(huì)共享線程,這些線程不會(huì)有任何的阻塞。
三、反應(yīng)式編程框架Flower的設(shè)計(jì)方法
但是直接使用 Actor 進(jìn)行編程有很多不便,F(xiàn)lower 框架對(duì) Actor 進(jìn)行了封裝,開發(fā)者只需要編寫一些細(xì)粒度的 Service,這些 Service 會(huì)被包裝在 Actor 里面,進(jìn)行異步通信。Flower Service 例子如下:
publicclassServiceAimplementsService<Message2>{
@Override
publicObjectprocess(Message2message){
returnmessage.getAge()+1;
}
}
每個(gè) Service 都需要實(shí)現(xiàn)框架的 Service 接口的 process 方法,process 方法的輸入?yún)?shù)就是前一個(gè) Service process 方法的返回值,這樣只需要將 Service 編排成一個(gè)流程,Service 的返回值就會(huì)變成 Actor 的一個(gè)消息,被發(fā)送給下一個(gè) Service,從而實(shí)現(xiàn)Service 的異步通信。
Service 的流程編排有兩種方式,一種方式是編程實(shí)現(xiàn),如下:
getServiceFlow().buildFlow("ServiceA","ServiceB");
表示 ServiceA 的返回值將作為消息發(fā)送給 ServiceB,成為 ServiceB 的輸入值,這樣兩個(gè)Service 就可以合作完成一些更復(fù)雜的業(yè)務(wù)邏輯。
Flower 還支持可視化的 Service 流程編排,像下面這張圖一樣編輯流程定義文件,就可以開發(fā)一個(gè)異步業(yè)務(wù)處理流程。
那么這個(gè) Flower 框架是如何實(shí)現(xiàn)的呢?
Flower 框架的設(shè)計(jì)也是基于依賴倒置原則。所有應(yīng)用開發(fā)者實(shí)現(xiàn)的Service 類都需要包裝在 Actor 里面進(jìn)行異步調(diào)用,但是 Actor 不會(huì)依賴開發(fā)者實(shí)現(xiàn)的Service 類,開發(fā)者也不會(huì)依賴 Actor 類,他們共同依賴一個(gè) Service 接口,這個(gè)接口是框架提供的,如上面例子所示。
Actor 與 Service 的依賴倒置關(guān)系如下圖所示:
每個(gè) Actor 都依賴一個(gè) Service 接口,而具體的 Service 實(shí)現(xiàn)類,比如 MyService,則實(shí)現(xiàn)這個(gè) Service 接口。在運(yùn)行期實(shí)例化 Actor 的時(shí)候,這個(gè)接口被注入具體的 Service 實(shí)現(xiàn)類,比如 MyService。在 Flower 中,調(diào)用 MyService 對(duì)象,其實(shí)就是給包裝MyService 對(duì)象的 Actor 發(fā)消息,Actor 收到消息,執(zhí)行自己的 onReceive 方法,在這個(gè)方法里,Actor 調(diào)用 MyService 的 process 方法,并將 onReceive 收到的 Message 對(duì)象當(dāng)做 process 的輸入?yún)?shù)傳入。
process 處理完成后,返回一個(gè) Object 對(duì)象。Actor 會(huì)根據(jù)編排好的流程,獲取MyService 在流程中的下一個(gè) Service 對(duì)應(yīng)的 Actor,即 nextServiceActor,將 process返回的 Object 對(duì)象當(dāng)做消息發(fā)送給這個(gè) nextServiceActor。這樣,Service 之間就根據(jù)編排好的流程,異步、無阻塞地調(diào)用執(zhí)行起來了。
四、反應(yīng)式編程框架Flower的落地效果
Flower 框架在部分項(xiàng)目中落地應(yīng)用,應(yīng)用效果較為顯著,一方面,F(xiàn)lower 可以顯著提高系統(tǒng)的性能。這是某個(gè) C# 開發(fā)的系統(tǒng)使用 Flower 重構(gòu)后的 TPS 性能比較,使用 Flower 開發(fā)的系統(tǒng) TPS 差不多是原來 C# 系統(tǒng)的兩倍。
另一方面,F(xiàn)lower 對(duì)系統(tǒng)可用性也有較大提升,目前常見互聯(lián)網(wǎng)應(yīng)用架構(gòu)如下圖:
用戶請(qǐng)求通過網(wǎng)關(guān)服務(wù)器調(diào)用微服務(wù)完成處理,那么當(dāng)有某個(gè)微服務(wù)連接的數(shù)據(jù)庫查詢執(zhí)行較慢時(shí),如圖中服務(wù) 1,那么按照傳統(tǒng)的線程阻塞模型,就會(huì)導(dǎo)致服務(wù) 1 的線程都被阻塞在這個(gè)慢查詢的數(shù)據(jù)庫操作上。同樣的,網(wǎng)關(guān)線程也會(huì)阻塞在調(diào)用這個(gè)延遲比較厲害的服務(wù)1 上。
最終的效果就是,網(wǎng)關(guān)所有的線程都被阻塞,即使是不調(diào)用服務(wù) 1 的用戶請(qǐng)求也無法處理,最后整個(gè)系統(tǒng)失去響應(yīng),應(yīng)用宕機(jī)。使用阻塞式編程,實(shí)際的壓測(cè)效果如下,當(dāng)服務(wù) 1響應(yīng)延遲,出錯(cuò)率大幅飆升的時(shí)候,通過網(wǎng)關(guān)調(diào)用正常的服務(wù) 2 的出錯(cuò)率也非常高。
使用 Flower 開發(fā)的網(wǎng)關(guān),實(shí)際壓測(cè)效果如下,同樣服務(wù) 1 響應(yīng)延遲,出錯(cuò)率極高的情況下,通過 Flower 網(wǎng)關(guān)調(diào)用服務(wù) 2 完全不受影響。
五、總結(jié)
事實(shí)上,F(xiàn)lower 不僅是一個(gè)反應(yīng)式 Web 編程框架,還是反應(yīng)式的微服務(wù)框架。也就是說,F(xiàn)lower 的 Service 可以遠(yuǎn)程部署到一個(gè) Service 容器里面,就像我們現(xiàn)在常用的微服務(wù)架構(gòu)一樣。Flower 會(huì)提供一個(gè)獨(dú)立的 Flower 容器,用于啟動(dòng)一些 Service,這些Service 在啟動(dòng)了以后,會(huì)向注冊(cè)中心進(jìn)行注冊(cè),而且應(yīng)用程序可以將這些分布式的Service 進(jìn)行流程編排,得到一個(gè)分布式非阻塞的微服務(wù)系統(tǒng)。整體架構(gòu)和主流的微服務(wù)架構(gòu)很像,主要的區(qū)別就是 Flower 的服務(wù)是異步的,通過流程編排的方式進(jìn)行服務(wù)調(diào)用,而不是通過接口依賴的方式進(jìn)行調(diào)用。






