RocketMQ源码系列(5) — Broker 基础

·  阅读 256
RocketMQ源码系列(5) — Broker 基础

Broker 启动

Broker 模块

Broker 相关代码集中在 rocketmq-broker 模块下,Broker 应该是 RocketMQ 中最为复杂的一部分,绝大部分消息队列的特性都在这个模块中。

一些核心的特性模块如下:

  • client:生产者管理器、消费者管理器、Broker-Client调用器等
  • dledger:基于 DLedger 技术的 Broker 高可用
  • filter:ConsumerFilter 相关,用于消费者消费消息过滤
  • filtersrv:FilterServer 相关,基于 FilterServer 服务器进行消息过滤
  • longpolling:PullRequest 相关,消费者读取消息
  • offset:ComsumerOffset 相关,消费偏移量管理
  • processor:Broker 的 Netty 处理器
  • salve:主从同步组件
  • subscription:订阅组管理
  • topic:Topic 管理
  • transaction:事务管理

image.png

BrokerStartup

NameServer 的启动类和控制器分别为 NamesrvStartupNamesrvController,对应的,Broker 的启动类和控制器分别为 BrokerStartupBrokerController,这便是阅读 Broker 源码的入口。

与 NamesrvStartup 类似,BrokerStartup 也是先构建控制器 BrokerController,再调用其 start() 方法启动。也就是说 Broker 启动的大部分逻辑都在 BrokerController 中。

public class BrokerStartup {
    public static void main(String[] args) {
        // 创建 Broker 控制器
        BrokerController brokerController = createBrokerController(args);
        // 启动控制器
        brokerController.start();
    }
}
复制代码

BrokerController 构建的逻辑与 NamesrvController 也是类似的,构建命令行参数、处理一些配置、创建 BrokerController 等,核心的逻辑如下:

  • 构建 Broker 端的命令行 CommandLine,这个就是用来在启动 Broker 时进行命令行参数交互的

  • 创建 Broker、Netty 服务器/客户端、消息存储等配置对象,可以看到,Broker 是既有 NettyServer,又有 NettyClient 的配置对象,因为 Broker 会作为客户端调用 NameServer,也会作为服务端被生产者/消费者调用。

  • Broker端的 NettyServer 监听端口默认设置为 10911

  • 读取命令行 -c 参数指定的 Broker 配置文件,加载配置;再读取命令行中的参数

  • 如果是 Master 节点,设置 brokerId=0,Slave节点的 bokerId 必须大于 0;而如果启用了 DLeger 技术,brokerId 都设置为 -1

  • 可以通过 -p 参数打印所有参数项,通过 -m 打印重要的参数项

  • 最后通过配置对象创建 BrokerController,并调用其 initialize 来初始化

public static BrokerController createBrokerController(String[] args) {
    try {
        // 创建 POSIX 风格的命令行选项
        Options options = ServerUtil.buildCommandlineOptions(new Options());
        commandLine = ServerUtil.parseCmdLine("mqbroker", args, buildCommandlineOptions(options), new PosixParser());

        // 创建配置对象
        final BrokerConfig brokerConfig = new BrokerConfig();
        final NettyServerConfig nettyServerConfig = new NettyServerConfig();
        final NettyClientConfig nettyClientConfig = new NettyClientConfig();
        final MessageStoreConfig messageStoreConfig = new MessageStoreConfig();
        
        // 设置是否启用 SSL/TLS
        nettyClientConfig.setUseTLS(Boolean.parseBoolean(System.getProperty(TLS_ENABLE, String.valueOf(TlsSystemConfig.tlsMode == TlsMode.ENFORCING))));
        // Broker 默认端口 10911
        nettyServerConfig.setListenPort(10911);

        // -c 参数指定 Broker 配置文件
        if (commandLine.hasOption('c')) {
            String file = commandLine.getOptionValue('c');
            if (file != null) {
                configFile = file;
                InputStream in = new BufferedInputStream(new FileInputStream(file));
                properties = new Properties();
                properties.load(in);

                // 加载配置文件中的配置
                properties2SystemEnv(properties);
                MixAll.properties2Object(properties, brokerConfig);
                MixAll.properties2Object(properties, nettyServerConfig);
                MixAll.properties2Object(properties, nettyClientConfig);
                MixAll.properties2Object(properties, messageStoreConfig);

                BrokerPathConfigHelper.setBrokerConfigPath(file);
                in.close();
            }
        }

        // 加载命令行中的参数
        MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), brokerConfig);
        
        // NameServer 地址
        String namesrvAddr = brokerConfig.getNamesrvAddr();

        // Master 节点设置 BrokerId=0
        switch (messageStoreConfig.getBrokerRole()) {
            case ASYNC_MASTER:
            case SYNC_MASTER:
                brokerConfig.setBrokerId(MixAll.MASTER_ID);
                break;
            case SLAVE:
                if (brokerConfig.getBrokerId() <= 0) {
                    System.out.printf("Slave's brokerId must be > 0");
                    System.exit(-3);
                }
                break;
            default:
                break;
        }

        // 启动了 DLeger 技术时,BrokerId 都设置为 -1
        if (messageStoreConfig.isEnableDLegerCommitLog()) {
            brokerConfig.setBrokerId(-1);
        }
        // HA 端口,默认为 10912
        messageStoreConfig.setHaListenPort(nettyServerConfig.getListenPort() + 1);

        // Logger 配置
        // LoggerContext lc = ....
        
        // 打印所有配置项
        if (commandLine.hasOption('p')) {
            // print....
            System.exit(0);
        }
        // 打印重要配置项
        else if (commandLine.hasOption('m')) {
            // print...
            System.exit(0);
        }

        // 创建 Broker 控制器
        final BrokerController controller = new BrokerController(
                brokerConfig, nettyServerConfig, nettyClientConfig, messageStoreConfig);

        // 控制器初始化
        boolean initResult = controller.initialize();

        // 添加JVM钩子函数在JVM停止时关闭 BrokerController
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    controller.shutdown();
                }
            }
        }, "ShutdownHook"));

        return controller;
    } catch (Throwable e) {
        System.exit(-1);
    }
    return null;
}
复制代码

