
內(nèi)容來源:2017年5月13日,餓了么資深Java工程師朱杰在“Java開發(fā)者大會(huì) | Java之美【上海站】”進(jìn)行《老樹新花-Java異步服務(wù)開發(fā)》演講分享。IT大咖說作為獨(dú)家視頻合作方,經(jīng)主辦方和講者審閱授權(quán)發(fā)布。
閱讀字?jǐn)?shù):1901 用時(shí): 13分鐘

摘要
餓了么資深Java工程師朱杰從同步異步概念介紹、使用Java來開發(fā)異步化服務(wù)、回調(diào)監(jiān)聽模式所遇到的問題和解決這三方面來我們?nèi)娼庾xJava異步服務(wù)開發(fā)。
嘉賓演講視頻回顧及PPT:http://suo.im/4rwo2Y
同步模型
以前在并發(fā)量很低的情況下,是通過線程去收取數(shù)據(jù)并發(fā)送數(shù)據(jù)給客戶端。但是當(dāng)并發(fā)量和客戶端連接數(shù)比較高的時(shí)候,服務(wù)器會(huì)出現(xiàn)明顯的瓶頸。

阻塞模型比較符合人的思考邏輯,但它會(huì)有線程阻塞的問題。阻塞模型會(huì)讓出CPU,不適用于高并發(fā)。
線程池或連接池只能解決一部分問題。因?yàn)榫€程池和連接池的本質(zhì)作用并不是能直接提高QPS,而是減少或銷毀線程的連接處以及開銷。
我們平時(shí)調(diào)用阻塞API的一個(gè)問題就在于單機(jī)的線程數(shù)是有限的。所以如果要提高服務(wù)端性能的話,首先就要去阻塞。
異步模型
異步阻塞模型處理I/O時(shí)大部分時(shí)間是非阻塞的(監(jiān)聽時(shí)除外),它調(diào)用的API會(huì)立即返回,這點(diǎn)是需要注意的。此外,處理結(jié)果的程序并不保證調(diào)用API當(dāng)前的線程,這點(diǎn)在處理線程安全的問題上尤其要注意。
Java中很多API都是基于操作系統(tǒng)底層API的模型,并沒有做更高層次的封裝。
Java把一層阻塞異部I/O做了封裝,這些就是Java或C語言異步模型的基石。
少數(shù)線程等待事件發(fā)生,再根據(jù)對(duì)應(yīng)類型處理相關(guān)事件。

