rocketMQ HA实现

rocketMQ在实现消息稳定,不丢失等高可用特性时,采用了2种技术方案,一种是经典Master/Slave设计风格,但是有个最致命的缺点在出现主节点故障时,无法切换某个slave作为主节点,导致该节点集群不可使用,必须将master节点恢复后才能使用。第二种是通过Dledger框架,不仅实现了数据消息的备份,也可以实现故障转移,保证节点集群可以继续使用。
本文主要熟悉Master/Slave是如何实现,并且rocketMQ也是有自身实现代码,并没有引入第三方框架。
作为Broker服务,他有2种角色,分别是Master,Slave。当然生产消息主要与Master交互,但是消息消费2者都能使用。为了消息的稳定,安全,持久化,将消息备份在异地服务器是重要手段,所以Slave节点主要是保持与Master中的消息持续获取,并且保持在本地磁盘中。当然Slave与Master同步数据,不仅仅只有消息,还有很多其他的数据。

SlaveSynchronize Slave同步服务

在BrokerController中,在start启动方法时,针对broker角色进行处理

        if (!messageStoreConfig.isEnableDLegerCommitLog()) {
            startProcessorByHa(messageStoreConfig.getBrokerRole());
            handleSlaveSynchronize(messageStoreConfig.getBrokerRole());
        }

当然采用Master/Slaver模式才会执行代码,在handleSlaveSynchronize方法中

    private void handleSlaveSynchronize(BrokerRole role) {
        if (role == BrokerRole.SLAVE) {
            if (null != slaveSyncFuture) {
                slaveSyncFuture.cancel(false);
            }
            this.slaveSynchronize.setMasterAddr(null);
            slaveSyncFuture = this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    try {
                        BrokerController.this.slaveSynchronize.syncAll();
                    }
                    catch (Throwable e) {
                        log.error("ScheduledTask SlaveSynchronize syncAll error.", e);
                    }
                }
            }, 1000 * 3, 1000 * 10, TimeUnit.MILLISECONDS);
        } else {
            //handle the slave synchronise
            if (null != slaveSyncFuture) {
                slaveSyncFuture.cancel(false);
            }
            this.slaveSynchronize.setMasterAddr(null);
        }
    }

在角色为Slave时,会开启定时任务,执行slaveSynchronize对象的syncAll同步方法

    public void syncAll() {
        this.syncTopicConfig();
        this.syncConsumerOffset();
        this.syncDelayOffset();
        this.syncSubscriptionGroupConfig();
    }

在这块同步代码块中,同步了很多内容,包括同步topic主题配置数据,消费进度数据,消息延迟队列数据同步,和订阅组配置同步。举例其中topic配置同步内容

    private void syncTopicConfig() {
        String masterAddrBak = this.masterAddr;
        if (masterAddrBak != null && !masterAddrBak.equals(brokerController.getBrokerAddr())) {
            try {
                TopicConfigSerializeWrapper topicWrapper =
                    this.brokerController.getBrokerOuterAPI().getAllTopicConfig(masterAddrBak);
                if (!this.brokerController.getTopicConfigManager().getDataVersion()
                    .equals(topicWrapper.getDataVersion())) {

                    this.brokerController.getTopicConfigManager().getDataVersion()
                        .assignNewOne(topicWrapper.getDataVersion());
                    this.brokerController.getTopicConfigManager().getTopicConfigTable().clear();
                    this.brokerController.getTopicConfigManager().getTopicConfigTable()
                        .putAll(topicWrapper.getTopicConfigTable());
                    this.brokerController.getTopicConfigManager().persist();

                    log.info("Update slave topic config from master, {}", masterAddrBak);
                }
            } catch (Exception e) {
                log.error("SyncTopicConfig Exception, {}", masterAddrBak, e);
            }
        }
    }

从master服务器上获取到了topicWrapper包装好的topic配置内容,然后更新自身brokerController中的相关的topic数据,最终进行持久化。其他的同步数据,也是一样,需要持久化到本地文件中。在这里,目前只是同步了一些外围的配置数据,消费进度数据等,但是并没有说明消息是如何同步的。

消息同步