最后,总结下 BrokerStartup 的启动初始化流程如下图所示:

image.png

BrokerController

1、中介者模式的控制器

BrokerController 中依赖了非常多的组件,与 NamesrvController 类似,大部分核心组件的初始化都在控制器中进行。

RocektMQ 定义了大量的职责独立的组件,如果让对象之间直接互相关联,势必会导致系统的结构变得很复杂,多个类互相耦合,依赖关系混乱,形成网状结构。

其实不难发现,BrokerController、NamesrvController 都是采用的 中介者模式 来避免组件之间直接依赖。

中介者模式就是用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。其它对象则直接依赖中介者对象,就是这里的控制器,然后通过控制器来获得想要的组件。

image.png

2、核心组件

BrokerController 作为中介者依赖了大量的组件,这里就不贴代码了,对其简单归类可分为如下几大类。

  • 配置对象:Broker、Netty、消息存储等配置
  • 网络组件:基于Netty的服务器和客户端、API调用等
  • Broker管理组件:Broker配置管理、Topic管理、订阅管理、状态统计等组件
  • 生产者组件:生产者管理组件
  • 消费者组件:消费者管理、拉取消息、消费偏移量管理等组件
  • 消息存储:消息本地存储组件
  • 事务管理:事务消息相关组件
  • 请求处理器:Broker 接收生产者、消费者请求的处理器,调用NameServer的处理器
  • 线程池:各个请求处理器绑定的线程池和阻塞队列

image.png

BrokerController 的构造和初始化看起来代码很多,其实比较简单,就是在组合创建各个组件,也不再贴代码展示了。

3、线程池和阻塞队列

上面提到过,BrokerController 中定义了很多线程池和阻塞队列,阻塞队列的容量和线程池的大小都是可以配置的,这块可以根据实际需要进行调优。

例如接收消息的队列和线程池定义:

// 接收客户端消息的处理队列
this.sendThreadPoolQueue = new LinkedBlockingQueue<>(this.brokerConfig.getSendThreadPoolQueueCapacity());

// 发送消息线程池
this.sendMessageExecutor = new BrokerFixedThreadPoolExecutor(
        this.brokerConfig.getSendMessageThreadPoolNums(), // 核心线程数
        this.brokerConfig.getSendMessageThreadPoolNums(), // 最大线程数
        1000 * 60,
        TimeUnit.MILLISECONDS,
        this.sendThreadPoolQueue, // 阻塞队列
        new ThreadFactoryImpl("SendMessageThread_"));
复制代码

BrokerController 初始化时,创建好各个线程池之后,会去注册请求处理器。其实就是一类请求用一个单独的处理器,然后每个处理器绑定一个单独的线程池,避免各类请求互相影响。

例如注册接收客户端消息的处理器:

// 创建发送消息处理器
SendMessageProcessor sendProcessor = new SendMessageProcessor(this);
sendProcessor.registerSendMessageHook(sendMessageHookList);
sendProcessor.registerConsumeMessageHook(consumeMessageHookList);

