Netty原理源碼總結(jié)01-異步,BIO,NIO
Netty是Java網(wǎng)絡(luò)一個(gè)大殺器,在很多行業(yè)都有廣泛的應(yīng)用,連Spring也在5版本提供了默認(rèn)基于Netty的webflux,總之深入學(xué)習(xí)一下Netty一定會受益良多
Netty,異步
說到Netty往往不能避開Java的io模型,一般的文章會先說到BIO,再說到NIO,但是這里我想要先說一下異步這個(gè)概念.
對于開發(fā)者來說,異步是一種程序設(shè)計(jì)的思想,使用異步模式設(shè)計(jì)的程序可以顯著減少線程等待,從而在高吞吐量的場景中,極大提升系統(tǒng)的整體性能,顯著降低時(shí)延。
為什么異步能極大提升系統(tǒng)整體性能呢?我用一個(gè)錢包系統(tǒng)來舉例.
同步
假定一個(gè)轉(zhuǎn)賬系統(tǒng),同步的核心代碼如下:
public void transfer(accountFrom, accountTo, amount){
add(accountFrom,-amount);//黑盒,耗時(shí)50ms
add(accountTo,amount);
}
上面的偽代碼從 accountFrom 的錢包扣除 amount,再把 amount 轉(zhuǎn)到 accountTo 的錢包里面去,這是同步的實(shí)現(xiàn)方式,但是性能如何呢?
假設(shè)微服務(wù) add 的平均響應(yīng)時(shí)間是50ms,那么整個(gè) transfer 的耗時(shí)就是100ms,也就是說一個(gè)線程一秒可以處理十筆轉(zhuǎn)賬請求,假設(shè)服務(wù)器線程最多是100,也就是說,整個(gè)服務(wù)器,每秒能處理1000個(gè)請求,超出的請求就只能進(jìn)入阻塞,或者等待延時(shí)了.
但是1000是不是系統(tǒng)極限呢,如果我們監(jiān)測一下服務(wù)器的各項(xiàng)指標(biāo),會發(fā)現(xiàn)服務(wù)器沒有一項(xiàng)是到了性能瓶頸的.這是因?yàn)? transfer 方法雖然耗時(shí)100ms,但是真正用在發(fā)送,接收和處理數(shù)據(jù)的時(shí)間都很短,絕大多數(shù)時(shí)間都用在等待add服務(wù)返回和網(wǎng)絡(luò)傳輸上了.
也就是說,采用同步實(shí)現(xiàn)的方式,整個(gè)服務(wù)器的所有線程大部分時(shí)間都沒有在工作,而是在等待
異步
如果是異步要怎么處理呢?核心代碼如下:
public void transfer(accountFrom, accountTo, amount){
CompletableFuture.runAsync(()->{
add(accountFrom,-amount);
return;
).whenComplete(()->add(accountTo,amount));
}
這里借助了Java提供的CompletableFuture來實(shí)現(xiàn)異步的調(diào)用,如果沒有用過沒有關(guān)系,關(guān)鍵是思想.這段代碼的流程是:執(zhí)行給accountFrom扣除金額的方法,在成功的時(shí)候,再執(zhí)行給accountTo添加金額的方法;
如果這個(gè)時(shí)候?qū)?strong>transfer方法進(jìn)行耗時(shí)統(tǒng)計(jì),會發(fā)現(xiàn)每次執(zhí)行時(shí)間只需要幾ms,系統(tǒng)每秒的處理數(shù)量也會遠(yuǎn)遠(yuǎn)的超過同步的方式,直到達(dá)到服務(wù)器的物理性能上限
個(gè)人理解,異步的本質(zhì)是提高cpu的利用率,同步時(shí)線程等待同樣會占用大量的cpu時(shí)間片,這樣的占用絕大多數(shù)是無意義的,而異步會減少線程等待占用的CPU時(shí)間片,從而提高了CPU時(shí)間片的利用率.
當(dāng)然這個(gè)例子里面沒有對add失敗進(jìn)行處理,真實(shí)開發(fā)的時(shí)候,異步會加大程序的復(fù)雜性,所以異步應(yīng)該應(yīng)用在性能敏感并且有io等耗時(shí)操作的地方.
BIO,NIO對比
明明是Netty,明明是講BIO,NIO,為什么我要先說異步呢,其實(shí)這個(gè)世界上新技術(shù)有很多,一味地追逐上層的技術(shù),人的精力有限,是做不到的.但是有很多東西其實(shí)是基石,講NIO之前我先講異步,等到我真的講NIO的時(shí)候,你會發(fā)現(xiàn)NIO超越BIO,理所應(yīng)當(dāng)啊,也不需要去背什么概念了,再到以后,你或許學(xué)到了消息隊(duì)列,Kafka,RocketMq,一個(gè)個(gè)產(chǎn)品花里胡哨,但是你會知道,他們最大的作用之一就是提供進(jìn)程間的異步,為什么要異步,跟你今天理解的異步,其實(shí)是沒有區(qū)別的.這個(gè)時(shí)候你會意識到,知識是成體系的,一個(gè)簡單的異步,就可以串聯(lián)起很多東西,這才是學(xué)習(xí)更重要的東西.
回到BIO和NIO,有一點(diǎn)需要認(rèn)識到,網(wǎng)絡(luò)并非時(shí)刻可讀可寫的,BIO就是不管不顧一直往Channel流讀寫數(shù)據(jù),即使無數(shù)據(jù)可讀,無數(shù)據(jù)緩沖可用的時(shí)候,也把持著線程資源. 我們用NIO就是在解決這個(gè)問題,NIO其實(shí)在讀寫操作的時(shí)候還是阻塞的,但是當(dāng)沒有數(shù)據(jù)可讀,沒有緩沖區(qū)可寫的時(shí)候就會讓渡出線程資源,等到有數(shù)據(jù)可讀可寫的時(shí)候在操作io.
舉個(gè)例子:
有一個(gè)養(yǎng)雞的農(nóng)場,里面養(yǎng)著來自各個(gè)農(nóng)戶(Thread)的雞(Socket),每家農(nóng)戶都在農(nóng)場中建立了自己的雞舍(SocketChannel)
- BIO:Block IO,每個(gè)農(nóng)戶盯著自己的雞舍,一旦有雞下蛋,就去做撿蛋處理;
- NIO:No-Block IO-單Selector,農(nóng)戶們花錢請了一個(gè)飼養(yǎng)員(Selector),并告訴飼養(yǎng)員(register)如果哪家的雞有任何情況(下蛋)均要向這家農(nóng)戶報(bào)告(select keys);
- NIO:No-Block IO-多Selector,當(dāng)農(nóng)場中的雞舍逐漸增多時(shí),一個(gè)飼養(yǎng)員巡視(輪詢)一次所需時(shí)間就會不斷地加長,這樣農(nóng)戶知道自己家的雞有下蛋的情況就會發(fā)生較大的延遲。怎么解決呢?沒錯(cuò),多請幾個(gè)飼養(yǎng)員(多Selector),每個(gè)飼養(yǎng)員分配管理雞舍,這樣就可以減輕一個(gè)飼養(yǎng)員的工作量,同時(shí)農(nóng)戶們可以更快的知曉自己家的雞是否下蛋了;
- Epoll模式:如果采用Epoll方式,農(nóng)場問題應(yīng)該如何改進(jìn)呢?其實(shí)就是飼養(yǎng)員不需要再巡視雞舍,而是聽到哪間雞舍的雞打鳴了(活躍連接),就知道哪家農(nóng)戶的雞下蛋了;
在連接數(shù)不多的時(shí)候,其實(shí)BIO的性能并不差,因?yàn)锽IO實(shí)現(xiàn)非常簡單,這是它的優(yōu)點(diǎn),所以我們也沒有必要去使用NIO.但是連接數(shù)多的時(shí)候,BIO就會存在很大的性能問題,也就是NIO發(fā)揮作用的時(shí)候了.
NIO和Reactor
上面的例子很生動(dòng)的說明了BIO和NIO的特性,NIO思路就是,創(chuàng)建一個(gè)Selector,Channel告訴Selector自己關(guān)心的事件(SelectKey),Selector自己循環(huán)判斷哪個(gè)Channel關(guān)心的事件可以觸發(fā)了,當(dāng)有事件的時(shí)候就通知Channel,Channel進(jìn)行讀寫創(chuàng)建連接等操作.這就是Reactor模式,同時(shí)一個(gè)Selector可以注冊很多Channel,這就是多路復(fù)用機(jī)制.
在上面的情況下,只有一個(gè)Selector,也就是在農(nóng)場例子中的單Selector的情況,也稱為Reactor單線程模式,但是在并發(fā)量很大的時(shí)候,單線程未必夠用,所以我們還可以創(chuàng)建多個(gè)Selectot,每個(gè)Selector負(fù)責(zé)一部分channel,這就是Reactor多線程模式
走到這一步,Reactor模式還可不可以再優(yōu)化呢?答案是可以的,對于服務(wù)器來說,接收連接是非常重要的事情,如果超長的讀寫操作影響了連接創(chuàng)建,這是不太能接收的,所以最主流的處理模式是Reactor主從模式,boss線程負(fù)責(zé)連接的創(chuàng)建,然后將創(chuàng)建好的連接交給子線程組,子線程組選取一個(gè)線程處理這個(gè)連接讀寫事件通知.
在Netty中,三種Reactor模式都是支持,但是除非選了一個(gè)垃圾的單核cpu的服務(wù)器,選主從模式就可以了,三種實(shí)現(xiàn)方式在下面:
//Reactor 單線程模式
EventLoopGroup eventGroup = new NioEventLoopGroup(1);
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);
//非主從 Reactor 多線程模式
EventLoopGroup eventGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(eventGroup);
//主從 Reactor 多線程模式
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);
最后再補(bǔ)充一下,Netty的boosGroup大多時(shí)候并不能用到一個(gè)線程組,只會用到線程組中的一個(gè),在Netty服務(wù)器啟動(dòng)的時(shí)候,會綁定地址和端口,一般來說我們服務(wù)器只會綁定一個(gè)地址和端口,所以實(shí)際上也只用到了bossGroup中的一個(gè)線程.如果我在文章中描述有什么不對的地方,歡迎指正.