消费者和消费组
消费者(Consumer)负责订阅 Kafka 中的主题(Topic),并且从订阅的主题上拉取消息。与其他一些消息中间件不同的是:在 Kafka 的消费理念中还有一层消费组(Consumer Group)的概念,每个消费者都有一个对应的消费组。当消息发布到主题后,只会被投递给订阅它的每个消费组中的一个消费者。
消费者和消费组与分区的关系
如上图,某个主题中共有4个分区(Partition):P0、P1、P2、P3。有两个消费组A和B都订阅了这个主题,消费组A中有4个消费者(C0、C1、C2和C3),消费组B中有2个消费者(C4和C5)。按照 Kafka 默认的规则,最后的分配结果是消费组A中的每一个消费者分配到1个分区,消费组B中的每一个消费者分配到2个分区,两个消费组之间互不影响。每个消费者只能消费所分配到的分区中的消息。换言之,每一个分区只能被一个消费组中的一个消费者所消费。
消费者个数变化所对应的分区分配的变化
假设目前某消费组内只有一个消费者C0,订阅了一个主题,这个主题包含7个分区:P0、P1、P2、P3、P4、P5、P6。也就是说,这个消费者C0订阅了7个分区,如下。
当消费组内又加入了一个新的消费者C1,C2,按照既定的逻辑,需要将原来消费者C0的部分分区分配给消费者C1,C2消费,如下图所示。消费者C0,C1和C2各自负责消费所分配到的分区,彼此之间并无逻辑上的干扰。
再均衡
我们从上面的消费者演变图中可以知道这么一个过程:最初是一个消费者订阅一个主题并消费其全部分区的消息,后来有一个消费者加入群组,随后又有更多的消费者加入群组,而新加入的消费者实例分摊了最初消费者的部分消息,这种把分区的所有权通过一个消费者转到其他消费者的行为称为再均衡,英文名也叫做 Rebalance 。
再均衡非常重要,它为消费者群组带来了高可用性和伸缩性,我们可以放心的添加消费者或移除消费者,不过,在再均衡期间,消费者无法读取消息,造成整个消费者组在再均衡的期间都不可用。另外,当分区被重新分配给另一个消费者时,消息当前的读取状态会丢失,比如消费者消费完某个分区中的一部分消息时还没有来得及提交消费位移就发生了再均衡操作,之后这个分区又被分配给了消费组内的另一个消费者,原来被消费完的那部分消息又被重新消费一遍,也就是发生了重复消费。所以一般情况下,应尽量避免不必要的再均衡的发生。它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。
消费者通过向组织协调者(Kafka Broker)发送心跳来维护自己是消费者组的一员并确认其拥有的分区。对于不同不的消费群体来说,其组织协调者可以是不同的。只要消费者定期发送心跳,就会认为消费者是存活的并处理其分区中的消息。当消费者检索记录或者提交它所消费的记录时就会发送心跳。
如果过了一段时间 Kafka 停止发送心跳了,会话(Session)就会过期,组织协调者就会认为这个 Consumer 已经死亡,就会触发一次再均衡。如果消费者宕机并且停止发送消息,组织协调者会等待几秒钟,确认它死亡了才会触发再均衡。在这段时间里,死亡的消费者将不处理任何消息。在清理消费者时,消费者将通知协调者它要离开群组,组织协调者会触发一次再均衡,尽量降低处理停顿。
再均衡是一把双刃剑,它为消费者群组带来高可用性和伸缩性的同时,还有有一些明显的缺点(bug),而这些 bug 到现在社区还无法修改。
再均衡的过程对消费者组有极大的影响。因为每次再均衡过程中都会导致STW,也就是说,在再均衡期间,消费者组中的消费者实例都会停止消费,等待再均衡的完成。而且再均衡这个过程很慢......
消费者与消费组这种模型可以让整体的消费能力具备横向伸缩性,我们可以增加(或减少)消费者的个数来提高(或降低)整体的消费能力。对于分区数固定的情况,一味地增加消费者并不会让消费能力一直得到提升,如果消费者过多,出现了消费者的个数大于分区个数的情况,就会有消费者分配不到任何分区。以上的分配逻辑都是基于默认的分区分配策略进行分析的,可以通过消费者客户端参数 partition.assignment.strategy 来设置消费者与订阅主题之间的分区分配策略。
消息投递模式
对于消息中间件而言,一般有两种消息投递模式:点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式。
点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息。
发布订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题(Topic),主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者从主题中订阅消息。主题使得消息的订阅者和发布者互相保持独立,不需要进行接触即可保证消息的传递,发布/订阅模式在消息的一对多广播时采用。Kafka 同时支持两种消息投递模式,而这正是得益于消费者与消费组模型的契合:
- 如果所有的消费者都隶属于同一个消费组,那么所有的消息都会被均衡地投递给每一个消费者,即每条消息只会被一个消费者处理,这就相当于点对点模式的应用。
- 如果所有的消费者都隶属于不同的消费组,那么所有的消息都会被广播给所有的消费者,即每条消息会被所有的消费者处理,这就相当于发布/订阅模式的应用。
消费组是一个逻辑上的概念,它将旗下的消费者归为一类,每一个消费者只隶属于一个消费组。每一个消费组都会有一个固定的名称,消费者在进行消费前需要指定其所属消费组的名称,这个可以通过消费者客户端参数 group.id 来配置,默认值为空字符串。
消费者并非逻辑上的概念,它是实际的应用实例,它可以是一个线程,也可以是一个进程。同一个消费组内的消费者既可以部署在同一台机器上,也可以部署在不同的机器上。
消费者客户端开发
消费者的hello world
public class KafkaCosumer {
public static final String brokerList = "localhost:9092";
public static final String topic = "topic-demo";
public static final String groupId = "group.demo";
public static final AtomicBoolean isRunning = new AtomicBoolean(true);
public static Properties initConfig(){
Properties props = new Properties();
props.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
props.put("bootstrap.servers", brokerList);
props.put("group.id", groupId);
props.put("client.id", "consumer.client.id.demo");
return props;
}
public static void main(String[] args) {
Properties props = initConfig();
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList(topic));
try {
while (isRunning.get()) {
ConsumerRecords<String, String> records =
consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
System.out.println("topic = " + record.topic()
+ ", partition = "+ record.partition()
+ ", offset = " + record.offset());
System.out.println("key = " + record.key()
+ ", value = " + record.value());
//do something to process record.
}
}
} catch (Exception e) {
log.error("occur exception ", e);
} finally {
consumer.close();
}
}
必要的参数配置
在创建真正的消费者实例之前需要做相应的参数配置,在 Kafka 消费者客户端 KafkaConsumer 中也有3个参数是必填的。
- bootstrap.servers:和生产者客户端 KafkaProducer 中的相同,用来指定连接 Kafka 集群所需的 broker 地址清单,具体内容形式为 host1:port1,host2:post,可以设置一个或多个地址,中间用逗号隔开,此参数的默认值为“”。注意这里并非需要设置集群中全部的 broker 地址,消费者会从现有的配置中查找到全部的 Kafka 集群成员。这里设置两个以上的 broker 地址信息,当其中任意一个宕机时,消费者仍然可以连接到 Kafka 集群上。
- group.id:消费者隶属的消费组的名称,默认值为“”。如果设置为空,则会报出异常:Exception in thread "main" org.apache.kafka.common.errors.InvalidGroupIdException: The configured groupId is invalid。一般而言,这个参数需要设置成具有一定的业务意义的名称。
- key.deserializer 和 value.deserializer:与生产者客户端 KafkaProducer 中的 key.serializer和value.serializer 参数对应。消费者从 broker 端获取的消息格式都是字节数组(byte[])类型,所以需要执行相应的反序列化操作才能还原成原有的对象格式。这两个参数分别用来指定消息中 key 和 value 所需反序列化操作的反序列化器,这两个参数无默认值。注意这里必须填写反序列化器类的全限定名,比如示例中的 org.apache.kafka.common.serialization.StringDeserializer。
此外,同kafkaProducer一样,initConfig() 方法里还设置了一个参数 client.id,这个参数用来设定 KafkaConsumer 对应的客户端id,默认值也为“”。如果客户端不设置,则 KafkaConsumer 会自动生成一个非空字符串,内容形式如“consumer-1”、“consumer-2”,即字符串“consumer-”与数字的拼接。还可以根据业务应用的实际需求来修改这些参数的默认值,以达到灵活调配的目的。我们可以直接使用客户端中的 org.apache.kafka.clients.consumer.ConsumerConfig 类来做一定程度上的预防,每个参数在 ConsumerConfig 类中都有对应的名称:
public class ConsumerConfig extends AbstractConfig {
private static final ConfigDef CONFIG;
public static final String GROUP_ID_CONFIG = "group.id";
private static final String GROUP_ID_DOC = "A unique string that identifies the consumer group this consumer belongs to. This property is required if the consumer uses either the group management functionality by using <code>subscribe(topic)</code> or the Kafka-based offset management strategy.";
public static final String MAX_POLL_RECORDS_CONFIG = "max.poll.records";
private static final String MAX_POLL_RECORDS_DOC = "The maximum number of records returned in a single call to poll().";
public static final String MAX_POLL_INTERVAL_MS_CONFIG = "max.poll.interval.ms";
private static final String MAX_POLL_INTERVAL_MS_DOC = "The maximum delay between invocations of poll() when using consumer group management. This places an upper bound on the amount of time that the consumer can be idle before fetching more records. If poll() is not called before expiration of this timeout, then the consumer is considered failed and the group will rebalance in order to reassign the partitions to another member. ";
public static final String SESSION_TIMEOUT_MS_CONFIG = "session.timeout.ms";
private static final String SESSION_TIMEOUT_MS_DOC = "The timeout used to detect consumer failures when using Kafka's group management facility. The consumer sends periodic heartbeats to indicate its liveness to the broker. If no heartbeats are received by the broker before the expiration of this session timeout, then the broker will remove this consumer from the group and initiate a rebalance. Note that the value must be in the allowable range as configured in the broker configuration by <code>group.min.session.timeout.ms</code> and <code>group.max.session.timeout.ms</code>.";
public static final String HEARTBEAT_INTERVAL_MS_CONFIG = "heartbeat.interval.ms";
private static final String HEARTBEAT_INTERVAL_MS_DOC = "The expected time between heartbeats to the consumer coordinator when using Kafka's group management facilities. Heartbeats are used to ensure that the consumer's session stays active and to facilitate rebalancing when new consumers join or leave the group. The value must be set lower than <code>session.timeout.ms</code>, but typically should be set no higher than 1/3 of that value. It can be adjusted even lower to control the expected time for normal rebalances.";
public static final String BOOTSTRAP_SERVERS_CONFIG = "bootstrap.servers";
public static final String ENABLE_AUTO_COMMIT_CONFIG = "enable.auto.commit";
private static final String ENABLE_AUTO_COMMIT_DOC = "If true the consumer's offset will be periodically committed in the background.";
public static final String AUTO_COMMIT_INTERVAL_MS_CONFIG = "auto.commit.interval.ms";
private static final String AUTO_COMMIT_INTERVAL_MS_DOC = "The frequency in milliseconds that the consumer offsets are auto-committed to Kafka if <code>enable.auto.commit</code> is set to <code>true</code>.";
public static final String PARTITION_ASSIGNMENT_STRATEGY_CONFIG = "partition.assignment.strategy";
private static final String PARTITION_ASSIGNMENT_STRATEGY_DOC = "The class name of the partition assignment strategy that the client will use to distribute partition ownership amongst consumer instances when group management is used";
public static final String AUTO_OFFSET_RESET_CONFIG = "auto.offset.reset";
public static final String AUTO_OFFSET_RESET_DOC = "What to do when there is no initial offset in Kafka or if the current offset does not exist any more on the server (e.g. because that data has been deleted): <ul><li>earliest: automatically reset the offset to the earliest offset<li>latest: automatically reset the offset to the latest offset</li><li>none: throw exception to the consumer if no previous offset is found for the consumer's group</li><li>anything else: throw exception to the consumer.</li></ul>";
public static final String FETCH_MIN_BYTES_CONFIG = "fetch.min.bytes";
private static final String FETCH_MIN_BYTES_DOC = "The minimum amount of data the server should return for a fetch request. If insufficient data is available the request will wait for that much data to accumulate before answering the request. The default setting of 1 byte means that fetch requests are answered as soon as a single byte of data is available or the fetch request times out waiting for data to arrive. Setting this to something greater than 1 will cause the server to wait for larger amounts of data to accumulate which can improve server throughput a bit at the cost of some additional latency.";
public static final String FETCH_MAX_BYTES_CONFIG = "fetch.max.bytes";
private static final String FETCH_MAX_BYTES_DOC = "The maximum amount of data the server should return for a fetch request. Records are fetched in batches by the consumer, and if the first record batch in the first non-empty partition of the fetch is larger than this value, the record batch will still be returned to ensure that the consumer can make progress. As such, this is not a absolute maximum. The maximum record batch size accepted by the broker is defined via <code>message.max.bytes</code> (broker config) or <code>max.message.bytes</code> (topic config). Note that the consumer performs multiple fetches in parallel.";
public static final int DEFAULT_FETCH_MAX_BYTES = 52428800;
public static final String FETCH_MAX_WAIT_MS_CONFIG = "fetch.max.wait.ms";
private static final String FETCH_MAX_WAIT_MS_DOC = "The maximum amount of time the server will block before answering the fetch request if there isn't sufficient data to immediately satisfy the requirement given by fetch.min.bytes.";
public static final String METADATA_MAX_AGE_CONFIG = "metadata.max.age.ms";
public static final String MAX_PARTITION_FETCH_BYTES_CONFIG = "max.partition.fetch.bytes";
private static final String MAX_PARTITION_FETCH_BYTES_DOC = "The maximum amount of data per-partition the server will return. Records are fetched in batches by the consumer. If the first record batch in the first non-empty partition of the fetch is larger than this limit, the batch will still be returned to ensure that the consumer can make progress. The maximum record batch size accepted by the broker is defined via <code>message.max.bytes</code> (broker config) or <code>max.message.bytes</code> (topic config). See fetch.max.bytes for limiting the consumer request size.";
public static final int DEFAULT_MAX_PARTITION_FETCH_BYTES = 1048576;
public static final String SEND_BUFFER_CONFIG = "send.buffer.bytes";
public static final String RECEIVE_BUFFER_CONFIG = "receive.buffer.bytes";
public static final String CLIENT_ID_CONFIG = "client.id";
public static final String RECONNECT_BACKOFF_MS_CONFIG = "reconnect.backoff.ms";
public static final String RECONNECT_BACKOFF_MAX_MS_CONFIG = "reconnect.backoff.max.ms";
public static final String RETRY_BACKOFF_MS_CONFIG = "retry.backoff.ms";
public static final String METRICS_SAMPLE_WINDOW_MS_CONFIG = "metrics.sample.window.ms";
public static final String METRICS_NUM_SAMPLES_CONFIG = "metrics.num.samples";
public static final String METRICS_RECORDING_LEVEL_CONFIG = "metrics.recording.level";
public static final String METRIC_REPORTER_CLASSES_CONFIG = "metric.reporters";
public static final String CHECK_CRCS_CONFIG = "check.crcs";
private static final String CHECK_CRCS_DOC = "Automatically check the CRC32 of the records consumed. This ensures no on-the-wire or on-disk corruption to the messages occurred. This check adds some overhead, so it may be disabled in cases seeking extreme performance.";
public static final String KEY_DESERIALIZER_CLASS_CONFIG = "key.deserializer";
public static final String KEY_DESERIALIZER_CLASS_DOC = "Deserializer class for key that implements the <code>org.apache.kafka.common.serialization.Deserializer</code> interface.";
public static final String VALUE_DESERIALIZER_CLASS_CONFIG = "value.deserializer";
public static final String VALUE_DESERIALIZER_CLASS_DOC = "Deserializer class for value that implements the <code>org.apache.kafka.common.serialization.Deserializer</code> interface.";
public static final String CONNECTIONS_MAX_IDLE_MS_CONFIG = "connections.max.idle.ms";
public static final String REQUEST_TIMEOUT_MS_CONFIG = "request.timeout.ms";
private static final String REQUEST_TIMEOUT_MS_DOC = "The configuration controls the maximum amount of time the client will wait for the response of a request. If the response is not received before the timeout elapses the client will resend the request if necessary or fail the request if retries are exhausted.";
public static final String DEFAULT_API_TIMEOUT_MS_CONFIG = "default.api.timeout.ms";
public static final String DEFAULT_API_TIMEOUT_MS_DOC = "Specifies the timeout (in milliseconds) for consumer APIs that could block. This configuration is used as the default timeout for all consumer operations that do not explicitly accept a <code>timeout</code> parameter.";
public static final String INTERCEPTOR_CLASSES_CONFIG = "interceptor.classes";
public static final String INTERCEPTOR_CLASSES_DOC = "A list of classes to use as interceptors. Implementing the <code>org.apache.kafka.clients.consumer.ConsumerInterceptor</code> interface allows you to intercept (and possibly mutate) records received by the consumer. By default, there are no interceptors.";
public static final String EXCLUDE_INTERNAL_TOPICS_CONFIG = "exclude.internal.topics";
private static final String EXCLUDE_INTERNAL_TOPICS_DOC = "Whether records from internal topics (such as offsets) should be exposed to the consumer. If set to <code>true</code> the only way to receive records from an internal topic is subscribing to it.";
public static final boolean DEFAULT_EXCLUDE_INTERNAL_TOPICS = true;
static final String LEAVE_GROUP_ON_CLOSE_CONFIG = "internal.leave.group.on.close";
public static final String ISOLATION_LEVEL_CONFIG = "isolation.level";
public static final String ISOLATION_LEVEL_DOC = "<p>Controls how to read messages written transactionally. If set to <code>read_committed</code>, consumer.poll() will only return transactional messages which have been committed. If set to <code>read_uncommitted</code>' (the default), consumer.poll() will return all messages, even transactional messages which have been aborted. Non-transactional messages will be returned unconditionally in either mode.</p> <p>Messages will always be returned in offset order. Hence, in <code>read_committed</code> mode, consumer.poll() will only return messages up to the last stable offset (LSO), which is the one less than the offset of the first open transaction. In particular any messages appearing after messages belonging to ongoing transactions will be withheld until the relevant transaction has been completed. As a result, <code>read_committed</code> consumers will not be able to read up to the high watermark when there are in flight transactions.</p><p> Further, when in <code>read_committed</mode> the seekToEnd method will return the LSO";
public static final String DEFAULT_ISOLATION_LEVEL;
同样,key.deserializer 和 value.deserializer 参数对应类的全限定名比较长,也比较容易写错,这里通过 Java 中的技巧来做进一步的改进,如下:
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
在KafkaConsumer 配置相关的内容基本上和介绍 KafkaProducer 配置时的一样,除了配置对应的反序列化器,只多了一个必要的 group.id 参数。
订阅主题和分区
在创建好消费者之后,我们就需要为该消费者订阅相关的主题了。一个消费者可以订阅一个或多个主题,hello world代码中我们使用 subscribe() 方法订阅了一个主题,对于这个方法而言,既可以以集合的形式订阅多个主题,也可以以正则表达式的形式订阅特定模式的主题。subscribe 的几个重载方法如下:
public void subscribe(Collection<String> topics,ConsumerRebalanceListener listener)
public void subscribe(Collection<String> topics)
public void subscribe(Pattern pattern, ConsumerRebalanceListener listener)
public void subscribe(Pattern pattern)
对于消费者使用集合的方式(subscribe(Collection))来订阅主题而言。如果前后两次订阅了不同的主题,那么消费者以最后一次的为准。
如果消费者采用的是正则表达式的方式(subscribe(Pattern))订阅,在之后的过程中,如果有人又创建了新的主题,并且主题的名字与正则表达式相匹配,那么这个消费者就可以消费到新添加的主题中的消息。如果应用程序需要消费多个主题,并且可以处理不同的类型,那么这种订阅方式就很有效。在 Kafka 和其他系统之间进行数据复制时,这种正则表达式的方式就显得很常见。正则表达式的方式订阅的示例如下:
consumer.subscribe(Pattern.compile("topic-.*"));
消费者不仅可以通过 KafkaConsumer.subscribe() 方法订阅主题,还可以直接订阅某些主题的特定分区,在 KafkaConsumer 中还提供了一个 assign() 方法来实现这些功能,此方法的具体定义如下:
public void assign(Collection<TopicPartition> partitions)
这个方法只接受一个参数 partitions,用来指定需要订阅的分区集合。其中TopicPartition 类,在 Kafka 的客户端中,它用来表示分区,这个类的部分内容如下所示。
public final class TopicPartition implements Serializable {
private final int partition;
private final String topic;
public TopicPartition(String topic, int partition) {
this.partition = partition;
this.topic = topic;
}
public int partition() {
return partition;
}
public String topic() {
return topic;
}
//省略hashCode()、equals()和toString()方法
}
TopicPartition 类只有2个属性:topic 和 partition,分别代表分区所属的主题和自身的分区编号,这个类可以和我们通常所说的主题—分区的概念映射起来。如下订阅 topic-demo 主题中分区编号为0的分区:
consumer.assign(Arrays.asList(new TopicPartition("topic-demo", 0)));
获取主题中的所有分区的信息
由于我们事先并不知道主题中有多少个分区,在KafkaConsumer 中的 partitionsFor() 方法可以用来查询指定主题的元
数据信息,由此得到分区编号的信息,partitionsFor() 方法的具体定义如下:
public List<PartitionInfo> partitionsFor(String topic)
其中 PartitionInfo 类型即为主题的分区元数据信息,此类的主要结构如下:
public class PartitionInfo {
private final String topic;
private final int partition;
private final Node leader;
private final Node[] replicas;
private final Node[] inSyncReplicas;
private final Node[] offlineReplicas;
//这里省略了构造函数、属性提取、toString等方法
}
PartitionInfo 类中的属性 topic 表示主题名称,partition 代表分区编号,leader 代表分区的 leader 副本所在的位置,replicas 代表分区的 AR 集合,inSyncReplicas 代表分区的 ISR 集合,offlineReplicas 代表分区的 OSR 集合。 通过 partitionFor() 方法的协助,我们可以通过 assign() 方法来实现订阅主题(全部分区)的功能,如下:
List<TopicPartition> partitions = new ArrayList<>();
List<PartitionInfo> partitionInfos = consumer.partitionsFor(topic);
if (partitionInfos != null) {
for (PartitionInfo tpInfo : partitionInfos) {
partitions.add(new TopicPartition(tpInfo.topic(), tpInfo.partition()));
}
}
consumer.assign(partitions);
取消订阅
方式1:可以使用 KafkaConsumer 中的 unsubscribe() 方法来取消主题的订阅。这个方法既可以取消通过 subscribe(Collection) 方式和 subscribe(Pattern) 方式实现的主题订阅,也可以取消通过 assign(Collection) 方式实现的分区订阅。如下:
consumer.unsubscribe();
方式2:将 subscribe(Collection) 或 assign(Collection) 中的集合参数设置为空集合,那么作用等同于 unsubscribe() 方法,下面示例中的三行代码的效果相同:
consumer.unsubscribe();
consumer.subscribe(new ArrayList<String>());
consumer.assign(new ArrayList<TopicPartition>());
如果没有订阅任何主题或分区,那么再继续执行消费程序的时候会报出 IllegalStateException 异常:
java.lang.IllegalStateException: Consumer is not subscribed to any topics or assigned any partitions
订阅的状态
集合订阅的方式 subscribe(Collection)、正则表达式订阅的方式 subscribe(Pattern) 和指定分区的订阅方式 assign(Collection) 分表代表了三种不同的订阅状态:AUTO_TOPICS、AUTO_PATTERN 和 USER_ASSIGNED(如果没有订阅,那么订阅状态为 NONE)。然而这三种状态是互斥的,在一个消费者中只能使用其中的一种,否则会报出 IllegalStateException 异常:
java.lang.IllegalStateException: Subscription to topics, partitions and pattern are mutually exclusive.
通过 subscribe() 方法订阅主题具有消费者自动再均衡的功能,在多个消费者的情况下可以根据分区分配策略来自动分配各个消费者与分区的关系。当消费组内的消费者增加或减少时,分区分配关系会自动调整,以实现消费负载均衡及故障自动转移。而通过 assign() 方法订阅分区时,是不具备消费者自动均衡的功能的,其实这一点从 assign() 方法的参数中就可以看出端倪,两种类型的 subscribe() 都有 ConsumerRebalanceListener 类型参数的方法,而 assign() 方法却没有。ConsumerRebalanceListener这个是用来设置相应的再均衡监听器的
消息的反序列化
与 KafkaProducer 对应的序列化器相对应的就是KafkaConsumer 的反序列化器。Kafka 所提供的反序列化器有: ByteBufferDeserializer、ByteArrayDeserializer、BytesDeserializer、DoubleDeserializer、FloatDeserializer、IntegerDeserializer、LongDeserializer、ShortDeserializer、StringDeserializer,它们分别用于 ByteBuffer、ByteArray、Bytes、Double、Float、Integer、Long、Short 及 String 类型的反序列化,这些序列化器也都实现了 Deserializer 接口,与 KafkaProducer 中提及的 Serializer 接口一样,Deserializer 接口也有三个方法。
public void configure(Map<String, ?> configs, boolean isKey)//用来配置当前类。
public byte[] deserialize(String topic, byte[] data)//用来执行反序列化。如果 data 为 null,那么处理的时候直接返回 null 而不是抛出一个异常。
public void close()//用来关闭当前序列化器。
在自定义的序列化器来序列化自定义的 Company 类,下面是Company对应的反序列化器 CompanyDeserializer 的具体实现
public class CompanyDeserializer implements Deserializer<Company> {
public void configure(Map<String, ?> configs, boolean isKey) {}
public Company deserialize(String topic, byte[] data) {
if (data == null) {
return null;
}
if (data.length < 8) {
throw new SerializationException("Size of data received " +
"by DemoDeserializer is shorter than expected!");
}
ByteBuffer buffer = ByteBuffer.wrap(data);
int nameLen, addressLen;
String name, address;
nameLen = buffer.getInt();
byte[] nameBytes = new byte[nameLen];
buffer.get(nameBytes);
addressLen = buffer.getInt();
byte[] addressBytes = new byte[addressLen];
buffer.get(addressBytes);
try {
name = new String(nameBytes, "UTF-8");
address = new String(addressBytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new SerializationException("Error occur when deserializing!");
}
return new Company(name,address);
}
public void close() {}
}
configure() 方法和 close() 方法都是空实现,而 deserializer() 方法就是将字节数组转换成对应 Company 对象。同样地,在使用自定义的反序列化器的时候只需要将相应的 value.deserializer 参数配置为 CompanyDeserializer 即可,示例如下:
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
CompanyDeserializer.class.getName());
注意如无特殊需要,不建议使用自定义的序列化器或反序列化器,因为这样会增加生产者与消费者之间的耦合度
消费消息
Kafka 中的消费是基于拉模式的。消息的消费一般有两种模式:推模式和拉模式。推模式是服务端主动将消息推送给消费者,而拉模式是消费者主动向服务端发起请求来拉取消息。
在hello world代码中,Kafka 中的消息消费是一个不断轮询的过程,消费者所要做的就是重复地调用 poll() 方法,而 poll() 方法返回的是所订阅的主题(分区)上的一组消息,对于 poll() 方法而言,如果订阅的某些分区中没有可供消费的消息,那么此分区对应的消息拉取的结果就为空;如果订阅的所有分区中都没有可供消费的消息,那么 poll() 方法返回为空的消息集合。
poll() 方法的具体定义如下:
public ConsumerRecords<K, V> poll(final Duration timeout)
注意到 poll() 方法里还有一个超时时间参数 timeout,用来控制 poll() 方法的阻塞时间,在消费者的缓冲区里没有可用数据时会发生阻塞。(这里 timeout 的类型是 Duration,它是 JDK8 中新增的一个与时间有关的类型。在 Kafka 2.0.0 之前的版本中,timeout 参数的类型为 long),timeout 的设置取决于应用程序对响应速度的要求,比如需要在多长时间内将控制权移交给执行轮询的应用线程。可以直接将 timeout 设置为0,这样 poll() 方法会立刻返回,而不管是否已经拉取到了消息。如果应用线程唯一的工作就是从 Kafka 中拉取并消费消息,则可以将这个参数设置为最大值 Long.MAX_VALUE。
poll() 方法的返回值类型是 ConsumerRecords,通过遍历消费者可以获取到的每条消息的类型为 ConsumerRecord(注意与 ConsumerRecords 的区别),这个和生产者发送的消息类型 ProducerRecord 相对应,不过 ConsumerRecord 中的内容更加丰富,结构参考如下:
public class ConsumerRecord<K, V> {
private final String topic;//消息所属主题
private final int partition;//消息所在分区
private final long offset;//所在分区的offset
private final long timestamp;//时间戳
private final TimestampType timestampType;//对应的 timestampType 表示时间戳的类型
//有两种类型:CreateTime和LogAppendTime,分别代表消息创建的时间戳和消息追加到日志的时间戳
private final int serializedKeySize;//key 经过序列化之后的大小,如果key为空,则这个值为-1
private final int serializedValueSize;//value经过序列化之后的大小,value为空,则这个值为-1
private final Headers headers;//消息的头部内容
private final K key;//消息的key
private final V value;//消息的value
private volatile Long checksum;//CRC32 的校验值
//省略若干方法
}
返回值ConsumerRecords 用来表示一次拉取操作所获得的消息集,内部包含了若干 ConsumerRecord,常用的方法如下:
//获取消息集中指定分区的消息
public List<ConsumerRecord<K, V>> records(TopicPartition partition)
//获取指定主题的消息
public Iterable<ConsumerRecord<K, V>> records(String topic)
//获取消息集中所有分区的消息
public Set<TopicPartition> partitions()
//迭代的方法,用来循环遍历消息集内部的消息
public Iterable<ConsumerRecord<K, V>> records(String topic)
//计算出消息集中的消息个数
public int count()
//用来判断消息集是否为空
public boolean isEmpty()
//用来获取一个空的消息集,返回类型是 ConsumerRecords<K,V>。
public static <K, V> ConsumerRecords<K, V> empty()