// 向服务器注册处理器和线程池
this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE, sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.SEND_MESSAGE_V2, sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.SEND_BATCH_MESSAGE, sendProcessor, this.sendMessageExecutor);
this.remotingServer.registerProcessor(RequestCode.CONSUMER_SEND_MSG_BACK, sendProcessor, this.sendMessageExecutor);
复制代码

注册处理器其实就是注册到 NettyRemotingAbstract 的处理器表中,在处理请求时,则从这个表中拿出请求码对应的处理器和线程池来处理业务请求。这块在前面的文章中已经详细分析过,就不在赘述。

// 请求编码对应的处理器和线程池
protected final HashMap<Integer/* request code */, Pair<NettyRequestProcessor, ExecutorService>> processorTable = 
    new HashMap<Integer, Pair<NettyRequestProcessor, ExecutorService>>(64);

public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) {
    ExecutorService executorThis = executor;
    // 线程池为 null,则默认用公共线程池
    if (null == executor) {
        executorThis = this.publicExecutor;
    }

    Pair<NettyRequestProcessor, ExecutorService> pair = new Pair<NettyRequestProcessor, ExecutorService>(processor, executorThis);
    // 放入处理器表中
    this.processorTable.put(requestCode, pair);
}
复制代码

配置管理器

Broker 启动时,在 BrokerController 构造方法中会创建多个配置管理器,这部分配置会持久化到磁盘,启动时也会从磁盘加载到内存。

Broker 管理活动相关的配置主要有如下几个:

  • TopicConfigManager:Topic 配置
  • ConsumerOffsetManager:消费偏移量
  • ConsumerFilterManager:消费过滤器
  • SubscriptionGroupManager:订阅组配置

启动 Broker 后,就可以看到这几个配置管理器对应的配置文件。

image.png

这几个配置管理器都继承自 ConfigManager,ConfigManager 提供了从磁盘加载配置文件到内存,以及将内存数据持久化到磁盘的基础能力。

ConfigManager 有如下几个抽象方法需要子类实现,提供配置文件的路径、编码、解码的方法。

public abstract class ConfigManager {
    // 将内存数据编码成 json 字符串
    public abstract String encode();
    // 配置文件的路径
    public abstract String configFilePath();
    // 将 json 字符串解码成内存数据
    public abstract void decode(final String jsonString);
    // 编码成 json 字符串
    public abstract String encode(final boolean prettyFormat);
}
复制代码

比如 TopicConfigManager,encode 就是将 topicConfigTable、dataVersion 序列化成 json 字符串,decode 就是将json字符串解码成对象。

配置文件的路径默认为:${user.home}/store/config/topics.json,可以通过 storePathRootDir 指定根目录。

public class TopicConfigManager extends ConfigManager {
    // Topic 配置内存表
    private final ConcurrentMap<String, TopicConfig> topicConfigTable = new ConcurrentHashMap<>(1024);
    // 数据版本号
    private final DataVersion dataVersion = new DataVersion();

    private transient BrokerController brokerController;

    @Override
    public String encode() {
        return encode(false);
    }

    @Override
    public String configFilePath() {
        return BrokerPathConfigHelper.getTopicConfigPath(
                this.brokerController.getMessageStoreConfig().getStorePathRootDir());
    }

    @Override
    public void decode(String jsonString) {
        if (jsonString != null) {
            TopicConfigSerializeWrapper topicConfigSerializeWrapper =
                    TopicConfigSerializeWrapper.fromJson(jsonString, TopicConfigSerializeWrapper.class);
            if (topicConfigSerializeWrapper != null) {
                this.topicConfigTable.putAll(topicConfigSerializeWrapper.getTopicConfigTable());
                this.dataVersion.assignNewOne(topicConfigSerializeWrapper.getDataVersion());
                this.printLoadDataWhenFirstBoot(topicConfigSerializeWrapper);
            }
        }
    }

    public String encode(final boolean prettyFormat) {
        TopicConfigSerializeWrapper topicConfigSerializeWrapper = new TopicConfigSerializeWrapper();
        topicConfigSerializeWrapper.setTopicConfigTable(this.topicConfigTable);
        topicConfigSerializeWrapper.setDataVersion(this.dataVersion);
        return topicConfigSerializeWrapper.toJson(prettyFormat);
    }

}
复制代码

ConfigManager 提供了 load 方法来从磁盘读取配置文件,如果配置文件没有内容,则加载 .bak 的备份文件,然后将内容解码到内存中。当修改了内存数据后,就会调用 persist 来将数据持久化到磁盘文件中,持久化的时候会自动创建一个 .bak 的备份文件。

