RocketMQ --- Advanced

1. Advanced features

1.1, message storage

Because distributed queues have high reliability requirements, data must be stored persistently.

insert image description here

  1. The message producer sends the message
  2. MQ receives the message, persists the message, and adds a new record to the storage
  3. Return ACK to producer
  4. MQ push message to the corresponding consumer, and then wait for the consumer to return ACK
  5. If the message consumer successfully returns ack within the specified time, then MQ considers that the message consumption is successful, deletes the message in the storage, and executes step 6; if MQ does not receive the ACK within the specified time, it considers that the message consumption has failed, and will try Re-push the message and repeat steps 4, 5, and 6
  6. MQ delete message

1.1.1. Storage media

  • Relational database DB

Another open source MQ under Apache—ActiveMQ (by default, KahaDB is used for message storage) can use JDBC for message persistence, and JDBC message storage can be realized through simple xml configuration information. Because, when the amount of data in a single table reaches tens of millions, ordinary relational databases (such as Mysql) often have bottlenecks in their IO read and write performance. In terms of reliability, this scheme is very dependent on the DB. If the DB fails, the MQ message cannot be stored on the disk, which will lead to online failure.

  • File system

At present, several products commonly used in the industry (RocketMQ/Kafka/RabbitMQ) all use message flashing to the file system of the deployed virtual machine/physical machine for persistence (the flashing can generally be divided into asynchronous flashing and synchronous flashing). disc two modes). Message flushing provides a high-efficiency, high-reliability, and high-performance data persistence method for message storage. Unless the MQ machine itself is deployed or the local disk is hung up, generally there will be no failures that cannot be persisted.

1.1.2. Performance comparison

File System>Relational Database DB

1.1.3. Message storage and sending

1.1.3.1, message storage

If the disk is used properly, the speed of the disk can completely match the data transmission speed of the network. The current high-performance disk, the sequential write speed can reach 600MB/s, exceeding the transmission speed of the general network card. However, the random write speed of the disk is only about 100KB/s, which is 6000 times different from the sequential write performance! Because there is such a huge speed difference, a good message queuing system will be orders of magnitude faster than a normal message queuing system. RocketMQ messages are written sequentially, which ensures the speed of message storage.

1.1.3.2, message sending

The Linux operating system is divided into [user mode] and [kernel mode]. File operations and network operations need to involve switching between these two modes, and data copying is inevitable.

A server sends the content of the local disk file to the client, generally divided into two steps:

1) read; read local file content;

2) write; send the read content through the network.

These two seemingly simple operations actually performed 4 data replications, namely:

  1. Copy data from disk to kernel-mode memory;
  2. Copy from kernel mode memory to user mode memory;
  3. Then copy from the user mode memory to the kernel mode memory of the network driver;
  4. Finally, it is copied from the kernel mode memory of the network driver to the network card for transmission.
    insert image description here

By using mmap, memory copying to user mode can be omitted and the speed can be increased. This mechanism is implemented in Java through MappedByteBuffer

RocketMQ makes full use of the above features, which is the so-called "zero copy" technology, to improve the speed of message storage and network sending.

It should be noted here that the memory mapping method of MappedByteBuffer has several limitations, one of which is that it can only map 1.5~2G files to the virtual memory in user mode at a time, which is why RocketMQ sets a single CommitLog log data file by default for the reason of 1G

1.1.4. Message storage structure

The storage of RocketMQ messages is completed by the cooperation of ConsumeQueue and CommitLog. The real physical storage file of the message is CommitLog. ConsumeQueue is the logical queue of the message, similar to the index file of the database, which stores the address pointing to the physical storage. Each Message Queue under each Topic has a corresponding ConsumeQueue file.

insert image description here

  • CommitLog: store the metadata of the message
  • ConsumerQueue: Store the index of the message in the CommitLog
  • IndexFile: Provides a method for querying messages by key or time interval for message query. This method of searching for messages through IndexFile does not affect the main process of sending and consuming messages

1.1.5. Disk brushing mechanism

RocketMQ messages are stored on the disk, which not only ensures recovery after power failure, but also allows the amount of stored messages to exceed the memory limit. In order to improve performance, RocketMQ will try to ensure the sequential writing of the disk as much as possible. When the message is written to RocketMQ through the Producer, there are two ways to write to the disk, 分布式同步刷盘and 异步刷盘.

insert image description here

1.1.5.1, synchronous brush disk

When returning a write-success status, the message has been written to disk. The specific process is that after the message is written into the PAGECACHE of the memory, the thread for flashing the disk is immediately notified to flush the disk, and then waits for the completion of the disk flushing. After the execution of the flushing thread is completed, the waiting thread is awakened, and the status of writing the message is returned successfully.

1.1.5.2. Asynchronous flash disk

When the write success status is returned, the message may only be written into the PAGECACHE of the memory. The return of the write operation is fast and the throughput is large; when the amount of messages in the memory accumulates to a certain level, the write to disk action is uniformly triggered and written quickly.

1.1.5.3, configuration

Whether to flush disk synchronously or asynchronously is set through the flushDiskType parameter in the Broker configuration file. This parameter is configured as one of SYNC_FLUSH and ASYNC_FLUSH.

1.2. High availability mechanism

insert image description here
The RocketMQ distributed cluster achieves high availability through the cooperation of Master and Slave.

The difference between Master and Slave: In the Broker configuration file, the value of the parameter brokerId is 0 to indicate that the Broker is a Master, greater than 0 to indicate that the Broker is a Slave, and the brokerRole parameter will also indicate whether the Broker is a Master or a Slave.

The Broker of the Master role supports reading and writing, and the Broker of the Slave role only supports reading, that is, the Producer can only connect to the Broker of the Master role to write messages; the Consumer can connect to the Broker of the Master role, and can also connect to the Broker of the Slave role to read information.

1.2.1. High availability of message consumption

In the Consumer configuration file, there is no need to set whether to read from the Master or the Slave. When the Master is unavailable or busy, the Consumer will automatically switch to read from the Slave. With the mechanism of automatic switching of consumers, when a machine with a master role fails, the consumer can still read messages from the slave without affecting the consumer program. This achieves high availability on the consumer side.

1.2.2, message sending high availability

When creating a Topic, create multiple Message Queues of the Topic on multiple Broker groups (machines with the same Broker name and different brokerIds form a Broker group), so that when the Master of a Broker group is unavailable, the Masters of other groups Still available, Producer can still send messages. RocketMQ does not currently support the automatic conversion of Slave to Master. If the machine resources are insufficient and you need to convert Slave to Master, you must manually stop the Broker in the role of Slave, change the configuration file, and start the Broker with the new configuration file.

insert image description here

1.2.3. Message master-slave replication

If a Broker group has a Master and a Slave, messages need to be copied from the Master to the Slave, and there are two replication methods: synchronous and asynchronous.

1.2.3.1, synchronous replication

The synchronous replication method is to wait for both Master and Slave to write successfully before feeding back the successful writing status to the client;

In the synchronous replication mode, if the Master fails, all the backup data on the Slave is easy to restore, but synchronous replication will increase the data writing delay and reduce the system throughput.

1.2.3.2. Asynchronous replication

The asynchronous replication method is that as long as the master writes successfully, it can feedback the write success status to the client.

In the asynchronous replication mode, the system has lower latency and higher throughput, but if the Master fails, some data may be lost because it has not been written to the Slave;

1.2.3.3. Configuration

Synchronous replication and asynchronous replication are set through the brokerRole parameter in the Broker configuration file. This parameter can be set to one of the three values ​​of ASYNC_MASTER, SYNC_MASTER, and SLAVE.

1.2.3.4. Summary

insert image description here

In practical applications, it is necessary to combine the business scenarios and reasonably set the disk flushing mode and master-slave replication mode, especially the SYNC_FLUSH mode, which will significantly reduce performance due to frequent triggering of disk write actions. Normally, Master and Save should be configured as ASYNC_FLUSH for flashing disks, and between master and slave as SYNC_MASTER for copying, so that even if one machine fails, data will still be guaranteed, which is a good choice.

1.3. Load balancing

1.3.1, Producer load balancing

On the Producer side, when each instance sends a message, it will poll all message queues by default to send messages, so that the messages fall on different queues on average. And because the queue can be scattered in different brokers, the message is sent to different brokers, as shown in the following figure:

insert image description here
The labels on the arrow lines in the figure represent the sequence. The publisher will send the first message to Queue 0, then the second message to Queue 1, and so on.

1.3.2, Consumer load balancing

1.3.2.1, cluster mode

In the cluster consumption mode, each message only needs to be delivered to an instance under the Consumer Group that subscribes to this topic. RocketMQ uses active pulling to pull and consume messages. When pulling, it is necessary to specify which message queue to pull.

Whenever the number of instances changes, a load balancing of all instances will be triggered. At this time, queues will be evenly allocated to each instance according to the number of queues and the number of instances.

The default allocation algorithm is AllocateMessageQueueAveragely, as shown below:
insert image description here

There is another average algorithm, AllocateMessageQueueAveragelyByCircle, which also allocates each queue equally, but divides the queues in a circular manner, as shown in the figure below: It should be noted that in the cluster mode, only
insert image description here
one instance is allowed to be allocated to the queue. This is because if multiple instances consume messages from a queue at the same time, which messages to pull are actively controlled by the consumer, which will cause the same message to be consumed multiple times under different instances, so the algorithm is that a queue only divides Given a consumer instance, a consumer instance can be assigned to different queues at the same time.

By adding consumer instances to share the consumption of the queue, it can play a role in horizontally expanding the consumption capacity. When an instance goes offline, load balancing will be triggered again. At this time, the originally allocated queue will be allocated to other instances to continue consumption.

However, if the number of consumer instances is greater than the total number of message queues, the extra consumer instances will not be assigned to queues, and messages will not be consumed, and they will not be able to share the load. Therefore, it is necessary to control the total number of queues to be greater than or equal to the number of consumers.

1.3.2.2, broadcast mode

Since the broadcast mode requires that a message needs to be delivered to all consumer instances under a consumer group, there is no saying that the message is allocated for consumption.

In terms of implementation, one of the differences is that when consumers allocate queues, all consumers are allocated to all queues.
insert image description here

1.4. Message retry

1.4.1. Retry of sequential messages

For sequential messages, when consumers fail to consume messages, the message queue RocketMQ will automatically retry messages continuously (each interval is 1 second), at this time, the application will be blocked for message consumption. Therefore, when using sequential messages, it is important to ensure that the application can monitor and handle consumption failures in a timely manner to avoid blocking.

1.4.2. Retry of out-of-order messages

For out-of-order messages (ordinary, timed, delayed, and transactional messages), when consumers fail to consume messages, you can achieve the result of message retry by setting the return status.

The retry of out-of-order messages is only effective for the cluster consumption mode; the broadcast mode does not provide the failure retry feature, that is, after the consumption fails, the failed message will not be retried, and the new message will continue to be consumed.

1.4.2.1, the number of retries

The message queue RocketMQ allows each message to retry up to 16 times by default, and the interval between each retry is as follows:

how many times to retry Time since last retry how many times to retry Time since last retry
1 10 seconds 9 7 minutes
2 30 seconds 10 8 minutes
3 1 minute 11 9 minutes
4 2 minutes 12 10 minutes
5 3 minutes 13 20 minutes
6 4 minutes 14 30 minutes
7 5 minutes 15 1 hour
8 6 minutes 16 2 hours

If the message still fails after 16 retries, the message will not be delivered. If calculated strictly according to the above retry interval, a message will be retried 16 times in the next 4 hours and 46 minutes under the premise that consumption fails all the time. Messages beyond this time range will not retry delivery .

Notice:

  • No matter how many times a message is retried, the Message ID of these retried messages will not change.

1.4.2.2, configuration method

After the consumption fails, retry the configuration method

In the cluster consumption mode, it is expected to retry the message after the message consumption fails, which needs to be explicitly configured in the implementation of the message listener interface (choose one of the three methods):

  • Return Action.ReconsumeLater (recommended)
  • returns Null
  • Throw an exception
public class MessageListenerImpl implements MessageListener {
    
    
    @Override
    public Action consume(Message message, ConsumeContext context) {
    
    
        //处理消息
        doConsumeMessage(message);
        //方式1:返回 Action.ReconsumeLater,消息将重试
        return Action.ReconsumeLater;
        //方式2:返回 null,消息将重试
        return null;
        //方式3:直接抛出异常, 消息将重试
        throw new RuntimeException("Consumer Message exceotion");
    }
}

After the consumption fails, the configuration method is not retried

In the cluster consumption mode, it is expected that the message will not be retried after the message fails, and it is necessary to catch the exception that may be thrown in the consumption logic, and finally return Action.CommitMessage, after which this message will not be retried.

public class MessageListenerImpl implements MessageListener {
    
    
    @Override
    public Action consume(Message message, ConsumeContext context) {
    
    
        try {
    
    
            doConsumeMessage(message);
        } catch (Throwable e) {
    
    
            //捕获消费逻辑中的所有异常,并返回 Action.CommitMessage;
            return Action.CommitMessage;
        }
        //消息处理正常,直接返回 Action.CommitMessage;
        return Action.CommitMessage;
    }
}

Maximum number of retries for custom messages

The message queue RocketMQ allows the consumer to set the maximum number of retries when starting, and the retry interval will follow the following strategy:

  • If the maximum number of retries is less than or equal to 16, the retry interval is the same as that described in the above table.
  • The maximum number of retries is greater than 16, and the interval between retries exceeding 16 times is 2 hours each time.
Properties properties = new Properties();
//配置对应 Group ID 的最大消息重试次数为 20 次
properties.put(PropertyKeyConst.MaxReconsumeTimes,"20");
Consumer consumer =ONSFactory.createConsumer(properties);

Notice:

  • The setting of the maximum number of message retries is valid for all Consumer instances under the same Group ID.
  • If MaxReconsumeTimes is set for only one of the two Consumer instances under the same Group ID, the configuration will take effect for both Consumer instances.
  • The configuration takes effect by overwriting, that is, the last consumer instance started will overwrite the configuration of the previously started instance

Get message retries

After the consumer receives the message, it can obtain the number of retries of the message as follows:

public class MessageListenerImpl implements MessageListener {
    
    
    @Override
    public Action consume(Message message, ConsumeContext context) {
    
    
        //获取消息的重试次数
        System.out.println(message.getReconsumeTimes());
        return Action.CommitMessage;
    }
}

1.5. Dead letter queue

When a message fails to be consumed for the first time, the message queue RocketMQ will automatically retry the message; after reaching the maximum number of retries, if the consumption still fails, it indicates that the consumer cannot consume the message correctly under normal circumstances. At this time, the message queue RocketMQ The message will not be discarded immediately, but will be sent to the special queue corresponding to the consumer.

In the message queue RocketMQ, messages that cannot be consumed under normal circumstances are called Dead-Letter Messages, and special queues that store dead-letter messages are called Dead-Letter Queues.

1.5.1. Dead letter characteristics

Dead letter messages have the following properties

  • It will no longer be consumed by consumers normally.
  • The validity period is the same as normal messages, both are 3 days, and will be automatically deleted after 3 days. Therefore, please deal with the dead letter message in time within 3 days after it is generated.

A dead letter queue has the following characteristics:

  • A dead letter queue corresponds to a Group ID, not to a single consumer instance.
  • If a Group ID does not generate a dead letter message, the message queue RocketMQ will not create a corresponding dead letter queue for it.
  • A dead letter queue contains all dead letter messages generated by the corresponding Group ID, no matter which Topic the message belongs to.

1.5.2. View dead letter information

  1. Query the topic information of the dead letter queue in the console
    insert image description here

  2. Query dead letter messages according to the subject in the message interface

insert image description here

  1. Choose to resend message

A message enters the dead letter queue, which means that some factors prevent consumers from consuming the message normally, so you usually need to handle it specially. After troubleshooting suspicious factors and solving the problem, you can resend the message on the RocketMQ console of the message queue, so that consumers can consume it again.

1.6. Consumption is idempotent

After the message queue RocketMQ consumer receives the message, it is necessary to perform idempotent processing on the message according to the unique key in the business.

1.6.1. The necessity of consumption idempotence

In Internet applications, especially when the network is unstable, the messages of the message queue RocketMQ may be repeated. This repetition can be summarized as follows:

  • Duplicate message when sending

    • When a message has been successfully sent to the server and persisted, there is a sudden network disconnection or the client crashes, causing the server to fail to respond to the client. If the producer realizes that the message has failed to send and tries to send the message again, the consumer will receive two messages with the same content and the same Message ID.
  • Duplicate message when delivered

    • In the message consumption scenario, the message has been delivered to the consumer and the business processing has been completed. When the client responds to the server, the network is disconnected. In order to ensure that the message is consumed at least once, the message queue RocketMQ server will try to deliver the previously processed message again after the network is restored, and the consumer will subsequently receive two messages with the same content and the same Message ID.
  • Message duplication during load balancing (including but not limited to network jitter, Broker restart, and subscriber application restart)

    • When the Broker or client of the message queue RocketMQ restarts, expands or shrinks, Rebalance will be triggered, and consumers may receive duplicate messages at this time.

1.6.2. Processing method

Because Message ID may conflict (duplicate), it is not recommended to use Message ID as the basis for truly safe idempotent processing. The best way is to use the unique identifier of the business as the key basis for idempotent processing, and the unique identifier of the business can be set through the message Key:

Message message = new Message();
message.setKey("ORDERID_100");
SendResult sendResult = producer.send(message);

When the subscriber receives the message, it can perform idempotent processing according to the Key of the message:

consumer.subscribe("ons_test", "*", new MessageListener() {
    
    
    public Action consume(Message message, ConsumeContext context) {
    
    
        String key = message.getKey()
        // 根据业务唯一标识的 key 做幂等处理
    }
});

2. Source code analysis

2.1. Environment construction

Dependency tool

  • JDK :1.8+
  • Maven
  • I understand the idea

2.1.1, source code pull

From the official warehouse https://github.com/apache/rocketmq clone or downloadsource code.

Source code directory structure:

  • broker: broker module (broke startup process)

  • client : message client, including message producer and message consumer related classes

  • common : public package

  • dev : developer information (not source code)

  • distribution : Deployment instance folder (not source code)

  • example: RocketMQ example code

  • filter : basic class related to message filtering

  • filtersrv: Message filtering server implementation related classes (Filter startup process)

  • logappender: log implementation related classes

  • namesrv: NameServer implements related classes (NameServer starts the process)

  • openmessageing: message open standard

  • remoting: remote communication module, given to Netty

  • srcutil: service tool class

  • store: Message storage implementation related classes

  • style: checkstyle related implementation

  • test: test related classes

  • tools: tool class, monitoring command related implementation class

2.1.2. Import IDEA

Execute the installation

clean install -Dmaven.test.skip=true

2.1.3. Debugging

Create confa configuration folder, distributioncopy from broker.confand logback_broker.xmlandlogback_namesrv.xml

2.1.3.1. Start NameServer

  • Expand the namesrv module, right-click NamesrvStartup.java

  • Configure ROCKETMQ_HOME

insert image description here

  • Restart

    console print result

The Name Server boot success. serializeType=JSON

2.1.3.2, start Broker

  • broker.confConfiguration file content
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
# namesrvAddr地址
namesrvAddr=127.0.0.1:9876
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
autoCreateTopicEnable=true

# 存储路径
storePathRootDir=E:\\RocketMQ\\data\\rocketmq\\dataDir
# commitLog路径
storePathCommitLog=E:\\RocketMQ\\data\\rocketmq\\dataDir\\commitlog
# 消息队列存储路径
storePathConsumeQueue=E:\\RocketMQ\\data\\rocketmq\\dataDir\\consumequeue
# 消息索引存储路径
storePathIndex=E:\\RocketMQ\\data\\rocketmq\\dataDir\\index
# checkpoint文件路径
storeCheckpoint=E:\\RocketMQ\\data\\rocketmq\\dataDir\\checkpoint
# abort文件存储路径
abortFile=E:\\RocketMQ\\data\\rocketmq\\dataDir\\abort
  • Create data folderdataDir
  • start BrokerStartup, configure broker.confandROCKETMQ_HOME

