You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

76 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 15 | 消费者组到底是什么?
你好我是胡夕。今天我要和你分享的主题是Kafka的消费者组。
消费者组即Consumer Group应该算是Kafka比较有亮点的设计了。那么何谓Consumer Group呢用一句话概括就是**Consumer Group是Kafka提供的可扩展且具有容错性的消费者机制**。既然是一个组那么组内必然可以有多个消费者或消费者实例Consumer Instance它们共享一个公共的ID这个ID被称为Group ID。组内的所有消费者协调在一起来消费订阅主题Subscribed Topics的所有分区Partition。当然每个分区只能由同一个消费者组内的一个Consumer实例来消费。个人认为理解Consumer Group记住下面这三个特性就好了。
1. Consumer Group下可以有一个或多个Consumer实例。这里的实例可以是一个单独的进程也可以是同一进程下的线程。在实际场景中使用进程更为常见一些。
2. Group ID是一个字符串在一个Kafka集群中它标识唯一的一个Consumer Group。
3. Consumer Group下所有实例订阅的主题的单个分区只能分配给组内的某个Consumer实例消费。这个分区当然也可以被其他的Group消费。
你应该还记得我在专栏[第1期](https://time.geekbang.org/column/article/98948)中提到的两种消息引擎模型吧?它们分别是**点对点模型和发布/订阅模型**,前者也称为消费队列。当然,你要注意区分很多架构文章中涉及的消息队列与这里的消息队列。国内很多文章都习惯把消息中间件这类框架统称为消息队列,我在这里不评价这种提法是否准确,只是想提醒你注意这里所说的消息队列,特指经典的消息引擎模型。
好了传统的消息引擎模型就是这两大类它们各有优劣。我们来简单回顾一下。传统的消息队列模型的缺陷在于消息一旦被消费就会从队列中被删除而且只能被下游的一个Consumer消费。严格来说这一点不算是缺陷只能算是它的一个特性。但很显然这种模型的伸缩性scalability很差因为下游的多个Consumer都要“抢”这个共享消息队列的消息。发布/订阅模型倒是允许消息被多个Consumer消费但它的问题也是伸缩性不高因为每个订阅者都必须要订阅主题的所有分区。这种全量订阅的方式既不灵活也会影响消息的真实投递效果。
如果有这么一种机制既可以避开这两种模型的缺陷又兼具它们的优点那就太好了。幸运的是Kafka的Consumer Group就是这样的机制。当Consumer Group订阅了多个主题后组内的每个实例不要求一定要订阅主题的所有分区它只会消费部分分区中的消息。
Consumer Group之间彼此独立互不影响它们能够订阅相同的一组主题而互不干涉。再加上Broker端的消息留存机制Kafka的Consumer Group完美地规避了上面提到的伸缩性差的问题。可以这么说**Kafka仅仅使用Consumer Group这一种机制却同时实现了传统消息引擎系统的两大模型**如果所有实例都属于同一个Group那么它实现的就是消息队列模型如果所有实例分别属于不同的Group那么它实现的就是发布/订阅模型。
在了解了Consumer Group以及它的设计亮点之后你可能会有这样的疑问在实际使用场景中我怎么知道一个Group下该有多少个Consumer实例呢**理想情况下Consumer实例的数量应该等于该Group订阅主题的分区总数。**
举个简单的例子假设一个Consumer Group订阅了3个主题分别是A、B、C它们的分区数依次是1、2、3总共是6个分区那么通常情况下为该Group设置6个Consumer实例是比较理想的情形因为它能最大限度地实现高伸缩性。
你可能会问我能设置小于或大于6的实例吗当然可以如果你有3个实例那么平均下来每个实例大约消费2个分区6 / 3 = 2如果你设置了8个实例那么很遗憾有2个实例8 6 = 2将不会被分配任何分区它们永远处于空闲状态。因此在实际使用过程中一般不推荐设置大于总分区数的Consumer实例。设置多余的实例只会浪费资源而没有任何好处。
好了说完了Consumer Group的设计特性我们来讨论一个问题针对Consumer GroupKafka是怎么管理位移的呢你还记得吧消费者在消费的过程中需要记录自己消费了多少数据即消费位置信息。在Kafka中这个位置信息有个专门的术语位移Offset
看上去该Offset就是一个数值而已其实对于Consumer Group而言它是一组KV对Key是分区V对应Consumer消费该分区的最新位移。如果用Java来表示的话你大致可以认为是这样的数据结构即Map<TopicPartition, Long>其中TopicPartition表示一个分区而Long表示位移的类型。当然我必须承认Kafka源码中并不是这样简单的数据结构而是要比这个复杂得多不过这并不会妨碍我们对Group位移的理解。
我在专栏[第4期](https://time.geekbang.org/column/article/100285)中提到过Kafka有新旧客户端API之分那自然也就有新旧Consumer之分。老版本的Consumer也有消费者组的概念它和我们目前讨论的Consumer Group在使用感上并没有太多的不同只是它管理位移的方式和新版本是不一样的。
老版本的Consumer Group把位移保存在ZooKeeper中。Apache ZooKeeper是一个分布式的协调服务框架Kafka重度依赖它实现各种各样的协调管理。将位移保存在ZooKeeper外部系统的做法最显而易见的好处就是减少了Kafka Broker端的状态保存开销。现在比较流行的提法是将服务器节点做成无状态的这样可以自由地扩缩容实现超强的伸缩性。Kafka最开始也是基于这样的考虑才将Consumer Group位移保存在独立于Kafka集群之外的框架中。
不过慢慢地人们发现了一个问题即ZooKeeper这类元框架其实并不适合进行频繁的写更新而Consumer Group的位移更新却是一个非常频繁的操作。这种大吞吐量的写操作会极大地拖慢ZooKeeper集群的性能因此Kafka社区渐渐有了这样的共识将Consumer位移保存在ZooKeeper中是不合适的做法。
于是在新版本的Consumer Group中Kafka社区重新设计了Consumer Group的位移管理方式采用了将位移保存在Kafka内部主题的方法。这个内部主题就是让人既爱又恨的\_\_consumer\_offsets。我会在专栏后面的内容中专门介绍这个神秘的主题。不过现在你需要记住新版本的Consumer Group将位移保存在Broker端的内部主题中。
最后我们来说说Consumer Group端大名鼎鼎的重平衡也就是所谓的Rebalance过程。我形容其为“大名鼎鼎”从某种程度上来说其实也是“臭名昭著”因为有关它的bug真可谓是此起彼伏从未间断。这里我先卖个关子后面我会解释它“遭人恨”的地方。我们先来了解一下什么是Rebalance。
**Rebalance本质上是一种协议规定了一个Consumer Group下的所有Consumer如何达成一致来分配订阅Topic的每个分区**。比如某个Group下有20个Consumer实例它订阅了一个具有100个分区的Topic。正常情况下Kafka平均会为每个Consumer分配5个分区。这个分配的过程就叫Rebalance。
那么Consumer Group何时进行Rebalance呢Rebalance的触发条件有3个。
1. 组成员数发生变更。比如有新的Consumer实例加入组或者离开组抑或是有Consumer实例崩溃被“踢出”组。
2. 订阅主题数发生变更。Consumer Group可以使用正则表达式的方式订阅主题比如consumer.subscribe(Pattern.compile("t.\*c"))就表明该Group订阅所有以字母t开头、字母c结尾的主题。在Consumer Group的运行过程中你新创建了一个满足这样条件的主题那么该Group就会发生Rebalance。
3. 订阅主题的分区数发生变更。Kafka当前只能允许增加一个主题的分区数。当分区数增加时就会触发订阅该主题的所有Group开启Rebalance。
Rebalance发生时Group下所有的Consumer实例都会协调在一起共同参与。你可能会问每个Consumer实例怎么知道应该消费订阅主题的哪些分区呢这就需要分配策略的协助了。
当前Kafka默认提供了3种分配策略每种策略都有一定的优势和劣势我们今天就不展开讨论了你只需要记住社区会不断地完善这些策略保证提供最公平的分配策略即每个Consumer实例都能够得到较为平均的分区数。比如一个Group内有10个Consumer实例要消费100个分区理想的分配策略自然是每个实例平均得到10个分区。这就叫公平的分配策略。如果出现了严重的分配倾斜势必会出现这种情况有的实例会“闲死”而有的实例则会“忙死”。
我们举个简单的例子来说明一下Consumer Group发生Rebalance的过程。假设目前某个Consumer Group下有两个Consumer比如A和B当第三个成员C加入时Kafka会触发Rebalance并根据默认的分配策略重新为A、B和C分配分区如下图所示
![](https://static001.geekbang.org/resource/image/b3/c0/b3b0a5917b03886d31db027b466200c0.jpg)
显然Rebalance之后的分配依然是公平的即每个Consumer实例都获得了2个分区的消费权。这是我们希望出现的情形。
讲完了Rebalance现在我来说说它“遭人恨”的地方。
首先Rebalance过程对Consumer Group消费过程有极大的影响。如果你了解JVM的垃圾回收机制你一定听过万物静止的收集方式即著名的stop the world简称STW。在STW期间所有应用线程都会停止工作表现为整个应用程序僵在那边一动不动。Rebalance过程也和这个类似在Rebalance过程中所有Consumer实例都会停止消费等待Rebalance完成。这是Rebalance为人诟病的一个方面。
其次目前Rebalance的设计是所有Consumer实例共同参与全部重新分配所有分区。其实更高效的做法是尽量减少分配方案的变动。例如实例A之前负责消费分区1、2、3那么Rebalance之后如果可能的话最好还是让实例A继续消费分区1、2、3而不是被重新分配其他的分区。这样的话实例A连接这些分区所在Broker的TCP连接就可以继续用不用重新创建连接其他Broker的Socket资源。
最后Rebalance实在是太慢了。曾经有个国外用户的Group内有几百个Consumer实例成功Rebalance一次要几个小时这完全是不能忍受的。最悲剧的是目前社区对此无能为力至少现在还没有特别好的解决方案。所谓“本事大不如不摊上”也许最好的解决方案就是避免Rebalance的发生吧。
## 小结
总结一下今天我跟你分享了Kafka Consumer Group的方方面面包括它是怎么定义的它解决了哪些问题有哪些特性。同时我们也聊到了Consumer Group的位移管理以及著名的Rebalance过程。希望在你开发Consumer应用时它们能够助你一臂之力。
![](https://static001.geekbang.org/resource/image/60/f5/60478ddbf101b19a747d8110ae019ef5.jpg)
## 开放讨论
今天我貌似说了很多Consumer Group的好话除了Rebalance你觉得这种消费者组设计的弊端有哪些呢
欢迎写下你的思考和答案,我们一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。