public abstract class ConfigManager {
    // 从磁盘加载配置文件
    public boolean load() {
        String fileName = null;
        try {
            // 加载配置文件,转成 json 字符串
            fileName = this.configFilePath();
            String jsonString = MixAll.file2String(fileName);
            // 配置文件为空时,加载备份文件
            if (null == jsonString || jsonString.length() == 0) {
                // 加载 .bak 备份文件
                return this.loadBak();
            } else {
                // 解码
                this.decode(jsonString);
                return true;
            }
        } catch (Exception e) {
            return this.loadBak(); // 加载备份文件
        }
    }

    // 加载 .bak 配置文件
    private boolean loadBak() {
        String fileName = null;
        try {
            fileName = this.configFilePath();
            String jsonString = MixAll.file2String(fileName + ".bak");
            if (jsonString != null && jsonString.length() > 0) {
                this.decode(jsonString);
                return true;
            }
        } catch (Exception e) {
            return false;
        }

        return true;
    }

    // 持久化
    public synchronized void persist() {
        String jsonString = this.encode(true);
        if (jsonString != null) {
            String fileName = this.configFilePath();
            try {
                MixAll.string2File(jsonString, fileName);
            } catch (IOException e) {
            }
        }
    }
}
复制代码

BrokerController 构造方法中创建好这几个配置管理器后,会在初始化方法中调用 load 方法来加载磁盘配置文件的内容到内存中。

// 从磁盘加载 Topic 元数据
this.topicConfigManager.load();
// 从磁盘加载各个消费组对topic的消费偏移量的元数据
this.consumerOffsetManager.load();
// 从磁盘加载订阅消费组的元数据
this.subscriptionGroupManager.load();
// 从磁盘加载消费过滤器的元数据
this.consumerFilterManager.load();
复制代码

Broker 注册与心跳

Broker注册

BrokerController 中提供了Broker注册的方法 registerBrokerAll

  • 如果参数指定了强制注册,或者判断需要注册才会去注册 Broker;needRegister 是将 Broker 的 TopicConfig 发送到 NameServer 判断数据版本(DataVersion)是否发生变更,只要其中一个 NameServer 的版本不一样,就需要注册。

  • 真正的注册逻辑在 BrokerOuterAPI 的 registerBrokerAll

  • 注册时会传入 Broker 的 Topic 配置数据

  • 注册完成后,会更新本地的HA高可用地址,以及主从同步的Master地址

public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway, boolean forceRegister) {
    // 封装数据 topicConfigTable、dataVersion
    TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper();
    //...
    // 强制注册,或者查询 NameServer 对比 DataVersion 看版本是否不一致
    if (forceRegister || needRegister(...)) {
        // 向所有 NameServer 注册
        doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper);
    }
}

private void doRegisterBrokerAll(boolean checkOrderConfig, boolean oneway, TopicConfigSerializeWrapper topicConfigWrapper) {
    // 向所有 NameServer 注册
    List<RegisterBrokerResult> registerBrokerResultList = this.brokerOuterAPI.registerBrokerAll(..., topicConfigWrapper, ...);

    if (registerBrokerResultList.size() > 0) {
        RegisterBrokerResult registerBrokerResult = registerBrokerResultList.get(0);
        if (registerBrokerResult != null) {
            // 更新HA地址
            if (this.updateMasterHAServerAddrPeriodically && registerBrokerResult.getHaServerAddr() != null) {
                this.messageStore.updateHaMasterAddress(registerBrokerResult.getHaServerAddr());
            }
            // 主从节点,设置Master地址
            this.slaveSynchronize.setMasterAddr(registerBrokerResult.getMasterAddr());
        }
    }
}
复制代码

BrokerOuterAPI 的 registerBrokerAll 才是真正向 NameServer 注册 Broker:

  • 可以看出,注册 Broker 时,是向所有 NameServer 异步注册,通过 CountDownLatch 等待所有注册返回。从这我们可以了解到,NameServer 集群是无状态的,每个 NameServer 都有一份完成的 Broker 数据,因为 Broker 会将自己注册给每个 NameServer。

  • 注册的请求码为 REGISTER_BROKER,就是将 Broker 端的数据封装到 RemotingCommand 中,然后调用 RemotingClient 执行RPC调用。

  • 注册完成后,NameServer 会返回 Broker 组中的 master 地址,HA 地址等,Broker 再去更新这些信息

