RocketMQ4.3.X笔记(6):分布式消息队列的协调者 NameServer

NameServer 的功能

  1. NameServer 是整个消息队列中的状态服务器,集群的各个组件通过它来了解全局的信息。各个角色的机器都要定期向NameServer 上报自己的状态,超时不上报的话, NameServer 会认为某个机器出故障不可用了,其他的组件会把这个机器从可用列表里移除
  2. NamServer 可以部署多个,相互之间独立,其他角色同时向多个NameServer机器上报状态信息,从而达到热备份的目的。
  3. NameServer 本身是无状态的, NameServer 中的Broker 、Topic 等状态信息不会持久存储,都是由各个角色定时上报并存储到内存中的( NameServer 支持配置参数的持久化, 一般用不到)。

集群状态的存储结构

org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager 类负责保存集群的状态信息

属性描述
HashMap<String/* topic */, List> topicQueueTabletopicQueueTable 这个结构的Key 是Topic 的名称,它存储了所有Topic的属性信息。Value 是个QueueData 队列, 队里的长度等于这个Topic数据存储的Master Broker 的个数, QueueData 里存储着Broker 的名称、读写queue 的数量、同步标识等
HashMap<String/* brokerName */, BrokerData> brokerAddrTable;BrokerAddrTable以BrokerName 为索引,相同名称的Broker 可能存在多台机器, 一个Master 和多个Slave 。这个结构存储着一个BrokerName 对应的属性信息,包括所属的Cluster 名称, 一个Master Broker 和多个Slave Broker的地址信息
HashMap<String/* clusterName /, Set<String/ brokerName */>> clusterAddrTable;ClusterAddrTable存储的是集群中C luster 的信息,结果很简单,就是一个Cluster 名称对应一个由BrokerName 组成的集合
HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;这个结构和BrokerAddrTable 有关系,但是内容完全不同,这个结构的Key 是BrokerAddr ,也就是对应着一台机器, BrokerAddrTable 中的Key是BrokerName , 多个机器的BrokerName 可以相同。BrokerLiveTable存储的内容是这台Broker 机器的实时状态,包括上次更新状态的时间戳, NameServer 会定期检查这个时间戳,超时没有更新就认为这个Broker 无效了,将其从Broker 列表里清除
HashMap<String/* brokerAddr /, List/ Filter Server */> filterServerTable;Filter Server 是过滤服务器,是RocketMQ 的一种服务端过滤方式,一个Broker 可以有一个或多个F ilter Server 。这个结构的Key 是Broker的地址, Value 是和这个Broker 关联的多个Filter Server 的地址

状态维护逻辑

  1. 其他角色会主动向NameServer上报状态,所以NameServer的主要逻辑在org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor类中,根据上报消息里的请求码做相应的处理,更新存储的对应信息。
  2. 连接断开的事件也会触发状态更新,具体逻辑在org.apache.rocketmq.namesrv.routeinfo.BrokerHousekeepingService 类中

各个角色间的交互流程(Topic 的创建为例)

