關于netty的學習和介紹处铛,可以去github看官方文檔晾嘶,這里良心推薦《netty實戰(zhàn)》和《netty權威指南》兩本書,前者對于新手更友好距糖,原理和應用都有講到涡真,多讀讀會發(fā)現(xiàn)很多高性能的優(yōu)化點。這里強力推薦netty作者的博客肾筐,干貨真的很多哆料。
1.盡可能的復用EventLoopGroup
這里就要涉及netty的線程模型了。netty實戰(zhàn)的第七章里有很細致的闡釋吗铐。簡單說EventLoopGroup包含了指定數(shù)量(如果沒有指定东亦,默認是cpu核數(shù)的兩倍,可以從源碼中看到)的EvenetLoop唬渗,Eve netLoop和channel的關系是一對多典阵,一個channel被分配給一個EventLoop,它生命周期中都會使用這個EventLoop镊逝,而EventLoop背后就是線程壮啊。見下圖。
因此不需要每次都new出一個EventLoopGroup撑蒜,其本質上是線程分配歹啼,可以復用同一個EventLoopGroup,減少資源的使用和線程的切換座菠。
2. 使用EventLoop的任務調度
在EventLoop的支持線程外使用channel狸眼,用
channel.eventLoop().execute(new Runnable() {
?? @Override
??? public void run() {
??????? channel.writeAndFlush(data)
??? }
});
而不是直接使用channel.writeAndFlush(data);
前者會直接放入channel所對應的EventLoop的執(zhí)行隊列浴滴,而后者會導致線程的切換拓萌。
3. 減少ChannelPipline的調用長度
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)過升略,而后者是從當前handler一直到pipline的尾部微王,調用更短屡限。
同樣,為了減少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<Channel> {
??? private static final ChannelHandler INSTANCE = new StatelessHandler();
??? @Override
??? public void initChannel(Channel ch) {
??????? ch.pipeline().addLast(INSTANCE);
??? }
}
同時需要注意ByteToMessageDecoder之類的編解碼器是有狀態(tài)的东帅,不能使用Sharable注解。
5. 減少系統(tǒng)調用(Flush)的調用
flush操作是將消息發(fā)送出去球拦,會引起系統(tǒng)調用靠闭,應該盡量減少flush操作,減少系統(tǒng)調用的開銷坎炼。
同時也要減少write的操作愧膀, 因為這樣消息會流過整個ChannelPipline。
6. 使用單鏈接
對于兩個指定的端點可以使用單一的channel谣光,在第一次創(chuàng)建之后保存channel檩淋,然后下次對于同一個IP地址可以復用該channel而不需要重新建立。
你可能需要一個map來保存對于不同ip的channel萄金,但是在初始化時這可能會有一些線程并發(fā)的問題。在這篇微信推文(https://mp.weixin.qq.com/s/JRsbK1Un2av9GKmJ8DK7IQ)中有提到對于這個的解決方案,在螞蟻金服的sofa-bolt項目中有類似情形案腺,不過不太理解另凌。
initialTask = this.connTasks.get(poolKey);
if (null == initialTask) {
??? initialTask = new RunStateRecordedFutureTask<ConnectionPool>(callable);
??? initialTask = this.connTasks.putIfAbsent(poolKey, initialTask);
??? if (null == initialTask) {
??????? initialTask = this.connTasks.get(poolKey);
??????? initialTask.run();
??? }
}
7. 利用netty零拷貝,在IO操作時使用池化的DirectBuffer
在bootstrap配置參數(shù)的時候孙乖,使用.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)來指定一個池化的Allocator浙炼,并且使用ByteBuf buf = allocator.directBuffer();來獲取Bytebuf。
PooledByteBufAllocator唯袄,netty會幫你復用(無需release鼓拧,除非你后面還需要用到同一個bytebuf)而不是每次都重新分配ByteBuf。在IO操作中越妈,分配直接內存而不是JVM的堆空間季俩,就避免了在發(fā)送數(shù)據(jù)時,從JVM到直接內存的拷貝過程梅掠,這也就是zero copy的含義酌住。
8. 一些配置參數(shù)的設置
ServerBootstrap啟動時店归,通常bossGroup只需要設置為1即可,因為ServerSocketChannel在初始化階段酪我,只會注冊到某一個eventLoop上消痛,而這個eventLoop只會有一個線程在運行,所以沒有必要設置為多線程都哭。而 IO 線程秩伞,為了充分利用 CPU,同時考慮減少線上下文切換的開銷欺矫,通常設置為 CPU 核數(shù)的兩倍纱新,這也是 Netty 提供的默認值。
在對于響應時間有高要求的場景穆趴,使用.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();
}
使用Atomic*FieldUpdater替換Atomic*。關于這個可以參考http://normanmaurer.me/blog/2013/10/28/Lesser-known-concurrent-classes-Part-1/迂烁。簡單說看尼,如果使用Atomic*,對于每個連接都會創(chuàng)建一個對象盟步,而如果使用Atomic*FieldUpdater則會省去這部分的開銷藏斩,只有一個static final變量。
private static final AtomicLongFieldUpdater<TheDeclaringClass> ATOMIC_UPDATER =
??????? AtomicLongFieldUpdater.newUpdater(TheDeclaringClass.class, "atomic");
private volatile long atomic;
public void yourMethod() {
??? ATOMIC_UPDATER.compareAndSet(this, 0, 1);
}
10. 響應順序的處理
當使用了單鏈接却盘,就有一個必須要解決的問題狰域,將請求和響應順序對應起來。因為所有的操作都是異步的黄橘,TCP是基于字節(jié)流的兆览,所以channel接收到的數(shù)據(jù)無法保證和發(fā)送順序一致。這個的解決方案就是塞关,對于每個請求指定一個id抬探,對于響應也攜帶該id。如果后發(fā)的請求的響應先到,則將其緩存起來(可以使用一個并發(fā)的隊列)小压,然后等待該id之前的所有響應全部接收到线梗,再按序返回。