RocketMQ的broker处理消息commit时,加锁应该使用自旋锁还是重入锁

讨论的主题

以下内容基于rocketmq4.7.1版本。

rocketmq的broker端关于消息发送的配置项中,有这样2项配置:

1. 是否使用重入锁

    /**
     * introduced since 4.0.x. Determine whether to use mutex reentrantLock when putting message.<br/>
     * By default it is set to false indicating using spin lock when putting message.
     */
    private boolean useReentrantLockWhenPutMessage = false;

这个默认值是false,使用自旋锁,该配置项的值,取决于下面这个配置:

2. 处理发送消息的工作线程数:

    /**
     * thread numbers for send message thread pool, since spin lock will be used by default since 4.0.x, the default
     * value is 1.
     */
    private int sendMessageThreadPoolNums = 1; //16 + Runtime.getRuntime().availableProcessors() * 4;

默认值是1,处理发送消息的线程池的数目。

本文要讨论的是,在实际业务场景中,使用自旋锁合适,还是重入锁更合适。sendMessageThreadPoolNums是使用默认的1还是后面注释的值更好。

在聊这之前,先解释下broker这个锁用在哪里。

加锁的位置

“锁”,对于程序的性能是很敏感的,有锁的地方,很容易成为程序处理能力的短板。rocketmq可以说吞吐量相当高的一款消息引擎了(不与Kafka对比)。那这个加锁的位置在哪?

在broker端收到producer客户端发送的请求过来的时候,会将该请求封装为一个任务提交到发送端线程池处理,如果要提高吞吐量,这个处理发送消息的线程池的线程数目肯定是要设置一个合适的值,才能发挥最佳性能,但是,这里设置的默认值为1???那它的原因是什么呢,往下看:

在处理发送消息的请求的时候,不论这个线程池的工作线程有多少条,在最终进行commit消息到page cache的时候,只能有一条工作线程进行操作,这是个串行的动作。

加锁的代码,在CommitLog类的putMessage方法(方法重载了,有多处,下面选择普通消息发送的,非批量的)

        // 这是加锁的位置
        putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
        try {
            long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
            // 不要忽视这个字段,影响很大的
            this.beginTimeInLock = beginLockTimestamp;

            // Here settings are stored timestamp, in order to ensure an orderly
            // global
            msg.setStoreTimestamp(beginLockTimestamp);

            if (null == mappedFile || mappedFile.isFull()) {
                mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
            }
            if (null == mappedFile) {
                log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                beginTimeInLock = 0;
                return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
            }

            // commit message 的动作
            result = mappedFile.appendMessage(msg, this.appendMessageCallback);

看下它的自旋锁和重入锁:

自旋锁,基于CAS实现:

public class PutMessageSpinLock implements PutMessageLock {
    //true: Can lock, false : in lock.
    private AtomicBoolean putMessageSpinLock = new AtomicBoolean(true);

    @Override
    public void lock() {
        boolean flag;
        do {
            flag = this.putMessageSpinLock.compareAndSet(true, false);
        }
        while (!flag);
    }

    @Override
    public void unlock() {
        this.putMessageSpinLock.compareAndSet(false, true);
    }
}

重入锁,用的就是java的重入锁:

public class PutMessageReentrantLock implements PutMessageLock {
    private ReentrantLock putMessageNormalLock = new ReentrantLock(); // NonfairSync

    @Override
    public void lock() {
        putMessageNormalLock.lock();
    }

    @Override
    public void unlock() {
        putMessageNormalLock.unlock();
    }
}

很明显前者是乐观锁,后者是悲观的互斥锁。如果是线程数低于3,我觉得使用自旋锁更好,线程数过多的话,就选择配置启用重入锁,避免很多线程在空转,浪费CPU。

现在一般都是多核处理器,发送消息的工作线程数如果为1,吞吐量应当是要低于多线程配置的。我之前在云服务器上压测,虚拟机,4C8G的配置,如果设置sendMessageThreadPoolNums为默认值1,发送1KB的消息体,单个broker的TPS勉强1000。设置重入锁,随着线程数配置的增大,TPS随着增高,64-128线程的时候超过1W5接近2W,当然,我并没有去测试在该服务器上这个性能拐点的最佳线程数。

那默认配置sendMessageThreadPoolNums的值为1的意义在哪?

sendMessageThreadPoolNums不为1可能有什么问题

如果sendMessageThreadPoolNums为1的时候,可以保证消息发送到broker后,在处理消息并commit然后刷盘,都是有序的,哪条消息最先发到broker,也是最先被写入。

如果工作线程有多条的话,在commit的时候,是串行的,只能有一条线程写入,其它处理请求的工作线程都被阻塞,等待获取锁(非公平锁),broker根本不会保证哪条线程获取到锁,那么结果就是在阻塞的这些线程在竞争到锁后,可能后发送过来的消息,先被写入。这样就无法保证broker收到的消息顺序与写入的顺序是一致的了。

不同场景配置

1.如果是分布式消息的有序性保障,那么使用默认配置:自旋锁,发送消息处理线程数为1。

2.其它场景下,如果更加关注消息发送的吞吐量,tps又上不来(可能和你的配置有关),那启用重入锁试试,并根据机器配置与实际场景进行压测,设置一个最优的消息发送处理线程池的线程数(如果懒得测,就参考注释的值CPU的4倍+16)。那它是否会影响顺序消息?完全不用担心,如果是要保证分布式场景下消息的有序性,请看第一条,如果不是,那就只要需要保障当前客户端的一个业务操作下消息的顺序,那么它肯定是有序的,因为顺序发送是同步阻塞的接口,在当前业务操作下,如果第一条消息没写入返回响应,下一条肯定也发送不了,所以这个问题就不用担心了,只要本次业务操作的消息是有序写入的即可,至于broker端其它的消息接收与写入的顺序是否一致,并不关心。

3.既需要分布式场景下消息的有序,还要足够高的发送TPS,那就只能横向扩展,多部署几个节点了。

关于这两个配置项,有其它看法,欢迎留言讨论。

后续补充,默认配置如果消息体只有1KB,只发送不消费,压测的时候tps达到了50000.上面提到压测不到1000是因为当时这个场景消息体有点大。如果消息体比较小,默认配置即可,如果是DLedger模型,同步刷盘,消息体很大等场景,tps上不来的时候,参考上面的建议,增大线程数,使用重入锁试试。

猜你喜欢

转载自blog.csdn.net/x763795151/article/details/111147255
今日推荐