消息同步,设计到存储相关,所以将这部分核心代码放在了store模块下,其中HAService是核心服务。
首先了解一下HAService服务内部类HAClient,他是最为Slave客户端,发起请求同步Master消息数据。

HAClient

        private final AtomicReference masterAddress = new AtomicReference<>(); // master地址
        private final ByteBuffer reportOffset = ByteBuffer.allocate(8);
        private SocketChannel socketChannel;
        private Selector selector;
        private long lastWriteTimestamp = System.currentTimeMillis();

        private long currentReportedOffset = 0;
        private int dispatchPosition = 0; // 该位置,是不停的增长的,当byteBufferRead写满,会重置为0
        private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);
        private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);

这部分是client的基本数据,其中记录了masterAddress地址,currentReportedOffset 当前上报的偏移量,dispatchPosition临时记录存储的位置。由于HAClient是实现了ServiceThread类,所以他也是一个线程类

        public void run() {
            log.info(this.getServiceName() + " service started");
            while (!this.isStopped()) {
                try {
                    if (this.connectMaster()) {
                        if (this.isTimeToReportOffset()) {
                            boolean result = this.reportSlaveMaxOffset(this.currentReportedOffset);
                            if (!result) {
                                this.closeMaster();
                            }
                        }
                        this.selector.select(1000);
                        boolean ok = this.processReadEvent();
                        if (!ok) {
                            this.closeMaster();
                        }
                        // 上报一下slave的最大偏移量进度
                        if (!reportSlaveMaxOffsetPlus()) {
                            continue;
                        }
                        long interval =
                            HAService.this.getDefaultMessageStore().getSystemClock().now()
                                - this.lastWriteTimestamp;
                        if (interval > HAService.this.getDefaultMessageStore().getMessageStoreConfig()
                            .getHaHousekeepingInterval()) {
                            log.warn("HAClient, housekeeping, found this connection[" + this.masterAddress
                                + "] expired, " + interval);
                            this.closeMaster();
                            log.warn("HAClient, master not response some time, so close connection");
                        }
                    } else {
                        this.waitForRunning(1000 * 5);
                    }
                } catch (Exception e) {
                    log.warn(this.getServiceName() + " service has exception. ", e);
                    this.waitForRunning(1000 * 5);
                }
            }
            log.info(this.getServiceName() + " service end");
        }

作为客户端,他需要主动发起连接master,所以在尝试得到获取数据时,确认master是否连接成功,在这里,rocketMQ自己实现了基于jdk的NIO实现通信。

        private boolean connectMaster() throws ClosedChannelException {
            if (null == socketChannel) {
                String addr = this.masterAddress.get();
                if (addr != null) {

                    SocketAddress socketAddress = RemotingUtil.string2SocketAddress(addr);
                    if (socketAddress != null) {
                        this.socketChannel = RemotingUtil.connect(socketAddress);
                        if (this.socketChannel != null) {
                            this.socketChannel.register(this.selector, SelectionKey.OP_READ);
                        }
                    }
                }

                this.currentReportedOffset = HAService.this.defaultMessageStore.getMaxPhyOffset();

                this.lastWriteTimestamp = System.currentTimeMillis();
            }

            return this.socketChannel != null;
        }

在连接master时,确认masterAddress地址是否存在,然后同步该地址得到channel,并且注册到selector中,进行读写监听。当然在连接初始化时,得到currentReportedOffset值,即为当前slave的存储消息的最大偏移量。在连接master成功后,都会定时进行上报master当前偏移量

        private boolean reportSlaveMaxOffset(final long maxOffset) {
            this.reportOffset.position(0);
            this.reportOffset.limit(8);
            this.reportOffset.putLong(maxOffset);
            this.reportOffset.position(0);
            this.reportOffset.limit(8);
            // 上报一个slave最大的偏移量
            for (int i = 0; i < 3 && this.reportOffset.hasRemaining(); i++) {
                try {
                    this.socketChannel.write(this.reportOffset);
                } catch (IOException e) {
                    log.error(this.getServiceName()
                        + "reportSlaveMaxOffset this.socketChannel.write exception", e);
                    return false;
                }
            }

            lastWriteTimestamp = HAService.this.defaultMessageStore.getSystemClock().now();
            return !this.reportOffset.hasRemaining();
        }

