总述
除了上一篇描述的 namespace 和 zk 不同之外。 RocketMq 和 Kafaka 之间最大的不同就是存储架构的不同,具体来看:
针对两个 Broker, 需要保存 topic A 和 B,每个 topic 两个 partition 的场景
Kafka 如下:
RocketMq 如下:
这里最重要的区别就是:Kafka 的同步复制组是以 partition 为单位的,而 RocketMq 的复制组是以 broker 为单位的。一眼就可以看出,kafka 的写负载会更均匀,每一个 broker 都会有流量。而 RocketMq 则只能在 Leader broker 上有写流量,其他 broker 只能作为高可用的备份。
据 ali 官方的说法,RocketMq 的吞吐量为 Kafka 的 3/4 左右,可能就和这点有关吧 (具体如何测试的不清楚)。但是 RocketMq 的这种选择肯定是有自己的考虑的。猜测有如下几点:
1. Kafka 的复制组的数量会远大于 RocketMq,复制组的一致性协议显然不是免费的,而是有很大的开销。如果 partition 的数量很多,kafka 的吞吐量一定会下降
2. 当一个 broker 上线或下线后,Kafka 需要重平衡数据,而 RocketMq 在新 broker 上线时只需要做 catch up 就可以了,会比 kafka 简单很多。当然,话说回来,两种 broker 要干的活也不同,不可相提并论。
好吧,无论怎样,架构的这一差异导致 Kafka 和 RocketMq 在文件格式,读写分离,重平衡等方面的巨大不同,下面详细的说一下:
Partition VS Queue
Kafka 的 topic 被分为多个 partition,每个 partition 主要由三类文件组成:
- log 文件,用于保存消息内容
- index 文件,用于做消息的 hash 索引
- timeindex 文件,用于做消息的基于消息时间戳的索引
RocketMq 的 topic 看上去被分割为多个 Queue,但这里 “分割” 是和 Kafka 有差别的。首先,消息进入 broker 后,会 append 到一个叫 commitlog 的文件中,这是 RocketMq 唯一存储消息具体内容的地方。这个基于日志的顺序写入的存储系统并不会区分 topic。在消息写入 commitlog 之后才会异步的转存至 topic 的某个 Queue 文件。Queue 类似于一个 index,其中的 item 为固定大小,并指向 commitlog 的一个 offset
Queue 中一个 item = commitLog offset (8字节) + 消息长度 (4字节) + 消息tag hashcode (8字节)
Topic 的消费端不会直接看到 commitlog,只能看到本地负责消费的 Queue,并不停的从 Queue 中拉取最新消息(拉消息具体内容时需要从 commitlog 指定的 offset 读取),并更新这个 queue 的消费进度。
所以,Kafka 的 partition 和 RocketMq 的 queue 有着本质的区别。Kafka 的 partition 是存储消息的单元,其中包含了数据,索引。而 RocketMq 的 queue 则非常简单,更像是一个为了方便消费负载均衡而存在的索引。
读写分离
Kafka 的架构决定了其写流量会比较均匀的分布到各个 broker 中。而 RocketMq 的写流量则完全集中在 leads 角色的 broker 上。以上面的 case 为例,如果 topic A,topic B 的 partition 0 和 partition 1写入流量均匀,则 Kafka 的 broker0 和 broker1 的写流量也是均匀的。而如果是 RockerMq,流量全部集中在 broker0。broker1 无流量。
消息中间件的存储,是典型的 写TPS 远远大于 读QPS 的场景。对于Kafka 来说,写流量已经做到了均匀分布了,读流量要不要分散到 follower 上意义不大。因此,kafka 的做法是读流量也必须走 leader partition,读写不分离。而对于 RocketMq,写流量集中在 broker0, 如果能将读流量分散到其他 broker 上,会起到一定的分散负载的效果。因此,即使有数据延迟的风险,RocketMq 仍然选择支持一定程度的读写分离。具体的做法是:
在 broker 拉取消息时,检查 commitlog 中当前消息写入 offset 和 消息拉取的 offset 的间隔。如果间隔大于物理内存大小 的 40%(默认),则会被认为当前 group 消费此 queue 的进度太慢了,建议从 follower 去拉取。
详见 DefaultMessageStore#getMessage (拉取消息) 方法中:
long diff = maxOffsetPy - maxPhyOffsetPulling;
long memory = (long) (StoreUtil.TOTAL_PHYSICAL_MEMORY_SIZE * (this.messageStoreConfig.getAccessMessageInMemoryMaxRatio() / 100.0));
getResult.setSuggestPullingFromSlave(diff > memory);
这里 maxOffsetPy 为 commitlog 的当前写入 offset,maxPhyOffsetPulling 为当前本拉取的 offset.
当 suggestPullingFromSlave 为 true 时,则之后会从订阅信息中获取一个备用的 brokerId 放入 SuggestWhichBrokerId 中。
详见 PullMessageProcessor#composeResponseHeader 方法中:
if (getMessageResult.isSuggestPullingFromSlave()) {
responseHeader.setSuggestWhichBrokerId(subscriptionGroupConfig.getWhichBrokerWhenConsumeSlowly());
}
在客户端中,每次从 broker 拉取到消息之后,都会更新这个来自 broker 的 suggest,并会影响下一次的拉取行为。
详见 PullAPIWrapper#processPullResult
(待续)