2.1.3.3. Send message

  • into the example moduleorg.apache.rocketmq.example.quickstart
  • Specify the Namesrv address
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.setNamesrvAddr("127.0.0.1:9876");
  • run mainmethod, send message

2.1.3.4, consumption news

  • into the example moduleorg.apache.rocketmq.example.quickstart
  • Specify the Namesrv address
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
consumer.setNamesrvAddr("127.0.0.1:9876");
  • Run mainmethod, consume message

2.2、NameServer

2.2.1, architecture design

The design idea of ​​message middleware is generally based on the mechanism of topic subscription and publishing. The message producer (Producer) sends a certain topic to the message server. The message server is responsible for persistent storage of the message, and the message consumer (Consumer) subscribes to the topic of interest. The message server pushes the message to the consumer (Push mode) or the consumer actively pulls it to the message server (Pull mode) according to the subscription information (routing information), so as to realize the decoupling of the message producer and the message consumer. In order to avoid the paralysis of the entire system caused by a single point of failure of the message server, multiple message servers are usually deployed to share the storage of messages. How does the message producer know which message server to send the message to? If a message server is down, how can the message producer perceive it without restarting the service?

NameServer is designed to solve the above problems.

insert image description here

The Broker message server registers with all NameServers when it starts, and the message producer (Producer) obtains the Broker server address list from the NameServer before sending a message, and then selects a server from the list to send according to the load balancing algorithm. NameServer maintains a long-term connection with each Broker, and checks whether the Broker is alive at an interval of 30 seconds. If it detects that the Broker is down, it will be deleted from the routing registry. But the route change will not notify the message producer immediately. The purpose of this design is to reduce the complexity of NameServer implementation and provide a fault-tolerant mechanism at the message sending end to ensure the availability of message sending.

The high availability of the NameServer itself is achieved by deploying multiple NameServers, but they do not communicate with each other, that is, the data between the NameServer servers at a certain moment is not exactly the same, but this will not have any impact on message sending. This is also a highlight of the NameServer design. In short, the RocketMQ design pursues simplicity and efficiency.

2.2.2. Startup process

insert image description here

Startup class:org.apache.rocketmq.namesrv.NamesrvStartup

2.2.2.1, Step 1

Parse the configuration file, fill in the attribute values ​​of NameServerConfig and NettyServerConfig, and create NamesrvController

Code: NamesrvController#createNamesrvController

//创建NamesrvConfig
final NamesrvConfig namesrvConfig = new NamesrvConfig();
//创建NettyServerConfig
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
//设置启动端口号
nettyServerConfig.setListenPort(9876);
//解析启动-c参数
if (commandLine.hasOption('c')) {
    
    
    String file = commandLine.getOptionValue('c');
    if (file != null) {
    
    
        InputStream in = new BufferedInputStream(new FileInputStream(file));
        properties = new Properties();
        properties.load(in);
        MixAll.properties2Object(properties, namesrvConfig);
        MixAll.properties2Object(properties, nettyServerConfig);

        namesrvConfig.setConfigStorePath(file);

        System.out.printf("load config properties file OK, %s%n", file);
        in.close();
    }
}
//解析启动-p参数
if (commandLine.hasOption('p')) {
    
    
    InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
    MixAll.printObjectProperties(console, namesrvConfig);
    MixAll.printObjectProperties(console, nettyServerConfig);
    System.exit(0);
}
//将启动参数填充到namesrvConfig,nettyServerConfig
MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);

//创建NameServerController
final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);

NamesrvConfig Property

private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
private String productEnvName = "center";
private boolean clusterTest = false;
private boolean orderMessageEnable = false;
  • rocketmqHome: rocketmq home directory

  • kvConfig: NameServer stores the persistent path of KV configuration properties

  • configStorePath: nameServer default configuration file path

  • orderMessageEnable: whether to support order messages

NettyServerConfig Properties

private int listenPort = 8888;
private int serverWorkerThreads = 8;
private int serverCallbackExecutorThreads = 0;
private int serverSelectorThreads = 3;
private int serverOnewaySemaphoreValue = 256;
private int serverAsyncSemaphoreValue = 64;
private int serverChannelMaxIdleTimeSeconds = 120;
private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
private boolean serverPooledByteBufAllocatorEnable = true;
private boolean useEpollNativeSelector = false;
  • listenPort: NameServer listening port, the value will be initialized to 9876 by default

  • serverWorkerThreads: the number of threads in the Netty business thread pool

  • serverCallbackExecutorThreads: The number of threads in the Netty public task thread pool. Netty network design will create different thread pools according to business types, such as processing message sending, message consumption, and heartbeat detection. If the business type is not registered with the thread pool, it will be executed by the public thread pool.

  • serverSelectorThreads: The number of IO thread pools, mainly the number of NameServer and Broker side parsing requests and returning the corresponding threads. These threads mainly process network requests, parse request packets, and then forward them to various business thread pools to complete specific operations , and then return the result to the caller;

  • serverOnewaySemaphoreValue: send oneway message request concurrent reading (Broker side parameter);

  • serverAsyncSemaphoreValue: the maximum concurrency of sending asynchronous messages;

  • serverChannelMaxIdleTimeSeconds : The maximum idle time of the network connection, the default is 120s.

  • serverSocketSndBufSize: The size of the network socket send buffer.

  • serverSocketRcvBufSize: The buffer size of the network receiving end.

  • serverPooledByteBufAllocatorEnable: whether to enable ByteBuffer cache;

  • useEpollNativeSelector: Whether to enable the Epoll IO model.

2.2.2.2, Step 2

Create a NamesrvController instance based on the startup properties and initialize the instance. The NameServerController instance is the NameServer core controller

Code: NamesrvController#initialize

public boolean initialize() {
    
    
	//加载KV配置
    this.kvConfigManager.load();
	//创建NettyServer网络处理对象
    this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
	//开启定时任务:每隔10s扫描一次Broker,移除不活跃的Broker
    this.remotingExecutor =
        Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
    this.registerProcessor();
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            NamesrvController.this.routeInfoManager.scanNotActiveBroker();
        }
    }, 5, 10, TimeUnit.SECONDS);
	//开启定时任务:每隔10min打印一次KV配置
	this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    
    

        @Override
        public void run() {
    
    
            NamesrvController.this.kvConfigManager.printAllPeriodically();
        }
    }, 1, 10, TimeUnit.MINUTES);
    return true;
}

2.2.2.3, Step 3

Before the JVM process is closed, the thread pool is closed to release resources in time

Code: NamesrvStartup#start

//注册JVM钩子函数代码
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
    
    
    @Override
    public Void call() throws Exception {
    
    
        //释放资源
        controller.shutdown();
        return null;
    }
}));

2.2.3, routing management

The main role of NameServer is to provide message producers and message consumers with routing information about topics, so NameServer needs to store basic routing information and manage Broker nodes, including routing registration and routing deletion.

2.2.3.1, routing meta information

Code: RouteInfoManager

private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

insert image description here

  • topicQueueTable: Topic message queue routing information, load balancing is performed according to the routing table when the message is sent

  • brokerAddrTable: Broker basic information, including brokerName, cluster name, active and standby Broker addresses

  • clusterAddrTable: Broker cluster information, storing all Broker names in the cluster

  • brokerLiveTable: Broker status information, NameServer will replace this information every time it receives a heartbeat packet

  • filterServerTable: A list of FilterServers on the Broker, used for class-mode message filtering.

RocketMQ is based on the scheduled publishing mechanism. A Topic has multiple message queues, and a Broker creates 4 read queues and 4 write queues for each topic. Multiple Brokers form a cluster, and the cluster consists of the same multiple Brokers to form a Master-Slave architecture. A brokerId of 0 represents a Master, and a value greater than 0 represents a Slave. lastUpdateTimestamp in BrokerLiveInfo stores the time when the Broker heartbeat packet was last received.

insert image description here

insert image description here

2.2.3.2, route registration

2.2.3.2.1, send heartbeat packet

insert image description here

RocketMQ route registration is realized through the heartbeat function of Broker and NameServer. When the Broker starts, it sends heartbeat information to all NameServers in the cluster, and sends heartbeat packets to all NameServers in the cluster every 30s. When the NameServer receives the heartbeat packet, it updates the lastUpdataTimeStamp information of BrokerLiveInfo in the brokerLiveTable cache, and then the NameServer scans the brokerLiveTable every 10s. If If no heartbeat packet is received for 120 seconds in a row, NameServer will remove the routing information of Broker and close the Socket connection at the same time.

Code: BrokerController#start

//注册Broker信息
this.registerBrokerAll(true, false, true);
//每隔30s上报Broker信息到NameServer
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);
        }
    }
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)),  TimeUnit.MILLISECONDS);

Code: BrokerOuterAPI#registerBrokerAll

//获得nameServer地址信息
List<String> nameServerAddressList = this.remotingClient.getNameServerAddressList();
//遍历所有nameserver列表
if (nameServerAddressList != null && nameServerAddressList.size() > 0) {
    
    

    //封装请求头
    final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
    requestHeader.setBrokerAddr(brokerAddr);
    requestHeader.setBrokerId(brokerId);
    requestHeader.setBrokerName(brokerName);
    requestHeader.setClusterName(clusterName);
    requestHeader.setHaServerAddr(haServerAddr);
    requestHeader.setCompressed(compressed);
	//封装请求体
    RegisterBrokerBody requestBody = new RegisterBrokerBody();
    requestBody.setTopicConfigSerializeWrapper(topicConfigWrapper);
    requestBody.setFilterServerList(filterServerList);
    final byte[] body = requestBody.encode(compressed);
    final int bodyCrc32 = UtilAll.crc32(body);
    requestHeader.setBodyCrc32(bodyCrc32);
    final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
    for (final String namesrvAddr : nameServerAddressList) {
    
    
        brokerOuterExecutor.execute(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    //分别向NameServer注册
                    RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
                    if (result != null) {
    
    
                        registerBrokerResultList.add(result);
                    }

                    log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
                } catch (Exception e) {
    
    
                    log.warn("registerBroker Exception, {}", namesrvAddr, e);
                } finally {
    
    
                    countDownLatch.countDown();
                }
            }
        });
    }

    try {
    
    
        countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS);
    } catch (InterruptedException e) {
    
    
    }
}

Code: BrokerOutAPI#registerBroker

if (oneway) {
    
    
    try {
    
    
        this.remotingClient.invokeOneway(namesrvAddr, request, timeoutMills);
    } catch (RemotingTooMuchRequestException e) {
    
    
        // Ignore
    }
    return null;
}
RemotingCommand response = this.remotingClient.invokeSync(namesrvAddr, request, timeoutMills);
2.2.3.2.2, processing heartbeat packets

insert image description here

org.apache.rocketmq.namesrv.processor.DefaultRequestProcessorThe network processing class parses the request type, and if the request type is REGISTER_BROKER , the request is forwarded toRouteInfoManager#regiesterBroker

代码:DefaultRequestProcessor#processRequest

//判断是注册Broker信息
case RequestCode.REGISTER_BROKER:
	Version brokerVersion = MQVersion.value2Version(request.getVersion());
	if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
    
    
	    return this.registerBrokerWithFilterServer(ctx, request);
	} else {
    
    
        //注册Broker信息
	    return this.registerBroker(ctx, request);
	}

代码:DefaultRequestProcessor#registerBroker

RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
    requestHeader.getClusterName(),
    requestHeader.getBrokerAddr(),
    requestHeader.getBrokerName(),
    requestHeader.getBrokerId(),
    requestHeader.getHaServerAddr(),
    topicConfigWrapper,
    null,
    ctx.channel()
);

Code: RouteInfoManager#registerBroker

Maintain routing information

//加锁
this.lock.writeLock().lockInterruptibly();
//维护clusterAddrTable
Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
if (null == brokerNames) {
    
    
    brokerNames = new HashSet<String>();
    this.clusterAddrTable.put(clusterName, brokerNames);
}
brokerNames.add(brokerName);
//维护brokerAddrTable
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
//第一次注册,则创建brokerData
if (null == brokerData) {
    
    
    registerFirst = true;
    brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
    this.brokerAddrTable.put(brokerName, brokerData);
}
//非第一次注册,更新Broker
Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
while (it.hasNext()) {
    
    
    Entry<Long, String> item = it.next();
    if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
    
    
        it.remove();
    }
}
String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
registerFirst = registerFirst || (null == oldAddr);
//维护topicQueueTable
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());
            }
        }
    }
}

Code: RouteInfoManager#createAndUpdateQueueData

private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) {
    
    
    //创建QueueData
	QueueData queueData = new QueueData();
	queueData.setBrokerName(brokerName);
	queueData.setWriteQueueNums(topicConfig.getWriteQueueNums());
	queueData.setReadQueueNums(topicConfig.getReadQueueNums());
	queueData.setPerm(topicConfig.getPerm());
	queueData.setTopicSynFlag(topicConfig.getTopicSysFlag());
	//获得topicQueueTable中队列集合
	List<QueueData> queueDataList = this.topicQueueTable.get(topicConfig.getTopicName());
    //topicQueueTable为空,则直接添加queueData到队列集合
	if (null == queueDataList) {
    
    
	    queueDataList = new LinkedList<QueueData>();
	    queueDataList.add(queueData);
	    this.topicQueueTable.put(topicConfig.getTopicName(), queueDataList);
	    log.info("new topic registered, {} {}", topicConfig.getTopicName(), queueData);
	} else {
    
    
        //判断是否是新的队列
	    boolean addNewOne = true;
	    Iterator<QueueData> it = queueDataList.iterator();
	    while (it.hasNext()) {
    
    
	        QueueData qd = it.next();
            //如果brokerName相同,代表不是新的队列
	        if (qd.getBrokerName().equals(brokerName)) {
    
    
	            if (qd.equals(queueData)) {
    
    
	                addNewOne = false;
	        } else {
    
    
	                    log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), qd,
	                        queueData);
	                    it.remove();
	                }
	            }
	        }
		//如果是新的队列,则添加队列到queueDataList
        if (addNewOne) {
    
    
            queueDataList.add(queueData);
        }
    }
}
//维护brokerLiveTable
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,new BrokerLiveInfo(
    System.currentTimeMillis(),
    topicConfigWrapper.getDataVersion(),
    channel,
    haServerAddr));
//维护filterServerList
if (filterServerList != null) {
    
    
    if (filterServerList.isEmpty()) {
    
    
        this.filterServerTable.remove(brokerAddr);
    } else {
    
    
        this.filterServerTable.put(brokerAddr, filterServerList);
    }
}

if (MixAll.MASTER_ID != brokerId) {
    
    
    String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
    if (masterAddr != null) {
    
    
        BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
        if (brokerLiveInfo != null) {
    
    
            result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
            result.setMasterAddr(masterAddr);
        }
    }
}

2.2.3.3. Route deletion

BrokerSend a heartbeat packet every 30s NameServer. The heartbeat packet contains address, BrokerIdname , cluster name, and associated list. But if there is a downtime and the heartbeat packet cannot be received, how to eliminate these invalid ones at this time ?BrokerBrokerBrokerBrokerFilterServerBrokerNameServerNameServerBroker

NameServerbrokerLiveTableThe status table will be scanned every 10s . If the timestamp BrokerLiveof the lastUpdateTimestamp exceeds 120s from the current time, it will be considered Brokerinvalid, remove it Broker, close Brokerthe connection, and update topicQueueTable, brokerAddrTable, brokerLiveTable, at the same time filterServerTable.

RocketMQ has two trigger points to delete routing information :

  • NameServer periodically scans the brokerLiveTable to detect the time difference between the last heartbeat packet and the current system. If the time exceeds 120s, the broker needs to be removed.
  • When Broker is shut down normally, it will execute the unregisterBroker command

The method of route deletion in these two ways is the same, that is, delete the information related to the broker from the relevant routing table.

insert image description here
Code: NamesrvController#initialize

//每隔10s扫描一次为活跃Broker
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    
    

    @Override
    public void run() {
    
    
        NamesrvController.this.routeInfoManager.scanNotActiveBroker();
    }
}, 5, 10, TimeUnit.SECONDS);

Code: RouteInfoManager#scanNotActiveBroker

public void scanNotActiveBroker() {
    
    
    //获得brokerLiveTable
    Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
    //遍历brokerLiveTable
    while (it.hasNext()) {
    
    
        Entry<String, BrokerLiveInfo> next = it.next();
        long last = next.getValue().getLastUpdateTimestamp();
        //如果收到心跳包的时间距当时时间是否超过120s
        if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
    
    
            //关闭连接
            RemotingUtil.closeChannel(next.getValue().getChannel());
            //移除broker
            it.remove();
            //维护路由表
            this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
        }
    }
}

Code: RouteInfoManager#onChannelDestroy

//申请写锁,根据brokerAddress从brokerLiveTable和filterServerTable移除
this.lock.writeLock().lockInterruptibly();
this.brokerLiveTable.remove(brokerAddrFound);
this.filterServerTable.remove(brokerAddrFound);
//维护brokerAddrTable
String brokerNameFound = null;
boolean removeBrokerName = false;
Iterator<Entry<String, BrokerData>> itBrokerAddrTable =this.brokerAddrTable.entrySet().iterator();
//遍历brokerAddrTable
while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
    
    
    BrokerData brokerData = itBrokerAddrTable.next().getValue();
    //遍历broker地址
    Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
    while (it.hasNext()) {
    
    
        Entry<Long, String> entry = it.next();
        Long brokerId = entry.getKey();
        String brokerAddr = entry.getValue();
        //根据broker地址移除brokerAddr
        if (brokerAddr.equals(brokerAddrFound)) {
    
    
            brokerNameFound = brokerData.getBrokerName();
            it.remove();
            log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
                brokerId, brokerAddr);
            break;
        }
    }
	//如果当前主题只包含待移除的broker,则移除该topic
    if (brokerData.getBrokerAddrs().isEmpty()) {
    
    
        removeBrokerName = true;
        itBrokerAddrTable.remove();
        log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
            brokerData.getBrokerName());
    }
}
//维护clusterAddrTable
if (brokerNameFound != null && removeBrokerName) {
    
    
    Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
    //遍历clusterAddrTable
    while (it.hasNext()) {
    
    
        Entry<String, Set<String>> entry = it.next();
        //获得集群名称
        String clusterName = entry.getKey();
        //获得集群中brokerName集合
        Set<String> brokerNames = entry.getValue();
        //从brokerNames中移除brokerNameFound
        boolean removed = brokerNames.remove(brokerNameFound);
        if (removed) {
    
    
            log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
                brokerNameFound, clusterName);

            if (brokerNames.isEmpty()) {
    
    
                log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
                    clusterName);
                //如果集群中不包含任何broker,则移除该集群
                it.remove();
            }

            break;
        }
    }
}
//维护topicQueueTable队列
if (removeBrokerName) {
    
    
    //遍历topicQueueTable
    Iterator<Entry<String, List<QueueData>>> itTopicQueueTable =
        this.topicQueueTable.entrySet().iterator();
    while (itTopicQueueTable.hasNext()) {
    
    
        Entry<String, List<QueueData>> entry = itTopicQueueTable.next();
        //主题名称
        String topic = entry.getKey();
        //队列集合
        List<QueueData> queueDataList = entry.getValue();
		//遍历该主题队列
        Iterator<QueueData> itQueueData = queueDataList.iterator();
        while (itQueueData.hasNext()) {
    
    
            //从队列中移除为活跃broker信息
            QueueData queueData = itQueueData.next();
            if (queueData.getBrokerName().equals(brokerNameFound)) {
    
    
                itQueueData.remove();
                log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
                    topic, queueData);
            }
        }
		//如果该topic的队列为空,则移除该topic
        if (queueDataList.isEmpty()) {
    
    
            itTopicQueueTable.remove();
            log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
                topic);
        }
    }
}
//释放写锁
finally {
    
    
    this.lock.writeLock().unlock();
}

2.2.3.4, route discovery

RocketMQ route discovery is non-real-time. When the topic route changes, the NameServer will not actively push it to the client, but the client will periodically pull the latest route of the topic.

代码:DefaultRequestProcessor#getRouteInfoByTopic

