netty性能調(diào)優(yōu)
點擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達(dá)
? 作者?|? tinysakurac
來源 |? urlify.cn/vueqqu
調(diào)優(yōu)思路
關(guān)于netty的學(xué)習(xí)和介紹,可以去github看官方文檔,這里良心推薦《netty實戰(zhàn)》和《netty權(quán)威指南》兩本書,前者對于新手更友好,原理和應(yīng)用都有講到,多讀讀會發(fā)現(xiàn)很多高性能的優(yōu)化點。
netty高性能優(yōu)化點
最近參加了阿里中間價性能比賽,為了提升netty寫的servive mesh的網(wǎng)絡(luò)通信的性能,最近幾天查了書、博客(這里強力推薦netty作者的博客,干貨真的很多),自己總結(jié)了如下一下優(yōu)化點。如果有錯誤希望能指正。
注:這里所討論的對應(yīng)的netty版本為netty4
首先要明確要netty優(yōu)化的幾個主要的關(guān)注點。
減少線程切換的開銷。
復(fù)用channel,可以選擇可共享的channel(@sharable,減少堆內(nèi)存開銷)
zero copy的應(yīng)用
減少并發(fā)下的競態(tài)情況
1 盡可能的復(fù)用EventLoopGroup。
這里就要涉及netty的線程模型了。netty實戰(zhàn)的第七章里有很細(xì)致的闡釋。簡單說EventLoopGroup包含了指定數(shù)量(如果沒有指定,默認(rèn)是cpu核數(shù)的兩倍,可以從源碼中看到)的EvenetLoop,Eve netLoop和channel的關(guān)系是一對多,一個channel被分配給一個EventLoop,它生命周期中都會使用這個EventLoop,而EventLoop背后就是線程。見下圖。
因此如果需要使用ThreadLocal保存上下文,那么許多channel就會共享同一個上下文。
因此不需要每次都new出一個EventLoopGroup,其本質(zhì)上是線程分配,可以復(fù)用同一個EventLoopGroup,減少資源的使用和線程的切換。特別是在服務(wù)端引導(dǎo)一個客戶端連接的時候。如下:
ServerBootstrap?bootstrap?=?new?ServerBootstrap();
bootstrap.group(new?NioEventLoopGroup(),?new?NioEventLoopGroup())
????????.channel(NioServerSocketChannel.class)
????????.childHandler(new?SimpleChannelInboundHandler()?{
????????????@Override
????????????protected?void?channelRead0(ChannelHandlerContext?ctx,?ByteBuf?byteBuf)?
????????????????throws?Exception?{
????????????????Bootstrap?bootstrap?=?new?Bootstrap();
????????????????bootstrap.channel(NioSocketChannel.class)
????????????????????.group(ctx.channel().eventLoop())
????????????????????.handler(new?SimpleChannelInboundHandler()?{
????????????????????????????@Override
????????????????????????????protected?void?channelRead0(ChannelHandlerContext?ctx,?ByteBuf?in)?
????????????????????????????????throws?Exception?{
????????????????????????????????System.out.println("Received?data");
????????????????????????????}
????????????????????????});
????????????????ChannelFuture?future?=??bootstrap.connect(new?InetSocketAddress(xxx,?80));
????????????????future.addListener(new?ChannelFutureListener()?{
????????????????????@Override
????????????????????public?void?operationComplete(ChannelFuture?future)?throws?Exception?{
????????????????????????//?do?something
????????????????????}
????????????????});
????????????}
????????});
bootstrap.bind(new?InetSocketAddress(8080)).sync();
2 使用EventLoop的任務(wù)調(diào)度
在EventLoop的支持線程外使用channel,用
channel.eventLoop().execute(new?Runnable()?{
???@Override
????public?void?run()?{
?????channel.writeAndFlush(data)
????}
});
而不是直接使用channel.writeAndFlush(data);
前者會直接放入channel所對應(yīng)的EventLoop的執(zhí)行隊列,而后者會導(dǎo)致線程的切換。
3 減少ChannelPipline的調(diào)用長度
public?class?YourHandler?extends?ChannelInboundHandlerAdapter?{
??@Override
??public?void?channelActive(ChannelHandlerContext?ctx)?{
????//?BAD?(most?of?the?times)
????ctx.channel().writeAndFlush(msg);?
????//?GOOD
????ctx.writeAndFlush(msg);?
???}
}
前者是將msg從整個ChannelPipline中走一遍,所有的handler都要經(jīng)過,而后者是從當(dāng)前handler一直到pipline的尾部,調(diào)用更短。
同樣,為了減少pipline的長度,如果一個handler只需要使用一次,那么可以在使用過之后,將其從pipline中remove。
4 減少ChannelHandler的創(chuàng)建
如果channelhandler是無狀態(tài)的(即不需要保存任何狀態(tài)參數(shù)),那么使用Sharable注解,并在bootstrap時只創(chuàng)建一個實例,減少GC。否則每次連接都會new出handler對象。
@ChannelHandler.Shareable?
public?class?StatelessHandler?extends?ChannelInboundHandlerAdapter?{
???@Override
???public?void?channelActive(ChannelHandlerContext?ctx)?{}
}
public?class?MyInitializer?extends?ChannelInitializer?{
???private?static?final?ChannelHandler?INSTANCE?=?new?StatelessHandler();
???@Override
???public?void?initChannel(Channel?ch)?{
?????ch.pipeline().addLast(INSTANCE);
???}
}
同時需要注意ByteToMessageDecoder之類的編解碼器是有狀態(tài)的,不能使用Sharable注解。
5 減少系統(tǒng)調(diào)用(Flush)的調(diào)用
flush操作是將消息發(fā)送出去,會引起系統(tǒng)調(diào)用,應(yīng)該盡量減少flush操作,減少系統(tǒng)調(diào)用的開銷。
同時也要減少write的操作, 因為這樣消息會流過整個ChannelPipline。
6 使用單鏈接
對于兩個指定的端點可以使用單一的channel,在第一次創(chuàng)建之后保存channel,然后下次對于同一個IP地址可以復(fù)用該channel而不需要重新建立。
你可能需要一個map來保存對于不同ip的channel,但是在初始化時這可能會有一些線程并發(fā)的問題。
7 利用netty零拷貝
在IO操作時使用池化的DirectBuffer, 在bootstrap配置參數(shù)的時候,使用.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)來指定一個池化的Allocator,并且使用ByteBuf buf = allocator.directBuffer();來獲取Bytebuf。
PooledByteBufAllocator,netty會幫你復(fù)用(無需release,除非你后面還需要用到同一個bytebuf)而不是每次都重新分配ByteBuf。在IO操作中,分配直接內(nèi)存而不是JVM的堆空間,就避免了在發(fā)送數(shù)據(jù)時,從JVM到直接內(nèi)存的拷貝過程,這也就是zero copy的含義。
8 一些配置參數(shù)的設(shè)置
ServerBootstrap啟動時,通常 bossGroup 只需要設(shè)置為 1 即可,因為 ServerSocketChannel 在初始化階段,只會注冊到某一個 eventLoop 上,而這個 eventLoop 只會有一個線程在運行,所以沒有必要設(shè)置為多線程。而 IO 線程,為了充分利用 CPU,同時考慮減少線上下文切換的開銷,通常設(shè)置為 CPU 核數(shù)的兩倍,這也是 Netty 提供的默認(rèn)值。
在對于響應(yīng)時間有高要求的場景,使用.childOption(ChannelOption.TCP_NODELAY, true)和.option(ChannelOption.TCP_NODELAY, true)來禁用nagle算法,不等待,立即發(fā)送。
9 小心的使用并發(fā)編程技巧
千萬不要阻塞EventLoop!包括了Thead.sleep() CountDownLatch 和一些耗時的操作等等,盡量使用netty中的各種future。如果必須盡量減少重量級的鎖的的使用。
在使用volatile時,
壞的:
private?volatile?Selector?selector;
public?void?method()?{
??selector.select();
??....
??selector.selectNow();
}
好的:先將volatile變量保存到方法棧中,jdk源碼中大量的使用了這種技巧。
private?volatile?Selector?selector;
public?void?method()?{
??Selector?selector?=?this.selector;
??selector.select();
??....
??selector.selectNow();
}
使用AtomicFieldUpdater替換Atomic。關(guān)于這個可以參考http://normanmaurer.me/blog/2013/10/28/Lesser-known-concurrent-classes-Part-1/。簡單說,如果使用Atomic*,對于每個連接都會創(chuàng)建一個對象,而如果使用Atomic*FieldUpdater則會省去這部分的開銷,只有一個static final變量。
private?static?final?AtomicLongFieldUpdater?ATOMIC_UPDATER?=
????????AtomicLongFieldUpdater.newUpdater(TheDeclaringClass.class,?"atomic");
private?volatile?long?atomic;
public?void?yourMethod()?{
????ATOMIC_UPDATER.compareAndSet(this,?0,?1);
}
10 響應(yīng)順序的處理
當(dāng)使用了單鏈接,就有一個必須要解決的問題,將請求和響應(yīng)順序?qū)?yīng)起來。因為所有的操作都是異步的,TCP是基于字節(jié)流的,所以channel接收到的數(shù)據(jù)無法保證和發(fā)送順序一致。這個的解決方案就是,對于每個請求指定一個id,對于響應(yīng)也攜帶該id。如果后發(fā)的請求的響應(yīng)先到,則將其緩存起來(可以使用一個并發(fā)的隊列),然后等待該id之前的所有響應(yīng)全部接收到,再按序返回。
很多rpc框架(如dubbo)都是有了這個技巧。
鋒哥最新SpringCloud分布式電商秒殺課程發(fā)布
??????
??長按上方微信二維碼?2 秒
感謝點贊支持下哈?
