一. 简介
1. 概况
一个分布式流式处理平台,具有高吞吐,可持久化,可水平扩展,支持流数据处理等多种特性;
2. 角色
消息系统:除传统消息中间件的解耦,冗余存储,流量削峰,缓冲,异步通信,扩展性,可恢复性等功能,还提供了消息顺序性保障及回溯消费的功能;
存储系统:可以把消息持久化到硬盘,可以把Kafka作为长期的数据存储系统来使用;
流式处理平台:提供了一个完整的流式处理类库;
3. 基本概念
Broker:Kafka集群中的一台或多台服务器;
Topic:发布到Kafka的消息的类别;
Partition:物理上的Topic分区;
Producer:向Kafka发消息的客户端;
Consumer:从Kafka取消息的客户端;
Consumer Group:用来实现一个Topic消息的广播和单播的手段;
一个典型的Kafka集群中包含若干生产者,若干Broker,若干消费者组及一个Zookeeper集群;
Kafka通过Zookeeper管理集群配置,选举leader,以及Rebalance;
4. 服务端参数配置
参数配置在 $KAFKA_HOME/config/server.properties
zookeeper.connect:broker要连接的ZK集群地址,每个节点用逗号隔开;
listeners:broker监听客户端连接的地址列表,即客户端要连接broker的入口地址列表;
broker.id:指定kafka集群中broker的唯一标识,默认值为-1;
log.dir:kafka日志文件的存放目录;
message.max.bytes:broker所能接收消息的最大值,默认约976.6KB,如果超过则抛出RecordTooLargeException;
二. 简单实例
1. 消息生产者
Map<String, Object> props = new HashMap<String, Object>(); props.put("bootstrap.servers","localhost:9092"); // Kafka集群 props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer"); // 消息的序列化类型 props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer"); props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer"); // 消息的反序列化类型 props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer"); props.put("zk.connect","127.0.0.1:2181"); // 指定Kafka连接Zookeeper的URL String topic = "test-topic"; Producer<String, String> producer = new KafkaProducer<String, String>(props); producer.send(new ProducerRecord<String, String>(topic, "idea-key2", "java-message 1")); // topic, key, value producer.send(new ProducerRecord<String, String>(topic, "idea-key2", "java-message 2")); producer.send(new ProducerRecord<String, String>(topic, "idea-key2", "java-message 3")); producer.close();
其中topic和value是必填的,partition和key是可选的,如果指定了partition,那么消息会被发送至指定的partition,如果没指定partition但指定了key,那么会按照hash(key)发送至对应的partition,如果都没指定,消息按照round-robin模式发送到每一个partition;
2. 消息消费者
String topic = "test-topic"; Properties props = new Properties(); props.put("bootstrap.servers","localhost:9092"); // 表示Kafka集群 props.put("group.id","testGroup1"); // 表示消费者的分组ID props.put("enable.auto.commit","true"); // 表示Consumer的offset是否自动提交 props.put("auto.commit.interval.ms","1000"); // 用于设置自动提交offset到Zookeeper的时间间隔 props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer"); Consumer<String, String> consumer = new KafkaConsumer(props); consumer.subscribe(Arrays.asList(topic)); // 消费者订阅topic while(true) { ConsumerRecords<String, String> records = consumer.poll(100); // poll()轮训集群消息,一致等待集群中没有消息或达到超时时间(100毫秒) for (ConsumerRecords<String, String> record : records) { System.out.printf("partition = %d, offset = %d, key = %s, value = %s%n", record.partition(), record.offset(), record.key(), record.value()); } }
三. 实践建议
1. 分区
一个Topic可以分为多个Partition,每个Partition都是一个有序的队列;在存储层面可以看做一个可追加的Log文件;追加时会分配一个特定的offset,offset是消息在分区中的唯一标识,通过它来保证消息在分区内的顺序性;分区具有多副本机制,副本之间是一主多从的关系;
实际上Kafka的基本存储单元是分区Partition,一个Topic可能会有一个或多个Partition,不同的Partition可位于不同的服务器节点上,物理上一个Partition对应于一个文件夹,Partition内包含一个或多个Segment,每个Segment又包含一个数据文件和一个与之对应的索引文件;
虽然物理上最小单元是Segment,但是Kafka并不提供同一个Partition内不同Segment的并行处理能力;
对于写操作,每次只会写Partition内的一个Segment,对于读操作,也只会顺序读取同一个Partition内的不同Segment;
从逻辑上看,可以把一个Partition当作一个非常长的数组,通过索引offset访问数据;
Partition提供的并行能力:不同的Partition可位于不同的机器上,可以实现机器间的并行处理;多个Partition也可位于同一台服务器上,使不同的Partition对应不同的磁盘,实现磁盘间的并行处理;
对生产者和Broker而言,不同Partition的写操作完全是并行的,对消费者来说,并发数取决于Partition的数量;
分区数量配置在每个Broker实例的server.properties文件中:num.partitions
2. 复制
Kafka使用Zookeeper实现了去中心化的集群功能;利用ZK维护集群成员的信息,每个Broker实例都会被设置一个唯一的标识符,在启动时通过创建临时节点的方式把自己的唯一标识符注册到ZK,Kafka会监视ZK中的/brokers/ids路径;
为了让Kafka在集群中某些节点不能提供服务的情况下,集群对外整体依然可用,提供了一种集群间数据的复制机制;
Kafka通过ZK提供的leader选举方式实现数据复制方案;复制操作也是针对分区的,一个分区有多个副本,每个Broker可以保存上千个不同主题和分区的副本,副本分为leader副本和follower副本,所有生产者和消费者的请求都会经过leader,follower的职责是从leader处复制消息数据,使自己和leader状态保持一致,一旦拉取到新的消息,follower会发送ack响应给生产者,如果leader宕机,则从follower选举新的leader;
Broker实例的server.properties文件中可以设置复制因子(分区的副本数):default.replication.factor
3. 消息发送
发送方式:
立即发送:只发消息,不关心结果;send()直接发送;
同步发送:发送消息后,同步等待结果;send()发送后通过get()获取返回的Future对象来查看是否成功;
异步发送:发送消息后,异步等待结果;send()发送消息时把回调函数作为入参传入,当生产者接收到服务器的响应时会触发回调函数;
确认方式:通过设置生产者对象初始化时的acks属性来表示;
属性值0表示:消息发送出去就认为是成功的;
属性值1表示:当leader确认接收到消息就认为投递是成功的,然后再由其他副本通过异步方式拉取;
属性值all表示:由所有的leader和follower都确认接收到消息才认为是成功的;可靠性最高,性能最低;
消息重发:
重发次数由初始化生产者对象时的retries属性决定,重试时间间隔由retry.backoff.ms属性修改;
批次发送:
当有多条消息被发送到同一个分区时,生产者会把它们放到同一个批次里,来提高吞吐量,但同时也会增加延迟;
对批次的控制主要是通过构建生产者对象时的两个属性来实现:
batch.size:每个分区的缓存消息数量达到这个数值时触发一次网络请求,批次里的所有消息会被发送出去;
linger.ms:每天消息在缓存中的最长时间,超过这个时间就会忽略batch.size的限制,立即把消息发送出去;
4. 消费者组
Consumer Group是一种具有容错性的消费机制,组内的消费者共享一个唯一标识,分组ID;一个分区只能由同一个组里的一个消费者来消费;
Topic的消息会被概念上的复制到所有的消费者组,如果要实现广播,只要每个消费者都有一个独立的消费者组就可以了,如果要实现单播,只要所有的消费者都在同一个消费者组中就行;
分区所有权从一个消费者转移到另一个消费者的行为叫作再均衡;
触发再均衡的三种场景:组内成员发生变更;订阅的主题数量发生变更;订阅主题的分区数量发生变更;
再均衡操作影响的范围是整个消费者组,即消费者组中的所有消费者全部暂停消费直到再均衡完成;
5. 消费偏移量
Kafka服务端不保存消息的状态,消费者每次poll时总是返回由生产者写入Kafka中但还没有消费的消息;
生产者的消息并不需要消费者确认,而消息在分区中顺序排列,通过一个偏移量来确定每一条的位置;
Kafka中有一个叫作_consumer_offset的特殊主题用来保存消息在每个分区的偏移量,消费者每次消费时都会往这个主题中发送消息,消息包含每个分区的偏移量;
维护消息偏移量对于避免消息被重复消费和遗漏消费,确保消息的ExactlyOnce至关重要;在KafkaConsumer类中提供了很多种方式提交偏移量;
自动提交:
自动提交设置:enable.auto.commit,提交的时间间隔:auto.commit.interval.ms
自动提交的间隔期间如果发生再均衡会导致重复处理消息的问题;
手动提交:
手动提交之前需要先关闭自动提交配置,使用Consumer.commitSync()方法会提交由poll返回的最新偏移量;
异步提交:
commitSync()方法在Broker对提交请求做出回应前是阻塞的,通过异步提交来解决;而且在成功提交或碰到无法恢复的错误前会一直重试;
commitAsync()方法只管发送提交请求,不需要等待Broker的立即回应;在提交失败时不会重试;支持回调,用于记录错误信息或生成某些应用度量指标;