public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    
    
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    final GetRouteInfoRequestHeader requestHeader =
        (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
	//调用RouteInfoManager的方法,从路由表topicQueueTable、brokerAddrTable、filterServerTable中分别填充TopicRouteData的List<QueueData>、List<BrokerData>、filterServer
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
	//如果找到主题对应你的路由信息并且该主题为顺序消息,则从NameServer KVConfig中获取关于顺序消息相关的配置填充路由信息
    if (topicRouteData != null) {
    
    
        if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
    
    
            String orderTopicConf =
                this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
                    requestHeader.getTopic());
            topicRouteData.setOrderTopicConf(orderTopicConf);
        }

        byte[] content = topicRouteData.encode();
        response.setBody(content);
        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
        return response;
    }

    response.setCode(ResponseCode.TOPIC_NOT_EXIST);
    response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
        + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
    return response;
}

2.2.4 Summary

insert image description here

2.3、Producer

The code of the message producer is in the client module. Compared with RocketMQ, the message producer is the client and the provider of the message.

insert image description here

2.3.1, methods and properties

2.3.1.1. Introduction of main methods

insert image description here

//创建主题
void createTopic(final String key, final String newTopic, final int queueNum) throws MQClientException;
//根据时间戳从队列中查找消息偏移量
long searchOffset(final MessageQueue mq, final long timestamp)
//查找消息队列中最大的偏移量
long maxOffset(final MessageQueue mq) throws MQClientException;
//查找消息队列中最小的偏移量
long minOffset(final MessageQueue mq) 
//根据偏移量查找消息
MessageExt viewMessage(final String offsetMsgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException;
//根据条件查找消息
QueryResult queryMessage(final String topic, final String key, final int maxNum, final long begin,  final long end) throws MQClientException, InterruptedException;
//根据消息ID和主题查找消息
MessageExt viewMessage(String topic,String msgId) throws RemotingException, MQBrokerException, InterruptedException, MQClientException;

insert image description here

//启动
void start() throws MQClientException;
//关闭
void shutdown();
//查找该主题下所有消息
List<MessageQueue> fetchPublishMessageQueues(final String topic) throws MQClientException;
//同步发送消息
SendResult send(final Message msg) throws MQClientException, RemotingException, MQBrokerException,  InterruptedException;
//同步超时发送消息
SendResult send(final Message msg, final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException;
//异步发送消息
void send(final Message msg, final SendCallback sendCallback) throws MQClientException,RemotingException, InterruptedException;
//异步超时发送消息
void send(final Message msg, final SendCallback sendCallback, final long timeout) throws MQClientException, RemotingException, InterruptedException;
//发送单向消息
void sendOneway(final Message msg) throws MQClientException, RemotingException, InterruptedException;
//选择指定队列同步发送消息
SendResult send(final Message msg, final MessageQueue mq) throws MQClientException, RemotingException, MQBrokerException, InterruptedException;
//选择指定队列异步发送消息
void send(final Message msg, final MessageQueue mq, final SendCallback sendCallback)   throws MQClientException, RemotingException, InterruptedException;
//选择指定队列单项发送消息
void sendOneway(final Message msg, final MessageQueue mq) throws MQClientException,    RemotingException, InterruptedException;
//批量发送消息
SendResult send(final Collection<Message> msgs) throws MQClientException, RemotingException, MQBrokerException,InterruptedException;

2.3.1.2, attribute introduction

insert image description here

producerGroup:生产者所属组
createTopicKey:默认Topic
defaultTopicQueueNums:默认主题在每一个Broker队列数量
sendMsgTimeout:发送消息默认超时时间,默认3s
compressMsgBodyOverHowmuch:消息体超过该值则启用压缩,默认4k
retryTimesWhenSendFailed:同步方式发送消息重试次数,默认为2,总共执行3次
retryTimesWhenSendAsyncFailed:异步方法发送消息重试次数,默认为2
retryAnotherBrokerWhenNotStoreOK:消息重试时选择另外一个Broker时,是否不等待存储结果就返回,默认为false
maxMessageSize:允许发送的最大消息长度,默认为4M

2.3.2. Startup process

insert image description here
Code: DefaultMQProducerImpl#start

//检查生产者组是否满足要求
this.checkConfig();
//更改当前instanceName为进程ID
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
    
    
    this.defaultMQProducer.changeInstanceNameToPID();
}
//获得MQ客户端实例
this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
  • There is only one MQClientManager instance in the entire JVM, and an MQClientInstance cache table is maintained

  • ConcurrentMap<String/* clientId */, MQClientInstance> factoryTable = new ConcurrentHashMap<String,MQClientInstance>();

  • Only one MQClientInstance will be created for the same clientId.

  • MQClientInstance encapsulates the RocketMQ network processing API, and is a network channel for message producers and message consumers to deal with NameServer and Broker

Code: MQClientManager#getAndCreateMQClientInstance

public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientConfig,  RPCHook rpcHook) {
    
    
    //构建客户端ID
    String clientId = clientConfig.buildMQClientId();
    //根据客户端ID或者客户端实例
    MQClientInstance instance = this.factoryTable.get(clientId);
    //实例如果为空就创建新的实例,并添加到实例表中
    if (null == instance) {
    
    
        instance = new MQClientInstance(clientConfig.cloneClientConfig(),this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
        MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
        if (prev != null) {
    
    
            instance = prev;
            log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
        } else {
    
    
            log.info("Created new MQClientInstance for clientId:[{}]", clientId);
        }
    }
    return instance;
}

Code: DefaultMQProducerImpl#start

//注册当前生产者到到MQClientInstance管理中,方便后续调用网路请求
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
if (!registerOK) {
    
    
    this.serviceState = ServiceState.CREATE_JUST;
    throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
        + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), null);
}
//启动生产者
if (startFactory) {
    
    
    mQClientFactory.start();
}

2.3.3. Message sending

insert image description here

Code: DefaultMQProducerImpl#send(Message msg)

//发送消息
public SendResult send(Message msg) {
    
    
    return send(msg, this.defaultMQProducer.getSendMsgTimeout());
}

代码:DefaultMQProducerImpl#send(Message msg,long timeout)

//发送消息,默认超时时间为3s
public SendResult send(Message msg,long timeout){
    
    
    return this.sendDefaultImpl(msg, CommunicationMode.SYNC, null, timeout);
}

Code: DefaultMQProducerImpl#sendDefaultImpl

//校验消息
Validators.checkMessage(msg, this.defaultMQProducer);

2.3.3.1, verification message

Code: Validators#checkMessage

public static void checkMessage(Message msg, DefaultMQProducer defaultMQProducer)
    throws MQClientException {
    
    
    //判断是否为空
    if (null == msg) {
    
    
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message is null");
    }
    // 校验主题
    Validators.checkTopic(msg.getTopic());
		
    // 校验消息体
    if (null == msg.getBody()) {
    
    
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body is null");
    }

    if (0 == msg.getBody().length) {
    
    
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL, "the message body length is zero");
    }

    if (msg.getBody().length > defaultMQProducer.getMaxMessageSize()) {
    
    
        throw new MQClientException(ResponseCode.MESSAGE_ILLEGAL,
            "the message body size over max value, MAX: " + defaultMQProducer.getMaxMessageSize());
    }
}

2.3.3.2, find the route

代码:DefaultMQProducerImpl#tryToFindTopicPublishInfo

private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
    
    
    //从缓存中获得主题的路由信息
    TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
    //路由信息为空,则从NameServer获取路由
    if (null == topicPublishInfo || !topicPublishInfo.ok()) {
    
    
        this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
    }

    if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
    
    
        return topicPublishInfo;
    } else {
    
    
        //如果未找到当前主题的路由信息,则用默认主题继续查找
        this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
        topicPublishInfo = this.topicPublishInfoTable.get(topic);
        return topicPublishInfo;
    }
}

insert image description here
Code: TopicPublishInfo

public class TopicPublishInfo {
    
    
    private boolean orderTopic = false;	//是否是顺序消息
    private boolean haveTopicRouterInfo = false; 
    private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>();	//该主题消息队列
    private volatile ThreadLocalIndex sendWhichQueue = new ThreadLocalIndex();//每选择一次消息队列,该值+1
    private TopicRouteData topicRouteData;//关联Topic路由元信息
}

Code: MQClientInstance#updateTopicRouteInfoFromNameServer

TopicRouteData topicRouteData;
//使用默认主题从NameServer获取路由信息
if (isDefault && defaultMQProducer != null) {
    
    
    topicRouteData = this.mQClientAPIImpl.getDefaultTopicRouteInfoFromNameServer(defaultMQProducer.getCreateTopicKey(),1000 * 3);
    if (topicRouteData != null) {
    
    
        for (QueueData data : topicRouteData.getQueueDatas()) {
    
    
            int queueNums = Math.min(defaultMQProducer.getDefaultTopicQueueNums(), data.getReadQueueNums());
            data.setReadQueueNums(queueNums);
            data.setWriteQueueNums(queueNums);
        }
    }
} else {
    
    
    //使用指定主题从NameServer获取路由信息
    topicRouteData = this.mQClientAPIImpl.getTopicRouteInfoFromNameServer(topic, 1000 * 3);
}

Code: MQClientInstance#updateTopicRouteInfoFromNameServer

//判断路由是否需要更改
TopicRouteData old = this.topicRouteTable.get(topic);
boolean changed = topicRouteDataIsChange(old, topicRouteData);
if (!changed) {
    
    
    changed = this.isNeedUpdateTopicRouteInfo(topic);
} else {
    
    
    log.info("the topic[{}] route info changed, old[{}] ,new[{}]", topic, old, topicRouteData);
}

Code: MQClientInstance#updateTopicRouteInfoFromNameServer

if (changed) {
    
    
    //将topicRouteData转换为发布队列
    TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
    publishInfo.setHaveTopicRouterInfo(true);
    //遍历生产
    Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
    while (it.hasNext()) {
    
    
        Entry<String, MQProducerInner> entry = it.next();
        MQProducerInner impl = entry.getValue();
        if (impl != null) {
    
    
            //生产者不为空时,更新publishInfo信息
            impl.updateTopicPublishInfo(topic, publishInfo);
        }
    }
}

Code: MQClientInstance#topicRouteData2TopicPublishInfo

public static TopicPublishInfo topicRouteData2TopicPublishInfo(final String topic, final TopicRouteData route) {
    
    
    	//创建TopicPublishInfo对象
        TopicPublishInfo info = new TopicPublishInfo();
    	//关联topicRoute
        info.setTopicRouteData(route);
    	//顺序消息,更新TopicPublishInfo
        if (route.getOrderTopicConf() != null && route.getOrderTopicConf().length() > 0) {
    
    
            String[] brokers = route.getOrderTopicConf().split(";");
            for (String broker : brokers) {
    
    
                String[] item = broker.split(":");
                int nums = Integer.parseInt(item[1]);
                for (int i = 0; i < nums; i++) {
    
    
                    MessageQueue mq = new MessageQueue(topic, item[0], i);
                    info.getMessageQueueList().add(mq);
                }
            }

            info.setOrderTopic(true);
        } else {
    
    
            //非顺序消息更新TopicPublishInfo
            List<QueueData> qds = route.getQueueDatas();
            Collections.sort(qds);
            //遍历topic队列信息
            for (QueueData qd : qds) {
    
    
                //是否是写队列
                if (PermName.isWriteable(qd.getPerm())) {
    
    
                    BrokerData brokerData = null;
                    //遍历写队列Broker
                    for (BrokerData bd : route.getBrokerDatas()) {
    
    
                        //根据名称获得读队列对应的Broker
                        if (bd.getBrokerName().equals(qd.getBrokerName())) {
    
    
                        brokerData = bd;
                        break;
                    }
                }

                if (null == brokerData) {
    
    
                    continue;
                }

                if (!brokerData.getBrokerAddrs().containsKey(MixAll.MASTER_ID)) {
    
    
                    continue;
                }
				//封装TopicPublishInfo写队列
                for (int i = 0; i < qd.getWriteQueueNums(); i++) {
    
    
                    MessageQueue mq = new MessageQueue(topic, qd.getBrokerName(), i);
                    info.getMessageQueueList().add(mq);
                }
            }
        }

        info.setOrderTopic(false);
    }
	//返回TopicPublishInfo对象
    return info;
}

2.3.3.3. Select queue

  • Broker failure delay mechanism is not enabled by default

代码:TopicPublishInfo#selectOneMessageQueue(lastBrokerName)

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
    
    
    //第一次选择队列
    if (lastBrokerName == null) {
    
    
        return selectOneMessageQueue();
    } else {
    
    
        //sendWhichQueue
        int index = this.sendWhichQueue.getAndIncrement();
        //遍历消息队列集合
        for (int i = 0; i < this.messageQueueList.size(); i++) {
    
    
            //sendWhichQueue自增后取模
            int pos = Math.abs(index++) % this.messageQueueList.size();
            if (pos < 0)
                pos = 0;
            //规避上次Broker队列
            MessageQueue mq = this.messageQueueList.get(pos);
            if (!mq.getBrokerName().equals(lastBrokerName)) {
    
    
                return mq;
            }
        }
        //如果以上情况都不满足,返回sendWhichQueue取模后的队列
        return selectOneMessageQueue();
    }
}

Code: TopicPublishInfo#selectOneMessageQueue()

//第一次选择队列
public MessageQueue selectOneMessageQueue() {
    
    
    //sendWhichQueue自增
    int index = this.sendWhichQueue.getAndIncrement();
    //对队列大小取模
    int pos = Math.abs(index) % this.messageQueueList.size();
    if (pos < 0)
        pos = 0;
    //返回对应的队列
    return this.messageQueueList.get(pos);
}
  • Enable Broker failure delay mechanism
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
    
    
    //Broker故障延迟机制
    if (this.sendLatencyFaultEnable) {
    
    
        try {
    
    
            //对sendWhichQueue自增
            int index = tpInfo.getSendWhichQueue().getAndIncrement();
            //对消息队列轮询获取一个队列
            for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
    
    
                int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                if (pos < 0)
                    pos = 0;
                MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                //验证该队列是否可用
                if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
    
    
                    //可用
                    if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
                        return mq;
                }
            }
			//从规避的Broker中选择一个可用的Broker
            final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
            //获得Broker的写队列集合
            int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
            if (writeQueueNums > 0) {
    
    
                //获得一个队列,指定broker和队列ID并返回
                final MessageQueue mq = tpInfo.selectOneMessageQueue();
                if (notBestBroker != null) {
    
    
                    mq.setBrokerName(notBestBroker);
                    mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                }
                return mq;
            } else {
    
    
                latencyFaultTolerance.remove(notBestBroker);
            }
        } catch (Exception e) {
    
    
            log.error("Error occurred when selecting message queue", e);
        }

        return tpInfo.selectOneMessageQueue();
    }

    return tpInfo.selectOneMessageQueue(lastBrokerName);
}

Broker failure delay mechanism core class

insert image description here

  • Delay Mechanism Interface Specification
public interface LatencyFaultTolerance<T> {
    
    
    //更新失败条目
    void updateFaultItem(final T name, final long currentLatency, final long notAvailableDuration);
	//判断Broker是否可用
    boolean isAvailable(final T name);
	//移除Fault条目
    void remove(final T name);
	//尝试从规避的Broker中选择一个可用的Broker
    T pickOneAtLeast();
}
  • FaultItem: failed entry
class FaultItem implements Comparable<FaultItem> {
    
    
    //条目唯一键,这里为brokerName
    private final String name;
    //本次消息发送延迟
    private volatile long currentLatency;
    //故障规避开始时间
    private volatile long startTimestamp;
}
  • Message Failure Policy
public class MQFaultStrategy {
    
    
   //根据currentLatency本地消息发送延迟,从latencyMax尾部向前找到第一个比currentLatency小的索引,如果没有找到,返回0
	private long[] latencyMax = {
    
    50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
    //根据这个索引从notAvailableDuration取出对应的时间,在该时长内,Broker设置为不可用
	private long[] notAvailableDuration = {
    
    0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
}

Principle analysis

Code: DefaultMQProducerImpl#sendDefaultImpl

sendResult = this.sendKernelImpl(msg, 
                                 mq, 
                                 communicationMode, 
                                 sendCallback, 
                                 topicPublishInfo, 
                                 timeout - costTime);
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);

If an exception occurs during the above sending process, callDefaultMQProducerImpl#updateFaultItem

public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
    
    
    //参数一:broker名称
    //参数二:本次消息发送延迟时间
    //参数三:是否隔离
    this.mqFaultStrategy.updateFaultItem(brokerName, currentLatency, isolation);
}

Code: MQFaultStrategy#updateFaultItem

public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
    
    
    if (this.sendLatencyFaultEnable) {
    
    
        //计算broker规避的时长
        long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
        //更新该FaultItem规避时长
        this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
    }
}

Code: MQFaultStrategy#computeNotAvailableDuration

private long computeNotAvailableDuration(final long currentLatency) {
    
    
    //遍历latencyMax
    for (int i = latencyMax.length - 1; i >= 0; i--) {
    
    
        //找到第一个比currentLatency的latencyMax值
        if (currentLatency >= latencyMax[i])
            return this.notAvailableDuration[i];
    }
    //没有找到则返回0
    return 0;
}

Code: LatencyFaultToleranceImpl#updateFaultItem

public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
    
    
    //获得原FaultItem
    FaultItem old = this.faultItemTable.get(name);
    //为空新建faultItem对象,设置规避时长和开始时间
    if (null == old) {
    
    
        final FaultItem faultItem = new FaultItem(name);
        faultItem.setCurrentLatency(currentLatency);
        faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);

        old = this.faultItemTable.putIfAbsent(name, faultItem);
        if (old != null) {
    
    
            old.setCurrentLatency(currentLatency);
            old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
        }
    } else {
    
    
        //更新规避时长和开始时间
        old.setCurrentLatency(currentLatency);
        old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
    }
}

2.3.3.4, send message

Message sending API core entry DefaultMQProducerImpl#sendKernelImpl

private SendResult sendKernelImpl(
    final Message msg,	//待发送消息
    final MessageQueue mq,	//消息发送队列
    final CommunicationMode communicationMode,		//消息发送内模式
    final SendCallback sendCallback,	pp	//异步消息回调函数
    final TopicPublishInfo topicPublishInfo,	//主题路由信息
    final long timeout	//超时时间
    )

Code: DefaultMQProducerImpl#sendKernelImpl

//获得broker网络地址信息
String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
if (null == brokerAddr) {
    
    
    //没有找到从NameServer更新broker网络地址信息
    tryToFindTopicPublishInfo(mq.getTopic());
    brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
}
//为消息分类唯一ID
if (!(msg instanceof MessageBatch)) {
    
    
    MessageClientIDSetter.setUniqID(msg);
}

boolean topicWithNamespace = false;
if (null != this.mQClientFactory.getClientConfig().getNamespace()) {
    
    
    msg.setInstanceId(this.mQClientFactory.getClientConfig().getNamespace());
    topicWithNamespace = true;
}
//消息大小超过4K,启用消息压缩
int sysFlag = 0;
boolean msgBodyCompressed = false;
if (this.tryToCompressMessage(msg)) {
    
    
    sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
    msgBodyCompressed = true;
}
//如果是事务消息,设置消息标记MessageSysFlag.TRANSACTION_PREPARED_TYPE
final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
    
    
    sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
}
//如果注册了消息发送钩子函数,在执行消息发送前的增强逻辑
if (this.hasSendMessageHook()) {
    
    
    context = new SendMessageContext();
    context.setProducer(this);
    context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
    context.setCommunicationMode(communicationMode);
    context.setBornHost(this.defaultMQProducer.getClientIP());
    context.setBrokerAddr(brokerAddr);
    context.setMessage(msg);
    context.setMq(mq);
    context.setNamespace(this.defaultMQProducer.getNamespace());
    String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
    if (isTrans != null && isTrans.equals("true")) {
    
    
        context.setMsgType(MessageType.Trans_Msg_Half);
    }

    if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) {
    
    
        context.setMsgType(MessageType.Delay_Msg);
    }
    this.executeSendMessageHookBefore(context);
}

Code: SendMessageHook

public interface SendMessageHook {
    
    
    String hookName();

    void sendMessageBefore(final SendMessageContext context);

    void sendMessageAfter(final SendMessageContext context);
}

Code: DefaultMQProducerImpl#sendKernelImpl