public List<RegisterBrokerResult> registerBrokerAll(
        final String clusterName, final String brokerAddr, final String brokerName,  final long brokerId,
        final String haServerAddr, final TopicConfigSerializeWrapper topicConfigWrapper, final List<String> filterServerList, 
        final boolean oneway, final int timeoutMills, final boolean compressed
) {
    final List<RegisterBrokerResult> registerBrokerResultList = new CopyOnWriteArrayList<>();
    // 向每个 NameServer 注册 Broker
    List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
    if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
        // 请求头数据
        final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
        requestHeader.setBrokerAddr(brokerAddr);
        requestHeader.setBrokerId(brokerId);
        //...
        // 请求体数据
        RegisterBrokerBody requestBody = new RegisterBrokerBody();
        requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper);
        requestBody.setFilterServerList(filterServerList);
        
        // 基于 CountDownLatch 发送批量请求,同步等待
        final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
        for (final String namesrvAddr : nameServerAddressList) {
            // 异步调用
            brokerOuterExecutor.execute(() -> {
                try {
                    RegisterBrokerResult result = registerBroker(namesrvAddr, oneway, timeoutMills, requestHeader, body);
                    registerBrokerResultList.add(result);
                } finally {
                    // 计数器减1
                    countDownLatch.countDown();
                }
            });
        }
        // 同步等待每个 NameServer 返回
        countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
    }

    return registerBrokerResultList;
}

private RegisterBrokerResult registerBroker(final String namesrvAddr, final boolean oneway, 
        final int timeoutMills,  final RegisterBrokerRequestHeader requestHeader, final byte[] body) {
    // Broker 注册
    RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader);
    request.setBody(body);

    // Oneway 调用
    if (oneway) {
        this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills);
        return null;
    }

    // 同步调用
    RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
    switch (response.getCode()) {
        case ResponseCode.SUCCESS: {
            RegisterBrokerResponseHeader responseHeader =
                    (RegisterBrokerResponseHeader) response.decodeCommandCustomHeader(RegisterBrokerResponseHeader.class);
            RegisterBrokerResult result = new RegisterBrokerResult();
            result.setMasterAddr(responseHeader.getMasterAddr());
            result.setHaServerAddr(responseHeader.getHaServerAddr());
            //...
            return result;
        }
        default:
            break;
    }

    throw new MQBrokerException(response.getCode(), response.getRemark(), requestHeader == null ? null : requestHeader.getBrokerAddr());
}
复制代码

NameServer 端的注册逻辑,主要就是将 Broker 信息注册到路由管理器 RouteInfoManager。这块的逻辑前面分析 NameServer 时已经详细分析过了,其实就是将 Broker 信息写入几个内存表中。

public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request) {
    final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);
    final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();
    final RegisterBrokerRequestHeader requestHeader = (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);

    // Broker 注册参数
    RegisterBrokerBody registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed());

    // 注册 TopicConfig、FilterServer
    RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
        requestHeader.getClusterName(), requestHeader.getBrokerAddr(), requestHeader.getBrokerName(),
        requestHeader.getBrokerId(), requestHeader.getHaServerAddr(),
        registerBrokerBody.getTopicConfigSerializeWrapper(),
        registerBrokerBody.getFilterServerList(), ctx.channel());

    // 返回HA地址、Master地址
    responseHeader.setHaServerAddr(result.getHaServerAddr());
    responseHeader.setMasterAddr(result.getMasterAddr());
    response.setCode(ResponseCode.SUCCESS);
    return response;
}
复制代码

定时心跳

start() 启动方法中,有一个调度器每隔 30秒 调用一次 registerBrokerAll 方法来注册 Broker。调度的周期可以通过 registerNameServerPeriod 来配置,默认为 30秒,不过看代码可以知道调度周期在 10~60 秒。

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        try {
            BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
        } catch (Throwable e) {
            log.error("registerBrokerAll Exception", e);
        }
    }
    // 30秒
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
复制代码

在注册时,会创建一个 BrokerLiveInfo 来保持与 Broker 的连接,其 lastUpdateTimestamp 参数就是 Broker 最近一次注册时间。

// 创建 Broker 保活信息
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
        new BrokerLiveInfo(System.currentTimeMillis(), // 当前时间
                topicConfigWrapper.getDataVersion(), // Broker Topic 版本
                channel, haServerAddr));
复制代码

NamesrvController中启动了一个调度器,每隔10s调用一次RouteInfoManagerscanNotActiveBroker方法。可以看到其核心逻辑就是定时扫描 brokerLiveTable,判断如果 BrokerLiveInfo 超过2min未更新,则判定 Broker 连接失活,会移除 Broker 相关信息。

所以可以得知,Broker 默认每隔30秒向所有 NameServer 注册,NameServer 则更新与 Broker 的连接保活时间,并定期扫描超过2分钟没有注册的Broker将其移除。

// 定时任务:每隔10秒扫描一次 Broker,移除非激活状态的 Broker
this.scheduledExecutorService.scheduleAtFixedRate(
    NamesrvController.this.routeInfoManager::scanNotActiveBroker, 
    5, 10, TimeUnit.SECONDS);

