简介
一个消费者组可以将 Topic 的分区分配给多个消费者。在稳定的组内,每个分区只有一个活跃的负责成员,而另一个组可以独立读取相同的保留记录。这样既能分担工作,又无需让所有应用争用一个全局读取位置。
你将在一个组中启动两个持续运行的控制台客户端,在发布前检查它们的真实分配,并与另一个组的观测结果比较。一个成员离开后,你将等到剩余成员负责两个分区,再发送一个小批次。准备好的发布工具会明确向两个分区发送记录,确保实验中每个分区都有工作;它也会检查向 Broker 的实际交付结果。
这个独立实验提供了一个双分区 Topic、公开的事件样例数据,以及有等待时限的只读工具。你只需掌握基本终端操作;使用样例工具无需编辑 Python 代码。控制台客户端会在后台运行,直到你在最后的步骤中停止它们。请一直使用同一个终端 shell,以便保存进程信息的变量保持可用。
启动两个成员并等待分区归属稳定
这一步,你将在给消费者组分派工作前观察分配情况。准备好的 Topic 有两个分区,尚无记录。检查其元数据:
cd /home/labex/project
kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic orders-shared
以下客户端使用同一个组 orders-team。--command-property 将各项配置值传给消费者客户端。client.id 让它们在报告中显示为 reader-a 和 reader-b。熟悉的自动提交设置会在它们持续轮询时,使读取进度得到提交并趋于稳定。
末尾的 & 在 shell 后台启动客户端,让你能在同一个终端中运行检查命令。$! 是刚启动的后台进程 ID;紧随其后的赋值语句会保存该 ID,便于稍后精确清理对应进程。
shell 保留两条输出流:标准输出用于记录,标准错误用于诊断信息。> 将第一条流写入 JSONL 文件;2> 将错误流写入日志。赋值语句 consumer_a_pid=$! 保存一个 shell 变量,= 两侧没有空格;稍后用 "$consumer_a_pid" 取出该特定进程 ID。普通记录和诊断信息分别写入不同文件,不会打断终端提示符。这些客户端会持续运行,直到接近实验末尾时用限定目标的 kill 命令停止它们:
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders-shared --group orders-team --from-beginning --command-property client.id=reader-a --command-property enable.auto.commit=true --command-property auto.commit.interval.ms=1000 --command-property max.poll.records=1 > /home/labex/project/output/reader-a.jsonl 2> /home/labex/project/output/reader-a.log &
consumer_a_pid=$!
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders-shared --group orders-team --from-beginning --command-property client.id=reader-b --command-property enable.auto.commit=true --command-property auto.commit.interval.ms=1000 --command-property max.poll.records=1 > /home/labex/project/output/reader-b.jsonl 2> /home/labex/project/output/reader-b.log &
consumer_b_pid=$!
等待实际的双成员分配完成,并保存报告:
/home/labex/project/tools/wait-assignment 2 > /home/labex/project/output/two-members.txt
cat /home/labex/project/output/two-members.txt
只读辅助工具会反复查询消费者组成员报告,直到两个带标签的客户端都具有非空且互不重叠的分配,并且两者合起来覆盖分区 0 和 1。它有截止时间,不会编造分配或修改进度。
报告中的 CLIENT-ID 标识 reader-a 或 reader-b,CURRENT-ASSIGNMENT 列出当前实际负责的分区。例如,orders-shared:0 表示该 Topic 的分区 0。目标分配可能仍待生效,因此应检查当前分配列。预期每个成员负责一个分区,不要求 reader-a 获得某个特定分区编号。再均衡会在成员加入或离开时重新分配分区归属。消费者组需要时间才能完成新分配;进程启动并不表示归属已经稳定。稳定的分配是工作分担的证据;仅有两个进程 ID 并不能证明它们分别负责哪些分区。
发布任务并与独立组比较
这一步,你将向两个已分配的分区发送记录,并观察第二个组读取相同历史记录的过程。检查公开的批次数据。initial 批次包含 share1 到 share6;followup 包含 share7 和 share8:
cat /home/labex/project/data/group-batches.json
提供的发布工具将选定的样例数据发送给实际 Broker,等待交付回调,并在交付不完整时失败。务必等分配稳定后再发布六条初始事件:
/home/labex/project/tools/publish-shared-batch initial
/home/labex/project/tools/wait-caught-up orders-team 3 > /home/labex/project/output/team-initial-position.txt
cat /home/labex/project/output/team-initial-position.txt
用于等待追平进度的只读辅助工具会等到两个分区的已提交下一个 offset 都为 3、末尾 offset 为 3、lag 为 0。每个分区收到了三条记录。检查两个客户端观察到的内容:
cat /home/labex/project/output/reader-a.jsonl
cat /home/labex/project/output/reader-b.jsonl
两个文件合起来应包含全部六个初始事件 ID。运行中的分配报告是归属稳定的证据;文件也可能包含分配改变之前的早期观测结果。
现在启动独立且有读取数量限制的组 orders-observer,从保留数据的起点开始读取:
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders-shared --group orders-observer --from-beginning --max-messages 6 --timeout-ms 30000 --command-property enable.auto.commit=true --command-property auto.commit.interval.ms=1000 --command-property max.poll.records=1 > /home/labex/project/output/observer-initial.jsonl
cat /home/labex/project/output/observer-initial.jsonl
/home/labex/project/tools/wait-caught-up orders-observer 3 > /home/labex/project/output/observer-initial-position.txt
cat /home/labex/project/output/observer-initial-position.txt
它应独立收到全部六条记录。由于属于不同的组,它不会从 orders-team 手中夺走分区。两个组可以同时追平进度,同时各自保留独立的已提交位置。
停止一个成员并观察剩余成员的分配
这一步,你将保持 reader-a 运行,只停止 reader-b。kill 向保存的进程发送终止信号,不会停止 Broker。wait 让 shell 完成对子进程的回收。收到信号的进程可能报告非零退出状态,因此末尾的 || true 可避免将这个有意执行的清理命令视为应用失败:
kill "$consumer_b_pid"
wait "$consumer_b_pid" || true
/home/labex/project/tools/wait-assignment 1 > /home/labex/project/output/one-member.txt
cat /home/labex/project/output/one-member.txt
预期消费者组稳定后,reader-a 会负责两个分区。等待工具会在下一次发布前检查真实报告。对于两个分区,添加两个以上的成员会使稳定分配中的部分成员没有分区;本练习无需启动第三个客户端。
向每个分区各发布一条 followup 事件,等待剩余成员追平进度:
/home/labex/project/tools/publish-shared-batch followup
/home/labex/project/tools/wait-caught-up orders-team 4 > /home/labex/project/output/team-followup-position.txt
cat /home/labex/project/output/team-followup-position.txt
cat /home/labex/project/output/reader-a.jsonl
reader-a 的文件现在应同时包含 share7 和 share8。reader-b 已停止,因此无法处理这些新记录。观察者组仍保留之前的已提交位置;恢复该组以读取这两条新事件:
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders-shared --group orders-observer --from-beginning --max-messages 2 --timeout-ms 30000 --command-property enable.auto.commit=true --command-property auto.commit.interval.ms=1000 --command-property max.poll.records=1 > /home/labex/project/output/observer-followup.jsonl
cat /home/labex/project/output/observer-followup.jsonl
/home/labex/project/tools/wait-caught-up orders-observer 4 > /home/labex/project/output/observer-followup-position.txt
cat /home/labex/project/output/observer-followup-position.txt
预期会看到 share7 和 share8,并且每个组在两个分区中的下一个 offset 均为 4。起点选项不会覆盖观察者组有效的已保存 offset 3。
如果离开的成员已经处理记录,却尚未提交相应进度,再均衡可能导致重复交付。因此观测到的事件可能重复;稳定分配仍会确保同一时刻每个分区只有一个负责成员。本实验检查真实分配和完整事件内容,不声称提供端到端恰好一次交付。
关闭剩余客户端并保留证据
这一步,你将停止唯一剩余的客户端,结束持续运行的实验。你已经观察到它的最终位置到达末尾,因此没有待处理的新任务。使用为它保存的 PID:
kill "$consumer_a_pid"
wait "$consumer_a_pid" || true
/home/labex/project/tools/wait-assignment 0 > /home/labex/project/output/no-members.txt
cat /home/labex/project/output/no-members.txt
该组现在应没有活跃成员。保存的 offset 和 Broker 事件仍然存在;客户端生命周期与保存的进度彼此独立。有数量限制的观察者客户端已经自动退出。
检查保留的接管前后成员报告:
cat /home/labex/project/output/two-members.txt /home/labex/project/output/one-member.txt
第一份报告展示了两个负责范围互不重叠的成员,第二份报告展示了一个成员负责两个分区。这些真实报告包含本次运行特有的成员标识符,并非需要记忆的固定分配。读取者文件和观察者文件保留了两个组的事件证据。你只关闭了自己的两个控制台客户端,保留了 Topic 和已提交位置供后续检查。
总结
你观察了两个消费者共享真实分区分配、独立组读取相同记录,以及新任务到来前剩余成员接管两个分区的过程。有时限的状态检查取代了对等待时间的猜测,限定目标的清理关闭了客户端,同时保留进度和事件。接下来,你将用 Python 生产者替换预置的发布工具,并能阅读和修改其中的序列化与交付处理逻辑。