//构建消息发送请求包
SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
//生产者组
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
//主题
requestHeader.setTopic(msg.getTopic());
//默认创建主题Key
requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
//该主题在单个Broker默认队列树
requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
//队列ID
requestHeader.setQueueId(mq.getQueueId());
//消息系统标记
requestHeader.setSysFlag(sysFlag);
//消息发送时间
requestHeader.setBornTimestamp(System.currentTimeMillis());
//消息标记
requestHeader.setFlag(msg.getFlag());
//消息扩展信息
requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
//消息重试次数
requestHeader.setReconsumeTimes(0);
requestHeader.setUnitMode(this.isUnitMode());
//是否是批量消息等
requestHeader.setBatch(msg instanceof MessageBatch);
if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
    
    
    String reconsumeTimes = MessageAccessor.getReconsumeTime(msg);
    if (reconsumeTimes != null) {
    
    
        requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes));
        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME);
    }

    String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg);
    if (maxReconsumeTimes != null) {
    
    
        requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes));
        MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES);
    }
}
case ASYNC:		//异步发送
    Message tmpMessage = msg;
    boolean messageCloned = false;
    if (msgBodyCompressed) {
    
    
        //If msg body was compressed, msgbody should be reset using prevBody.
        //Clone new message using commpressed message body and recover origin massage.
        //Fix bug:https://github.com/apache/rocketmq-externals/issues/66
        tmpMessage = MessageAccessor.cloneMessage(msg);
        messageCloned = true;
        msg.setBody(prevBody);
    }

    if (topicWithNamespace) {
    
    
        if (!messageCloned) {
    
    
            tmpMessage = MessageAccessor.cloneMessage(msg);
            messageCloned = true;
        }
        msg.setTopic(NamespaceUtil.withoutNamespace(msg.getTopic(), 
                                                    this.defaultMQProducer.getNamespace()));
    }

		long costTimeAsync = System.currentTimeMillis() - beginStartTime;
		if (timeout < costTimeAsync) {
    
    
		    throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
		}
		sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
        			brokerAddr,
        			mq.getBrokerName(),
        			tmpMessage,
        			requestHeader,
        			timeout - costTimeAsync,
        			communicationMode,
        			sendCallback,
        			topicPublishInfo,
        			this.mQClientFactory,
        			this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),
        			context,
        			this);
    	break;
case ONEWAY:
case SYNC:		//同步发送
    long costTimeSync = System.currentTimeMillis() - beginStartTime;
        if (timeout < costTimeSync) {
    
    
            throw new RemotingTooMuchRequestException("sendKernelImpl call timeout");
        }
        sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
            brokerAddr,
            mq.getBrokerName(),
            msg,
            requestHeader,
            timeout - costTimeSync,
            communicationMode,
            context,
            this);
        break;
    default:
        assert false;
        break;
}
//如果注册了钩子函数,则发送完毕后执行钩子函数
if (this.hasSendMessageHook()) {
    
    
    context.setSendResult(sendResult);
    this.executeSendMessageHookAfter(context);
}

2.3.4. Batch message sending

insert image description here
Batch message sending is to package multiple messages of the same topic together and send them to the message server, reducing the number of network calls and improving network transmission efficiency. Of course, it is not that the more messages sent in the same batch, the better. The judgment is based on the length of a single message. If the content of a single message is relatively long, sending multiple messages in a package will affect the response time of other threads sending messages. And the total length of a single batch of messages cannot exceed DefaultMQProducer#maxMessageSize.

The problem to be solved in batch message sending is how to encode these messages so that the server can correctly decode the message content of each message.

Code: DefaultMQProducer#send

public SendResult send(Collection<Message> msgs) 
    throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
    
    
    //压缩消息集合成一条消息,然后发送出去
    return this.defaultMQProducerImpl.send(batch(msgs));
}

Code: DefaultMQProducer#batch

private MessageBatch batch(Collection<Message> msgs) throws MQClientException {
    
    
    MessageBatch msgBatch;
    try {
    
    
        //将集合消息封装到MessageBatch
        msgBatch = MessageBatch.generateFromList(msgs);
        //遍历消息集合,检查消息合法性,设置消息ID,设置Topic
        for (Message message : msgBatch) {
    
    
            Validators.checkMessage(message, this);
            MessageClientIDSetter.setUniqID(message);
            message.setTopic(withNamespace(message.getTopic()));
        }
        //压缩消息,设置消息body
        msgBatch.setBody(msgBatch.encode());
    } catch (Exception e) {
    
    
        throw new MQClientException("Failed to initiate the MessageBatch", e);
    }
    //设置msgBatch的topic
    msgBatch.setTopic(withNamespace(msgBatch.getTopic()));
    return msgBatch;
}

2.4, message storage

2.4.1, message storage core class

DefaultMessageStore

private final MessageStoreConfig messageStoreConfig;	//消息配置属性
private final CommitLog commitLog;		//CommitLog文件存储的实现类
private final ConcurrentMap<String/* topic */, ConcurrentMap<Integer/* queueId */, ConsumeQueue>> consumeQueueTable;	//消息队列存储缓存表,按照消息主题分组
private final FlushConsumeQueueService flushConsumeQueueService;	//消息队列文件刷盘线程
private final CleanCommitLogService cleanCommitLogService;	//清除CommitLog文件服务
private final CleanConsumeQueueService cleanConsumeQueueService;	//清除ConsumerQueue队列文件服务
private final IndexService indexService;	//索引实现类
private final AllocateMappedFileService allocateMappedFileService;	//MappedFile分配服务
private final ReputMessageService reputMessageService;//CommitLog消息分发,根据CommitLog文件构建ConsumerQueue、IndexFile文件
private final HAService haService;	//存储HA机制
private final ScheduleMessageService scheduleMessageService;	//消息服务调度线程
private final StoreStatsService storeStatsService;	//消息存储服务
private final TransientStorePool transientStorePool;	//消息堆外内存缓存
private final BrokerStatsManager brokerStatsManager;	//Broker状态管理器
private final MessageArrivingListener messageArrivingListener;	//消息拉取长轮询模式消息达到监听器
private final BrokerConfig brokerConfig;	//Broker配置类
private StoreCheckpoint storeCheckpoint;	//文件刷盘监测点
private final LinkedList<CommitLogDispatcher> dispatcherList;	//CommitLog文件转发请求

2.4.2. Message storage process

insert image description here
Message storage entry: DefaultMessageStore#putMessage

//判断Broker角色如果是从节点,则无需写入
if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
    
    
        long value = this.printTimes.getAndIncrement();
        if ((value % 50000) == 0) {
    
    
            log.warn("message store is slave mode, so putMessage is forbidden ");
        }

    return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
}

//判断当前写入状态如果是正在写入,则不能继续
if (!this.runningFlags.isWriteable()) {
    
    
        long value = this.printTimes.getAndIncrement();
    	return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
} else {
    
    
    this.printTimes.set(0);
}
//判断消息主题长度是否超过最大限制
if (msg.getTopic().length() > Byte.MAX_VALUE) {
    
    
    log.warn("putMessage message topic length too long " + msg.getTopic().length());
    return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null);
}
//判断消息属性长度是否超过限制
if (msg.getPropertiesString() != null && msg.getPropertiesString().length() > Short.MAX_VALUE) {
    
    
    log.warn("putMessage message properties length too long " + msg.getPropertiesString().length());
    return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null);
}
//判断系统PageCache缓存去是否占用
if (this.isOSPageCacheBusy()) {
    
    
    return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null);
}

//将消息写入CommitLog文件
PutMessageResult result = this.commitLog.putMessage(msg);

Code: CommitLog#putMessage

//记录消息存储时间
msg.setStoreTimestamp(beginLockTimestamp);

//判断如果mappedFile如果为空或者已满,创建新的mappedFile文件
if (null == mappedFile || mappedFile.isFull()) {
    
    
    mappedFile = this.mappedFileQueue.getLastMappedFile(0); 
}
//如果创建失败,直接返回
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);
}

//写入消息到mappedFile中
result = mappedFile.appendMessage(msg, this.appendMessageCallback);

Code: MappedFile#appendMessagesInner

//获得文件的写入指针
int currentPos = this.wrotePosition.get();

//如果指针大于文件大小则直接返回
if (currentPos < this.fileSize) {
    
    
    //通过writeBuffer.slice()创建一个与MappedFile共享的内存区,并设置position为当前指针
    ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
    byteBuffer.position(currentPos);
    AppendMessageResult result = null;
    if (messageExt instanceof MessageExtBrokerInner) {
    
    
       	//通过回调方法写入
        result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
    } else if (messageExt instanceof MessageExtBatch) {
    
    
        result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
    } else {
    
    
        return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
    }
    this.wrotePosition.addAndGet(result.getWroteBytes());
    this.storeTimestamp = result.getStoreTimestamp();
    return result;
}

Code: CommitLog#doAppend

//文件写入位置
long wroteOffset = fileFromOffset + byteBuffer.position();
//设置消息ID
this.resetByteBuffer(hostHolder, 8);
String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset);

//获得该消息在消息队列中的偏移量
keyBuilder.setLength(0);
keyBuilder.append(msgInner.getTopic());
keyBuilder.append('-');
keyBuilder.append(msgInner.getQueueId());
String key = keyBuilder.toString();
Long queueOffset = CommitLog.this.topicQueueTable.get(key);
if (null == queueOffset) {
    
    
    queueOffset = 0L;
    CommitLog.this.topicQueueTable.put(key, queueOffset);
}

//获得消息属性长度
final byte[] propertiesData =msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8);

final int propertiesLength = propertiesData == null ? 0 : propertiesData.length;

if (propertiesLength > Short.MAX_VALUE) {
    
    
    log.warn("putMessage message properties length too long. length={}", propertiesData.length);
    return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED);
}

//获得消息主题大小
final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8);
final int topicLength = topicData.length;

//获得消息体大小
final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length;
//计算消息总长度
final int msgLen = calMsgLength(bodyLength, topicLength, propertiesLength);

Code: CommitLog#calMsgLength

protected static int calMsgLength(int bodyLength, int topicLength, int propertiesLength) {
    
    
    final int msgLen = 4 //TOTALSIZE
        + 4 //MAGICCODE  
        + 4 //BODYCRC
        + 4 //QUEUEID
        + 4 //FLAG
        + 8 //QUEUEOFFSET
        + 8 //PHYSICALOFFSET
        + 4 //SYSFLAG
        + 8 //BORNTIMESTAMP
        + 8 //BORNHOST
        + 8 //STORETIMESTAMP
        + 8 //STOREHOSTADDRESS
        + 4 //RECONSUMETIMES
        + 8 //Prepared Transaction Offset
        + 4 + (bodyLength > 0 ? bodyLength : 0) //BODY
        + 1 + topicLength //TOPIC
        + 2 + (propertiesLength > 0 ? propertiesLength : 0) //propertiesLength
        + 0;
    return msgLen;
}

Code: CommitLog#doAppend

//消息长度不能超过4M
if (msgLen > this.maxMessageSize) {
    
    
    CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength
        + ", maxMessageSize: " + this.maxMessageSize);
    return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED);
}

//消息是如果没有足够的存储空间则新创建CommitLog文件
if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) {
    
    
    this.resetByteBuffer(this.msgStoreItemMemory, maxBlank);
    // 1 TOTALSIZE
    this.msgStoreItemMemory.putInt(maxBlank);
    // 2 MAGICCODE
    this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE);
    // 3 The remaining space may be any value
    // Here the length of the specially set maxBlank
    final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
    byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank);
    return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(),
        queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
}

//将消息存储到ByteBuffer中,返回AppendMessageResult
final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
// Write messages to the queue buffer
byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);
AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, 
                                                     msgLen, msgId,msgInner.getStoreTimestamp(), 
                                                     queueOffset, 
                                                     CommitLog.this.defaultMessageStore.now() 
                                                     -beginTimeMills);
switch (tranType) {
    
    
    case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
    case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
        break;
    case MessageSysFlag.TRANSACTION_NOT_TYPE:
    case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
        //更新消息队列偏移量
        CommitLog.this.topicQueueTable.put(key, ++queueOffset);
        break;
    default:
        break;
}

Code: CommitLog#putMessage

//释放锁
putMessageLock.unlock();
//刷盘
handleDiskFlush(result, putMessageResult, msg);
//执行HA主从同步
handleHA(result, putMessageResult, msg);

2.4.3. Store files

  • commitLog: message storage directory
  • config: some configuration information during operation
  • consumerqueue: message consumption queue storage directory
  • index: message index file storage directory
  • abort: If there is a change in the file life Broker closed abnormally
  • checkpoint: File checkpoint, which stores the timestamp of the last disk flushing of the CommitLog file, the time of the last disk flushing of consumerquueue, and the timestamp of the last disk flushing of the index index file.

2.4.4 Store file memory map

RocketMQ improves IO access performance by using memory-mapped files. Whether it is CommitLog, ConsumerQueue or IndexFile, a single file is designed to be of fixed length. If a new file is created after a file is full, the file name will be the first message of the file. The corresponding global physical offset.

2.4.4.1、MappedFileQueue

String storePath;	//存储目录
int mappedFileSize;	// 单个文件大小
CopyOnWriteArrayList<MappedFile> mappedFiles;	//MappedFile文件集合
AllocateMappedFileService allocateMappedFileService;	//创建MapFile服务类
long flushedWhere = 0;		//当前刷盘指针
long committedWhere = 0;	//当前数据提交指针,内存中ByteBuffer当前的写指针,该值大于等于flushWhere
  • Query MappedFile based on storage time
public MappedFile getMappedFileByTime(final long timestamp) {
    
    
    Object[] mfs = this.copyMappedFiles(0);
	
    if (null == mfs)
        return null;
	//遍历MappedFile文件数组
    for (int i = 0; i < mfs.length; i++) {
    
    
        MappedFile mappedFile = (MappedFile) mfs[i];
        //MappedFile文件的最后修改时间大于指定时间戳则返回该文件
        if (mappedFile.getLastModifiedTimestamp() >= timestamp) {
    
    
            return mappedFile;
        }
    }

    return (MappedFile) mfs[mfs.length - 1];
}
  • Find MappedFile according to message offset offset
public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
    
    
    try {
    
    
        //获得第一个MappedFile文件
        MappedFile firstMappedFile = this.getFirstMappedFile();
        //获得最后一个MappedFile文件
        MappedFile lastMappedFile = this.getLastMappedFile();
        //第一个文件和最后一个文件均不为空,则进行处理
        if (firstMappedFile != null && lastMappedFile != null) {
    
    
            if (offset < firstMappedFile.getFileFromOffset() || 
                offset >= lastMappedFile.getFileFromOffset() + this.mappedFileSize) {
    
    
            } else {
    
    
                //获得文件索引
                int index = (int) ((offset / this.mappedFileSize) 
                                   - (firstMappedFile.getFileFromOffset() / this.mappedFileSize));
                MappedFile targetFile = null;
                try {
    
    
                    //根据索引返回目标文件
                    targetFile = this.mappedFiles.get(index);
                } catch (Exception ignored) {
    
    
                }

                if (targetFile != null && offset >= targetFile.getFileFromOffset()
                    && offset < targetFile.getFileFromOffset() + this.mappedFileSize) {
    
    
                    return targetFile;
                }

                for (MappedFile tmpMappedFile : this.mappedFiles) {
    
    
                    if (offset >= tmpMappedFile.getFileFromOffset()
                        && offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) {
    
    
                        return tmpMappedFile;
                    }
                }
            }

            if (returnFirstOnNotFound) {
    
    
                return firstMappedFile;
            }
        }
    } catch (Exception e) {
    
    
        log.error("findMappedFileByOffset Exception", e);
    }

    return null;
}
  • Get the minimum offset of the storage file
public long getMinOffset() {
    
    

    if (!this.mappedFiles.isEmpty()) {
    
    
        try {
    
    
            return this.mappedFiles.get(0).getFileFromOffset();
        } catch (IndexOutOfBoundsException e) {
    
    
            //continue;
        } catch (Exception e) {
    
    
            log.error("getMinOffset has exception.", e);
        }
    }
    return -1;
}
  • Get the maximum offset of the storage file
public long getMaxOffset() {
    
    
    MappedFile mappedFile = getLastMappedFile();
    if (mappedFile != null) {
    
    
        return mappedFile.getFileFromOffset() + mappedFile.getReadPosition();
    }
    return 0;
}
  • Returns the current write pointer of the storage file
public long getMaxWrotePosition() {
    
    
    MappedFile mappedFile = getLastMappedFile();
    if (mappedFile != null) {
    
    
        return mappedFile.getFileFromOffset() + mappedFile.getWrotePosition();
    }
    return 0;
}

2.4.4.2、MappedFile

int OS_PAGE_SIZE = 1024 * 4;		//操作系统每页大小,默认4K
AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);	//当前JVM实例中MappedFile虚拟内存
AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);	//当前JVM实例中MappedFile对象个数
AtomicInteger wrotePosition = new AtomicInteger(0);	//当前文件的写指针
AtomicInteger committedPosition = new AtomicInteger(0);	//当前文件的提交指针
AtomicInteger flushedPosition = new AtomicInteger(0);	//刷写到磁盘指针
int fileSize;	//文件大小
FileChannel fileChannel;	//文件通道	
ByteBuffer writeBuffer = null;	//堆外内存ByteBuffer
TransientStorePool transientStorePool = null;	//堆外内存池
String fileName;	//文件名称
long fileFromOffset;	//该文件的处理偏移量
File file;	//物理文件
MappedByteBuffer mappedByteBuffer;	//物理文件对应的内存映射Buffer
volatile long storeTimestamp = 0;	//文件最后一次内容写入时间
boolean firstCreateInQueue = false;	//是否是MappedFileQueue队列中第一个文件

MappedFile initialization

  • not turned on transientStorePoolEnable. transientStorePoolEnable=trueTo trueindicate that the data is first stored in the off-heap memory, and then Committhe data is submitted to the memory-mapped Buffer through the thread, and then the data in Flushthe memory map is Bufferpersisted to the disk through the thread.
private void init(final String fileName, final int fileSize) throws IOException {
    
    
    this.fileName = fileName;
    this.fileSize = fileSize;
    this.file = new File(fileName);
    this.fileFromOffset = Long.parseLong(this.file.getName());
    boolean ok = false;
	
    ensureDirOK(this.file.getParent());

    try {
    
    
        this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
        this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
        TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
        TOTAL_MAPPED_FILES.incrementAndGet();
        ok = true;
    } catch (FileNotFoundException e) {
    
    
        log.error("create file channel " + this.fileName + " Failed. ", e);
        throw e;
    } catch (IOException e) {
    
    
        log.error("map file " + this.fileName + " Failed. ", e);
        throw e;
    } finally {
    
    
        if (!ok && this.fileChannel != null) {
    
    
            this.fileChannel.close();
        }
    }
}

turn ontransientStorePoolEnable

public void init(final String fileName, final int fileSize,
    final TransientStorePool transientStorePool) throws IOException {
    
    
    init(fileName, fileSize);
    this.writeBuffer = transientStorePool.borrowBuffer();	//初始化writeBuffer
    this.transientStorePool = transientStorePool;
}

MappedFile Submission

Submit data to FileChannel, and commitLeastPages is the minimum number of pages submitted this time. If the data to be submitted is less than commitLeastPages, this submission operation will not be performed. If the writeBuffer is empty, the writePosition pointer is returned directly, and there is no need to perform a commit operation. The main body of the table name commit operation is writeBuffer.

public int commit(final int commitLeastPages) {
    
    
    if (writeBuffer == null) {
    
    
        //no need to commit data to file channel, so just regard wrotePosition as committedPosition.
        return this.wrotePosition.get();
    }
    //判断是否满足提交条件
    if (this.isAbleToCommit(commitLeastPages)) {
    
    
        if (this.hold()) {
    
    
            commit0(commitLeastPages);
            this.release();
        } else {
    
    
            log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
        }
    }

    // 所有数据提交后,清空缓冲区
    if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
    
    
        this.transientStorePool.returnBuffer(writeBuffer);
        this.writeBuffer = null;
    }

    return this.committedPosition.get();
}

MappedFile#isAbleToCommit

