吞吐量对比
Kafka、RabbitMQ、RocketMQ消息中间件的对比------ 消息发送性能
测试目的
对比Kafka、RabbitMQ、RocketMQ发送小消息(124字节)的性能。这次压测我们只关注服务端的性能指标,所以压测的标准是:
不断增加发送端的压力,直到系统吞吐量不再上升,而响应时间拉长。这时服务端已出现性能瓶颈,可以获得相应的系统最佳吞吐量。
在同步发送场景中,三个消息中间件的表现区分明显:
Kafka的吞吐量高达17.3w/s,不愧是高吞吐量消息中间件的行业老大。这主要取决于它的队列模式保证了写磁盘的过程是线性IO。此时broker磁盘IO已达瓶颈。
RocketMQ也表现不俗,吞吐量在11.6w/s,磁盘IO %util已接近100%。RocketMQ的消息写入内存后即返回ack,由单独的线程专门做刷盘的操作,所有的消息均是顺序写文件。
RabbitMQ的吞吐量5.95w/s,CPU资源消耗较高。它支持AMQP协议,实现非常重量级,为了保证消息的可靠性在吞吐量上做了取舍。我们还做了RabbitMQ在消息持久化场景下的性能测试,吞吐量在2.6w/s左右。
测试结论
在服务端处理同步发送的性能上,Kafka>RocketMQ>RabbitMQ。
测试环境
服务端为单机部署,机器配置如下:
CPU 24核
内存 94G
硬盘 Seagate Constellation ES (SATA 6Gb/s) 2,000,398,934,016 bytes [2.00 TB] 7202 rpm
网卡 1000Mb/s
应用版本:
消息中间件 版本
Kafka 0.8.2
RabbitMQ 3.5.4
RocketMQ 3.4.6
测试脚本
压力端 Jmeter的java客户端
消息大小 128字节
并发数 能达到服务端最大TPS的最优并发
Topic分区数量 8
刷盘策略 Kafka和RocketMQ为异步落盘,RabbitMQ的Queue不开启durable持久化
kafka与RocketMQ的对比
淘宝内部的交易系统使用了淘宝自主研发的Notify消息中间件,使用Mysql作为消息存储媒介,可完全水平扩容,为了进一步降低成本,我们认为存储部分可以进一步优化,2011年初,Linkin开源了Kafka这个优秀的消息中间件,淘宝中间件团队在对Kafka做过充分Review之后,Kafka无限消息堆积,高效的持久化速度吸引了我们,但是同时发现这个消息系统主要定位于日志传输,对于使用在淘宝交易、订单、充值等场景下还有诸多特性不满足,为此我们重新用Java语言编写了RocketMQ,定位于非日志的可靠消息传输(日志场景也OK),目前RocketMQ在阿里集团被广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog分发等场景。
数据可靠性
RocketMQ支持异步实时刷盘,同步刷盘,同步Replication,异步Replication
Kafka使用异步刷盘方式,异步Replication/同步Replication
总结:RocketMQ的同步刷盘在单机可靠性上比Kafka更高,不会因为操作系统Crash,导致数据丢失。
Kafka同步Replication理论上性能低于RocketMQ的同步Replication,原因是Kafka的数据以分区为单位组织,意味着一个Kafka实例上会有几百个数据分区,RocketMQ一个实例上只有一个数据分区,RocketMQ可以充分利用IO Group Commit机制,批量传输数据,配置同步Replication与异步Replication相比,性能损耗约20%~30%,Kafka没有亲自测试过,但是个人认为理论上会低于RocketMQ。
性能对比
Kafka单机写入TPS约在百万条/秒,消息大小10个字节
RocketMQ单机写入TPS单实例约7万条/秒,单机部署3个Broker,可以跑到最高12万条/秒,消息大小10个字节
总结:Kafka的TPS跑到单机百万,主要是由于Producer端将多个小消息合并,批量发向Broker。
RocketMQ为什么没有这么做?
Producer通常使用Java语言,缓存过多消息,GC是个很严重的问题
Producer调用发送消息接口,消息未发送到Broker,向业务返回成功,此时Producer宕机,会导致消息丢失,业务出错
Producer通常为分布式系统,且每台机器都是多线程发送,我们认为线上的系统单个Producer每秒产生的数据量有限,不可能上万。
缓存的功能完全可以由上层业务完成。
单机支持的队列数
Kafka单机超过64个队列/分区,Load会发生明显的飙高现象,队列越多,load越高,发送消息响应时间变长。Kafka分区数无法过多的问题
RocketMQ单机支持最高5万个队列,Load不会发生明显变化
队列多有什么好处?
单机可以创建更多Topic,因为每个Topic都是由一批队列组成
Consumer的集群规模和队列数成正比,队列越多,Consumer集群可以越大
消息投递实时性
Kafka使用短轮询方式,实时性取决于轮询间隔时间,0.8以后版本支持长轮询。
RocketMQ使用长轮询,同Push方式实时性一致,消息的投递延时通常在几个毫秒。
消费失败重试
Kafka消费失败不支持重试。
RocketMQ消费失败支持定时重试,每次重试间隔时间顺延
总结:例如充值类应用,当前时刻调用运营商网关,充值失败,可能是对方压力过多,稍后再调用就会成功,如支付宝到银行扣款也是类似需求。
这里的重试需要可靠的重试,即失败重试的消息不因为Consumer宕机导致丢失。
严格的消息顺序
Kafka支持消息顺序,但是一台Broker宕机后,就会产生消息乱序
RocketMQ支持严格的消息顺序,在顺序消息场景下,一台Broker宕机后,发送消息会失败,但是不会乱序
Mysql Binlog分发需要严格的消息顺序
定时消息
Kafka不支持定时消息
RocketMQ支持两类定时消息
开源版本RocketMQ仅支持定时Level,定时Level用户可定制
阿里云ONS支持定时Level,以及指定的毫秒级别的延时时间
分布式事务消息
Kafka不支持分布式事务消息
阿里云ONS支持分布式定时消息,未来开源版本的RocketMQ也有计划支持分布式事务消息
消息查询
Kafka不支持消息查询
RocketMQ支持根据Message Id查询消息,也支持根据消息内容查询消息(发送消息时指定一个Message Key,任意字符串,例如指定为订单Id)
总结:消息查询对于定位消息丢失问题非常有帮助,例如某个订单处理失败,是消息没收到还是收到处理出错了。
消息回溯
Kafka理论上可以按照Offset来回溯消息
RocketMQ支持按照时间来回溯消息,精度毫秒,例如从一天之前的某时某分某秒开始重新消费消息
总结:典型业务场景如consumer做订单分析,但是由于程序逻辑或者依赖的系统发生故障等原因,导致今天消费的消息全部无效,需要重新从昨天零点开始消费,那么以时间为起点的消息重放功能对于业务非常有帮助。
消费并行度
Kafka的消费并行度依赖Topic配置的分区数,如分区数为10,那么最多10台机器来并行消费(每台机器只能开启一个线程),或者一台机器消费(10个线程并行消费)。即消费并行度和分区数一致。
RocketMQ消费并行度分两种情况
顺序消费方式并行度同Kafka完全一致
乱序方式并行度取决于Consumer的线程数,如Topic配置10个队列,10台机器消费,每台机器100个线程,那么并行度为1000。
消息轨迹
Kafka不支持消息轨迹
阿里云ONS支持消息轨迹
开发语言友好性
Kafka采用Scala编写
RocketMQ采用Java语言编写
Broker端消息过滤
Kafka不支持Broker端的消息过滤
RocketMQ支持两种Broker端消息过滤方式
根据Message Tag来过滤,相当于子topic概念
向服务器上传一段Java代码,可以对消息做任意形式的过滤,甚至可以做Message Body的过滤拆分。
消息堆积能力
理论上Kafka要比RocketMQ的堆积能力更强,不过RocketMQ单机也可以支持亿级的消息堆积能力,我们认为这个堆积能力已经完全可以满足业务需求。
开源社区活跃度
Kafka社区更新较慢
RocketMQ的github社区有250个个人、公司用户登记了联系方式,QQ群超过1000人。
成熟度
Kafka在日志领域比较成熟
RocketMQ在阿里集团内部有大量的应用在使用,每天都产生海量的消息,并且顺利支持了多次天猫双十一海量消息考验,是数据削峰填谷的利器。
批量、重试、异步
1、同步发送、异步发送
2、单笔发送、批量发送
3、发送失败重试
4、消费失败重新消费
5、批量消费
Apache RocketMQ 是一款分布式消息中间件,支持多种类型的消息发送模式。批量发送消息和单笔发送消息是其中两种常见的消息发送方式,它们的主要区别在于性能、资源消耗以及错误处理机制。
单笔发送消息
- 定义:每次只发送一条消息到消息队列。
- 优点 :
- 实现简单,易于理解和调试。
- 发送失败时,可以精确地知道哪条消息需要重试或采取其他补救措施。
- 缺点 :
- 相对于批量发送,单条发送消息的网络开销较大,效率较低。
- 在高并发场景下,可能会导致较高的系统负载。
批量发送消息
- 定义:将多条消息打包成一个批次进行发送。
- 优点 :
- 减少了网络往返次数,提高了发送效率。
- 降低了客户端与服务端之间的通信开销,提升了整体吞吐量。
- 缺点 :
- 如果整个批次发送失败,可能需要重新发送整个批次,这可能导致部分消息被重复发送。
- 错误处理相对复杂,因为需要跟踪整个批次的状态,并在必要时对失败的消息进行重试。
总结
- 性能:批量发送通常比单笔发送更高效,尤其是在消息体较小的情况下。
- 资源消耗:批量发送减少了网络IO次数,降低了系统资源消耗。
- 错误处理:单笔发送更容易实现精确的错误处理逻辑;批量发送则需要更复杂的逻辑来确保消息的正确性。
在实际应用中,可以根据业务需求和性能要求选择合适的消息发送方式。例如,在对实时性和准确性要求非常高的场景下,可以选择单笔发送;而在追求高吞吐量且能接受一定延迟的场景下,则可以采用批量发送。
在 Apache RocketMQ 中,无论是批量发送消息还是单笔发送消息,都可以选择同步发送或者异步发送的方式。这两种发送方式的选择取决于您的具体需求,比如性能要求、可靠性考虑等。下面分别解释同步发送和异步发送的特点:
同步发送
- 特点 :
- 发送方发出消息后会等待接收方(通常是 Broker)确认接收。
- 只有当消息成功发送并收到确认后,发送方才会继续执行后续操作。
- 提供了较高的消息发送可靠性。
- 发送速度相对较慢,因为需要等待确认响应。
- 应用场景 :
- 当消息的可靠性和一致性非常重要时使用。
- 对于消息发送延迟有一定容忍度的情况。
异步发送
- 特点 :
- 发送方发出消息后立即返回,无需等待接收方的确认。
- 发送方可以通过回调函数等方式处理发送结果。
- 提供了更高的消息发送吞吐量。
- 相对于同步发送,可能需要额外的机制来保证消息的可靠性。
- 应用场景 :
- 当需要尽可能提高消息发送速度和系统吞吐量时使用。
- 对于消息发送延迟要求不高,但对吞吐量有较高要求的情况。
批量发送与单笔发送
- 批量发送 :
- 可以选择同步发送或异步发送。
- 如果选择异步发送,那么整个批次的消息将作为一个单元进行处理。
- 如果批次中有任何消息发送失败,通常需要重试整个批次。
- 单笔发送 :
- 同样可以选择同步发送或异步发送。
- 如果选择异步发送,每条消息独立处理,失败时只需重试该条消息。
总结
- 批量发送:无论同步还是异步,都是针对一批消息进行的操作,能够有效减少网络交互次数,提高发送效率。
- 单笔发送:同步或异步都是针对单条消息进行的操作,更易于实现精确的错误处理。
在实际应用中,您可以根据业务需求选择最适合的发送策略。如果需要保证消息的可靠性和一致性,可以考虑使用同步发送;如果需要追求高吞吐量和低延迟,则可以考虑使用异步发送。
Apache RocketMQ 支持多种发送重试机制,这些机制可以帮助您确保消息能够可靠地发送到目标队列。以下是一些关键的发送重试机制:
消息发送重试机制
1. 同步发送重试
- 描述:当消息通过同步方式发送失败时,RocketMQ 会自动尝试重新发送消息。
- 原理:客户端会在发送消息后等待 Broker 的响应。如果在超时时间内没有接收到确认,客户端将重新发送消息。
- 配置 :可以通过设置客户端的
sendMsgTimeout
参数来控制同步发送的超时时间。
2. 异步发送重试
- 描述:异步发送时,客户端可以配置重试机制,如设置重试次数和重试间隔。
- 原理:客户端在异步发送消息时,如果消息发送失败,可以根据预先配置的策略自动重试。
- 配置 :可以通过设置客户端的
maxReSendTimes
和retryBackoff
参数来控制最大重试次数和重试间隔。
3. 自动重试
- 描述:RocketMQ 客户端在遇到网络问题或其他临时故障时,会自动尝试重新发送消息。
- 配置:可以通过客户端配置文件中的相关参数调整自动重试的行为。
4. 手动重试
- 描述:在某些情况下,开发者可能需要手动控制消息的重试过程。
- 原理:可以在回调函数中捕获异常并决定是否重试。
- 示例:使用异步发送时,可以在回调函数中检查发送状态,并根据状态决定是否重新发送消息。
配置示例
以下是使用 Java 客户端配置同步发送重试的一个示例:
java
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
public class SendDemo {
public static void main(String[] args) throws Exception {
// 创建一个生产者实例
DefaultMQProducer producer = new DefaultMQProducer("YourProducerGroup");
// 设置 NameServer 地址
producer.setNamesrvAddr("localhost:9876");
// 启动生产者实例
producer.start();
// 创建消息对象
Message msg = new Message("YourTopic", "TagA", "OrderID188", "Hello world".getBytes());
// 发送消息
SendResult sendResult = producer.send(msg);
// 关闭生产者实例
producer.shutdown();
}
}
在上述代码中,可以通过以下方式配置同步发送的超时时间:
java
producer.setSendMsgTimeout(3000); // 设置超时时间为3秒
对于异步发送,可以配置重试次数和重试间隔:
java
producer.setMaxReSendTimes(5); // 最大重试次数为5次
producer.setRetryBackoff(1000); // 重试间隔为1秒
注意事项
- 重试次数:需要合理设置重试次数,避免无限重试
发送批量消息
java
@Test
public void testBatchProducer() throws Exception {
// 创建默认的生产者
DefaultMQProducer producer = new DefaultMQProducer("test-batch-group");
//连接namesrv,参数是namesrv的ip地址:端口号
producer.setNamesrvAddr(MqConstant.NAME_SERVER_ADDR);
// 启动实例
producer.start();
List<Message> msgs = Arrays.asList(
new Message("batchTopicTest", "我是一组消息的A消息".getBytes()),
new Message("batchTopicTest", "我是一组消息的B消息".getBytes()),
new Message("batchTopicTest", "我是一组消息的C消息".getBytes())
);
SendResult send = producer.send(msgs);
System.out.println(send);
// 关闭实例
producer.shutdown();
}
producer.send(messages); 调用很多次,算不算批量发送
如果您多次调用 producer.send(messages)
,即使每次调用都发送了一个包含多条消息的列表,这仍然被视为多次发送操作,而不是一次真正的批量发送。真正的批量发送是指一次性发送一个消息列表给 Broker。
示例代码
假设您有多个消息列表,每个列表包含多条消息,您希望分批发送这些列表。下面是分批发送这些列表的示例代码:
java
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.exception.RemotingException;
import java.util.ArrayList;
import java.util.List;
public class BatchMessageSender {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException {
// 创建一个生产者实例
DefaultMQProducer producer = new DefaultMQProducer("BatchMessageProducerGroup");
// 设置 NameServer 地址
producer.setNamesrvAddr("localhost:9876");
// 启动生产者实例
producer.start();
// 创建消息列表
List<List<Message>> messageLists = createMessageLists();
// 分批发送消息列表
for (List<Message> messages : messageLists) {
sendMessages(producer, messages);
}
// 关闭生产者实例
producer.shutdown();
}
private static List<List<Message>> createMessageLists() {
List<List<Message>> messageLists = new ArrayList<>();
for (int i = 0; i < 5; i++) {
List<Message> messages = new ArrayList<>();
for (int j = 0; j < 10; j++) {
String body = "Batch Message " + i + "-" + j;
Message msg = new Message("BatchTopic", "BatchTag", body.getBytes());
messages.add(msg);
}
messageLists.add(messages);
}
return messageLists;
}
private static void sendMessages(DefaultMQProducer producer, List<Message> messages) throws RemotingException, InterruptedException {
// 发送消息列表
producer.send(messages);
}
}
解释
在这个示例中,我们创建了一个 List<List<Message>>
来存储多个消息列表,然后循环遍历这些列表,每次调用 producer.send(messages)
发送一个列表。这意味着虽然每个列表包含多条消息,但实际上是通过多次调用来发送的。
批量发送与多次发送的区别
- 批量发送 :指一次性发送多个消息,即一次调用
producer.send(messages)
发送一个包含多个消息的列表。 - 多次发送 :指通过多次调用
producer.send(messages)
来发送多个消息列表,即使每个列表本身包含多条消息。
性能差异
- 批量发送:减少网络交互次数,提高发送效率。
- 多次发送:虽然每次发送的是一个列表,但如果调用多次,则会增加网络交互次数,相对于批量发送来说效率较低。
因此,如果您想要真正实现批量发送,您应该尽量减少 producer.send()
的调用次数,将更多的消息打包到一个列表中,然后一次性发送。