Seata源码分析——RPC模块底层实现
前言
Seata是一个分布式事务解决方案框架,TC,TM,RM之间需要相互通讯来进行全局事务和分支事务的注册,回滚等等... Seata底层就是用了Netty,并定义了私有的RPC协议进行通讯的,本文就从RPC和Netty入手,来揭开Seata RPC模块神秘的面纱。
总览
Seata的RPC模块位于seata.core模块中:
在netty包中,封装了seata对netty使用,类图如下:
*这张图是按照 Seata RPC 模块的重构之路画的(作者是seata的贡献者)
从图中看到,服务器(TC),和客户端(TM,RM)的类还是比较对称的,Server端的比较简单,我们以client为例,看看它的发送一个RPC请求的流程:
AbstractNettyRemotingClient——一个RPC请求方法
这个类主要定义了客户端发送同步或异步rpc请求的方法:
我们来看下sendSyncRequest()方法:重点就是看看seata用netty发送了什么(RpcMessage),不需要太深入了解它的发送逻辑。
//省略了部分代码
@Override
public Object sendSyncRequest(Object msg) throws TimeoutException {
// 1.根据负载均衡算法获取服务器节点的ip和port地址。
String serverAddress = loadBalance(getTransactionServiceGroup(), msg);
//2.获取配置的rpc请求超时时间。
long timeoutMillis = this.getRpcRequestTimeout();
//3.创建rpc请求消息体
RpcMessage rpcMessage = buildRequestMessage(msg, ProtocolConstants.MSGTYPE_RESQUEST_SYNC);
//4.判断是否开启了批量请求,如果开启了则走批量请求逻辑
// send batch message
// put message into basketMap, @see MergedSendRunnable
if (this.isEnableClientBatchSendRequest()) {
// send batch message is sync request, needs to create messageFuture and put it in futures.
MessageFuture messageFuture = new MessageFuture();
messageFuture.setRequestMessage(rpcMessage);
messageFuture.setTimeout(timeoutMillis);
//5.把请求的MessageFuture对象放入future这个concurrenthashmap中,用于rpc返回结果后异步回调MessageFuture对象
futures.put(rpcMessage.getId(), messageFuture);
// put message into basketMap
// 把请求放入阻塞队列里,MergedSendRunnable线程会读取阻塞队列的请求并作合并发送。
BlockingQueue<RpcMessage> basket = CollectionUtils.computeIfAbsent(basketMap, serverAddress,
key -> new LinkedBlockingQueue<>());
if (!basket.offer(rpcMessage)) {
LOGGER.error("put message into basketMap offer failed, serverAddress:{},rpcMessage:{}",
serverAddress, rpcMessage);
return null;
}
if (!isSending) {
synchronized (mergeLock) {
mergeLock.notifyAll();
}
}
} else {
// 非批处理模式,调用netty的代码,这里的clientChannelManager应该是seata对channel做了池化
Channel channel = clientChannelManager.acquireChannel(serverAddress);
return super.sendSync(channel, rpcMessage, timeoutMillis);
}
}
RpcMessage——RPC协议
上面我们看到,Seata客户端发送的MessageFuture
里面包含了RpcMessage,它是封装用来RPC协议的。
先看RPC协议的设计:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
| magic |Proto| Full length | Head | Msg |Seria|Compr| RequestId |
| code |colVer| (head+body) | Length |Type |lizer|ess | |
+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+
| |
| Head Map [Optional] |
+-----------+-----------+-----------+-----------+-----------+-----------+-----------+-----------+
| |
| body |
| |
| ... ... |
+-----------------------------------------------------------------------------------------------+
Len | Param | Desc | Desc in chinese |
---|---|---|---|
2B | Magic Code | 0xdada | 魔术位 |
1B | ProtocolVersion | 1 | 协议版本:用于非兼容性升级 |
4B | FullLength | include front 3 bytes and self 4 bytes | 总长度 :用于拆包,包括前3位和自己4位 |
2B | HeadLength | include front 7 bytes, self 4 bytes, and head map | 头部长度:包括前面7位,自己4位,以及 HeadMap |
1B | Message type | request(oneway/twoway)/response/heartbeat/callback | 消息类型:请求(单向/双向)/响应/心跳/回调/go away等 |
1B | Serialization | custom, hessian, pb | 序列化类型:内置/hessian/protobuf等 |
1B | CompressType | None/gzip/snappy... | 压缩算法:无/gzip/snappy |
4B | MessageId | Integer | 消息 Id |
2B | TypeCode | code in AbstractMessage | 消息类型: AbstractMessage 里的类型 |
?B | HeadMap[Optional] | exists when if head length > 16 | 消息Map(可选的,如果头部长度大于16,代表存在HeadMap) |
ATTR_KEY(?B) key:string:length(2B)+data ATTR_TYPE(1B) 1:int; 2:string; 3:byte; 4:short ATTR_VAL(?B) int:(4B); string:length(2B)+data; byte:(1B); short:(2B) } | Key: 字符串 Value 类型 Value 值 | ||
?b | Body | (FullLength-HeadLength) |
可以看到Seata的RPC协议头如果不包含HeadMap(可拓展)的话总共是18B,总体来说还是比较小的。Seata rpc协议设计有一个亮点,那就是支持协议头拓展(HeadLength、HeadMap[Optional]),他这种设计既灵活性能又高(一些透传参数可以不用放在Body里面,这样就不需要对body进行编解码直接在协议头部获取)
编码&解码
Seata中有编码器ProtocolV1Encoder和解码器ProtocolV1Decoder,分别继承了MessageToByteEncoder
和LengthFieldBasedFrameDecoder
简单了解一下这两个netty提供的类:
-
MessageToByteEncoder:抽象类,继承了
ChannelOutboundHandlerAdapter
。用于将消息转换成字节,需要实现的只有一个方法
void encode(ChannelHandlerContext var1, I var2, ByteBuf var3) throws Exception;
它被调用时将会传入要被该类编码为 ByteBuf 的(类型为 I 的)出站消息。该 ByteBuf 随后将会被转发给 ChannelPipeline中的下一个 ChannelOutboundHandler -
LengthFieldBasedFrameDecoder:继承自
ByteToMessageDecoder
,因为Seata RPC是基于长度的协议,Netty提供了此类对这种协议进行解码,并且提供了几种构造函数来支持各种各样的头部配置情况。
public LengthFieldBasedFrameDecoder(
int maxFrameLength,
int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip)
//maxFrameLength: 最大帧长度,超过此长度的数据会被丢弃
//lengthFieldOffset:长度域偏移。数据开始的几个字节可能不是表示数据长度(这里指魔数和版本号),需要后移几个字节才是长度域。 根据上面的定义:这里是3B
//lengthFieldLength:长度域字节数。用几个字节来表示数据长度。 这里是4B
//lengthAdjustment:数据长度修正。因为长度域指定的长度可以使header+body的整个长度,也可以只是body的长度。如果表示header+body的整个长度,需要修正数据长度。
//initialBytesToStrip:跳过的字节数。如果你需要接收header+body的所有数据,此值就是0,如果你只想接收body数据,那么需要跳过header所占用的字节数。
接下来我们看解码器的解码方法,它的作用是根据自定义的RPC协议将ByteBuf字节转换成RpcMessage
ProtocolV1Decoder#decodeFrame
public Object decodeFrame(ByteBuf frame) {
//1.校验魔数
byte b0 = frame.readByte();
byte b1 = frame.readByte();
if (ProtocolConstants.MAGIC_CODE_BYTES[0] != b0
|| ProtocolConstants.MAGIC_CODE_BYTES[1] != b1) {
throw new IllegalArgumentException("Unknown magic code: " + b0 + ", " + b1);
}
byte version = frame.readByte(); //版本号
// TODO check version compatible here
int fullLength = frame.readInt();
short headLength = frame.readShort();
byte messageType = frame.readByte();
byte codecType = frame.readByte();
byte compressorType = frame.readByte();
int requestId = frame.readInt();
RpcMessage rpcMessage = new RpcMessage();
rpcMessage.setCodec(codecType); //消息类型
rpcMessage.setId(requestId); // 消息ID
rpcMessage.setCompressor(compressorType); // 压缩算法
rpcMessage.setMessageType(messageType); //报文类型
// direct read head with zero-copy 如果有拓展请求头
int headMapLength = headLength - ProtocolConstants.V1_HEAD_LENGTH;
if (headMapLength > 0) {
Map<String, String> map = HeadMapSerializer.getInstance().decode(frame, headMapLength);
rpcMessage.getHeadMap().putAll(map);
}
// read body
if (messageType == ProtocolConstants.MSGTYPE_HEARTBEAT_REQUEST) {
rpcMessage.setBody(HeartbeatMessage.PING);
} else if (messageType == ProtocolConstants.MSGTYPE_HEARTBEAT_RESPONSE) {
rpcMessage.setBody(HeartbeatMessage.PONG);
} else {
int bodyLength = fullLength - headLength;
if (bodyLength > 0) {
byte[] bs = new byte[bodyLength];
frame.readBytes(bs);
Compressor compressor = CompressorFactory.getCompressor(compressorType);
bs = compressor.decompress(bs); //解压缩
Serializer serializer = SerializerServiceLoader.load(SerializerType.getByCode(rpcMessage.getCodec()));
rpcMessage.setBody(serializer.deserialize(bs)); //反序列化
}
}
return rpcMessage;
}
编码部分和解码的差不多,就不分析了、
Server端Netty初始化
上面我们从客户端发送一次RPC请求讲起,简单分析了Seata RPC协议的定义以及编解码处理,现在我们来看Netty初始化的部分,也就是NettyRemotingServer的初始化:
NettyRemotingServer是TM服务端核心类之一,它用于启动Netty,监听服务器端口,接收TM,RM的请求,还为处理不同的请求创建不同的处理器。
//调用过程:
// ->nettyRemotingServer.init();
// ->AbstractNettyRemotingServer.init();
// ->serverBootstrap.start();
//NettyServerBootstrap#start()
@Override
public void start() {
this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupWorker)
.channel(NettyServerConfig.SERVER_CHANNEL_CLAZZ)
.option(ChannelOption.SO_BACKLOG, nettyServerConfig.getSoBackLogSize())
.option(ChannelOption.SO_REUSEADDR, true)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSendBufSize())
.childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketResvBufSize())
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK,
new WriteBufferWaterMark(nettyServerConfig.getWriteBufferLowWaterMark(),
nettyServerConfig.getWriteBufferHighWaterMark()))
.localAddress(new InetSocketAddress(getListenPort()))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new IdleStateHandler(nettyServerConfig.getChannelMaxReadIdleSeconds(), 0, 0))
.addLast(new ProtocolV1Decoder())
.addLast(new ProtocolV1Encoder());
if (channelHandlers != null) {
addChannelPipelineLast(ch, channelHandlers);
}
}
});
try {
this.serverBootstrap.bind(getListenPort()).sync();
XID.setPort(getListenPort());
LOGGER.info("Server started, service listen port: {}", getListenPort());
RegistryFactory.getInstance().register(new InetSocketAddress(XID.getIpAddress(), XID.getPort()));
initialized.set(true);
}
}
①、在初始化时配置了TCP KeepAlive和缓冲区的水位线,主要是配合当前缓冲区待发送内容用来判断channel是否可写相关。
②、从代码中可以看到,Seata使用4个Handler来处理rpc的心跳、编解码、业务请求处理:
- IdleStateHandler Netty内置的心跳检测handler
- ProtocolV1Decoder Seata解码器
- ProtocolV1Encoder Seata编码器
- ServerHandler 业务请求处理handler
两个编码器我们都分析过了,这里我们看业务处理的ServerHandler:
ServerHandler
它是定义在AbstractNettyRemotingServer里的内部类,有@Sharable注解,说明这是一个可以多次加到channelPipeline中而不用担心线程安全的问题.
- 继承了ChannelDuplexHandler,代表一个双向的ChannelHandler,它可以处理出站和入站的消息.
@ChannelHandler.Sharable
class ServerHandler extends ChannelDuplexHandler {
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
if (!(msg instanceof RpcMessage)) {
return;
}
processMessage(ctx, (RpcMessage) msg);
}
}
在#ChannelRead方法中,我们看到它调用了处理RpcMessage的一个核心方法:processMessage
到这里,我们就引出了Seata Netty模块第二条线:Processor。
我们知道Netty的handler中可以定义我们的业务逻辑,但是这样做会导致Netty Handler和处理逻辑耦合起来,对程序的易读性和健壮性都有影响。在Seata RPC 模块的重构之路中,作者(seata贡献者)对Seata RPC重构,并且引入了一系列Processor对不同的消息类型进行处理,从而达到解耦效果。
Processor消息处理器
首先我们知道,Seata中TM,TC,RM的请求大概围绕着全局事务和分支事务的注册,开启等等,可以将不同消息进行分类,作为一种消息的类型。整理如下:
*非原创,图源见文末
梳理清楚交互的逻辑后,我们可以根据消息的类型来定义Processor,每一个Processor可以处理一个或多个类型的消息.并将这些Processor统一放到一个表中,到时候根据消息类型查表得到对应的Processor就可以对消息进行处理了: 这个表就是ProcessorTable,后面会继续讲它
总览
Processer的定义在core.rpc.processor包中,分为client和server端.它们都实现了接口 RemotingProcessor
* 省略了客户端
RemotingProcessor
Processor的顶级接口,它只有一个方法:就是处理RpcMessage。具体实现都交给了各种类型的Processor。
public interface RemotingProcessor {
void process(ChannelHandlerContext ctx, RpcMessage rpcMessage) throws Exception;
}
processMessage
Seata的客户端发送消息时,会把消息类型添加到消息中,客户端收到消息后,就可以根据消息类型解析成对应的消息对象,消息对象最终被转发到AbstractNettyRemoting的processMessage方法,processMessage里面根据消息对象中的消息类型从processorTable中找到对应的处理器,之后处理器在将消息对象做进一步处理,processMessage方法的代码如下:
//AbstractNettyRemoting#processMessage
protected void processMessage(ChannelHandlerContext ctx, RpcMessage rpcMessage) throws Exception {
Object body = rpcMessage.getBody();
if (body instanceof MessageTypeAware) {
//Seata的消息类型都是MessageTypeAware
MessageTypeAware messageTypeAware = (MessageTypeAware) body;
//ProcessorTable(后面会继续分析它),根据消息类型从processorTable中找到对应的处理器和线程池
final Pair<RemotingProcessor, ExecutorService> pair = this.processorTable.get((int) messageTypeAware.getTypeCode());
if (pair != null) {
//根据线程池是否为null,处理消息分为同步和异步.
if (pair.getSecond() != null) {
try {
pair.getSecond().execute(() -> {
try {
//异步调用处理器的process方法
pair.getFirst().process(ctx, rpcMessage);
} catch (Throwable th) {
LOGGER.error(FrameworkErrorCode.NetDispatch.getErrCode(), th.getMessage(), th);
} finally {
MDC.clear();
}
});
} catch (RejectedExecutionException e) {
//...
}
} else {
try {
//同步调用处理器的process方法
pair.getFirst().process(ctx, rpcMessage);
}//..
}
}
这里留个坑,Processor的Process方法留到下一篇再分析。
Processor的注册
前面我们学习到了RPC消息的是根据不同消息类型分发给不同的Processor来处理的,并且用了ProcessorTable来维护维护消息类型和消息处理器的映射关系。本节就讲讲Processor注册到ProcessorTable的流程。
在NettyRemotingServer的init()方法中,就调用了registerProcessor()来注册处理器。
private void registerProcessor() {
// 1. registry on request message processor
ServerOnRequestProcessor onRequestProcessor =
new ServerOnRequestProcessor(this, getHandler());
ShutdownHook.getInstance().addDisposable(onRequestProcessor);
super.registerProcessor(MessageType.TYPE_BRANCH_REGISTER, onRequestProcessor, messageExecutor);
super.registerProcessor(MessageType.TYPE_BRANCH_STATUS_REPORT, onRequestProcessor, messageExecutor);
super.registerProcessor(MessageType.TYPE_GLOBAL_BEGIN, onRequestProcessor, messageExecutor);
super.registerProcessor(MessageType.TYPE_GLOBAL_COMMIT, onRequestProcessor, messageExecutor);
super.registerProcessor(MessageType.TYPE_GLOBAL_LOCK_QUERY, onRequestProcessor, messageExecutor);
super.registerProcessor(MessageType.TYPE_GLOBAL_REPORT, onRequestProcessor, messageExecutor);
super.registerProcessor(MessageType.TYPE_GLOBAL_ROLLBACK, onRequestProcessor, messageExecutor);
super.registerProcessor(MessageType.TYPE_GLOBAL_STATUS, onRequestProcessor, messageExecutor);
super.registerProcessor(MessageType.TYPE_SEATA_MERGE, onRequestProcessor, messageExecutor);
// 2. registry on response message processor
ServerOnResponseProcessor onResponseProcessor =
new ServerOnResponseProcessor(getHandler(), getFutures());
super.registerProcessor(MessageType.TYPE_BRANCH_COMMIT_RESULT, onResponseProcessor, branchResultMessageExecutor);
super.registerProcessor(MessageType.TYPE_BRANCH_ROLLBACK_RESULT, onResponseProcessor, branchResultMessageExecutor);
// 3. registry rm message processor
RegRmProcessor regRmProcessor = new RegRmProcessor(this);
super.registerProcessor(MessageType.TYPE_REG_RM, regRmProcessor, messageExecutor);
// 4. registry tm message processor
RegTmProcessor regTmProcessor = new RegTmProcessor(this);
super.registerProcessor(MessageType.TYPE_REG_CLT, regTmProcessor, null);
// 5. registry heartbeat message processor
ServerHeartbeatProcessor heartbeatMessageProcessor = new ServerHeartbeatProcessor(this);
super.registerProcessor(MessageType.TYPE_HEARTBEAT_MSG, heartbeatMessageProcessor, null);
}
这个方法比较好理解,就是注册了一系列处理器,并关联到对应的MessageType上,(映射关系在上文的表格中)都调用了父类的registerProcessor方法
@Override
public void registerProcessor(int messageType, RemotingProcessor processor, ExecutorService executor) {
Pair<RemotingProcessor, ExecutorService> pair = new Pair<>(processor, executor);
this.processorTable.put(messageType, pair);
}
public final class Pair<T1, T2> {
private final T1 first;
private final T2 second;
public Pair(T1 first, T2 second) {
this.first = first;
this.second = second;
}
public T1 getFirst() {
return first;
}
public T2 getSecond() {
return second;
}
}
可以看到有一个自定义类为Pair,和C++的Pair对象有点像,(这里我不太理解为啥不直接用HashMap)。并且用了线程池的技术。
总结
这篇算是入门篇,大致分析了整体的流程。后面会针对其中的细节分析有关网络和Netty核心的部分。可以先看推荐阅读了解一下
最后再放一张大佬画的以 TM 发起全局事务提交请求的流程图,看你能不能看懂了:
参考链接:
Seata解析-seata核心类NettyRemotingServer详解
Seata RPC 模块的重构之路
推荐阅读:
Seata通信模块分析