上报内容也很简单,将reportOffset 字节缓存进行重置,并且添加8个子节点maxOffset值,然后写入到socketChannel中。这样master就能直到当前slave的上报进度了。当然定时上报还有另外好处,可以用这样的方式代替实时心跳检测,保证channel长时间连接。
在master下发数据时候,slave就能监听到读事件,然后channel就能从通道中读取字节流。

        private boolean processReadEvent() {
            int readSizeZeroTimes = 0;
            while (this.byteBufferRead.hasRemaining()) {
                try {
                    int readSize = this.socketChannel.read(this.byteBufferRead);
                    if (readSize > 0) {
                        readSizeZeroTimes = 0;
                        boolean result = this.dispatchReadRequest();
                        if (!result) {
                            log.error("HAClient, dispatchReadRequest error");
                            return false;
                        }
                    } else if (readSize == 0) {
                        if (++readSizeZeroTimes >= 3) {
                            break;
                        }
                    } else {
                        log.info("HAClient, processReadEvent read socket < 0");
                        return false;
                    }
                } catch (IOException e) {
                    log.info("HAClient, processReadEvent read socket exception", e);
                    return false;
                }
            }

            return true;
        }

processReadEvent 就是用以处理读事件的,其中byteBufferRead是用来保存读取channel通道中的字节流。当读取到内容时,readSize是大于0的。读取完内容后,就开始执行分发读请求 dispatchReadRequest方法。

        private boolean dispatchReadRequest() {
            final int msgHeaderSize = 8 + 4; // phyoffset + size
            int readSocketPos = this.byteBufferRead.position();

            while (true) {
                int diff = this.byteBufferRead.position() - this.dispatchPosition;
                if (diff >= msgHeaderSize) {
                    // 得到物理偏移量
                    long masterPhyOffset = this.byteBufferRead.getLong(this.dispatchPosition);
                    // 得到body的大小
                    int bodySize = this.byteBufferRead.getInt(this.dispatchPosition + 8);

                    long slavePhyOffset = HAService.this.defaultMessageStore.getMaxPhyOffset();
                    if (slavePhyOffset != 0) {
                        if (slavePhyOffset != masterPhyOffset) {
                            log.error("master pushed offset not equal the max phy offset in slave, SLAVE: "
                                + slavePhyOffset + " MASTER: " + masterPhyOffset);
                            return false;
                        }
                    }
                    if (diff >= (msgHeaderSize + bodySize)) { // 至少是一个完整的包
                        byte[] bodyData = new byte[bodySize];
                        this.byteBufferRead.position(this.dispatchPosition + msgHeaderSize);
                        this.byteBufferRead.get(bodyData);
                        // 写入到slave中去   
                        HAService.this.defaultMessageStore.appendToCommitLog(masterPhyOffset, bodyData);

                        this.byteBufferRead.position(readSocketPos);
                        this.dispatchPosition += msgHeaderSize + bodySize; // dispatchPosition 记录的是n个完整的包长度
                        if (!reportSlaveMaxOffsetPlus()) {
                            // 上报一下slave的最大偏移量
                            return false;
                        }
                        continue;
                    }
                }
                if (!this.byteBufferRead.hasRemaining()) {
                    this.reallocateByteBuffer();
                }

                break;
            }

            return true;
        }