// 扫描失效的Broker
public int scanNotActiveBroker() {
    int removeCount = 0;
    Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<String, BrokerLiveInfo> next = it.next();
        long last = next.getValue().getLastUpdateTimestamp();
        // 默认超过2min未更新就判断失效,关闭 Channel、移除 Broker
        if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
            // 关闭连接通道
            RemotingUtil.closeChannel(next.getValue().getChannel());
            // 移除 BrokerLiveInfo
            it.remove();
            this.onChannelDestroy(next.getKey(), next.getValue().getChannel());

            removeCount++;
        }
    }

    return removeCount;
}
复制代码

Broker下线

Broker 下线时,会向 NameServer 下线,下线逻辑相对比较简单,就是遍历每个 NameServer,发送下线请求。

public void shutdown() {
    this.unregisterBrokerAll();
    //....
}

private void unregisterBrokerAll() {
    this.brokerOuterAPI.unregisterBrokerAll(
            this.brokerConfig.getBrokerClusterName(),
            this.getBrokerAddr(),
            this.brokerConfig.getBrokerName(),
            this.brokerConfig.getBrokerId());
}
复制代码

NameServer 处理下线请求就是从 RouteInfoManager 中移除其相关信息。

public RemotingCommand unregisterBroker(ChannelHandlerContext ctx, RemotingCommand request) {
    
    // 下线
    this.namesrvController.getRouteInfoManager().unregisterBroker(...);

    return response;
}
复制代码

整体架构

通过前面的分析,Broker 注册、心跳的架构图如下图所示,其核心逻辑主要在于 RouteInfoManager 对各个内存结构的更新。

image.png

Broker 网络客户端

网络服务器

Broker 首先会作为客户端的服务器被调用,例如生产者发送消息,消费者拉取消息。

Boker 网络服务器组件是 NettyRemotingServer,这个组件在前面介绍 NameServer 网络服务器时已经详细分析过了,可以参考下:RocketMQ源码系列(4) — 基于Netty的网络服务器

// Broker服务器,端口 10911
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.clientHousekeepingService);
复制代码

网络客户端

1、BrokerOuterAPI

Broker 还会作为网络客户端调用 NameServer,或者 Slave Broker 调用 Master Broker 来同步数据, 调用组件为 BrokerOuterAPI

BrokerOuterAPI 的初始化比较简单,其核心组件主要在于 RemotingClient,这就是基于 Netty 的网络客户端,对网络服务器的RPC调用最终都是通过这个组件进行的。构造方法中创建了 NettyRemotingClient,并在 start() 方法中启动网络客户端。

public class BrokerOuterAPI {
    // Netty 客户端
    private final RemotingClient remotingClient;
    // AddressServer 配置
    private final TopAddressing topAddressing = new TopAddressing(MixAll.getWSAddr());
    // NameServer 地址
    private String nameSrvAddr = null;
    // 线程池
    private final BrokerFixedThreadPoolExecutor brokerOuterExecutor = new BrokerFixedThreadPoolExecutor(4, 10, 1, TimeUnit.MINUTES,
            new ArrayBlockingQueue<>(32), new ThreadFactoryImpl("brokerOutApi_thread_", true));

    public BrokerOuterAPI(final NettyClientConfig nettyClientConfig, RPCHook rpcHook) {
        // 创建 Netty 客户端
        this.remotingClient = new NettyRemotingClient(nettyClientConfig);
        // 注册 RPC 钩子函数
        this.remotingClient.registerRPCHook(rpcHook);
    }

    // 启动 Netty 客户端
    public void start() {
        this.remotingClient.start();
    }
}
复制代码

BrokerOuterAPI 主要提供了如下的一些API调用,可以看出 Broker 作为客户端主要是向 NameServer 注册,以及 Slave 节点从 Master 节点同步数据:

// 向所有 NameServer 注册 Broker
public List<RegisterBrokerResult> registerBrokerAll()

// 向所有 NameServer 下线 Broker
public void unregisterBrokerAll()

// Slave 节点从 Master 同步 Topic 配置数据
public TopicConfigSerializeWrapper getAllTopicConfig(final String addr)

// Slave 节点从 Master 同步消费偏移量
public ConsumerOffsetSerializeWrapper getAllConsumerOffset(final String addr)

// Slave 节点从 Master 同步延迟偏移量
public String getAllDelayOffset(final String addr)

// Slave 节点从 Master 同步订阅组数据
public SubscriptionGroupWrapper getAllSubscriptionGroupConfig(final String addr)
复制代码

2、NameServer 地址