交互流程源码分析

  1. 创建Topic 的代码在org.apache.rocketmq.tools.command.topic.UpdateTopicSubCommand类中,创建Topic 的命令是updateTopic,而在UpdateTopicSubCommand类中的工作主要是 TopicConfig 的组件工作。

  2. updateTopic的命令中,其中b 和c 参数比较重要, 而且他们俩只有一个会起作用( -b 优先), b 参
    数指定在哪个Broker 上创建本Topic 的Message Queue , c 参数表示在这个Cluster 下面所有的Master Broker 上创建这个Topic 的Message Queue , 从而达.到高可用性的目的。具体的创建动作是通过发送命令触发的

  3. 真正的命令在org.apache.rocketmq.client.impl.MQClientAPIImpl类里面发送的,TopicConfig 会作为参数传入相应的方法中:

    public void createTopic(final String addr, final String defaultTopic, final TopicConfig topicConfig, final long timeoutMillis) throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
        # 根据 TopicConfig 组装命令
        CreateTopicRequestHeader requestHeader = new CreateTopicRequestHeader();
        requestHeader.setTopic(topicConfig.getTopicName());
        requestHeader.setDefaultTopic(defaultTopic);
        requestHeader.setReadQueueNums(topicConfig.getReadQueueNums());
        requestHeader.setWriteQueueNums(topicConfig.getWriteQueueNums());
        requestHeader.setPerm(topicConfig.getPerm());
        requestHeader.setTopicFilterType(topicConfig.getTopicFilterType().name());
        requestHeader.setTopicSysFlag(topicConfig.getTopicSysFlag());
        requestHeader.setOrder(topicConfig.isOrder());

        # 创建命令
        RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.UPDATE_AND_CREATE_TOPIC, requestHeader);
        # 发送命令
        RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),
            request, timeoutMillis);
        assert response != null;
        switch (response.getCode()) {
            case ResponseCode.SUCCESS: {
                return;
            }
            default:
                break;
        }

        throw new MQClientException(response.getCode(), response.getRemark());
    }
  1. 创建Topic 的命令被发往对应的Broker , Broker 接到创建Topic 的请求后,执行具体的创建逻辑在org.apache.rocketmq.broker.processor.AdminBrokerProcessor类中。如updateAndCreateTopic方法
  • 接收传输的命令,更新本地的 topicConfig
  • 方法最后一步是 向NameServer 发送注册信息,NameServer 完成创建Topic 的逻辑后,其他客户端才能发现新增的Topic
    private synchronized RemotingCommand updateAndCreateTopic(ChannelHandlerContext ctx,
        RemotingCommand request) throws RemotingCommandException {
        final RemotingCommand response = RemotingCommand.createResponseCommand(null);
        final CreateTopicRequestHeader requestHeader =
            (CreateTopicRequestHeader) request.decodeCommandCustomHeader(CreateTopicRequestHeader.class);
        log.info("updateAndCreateTopic called by {}", RemotingHelper.parseChannelRemoteAddr(ctx.channel()));

        if (requestHeader.getTopic().equals(this.brokerController.getBrokerConfig().getBrokerClusterName())) {
            String errorMsg = "the topic[" + requestHeader.getTopic() + "] is conflict with system reserved words.";
            log.warn(errorMsg);
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark(errorMsg);
            return response;
        }

        try {
            response.setCode(ResponseCode.SUCCESS);
            response.setOpaque(request.getOpaque());
            response.markResponseType();
            response.setRemark(null);
            ctx.writeAndFlush(response);
        } catch (Exception e) {
            log.error("Failed to produce a proper response", e);
        }

        TopicConfig topicConfig = new TopicConfig(requestHeader.getTopic());
        topicConfig.setReadQueueNums(requestHeader.getReadQueueNums());
        topicConfig.setWriteQueueNums(requestHeader.getWriteQueueNums());
        topicConfig.setTopicFilterType(requestHeader.getTopicFilterTypeEnum());
        topicConfig.setPerm(requestHeader.getPerm());
        topicConfig.setTopicSysFlag(requestHeader.getTopicSysFlag() == null ? 0 : requestHeader.getTopicSysFlag());

        # 更新本地的 topicConfig
        this.brokerController.getTopicConfigManager().updateTopicConfig(topicConfig);
        
        # 向NameServer 发送注册信息
        this.brokerController.registerIncrementBrokerData(topicConfig,this.brokerController.getTopicConfigManager().getDataVersion());

        return null;
    }
  1. NameServer 创建的操作在 org.apache.rocketmq.namesrv.routeinfo.RouteInfoManagerregisterBroker() 方法中:
  • 更新 Broker 信息
  • 对每个Master 角色的Broker,创建一个QueueData 对象
  • 如果是新建Topic,就是添加QueueData 对象;如果是修改Topic,就是把旧的QueueData 删除,加入新的QueueData 。
  • 注意:写锁同步的实现,避免在代码中频繁的topic的创建销毁操作
    public RegisterBrokerResult registerBroker(
        final String clusterName,
        final String brokerAddr,
        final String brokerName,
        final long brokerId,
        final String haServerAddr,
        final TopicConfigSerializeWrapper topicConfigWrapper,
        final List<String> filterServerList,
        final Channel channel) {
        RegisterBrokerResult result = new RegisterBrokerResult();
        try {
            try {
                this.lock.writeLock().lockInterruptibly();

                # 更新 Broker 信息,配置的 brokerClusterName,获取 brokerName
                Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
                if (null == brokerNames) {
                    brokerNames = new HashSet<String>();
                    this.clusterAddrTable.put(clusterName, brokerNames);
                }
                brokerNames.add(brokerName);

                boolean registerFirst = false;

                #  BrokerData 内部维护 cluster、brokerName、 brokerAddrs
                BrokerData brokerData = this.brokerAddrTable.get(brokerName);
                if (null == brokerData) {
                    registerFirst = true;
                    brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
                    this.brokerAddrTable.put(brokerName, brokerData);
                }
                String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
                registerFirst = registerFirst || (null == oldAddr);

                # 对每个Master 角色的Broker,创建一个QueueData 对象
                if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId) {
                    if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion()) || registerFirst) {
                        ConcurrentMap<String, TopicConfig> tcTable = topicConfigWrapper.getTopicConfigTable();
                        if (tcTable != null) {
                            for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                                this.createAndUpdateQueueData(brokerName, entry.getValue());
                            }
                        }
                    }
                }
                
                # 更新信息并且返回,主要是维护 brokerLiveTable 和 filterServerTable ,用于检查Broker的状态与过滤器
            } finally {
                this.lock.writeLock().unlock();
            }
        } catch (Exception e) {
            log.error("registerBroker Exception", e);
        }

        return result;
    }

