Apache RocketMQ作爲阿里開源的一款高性能、高吞吐量的分佈式消息中間件,它的通信能力已得到業界的公認。它的通信層是藉助於異常網絡框架Netty實現的。在開發網絡遊戲的時候,Netty也常用於遊戲服務器或網關的通信層框架,所以,可以通過學習RocketMQ是如何使用Netty框架,從中借鑑一此應用技巧。
在RocketMQ中的rocketmq-remoting項目就是針對網絡層封裝的項目,其實使用Netty的時候,最主要的就是以下幾個部分:
- 線程池的劃分
- 是否使用Epoll
- Netty服務啓動的相關配置
- Netty內存池的使用。
- 使用共享的Handler
- 服務優雅的關閉
線程池的劃分
RocketMQ的服務端Netty啓動類是NettyRemotingServer
,在這裏聲明瞭四個線程池
//這個其實就是netty中的work線程池,默認用來處理Handler方法的調用
private final EventLoopGroup eventLoopGroupSelector;
// Netty的Boss線程
private final EventLoopGroup eventLoopGroupBoss;
// 公共線程池,這裏用來處理RocketMQ的業務調用,這個有Netty沒有什麼關係
private final ExecutorService publicExecutor;
// 用來處理Handler的線程池
private DefaultEventExecutorGroup defaultEventExecutorGroup;
RocketMQ的Netty啓動代碼
下面是RocketMQ的Netty服務啓動代碼,如果我們自己想要創建一個Netty服務,直接抄下面的代碼,修改一下相關的配置和Handler就可以了:
ServerBootstrap childHandler =
this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
.channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_REUSEADDR, true)
.option(ChannelOption.SO_KEEPALIVE, false)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize())
.childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize())
.localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)
.addLast(defaultEventExecutorGroup,
encoder,
new NettyDecoder(),
new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
connectionManageHandler,
serverHandler
);
}
});
if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {
childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
}
try {
ChannelFuture sync = this.serverBootstrap.bind().sync();
InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress();
this.port = addr.getPort();
} catch (InterruptedException e1) {
throw new RuntimeException("this.serverBootstrap.bind().sync() InterruptedException", e1);
}
儘量使用Epoll
epoll是Linux下多路複用IO接口select/poll的增強版本,它能顯著減少程序在大量併發連接中只有少量活躍的情況下的系統CPU利用率。先不管epoll的原理是什麼,這都是底層的實現,只需要知道epoll的效率比其它的方式高就可以,在能使用epoll的情況下,就應該選擇使用Netty socket的Epoll模式。從RocketMQ的Netty啓動代碼來看,在選擇使用的ServerSocketChannel時,它使用了一個方法來判斷是否使用Epoll。
private boolean useEpoll() {
return RemotingUtil.isLinuxPlatform()
&& nettyServerConfig.isUseEpollNativeSelector()
&& Epoll.isAvailable();
}
nettyServerConfig.isUseEpollNativeSelector可以讓使用者在可以使用epoll的環境下,強制不使用epoll。
使用Netty的ByteBuf內存池
使用Netty的內存池可以減少對象的創建和內存分配,進而減少gc的量。在RocketMQ的服務中,默認是netty開啓使用netty的內存池的。
if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {
childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
}
使用共享的Handler
在netty中,使用Handler處理網絡的消息,不管是接收網絡消息,還是返回網絡消息,都會經過Handler的處理方法。在一個網絡連接創建channel時,channel初始化時就會添加相應的Handler。如果每個Handler內都沒有共用的對象,那麼這些Handler最好標記爲共享的,這樣可以減少Handler對象的創建。比如RocketMQ的編碼Handler
@ChannelHandler.Sharable
public class NettyEncoder extends MessageToByteEncoder<RemotingCommand> {
private static final InternalLogger log = InternalLoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING);
@Override
public void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, ByteBuf out)
throws Exception {
try {
ByteBuffer header = remotingCommand.encodeHeader();
out.writeBytes(header);
byte[] body = remotingCommand.getBody();
if (body != null) {
out.writeBytes(body);
}
} catch (Exception e) {
log.error("encode exception, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e);
if (remotingCommand != null) {
log.error(remotingCommand.toString());
}
RemotingUtil.closeChannel(ctx.channel());
}
}
}
在服務啓動時,會只在NettyRemotingServer創建一個NettyEncoder對象實例,所有的channel實例共同使用這個編碼實例。
服務優雅的關閉
所謂服務優雅的關閉,是指在服務需要關閉的時候,在關閉之前,需要把任務處理完,而且在收到關閉時,不再接收新的任務。在所有的Netty業務中,有業務相關的線程池就是NettyRemotingServer中創建的四個線程池,所以在關閉服務的時候,只需要關閉這幾個線程池即可。並等待線程池中的任務處理完。
在NettyRemotingServer中有一個shutdown()方法。
@Override
public void shutdown() {
try {
if (this.timer != null) {
this.timer.cancel();
}
this.eventLoopGroupBoss.shutdownGracefully();
this.eventLoopGroupSelector.shutdownGracefully();
if (this.nettyEventExecutor != null) {
this.nettyEventExecutor.shutdown();
}
if (this.defaultEventExecutorGroup != null) {
this.defaultEventExecutorGroup.shutdownGracefully();
}
} catch (Exception e) {
log.error("NettyRemotingServer shutdown exception, ", e);
}
if (this.publicExecutor != null) {
try {
this.publicExecutor.shutdown();
} catch (Exception e) {
log.error("NettyRemotingServer shutdown exception, ", e);
}
}
}
這裏面調用的shutdownGracefully()就是
@Override
public Future<?> shutdownGracefully() {
return shutdownGracefully(2, 15, TimeUnit.SECONDS);
}
它會等待執行完線程池中當前已所有任務,然後再關閉線程池。
那麼,何時調用的這個shutdown()方法呢。在RocketMQ服務啓動的時候,會添加一個回調鉤子,比如Namesrv服務在啓動的時候會執行下面的代碼:
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
@Override
public Void call() throws Exception {
controller.shutdown();
return null;
}
}));
這樣在服務器關閉的時候,就會觸發controller.shudown()。然後執行關閉線程池的操作。
注意,關閉服務器一般使用kill pid的命令,RocketMQ的發佈包裏面的bin下面,有一個mqshutdown的腳本,就是使用的kill pid 命令。
pid=`ps ax | grep -i 'org.apache.rocketmq.namesrv.NamesrvStartup' |grep java | grep -v grep | awk '{print $1}'`
if [ -z "$pid" ] ; then
echo "No mqnamesrv running."
exit -1;
fi
echo "The mqnamesrv(${pid}) is running..."
kill ${pid}
首先是使用腳本獲取進程名的pid,然後使用 kill pid將進程殺死。
但是不能使用kill -9 pid,因爲這個命令不會給進程執行回調的機會,就把進程殺死了。