Seata源码分析——RPC模块底层实现

前言

Seata是一个分布式事务解决方案框架,TC,TM,RM之间需要相互通讯来进行全局事务和分支事务的注册,回滚等等... Seata底层就是用了Netty,并定义了私有的RPC协议进行通讯的,本文就从RPC和Netty入手,来揭开Seata RPC模块神秘的面纱。

总览

Seata的RPC模块位于seata.core模块中:
image

在netty包中,封装了seata对netty使用,类图如下:

image
*这张图是按照 Seata RPC 模块的重构之路画的(作者是seata的贡献者)

从图中看到,服务器(TC),和客户端(TM,RM)的类还是比较对称的,Server端的比较简单,我们以client为例,看看它的发送一个RPC请求的流程:

AbstractNettyRemotingClient——一个RPC请求方法

这个类主要定义了客户端发送同步或异步rpc请求的方法:
image

我们来看下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,分别继承了MessageToByteEncoderLengthFieldBasedFrameDecoder
简单了解一下这两个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的请求大概围绕着全局事务和分支事务的注册,开启等等,可以将不同消息进行分类,作为一种消息的类型。整理如下:

image
*非原创,图源见文末

梳理清楚交互的逻辑后,我们可以根据消息的类型来定义Processor,每一个Processor可以处理一个或多个类型的消息.并将这些Processor统一放到一个表中,到时候根据消息类型查表得到对应的Processor就可以对消息进行处理了: 这个表就是ProcessorTable,后面会继续讲它
image

总览

Processer的定义在core.rpc.processor包中,分为client和server端.它们都实现了接口 RemotingProcessor
image * 省略了客户端

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()来注册处理器。
image

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 发起全局事务提交请求的流程图,看你能不能看懂了:
image

参考链接:

seata源码解析:RPC模块详解

Seata解析-seata核心类NettyRemotingServer详解

Seata RPC 模块的重构之路
推荐阅读:
Seata通信模块分析

posted @ 2022-05-31 10:04  飞僧明天起飞  阅读(437)  评论(0编辑  收藏  举报