Seata通信模块分析
Server
整体概览
在seata项目中,client与server是利用netty来完成基于tcp的通信的。在server模块中,RemotingServer接口定义了Server的基本功能,AbstractNettyRemoting实现了远程消息的处理、同步/异步发送、任务超时管理等基本功能,AbstractNettyRemotingServer基于AbstractNettyRemoting近一步进行封装,定义了处理远程消息的ServerHandler,实现了RemotingServer中的业务接口。而NettyRemotingServer则是负责进行业务处理器的组装。
AbstractNettyRemoting
在AbstractNettyRemoting中,所有需要同步发送中的消息都被放在一个ConcurrentHashMap中,key是同步消息的id,value是自定义的MessageFuture。同步消息在发送时首先会初始化一个MessageFuture,通过MessageFuture中的CompletableFuture来实现异步结果的阻塞等待与异步结果设置的功能(同Netty中的ChannelPromise)。而AbstractNettyRemoting的构造函数中,会初始化一个定时任务,来定时的从这些Map中清理掉所有已经超时的同步消息请求。
protected Object sendSync(Channel channel, RpcMessage rpcMessage, long timeoutMillis) throws TimeoutException {
if (timeoutMillis <= 0) {
throw new FrameworkException("timeout should more than 0ms");
}
if (channel == null) {
LOGGER.warn("sendSync nothing, caused by null channel.");
return null;
}
MessageFuture messageFuture = new MessageFuture();
messageFuture.setRequestMessage(rpcMessage);
messageFuture.setTimeout(timeoutMillis);
futures.put(rpcMessage.getId(), messageFuture);
channelWritableCheck(channel, rpcMessage.getBody());
String remoteAddr = ChannelUtil.getAddressFromChannel(channel);
doBeforeRpcHooks(remoteAddr, rpcMessage);
channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> {
if (!future.isSuccess()) {
MessageFuture messageFuture1 = futures.remove(rpcMessage.getId());
if (messageFuture1 != null) {
messageFuture1.setResultMessage(future.cause());
}
destroyChannel(future.channel());
}
});
try {
Object result = messageFuture.get(timeoutMillis, TimeUnit.MILLISECONDS);
doAfterRpcHooks(remoteAddr, rpcMessage, result);
return result;
} catch (Exception exx) {
LOGGER.error("wait response error:{},ip:{},request:{}", exx.getMessage(), channel.remoteAddress(),
rpcMessage.getBody());
if (exx instanceof TimeoutException) {
throw (TimeoutException) exx;
} else {
throw new RuntimeException(exx);
}
}
}
在发送请求之前,首先会检查对应channel是否可写,然后调用自定义的hook,hook的加载利用的是EnhancedServiceLoader实现的SPI,最后会注册一个发送监听器,当发送失败时会关闭channel。值得一提的是在检查channel是否可写时用到了锁,当channel不可用时会调用wait释放锁,而当channel中的channelWritabilityChanged回调后再notify所有的写操作。从全局角度去看不同的channel用的是同一个锁,虽然牺牲了一定的并发效率,但是当channel不可写时通常意味着I/O此时遇到了瓶颈,如果不受控制的将数据写入到channel中,数据将会在channel的ChannelOutboundBuffer缓冲中排队,会造成数据积压的恶性循环引发OOM,用最简单明了的方法避免了这个问题。
private void channelWritableCheck(Channel channel, Object msg) {
int tryTimes = 0;
synchronized (lock) {
while (!channel.isWritable()) {
try {
tryTimes++;
if (tryTimes > NettyClientConfig.getMaxNotWriteableRetry()) {
destroyChannel(channel);
throw new FrameworkException("msg:" + ((msg == null) ? "null" : msg.toString()),
FrameworkErrorCode.ChannelIsNotWritable);
}
lock.wait(NOT_WRITEABLE_CHECK_MILLS);
} catch (InterruptedException exx) {
LOGGER.error(exx.getMessage());
}
}
}
}
异步消息发送与同步消息发送类似,省去了放入到futureMap的过程。再来看下处理消息的过程,实际上处理过程与Sentinel的处理过程也比较的类似,都是获取到消息体的类型,然后从处理器map中获取到对应的处理器,与Sentinel不一样的是,Sentinel的处理是在netty的I/O线程中直接处理请求的,seata server提供了在ExecutorService中异步执行代码的功能,主要是为了处理一些例如包含数据库的阻塞I/O操作避免阻塞通信的I/O线程。
protected void processMessage(ChannelHandlerContext ctx, RpcMessage rpcMessage) throws Exception {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("%s msgId:%s, body:%s", this, rpcMessage.getId(), rpcMessage.getBody()));
}
Object body = rpcMessage.getBody();
if (body instanceof MessageTypeAware) {
MessageTypeAware messageTypeAware = (MessageTypeAware) body;
final Pair<RemotingProcessor, ExecutorService> pair = this.processorTable.get((int) messageTypeAware.getTypeCode());
if (pair != null) {
if (pair.getSecond() != null) {
try {
pair.getSecond().execute(() -> {
try {
pair.getFirst().process(ctx, rpcMessage);
} catch (Throwable th) {
LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th);
} finally {
MDC.clear();
}
});
} catch (RejectedExecutionException e) {
LOGGER.error(FrameworkErrorCode.ThreadPoolFull.getErrCode(),
"thread pool is full, current max pool size is " + messageExecutor.getActiveCount());
if (allowDumpStack) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String pid = name.split("@")[0];
int idx = new Random().nextInt(100);
try {
Runtime.getRuntime().exec("jstack " + pid + " >d:/" + idx + ".log");
} catch (IOException exx) {
LOGGER<