定义了一个msgHeaderSize 消息头长度,readSocketPos是当前byteBufferRead的位置,首先明确,byteBufferRead如果从channel中读取数据时候,他的pos位置也是增长的。所以readSocketPos不是读取消息的位置,当前消息的终点位置。那么怎么得到读取消息的起始位置呢,在HAClient中,有个属性dispatchPosition 就是用来保存上一次读取的位置。在master发送消息数据的时候,基本协议的结构是 消息头(包括物理偏移量,消息body的长度)+ 数据内容。在准备读取内容时,确认上一次读取的位置dispatchPosition与当前byteBufferRead的位置的长度差大于等于一个消息头的大小。先从上一个起始点开始读取8个字节,即为masterPhyOffset最大偏移量,然后读取4个字节bodySize,消息内容长度。当前会进行判断slave服务中当前最大的物理偏移量slavePhyOffset 是否与master推送过来的masterPhyOffset是否一致。这样的目的是保证slave与master数据同步一致性,保证是不会丢失。在diff >= (msgHeaderSize + bodySize)比较中,确定是否存在一个完整的数据包。然后声明长度为bodySize的bodyData字节数组,并且重置了byteBufferRead的pos位置,然后将数据复制给bodyData,然后将数据添加到commitLog中。数据添加完成后,又将byteBufferRead的pos位置重置为readSocketPos,并且dispatchPosition 又增加了一个完整数据包的长度,最后及时上报master服务当前slave的最大物理偏移量。
byteBufferRead最为存储通信数据的载体,长度为4M。在目前数据获取时,byteBufferRead是一直在增加,即pos在增加,即有读取到新得数据,byteBufferRead的pos一直变大,直到limit=pos时无法添加内容。为保证数据持续获取,肯定需要将byteBufferRead重置,然后再读取。所以才会有reallocateByteBuffer方法

        private void reallocateByteBuffer() {
            int remain = READ_MAX_BUFFER_SIZE - this.dispatchPosition;
            if (remain > 0) {
                this.byteBufferRead.position(this.dispatchPosition);

                this.byteBufferBackup.position(0);
                this.byteBufferBackup.limit(READ_MAX_BUFFER_SIZE);
                this.byteBufferBackup.put(this.byteBufferRead); // 将read没有读取的部分放入到backup中
            }

            this.swapByteBuffer();

            this.byteBufferRead.position(remain);
            this.byteBufferRead.limit(READ_MAX_BUFFER_SIZE);
            this.dispatchPosition = 0;
        }

当byteBufferRead数据存满,但是任然会出现 最后一个数据包是不完整的。肯定需要将最后一段数据进行保存起来,下次继续使用。在这里,他采用了byteBufferBackup一个备份的字节缓存。remain是指一个数据包的部分数据长度。当remain大于0,说明包不完整,就会将byteBufferRead剩余部分复制给重置后的byteBufferBackup,此时byteBufferBackup是存在数据,并且当前pos为remain。swapByteBuffer方法就是将byteBufferRead和byteBufferBackup执行对象互相交换,即现在的byteBufferRead就是原来的byteBufferBackup对象。然后又重置了byteBufferRead的limit值,并且dispatchPosition的位置也变成0了。下次byteBufferRead再从socketChannel中读取的位置就是从remain开始了。
这是作为slave端,如何主动发起连接,并且上报最大偏移量和消息数据获取并存储的逻辑。

Master服务端

最为服务端,首先他可以由多个slave同时同步消息数据,所以需要有个管理客户端的服务。

AcceptSocketService

该类是HAService内部类,他是监听管理连接slave的connection服务
在配置通信服务端时

        public void beginAccept() throws Exception {
            this.serverSocketChannel = ServerSocketChannel.open();
            this.selector = RemotingUtil.openSelector();
            this.serverSocketChannel.socket().setReuseAddress(true);
            this.serverSocketChannel.socket().bind(this.socketAddressListen);
            this.serverSocketChannel.configureBlocking(false);
            this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
        }

绑定端口,配置非阻塞,并且注册了接收事件,即接受客户端连接请求。
在开启线程时

        public void run() {
            log.info(this.getServiceName() + " service started");

            while (!this.isStopped()) {
                try {
                    this.selector.select(1000);
                    Set selected = this.selector.selectedKeys();

                    if (selected != null) {
                        for (SelectionKey k : selected) {
                            if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) {
                                // 有请求连接
                                SocketChannel sc = ((ServerSocketChannel) k.channel()).accept();

                                if (sc != null) {
                                    HAService.log.info("HAService receive new connection, "
                                        + sc.socket().getRemoteSocketAddress());

                                    try {
                                        // 创建一个客户端的连接,
                                        HAConnection conn = new HAConnection(HAService.this, sc);
                                        conn.start();
                                        HAService.this.addConnection(conn);
                                    } catch (Exception e) {
                                        log.error("new HAConnection exception", e);
                                        sc.close();
                                    }
                                }
                            } else {
                                log.warn("Unexpected ops in select " + k.readyOps());
                            }
                        }

                        selected.clear();
                    }
                } catch (Exception e) {
                    log.error(this.getServiceName() + " service has exception.", e);
                }
            }

            log.info(this.getServiceName() + " service end");
        }

