netty高效能調優點
netty
關於netty的學習和介紹,可以去github看官方文件,這裡良心推薦《netty實戰》和《netty權威指南》兩本書,前者對於新手更友好,原理和應用都有講到,多讀讀會發現很多高效能的優化點。
netty高效能優化點
最近參加了阿里中間價效能比賽,為了提升netty寫的servive mesh的網路通訊的效能,最近幾天查了書、部落格(這裡強力推薦netty作者的部落格,乾貨真的很多),自己總結了如下一下優化點。如果有錯誤希望能指正。
注:這裡所討論的對應的netty版本為netty4
首先要明確要netty優化的幾個主要的關注點。
- 減少執行緒切換的開銷。
- 複用channel,可以選擇池化channel
- zero copy的應用
- 減少併發下的競態情況
接下來將細數一下總結的優化點
1. 儘可能的複用EventLoopGroup
。
這裡就要涉及netty的執行緒模型了。netty實戰的第七章裡有很細緻的闡釋。簡單說EventLoopGroup
包含了指定數量(如果沒有指定,預設是cpu核數的兩倍,可以從原始碼中看到)的EvenetLoop
,Eve netLoop
和channel
的關係是一對多,一個channel
被分配給一個EventLoop
,它生命週期中都會使用這個EventLoop
,而EventLoop
背後就是執行緒。見下圖。
因此如果需要使用
ThreadLocal
儲存上下文,那麼許多channel就會共享同一個上下文。
因此不需要每次都new出一個EventLoopGroup
,其本質上是執行緒分配,可以複用同一個EventLoopGroup
,減少資源的使用和執行緒的切換。特別是在服務端引導一個客戶端連線的時候。如下:
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(new NioEventLoopGroup(), new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new SimpleChannelInboundHandler<ByteBuf>() {
@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<ByteBuf>() {
@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
的任務排程
在EventLoop的支援執行緒外使用channel,用
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
channel.writeAndFlush(data)
}
});
而不是直接使用channel.writeAndFlush(data)
;
前者會直接放入channel所對應的EventLoop的執行佇列,而後者會導致執行緒的切換。
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都要經過,而後者是從當前handler一直到pipline的尾部,呼叫更短。
同樣,為了減少pipline的長度,如果一個handler只需要使用一次,那麼可以在使用過之後,將其從pipline中remove。
4. 減少ChannelHandler的建立
如果channelhandler是無狀態的(即不需要儲存任何狀態引數),那麼使用Sharable
註解,並在bootstrap時只建立一個例項,減少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
之類的編解碼器是有狀態的,不能使用Sharable
註解。
5. 減少系統呼叫(Flush)的呼叫
flush操作是將訊息傳送出去,會引起系統呼叫,應該儘量減少flush操作,減少系統呼叫的開銷。
同時也要減少write的操作, 因為這樣訊息會流過整個ChannelPipline。
6. 使用單鏈接
對於兩個指定的端點可以使用單一的channel,在第一次建立之後儲存channel,然後下次對於同一個IP地址可以複用該channel
而不需要重新建立。
你可能需要一個map來儲存對於不同ip的channel,但是在初始化時這可能會有一些執行緒併發的問題。在這篇微信推文(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配置引數的時候,使用.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
來指定一個池化的Allocator,並且使用ByteBuf buf = allocator.directBuffer();
來獲取Bytebuf。
PooledByteBufAllocator,netty會幫你複用(無需release,除非你後面還需要用到同一個bytebuf)而不是每次都重新分配ByteBuf。在IO操作中,分配直接記憶體而不是JVM的堆空間,就避免了在傳送資料時,從JVM到直接記憶體的拷貝過程,這也就是zero copy的含義。
8. 一些配置引數的設定
ServerBootstrap啟動時,通常 bossGroup
只需要設定為 1
即可,因為 ServerSocketChannel
在初始化階段,只會註冊到某一個 eventLoop
上,而這個 eventLoop
只會有一個執行緒在執行,所以沒有必要設定為多執行緒。而 IO 執行緒,為了充分利用 CPU,同時考慮減少線上下文切換的開銷,通常設定為 CPU 核數的兩倍,這也是 Netty 提供的預設值。
在對於響應時間有高要求的場景,使用.childOption(ChannelOption.TCP_NODELAY, true)
和.option(ChannelOption.TCP_NODELAY, true)
來禁用nagle演算法,不等待,立即傳送。
9. 小心的使用併發程式設計技巧
千萬不要阻塞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*
,對於每個連線都會建立一個物件,而如果使用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是基於位元組流的,所以channel接收到的資料無法保證和傳送順序一致。這個的解決方案就是,對於每個請求指定一個id,對於響應也攜帶該id。如果後發的請求的響應先到,則將其快取起來(可以使用一個併發的佇列),然後等待該id之前的所有響應全部接收到,再按序返回。