Determine whether to execute the commit operation, and return true if the file is full; if commitLeastpages is greater than 0, compare the difference between writePosition and the pointer commitPosition submitted last time, divide it by OS_PAGE_SIZE to get the current number of dirty pages, and return true if it is greater than commitLeastPages, if commitLeastpages is less than 0, which means commit as long as there are dirty pages.

protected boolean isAbleToCommit(final int commitLeastPages) {
    
    
    //已经刷盘指针
    int flush = this.committedPosition.get();
    //文件写指针
    int write = this.wrotePosition.get();
	//写满刷盘
    if (this.isFull()) {
    
    
        return true;
    }

    if (commitLeastPages > 0) {
    
    
        //文件内容达到commitLeastPages页数,则刷盘
        return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= commitLeastPages;
    }

    return write > flush;
}

MappedFile#commit0

The implementation of the specific submission, first create the shared buffer area of ​​the WriteBuffer area, then roll back the newly created position to the last submitted position (commitPosition), set the limit to writtenPosition (the current maximum valid data pointer), and then transfer the commitPosition to the writtenPosition data Write to FileChannel, and then update the committedPosition pointer to writtenPosition. The function of commit is to submit the data in the writeBuffer of MappedFile to the file channel FileChannel.

protected void commit0(final int commitLeastPages) {
    
    
    //写指针
    int writePos = this.wrotePosition.get();
    //上次提交指针
    int lastCommittedPosition = this.committedPosition.get();

    if (writePos - this.committedPosition.get() > 0) {
    
    
        try {
    
    
            //复制共享内存区域
            ByteBuffer byteBuffer = writeBuffer.slice();
            //设置提交位置是上次提交位置
            byteBuffer.position(lastCommittedPosition);
            //最大提交数量
            byteBuffer.limit(writePos);
            //设置fileChannel位置为上次提交位置
            this.fileChannel.position(lastCommittedPosition);
            //将lastCommittedPosition到writePos的数据复制到FileChannel中
            this.fileChannel.write(byteBuffer);
            //重置提交位置
            this.committedPosition.set(writePos);
        } catch (Throwable e) {
    
    
            log.error("Error occurred when commit data to FileChannel.", e);
        }
    }
}

MappedFile#flush

To flush the disk, directly call the force method of MappedByteBuffer or fileChannel to persist the data in memory to the disk, then flushedPosition should be equal to the write pointer in MappedByteBuffer; if writeBuffer is not empty, flushPosition should be equal to the last commit pointer; because the above The data submitted once is the data entered into the MappedByteBuffer; if the writeBuffer is empty, the data will directly enter the MappedByteBuffer, and the writtenPosition represents the pointer in the MappedByteBuffer, so set flushPosition to writtenPosition.

insert image description here

public int flush(final int flushLeastPages) {
    
    
    //数据达到刷盘条件
    if (this.isAbleToFlush(flushLeastPages)) {
    
    
        //加锁,同步刷盘
        if (this.hold()) {
    
    
            //获得读指针
            int value = getReadPosition();
            try {
    
    
                //数据从writeBuffer提交数据到fileChannel再刷新到磁盘
                if (writeBuffer != null || this.fileChannel.position() != 0) {
    
    
                    this.fileChannel.force(false);
                } else {
    
    
                    //从mmap刷新数据到磁盘
                    this.mappedByteBuffer.force();
                }
            } catch (Throwable e) {
    
    
                log.error("Error occurred when force data to disk.", e);
            }
			//更新刷盘位置
            this.flushedPosition.set(value);
            this.release();
        } else {
    
    
            log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
            this.flushedPosition.set(getReadPosition());
        }
    }
    return this.getFlushedPosition();
}

MappedFile#getReadPosition

Get the maximum readable pointer of the current file. If writeBuffer is empty, it will directly return the current write pointer; if writeBuffer is not empty, it will return the pointer submitted last time. In the MappedFile setting, only submitted data (data written to MappedByteBuffer or FileChannel) is safe data

public int getReadPosition() {
    
    
    //如果writeBuffer为空,刷盘的位置就是应该等于上次commit的位置,如果为空则为mmap的写指针
    return this.writeBuffer == null ? this.wrotePosition.get() : this.committedPosition.get();
}

MappedFile#selectMappedBuffer

Find the data between pos and the current maximum readable. Since the pointer of the MappedByteBuffer has not been changed during the entire writing period, if the shared buffer space returned by the mappedByteBuffer.slice() method is the entire MappedFile, then by setting the position of the ByteBuffer to be The searched value, the read byte length is currently the maximum length that can be read, and the limit of the finally returned ByteBuffer is size. The capacity of the entire shared buffer is (MappedFile#fileSize-pos). Therefore, in the operation of SelectMappedBufferResult, the filp method cannot be called on the ByteBuffer contained in it.

public SelectMappedBufferResult selectMappedBuffer(int pos) {
    
    
    //获得最大可读指针
    int readPosition = getReadPosition();
    //pos小于最大可读指针,并且大于0
    if (pos < readPosition && pos >= 0) {
    
    
        if (this.hold()) {
    
    
            //复制mappedByteBuffer读共享区
            ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
            //设置读指针位置
            byteBuffer.position(pos);
            //获得可读范围
            int size = readPosition - pos;
            //设置最大刻度范围
            ByteBuffer byteBufferNew = byteBuffer.slice();
            byteBufferNew.limit(size);
            return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
        }
    }

    return null;
}

MappedFile#shutdown

The implementation method of MappedFile file destruction is public boolean destroy(long intervalForcibly), and intervalForcibly indicates the maximum survival time for refusing to be destroyed.

public void shutdown(final long intervalForcibly) {
    
    
    if (this.available) {
    
    
        //关闭MapedFile
        this.available = false;
        //设置当前关闭时间戳
        this.firstShutdownTimestamp = System.currentTimeMillis();
        //释放资源
        this.release();
    } else if (this.getRefCount() > 0) {
    
    
        if ((System.currentTimeMillis() - this.firstShutdownTimestamp) >= intervalForcibly) {
    
    
            this.refCount.set(-1000 - this.getRefCount());
            this.release();
        }
    }
}

2.4.4.3、TransientStorePool

Ephemeral storage pools. RocketMQ separately creates a MappedByteBuffer memory cache pool to temporarily store data. The data is first written into the memory map, and then the commit thread regularly copies the data from the memory to the memory map corresponding to the target physical file. The main reason why RocketMQ introduces this mechanism is to provide a memory lock, which keeps the current off-heap memory locked in memory and prevents the process from swapping memory to disk.

private final int poolSize;		//availableBuffers个数
private final int fileSize;		//每隔ByteBuffer大小
private final Deque<ByteBuffer> availableBuffers;	//ByteBuffer容器。双端队列

initialization

public void init() {
    
    
    //创建poolSize个堆外内存
    for (int i = 0; i < poolSize; i++) {
    
    
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize);
        final long address = ((DirectBuffer) byteBuffer).address();
        Pointer pointer = new Pointer(address);
        //使用com.sun.jna.Library类库将该批内存锁定,避免被置换到交换区,提高存储性能
        LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize));

        availableBuffers.offer(byteBuffer);
    }
}

2.4.5. Update message consumption queue and index file in real time

The message consumption team file and the message attribute index file are built based on the CommitLog file. When the message submitted by the message producer is stored in the CommitLog file, the ConsumerQueue and IndexFile need to be updated in time, otherwise the message cannot be consumed in time. There will be a large delay. RocketMQ forwards the CommitLog file update event in quasi-real time by opening a thread ReputMessageService, and the corresponding task processor updates the ConsumerQueue and IndexFile files in time according to the forwarded message.
insert image description here

insert image description here
Code: DefaultMessageStore: start

//设置CommitLog内存中最大偏移量
this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
//启动
this.reputMessageService.start();

Code: DefaultMessageStore: run

public void run() {
    
    
    DefaultMessageStore.log.info(this.getServiceName() + " service started");
	//每隔1毫秒就继续尝试推送消息到消息消费队列和索引文件
    while (!this.isStopped()) {
    
    
        try {
    
    
            Thread.sleep(1);
            this.doReput();
        } catch (Exception e) {
    
    
            DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }

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

Code: DefaultMessageStore:deReput

//从result中循环遍历消息,一次读一条,创建DispatherRequest对象。
for (int readSize = 0; readSize < result.getSize() && doNext; ) {
    
    
	DispatchRequest dispatchRequest =                               DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
	int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();

	if (dispatchRequest.isSuccess()) {
    
    
	    if (size > 0) {
    
    
	        DefaultMessageStore.this.doDispatch(dispatchRequest);
	    }
    }
}

DispatchRequest

String topic; //消息主题名称
int queueId;  //消息队列ID
long commitLogOffset;	//消息物理偏移量
int msgSize;	//消息长度
long tagsCode;	//消息过滤tag hashCode
long storeTimestamp;	//消息存储时间戳
long consumeQueueOffset;	//消息队列偏移量
String keys;	//消息索引key
boolean success;	//是否成功解析到完整的消息
String uniqKey;	//消息唯一键
int sysFlag;	//消息系统标记
long preparedTransactionOffset;	//消息预处理事务偏移量
Map<String, String> propertiesMap;	//消息属性
byte[] bitMap;	//位图

2.4.5.1, Forward to ConsumerQueue

insert image description here

class CommitLogDispatcherBuildConsumeQueue implements CommitLogDispatcher {
    
    
    @Override
    public void dispatch(DispatchRequest request) {
    
    
        final int tranType = MessageSysFlag.getTransactionValue(request.getSysFlag());
        switch (tranType) {
    
    
            case MessageSysFlag.TRANSACTION_NOT_TYPE:
            case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
                //消息分发
                DefaultMessageStore.this.putMessagePositionInfo(request);
                break;
            case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
            case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
                break;
        }
    }
}

代码:DefaultMessageStore#putMessagePositionInfo

public void putMessagePositionInfo(DispatchRequest dispatchRequest) {
    
    
    //获得消费队列
    ConsumeQueue cq = this.findConsumeQueue(dispatchRequest.getTopic(), dispatchRequest.getQueueId());
    //消费队列分发消息
    cq.putMessagePositionInfoWrapper(dispatchRequest);
}

代码:DefaultMessageStore#putMessagePositionInfo

//依次将消息偏移量、消息长度、tag写入到ByteBuffer中
this.byteBufferIndex.flip();
this.byteBufferIndex.limit(CQ_STORE_UNIT_SIZE);
this.byteBufferIndex.putLong(offset);
this.byteBufferIndex.putInt(size);
this.byteBufferIndex.putLong(tagsCode);
//获得内存映射文件
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile(expectLogicOffset);
if (mappedFile != null) {
    
    
    //将消息追加到内存映射文件,异步输盘
    return mappedFile.appendMessage(this.byteBufferIndex.array());
}

2.4.5.2, Forward to Index

insert image description here

class CommitLogDispatcherBuildIndex implements CommitLogDispatcher {
    
    

    @Override
    public void dispatch(DispatchRequest request) {
    
    
        if (DefaultMessageStore.this.messageStoreConfig.isMessageIndexEnable()) {
    
    
            DefaultMessageStore.this.indexService.buildIndex(request);
        }
    }
}

Code: DefaultMessageStore#buildIndex

public void buildIndex(DispatchRequest req) {
    
    
    //获得索引文件
    IndexFile indexFile = retryGetAndCreateIndexFile();
    if (indexFile != null) {
    
    
        //获得文件最大物理偏移量
        long endPhyOffset = indexFile.getEndPhyOffset();
        DispatchRequest msg = req;
        String topic = msg.getTopic();
        String keys = msg.getKeys();
        //如果该消息的物理偏移量小于索引文件中的最大物理偏移量,则说明是重复数据,忽略本次索引构建
        if (msg.getCommitLogOffset() < endPhyOffset) {
    
    
            return;
        }

        final int tranType = MessageSysFlag.getTransactionValue(msg.getSysFlag());
        switch (tranType) {
    
    
            case MessageSysFlag.TRANSACTION_NOT_TYPE:
            case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
            case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
                break;
            case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
                return;
        }
		
        //如果消息ID不为空,则添加到Hash索引中
        if (req.getUniqKey() != null) {
    
    
            indexFile = putKey(indexFile, msg, buildKey(topic, req.getUniqKey()));
            if (indexFile == null) {
    
    
                return;
            }
        }
		//构建索引key,RocketMQ支持为同一个消息建立多个索引,多个索引键空格隔开.
        if (keys != null && keys.length() > 0) {
    
    
            String[] keyset = keys.split(MessageConst.KEY_SEPARATOR);
            for (int i = 0; i < keyset.length; i++) {
    
    
                String key = keyset[i];
                if (key.length() > 0) {
    
    
                    indexFile = putKey(indexFile, msg, buildKey(topic, key));
                    if (indexFile == null) {
    
    

                        return;
                    }
                }
            }
        }
    } else {
    
    
        log.error("build index error, stop building index");
    }
}

2.4.6, message queue and index file recovery

Because the RocketMQ storage first stores the full amount of messages in the CommitLog file, and then asynchronously generates forwarding tasks to update the ConsumerQueue and Index files. If the message is successfully stored in the CommitLog file, but the forwarding task is not successfully executed, the message server Broker is down due to a certain willingness, resulting in inconsistent data in the CommitLog, ConsumerQueue, and IndexFile files. If it is not manually repaired, even if some messages exist in the file in the CommitLog, but because they are not forwarded to the ConsumerQueue, this part of the message will always be recurred and consumed by consumers.

insert image description here

2.4.6.1, Stored file loading

Code: DefaultMessageStore#load

Determine whether the last exit was abnormal. The implementation mechanism is that Broker creates an abort file when it starts, and deletes the abort file through the JVM hook function when it exits. If the abort file exists at next startup. It means that if the Broker exits abnormally, the CommitLog and ConsumerQueue data may be inconsistent and need to be repaired.

//判断临时文件是否存在
boolean lastExitOK = !this.isTempFileExist();
//根据临时文件判断当前Broker是否异常退出
private boolean isTempFileExist() {
    
    
    String fileName = StorePathConfigHelper
        .getAbortFile(this.messageStoreConfig.getStorePathRootDir());
    File file = new File(fileName);
    return file.exists();
}

Code: DefaultMessageStore#load

//加载延时队列
if (null != scheduleMessageService) {
    
    
    result = result && this.scheduleMessageService.load();
}

// 加载CommitLog文件
result = result && this.commitLog.load();

// 加载消费队列文件
result = result && this.loadConsumeQueue();

if (result) {
    
    
	//加载存储监测点,监测点主要记录CommitLog文件、ConsumerQueue文件、Index索引文件的刷盘点
    this.storeCheckpoint =new StoreCheckpoint(StorePathConfigHelper.getStoreCheckpoint(this.messageStoreConfig.getStorePathRootDir()));
	//加载index文件
    this.indexService.load(lastExitOK);
	//根据Broker是否异常退出,执行不同的恢复策略
    this.recover(lastExitOK);
}

Code: MappedFileQueue#load

Load CommitLog to map file

//指向CommitLog文件目录
File dir = new File(this.storePath);
//获得文件数组
File[] files = dir.listFiles();
if (files != null) {
    
    
    // 文件排序
    Arrays.sort(files);
    //遍历文件
    for (File file : files) {
    
    
		//如果文件大小和配置文件不一致,退出
        if (file.length() != this.mappedFileSize) {
    
    
            
            return false;
        }

        try {
    
    
            //创建映射文件
            MappedFile mappedFile = new MappedFile(file.getPath(), mappedFileSize);
            mappedFile.setWrotePosition(this.mappedFileSize);
            mappedFile.setFlushedPosition(this.mappedFileSize);
            mappedFile.setCommittedPosition(this.mappedFileSize);
            //将映射文件添加到队列
            this.mappedFiles.add(mappedFile);
            log.info("load " + file.getPath() + " OK");
        } catch (IOException e) {
    
    
            log.error("load file " + file + " error", e);
            return false;
        }
    }
}

return true;

代码:DefaultMessageStore#loadConsumeQueue

Load message consumption queue

//执行消费队列目录
File dirLogic = new File(StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()));
//遍历消费队列目录
File[] fileTopicList = dirLogic.listFiles();
if (fileTopicList != null) {
    
    

    for (File fileTopic : fileTopicList) {
    
    
        //获得子目录名称,即topic名称
        String topic = fileTopic.getName();
		//遍历子目录下的消费队列文件
        File[] fileQueueIdList = fileTopic.listFiles();
        if (fileQueueIdList != null) {
    
    
            //遍历文件
            for (File fileQueueId : fileQueueIdList) {
    
    
                //文件名称即队列ID
                int queueId;
                try {
    
    
                    queueId = Integer.parseInt(fileQueueId.getName());
                } catch (NumberFormatException e) {
    
    
                    continue;
                }
                //创建消费队列并加载到内存
                ConsumeQueue logic = new ConsumeQueue(
                    topic,
                    queueId,
                    StorePathConfigHelper.getStorePathConsumeQueue(this.messageStoreConfig.getStorePathRootDir()),
            this.getMessageStoreConfig().getMapedFileSizeConsumeQueue(),
                    this);
                this.putConsumeQueue(topic, queueId, logic);
                if (!logic.load()) {
    
    
                    return false;
                }
            }
        }
    }
}

log.info("load logics queue all over, OK");

return true;

Code: IndexService#load

load index file

public boolean load(final boolean lastExitOK) {
    
    
    //索引文件目录
    File dir = new File(this.storePath);
    //遍历索引文件
    File[] files = dir.listFiles();
    if (files != null) {
    
    
        //文件排序
        Arrays.sort(files);
        //遍历文件
        for (File file : files) {
    
    
            try {
    
    
                //加载索引文件
                IndexFile f = new IndexFile(file.getPath(), this.hashSlotNum, this.indexNum, 0, 0);
                f.load();

                if (!lastExitOK) {
    
    
                    //索引文件上次的刷盘时间小于该索引文件的消息时间戳,该文件将立即删除
                    if (f.getEndTimestamp() > this.defaultMessageStore.getStoreCheckpoint()
                        .getIndexMsgTimestamp()) {
    
    
                        f.destroy(0);
                        continue;
                    }
                }
				//将索引文件添加到队列
                log.info("load index file OK, " + f.getFileName());
                this.indexFileList.add(f);
            } catch (IOException e) {
    
    
                log.error("load file {} error", file, e);
                return false;
            } catch (NumberFormatException e) {
    
    
                log.error("load file {} error", file, e);
            }
        }
    }

    return true;
}

Code: DefaultMessageStore#recover

File recovery, implement different recovery strategies according to whether the Broker exits normally

private void recover(final boolean lastExitOK) {
    
    
    //获得最大的物理便宜消费队列
    long maxPhyOffsetOfConsumeQueue = this.recoverConsumeQueue();

    if (lastExitOK) {
    
    
        //正常恢复
        this.commitLog.recoverNormally(maxPhyOffsetOfConsumeQueue);
    } else {
    
    
        //异常恢复
        this.commitLog.recoverAbnormally(maxPhyOffsetOfConsumeQueue);
    }
	//在CommitLog中保存每个消息消费队列当前的存储逻辑偏移量
    this.recoverTopicQueueTable();
}

代码:DefaultMessageStore#recoverTopicQueueTable

After the ConsumerQueue is restored, the current storage logical offset of each message queue will be saved in the CommitLog instance, which is the key to storing not only the topic, message queue ID, but also the message queue in the message.

public void recoverTopicQueueTable() {
    
    
    HashMap<String/* topic-queueid */, Long/* offset */> table = new HashMap<String, Long>(1024);
    //CommitLog最小偏移量
    long minPhyOffset = this.commitLog.getMinOffset();
    //遍历消费队列,将消费队列保存在CommitLog中
    for (ConcurrentMap<Integer, ConsumeQueue> maps : this.consumeQueueTable.values()) {
    
    
        for (ConsumeQueue logic : maps.values()) {
    
    
            String key = logic.getTopic() + "-" + logic.getQueueId();
            table.put(key, logic.getMaxOffsetInQueue());
            logic.correctMinOffset(minPhyOffset);
        }
    }
    this.commitLog.setTopicQueueTable(table);
}

2.4.6.2, normal recovery

Code: CommitLog#recoverNormally

public void recoverNormally(long maxPhyOffsetOfConsumeQueue) {
    
    
	
    final List<MappedFile> mappedFiles = this.mappedFileQueue.getMappedFiles();
    if (!mappedFiles.isEmpty()) {
    
    
         //Broker正常停止再重启时,从倒数第三个开始恢复,如果不足3个文件,则从第一个文件开始恢复。
        int index = mappedFiles.size() - 3;
        if (index < 0)
            index = 0;
        MappedFile mappedFile = mappedFiles.get(index);
        ByteBuffer byteBuffer = mappedFile.sliceByteBuffer();
        long processOffset = mappedFile.getFileFromOffset();
        //代表当前已校验通过的offset
        long mappedFileOffset = 0;
        while (true) {
    
    
            //查找消息
            DispatchRequest dispatchRequest = this.checkMessageAndReturnSize(byteBuffer, checkCRCOnRecover);
            //消息长度
            int size = dispatchRequest.getMsgSize();
           	//查找结果为true,并且消息长度大于0,表示消息正确.mappedFileOffset向前移动本消息长度
            if (dispatchRequest.isSuccess() && size > 0) {
    
    
                mappedFileOffset += size;
            }
			//如果查找结果为true且消息长度等于0,表示已到该文件末尾,如果还有下一个文件,则重置processOffset和MappedFileOffset重复查找下一个文件,否则跳出循环。
            else if (dispatchRequest.isSuccess() && size == 0) {
    
    
              index++;
              if (index >= mappedFiles.size()) {
    
    
                  // Current branch can not happen
                  break;
              } else {
    
    
                  //取出每个文件
                  mappedFile = mappedFiles.get(index);
                  byteBuffer = mappedFile.sliceByteBuffer();
                  processOffset = mappedFile.getFileFromOffset();
                  mappedFileOffset = 0;
                  
          		}
            }
            // 查找结果为false,表明该文件未填满所有消息,跳出循环,结束循环
            else if (!dispatchRequest.isSuccess()) {
    
    
                log.info("recover physics file end, " + mappedFile.getFileName());
                break;
            }
        }
		//更新MappedFileQueue的flushedWhere和committedWhere指针
        processOffset += mappedFileOffset;
        this.mappedFileQueue.setFlushedWhere(processOffset);
        this.mappedFileQueue.setCommittedWhere(processOffset);
        //删除offset之后的所有文件
        this.mappedFileQueue.truncateDirtyFiles(processOffset);

        
        if (maxPhyOffsetOfConsumeQueue >= processOffset) {
    
    
            this.defaultMessageStore.truncateDirtyLogicFiles(processOffset);
        }
    } else {
    
    
        this.mappedFileQueue.setFlushedWhere(0);
        this.mappedFileQueue.setCommittedWhere(0);
        this.defaultMessageStore.destroyLogics();
    }
}

Code: MappedFileQueue#truncateDirtyFiles

public void truncateDirtyFiles(long offset) {
    
    
    List<MappedFile> willRemoveFiles = new ArrayList<MappedFile>();
	//遍历目录下文件
    for (MappedFile file : this.mappedFiles) {
    
    
        //文件尾部的偏移量
        long fileTailOffset = file.getFileFromOffset() + this.mappedFileSize;
        //文件尾部的偏移量大于offset
        if (fileTailOffset > offset) {
    
    
            //offset大于文件的起始偏移量
            if (offset >= file.getFileFromOffset()) {
    
    
                //更新wrotePosition、committedPosition、flushedPosistion
                file.setWrotePosition((int) (offset % this.mappedFileSize));
                file.setCommittedPosition((int) (offset % this.mappedFileSize));
                file.setFlushedPosition((int) (offset % this.mappedFileSize));
            } else {
    
    
                //offset小于文件的起始偏移量,说明该文件是有效文件后面创建的,释放mappedFile占用内存,删除文件
                file.destroy(1000);
                willRemoveFiles.add(file);
            }
        }
    }

    this.deleteExpiredFile(willRemoveFiles);
}

2.4.6.3, abnormal recovery

The implementation of Broker abnormal stop file recovery is CommitLog#recoverAbnormally. The abnormal file recovery procedure is basically the same as the normal stop file recovery process, with two main differences. First of all, normal stop restores from the penultimate file by default, while abnormal stop needs to go forward from the last file to find the first file with normal message storage. Secondly, if there is no message file in the CommitLog directory, if there is a file in the message consumption queue directory, it needs to be destroyed.

Code: CommitLog#recoverAbnormally

if (!mappedFiles.isEmpty()) {
    
    
    // Looking beginning to recover from which file
    int index = mappedFiles.size() - 1;
    MappedFile mappedFile = null;
    for (; index >= 0; index--) {
    
    
        mappedFile = mappedFiles.get(index);
        //判断消息文件是否是一个正确的文件
        if (this.isMappedFileMatchedRecover(mappedFile)) {
    
    
            log.info("recover from this mapped file " + mappedFile.getFileName());
            break;
        }
    }
	//根据索引取出mappedFile文件
    if (index < 0) {
    
    
        index = 0;
        mappedFile = mappedFiles.get(index);
    }
    //...验证消息的合法性,并将消息转发到消息消费队列和索引文件
       
}else{
    
    
    //未找到mappedFile,重置flushWhere、committedWhere都为0,销毁消息队列文件
    this.mappedFileQueue.setFlushedWhere(0);
    this.mappedFileQueue.setCommittedWhere(0);
    this.defaultMessageStore.destroyLogics();
}

2.4.7, disk brushing mechanism

The storage of RocketMQ is based on the memory mapping mechanism (MappedByteBuffer) of JDK NIO. The message storage first appends the message to the memory, and then flushes the disk at different times according to the configured disk flushing strategy.

2.4.7.1, Synchronous brush disk

After the message is appended to the memory, immediately flush the data to the disk file
insert image description here
code: CommitLog#handleDiskFlush

//刷盘服务
final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
if (messageExt.isWaitStoreMsgOK()) {
    
    
    //封装刷盘请求
    GroupCommitRequest request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
    //提交刷盘请求
    service.putRequest(request);
    //线程阻塞5秒,等待刷盘结束
    boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
    if (!flushOK) {
    
    
        putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
    }

GroupCommitRequest

long nextOffset;	//刷盘点偏移量
CountDownLatch countDownLatch = new CountDownLatch(1);	//倒计树锁存器
volatile boolean flushOK = false;	//刷盘结果;默认为false

Code: GroupCommitService#run

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

    while (!this.isStopped()) {
    
    
        try {
    
    
            //线程等待10ms
            this.waitForRunning(10);
            //执行提交
            this.doCommit();
        } catch (Exception e) {
    
    
            CommitLog.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }
	...
}

Code: GroupCommitService#doCommit

private void doCommit() {
    
    
    //加锁
    synchronized (this.requestsRead) {
    
    
        if (!this.requestsRead.isEmpty()) {
    
    
            //遍历requestsRead
            for (GroupCommitRequest req : this.requestsRead) {
    
    
                // There may be a message in the next file, so a maximum of
                // two times the flush
                boolean flushOK = false;
                for (int i = 0; i < 2 && !flushOK; i++) {
    
    
                    flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();
					//刷盘
                    if (!flushOK) {
    
    
                        CommitLog.this.mappedFileQueue.flush(0);
                    }
                }
				//唤醒发送消息客户端
                req.wakeupCustomer(flushOK);
            }
			
            //更新刷盘监测点
            long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
            if (storeTimestamp > 0) {
    
                   CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);
            }
			
            this.requestsRead.clear();
        } else {
    
    
            // Because of individual messages is set to not sync flush, it
            // will come to this process
            CommitLog.this.mappedFileQueue.flush(0);
        }
    }
}

2.4.7.2. Asynchronous flash disk

After the message is appended to the memory, it returns to the sender of the message immediately. If transientStorePoolEnable is enabled, RocketMQ will separately apply for an off-heap memory of the same size as the target physical file (commitLog). The off-heap memory will be locked using memory to ensure that it will not be replaced into virtual memory. The message will first be appended to the off-heap memory , and then submitted to the memory map of the physical file, and then flushed to disk. If transientStorePoolEnable is not enabled, the message is directly appended to the direct mapping file of the physical file, and then flushed to the disk.

insert image description here

Asynchronous flashing steps after enabling transientStorePoolEnable:

  1. Append message directly to ByteBuffer (off-heap memory)
  2. The CommitRealTimeService thread submits new ByteBuffer content to MappedByteBuffer every 200ms
  3. MappedByteBuffer appends the submitted content in memory, and the writtenPosition pointer moves backward
  4. The commit operation returns successfully, and the committedPosition is restored
  5. The FlushRealTimeService thread flushes the newly added memory in the MappedByteBuffer to the disk every 500ms by default

Code: CommitLog$CommitRealTimeService#run

Submit thread working mechanism

//间隔时间,默认200ms
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();

//一次提交的至少页数
int commitDataLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogLeastPages();

//两次真实提交的最大间隔,默认200ms
int commitDataThoroughInterval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitCommitLogThoroughInterval();

//上次提交间隔超过commitDataThoroughInterval,则忽略提交commitDataThoroughInterval参数,直接提交
long begin = System.currentTimeMillis();
if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
    
    
    this.lastCommitTimestamp = begin;
    commitDataLeastPages = 0;
}