就是实时监听连接请求,selected 即为得到事件的所以连接通道,然后判断确认存在OP_ACCEPT事件,将客户端SocketChannel封装成HAConnection并且添加到HAService中connectionList集合中。当然AcceptSocketService只是管理连接客户端数,服务器真正读写逻辑在HAConnection完成的。

HAConnection

    public HAConnection(final HAService haService, final SocketChannel socketChannel) throws IOException {
        this.haService = haService;
        this.socketChannel = socketChannel;
        this.clientAddr = this.socketChannel.socket().getRemoteSocketAddress().toString();
        this.socketChannel.configureBlocking(false);
        this.socketChannel.socket().setSoLinger(false, -1);
        this.socketChannel.socket().setTcpNoDelay(true);
        this.socketChannel.socket().setReceiveBufferSize(1024 * 64);
        this.socketChannel.socket().setSendBufferSize(1024 * 64);
        this.writeSocketService = new WriteSocketService(this.socketChannel);
        this.readSocketService = new ReadSocketService(this.socketChannel);
        this.haService.getConnectionCount().incrementAndGet();
    }

构成器中创建了readSocketService读服务,和writeSocketService写服务。当然读服务主要任务是读取 slave上报的最大偏移量,而写服务是写入消息数据到通道中。

ReadSocketService

读服务只要监听读事件即可,并且通过线程,实时监听数据,然后执行处理读请求

        private boolean processReadEvent() {
            int readSizeZeroTimes = 0;

            if (!this.byteBufferRead.hasRemaining()) {
                this.byteBufferRead.flip();
                this.processPosition = 0;
            }

            while (this.byteBufferRead.hasRemaining()) {
                try {
                    int readSize = this.socketChannel.read(this.byteBufferRead);
                    if (readSize > 0) {
                        readSizeZeroTimes = 0;
                        this.lastReadTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now();
                        if ((this.byteBufferRead.position() - this.processPosition) >= 8) {
                            // slave上报的最大偏移量占8个子节,正常情况下byteBufferRead的position是8的倍数,但是不能确定出现粘包情况的出现
                            // 所以pos重新计算,规避该情况的出现,保证获取的位置是正确的
                            int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % 8);
                            long readOffset = this.byteBufferRead.getLong(pos - 8);
                            this.processPosition = pos;

                            HAConnection.this.slaveAckOffset = readOffset;
                            if (HAConnection.this.slaveRequestOffset < 0) {
                                HAConnection.this.slaveRequestOffset = readOffset;
                                log.info("slave[" + HAConnection.this.clientAddr + "] request offset " + readOffset);
                            }

                            HAConnection.this.haService.notifyTransferSome(HAConnection.this.slaveAckOffset);
                        }
                    } else if (readSize == 0) {
                        if (++readSizeZeroTimes >= 3) {
                            break;
                        }
                    } else {
                        log.error("read socket[" + HAConnection.this.clientAddr + "] < 0");
                        return false;
                    }
                } catch (IOException e) {
                    log.error("processReadEvent exception", e);
                    return false;
                }
            }

            return true;
        }
    }

