Seata Server的启动和注册RM、TM的过程

元始一副智珠在握的表情,就差吟诵一首诗:手握日月摘星辰,世间无我这般人。我欲乘风归去,又恐琼楼玉宇啊。咦,我怎么在半空中呢?
傅青阳摆摆手,元始的脸部就与地面亲密接触了。心想:最讨厌别人装了,说道:继续啊,前面提到的RM作为Netty Client连接到Seata Server,那么这里的Seata Server是如何启动的呢?
元始揉一揉面部,恢复一下严谨的形象:这就要看刚刚的seata-server.sh启动脚本了。
我们阅读源码找入口类的方法,一般有2种:一种就是前面提到的,找出跟框架相关的代码配置;一种就是从启动脚本里面找到main方法的入口类。
从下面这段脚本可以看到入口类 io.seata.server.Server

exec "$JAVACMD" $JAVA_OPTS -server -Xmx2048m -Xms2048m -Xmn1024m -Xss512k //...
  io.seata.server.Server \
  "$@"

从上面的脚本就能直接看到入口类io.seata.server.Server,我们跟进去看代码。

public static void main(String[] args) throws IOException {
    //......
    DefaultCoordinator coordinator = new DefaultCoordinator(nettyRemotingServer);
    coordinator.init();
    nettyRemotingServer.setHandler(coordinator);
    //......
    try {
        // netty server初始化
        nettyRemotingServer.init();
    } //......
}

Seata Server的启动

这里看到封装了一个NettyRemotingServer,很明显就是一个Netty Server的封装,我们继续跟进init方法。

@Override
public void init() {
    // registry processor
    // 定义各种消息类型对应的处理器
    registerProcessor();
    if (initialized.compareAndSet(false, true)) {
        // 继续调用父类初始化方法
        super.init();
    }
}
public void init() {
    super.init();
    // 初始化netty server
    serverBootstrap.start();
}
@Override
public void start() {
    this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupWorker)
        .channel(NettyServerConfig.SERVER_CHANNEL_CLAZZ)
            // TCP 3次握手的队列缓冲区大小
        .option(ChannelOption.SO_BACKLOG, nettyServerConfig.getSoBackLogSize())
            //一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用。
        .option(ChannelOption.SO_REUSEADDR, true)
            // 连接保活
        .childOption(ChannelOption.SO_KEEPALIVE, true)
            // 不启用开启Nagle算法,Nagle算法会收集网络小包再一次性发送,不启用则是即时发送
        .childOption(ChannelOption.TCP_NODELAY, true)
            // 发送数据缓冲区大小,默认150KB
        .childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSendBufSize())
            // 接收数据缓冲区大小,默认150KB
        .childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketResvBufSize())
            // 控制网络水位,将网络传输速度维持在比较平稳的状态
        .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK,
            new WriteBufferWaterMark(nettyServerConfig.getWriteBufferLowWaterMark(),
                nettyServerConfig.getWriteBufferHighWaterMark()))
        .localAddress(new InetSocketAddress(listenPort))
        .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 {
        ChannelFuture future = this.serverBootstrap.bind(listenPort).sync();
        LOGGER.info("Server started, listen port: {}", listenPort);
        RegistryFactory.getInstance().register(new InetSocketAddress(XID.getIpAddress(), XID.getPort()));
        initialized.set(true);
        future.channel().closeFuture().sync();
    } //......
}

看到这里先打住,我们先来分析一下netty的reactor线程模型,也就是上面代码应用到的netty线程池模型:

this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupWorker)

可以看到这里设置了2个线程池,eventLoopGroupBoss线程池主要用于处理客户端的accept连接、断开连接等,eventLoopGroupWorker线程池处理数据的传输,包括read、write事件和pipeline链条中的handler。

简单来说,reactor线程模型就是将线程池的职责分开,处理连接的属于低频操作且阻塞时间相对较长(因为需要进行TCP3次握手),处理数据的IO传输属于高频且和业务相关性紧密,使用互不干扰的线程池可以提高效率。画图如下:

reactor线程模型