//执行提交操作,将待提交数据提交到物理文件的内存映射区
boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
long end = System.currentTimeMillis();
if (!result) {
    
    
    this.lastCommitTimestamp = end; // result = false means some data committed.
    //now wake up flush thread.
    //唤醒刷盘线程
    flushCommitLogService.wakeup();
}

if (end - begin > 500) {
    
    
    log.info("Commit data to file costs {} ms", end - begin);
}
this.waitForRunning(interval);

Code: CommitLog$FlushRealTimeService#run

The working mechanism of the flashing thread

//表示await方法等待,默认false
boolean flushCommitLogTimed = CommitLog.this.defaultMessageStore.getMessageStoreConfig().isFlushCommitLogTimed();
//线程执行时间间隔
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushIntervalCommitLog();
//一次刷写任务至少包含页数
int flushPhysicQueueLeastPages = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogLeastPages();
//两次真实刷写任务最大间隔
int flushPhysicQueueThoroughInterval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getFlushCommitLogThoroughInterval();
...
//距离上次提交间隔超过flushPhysicQueueThoroughInterval,则本次刷盘任务将忽略flushPhysicQueueLeastPages,直接提交
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis >= (this.lastFlushTimestamp + flushPhysicQueueThoroughInterval)) {
    
    
    this.lastFlushTimestamp = currentTimeMillis;
    flushPhysicQueueLeastPages = 0;
    printFlushProgress = (printTimes++ % 10) == 0;
}
...
//执行一次刷盘前,先等待指定时间间隔
if (flushCommitLogTimed) {
    
    
    Thread.sleep(interval);
} else {
    
    
    this.waitForRunning(interval);
}
...
long begin = System.currentTimeMillis();
//刷写磁盘
CommitLog.this.mappedFileQueue.flush(flushPhysicQueueLeastPages);
long storeTimestamp = CommitLog.this.mappedFileQueue.getStoreTimestamp();
if (storeTimestamp > 0) {
    
    
//更新存储监测点文件的时间戳
CommitLog.this.defaultMessageStore.getStoreCheckpoint().setPhysicMsgTimestamp(storeTimestamp);

2.4.8. Expired file deletion mechanism

Because RocketMQ operates CommitLog and ConsumerQueue files based on the memory mapping mechanism and loads all files under the CommitLog and ConsumerQueue directories at startup, in order to avoid waste of memory and disk, it is impossible to permanently store messages on the message server, so it is necessary to introduce A mechanism to delete files that have expired. RocketMQ writes CommitLog and ConsumerQueue files sequentially. All write operations fall on the last CommitLog or ConsumerQueue file. The previous files will not be updated after the next file is created. When RocketMQ clears expired files: If the current file is not consumed again within a certain time interval, it is considered an expired file and can be deleted. RocketMQ will not pay attention to whether all the messages on this file are consumed. The default expiration time for each file is 72 hours. You can change the expiration time by setting fileReservedTime in the Broker configuration file, and the unit is hours.

Code: DefaultMessageStore#addScheduleTask

private void addScheduleTask() {
    
    
	//每隔10s调度一次清除文件
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    
    
        @Override
        public void run() {
    
    
            DefaultMessageStore.this.cleanFilesPeriodically();
        }
    }, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS);
	...
}

Code: DefaultMessageStore#cleanFilesPeriodically

private void cleanFilesPeriodically() {
    
    
    //清除存储文件
    this.cleanCommitLogService.run();
    //清除消息消费队列文件
    this.cleanConsumeQueueService.run();
}

代码:DefaultMessageStore#deleteExpiredFiles

private void deleteExpiredFiles() {
    
    
    //删除的数量
    int deleteCount = 0;
    //文件保留的时间
    long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime();
    //删除物理文件的间隔
    int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval();
    //线程被占用,第一次拒绝删除后能保留的最大时间,超过该时间,文件将被强制删除
    int destroyMapedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly();

boolean timeup = this.isTimeToDelete();
boolean spacefull = this.isSpaceToDelete();
boolean manualDelete = this.manualDeleteFileSeveralTimes > 0;
if (timeup || spacefull || manualDelete) {
    
    
	...执行删除逻辑
}else{
    
    
    ...无作为
}

Conditions for delete file operations

  1. Specify the time point to delete the file. RocketMQ uses deleteWhen to set a fixed time of the day to perform an operation to delete expired files. The default is 4 o'clock
  2. If there is insufficient disk space, delete expired files
  3. Reserved, manually triggered.

Code: CleanCommitLogService#isSpaceToDelete

Perform deletion of expired files when disk space is low

private boolean isSpaceToDelete() {
    
    
    //磁盘分区的最大使用量
    double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0;
	//是否需要立即执行删除过期文件操作
    cleanImmediately = false;

    {
    
    
        String storePathPhysic = DefaultMessageStore.this.getMessageStoreConfig().getStorePathCommitLog();
        //当前CommitLog目录所在的磁盘分区的磁盘使用率
        double physicRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic);
        //diskSpaceWarningLevelRatio:磁盘使用率警告阈值,默认0.90
        if (physicRatio > diskSpaceWarningLevelRatio) {
    
    
            boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull();
            if (diskok) {
    
    
                DefaultMessageStore.log.error("physic disk maybe full soon " + physicRatio + ", so mark disk full");
            }
			//diskSpaceCleanForciblyRatio:强制清除阈值,默认0.85
            cleanImmediately = true;
        } else if (physicRatio > diskSpaceCleanForciblyRatio) {
    
    
            cleanImmediately = true;
        } else {
    
    
            boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK();
            if (!diskok) {
    
    
            DefaultMessageStore.log.info("physic disk space OK " + physicRatio + ", so mark disk ok");
        }
    }

    if (physicRatio < 0 || physicRatio > ratio) {
    
    
        DefaultMessageStore.log.info("physic disk maybe full soon, so reclaim space, " + physicRatio);
        return true;
    }
}

代码:MappedFileQueue#deleteExpiredFileByTime

Perform file destruction and deletion

for (int i = 0; i < mfsLength; i++) {
    
    
    //遍历每隔文件
    MappedFile mappedFile = (MappedFile) mfs[i];
    //计算文件存活时间
    long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime;
    //如果超过72小时,执行文件删除
    if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) {
    
    
        if (mappedFile.destroy(intervalForcibly)) {
    
    
            files.add(mappedFile);
            deleteCount++;

            if (files.size() >= DELETE_FILES_BATCH_MAX) {
    
    
                break;
            }

            if (deleteFilesInterval > 0 && (i + 1) < mfsLength) {
    
    
                try {
    
    
                    Thread.sleep(deleteFilesInterval);
                } catch (InterruptedException e) {
    
    
                }
            }
        } else {
    
    
            break;
        }
    } else {
    
    
        //avoid deleting files in the middle
        break;
    }
}

2.4.9 Summary

RocketMQ's storage files include message files (Commitlog), message consumption queue files (ConsumerQueue), Hash index files (IndexFile), monitoring point files (checkPoint), and abort (close exception files). The length of a single message storage file, message consumption queue file, and Hash index file is fixed to use the memory mapping mechanism for file read and write operations. RocketMQ organizes files to order files with the starting offset of the file, so that the real physical file can be quickly located according to the offset. Based on the memory-mapped file mechanism, RocketMQ provides two mechanisms: synchronous disk flushing and asynchronous disk flushing. Asynchronous disk flushing means that when the message is stored, it is first appended to the memory-mapped file, and then a dedicated thread for flushing the disk is started to periodically refresh the file data in the memory. write to disk.

CommitLog, message storage file, in order to ensure high throughput of message sending, RocketMQ uses a single file to store all topic messages to ensure that message storage is written in complete order, but this brings inconvenience to file reading, so RocketMQ is for the convenience of message Consumption builds a message consumption queue file, which is organized based on topics and queues. At the same time, RocketMQ implements a Hash index for messages, and can set index keys for messages. Therefore, messages can be quickly retrieved from CommitLog files.

When the message reaches the CommitLog, the message will be forwarded to the message consumption queue file and index file in near real time through the ReputMessageService thread. For safety reasons, RocketMQ introduces the abort file to record whether the Broker shuts down normally or abnormally. When restarting the Broker, in order to ensure the correctness of the CommitLog file, the message consumption queue file and the Hash index file, different strategies are used to restore the files.

RocketMQ does not permanently store message files and message consumption queue files, but activates the file expiration mechanism and deletes expired files when the disk space is insufficient or at 4:00 a.m. by default. The files are saved for 72 hours and will not be judged when the file is deleted. Whether the message is consumed.

2.5、Consumer

2.5.1. Overview of message consumption

Message consumption is carried out in a group mode. A consumer group can contain multiple consumers, and each consumer group can subscribe to multiple topics. There are two consumption modes: cluster mode and broadcast mode among consumer groups. In cluster mode, the same message under the topic is only allowed to be consumed by one of the consumers. In broadcast mode, the same message under the topic will be consumed once by all consumers in the cluster. There are also two modes of message delivery between the message server and consumers: push mode and pull mode. The so-called pull mode is that the consumer actively pulls the message request, while the push mode is that the message is pushed to the message consumer after the message reaches the message server. The implementation of the RocketMQ message push mode is based on the pull mode, which wraps a layer on the pull mode, and starts the next pull task after one pull task is completed.

In cluster mode, how do multiple consumers load the message queue? The message queue load mechanism follows a general idea: a message queue can only be consumed by one consumer at a time, and a consumer can consume multiple message queues.

RocketMQ supports partial sequential message consumption, that is, to ensure the sequential consumption of messages on the same message queue. Global sequential consumption of messages is not supported. If you want to implement global sequential consumption of a certain topic, you can set the number of queues of the topic to 1, sacrificing high availability.

2.5.2. Preliminary Study on Message Consumption

Message push mode

Important methods of message consumption

void sendMessageBack(final MessageExt msg, final int delayLevel, final String brokerName):发送消息确认
Set<MessageQueue> fetchSubscribeMessageQueues(final String topic) :获取消费者对主题分配了那些消息队列
void registerMessageListener(final MessageListenerConcurrently messageListener):注册并发事件监听器
void registerMessageListener(final MessageListenerOrderly messageListener):注册顺序消息事件监听器
void subscribe(final String topic, final String subExpression):基于主题订阅消息,消息过滤使用表达式
void subscribe(final String topic, final String fullClassName,final String filterClassSource):基于主题订阅消息,消息过滤使用类模式
void subscribe(final String topic, final MessageSelector selector) :订阅消息,并指定队列选择器
void unsubscribe(final String topic):取消消息订阅

DefaultMQPushConsumer