byteBufferRead 存储最大长度为1M,但是当byteBufferRead满后,就直接重置,并没有处理未完整包,为什么呢?因为slave上报最大偏移量时,占用8个字节,所以byteBufferRead的长度肯定能存储完整的包。processPosition是上一次读取数据的位置。从socketChannel读取内容时都会复制到byteBufferRead中,然后与上一次去读位置比较,因为一个包只要8个字节,所以差值大于等于8即可。但是在获取slave上报的偏移量时,他只是获取了最近的一个包,即如果this.byteBufferRead.position() - this.processPosition是8的好几倍,他也是读取最某位的数据。不关心,中间包的数据情况。
int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % 8); 该pos获取的方式,就是为了保证pos是8的倍数,剔除掉某位可能出现不完整包的情况。然后readOffset的偏移量就是pos-8 的位置读取8个字节的数据。并且重置了processPosition的位置。slaveAckOffset 就是认为slave存储的数据也是持久化成功的确认位置,然后执行haService.notifyTransferSome服务(待会聊,为什么需要这个通知服务)。其中slaveRequestOffset 是HAConnection的属性,当小于0时需要将此次读取到readOffset赋值给slaveRequestOffset。
这是一个读取服务,既可以直到当前slave的消息持久的位置,也能保证心跳连接方式。

WriteSocketService

该服务肯定是推送消息数据给slave,并且注册了写事件。在slave实现读消息时,已经知道一个包的组成结构,消息头(起始偏移量+消息长度)+ 消息内容。在实现线程接口方法时,分开讲解,一部分 初始情况怎么做,第二部数据是怎么写的,如果没有写完怎么做

第一部分 初始操作


                    if (-1 == HAConnection.this.slaveRequestOffset) {
                        Thread.sleep(10);
                        continue;
                    }
                    if (-1 == this.nextTransferFromWhere) {
                        // 初始化
                        if (0 == HAConnection.this.slaveRequestOffset) {
                            // slave 没有任何请求偏移量,那么默认从一个文件开始
                            long masterOffset = HAConnection.this.haService.getDefaultMessageStore().getCommitLog().getMaxOffset();
                            masterOffset =
                                masterOffset - (masterOffset % HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig()
                                    .getMappedFileSizeCommitLog());

                            if (masterOffset < 0) {
                                masterOffset = 0;
                            }

                            this.nextTransferFromWhere = masterOffset;
                        } else {
                            this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset;
                        }

                        log.info("master transfer data from " + this.nextTransferFromWhere + " to slave[" + HAConnection.this.clientAddr
                            + "], and slave request " + HAConnection.this.slaveRequestOffset);
                    }

slaveRequestOffset 等于-1 说明slave还没有发送当前最大偏移量,作为master,是无法确定给slave推送消息数据的起始位置,所以不能继续执行。
当nextTransferFromWhere为-1时,说明master还没有开始推送,需要确认推送给slave的起始位置。如果slaveRequestOffset等于0,说明slave当前状况完全是新服务,没有本地存储的消息,那么master推送给slave的起始位置是存储消息的最后一个文件开始位置。masterOffset是当前master最大偏移量,最终取模相减之后,即为最后文件的起始位置。
如果说slaveRequestOffset大于0,那么master推送给slave起始位置,即为slave当前最大的位置slaveRequestOffset。
这是初始工作,最重要的任务就是需要确认master推送数据时的起始位置。

第二部分 推送数据

                    SelectMappedBufferResult selectResult =
                        HAConnection.this.haService.getDefaultMessageStore().getCommitLogData(this.nextTransferFromWhere);
                    if (selectResult != null) {
                        int size = selectResult.getSize();
                        if (size > HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize()) {
                            size = HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaTransferBatchSize();
                        }

                        long thisOffset = this.nextTransferFromWhere;
                        this.nextTransferFromWhere += size;

                        selectResult.getByteBuffer().limit(size);
                        this.selectMappedBufferResult = selectResult;

                        // Build Header
                        this.byteBufferHeader.position(0);
                        this.byteBufferHeader.limit(headerSize);
                        this.byteBufferHeader.putLong(thisOffset);
                        this.byteBufferHeader.putInt(size);
                        this.byteBufferHeader.flip();

                        this.lastWriteOver = this.transferData();
                    } else {

                        HAConnection.this.haService.getWaitNotifyObject().allWaitForRunning(100);
                    }

