一、简介
kafka是分布式消息发布和订阅系统,具有高性能高吞吐量的特点被广泛应用于大数据场景传输。kafka提供了类似JMS的特性但是在设计和实现上是完全不同的,而且他不是JMS规范实现的。
二、应用场景
由于kafka具有很好的吞吐量、内置分区、冗余及容错的有点,让kafka成为了一个很好的大规模消息处理应用的解决方案。通常会应用于以下几个方面
- 行为跟踪:kafka可以用于跟踪用户浏览页面、搜索及其他行为。通过发布订阅模式记录到对应的topic中,通过后端大数据平台接入处理分析。
- 日志收集:
三、架构
典型的kafka集群包含若干Producer,若干个Broker、若干个Consumer Group,以及一个zookeeper集群。kafka通过zookeeper管理集群配置及服务协同。Producer使用push模式将消息发布到broker,consumer通过监听使用pull模式从broker订阅并消费消息。
多个broker协同工作,producer和consumer部署在各个业务逻辑中。三者通过zookeeper管理协调请求和转发。
与activemq不同,kafka的消费端是主动从broker拉取消息
四、常用命令
//kafka内置有zookeeper,如果没有独立部署zk,可以使用默认zk
> bin/zookeeper-server-start.sh config/zookeeper.properties
//启动命令 -daemon 后台启动
> bin/kafka-server-start.sh -daemon ../config/server.properties
//创建topic --replication-factor标识该topic需要再不同的broker中保存积分 --partitions 分区数
> bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test
//查看topic
> bin/kafka-topics.sh --list --zookeeper localhost:2181 test
//发送命令
> bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
//消费者连接
> bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
五、集群搭建
master节点选举是根据启动时间确定
更新配置文件
- 修改server.properties.broker.id=0/1/2…
- server.properties中修改监听地址为本机地址
advertised.listeners=PLAINTEXT://192.168.1.11:9092
当kafka broker启动时会在zk上注册自己的ip和端口号,客户端通过ip和端口号连接
六、重点配置
6.1 生产者
acks
producer发送消息到broker上以后的确认值
0 消息发送给broker后不需要确认(性能较高,会出现数据丢失)
1 只需要获得kafka集群总leader节点确认即可返回
all(-1) 需要ISR中所有的Replica确认,最安全性能最低,但是由于ISR可能会只有一个Replica,此时如果leader和follower节点相继宕机可能数据丢失
batch.size
发送多个消息到同一个broker上同一个分区时是通过批量的方式提交的。这个参数控制批量提交的字节数大小,默认值是16kb
linger.ms
producer会把两次发送时间间隔内收集到的所有Requests进行一次聚合再发送,该参数就是每次发送到broker上的请求增加一些delay,用来聚合Message Request。类似TCP的Nagle算法,为了避免大量小数据包的发送采用了基于小包的等-停协议
max.request.size
请求数据的最大字节数,默认值是1MB
6.2 消费者
group.id
consumer group是kafka提供的可扩展且具有容错性的消费机制。组内所有消费者一起消费订阅的主体的所有分区(partition)。每个分区只能由同一个消费组内的一个consumer来消费。一个消息每个订阅了的组都可以消费。组间共享,组内竞争
auto.offset.reset
对于新的group中的消费者,当有新group的消费者来消费指定topic时
earliest:对于新的groupId重置offset
lastest:对于新的groupId,直接取已经消费并且提交的最大的offset
none:没有offset则报错
enable.auto.commit
消费者消费消息后自动提交,只有当消息提交以后该消息才不会被再次接受到。可以配置auto.commit.interval.ms控制自动提交频率,也可以通过consumer.commitSync()方式手动提交
七、Topic&Partition
topic是存储消息的逻辑概念。是一个消息的集合。不同topic的消息是分开存储的。每个topic可以有多个生产者发送消息也可以有多个消费者消费消息。
每个topic可以划分为多个分区,同一个topic下的不同分区包含的消息是不同的,类似数据库的分表。每个消息被添加到分区时会被分配一个offset,它是消息本分区的唯一标识。kafka通过offset保证消息在分区内的顺序,不同分区之间消息不能保证顺序。
如图,一个topic分了三个partition,如果partition规则设置的合理,那么消息就会均匀分布在三个分区中,起到了数据库分表的作用。
八、Topic&Partition的存储
partition是以文件的形式存储在文件中,例如一个topic myTopic创建了三个partition,那么在kafka的数据目录中就有三个文件夹,myTopic-0 myTopic-1 myTopic2
可以通过命令创建分区
./kafka-topics.sh --create --zookeeper 127.0.0.1:2181 --replication–factor 1 --partitions 3 --topic firstTopic
九、消息分发
9.1 生产者发送消息
消息由key、value两部分构成,在发送消息是可以指定ket,那么producer会根据key和partition机制来判断消息该发送并存储到哪个partition中。
默认情况下,kafka采用hash取模的分区算法。如果key=nullkafka会在metadata.max.age.ms的时间范围内随机分配分区。对于这个时间段内,如果key=null那么会发送到唯一的一个分区,默认是10分钟。
9.2 消费者消费消息
消费者可以通过以下代码指定消费哪个分区的消息
//消费者指定分区时不需要订阅
//kafkaConsumer.subscribe(Collections.singletonList(topic));
//消费者指定分区
TopicPartition topicTartition = new TopicPartition(topic,0);
kafkaConsumer.assign(Arrays.asList(topicPartition));
9.3 消息消费原理
当一个topic有多个partition,同时一个consumer group下有多个consumer时,每个consumer会根据分区分配策略分配固定的partition。
分区分配策略
Range strategy(范围分区)
RoundRobin strategy(轮训分区)
当出现以下几种情况会出现重新分配分区(rebalance)
- 同一个consumer group内新增了消费者
- 消费者离开当前所属consumer group
- topic新增了分区
kafka提供了一个角色:coordinator来执行对consumer group的管理。当consumer group的第一个consumer启动时会去和kafka server确定谁是他们group 的coordinator。之后该group的所有成员都会和该coordinator进行协调通信。consumer向kafka集群中的任意一个broker发送一个GroupCoordinatorRequest请求,服务端会返回一个负载最小的broker节点的id,将该broker设置为coordinator。
9.4 rebalance
整个rebalance过程分为两步,join和sync
join表示加入到consumer group中,这一步中所有的成员都会想coordinator发送join Group的请求,一旦所有成员都发送了joinGroup的请求,那么coordinator会选择一个consumer担任leader角色,并把组员信息和订阅信息发送给消费者
protocol_metadata:序列化后的消费者订阅信息
leader_id:consumer group中的leader
member_metadata:对应消费者的订阅信息
members:consumer group中全部的消费者的订阅信息
generation_id:年代信息。每次rebalance会+1,主要用来隔离无效的offset提交,上一轮的consumer成员无法提交offset到新的consumer grouo中
sync阶段 完成分区分配后进入了Synchronizing Group State阶段。此阶段想Coordinator发送SyncGroupRequest请求,并且处理SyncGroupResponse响应。
每个消费者向coordinator发送SyncGroupRequest,只有leader节点会发送分配方案。当leader把分配方案发送给coordinator后,coordinator会把结果设置到SyncGroupReponse中。
9.5 offset
offset是消息在自己partition的唯一标识,kafka提供了一个_consumer_offsets_*的一个topic,把offset信息写入到这个topic中。_consumer_offsets默认有50个分区。根据groupid。hashcode取模确认保存在哪个partition。
十、消息的存储##
kafka使用日志we年来保存生产者和发送者的消息。kafka中村粗的一般都是海量的数据,为了避免日志文件过啊,Log并不是直接对用在一个裁判上的日志文件,而是对应磁盘上的一个目录。
kafka通过以下手段提升写入性能
- 顺序写入
- 零拷贝
- 批量写入
- 分段
kafka通过分段方式将Log分为多个LogSegment,一个LogSegment对应磁盘上的一个日志文件和一个索引文件。为了提高查找消息的性能,kafka为每个日志文件添加2个索引文件:offsetIndex和timeIndex。
index中存储了索引以及物理便宜量。log中存储了消息内容。索引文件的元数据执行对应数据文件中的物理偏移地址(ByteBuffer的指针位置)。
消息的查找过程
- 由于index文件根据offset明明的,即下一个index文件名是上一个文件最后一个offset+1。所以通过二分法可以快速找到index文件
- 根据offset找到索引文件中服务范围的索引(稀疏索引提升性能,相同的position只记录一个其实offset)
- 拿到position后到对应的log文件中从position开始查找offset对应的消息。
十一、日志清除策略和压缩策略
11.1 日志清理
日志分段存储减少了单个文件的大小同时也方便了日志清理
清理策略有两个
- 根据消息保留时间,当保存的消息超过指定时间(log.retention.hours)出发清理
- 当日志文件大于指定的阈值(log.retention.bytes)删除最旧的消息
11.2 日志压缩
kafka服务端会在后台启动Cleaner线程池,定义将相同key的消息进行合并,只保留最新的value
十二、副本机制
虽然kafka可以为每个topic指定partion。但是每个partition都是单节点的,不是高可用的。所以kafka提出了副本(replica)的概念。通过副本实现冗余备份。每个partition可以有多个副本,但只有一个leader副本。所有读写请求都是到leader副本,follower副本只负责同步消息日志。当leader副本宕机后通过重新选举会产生新的leader副本从而实现高可用。leader负责维护和跟踪ISR(副本同步队列)中所有follower之后状态,如果follower消息滞后达到阈值则会将follower移除ISR。
12.1 副本数据同步
副本主要有三类
- leader副本
- follower副本
- ISR副本:包含了leader副本和与leader保持同步的follower副本
每个副本都有两个重要属性
- LEO:日志末端位移,该副本下一条消息的offset
- HW: 已备份的最大消息的offset
一条消息只有被ISR里所有的follower都从leader复制过去才会被认为已提交。
producer将消息发送到leader,leader将消息写入到本地Log,每个follower都从leader pull数据。follower收到消息并写入log后想leade发送ACK,一旦leader收到ISR中的所有的Replica的ACK,该消息就被认为是已经commit了。
消息同步过程
- leader在收到一个消息后将消息追加到log文件通话更新leaderd LEO
- 尝试更新leader HW.这是follower还没有发送fetch请求,那么leader的remote LEO还是0。leader对比自己的LEO和remote LEO,发现最小值是0,与HW相同,不更新HW
- follower发送fetch请求,leader副本读取log数据,更新remote LEO=0
- leader尝试更新HW,因为这个时候LEO=1 remoteLEO=0 所以HW=0
- leader把消息内容和当前HW发送给follower,follower收到response后 将消息写入本地log,同时更新follow的LEO
- follower尝试更新HW。本地的LEO和leader返回的HW取较小的值,HW=0
经过第一次交互后HW任然是0
follow 第二次发起fetch请求
- leader读取log数据
- leader更新remote LEO=1
- 更新分区的HW,这是LEO 和remote LEO都是1,所以HW=1
- 把数据和当前分区的HW值返回给follower
- follower收到response后更新自己的LEO
- follower更新自己的HW值
至此数据同步完成,消费端可以消费offset=0的消息了
当ISR中最小副本数=1且acks=-1(需要所有副本确认)时,一旦消息本写入leadr端即任务消息已提交,follower副本还没收到结果之前宕机了,那么重启后该follower会调整LEO至为之前的HW。此时会把这条消息从日志中删除同时调整LEO。follower恢复之后发起fetch请求,此时leader宕机,此时原来的follower几点会被重新选举为leader,原来的leader节点重启后变成follower节点,按照follower节点的处理逻辑删除了该消息调整了LEO,这样导致消息丢失。
kafka使用leader epoch来解决这个问题,epoch是leader的版本号。kafka使用leader-epoch-checkpoint文件记录该值。值的形式为(0,1) (1,15) 标识对应epoch的leader第一条消息的offset。
follower副本在宕机重启后会拉去epoch值,检测发现没有offset大于当前值的epoch,此时不会更新LEO也不会删除日志。
十三、使用
13.1 生产者
private final KafkaProducer<Integer,String> producer;
private final String topic;
public KafkaProducerDemo(String topic){
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"172.31.17.48:9092");
properties.put(ProducerConfig.CLIENT_ID_CONFIG,"KafkaProducerDemo");
properties.put(ProducerConfig.ACKS_CONFIG,"-1");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.IntegerSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
producer = new KafkaProducer<Integer, String>(properties);
this.topic = topic;
producer.send(new ProducerRecord<Integer,String>(topic,"111"));
}
13.2 消费者
private final KafkaConsumer<Integer,String> consumer;
public KafkaConsumerDemo(String topic){
Properties properties = new Properties();
properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"172.31.17.48:9092");
properties.put(ConsumerConfig.GROUP_ID_CONFIG,"KafkaConsumerDemo");
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.IntegerDeserializer");
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
consumer = new KafkaConsumer<Integer, String>(properties);
consumer.subscribe(Collections.singletonList(topic));
ConsumerRecords<Integer, String> records = consumer.poll(1000);
}