BrokerController 初始化方法中会去设置 BrokerOuterAPI 的 NameServer 地址。如果配置中指定了 NameServer 的地址,就使用配置中的 NameServer 地址。可以看到还有另一种方式就是部署一个 AddressServer,然后定期从 AddressServer 拉取 NameServer 的地址。

// 更新 NameServer 地址
if (this.brokerConfig.getNamesrvAddr() != null) {
    this.brokerOuterAPI.updateNameServerAddressList(this.brokerConfig.getNamesrvAddr());
}
// 没有配置NameServer地址时,每隔2min从AddressServer拉取NameServer地址更新
else if (this.brokerConfig.isFetchNamesrvAddrByAddressServer()) {
    this.scheduledExecutorService.scheduleAtFixedRate(() -> {
        BrokerController.this.brokerOuterAPI.fetchNameServerAddr();
    }, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
}
复制代码

更新 NameServer 地址主要就是在设置 RemotingClient 的 NameServer 地址列表。

public void updateNameServerAddressList(final String addrs) {
    List<String> lst = new ArrayList<String>();
    String[] addrArray = addrs.split(";"); // 多个用 ; 分隔
    for (String addr : addrArray) {
        lst.add(addr);
    }
    this.remotingClient.updateNameServerAddressList(lst);
}
复制代码

NettyRemotingClient

1、基础结构

NettyRemotingClient 继承自网络抽象积累 NettyRemotingAbstract,这个类在前面已经详细分析过了,它主要就是封装了请求处理和响应处理的逻辑。

NettyRemotingClient 的构造初始化比较简单,主要是创建 Netty 的工作线程组 EventLoopGroup 以及处理器线程池,如果启用了 SSL/TLS,就加载 SSL 上下文。

public class NettyRemotingClient extends NettyRemotingAbstract implements RemotingClient {
    // Netty 客户端配置
    private final NettyClientConfig nettyClientConfig;
    // Netty 网络客户端
    private final Bootstrap bootstrap = new Bootstrap();
    // 事件轮询线程组
    private final EventLoopGroup eventLoopGroupWorker;
    // 网络连接事件监听器
    private final ChannelEventListener channelEventListener;
    // 处理器的线程组
    private DefaultEventExecutorGroup defaultEventExecutorGroup;

    public NettyRemotingClient(final NettyClientConfig nettyClientConfig,
        final ChannelEventListener channelEventListener) {
        super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue());
        // Netty 客户端配置
        this.nettyClientConfig = nettyClientConfig;
        this.channelEventListener = channelEventListener;

        // 创建线程组
        this.eventLoopGroupWorker = new NioEventLoopGroup(1, new  ThreadFactory() {});

        // 加载SSL上下文
        if (nettyClientConfig.isUseTLS()) {
            sslContext = TlsHelper.buildSslContext(true);
        }
    }
}
复制代码

2、启动Netty客户端

启动方法中就是在真正启动基于 Netty 的网络客户端了:

  • 首先创建了处理器的线程组 DefaultEventExecutorGroup,可以看出它主要用于处理器的业务处理

  • 接着创建 Netty 客户端 Bootstrap,并设置线程组 eventLoopGroupWorker,网络连接类型为 NioSocketChannel,服务器对应的是 ServerBootstrapEpollServerSocketChannel / NioServerSocketChannel

  • 接着就是配置网络连接的管道,添加一系列的处理器。

  • Bootstrap 构造完成后,启动了一个定时任务每隔3秒扫描一次响应表 responseTable,处理超时的请求。

  • 最后是启动父类中的事件执行器 NettyEventExecutor,其主要就是处理一些网络连接事件的。

public void start() {
    // 处理器线程组
    this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
            nettyClientConfig.getClientWorkerThreads(), new ThreadFactory() {...});

    // Netty 网络客户端
    Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker)
            .channel(NioSocketChannel.class)
            // TCP 配置
            .option(ChannelOption.TCP_NODELAY, true)
            .option(ChannelOption.SO_KEEPALIVE, false)
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis())
            // 配置 ChannelPipeline
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    if (nettyClientConfig.isUseTLS()) {
                        if (null != sslContext) {
                            pipeline.addFirst(defaultEventExecutorGroup, "sslHandler", sslContext.newHandler(ch.alloc()));
                        }
                    }
                    pipeline.addLast(
                            defaultEventExecutorGroup,
                            new NettyEncoder(), // 解码器
                            new NettyDecoder(), // 编码器
                            new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()), // 空闲状态处理器
                            new NettyConnectManageHandler(), // 连接管理器
                            new NettyClientHandler()); // Netty 客户端处理器
                }
            });
    // 其它配置...

    // 每3秒扫描一次 responseTable
    this.timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            NettyRemotingClient.this.scanResponseTable();
        }
    }, 1000 * 3, 1000);

    // 启动 Netty 事件执行器
    if (this.channelEventListener != null) {
        this.nettyEventExecutor.start();
    }
}
复制代码

