元始一副智珠在握的表情,就差吟诵一首诗:手握日月摘星辰,世间无我这般人。我欲乘风归去,又恐琼楼玉宇啊。咦,我怎么在半空中呢?
傅青阳摆摆手,元始的脸部就与地面亲密接触了。心想:最讨厌别人装了,说道:继续啊,前面提到的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传输属于高频且和业务相关性紧密,使用互不干扰的线程池可以提高效率。画图如下:
接下来我们看看Netty Server的参数优化设置:
- SO_BACKLOG:3次握手的队列缓冲区大小,防止一下子涌入太多建立连接的请求,默认值太小导致无法连接。
- SO_REUSEADDR:一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用,防止短暂重启无法监听端口。
- SO_KEEPALIVE:默认每隔1小时通过TCP探测包维持连接。
- TCP_NODELAY:Nagle算法会收集网络小包再一次性发送,不启用则是即时发送,防止小的请求一直被堆积。
- SO_SNDBUF:发送数据缓冲区大小,默认150KB,提高吞吐量。
- SO_RCVBUF:接收数据缓冲区大小,默认150KB,提高吞吐量。
- WRITE_BUFFER_WATER_MARK:控制网络水位,将网络传输速度维持在比较平稳的状态,防止出现瞬时读写流量过大打满网卡,导致其他应用无法进行网络通信。
- 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<>();
到了目前为止,就轮到我装一下了,挥坼方遒,绣口一吐就是半个盛唐,我们可以画出目前的整体流程图: