1. 程式人生 > >netty高效能調優點

netty高效能調優點

netty

關於netty的學習和介紹,可以去github看官方文件,這裡良心推薦《netty實戰》和《netty權威指南》兩本書,前者對於新手更友好,原理和應用都有講到,多讀讀會發現很多高效能的優化點。

netty高效能優化點

最近參加了阿里中間價效能比賽,為了提升netty寫的servive mesh的網路通訊的效能,最近幾天查了書、部落格(這裡強力推薦netty作者的部落格,乾貨真的很多),自己總結了如下一下優化點。如果有錯誤希望能指正。

注:這裡所討論的對應的netty版本為netty4

首先要明確要netty優化的幾個主要的關注點。

  1. 減少執行緒切換的開銷。
  2. 複用channel,可以選擇池化channel
  3. zero copy的應用
  4. 減少併發下的競態情況

接下來將細數一下總結的優化點

1. 儘可能的複用EventLoopGroup

這裡就要涉及netty的執行緒模型了。netty實戰的第七章裡有很細緻的闡釋。簡單說EventLoopGroup包含了指定數量(如果沒有指定,預設是cpu核數的兩倍,可以從原始碼中看到)的EvenetLoopEve netLoopchannel的關係是一對多,一個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();
}
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之前的所有響應全部接收到,再按序返回。