通过nextTransferFromWhere位置截取SelectMappedBufferResult数据,由于受到推送最大长度限制,默认最大size为32k数据。需要对byteBuffer进行limit 限制最大为size长度,然后将selectResult对象指向给当前属性的selectMappedBufferResult。开始先定义头数据byteBufferHeader ,放入了nextTransferFromWhere值8个字节,和内容长度size 4个字节。最后执行传输数据方法transferData

        private boolean transferData() throws Exception {
            int writeSizeZeroTimes = 0;
            // Write Header
            while (this.byteBufferHeader.hasRemaining()) {
                int writeSize = this.socketChannel.write(this.byteBufferHeader);
                if (writeSize > 0) {
                    writeSizeZeroTimes = 0;
                    this.lastWriteTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now();
                } else if (writeSize == 0) {
                    if (++writeSizeZeroTimes >= 3) {
                        break;
                    }
                } else {
                    throw new Exception("ha master write header error < 0");
                }
            }

            if (null == this.selectMappedBufferResult) {
                return !this.byteBufferHeader.hasRemaining();
            }

            writeSizeZeroTimes = 0;

            // Write Body
            if (!this.byteBufferHeader.hasRemaining()) {
                while (this.selectMappedBufferResult.getByteBuffer().hasRemaining()) {
                    int writeSize = this.socketChannel.write(this.selectMappedBufferResult.getByteBuffer());
                    if (writeSize > 0) {
                        writeSizeZeroTimes = 0;
                        this.lastWriteTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now();
                    } else if (writeSize == 0) {
                        if (++writeSizeZeroTimes >= 3) {
                            break;
                        }
                    } else {
                        throw new Exception("ha master write body error < 0");
                    }
                }
            }

            boolean result = !this.byteBufferHeader.hasRemaining() && !this.selectMappedBufferResult.getByteBuffer().hasRemaining();

            if (!this.selectMappedBufferResult.getByteBuffer().hasRemaining()) {
                this.selectMappedBufferResult.release();
                this.selectMappedBufferResult = null;
            }

            return result;
        }

写数据分2部分,一部分是头数据,一部分是写消息数据。首先保证byteBufferHeader写完,才能写入消息内容。在写完消息数据后,有个注意点,就是将selectMappedBufferResult进行release释放。因为从commitLog中获取selectMappedBufferResult时,标记为使用状态。如果用完,需要主动释放。假设此次没有写完时,在下个循环会继续执行。
有个问题需要思考一下,在master推送数据时,推送的数据都是完整的消息内容吗?
答案肯定是否定,首先.规定了消息最大推送长度为32k,假设slave是全新的服务器或者长时间没有同步,导致slave与master服务的最大偏移量差会很大。那么每次推送的数据中肯定都不是非常完整的消息内容了。所以只要偏差小于32k,基本上就能推送完整的消息了。

GroupTransferService

在master读取slave最大偏移量时,会执行HAService中的notifyTransferSome方法

    public void notifyTransferSome(final long offset) {
        for (long value = this.push2SlaveMaxOffset.get(); offset > value; ) {
            // 通知一下slave已经更新到了最大的偏移量,有些同步等待的可以确认slave成功了
            boolean ok = this.push2SlaveMaxOffset.compareAndSet(value, offset);
            if (ok) {
                this.groupTransferService.notifyTransferSome();
                break;
            } else {
                value = this.push2SlaveMaxOffset.get();
            }
        }
    }

因为一个master同时保持多个slave连接,并且每个slave上报的偏移量可能都不太一致,所以该放入通过原子长整型对象push2SlaveMaxOffset,来保证原子性。当offset确实大于了value值,并且push2SlaveMaxOffset更新成功,那么才能执行groupTransferService.notifyTransferSome();方法。如果更新失败,那么从新判断更新。
那么GroupTransferService最用是什么呢?在之前消息存储讲到,当broker是同步状态时候,一条消息存储成功,是需要消息在本地broker持久化成功,第二如果存在Slave,保证slave也需要将消息持久化成功。
在CommitLog类handleHA方法

    public void handleHA(AppendMessageResult result, PutMessageResult putMessageResult, MessageExt messageExt) {
        if (BrokerRole.SYNC_MASTER == this.defaultMessageStore.getMessageStoreConfig().getBrokerRole()) {
            HAService service = this.defaultMessageStore.getHaService();
            if (messageExt.isWaitStoreMsgOK()) {
                // Determine whether to wait
                if (service.isSlaveOK(result.getWroteOffset() + result.getWroteBytes())) {
                    GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
                    service.putRequest(request);
                    service.getWaitNotifyObject().wakeupAll();
                    boolean flushOK =
                        request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
                    if (!flushOK) {
                        log.error("do sync transfer other node, wait return, but failed, topic: " + messageExt.getTopic() + " tags: "
                            + messageExt.getTags() + " client address: " + messageExt.getBornHostNameString());
                        putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_SLAVE_TIMEOUT);
                    }
                }
                // Slave problem
                else {
                    // Tell the producer, slave not available
                    putMessageResult.setPutMessageStatus(PutMessageStatus.SLAVE_NOT_AVAILABLE);
                }
            }
        }

    }

