RocketMQ源码解析——存储部分(6)RocketMQ主从同步原理相关的HAService和HAConnection

引导

 前面介绍了RocketMQ的CommitLog文件相关的类分析CommitLog物理日志相关的CommitLog。其中有介绍到消息刷盘时高可用对应的handleHA方法,handleHA方法中如果配置的服务器的角色为SYNC_MASTER(从master同步),就会等待主从之间消息同步的进度达到设定的值之后才正常返回,如果超时则返回同步超时

    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()) {
                // 检查slave同步的位置是否小于 最大容忍的同步落后偏移量参数 haSlaveFallbehindMax,如果是的则进行主从同步刷盘
                if (service.isSlaveOK(result.getWroteOffset() + result.getWroteBytes())) {
                    GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
                    service.putRequest(request);
                    service.getWaitNotifyObject().wakeupAll();
                    //countDownLatch.await 同步等待刷新,除非等待超时
                    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);
                }
            }
        }
    }

 这段代码的主要逻辑如下:

  1. 如果服务器的角色设置为SYNC_MASTER,则进行下一步,否则直接跳过主从同步
  2. 获取HAService对象,检查消息是否本地存储完毕,如果没有则结束,否则进入下一步
  3. 检查slave同步的位置是否小于 最大容忍的同步落后偏移量参数haSlaveFallbehindMax,如果是的则进行主从同步刷盘。如果没有则返回slave不可用的状态
  4. 将消息落盘的最大物理偏移量也就是CommitLog上的偏移量作为参数构建一个GroupCommitRequest对象,然后提交到HAService
  5. 最多等待syncFlushTimeout长的时间,默认为5秒。在5秒内获取结果,然后根据结果判断是否返回超时

同步流程

 上面那段代码比较简单,因为主从的逻辑全部交给了HAServiceHAConnection两个类处理了。这里先简单介绍一下整个同步的流程(同步模式)

在这里插入图片描

 这个题可能不好理解,等源码逻辑分析完之后再看可能会清楚点。

高可用服务HAService

HAService是在RocketMQ的Broker启动的时候就会创建的,而创建的点在DefaultMessageStore这个消息存储相关的综合类中,在这个类的构造器中会创建HAService无论当前的Broker是什么角色。这个类后续会有文章分析
 这里需要说明的是Broker中的Master和Slaver两个角色,代码都是一样的,只不过是在实际执行的时候,走的分支不一样

内部属性

 在HAService中有几个比较重要的属性,这里需要简单的介绍一下

参数 说明
connectionList 连接到master的slave连接列表,用于管理连接
acceptSocketService 用于接收连接用的服务,只监听OP_ACCEPT事件,监听到连接事件时候,创建HAConnection来处理读写请求事件
waitNotifyObject 一个消费等待模型类,用于处理高可用线程和CommitLog的刷盘线程交互
push2SlaveMaxOffset master同步到slave的偏移量
groupTransferService 主从同步的检测服务,用于检查是否同步完成
haClient 高可用的服务,slave用来跟master建立连接,像master汇报偏移量和拉取消息

    /**
     * 连接到本机的数量
     */
    private final AtomicInteger connectionCount = new AtomicInteger(0);
    /**
     * 连接列表,用于管理连接
     */
    private final List<HAConnection> connectionList = new LinkedList<>();
    /**
     * 接受和监听slave的连接服务
     */
    private final AcceptSocketService acceptSocketService;

    private final DefaultMessageStore defaultMessageStore;
    /**
     * 一个服务消费者线程模型的对象,用于跟CommitLog的刷盘线程交互
     */
    private final WaitNotifyObject waitNotifyObject = new WaitNotifyObject();
    /**
     * master跟slave 消息同步的位移量
     */
    private final AtomicLong push2SlaveMaxOffset = new AtomicLong(0);
    /**
     * 主从信息同步的服务
     */
    private final GroupTransferService groupTransferService;
    /**
     * 操作主从的类
     */
    private final HAClient haClient;
构造函数