3、Netty 处理器

在创建 Bootstrap 时,依次配置了如下的处理器,客户端的处理器与服务端的处理器都是相对应的。

  • SslHandler:SSL/TLS 处理器

  • NettyEncoder:编码器

  • NettyDecoder:解码器

  • IdleStateHandler:网络连接空闲状态处理器

  • NettyConnectManageHandler:网络连接管理处理器,网络事件将发布到 NettyEventExecutor

  • NettyClientHandler:Netty 客户端业务处理器,其实质就是在调用父类的 processMessageReceived 来处理器请求和响应。

4、RPC 调用

我们以 Broker 注册为例来看看客户端是如何调用服务器的。

前面已经分析过 Broker 注册的逻辑,在调用 RemotingClient 进行 RPC 调用时,会传入 NameServer 的地址。

private RegisterBrokerResult registerBroker(final String namesrvAddr,...) {
    // Broker 注册命令
    RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.REGISTER_BROKER, requestHeader);

    // 同步调用
    RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
    //....
}
复制代码

可以看到 NettyRemotingClient 的RPC调用逻辑主要是,先根据地址获取或创建网络连接 Channel,然后就可以通过这个 Channel 发起网络调用了。

public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis) {
    // 获取或创建网络连接 Channel
    final Channel channel = this.getAndCreateChannel(addr);
    if (channel != null && channel.isActive()) {
        try {
            doBeforeRpcHooks(addr, request);
            // 同步调用
            RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis);
            doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
            return response;
        } catch (Exception e) {
        }
    } else {
        this.closeChannel(addr, channel);
        throw new RemotingConnectException(addr);
    }
}
复制代码

可以看到,NettyRemotingClient 用了一个内存表 channelTables 来存储服务器地址和 Channel 的关系,如果已经存在且 Channel 为激活状态,则直接返回对应的 Channel,否则就去创建一个新的 Channel,并放入 channelTables 中。

创建 Channel 最核心的代码便是 bootstrap.connect(SocketAddress),这里就会用到 Netty 客户端组件 Bootstrap,Netty 客户端就是通过 Bootstrap 来连接服务器,并返回一个 ChannelFuture,通过 ChannelFuture 可以拿到网络连接 Channel

public class NettyRemotingClient extends NettyRemotingAbstract implements RemotingClient {
    // channelTables 并发锁控制
    private final Lock lockChannelTables = new ReentrantLock();
    private final ConcurrentMap<String /* addr */, ChannelWrapper> channelTables = new ConcurrentHashMap<String, ChannelWrapper>();

    private Channel getAndCreateChannel(final String addr) {
        ChannelWrapper cw = this.channelTables.get(addr);
        if (cw != null && cw.isOK()) {
            return cw.getChannel();
        }
        return this.createChannel(addr);
    }

    // 创建 Channel
    private Channel createChannel(final String addr) throws InterruptedException {
        // 加锁,超时3秒
        if (this.lockChannelTables.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
            try {
                boolean createNewConnection = true;
                cw = this.channelTables.get(addr);
                if (cw != null) {
                    if (cw.isOK()) {
                        return cw.getChannel();
                    } else if (!cw.getChannelFuture().isDone()) {
                        createNewConnection = false;
                    } else {
                        // 移除后重新创建
                        this.channelTables.remove(addr);
                        createNewConnection = true;
                    }
                }

                // 创建新的 Channel
                if (createNewConnection) {
                    // 连接服务器
                    ChannelFuture channelFuture = this.bootstrap.connect(RemotingHelper.string2SocketAddress(addr));
                    cw = new ChannelWrapper(channelFuture);
                    this.channelTables.put(addr, cw);
                }
            } finally {
                this.lockChannelTables.unlock();
            }
        }

        // 获取 Channel
        if (cw != null) {
            ChannelFuture channelFuture = cw.getChannelFuture();
            // 建立连接
            if (channelFuture.awaitUninterruptibly(this.nettyClientConfig.getConnectTimeoutMillis())) {
                if (cw.isOK()) {
                    return cw.getChannel();
                } else {
                    // 连接事变
                }
            }
        }
        return null;
    }
}
复制代码

Broker 网络架构

Broker 即作为网络服务器,又作为网络客户端,所以 BrokerController 在初始化时会创建 Netty 服务器 ServerBootstrap 和 Netty 客户端 Bootstrap。角色不同,网络连接管道 ChannelPipeline 所绑定的处理器不同,应用时机也会不同,但整体机制都是类似的。

image.png

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改