接下来我们看看Netty Server的参数优化设置:

  1. SO_BACKLOG:3次握手的队列缓冲区大小,防止一下子涌入太多建立连接的请求,默认值太小导致无法连接。
  2. SO_REUSEADDR:一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用,防止短暂重启无法监听端口。
  3. SO_KEEPALIVE:默认每隔1小时通过TCP探测包维持连接。
  4. TCP_NODELAY:Nagle算法会收集网络小包再一次性发送,不启用则是即时发送,防止小的请求一直被堆积。
  5. SO_SNDBUF:发送数据缓冲区大小,默认150KB,提高吞吐量。
  6. SO_RCVBUF:接收数据缓冲区大小,默认150KB,提高吞吐量。
  7. WRITE_BUFFER_WATER_MARK:控制网络水位,将网络传输速度维持在比较平稳的状态,防止出现瞬时读写流量过大打满网卡,导致其他应用无法进行网络通信。
  8. IdleStateHandler:使用心跳机制,保持会话状态。

到了这里我们就明白了,Seata Server的启动其实就是先注册了各种消息的处理器,然后启动一个Netty Server端,等待业务服务的RM和TM连接并发送请求过来。

RM注册过程

傅青阳:既然有了Netty Server端,那就顺便看看Netty Client端的初始化吧。
元始:好的,上一篇我们已经看到了RM是如何初始化的,其实也是初始化了一个Netty Client,这里把代码贴一下:

/**
 * 初始化netty client
 */
@Override
public void start() {
    //......
    this.bootstrap.group(this.eventLoopGroupWorker).channel(
        nettyClientConfig.getClientChannelClazz())
            // 不启用开启Nagle算法,Nagle算法会收集网络小包再一次性发送,不启用则是即时发送
            .option(ChannelOption.TCP_NODELAY, true)
            // 连接保活
            .option(ChannelOption.SO_KEEPALIVE, true)
            // 客户端连接超时时间
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis())
            // 发送数据缓冲区大小
            .option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize())
            // 接收数据缓冲区大小
            .option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize());

    //......
    bootstrap.handler(
        new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) {
                ChannelPipeline pipeline = ch.pipeline();
                pipeline.addLast(
                        // 基于心跳保持会话状态
                    new IdleStateHandler(nettyClientConfig.getChannelMaxReadIdleSeconds(),
                        nettyClientConfig.getChannelMaxWriteIdleSeconds(),
                        nettyClientConfig.getChannelMaxAllIdleSeconds()))
                       //......
            }
        });
}

参数的优化设置基本与Server端差不多。
业务服务启动的时候,除了会初始化RM的Netty Client之外,还会将RM注册到Seata Server端,而注册的地方就是在DataSourceProxy#init(),从这里也可以看出来,一个RM其实可以对应多个数据源,我们进入代码看看。

private void init(DataSource dataSource, String resourceGroupId) {
    //......
    // 注册事务资源信息
    DefaultResourceManager.get().registerResource(this);
    //......
    // 设置默认的事务模式为AT
    RootContext.setDefaultBranchType(this.getBranchType());
}
@Override
public void registerResource(Resource resource) {
    getResourceManager(resource.getBranchType()).registerResource(resource);
}

由于是AT模式,所以默认走模板类AbstractResourceManager#registerResource()处理。

@Override
public void registerResource(Resource resource) {
    RmNettyRemotingClient.getInstance().registerResource(resource.getResourceGroupId(), resource.getResourceId());
}
public void registerResource(String resourceGroupId, String resourceId) {
    //......
    synchronized (getClientChannelManager().getChannels()) {
        // 遍历已经连接的Seata Server端,发送注册RM请求
        for (Map.Entry<String, Channel> entry : getClientChannelManager().getChannels().entrySet()) {
            String serverAddress = entry.getKey();
            Channel rmChannel = entry.getValue();
            //......
            sendRegisterMessage(serverAddress, rmChannel, resourceId);
        }
    }
}
public void sendRegisterMessage(String serverAddress, Channel channel, String resourceId) {
    // 注册RM的请求
    RegisterRMRequest message = new RegisterRMRequest(applicationId, transactionServiceGroup);
    message.setResourceIds(resourceId);
    try {
        super.sendAsyncRequest(channel, message);
    } //......
}