HAService只有一个构造器。逻辑也比较简单,创建一个AcceptSocketService开放一个端口为 10912的端口用于slave来简历连接,同时启动主从信息同步的任务groupTransferService用于接收CommitLog在高可用刷盘时提交任务

public HAService(final DefaultMessageStore defaultMessageStore) throws IOException {
        this.defaultMessageStore = defaultMessageStore;
        //创建,接受连接的服务, 开放的端口号为10912
        this.acceptSocketService =
            new AcceptSocketService(defaultMessageStore.getMessageStoreConfig().getHaListenPort());
        //创建主从信息同步的线程
        this.groupTransferService = new GroupTransferService();
        this.haClient = new HAClient();
    }
内部类分析

HAService在创建之后,会在DefaultMessageStore中调用其start方法,这个方法会启动其内部的几个内部类,用来主从同步

 public void start() throws Exception {
        //接受连接的服务,开启端口,设置监听的事件
        this.acceptSocketService.beginAccept();
        //开启服务不断检查是否有连接
        this.acceptSocketService.start();
        //开启groupTransferService,接受CommitLog的主从同步请求
        this.groupTransferService.start();
        //开启haClient,用于slave来建立与Master连接和同步
        this.haClient.start();
    }

 接下来对这几个内部类进行分析

用于接受Slave连接的AcceptSocketService

AcceptSocketService这个类在Broker的Master和Slaver两个角色启动时都会创建,只不过区别是Slaver开启端口之后,并不会有别的Broker与其建立连接。因为只有在Broker的角色是Slave的时候才会指定要连接的Master地址。这个逻辑,在Broker启动的时候BrokerController类中运行的。

    public void beginAccept() throws Exception {
            //创建ServerSocketChannel
            this.serverSocketChannel = ServerSocketChannel.open();
            //创建selector
            this.selector = RemotingUtil.openSelector();
            //设置SO_REUSEADDR   https://blog.csdn.net/u010144805/article/details/78579528
            this.serverSocketChannel.socket().setReuseAddress(true);
            //设置绑定的地址
            this.serverSocketChannel.socket().bind(this.socketAddressListen);
            //设置为非阻塞模式
            this.serverSocketChannel.configureBlocking(false);
            //注册监听事件为 连接事件
            this.serverSocketChannel.register(this.selector, SelectionKey.OP_ACCEPT);
        }