//消费者组
private String consumerGroup;	
//消息消费模式
private MessageModel messageModel = MessageModel.CLUSTERING;	
//指定消费开始偏移量(最大偏移量、最小偏移量、启动时间戳)开始消费
private ConsumeFromWhere consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
//集群模式下的消息队列负载策略
private AllocateMessageQueueStrategy allocateMessageQueueStrategy;
//订阅信息
private Map<String /* topic */, String /* sub expression */> subscription = new HashMap<String, String>();
//消息业务监听器
private MessageListener messageListener;
//消息消费进度存储器
private OffsetStore offsetStore;
//消费者最小线程数量
private int consumeThreadMin = 20;
//消费者最大线程数量
private int consumeThreadMax = 20;
//并发消息消费时处理队列最大跨度
private int consumeConcurrentlyMaxSpan = 2000;
//每1000次流控后打印流控日志
private int pullThresholdForQueue = 1000;
//推模式下任务间隔时间
private long pullInterval = 0;
//推模式下任务拉取的条数,默认32条
private int pullBatchSize = 32;
//每次传入MessageListener#consumerMessage中消息的数量
private int consumeMessageBatchMaxSize = 1;
//是否每次拉取消息都订阅消息
private boolean postSubscriptionWhenPull = false;
//消息重试次数,-1代表16次
private int maxReconsumeTimes = -1;
//消息消费超时时间
private long consumeTimeout = 15;

2.5.3. Consumer initiation process

insert image description here
Code: DefaultMQPushConsumerImpl#start

public synchronized void start() throws MQClientException {
    
    
    switch (this.serviceState) {
    
    
        case CREATE_JUST:
            
                this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode());
            this.serviceState = ServiceState.START_FAILED;
			//检查消息者是否合法
            this.checkConfig();
			//构建主题订阅信息
            this.copySubscription();
			//设置消费者客户端实例名称为进程ID
            if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) {
    
    
                this.defaultMQPushConsumer.changeInstanceNameToPID();
            }
			//创建MQClient实例
            this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
			//构建rebalanceImpl
            this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup());
            this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel());
            this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy());
            this.rebalanceImpl.setmQClientFactory(this.mQClientFactor
            this.pullAPIWrapper = new PullAPIWrapper(
                mQClientFactory,
                this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode());
            this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookLis
            if (this.defaultMQPushConsumer.getOffsetStore() != null) {
    
    
                this.offsetStore = this.defaultMQPushConsumer.getOffsetStore();
            } else {
    
    
           		switch (this.defaultMQPushConsumer.getMessageModel()) {
    
    
               
           	    case BROADCASTING:	 //消息消费广播模式,将消费进度保存在本地
           	        this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
           	            break;
           	        case CLUSTERING:	//消息消费集群模式,将消费进度保存在远端Broker
           	            this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup());
           	            break;
           	        default:
           	            break;
           	    }
           	    this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);
           	}
            this.offsetStore.load
            //创建顺序消息消费服务
            if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
    
    
                this.consumeOrderly = true;
                this.consumeMessageService =
                    new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
                //创建并发消息消费服务
            } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
    
    
                this.consumeOrderly = false;
                this.consumeMessageService =
                    new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
            }
            //消息消费服务启动
            this.consumeMessageService.start();
            //注册消费者实例
            boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this);
            
            if (!registerOK) {
    
    
                this.serviceState = ServiceState.CREATE_JUST;
                this.consumeMessageService.shutdown();
                throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup()
                    + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
                    null);
            //启动消费者客户端
            mQClientFactory.start();
            log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup());
            this.serviceState = ServiceState.RUNNING;
            break;
            case RUNNING:
            case START_FAILED:
        case SHUTDOWN_ALREADY:
            throw new MQClientException("The PushConsumer service state not OK, maybe started once, "
                + this.serviceState
                + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
                null);
        default:
            break;
    }

    this.updateTopicSubscribeInfoWhenSubscriptionChanged();
    this.mQClientFactory.checkClientInBroker();
    this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
    this.mQClientFactory.rebalanceImmediately();
}

2.5.4. Message pull

There are two modes of message consumption mode: broadcast mode and cluster mode. The broadcast mode is relatively simple, and each consumer needs to pull messages from all queues under the subscribed topic. This article focuses on cluster mode. In cluster mode, there are multiple message consumers in the same consumer group, and multiple consumption queues exist in the same topic, and consumers consume messages through load balancing.

The usual practice of message queue load balancing is that a message queue can only be consumed by one consumer at a time, and a message consumer can consume multiple message queues at the same time.

2.5.4.1, PullMessageService implementation mechanism

It can be seen from the startup process of MQClientInstance that RocketMQ uses a separate thread PullMessageService to be responsible for pulling messages.

insert image description here
Code: PullMessageService#run

public void run() {
    
    
    log.info(this.getServiceName() + " service started");
	//循环拉取消息
    while (!this.isStopped()) {
    
    
        try {
    
    
            //从请求队列中获取拉取消息请求
            PullRequest pullRequest = this.pullRequestQueue.take();
            //拉取消息
            this.pullMessage(pullRequest);
        } catch (InterruptedException ignored) {
    
    
        } catch (Exception e) {
    
    
            log.error("Pull Message Service Run Method exception", e);
        }
    }

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

PullRequest

private String consumerGroup;	//消费者组
private MessageQueue messageQueue;	//待拉取消息队列
private ProcessQueue processQueue;	//消息处理队列
private long nextOffset;	//待拉取的MessageQueue偏移量
private boolean lockedFirst = false;	//是否被锁定

Code: PullMessageService#pullMessage

private void pullMessage(final PullRequest pullRequest) {
    
    
    //获得消费者实例
    final MQConsumerInner consumer = this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
    if (consumer != null) {
    
    
        //强转为推送模式消费者
        DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
        //推送消息
        impl.pullMessage(pullRequest);
    } else {
    
    
        log.warn("No matched consumer for the PullRequest {}, drop it", pullRequest);
    }
}

2.5.4.2, ProcessQueue implementation mechanism

ProcessQueue is the reproduction and snapshot of MessageQueue on the consumer side. PullMessageService pulls 32 messages from the message server each time by default, and stores them in the ProcessQueue in the order of the queue offset of the messages. PullMessageService then submits the messages to the consumer thread pool. After the messages are successfully consumed, they are removed from the ProcessQueue.

Attributes

//消息容器
private final TreeMap<Long, MessageExt> msgTreeMap = new TreeMap<Long, MessageExt>();
//读写锁
private final ReadWriteLock lockTreeMap = new ReentrantReadWriteLock();
//ProcessQueue总消息树
private final AtomicLong msgCount = new AtomicLong();
//ProcessQueue队列最大偏移量
private volatile long queueOffsetMax = 0L;
//当前ProcessQueue是否被丢弃
private volatile boolean dropped = false;
//上一次拉取时间戳
private volatile long lastPullTimestamp = System.currentTimeMillis();
//上一次消费时间戳
private volatile long lastConsumeTimestamp = System.currentTimeMillis();

method

//移除消费超时消息
public void cleanExpiredMsg(DefaultMQPushConsumer pushConsumer)
//添加消息
public boolean putMessage(final List<MessageExt> msgs)
//获取消息最大间隔
public long getMaxSpan()
//移除消息
public long removeMessage(final List<MessageExt> msgs)
//将consumingMsgOrderlyTreeMap中消息重新放在msgTreeMap,并清空consumingMsgOrderlyTreeMap   
public void rollback() 
//将consumingMsgOrderlyTreeMap消息清除,表示成功处理该批消息
public long commit()
//重新处理该批消息
public void makeMessageToCosumeAgain(List<MessageExt> msgs) 
//从processQueue中取出batchSize条消息
public List<MessageExt> takeMessags(final int batchSize)

2.5.4.3, the basic process of message pull

2.5.4.3.1. The client initiates a pull request

insert image description here
Code: DefaultMQPushConsumerImpl#pullMessage

public void pullMessage(final PullRequest pullRequest) {
    
    
    //从pullRequest获得ProcessQueue
    final ProcessQueue processQueue = pullRequest.getProcessQueue();
    //如果处理队列被丢弃,直接返回
    if (processQueue.isDropped()) {
    
    
        log.info("the pull request[{}] is dropped.", pullRequest.toString());
        return;
    }
	//如果处理队列未被丢弃,更新时间戳
    pullRequest.getProcessQueue().setLastPullTimestamp(System.currentTimeMillis());

    try {
    
    
        this.makeSureStateOK();
    } catch (MQClientException e) {
    
    
        log.warn("pullMessage exception, consumer state not ok", e);
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
        return;
    }
	//如果处理队列被挂起,延迟1s后再执行
    if (this.isPause()) {
    
    
        log.warn("consumer was paused, execute pull request later. instanceName={}, group={}", this.defaultMQPushConsumer.getInstanceName(), this.defaultMQPushConsumer.getConsumerGroup());
        this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_SUSPEND);
        return;
    }
	//获得最大待处理消息数量
	long cachedMessageCount = processQueue.getMsgCount().get();
    //获得最大待处理消息大小
	long cachedMessageSizeInMiB = processQueue.getMsgSize().get() / (1024 * 1024);
	//从数量进行流控
	if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
    
    
	    this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
	    if ((queueFlowControlTimes++ % 1000) == 0) {
    
    
	        log.warn(
	            "the cached message count exceeds the threshold {}, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
	            this.defaultMQPushConsumer.getPullThresholdForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
	    }
	    return;
	}
	//从消息大小进行流控
	if (cachedMessageSizeInMiB > this.defaultMQPushConsumer.getPullThresholdSizeForQueue()) {
    
    
	    this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);
	    if ((queueFlowControlTimes++ % 1000) == 0) {
    
    
	        log.warn(
	            "the cached message size exceeds the threshold {} MiB, so do flow control, minOffset={}, maxOffset={}, count={}, size={} MiB, pullRequest={}, flowControlTimes={}",
	            this.defaultMQPushConsumer.getPullThresholdSizeForQueue(), processQueue.getMsgTreeMap().firstKey(), processQueue.getMsgTreeMap().lastKey(), cachedMessageCount, cachedMessageSizeInMiB, pullRequest, queueFlowControlTimes);
	    }
	    return;
    }
    	//获得订阅信息
		 final SubscriptionData subscriptionData = this.rebalanceImpl.getSubscriptionInner().get(pullRequest.getMessageQueue().getTopic());
    	if (null == subscriptionData) {
    
    
    	    this.executePullRequestLater(pullRequest, PULL_TIME_DELAY_MILLS_WHEN_EXCEPTION);
    	    log.warn("find the consumer's subscription failed, {}", pullRequest);
    	    return;
		//与服务端交互,获取消息
	    this.pullAPIWrapper.pullKernelImpl(
	    pullRequest.getMessageQueue(),
	    subExpression,
	    subscriptionData.getExpressionType(),
	    subscriptionData.getSubVersion(),
	    pullRequest.getNextOffset(),
	    this.defaultMQPushConsumer.getPullBatchSize(),
	    sysFlag,
	    commitOffsetValue,
	    BROKER_SUSPEND_MAX_TIME_MILLIS,
	    CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,
	    CommunicationMode.ASYNC,
	    pullCallback
	);
            
}
2.5.4.3.2, message server Broker assembly message

insert image description here
Code: PullMessageProcessor#processRequest

//构建消息过滤器
MessageFilter messageFilter;
if (this.brokerController.getBrokerConfig().isFilterSupportRetry()) {
    
    
    messageFilter = new ExpressionForRetryMessageFilter(subscriptionData, consumerFilterData,
        this.brokerController.getConsumerFilterManager());
} else {
    
    
    messageFilter = new ExpressionMessageFilter(subscriptionData, consumerFilterData,
        this.brokerController.getConsumerFilterManager());
}
//调用MessageStore.getMessage查找消息
final GetMessageResult getMessageResult =
    this.brokerController.getMessageStore().getMessage(
    				requestHeader.getConsumerGroup(), //消费组名称								
    				requestHeader.getTopic(),	//主题名称
        			requestHeader.getQueueId(), //队列ID
    				requestHeader.getQueueOffset(), 	//待拉取偏移量
    				requestHeader.getMaxMsgNums(), 	//最大拉取消息条数
    				messageFilter	//消息过滤器
    		);

Code: DefaultMessageStore#getMessage

GetMessageStatus status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
long nextBeginOffset = offset;	//查找下一次队列偏移量
long minOffset = 0;		//当前消息队列最小偏移量
long maxOffset = 0;		//当前消息队列最大偏移量
GetMessageResult getResult = new GetMessageResult();
final long maxOffsetPy = this.commitLog.getMaxOffset();	//当前commitLog最大偏移量
//根据主题名称和队列编号获取消息消费队列
ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);

...
minOffset = consumeQueue.getMinOffsetInQueue();
maxOffset = consumeQueue.getMaxOffsetInQueue();
//消息偏移量异常情况校对下一次拉取偏移量
if (maxOffset == 0) {
    
    	//表示当前消息队列中没有消息
    status = GetMessageStatus.NO_MESSAGE_IN_QUEUE;
    nextBeginOffset = nextOffsetCorrection(offset, 0);
} else if (offset < minOffset) {
    
    	//待拉取消息的偏移量小于队列的其实偏移量
    status = GetMessageStatus.OFFSET_TOO_SMALL;
    nextBeginOffset = nextOffsetCorrection(offset, minOffset);
} else if (offset == maxOffset) {
    
    	//待拉取偏移量为队列最大偏移量
    status = GetMessageStatus.OFFSET_OVERFLOW_ONE;
    nextBeginOffset = nextOffsetCorrection(offset, offset);
} else if (offset > maxOffset) {
    
    	//偏移量越界
    status = GetMessageStatus.OFFSET_OVERFLOW_BADLY;
    if (0 == minOffset) {
    
    
        nextBeginOffset = nextOffsetCorrection(offset, minOffset);
    } else {
    
    
        nextBeginOffset = nextOffsetCorrection(offset, maxOffset);
    }
}
...
//根据偏移量从CommitLog中拉取32条消息
SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy);

Code: PullMessageProcessor#processRequest

//根据拉取结果填充responseHeader
response.setRemark(getMessageResult.getStatus().name());
responseHeader.setNextBeginOffset(getMessageResult.getNextBeginOffset());
responseHeader.setMinOffset(getMessageResult.getMinOffset());
responseHeader.setMaxOffset(getMessageResult.getMaxOffset());

//判断如果存在主从同步慢,设置下一次拉取任务的ID为主节点
switch (this.brokerController.getMessageStoreConfig().getBrokerRole()) {
    
    
    case ASYNC_MASTER:
    case SYNC_MASTER:
        break;
    case SLAVE:
        if (!this.brokerController.getBrokerConfig().isSlaveReadEnable()) {
    
    
            response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY);
            responseHeader.setSuggestWhichBrokerId(MixAll.MASTER_ID);
        }
        break;
}
...
//GetMessageResult与Response的Code转换
switch (getMessageResult.getStatus()) {
    
    
    case FOUND:			//成功
        response.setCode(ResponseCode.SUCCESS);
        break;
    case MESSAGE_WAS_REMOVING:	//消息存放在下一个commitLog中
        response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY);	//消息重试
        break;
    case NO_MATCHED_LOGIC_QUEUE:	//未找到队列
    case NO_MESSAGE_IN_QUEUE:	//队列中未包含消息
        if (0 != requestHeader.getQueueOffset()) {
    
    
            response.setCode(ResponseCode.PULL_OFFSET_MOVED);
            requestHeader.getQueueOffset(),
            getMessageResult.getNextBeginOffset(),
            requestHeader.getTopic(),
            requestHeader.getQueueId(),
            requestHeader.getConsumerGroup()
            );
        } else {
    
    
            response.setCode(ResponseCode.PULL_NOT_FOUND);
        }
        break;
    case NO_MATCHED_MESSAGE:	//未找到消息
        response.setCode(ResponseCode.PULL_RETRY_IMMEDIATELY);
        break;
    case OFFSET_FOUND_NULL:	//消息物理偏移量为空
        response.setCode(ResponseCode.PULL_NOT_FOUND);
        break;
    case OFFSET_OVERFLOW_BADLY:	//offset越界
        response.setCode(ResponseCode.PULL_OFFSET_MOVED);
        // XXX: warn and notify me
        log.info("the request offset: {} over flow badly, broker max offset: {}, consumer: {}",
                requestHeader.getQueueOffset(), getMessageResult.getMaxOffset(), channel.remoteAddress());
        break;
    case OFFSET_OVERFLOW_ONE:	//offset在队列中未找到
        response.setCode(ResponseCode.PULL_NOT_FOUND);
        break;
    case OFFSET_TOO_SMALL:	//offset未在队列中
        response.setCode(ResponseCode.PULL_OFFSET_MOVED);
        requestHeader.getConsumerGroup(), 
        requestHeader.getTopic(), 
        requestHeader.getQueueOffset(),
        getMessageResult.getMinOffset(), channel.remoteAddress());
        break;
    default:
        assert false;
        break;
}
...
//如果CommitLog标记可用,并且当前Broker为主节点,则更新消息消费进度
boolean storeOffsetEnable = brokerAllowSuspend;
storeOffsetEnable = storeOffsetEnable && hasCommitOffsetFlag;
storeOffsetEnable = storeOffsetEnable
    && this.brokerController.getMessageStoreConfig().getBrokerRole() != BrokerRole.SLAVE;
if (storeOffsetEnable) {
    
    
    this.brokerController.getConsumerOffsetManager().commitOffset(RemotingHelper.parseChannelRemoteAddr(channel),
        requestHeader.getConsumerGroup(), requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getCommitOffset());
}
2.5.4.3.3, message pull client process message

insert image description here
Code: MQClientAPIImpl#processPullResponse

private PullResult processPullResponse(
    final RemotingCommand response) throws MQBrokerException, RemotingCommandException {
    
    
    PullStatus pullStatus = PullStatus.NO_NEW_MSG;
   	//判断响应结果
    switch (response.getCode()) {
    
    
        case ResponseCode.SUCCESS:
            pullStatus = PullStatus.FOUND;
            break;
        case ResponseCode.PULL_NOT_FOUND:
            pullStatus = PullStatus.NO_NEW_MSG;
            break;
        case ResponseCode.PULL_RETRY_IMMEDIATELY:
            pullStatus = PullStatus.NO_MATCHED_MSG;
            break;
        case ResponseCode.PULL_OFFSET_MOVED:
            pullStatus = PullStatus.OFFSET_ILLEGAL;
            break;

        default:
            throw new MQBrokerException(response.getCode(), response.getRemark());
    }
	//解码响应头
    PullMessageResponseHeader responseHeader =
        (PullMessageResponseHeader) response.decodeCommandCustomHeader(PullMessageResponseHeader.class);
	//封装PullResultExt返回
    return new PullResultExt(pullStatus, responseHeader.getNextBeginOffset(), responseHeader.getMinOffset(),
        responseHeader.getMaxOffset(), null, responseHeader.getSuggestWhichBrokerId(), response.getBody());
}

PullResult class

private final PullStatus pullStatus;	//拉取结果
private final long nextBeginOffset;	//下次拉取偏移量
private final long minOffset;	//消息队列最小偏移量
private final long maxOffset;	//消息队列最大偏移量
private List<MessageExt> msgFoundList;	//拉取的消息列表

代码:DefaultMQPushConsumerImpl$PullCallback#OnSuccess

//将拉取到的消息存入processQueue
boolean dispatchToConsume = processQueue.putMessage(pullResult.getMsgFoundList());
//将processQueue提交到consumeMessageService中供消费者消费
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(
    pullResult.getMsgFoundList(),
    processQueue,
    pullRequest.getMessageQueue(),
    dispatchToConsume);
//如果pullInterval大于0,则等待pullInterval毫秒后将pullRequest对象放入到PullMessageService中的pullRequestQueue队列中
if (DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval() > 0) {
    
    
    DefaultMQPushConsumerImpl.this.executePullRequestLater(pullRequest,
        DefaultMQPushConsumerImpl.this.defaultMQPushConsumer.getPullInterval());
} else {
    
    
    DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);
}
2.5.4.3.4 Summary of message pull

insert image description here

2.5.4.4. Analysis of message pull long polling mechanism

RocketMQ does not actually implement the message push mode, but the consumer actively pulls the message from the message server. The RocketMQ push mode initiates a message pull request to the message server in a loop. If the message consumer pulls the message from RocketMQ, the message does not reach the consumer. When queuing, if the long polling mechanism is not enabled, the server will wait for the shortPollingTimeMills time (suspended) before judging whether the message has arrived at the specified message queue. If the message has not yet arrived, it will prompt the client to pull the message PULL—NOT —FOUND (the message does not exist); if the long polling mode is enabled, RocketMQ will poll every 5s to check whether the message is reachable, and at the same time, once a message arrives, it will immediately notify the suspended thread to verify whether the message is of interest to itself If it is, extract the message from the CommitLog file and return it to the message pull client, otherwise until the suspension timeout, the timeout period is encapsulated in the request parameter by the message puller when the message is pulled, PUSH mode is 15s, PULL The mode is set by DefaultMQPullConsumer#setBrokerSuspendMaxTimeMillis. RocketMQ enables the long polling mode by configuring longPollingEnable to true on the Broker client.