上面的代码很清晰了,就是发送了一个注册RM的请求RegisterRMRequest给Server端。
回过头在registerProcessor()看一下RegisterRMRequest对应的处理器是哪个:

private void registerProcessor() {
    //......
    // 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);
}

很明显是RegRmProcessor进行处理的,进入看看:

private void onRegRmMessage(ChannelHandlerContext ctx, RpcMessage rpcMessage) {
    //......
    try {
        if (null == checkAuthHandler || checkAuthHandler.regResourceManagerCheckAuth(message)) {
        	// 注册RM与channel的关系
            ChannelManager.registerRMChannel(message, ctx.channel());
            Version.putChannelVersion(ctx.channel(), message.getVersion());
            isSuccess = true;
        }
    } catch (Exception exx) {
        isSuccess = false;
        errorInfo = exx.getMessage();
    }
    RegisterRMResponse response = new RegisterRMResponse(isSuccess);
    if (StringUtils.isNotEmpty(errorInfo)) {
        response.setMsg(errorInfo);
    }
    remotingServer.sendAsyncResponse(rpcMessage, ctx.channel(), response);
    //......
}
/**
 * resourceId -> applicationId -> ip -> port -> RpcContext
 */
private static final ConcurrentMap<String, ConcurrentMap<String, ConcurrentMap<String,
    ConcurrentMap<Integer, RpcContext>>>> RM_CHANNELS = new ConcurrentHashMap<>();

就是通过一个map将RM资源与channel的关系记录起来,方便后续的往channel写回数据。

注册TM的过程

傅青阳:既然知道了RM的注册过程,那么TM是否也类似?
元始:是的,但是有一点不同,TM是使用了对象池。
先普及一下对象池 GenericKeyedObjectPool 的使用场景:一般用于存储重量级、初始化耗费资源的对象,比如创建数据库连接等。
创建对象是使用了 borrowObject方法,销毁对象是使用了returnObject方法。实际上并不会马上销毁,只会先放入池中,有需要使用的时候再提取出来。

注册TM的入口就在这里:NettyClientChannelManager->NettyPoolableFactory#makeObject()
从代码可以看到:

private Channel doConnect(String serverAddress) {
    Channel channelToServer = channels.get(serverAddress);
    if (channelToServer != null && channelToServer.isActive()) {
        return channelToServer;
    }
    Channel channelFromPool;
    try {
        //...
        // 从对象池中取出一个netty连接,如果一开始没有netty连接的时候,会发起一次TM注册请求
        channelFromPool = nettyClientKeyPool.borrowObject(poolKeyMap.get(serverAddress));
        channels.put(serverAddress, channelFromPool);
    } 
    return channelFromPool;
}

@Override
public Channel makeObject(NettyPoolKey key) {
    //......
    try {
        /**
         * 发送注册TM的请求
         */
        response = rpcRemotingClient.sendSyncRequest(tmpChannel, key.getMessage());
        // 注册TM失败
        if (!isRegisterSuccess(response, key.getTransactionRole())) {
            rpcRemotingClient.onRegisterMsgFail(key.getAddress(), tmpChannel, response, key.getMessage());
        }
        // 注册TM成功
        else {
            channelToServer = tmpChannel;
            rpcRemotingClient.onRegisterMsgSuccess(key.getAddress(), tmpChannel, response, key.getMessage());
        }
    } 
    return channelToServer;
}

举一反三,TM的注册处理器就是RegTmProcessor,我们在这里也是看到Seata Server端也是使用了一个map来存储TM客户端与channel的关系:

/**
* ip+appname,port
 */
private static final ConcurrentMap<String, ConcurrentMap<Integer, RpcContext>> TM_CHANNELS
    = new ConcurrentHashMap<>();

到了目前为止,就轮到我装一下了,挥坼方遒,绣口一吐就是半个盛唐,我们可以画出目前的整体流程图:
seata的AT模式事务主干流程

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gemini技术窝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值