beginAccept方法就是开启Socket,绑定10912端口,然后注册selector和指定监听的事件为OP_ACCEPT也就是建立连接事件。对应的IO模式为NIO模式。主要看其run方法,这个方法是Master用来接受Slave连接的核心。

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

            while (!this.isStopped()) {
                try {
                    //设置阻塞等待时间
                    this.selector.select(1000);
                    //获取selector 下的所有selectorKey ,后续迭代用
                    Set<SelectionKey> selected = this.selector.selectedKeys();

                    if (selected != null) {
                        for (SelectionKey k : selected) {
                            //检测有连接事件的selectorKey
                            if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) {
                                //获取selectorKey的Channel
                                SocketChannel sc = ((ServerSocketChannel) k.channel()).accept();

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

                                    try {
                                        //创建HAConnection,建立连接
                                        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");
        }

 这里的逻辑比较简单。就是每过一秒检查一次是否有连接事件,如果有则建立连接,并把建立起来的连接加入到连接列表中进行保存。一直循环这个逻辑。

检查同步进度和唤醒CommitLog刷盘线程的GroupTransferService

GroupTransferService是CommitLog消息刷盘的类CommitLogHAService打交道的一个中间类。在CommitLog中进行主从刷盘的时候,会创建一个CommitLog.GroupCommitRequest的内部类,这个类包含了当前Broker最新的消息的物理偏移量信息。然后把这个类丢给GroupTransferService处理,然后唤醒GroupTransferService。起始这个逻辑跟CommitLog内部的GroupCommitService逻辑一样。只不过对于同步部分的逻辑不一样,这里可以参考前面的文章存储部分(3)CommitLog物理日志相关的CommitLog
 先看run方法

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

            while (!this.isStopped()) {
                try {
                    /**
                     * 这里进入等待,等待被唤醒,进入等待之前会调用onWaitEnd方法,然后调用swapRequests方法,
                     * 吧requestsWrite转换为requestsRead
                     */
                    this.waitForRunning(10);
                    /**
                     * 进行请求处理
                     */
                    this.doWaitTransfer();
                } catch (Exception e) {
                    log.warn(this.getServiceName() + " service has exception. ", e);
                }
            }

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

 在run方法中会将传入的CommitLog.GroupCommitRequestrequestsWrite转换到requestsRead中然后进行处理检查对应的同步请求的进度。检查的逻辑在doWaitTransfer

        private void doWaitTransfer() {
            //对requestsRead请求加锁
            synchronized (this.requestsRead) {
                //如果读请求不为空
                if (!this.requestsRead.isEmpty()) {
                    for (CommitLog.GroupCommitRequest req : this.requestsRead) {
                        //如果push到slave的偏移量 大于等于 请求中的消息的最大偏移量 表示slave同步完成
                        boolean transferOK = HAService.this.push2SlaveMaxOffset.get() >= req.getNextOffset();
                        //计算这次同步超时的时间点  同步的超时时间段为5s
                        long waitUntilWhen = HAService.this.defaultMessageStore.getSystemClock().now()
                            + HAService.this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout();
                        //如果没有同步完毕,并且还没达到超时时间,则等待1秒之后检查同步的进度,大概检查5次
                        while (!transferOK && HAService.this.defaultMessageStore.getSystemClock().now() < waitUntilWhen) {
                            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 ? PutMessageStatus.PUT_OK : PutMessageStatus.FLUSH_SLAVE_TIMEOUT);
                    }

                    this.requestsRead.clear();
                }
            }
        }

 主要逻辑如下:

  1. 比较Master推送到Slave的 偏移量push2SlaveMaxOffset是不是大于传进来的CommitLog.GroupCommitRequest中的偏移量
  2. 计算本次同步超时的时间节点,时间为当前时间加上参数系统配置参数syncFlushTimeout默认为5秒
  3. 如果第一步结果为true,则返回结果为PUT_OK。如果第一步为false,则每过一秒检查一次结果,如果超过5次了还没同步完成,则表示超时了那么返回结果为FLUSH_SLAVE_TIMEOUT。同时会唤醒CommitLog的刷盘线程。
与Slave紧密相关的HAClient

 前面我们说到了只有是Salve角色的Broker才会真正的配置Master的地址,而HAClient是需要Master地址的,因此这个类真正在运行的时候只有Slave才会真正的使用到。
 先看看核心的参数信息

 //Socket读缓存区大小
        private static final int READ_MAX_BUFFER_SIZE = 1024 * 1024 * 4;
        //master地址
        private final AtomicReference<String> masterAddress = new AtomicReference<>();
        //Slave向Master发起主从同步的拉取偏移量,固定8个字节
        private final ByteBuffer reportOffset = ByteBuffer.allocate(8);
        private SocketChannel socketChannel;
        private Selector selector;
        //上次同步偏移量的时间戳
        private long lastWriteTimestamp = System.currentTimeMillis();
        //反馈Slave当前的复制进度,commitlog文件最大偏移量
        private long currentReportedOffset = 0;
        private int dispatchPosition = 0;
        //读缓冲大小
        private ByteBuffer byteBufferRead = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);
        private ByteBuffer byteBufferBackup = ByteBuffer.allocate(READ_MAX_BUFFER_SIZE);

 基本上都是缓冲相关的配置。这里主要分析的是run方法中的逻辑

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

            while (!this.isStopped()) {
                try {
                    //连接master,同时监听读请求事件
                    if (this.connectMaster()) {
                        //是否需要汇报偏移量,间隔需要大于心跳的时间(5s)
                        if (this.isTimeToReportOffset()) {
                            //向master 汇报当前 salve 的CommitLog的最大偏移量,并记录这次的同步时间
                            boolean result = this.reportSlaveMaxOffset(this.currentReportedOffset);
                            //如果汇报完了就关闭连接
                            if (!result) {
                                this.closeMaster();
                            }
                        }
                        
                        this.selector.select(1000);
                        //向master拉取的信息
                        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");
        }

 主要的逻辑如下:

  1. 连接master,如果当前的broker角色是master,那么对应的masterAddress是空的,不会有后续逻辑。如果是slave,并且配置了master地址,则会进行连接进行后续逻辑处理
  2. 检查是否需要向master汇报当前的同步进度,如果两次同步的时间小于5s,则不进行同步。每次同步之间间隔在5s以上,这个5s是心跳连接的间隔参数为haSendHeartbeatInterval
  3. 向master 汇报当前 salve 的CommitLog的最大偏移量,并记录这次的同步时间
  4. 从master拉取日志信息,主要就是进行消息的同步,同步出问题则关闭连接
  5. 再次同步slave的偏移量,如果最新的偏移量大于已经汇报的情况下则从步骤1重头开始

 这里分析完了run方法,然后就要分析主要的日志同步的逻辑了,这个逻辑在processReadEvent方法中

        private boolean processReadEvent() {
            int readSizeZeroTimes = 0;
            //如果读取缓存还有没读取完,则一直读取
            while (this.byteBufferRead.hasRemaining()) {
                try {
                    //从master读取消息
                    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;
        }

        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) {
                    //获取主master的最大偏移量
                    long masterPhyOffset = this.byteBufferRead.getLong(this.dispatchPosition);
                    //获取消息体
                    int bodySize = this.byteBufferRead.getInt(this.dispatchPosition + 8);
                    //获取salve的最大偏移量
                    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的 CommitLog
                        HAService.this.defaultMessageStore.appendToCommitLog(masterPhyOffset, bodyData);

                        this.byteBufferRead.position(readSocketPos);
                        //记录分发的位置
                        this.dispatchPosition += msgHeaderSize + bodySize;

                        if (!reportSlaveMaxOffsetPlus()) {
                            return false;
                        }

                        continue;
                    }
                }

                if (!this.byteBufferRead.hasRemaining()) {
                    this.reallocateByteBuffer();
                }

                break;
            }

            return true;
        }

 每一步的逻辑都是比较清楚的,这里不进行讲解。

Master用来同步日志用的HAConnection

 前面说过,在HAServiceAcceptSocketService内部类中,Master会在建立连接的时候创建HAConnection用来处理读写事件。这里主要介绍构造函数和内部类就能了解原理了。

构造函数
    public HAConnection(final HAService haService, final SocketChannel socketChannel) throws IOException {
        //指定所属的 HAService
        this.haService = haService;
        //指定的NIO的socketChannel
        this.socketChannel = socketChannel;
        //客户端的地址
        this.clientAddr = this.socketChannel.socket().getRemoteSocketAddress().toString();
        //这是为非阻塞
        this.socketChannel.configureBlocking(false);
        /**
         * 是否启动SO_LINGER
         * SO_LINGER作用
         * 设置函数close()关闭TCP连接时的行为。缺省close()的行为是,如果有数据残留在socket发送缓冲区中则系统将继续发送这些数据给对方,等待被确认,然后返回。
         *
         * https://blog.csdn.net/u012635648/article/details/80279338
         */
        this.socketChannel.socket().setSoLinger(false, -1);
        /**
         * 是否开启TCP_NODELAY
         * https://blog.csdn.net/lclwjl/article/details/80154565
         */
        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);
        //增加haService中的连接数字段
        this.haService.getConnectionCount().incrementAndGet();
    }
内部类分析
监听slave日志同步进度和同步日志的WriteSocketService

WriteSocketService监听的是OP_WRITE事件,注册的端口就是在HAService中开启的端口。直接看对应的核心方法run方法,方法有点长这里只看看核心的部分

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

            while (!this.isStopped()) {
                try {
                    this.selector.select(1000);
                    //如果slave的读请求为 -1 表示没有slave 发出写请求,不需要处理
                    if (-1 == HAConnection.this.slaveRequestOffset) {
                        Thread.sleep(10);
                        continue;
                    }
                    //nextTransferFromWhere 为-1 表示初始第一次同步,需要进行计算
                    if (-1 == this.nextTransferFromWhere) {
                        //如果slave 同步完成 则下次同步从CommitLog的最大偏移量开始同步
                        if (0 == HAConnection.this.slaveRequestOffset) {
                            //获取master 上面的 CommitLog 最大偏移量
                            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 {
                            //设置下次同步的位置,为 salve 读请求的位置
                            this.nextTransferFromWhere = HAConnection.this.slaveRequestOffset;
                        }

                        log.info("master transfer data from " + this.nextTransferFromWhere + " to slave[" + HAConnection.this.clientAddr
                            + "], and slave request " + HAConnection.this.slaveRequestOffset);
                    }
                    //上次同步是否完成
                    if (this.lastWriteOver) {
                        //获取两次写请求的周期时间
                        long interval =
                            HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now() - this.lastWriteTimestamp;
                        //如果周期大于 心跳间隔 。需要先发送一次心跳 心跳间隔为 5000毫秒
                        if (interval > HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig()
                            .getHaSendHeartbeatInterval()) {

                            // 创建请求头,心跳请求大小为12 字节
                            this.byteBufferHeader.position(0);
                            this.byteBufferHeader.limit(headerSize);
                            this.byteBufferHeader.putLong(this.nextTransferFromWhere);
                            this.byteBufferHeader.putInt(0);
                            this.byteBufferHeader.flip();
                            //进行消息同步
                            this.lastWriteOver = this.transferData();
                            if (!this.lastWriteOver)
                                continue;
                        }
                    } else {
                        //如果上次同步没有完成,则继续同步
                        this.lastWriteOver = this.transferData();
                        //如果还没同步完成则继续
                        if (!this.lastWriteOver)
                            continue;
                    }
                    //获取开始同步位置之后的消息
                    SelectMappedBufferResult selectResult =
                        HAConnection.this.haService.getDefaultMessageStore().getCommitLogData(this.nextTransferFromWhere);
                    //
                    if (selectResult != null) {
                        int size = selectResult.getSize();
                        //检查要同步消息的长度,是不是大于单次同步的最大限制 默认为 32kb
                        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 {
                        //如果需要同步的为空,则在等待100毫秒
                        HAConnection.this.haService.getWaitNotifyObject().allWaitForRunning(100);
                    }
                } catch (Exception e) {

                    HAConnection.log.error(this.getServiceName() + " service has exception.", e);
                    break;
                }
            }
        

 主要的逻辑如下:

  1. 如果slave进行了日志偏移量的汇报,判断是不是第一次的进行同步以及对应的同步进度。设置下一次的同步位置
  2. 检查上次同步是不是已经完成了,检查两次同步的周期是不是超过心跳间隔,如果是的则需要把心跳信息放到返回的头里面,然后进行消息同步。如果上次同步还没完成,则等待上次同步完成之后再继续
  3. 从Master本地读取CommitLog的最大偏移量,根据上次同步的位置开始从CommitLog获取日志信息,然后放到缓存中
  4. 如果缓存的大小大于单次同步的最大大小haTransferBatchSize默认是32kb,那么只同步32kb大小的日志。如果缓存为null,则等待100毫秒

 其中日志同步的逻辑在transferData方法中,这里就把代码贴出来

private boolean transferData() throws Exception {
            int writeSizeZeroTimes = 0;
            //心跳的头没写满,先写头
            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) {
                    //重试3次 则不再重试
                    if (++writeSizeZeroTimes >= 3) {
                        break;
                    }
                } else {
                    throw new Exception("ha master write header error < 0");
                }
            }
            //如果要同步的日志为null,则直接返回这次同步的结果是否同步完成
            if (null == this.selectMappedBufferResult) {
                return !this.byteBufferHeader.hasRemaining();
            }

            writeSizeZeroTimes = 0;

            // 填充请求体
            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) {
                        //重试3次
                        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;
        }
根据同步进度来唤醒刷盘CommitLog线程的ReadSocketService

ReadSocketService的作用主要是:根据Slave推送的日志同步进度,来唤醒HAServiceGroupTransferService然后进一步唤醒CommitLog的日志刷盘线程。这里主要看run方法和processReadEvent方法。

 public void run() {
            HAConnection.log.info(this.getServiceName() + " service started");
            //任务是否结束
            while (!this.isStopped()) {
                try {
                    //设置selector的阻塞时间
                    this.selector.select(1000);
                    //处理salver读取消息的事件
                    boolean ok = this.processReadEvent();
                    if (!ok) {
                        HAConnection.log.error("processReadEvent error");
                        break;
                    }
                    //检查此次处理时间是否超过心跳连接时间
                    long interval = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now() - this.lastReadTimestamp;
                    if (interval > HAConnection.this.haService.getDefaultMessageStore().getMessageStoreConfig().getHaHousekeepingInterval()) {
                        log.warn("ha housekeeping, found this connection[" + HAConnection.this.clientAddr + "] expired, " + interval);
                        break;
                    }
                } catch (Exception e) {
                    HAConnection.log.error(this.getServiceName() + " service has exception.", e);
                    break;
                }
            }
    ......
    }
    
    private boolean processReadEvent() {
            int readSizeZeroTimes = 0;
            //检查 读取请求缓冲是否已经满了,
            if (!this.byteBufferRead.hasRemaining()) {
                //读请求缓冲转变为读取模式。
                this.byteBufferRead.flip();
                this.processPosition = 0;
            }

            while (this.byteBufferRead.hasRemaining()) {
                try {
                    //从byteBufferRead读取
                    int readSize = this.socketChannel.read(this.byteBufferRead);
                    if (readSize > 0) {
                        readSizeZeroTimes = 0;
                        this.lastReadTimestamp = HAConnection.this.haService.getDefaultMessageStore().getSystemClock().now();
                        //读取请求缓冲的位置 如果大于处理的8字节 表示有读取的请求没处理。为什么是8个字节,因为salver向master发去拉取请求时,偏移量固定为8
                        if ((this.byteBufferRead.position() - this.processPosition) >= 8) {
                            //获取消息开始的位置
                            int pos = this.byteBufferRead.position() - (this.byteBufferRead.position() % 8);
                            //从开始位置读取8个字节,获取 slave的读请求偏移量
                            long readOffset = this.byteBufferRead.getLong(pos - 8);
                            //设置处理的位置
                            this.processPosition = pos;
                            //设置 salver读取的位置
                            HAConnection.this.slaveAckOffset = readOffset;
                            //如果slave的 读请求 偏移量小于0 表示同步完成了
                            if (HAConnection.this.slaveRequestOffset < 0) {
                                //重新设置slave的 读请求的 偏移量
                                HAConnection.this.slaveRequestOffset = readOffset;
                                log.info("slave[" + HAConnection.this.clientAddr + "] request offset " + readOffset);
                            }
                            //唤醒阻塞的线程, 在消息的主从同步选择的模式是同步的时候,会唤醒被阻塞的消息写入的线程
                            HAConnection.this.haService.notifyTransferSome(HAConnection.this.slaveAckOffset);
                        }
                    } else if (readSize == 0) {
                        //如果数据为0超过3次,表示同步完成,直接结束
                        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;
        }

 整体的逻辑如下:

  1. 每1s执行一次事件就绪选择,然后调用processReadEvent方法处理读请求,读取从服务器的拉取请求
  2. 获取slave已拉取偏移量,因为有新的从服务器反馈拉取进度,需要通知某些生产者以便返回,因为如果消息发送使用同步方式,需要等待将消息复制到从服务器,然后才返回,故这里需要唤醒相关线程去判断自己关注的消息是否已经传输完成。也就是HAServiceGroupTransferService
  3. 如果读取到的字节数等于0,则重复三次,否则结束本次读请求处理;如果读取到的字节数小于0,表示连接被断开,返回false,后续会断开该连接。

总结

 RocketMQ的主从同步之间的核心类就是HAServiceHAConnection和其中的几个子类。结合前面的那个图可以简单的理解一下。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容