可以,而且这是分区数多于消费者数时的常态:一个消费者会同时持有多个分区。真正的问题是它读到的顺序是什么样的。这次往一个 6 分区的 topic 里按序号 0 到 599 依次发送 600 条消息(37 个不同的 key),再用同一个消费组里的单个 Java Consumer 读回来,检查它持有哪些分区、消息以什么顺序到达,以及"发送顺序"和"消费顺序"到底还一不一致。
命令 · java // topic multi-part-demo: 6 分区、3 副本。Producer 依次发送 value=0..599,key = "key-" + (i % 37) // 单个 Consumer:subscribe 后打印 assignment,记录每条的 (partition, offset, 发送序号)
assignment = [0, 1, 2, 3, 4, 5] 消费到 600 条,poll 次数 8,单次 poll 最多同时包含 4 个分区 各分区条数 = [49, 162, 179, 97, 64, 49] 分区内 offset/发送顺序 不递增的次数 = 0
只有一个消费者的组里,它自动持有了全部 6 个分区。600 条全部读到,poll 8 次;单次 poll() 返回的批次最多同时含 4 个分区的数据。每个分区内部,消费顺序的 offset 和当初的发送序号都严格递增,不递增的次数是 0。各分区的条数很不均匀(49 到 179):这是 37 个 key 经过哈希分到 6 个分区的结果,key 的个数太少,分布不均,与消费者无关。
命令 · java // 记录到达顺序,统计: // 1) 每条消息相对已读到的最大发送序号是否"更晚才读到" // 2) 按到达顺序把连续来自同一分区的记录压缩成段
第一次运行,前 24 条 (分区:发送序号) = 0:1 0:17 0:19 0:38 0:54 0:56 0:75 0:91 0:93 0:112 ... 0:278 第二次运行(新消费组重新读同一批数据),前 24 条 = 2:2 2:5 2:6 2:10 2:12 2:23 2:24 2:25 2:30 ... 连续来自同一分区的最长一段 = 179 乱序条数 = 420 / 600 按到达顺序压缩后的分区段 = p2x179 p3x97 p1x162 p5x49 p0x49 p4x64
第二次运行(另起一个新的消费组,从头读同一批 600 条)里,这个消费者先把分区 2 的 179 条整块读完,然后是分区 3 的 97 条、分区 1 的 162 条、分区 5、分区 0、分区 4,每个分区的数据几乎是一整块连续到达的。发送序号是 0 到 599 依次递增,所以有 420 条消息(70%)是在比它更晚发送的消息之后才被读到的。两次运行的分区顺序也不一样:第一次先读分区 0,第二次先读分区 2。所以"同一个消费者消费多个分区"能保证的只有分区内顺序;跨分区的先后顺序既不等于发送顺序,也不是稳定的。这里我把整批数据放在一个 topic 里静态读取,到达顺序的块状特征在有持续新消息写入时会更混杂,这次没有测持续写入的场景。
一个消费者可以同时消费多个分区,分区数多于消费者数时这是必然情况。它读到的数据满足"每个分区内按 offset 递增、与发送顺序一致",但跨分区没有任何顺序保证:本次实验里 600 条消息有 420 条出现了"比它更晚发送的消息先被读到",而且每次运行分区的读取先后可能不同。业务上需要严格先后关系的消息,应该用同一个 key 让它们进入同一个分区。
每一块的分区号和条数,都是本文上面一节运行Java Consumer得到的原始到达顺序。