最近“協(xié)程”這個(gè)詞比較火,看上去能解決異步模型的大部分問題。它是一個(gè)輕量級(jí)線程,可以直接當(dāng)作線程來用。還能阻塞I/O API,阻塞的是協(xié)程而非線程。
協(xié)程是用戶態(tài)資源,用戶態(tài)調(diào)度,消耗極低,可以啟動(dòng)數(shù)十萬個(gè)協(xié)程。
它的實(shí)現(xiàn)和線程不是1對(duì)1 的關(guān)系,難點(diǎn)在于編程語言的內(nèi)部實(shí)現(xiàn)。
Python雖然支持協(xié)程,但是由于全局解釋鎖的關(guān)系,同一時(shí)刻只有單個(gè)CPU在運(yùn)行。所以python選擇多進(jìn)程+協(xié)程的做法。
Go語言完美解決了支持不阻塞線程的I/O操作,并支持多線程。
要能像同步I/O一樣編寫代碼,不會(huì)創(chuàng)造過多數(shù)量的線程。盡量讓CPU處于忙碌狀態(tài)而非等待,并尋找滿足以上條件的Java庫。但是Java由于本身語言的問題,即使是Java協(xié)程三方庫也只能部分支持協(xié)程。
退而求其次,我們只能使用Java異步工具庫。如果要提高并發(fā)量,可以使用異步JDBC和異步HTTP CLIENT,這個(gè)庫基于NETTY。
做到服務(wù)異步化,要查看接口是否可支持異步。還可以使用Java的異步工具庫,比如Java的異步數(shù)據(jù)訪問方式和異步HTTP CLIENT。如果使用的是三方框架,可以修改調(diào)用方式,有的框架支持異步回調(diào)和事件監(jiān)聽。最重要的是要注意線程安全問題。
異步化的優(yōu)勢(shì)就是極大提高了I/O密集型業(yè)務(wù)的性能,保守估計(jì)有10~100倍,也就解決了線程數(shù)創(chuàng)建過多的問題。
而它的缺點(diǎn)是增加了編程難度,包括狀態(tài)保存、回調(diào)處理以及線程安全等。也存在壓垮下游服務(wù)的問題:)
老樹新花-基于Netty的Java模型
Netty是基于原生的異步模型,封裝并優(yōu)化。它修復(fù)JDK中的一些BUG,提供了多種輔助類方便開發(fā)。編程模型高效簡(jiǎn)單,開發(fā)者只需關(guān)心具體實(shí)現(xiàn)邏輯即可,基本不用花精力做Java網(wǎng)絡(luò)層面的優(yōu)化。此外,Netty成熟穩(wěn)定,業(yè)界使用多,我們能夠相信,使用它不會(huì)遇到難以解決的大問題。
Netty基于事件連接,如果有數(shù)據(jù)傳入以及連接上有異常事件或自定義事件,只需復(fù)寫它的回調(diào)函數(shù)就能做相應(yīng)的處理。
Netty所有I/O API全都是消除阻塞異步化。線程模式也非常好,它單個(gè)連接上的所有I/O事件都由同一個(gè)線程執(zhí)行,避免了線程安全問題。

Netty的單個(gè)鏈接綁定一個(gè)線程,EVENTLOOP即一個(gè)線程,EVENTLOOPGROUP是一個(gè)線程組。
Netty任何的I/O API都是產(chǎn)生一個(gè)任務(wù),放入該連接對(duì)應(yīng)的線程里執(zhí)行,做到局部串行化。
Netty一切操作都是以事件驅(qū)動(dòng)來執(zhí)行,所有I/O API都是用異步+回調(diào)監(jiān)聽的方式來處理消息。單一的連接處理都在一個(gè)線程里,來避免線程安全問題。
案例-餓了么數(shù)據(jù)庫中間件
我們是一個(gè)實(shí)現(xiàn)了MYSQL協(xié)議的中間代理服務(wù)。上游至少要支持上萬客戶端的連接,下游要支持上千數(shù)據(jù)庫連接。
為了快速實(shí)現(xiàn)功能,我們最早是找了一個(gè)基于GITHUB阻塞I/O開源庫,首要任務(wù)是把同步改為了異步。
線程模型的上下游各有Netty的一個(gè)線程組,中間件內(nèi)部還有一個(gè)處理業(yè)務(wù)的線程組。

但有一個(gè)最本質(zhì)的問題在于這三個(gè)線程組之間的線程安全得不到保證。
因?yàn)檫@個(gè)中間件前后端都是異步的,所以按正常流程是由協(xié)議來保證順序執(zhí)行,而異常中斷是并發(fā)執(zhí)行的。
參考Netty,我們把每一個(gè)連接綁定到一個(gè)單線程池,保證Task串行執(zhí)行。

前后端異步的好處在于模型簡(jiǎn)單,便于后續(xù)修改。
我個(gè)人認(rèn)為,在程序里過多的使用WAIT/NOTIFY/LOCK不一定代表良好的多線程編程能力,卻可能代表這是不夠優(yōu)雅的設(shè)計(jì)。

除了前后端異步和信號(hào)量控制異步,在中間件中我們還用到了日志異步、心跳異步、JOB異步和配置變更異步。
在餓了么數(shù)據(jù)庫中間件開發(fā)過程中,異步化所有API以去除阻塞,局部串行化解決線程安全問題,模型簡(jiǎn)單,易于修改和理解。
今天的分享就到這里,謝謝大家!