Code: PullMessageProcessor#processRequest

//当没有拉取到消息时,通过长轮询方式继续拉取消息
case ResponseCode.PULL_NOT_FOUND:
    if (brokerAllowSuspend && hasSuspendFlag) {
    
    
        long pollingTimeMills = suspendTimeoutMillisLong;
        if (!this.brokerController.getBrokerConfig().isLongPollingEnable()) {
    
    
            pollingTimeMills = this.brokerController.getBrokerConfig().getShortPollingTimeMills();
        }

        String topic = requestHeader.getTopic();
        long offset = requestHeader.getQueueOffset();
        int queueId = requestHeader.getQueueId();
        //构建拉取请求对象
        PullRequest pullRequest = new PullRequest(request, channel, pollingTimeMills,
            this.brokerController.getMessageStore().now(), offset, subscriptionData, messageFilter);
        //处理拉取请求
        this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest);
        response = null;
        break;
    }

PullRequestHoldService way to achieve long polling

代码:PullRequestHoldService#suspendPullRequest

//将拉取消息请求,放置在ManyPullRequest集合中
public void suspendPullRequest(final String topic, final int queueId, final PullRequest pullRequest) {
    
    
    String key = this.buildKey(topic, queueId);
    ManyPullRequest mpr = this.pullRequestTable.get(key);
    if (null == mpr) {
    
    
        mpr = new ManyPullRequest();
        ManyPullRequest prev = this.pullRequestTable.putIfAbsent(key, mpr);
        if (prev != null) {
    
    
            mpr = prev;
        }
    }

    mpr.addPullRequest(pullRequest);
}

Code: PullRequestHoldService#run

public void run() {
    
    
    log.info("{} service started", this.getServiceName());
    while (!this.isStopped()) {
    
    
        try {
    
    
            //如果开启长轮询每隔5秒判断消息是否到达
            if (this.brokerController.getBrokerConfig().isLongPollingEnable()) {
    
    
                this.waitForRunning(5 * 1000);
            } else {
    
    
                //没有开启长轮询,每隔1s再次尝试
              this.waitForRunning(this.brokerController.getBrokerConfig().getShortPollingTimeMills());
            }

            long beginLockTimestamp = this.systemClock.now();
            this.checkHoldRequest();
            long costTime = this.systemClock.now() - beginLockTimestamp;
            if (costTime > 5 * 1000) {
    
    
                log.info("[NOTIFYME] check hold request cost {} ms.", costTime);
            }
        } catch (Throwable e) {
    
    
            log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }

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

Code: PullRequestHoldService#checkHoldRequest

//遍历拉取任务
private void checkHoldRequest() {
    
    
    for (String key : this.pullRequestTable.keySet()) {
    
    
        String[] kArray = key.split(TOPIC_QUEUEID_SEPARATOR);
        if (2 == kArray.length) {
    
    
            String topic = kArray[0];
            int queueId = Integer.parseInt(kArray[1]);
            //获得消息偏移量
            final long offset = this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId);
            try {
    
    
                //通知有消息达到
                this.notifyMessageArriving(topic, queueId, offset);
            } catch (Throwable e) {
    
    
                log.error("check hold request failed. topic={}, queueId={}", topic, queueId, e);
            }
        }
    }
}

代码:PullRequestHoldService#notifyMessageArriving

//如果拉取消息偏移大于请求偏移量,如果消息匹配调用executeRequestWhenWakeup处理消息
if (newestOffset > request.getPullFromThisOffset()) {
    
    
    boolean match = request.getMessageFilter().isMatchedByConsumeQueue(tagsCode,
        new ConsumeQueueExt.CqExtUnit(tagsCode, msgStoreTime, filterBitMap));
    // match by bit map, need eval again when properties is not null.
    if (match && properties != null) {
    
    
        match = request.getMessageFilter().isMatchedByCommitLog(null, properties);
    }

    if (match) {
    
    
        try {
    
    
            this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
                request.getRequestCommand());
        } catch (Throwable e) {
    
    
            log.error("execute request when wakeup failed.", e);
        }
        continue;
    }
}
//如果过期时间超时,则不继续等待将直接返回给客户端消息未找到
if (System.currentTimeMillis() >= (request.getSuspendTimestamp() + request.getTimeoutMillis())) {
    
    
    try {
    
    
        this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(request.getClientChannel(),
            request.getRequestCommand());
    } catch (Throwable e) {
    
    
        log.error("execute request when wakeup failed.", e);
    }
    continue;
}

If the long polling mechanism is enabled, the PullRequestHoldService will wake up every 5s to try to detect whether there is a new message coming before responding to the client, or until it times out before responding to the client. The real-time performance of the message is relatively poor. In order to avoid this In this case, RocketMQ introduces another mechanism: wake up the suspended thread to trigger a check when the message arrives.

DefaultMessageStore$ReputMessageService mechanism

Code: DefaultMessageStore#start

//长轮询入口
this.reputMessageService.setReputFromOffset(maxPhysicalPosInLogicQueue);
this.reputMessageService.start();

Code: DefaultMessageStore$ReputMessageService#run

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

    while (!this.isStopped()) {
    
    
        try {
    
    
            Thread.sleep(1);
            //长轮询核心逻辑代码入口
            this.doReput();
        } catch (Exception e) {
    
    
            DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }

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

Code: DefaultMessageStore$ReputMessageService#deReput

//当新消息达到是,进行通知监听器进行处理
if (BrokerRole.SLAVE != DefaultMessageStore.this.getMessageStoreConfig().getBrokerRole()
    && DefaultMessageStore.this.brokerConfig.isLongPollingEnable()) {
    
    
    DefaultMessageStore.this.messageArrivingListener.arriving(dispatchRequest.getTopic(),
        dispatchRequest.getQueueId(), dispatchRequest.getConsumeQueueOffset() + 1,
        dispatchRequest.getTagsCode(), dispatchRequest.getStoreTimestamp(),
        dispatchRequest.getBitMap(), dispatchRequest.getPropertiesMap());
}

代码:NotifyMessageArrivingListener#arriving

public void arriving(String topic, int queueId, long logicOffset, long tagsCode,
    long msgStoreTime, byte[] filterBitMap, Map<String, String> properties) {
    
    
    this.pullRequestHoldService.notifyMessageArriving(topic, queueId, logicOffset, tagsCode,
        msgStoreTime, filterBitMap, properties);
}

2.5.5 Message queue load and redistribution mechanism

RocketMQ message queue reallocation is implemented by the RebalanceService thread. An MQClientInstance holds a RebalanceService implementation and starts with the MQClientInstance.

Code: RebalanceService#run

public void run() {
    
    
    log.info(this.getServiceName() + " service started");
	//RebalanceService线程默认每隔20s执行一次mqClientFactory.doRebalance方法
    while (!this.isStopped()) {
    
    
        this.waitForRunning(waitInterval);
        this.mqClientFactory.doRebalance();
    }

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

Code: MQClientInstance#doRebalance

public void doRebalance() {
    
    
    //MQClientInstance遍历以注册的消费者,对消费者执行doRebalance()方法
    for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
    
    
        MQConsumerInner impl = entry.getValue();
        if (impl != null) {
    
    
            try {
    
    
                impl.doRebalance();
            } catch (Throwable e) {
    
    
                log.error("doRebalance exception", e);
            }
        }
    }
}

Code: RebalanceImpl#doRebalance

//遍历订阅消息对每个主题的订阅的队列进行重新负载
public void doRebalance(final boolean isOrder) {
    
    
    Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
    if (subTable != null) {
    
    
        for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
    
    
            final String topic = entry.getKey();
            try {
    
    
                this.rebalanceByTopic(topic, isOrder);
            } catch (Throwable e) {
    
    
                if (!topic.startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
    
    
                    log.warn("rebalanceByTopic Exception", e);
                }
            }
        }
    }

    this.truncateMessageQueueNotMyTopic();
}

Code: RebalanceImpl#rebalanceByTopic

//从主题订阅消息缓存表中获取主题的队列信息
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
//查找该主题订阅组所有的消费者ID
List<String> cidAll = this.mQClientFactory.findConsumerIdList(topic, consumerGroup);

//给消费者重新分配队列
if (mqSet != null && cidAll != null) {
    
    
    List<MessageQueue> mqAll = new ArrayList<MessageQueue>();
    mqAll.addAll(mqSet);

    Collections.sort(mqAll);
    Collections.sort(cidAll);

    AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;

    List<MessageQueue> allocateResult = null;
    try {
    
    
        allocateResult = strategy.allocate(
            this.consumerGroup,
            this.mQClientFactory.getClientId(),
            mqAll,
            cidAll);
    } catch (Throwable e) {
    
    
        log.error("AllocateMessageQueueStrategy.allocate Exception. allocateMessageQueueStrategyName={}", strategy.getName(),
            e);
        return;
    }

RocketMQ provides 5 load balancing distribution algorithms by default

AllocateMessageQueueAveragely:平均分配
举例:8个队列q1,q2,q3,q4,q5,a6,q7,q8,消费者3:c1,c2,c3
分配如下:
c1:q1,q2,q3
c2:q4,q5,a6
c3:q7,q8
AllocateMessageQueueAveragelyByCircle:平均轮询分配
举例:8个队列q1,q2,q3,q4,q5,a6,q7,q8,消费者3:c1,c2,c3
分配如下:
c1:q1,q4,q7
c2:q2,q5,a8
c3:q3,q6

Note: The allocation of message queues follows that one consumer can be allocated to multiple queues, but the same message queue will only be allocated to one consumer, so if the number of consumers is greater than the number of message queues, some consumers cannot consume messages.

2.5.6. Message consumption process

PullMessageService is responsible for pulling the message from the message queue. After pulling the message from the remote server, the message is stored in the ProcessQueue message queue processing queue, and then the ConsumeMessageService#submitConsumeRequest method is called to consume the message, and the thread pool is used to consume the message, ensuring that the message is pulled Decoupling from message consumption. ConsumeMessageService supports sequential messages and concurrent messages, the core class diagram is as follows:
insert image description here
concurrent message consumption

代码:ConsumeMessageConcurrentlyService#submitConsumeRequest

//消息批次单次
final int consumeBatchSize = this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize();
//msgs.size()默认最多为32条。
//如果msgs.size()小于consumeBatchSize,则直接将拉取到的消息放入到consumeRequest,然后将consumeRequest提交到消费者线程池中
if (msgs.size() <= consumeBatchSize) {
    
    
    ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
    try {
    
    
        this.consumeExecutor.submit(consumeRequest);
    } catch (RejectedExecutionException e) {
    
    
        this.submitConsumeRequestLater(consumeRequest);
    }
}else{
    
    	//如果拉取的消息条数大于consumeBatchSize,则对拉取消息进行分页
       for (int total = 0; total < msgs.size(); ) {
    
    
   		    List<MessageExt> msgThis = new ArrayList<MessageExt>(consumeBatchSize);
   		    for (int i = 0; i < consumeBatchSize; i++, total++) {
    
    
   		        if (total < msgs.size()) {
    
    
   		            msgThis.add(msgs.get(total));
   		        } else {
    
    
   		            break;
   		        }
   		
   		    ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue);
   		    try {
    
    
   		        this.consumeExecutor.submit(consumeRequest);
   		    } catch (RejectedExecutionException e) {
    
    
   		        for (; total < msgs.size(); total++) {
    
    
   		            msgThis.add(msgs.get(total));
   		 
   		        this.submitConsumeRequestLater(consumeRequest);
   		    }
   		}
}

代码:ConsumeMessageConcurrentlyService$ConsumeRequest#run

//检查processQueue的dropped,如果为true,则停止该队列消费。
if (this.processQueue.isDropped()) {
    
    
    log.info("the message queue not be able to consume, because it's dropped. group={} {}", ConsumeMessageConcurrentlyService.this.consumerGroup, this.messageQueue);
    return;
}

...
//执行消息处理的钩子函数
if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
    
    
    consumeMessageContext = new ConsumeMessageContext();
    consumeMessageContext.setNamespace(defaultMQPushConsumer.getNamespace());
    consumeMessageContext.setConsumerGroup(defaultMQPushConsumer.getConsumerGroup());
    consumeMessageContext.setProps(new HashMap<String, String>());
    consumeMessageContext.setMq(messageQueue);
    consumeMessageContext.setMsgList(msgs);
    consumeMessageContext.setSuccess(false);
    ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookBefore(consumeMessageContext);
}
...
//调用应用程序消息监听器的consumeMessage方法,进入到具体的消息消费业务处理逻辑
status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);

//执行消息处理后的钩子函数
if (ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.hasHook()) {
    
    
    consumeMessageContext.setStatus(status.toString());
    consumeMessageContext.setSuccess(ConsumeConcurrentlyStatus.CONSUME_SUCCESS == status);
    ConsumeMessageConcurrentlyService.this.defaultMQPushConsumerImpl.executeHookAfter(consumeMessageContext);
}

2.5.7. Timing message mechanism

Timing message means that after the message is sent to the Broker, it will not be consumed by consumers immediately but will be consumed after a specific time. RocketMQ does not support arbitrary time precision. If you want to support arbitrary time precision timing scheduling, you will inevitably need Sorting messages at the Broker layer, coupled with the consideration of persistence, will inevitably bring huge performance consumption, so RocketMQ only supports a specific level of delayed messages. The message delay level is configured on the Broker side through messageDelayLevel, the default is "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h", delayLevel=1 means delay message 1s, delayLevel=2 means delay 5s, and so on .

The RocketMQ timing message implementation class is ScheduleMessageService, which is created in DefaultMessageStore. Load the class by calling the load method in DefaultMessageStore and call the start method to start.

Code: ScheduleMessageService#load

//加载延迟消息消费进度的加载与delayLevelTable的构造。延迟消息的进度默认存储路径为/store/config/delayOffset.json
public boolean load() {
    
    
    boolean result = super.load();
    result = result && this.parseDelayLevel();
    return result;
}

Code: ScheduleMessageService#start

//遍历延迟队列创建定时任务,遍历延迟级别,根据延迟级别level从offsetTable中获取消费队列的消费进度。如果不存在,则使用0
for (Map.Entry<Integer, Long> entry : this.delayLevelTable.entrySet()) {
    
    
    Integer level = entry.getKey();
    Long timeDelay = entry.getValue();
    Long offset = this.offsetTable.get(level);
    if (null == offset) {
    
    
        offset = 0L;
    }

    if (timeDelay != null) {
    
    
        this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
    }
}

//每隔10s持久化一次延迟队列的消息消费进度
this.timer.scheduleAtFixedRate(new TimerTask() {
    
    

    @Override
    public void run() {
    
    
        try {
    
    
            if (started.get()) ScheduleMessageService.this.persist();
        } catch (Throwable e) {
    
    
            log.error("scheduleAtFixedRate flush exception", e);
        }
    }
}, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());

scheduling mechanism

After the start method of ScheduleMessageService starts, a scheduling task will be created for each delay level, and each delay level corresponds to a message consumption queue under the topic SCHEDULE_TOPIC_XXXX. The implementation class of scheduled scheduling tasks is DeliverDelayedMessageTimerTask, and the core implementation method is executeOnTimeup

代码:ScheduleMessageService$DeliverDelayedMessageTimerTask#executeOnTimeup

//根据队列ID与延迟主题查找消息消费队列
ConsumeQueue cq =
    ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(SCHEDULE_TOPIC,
        delayLevel2QueueId(delayLevel));
...
//根据偏移量从消息消费队列中获取当前队列中所有有效的消息
SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);

...
//遍历ConsumeQueue,解析消息队列中消息
for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
    
    
    long offsetPy = bufferCQ.getByteBuffer().getLong();
    int sizePy = bufferCQ.getByteBuffer().getInt();
    long tagsCode = bufferCQ.getByteBuffer().getLong();

    if (cq.isExtAddr(tagsCode)) {
    
    
        if (cq.getExt(tagsCode, cqExtUnit)) {
    
    
            tagsCode = cqExtUnit.getTagsCode();
        } else {
    
    
            //can't find ext content.So re compute tags code.
            log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}",
                tagsCode, offsetPy, sizePy);
            long msgStoreTime = defaultMessageStore.getCommitLog().pickupStoreTimestamp(offsetPy, sizePy);
            tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
        }
    }

    long now = System.currentTimeMillis();
    long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);
    
    ...
    //根据消息偏移量与消息大小,从CommitLog中查找消息.
  	MessageExt msgExt =
   ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
       offsetPy, sizePy);
} 

2.5.8. Sequential messages

The sequential message implementation class is org.apache.rocketmq.client.impl.consumer.ConsumeMessageOrderlyService

Code: ConsumeMessageOrderlyService#start

public void start() {
    
    
    //如果消息模式为集群模式,启动定时任务,默认每隔20s执行一次锁定分配给自己的消息消费队列
    if (MessageModel.CLUSTERING.equals(ConsumeMessageOrderlyService.this.defaultMQPushConsumerImpl.messageModel())) {
    
    
        this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                ConsumeMessageOrderlyService.this.lockMQPeriodically();
            }
        }, 1000 * 1, ProcessQueue.REBALANCE_LOCK_INTERVAL, TimeUnit.MILLISECONDS);
    }
}

代码:ConsumeMessageOrderlyService#submitConsumeRequest

//构建消息任务,并提交消费线程池中
public void submitConsumeRequest(
    final List<MessageExt> msgs,
    final ProcessQueue processQueue,
    final MessageQueue messageQueue,
    final boolean dispathToConsume) {
    
    
    if (dispathToConsume) {
    
    
        ConsumeRequest consumeRequest = new ConsumeRequest(processQueue, messageQueue);
        this.consumeExecutor.submit(consumeRequest);
    }
}

代码:ConsumeMessageOrderlyService$ConsumeRequest#run

//如果消息队列为丢弃,则停止本次消费任务
if (this.processQueue.isDropped()) {
    
    
    log.warn("run, the message queue not be able to consume, because it's dropped. {}", this.messageQueue);
    return;
}
//从消息队列中获取一个对象。然后消费消息时先申请独占objLock锁。顺序消息一个消息消费队列同一时刻只会被一个消费线程池处理
final Object objLock = messageQueueLock.fetchLockObject(this.messageQueue);
synchronized (objLock) {
    
    
	...
}

2.5.9 Summary

RocketMQ message consumption methods are cluster mode and broadcast mode.

The message queue load is carried out by the RebalanceService thread every 20s by default. According to the number of consumers in the current consumer group and the number of topic queues, the queue is allocated according to a certain load algorithm. The allocation principle is that the same consumer can be allocated multiple A message consumption queue, the same message consumption queue will only be allocated to one consumer at the same time.

Message pulling is performed by the PullMessageService thread based on the pull task created by the RebalanceService thread. By default, 32 messages are pulled each time, and they are submitted to the consumer thread to continue the next message pull. If the message consumption is too slow to generate message accumulation, it will trigger message consumption pull flow control.

Concurrent message consumption means that threads in the consuming thread pool can consume messages from the same message queue concurrently. After successful consumption, the smallest message offset in the message queue is taken out as the message consumption progress offset and stored in the message consumption progress storage file In the cluster mode message consumption progress is stored in the Broker (message server), and the broadcast mode message consumption progress is stored in the consumer side.

RocketMQ does not support timing scheduling messages with arbitrary precision, but only supports custom message delay levels, such as 1s, 2s, 5s, etc., which can be set by setting messageDelayLevel in the broker configuration file.

Sequential messages generally use the cluster mode, which means that the threads in the thread pool in the message consumer can only consume the message consumption queue serially. The most essential difference between concurrent message consumption is that the message consumption queue must be successfully locked during message consumption, and the lock occupancy of the message consumption queue will be stored on the Broker side.

Guess you like

Origin blog.csdn.net/shuai_h/article/details/130897455