为何不用ZooKeeper

RocketMQ 的架构设计决定了它不需要进行Master选举,用不到这些复杂的功能,只需要一个轻量级的元数据服务器就足够了

底层通信机制

Remoting 模块

  1. 类图如下:

Remoting 模块

  1. 最上层接 RemotingService
  • void start();
  • void shutdown();
  • void registerRPCHook(RPCHook rpcHook);
  1. RemotingClientRemotingServer 接口继承RemotingService 接口,并增加了自己特有的方法。NettyRemotingClientNettyRemotingServer 分别实现了RemotingCIientRemotingServer,而且都继承了NettyRemotingAbstract

  2. 统一格式的自定义消息类: RemotingCommand,并且处理编解码的

  3. 比如NameServer 模块中NamesrvControllerremotingServer 变量以及注册处理器处理器DefaultRequestProcessorNamesrvControllerDefaultRequestProcessor 部分代码如下:

# NamesrvController 部分代码
public class NamesrvController {
    private RemotingServer remotingServer;
    
    # 初始化 remotingServer
    public boolean initialize() {
        ... ...
        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
        
        this.registerProcessor();
        ... ...
    }
    
    # 注册处理器
    private void registerProcessor() {
        if (namesrvConfig.isClusterTest()) {
            this.remotingServer.registerDefaultProcessor(new ClusterTestRequestProcessor(this, namesrvConfig.getProductEnvName()), this.remotingExecutor);
        } else {
            this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor);
        }
    }

}    

# DefaultRequestProcessor 类部分代码,核心在于 processRequest()方法的逻辑处理
public class DefaultRequestProcessor implements NettyRequestProcessor {
    # 拿到请求的 RemotingCommand ,然后处理
    @Override
    public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException {
        # 业务处理
        switch (request.getCode()) {
            case RequestCode.PUT_KV_CONFIG:
                return this.putKVConfig(ctx, request);
            case RequestCode.GET_KV_CONFIG:
                return this.getKVConfig(ctx, request);
            case RequestCode.DELETE_KV_CONFIG:
                return this.deleteKVConfig(ctx, request);
            case RequestCode.QUERY_DATA_VERSION:
                return queryBrokerTopicConfig(ctx, request);
            省略 ... ...
            default:
                break;
        }
        return null;
    }
    
}

协议设计和编解码

  1. 通信协议设计
    通信协议设计
  • 第一部分是大端4 个字节整数,值等于第二、三、四部分长度的总和;
  • 第二部分是大端4 个字节整数,值等于第三部分的长度;
  • 第三部分是通过 Json 序列化的数据;
  • 第四部分是通过应用自定义二进制序列化的数据。
  1. 信息的编码过程在 RemotingCommandencode()方法中完成
    public ByteBuffer encode() {
        // 1> header length size
        int length = 4;

        // 2> header data length
        byte[] headerData = this.headerEncode();
        length += headerData.length;

        // 3> body data length
        if (this.body != null) {
            length += body.length;
        }

        ByteBuffer result = ByteBuffer.allocate(4 + length);

        // length
        result.putInt(length);

        // header length
        result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC));

        // header data
        result.put(headerData);

        // body data;
        if (this.body != null) {
            result.put(this.body);
        }

        result.flip();

        return result;
    }
  1. 消息的编解码过程在RemotingCommanddecode() 方法中完成
    public static RemotingCommand decode(final ByteBuffer byteBuffer) {
        int length = byteBuffer.limit();
        int oriHeaderLen = byteBuffer.getInt();
        int headerLength = getHeaderLength(oriHeaderLen);

        byte[] headerData = new byte[headerLength];
        byteBuffer.get(headerData);

        RemotingCommand cmd = headerDecode(headerData, getProtocolType(oriHeaderLen));

        int bodyLength = length - 4 - headerLength;
        byte[] bodyData = null;
        if (bodyLength > 0) {
            bodyData = new byte[bodyLength];
            byteBuffer.get(bodyData);
        }
        cmd.body = bodyData;

        return cmd;
    }

Netty 库

RocketMQ 是基于Netty 库来完成 RemotingServerRemotingClient 具体的通信实现的。主要在于 NettyRemotingServerNettyRemotingClient 两个类

参考

  1. Apache RocketMQ 官网
  2. Best Practice For NameServer
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值