通过DefaulteMessageStore中获取到HAService服务,并且slave都正常,然后创建GroupCommitRequest 对象放入到HAservice中,即为GroupTransferService中,然后进行等待,等待核心就是通过CountDownLatch实现。
GroupTransferService 服务也是实现ServiceThread,

        private final WaitNotifyObject notifyTransferObject = new WaitNotifyObject();
        private volatile List requestsWrite = new ArrayList<>();
        private volatile List requestsRead = new ArrayList<>();

其中requestsWrite 用于放置提交请求的,requestsRead是用来去读请求,通过swapRequests方法进行读写转换。swapRequests方法是通过等待结束后会调用onWaitEnd方法,然后执行读写转换。
首先是放入提交请求方法。

        public synchronized void putRequest(final CommitLog.GroupCommitRequest request) {
            synchronized (this.requestsWrite) {
                this.requestsWrite.add(request);
            }
            if (hasNotified.compareAndSet(false, true)) {
                waitPoint.countDown(); // notify
            }
        }

首先对requestsWrite进行锁定,然后放入提交请求。如果当前线程没有通知,那么将hasNotified标记为已经通知,然后唤醒线程,因为线程await是通过waitPoint实现的。

        private void doWaitTransfer() {
            synchronized (this.requestsRead) {
                if (!this.requestsRead.isEmpty()) {
                    for (CommitLog.GroupCommitRequest req : this.requestsRead) {
                        boolean transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset();
                        for (int i = 0; !transferOK && i < 5; i++) {
                            this.notifyTransferObject.waitForRunning(1000);
                            transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset();
                        }

                        if (!transferOK) {
                            log.warn("transfer messsage to slave timeout, " + req.getNextOffset());
                        }

                        req.wakeupCustomer(transferOK);
                    }

                    this.requestsRead.clear();
                }
            }
        }

doWaitTransfer方法是核心,首先锁住requestsRead,然后就行遍历。当push2SlaveMaxOffset值大于等于了req中物理偏移量值(该值是消息的起始物理偏移量+消息长度),那么需要唤醒写入线程,并且标记成功。如何transferOK =false,则会进行等待,等待时长最多 5s。如果失败,然后会写入唤醒线程,但是标记为失败。写入线程唤醒后,通过状态进行反馈,如果flushOK为false ,putMessageResult放置状态FLUSH_SLAVE_TIMEOUT。
那么有没有可能,存在request丢失的情况,不会出现。极端情况,当put请求时当前对象为write,但此时GroupTransferService线程正好等待结束,进行读写转换了,此时读的对象变成了写,写的对象变成了读,但是放置线程对象执行了读的遍历。因为在放置时,还是读取遍历时,都进行对对象锁定。保证其他线程不能修改了。但是会出现一种情况,就是读取线程遍历完后,会清空读请求,然后释放锁,此时put线程就获取到该对象,然后放置了request对象。那么该对象何时会被读取到呢?需要进行2次swap后才能被读取到,但是不会被清空。


同步流程

作为broker 节点 slave角色,需要同步很多数据,但是其中有2个数据不同步,一个时消费队列ConsumeQueue数据,一个是索引数据IndexFile。那么回顾一下这2个数据是如何实现的,在BrokerController中,有一个线程服务ReputMessageService,会不停的从commitLog中获取消息,然后进行分发,从而会添加消费队列数据和索引数据。

你可能感兴趣的:(rocketMQ HA实现)