kafka简介
Kafka是由Apache软件基金会开发的一个开源流平台,由Scala和Java编写。
官网描述:
Apache Kafka是一个分布式流平台。一个分布式的流平台应该包含3点关键的能力:
- 发布和订阅流数据流,类似于消息队列或者是企业消息传递系统
- 以容错的持久化方式存储数据流
- 处理数据流
kafka重要概念
架构
简单的结构图
1.broker
一个独立的kafka服务器被称为broker。broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker为消费者提供服务,对读取分区的请求作出相应,返回已经提交到磁盘上的消息。
- 一个Kafka的集群通常由多个broker组成,这样才能实现负载均衡、以及容错
- broker是**无状态(Sateless)**的,它们是通过ZooKeeper来维护集群状态
- 一个Kafka的broker每秒可以处理数十万次读写,每个broker都可以处理TB消息而不影响性能
2.zookeeper
- ZK用来管理和协调broker,并且存储了Kafka的元数据(例如:有多少topic、partition、consumer)
- ZK服务主要用于通知生产者和消费者Kafka集群中有新的broker加入、或者Kafka集群中出现故障的broker。
PS:Kafka正在逐步想办法将ZooKeeper剥离,维护两套集群成本较高,社区提出KIP-500就是要替换掉ZooKeeper的依赖。“Kafka on Kafka”——Kafka自己来管理自己的元数据
3.producer(生产者)
- 生产者负责将数据推送给broker的topic
4.consumer(消费者)
- 消费者负责从broker的topic中拉取数据,并自己进行处理
5.consumer group(消费者组)
- consumer group是kafka提供的可扩展且具有容错性的消费者机制
- 一个消费者组可以包含多个消费者
- 一个消费者组有一个唯一的ID(group Id)
- 组内的消费者一起消费主题的所有分区数据
6.分区(Partitions)
在Kafka集群中,主题被分为多个分区
7.副本(Replicas)
- 副本可以确保某个服务器出现故障时,确保数据依然可用
- 在Kafka中,一般都会设计副本的个数>1
8.主题(Topic)
- 主题是一个逻辑概念,用于生产者发布数据,消费者拉取数据
- Kafka中的主题必须要有标识符,而且是唯一的,Kafka中可以有任意数量的主题,没有数量上的限制
- 在主题中的消息是有结构的,一般一个主题包含某一类消息
- 一旦生产者发送消息到主题中,这些消息就不能被更新(更改)
9. 偏移量(offset)
- offset记录着下一条将要发送给Consumer的消息的序号
- 默认Kafka将offset存储在ZooKeeper中
- 在一个分区中,消息是有顺序的方式存储着,每个在分区的消费都是有一个递增的id。这个就是偏移量offset
- 偏移量在分区中才是有意义的。在分区之间,offset是没有任何意义的
kafka生产者幂等性
什么是幂等性
幂等性的定义是:一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
幂等性产生原因
- 前端未做限制,导致用户重复提交
- 使用浏览器后退,或者按F5刷新,或者使用历史记录,重复提交表单
- 网络波动,引起重复请求
- 超时重试,引起接口重复调用
- 定时任务设置不合理,导致数据重复处理
- 使用消息队列时,消息重复消费
1.1.1 Kafka生产者幂等性
在生产者生产消息时,如果出现retry时,有可能会一条消息被发送了多次,如果Kafka不具备幂等性的,就有可能会在partition中保存多条一模一样的消息。
1.1.1 配置幂等性
props.put("enable.idempotence",true);
1.1.2 幂等性原理
为了实现生产者的幂等性,Kafka引入了 Producer ID(PID)和 Sequence Number的概念。
- PID:每个Producer在初始化时,都会分配一个唯一的PID,这个PID对用户来说,是透明的,对客户使用段是不可见的。
- Sequence Number:对于每个ProducerID,Producer发送数据的每个Topic和Partition都对应⼀个从0开始单调递增的SequenceNumber值。
kafka事务
事务
Kafka事务是2017年Kafka 0.11.0.0引入的新特性。类似于数据库的事务。Kafka事务指的是生产者生产消息以及消费者提交offset的操作可以在一个原子操作中,要么都成功,要么都失败。尤其是在生产者、消费者并存时,事务的保障尤其重要。(consumer-transform-producer模式)
1.事务操作API
Producer接口中定义了以下5个事务相关方法:
-
initTransactions(初始化事务):要使用Kafka事务,必须先进行初始化操作
-
beginTransaction(开始事务):启动一个Kafka事务
-
sendOffsetsToTransaction(提交偏移量):批量地将分区对应的offset发送到事务中,方便后续一块提交
-
commitTransaction(提交事务):提交事务
-
abortTransaction(放弃事务):取消事务
2.事务相关属性配置
1.生产者
//配置事务的id,开启了事务会默认开启幂等性
props.put("transactional.id"**, **"first-transactional");
2.消费者
// 1.消费者需要设置隔离级别
props.put("isolation.level","read_committed");
// 2.关闭自动提交
props.put("enable.auto.commit","false");
3.生产者事务代码示例
public static void main(String[] args) {
Consumer<String, String> consumer = createConsumer();
Producer<String, String> producer = createProducer();
// 初始化事务
producer.initTransactions();
while(true) {
try {
// 1. 开启事务
producer.beginTransaction();
// 2. 定义Map结构,用于保存分区对应的offset
Map<TopicPartition, OffsetAndMetadata> offsetCommits = new HashMap<>();
// 2. 拉取消息
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(2));
for (ConsumerRecord<String, String> record : records) {
// 3. 保存偏移量
offsetCommits.put(new TopicPartition(record.topic(), record.partition()),
new OffsetAndMetadata(record.offset() + 1));
// 4. 进行转换处理
String[] fields = record.value().split(",");
fields[1] = fields[1].equalsIgnoreCase("1") ? "男":"女";
String message = fields[0] + "," + fields[1] + "," + fields[2];
// 5. 生产消息到dwd_user
producer.send(new ProducerRecord<>("dwd_user", message));
}
// 6. 提交偏移量到事务
producer.sendOffsetsToTransaction(offsetCommits, "ods_user");
// 7. 提交事务
producer.commitTransaction();
} catch (Exception e) {
// 8. 放弃事务
producer.abortTransaction();
}
}
}
//可以自行模拟测试异常情况,发现如果中间出现异常的话,offset是不会被提交的,除非消费、生产消息都成功,才会提交事务。
kafka分区
Kafka的消息通过主题(Topic)进行分类,就好比是数据库的表,或者是文件系统里的文件夹。主题可以被分为若干个分区(Partition),一个分区就是一个提交日志。消息以追加的方式写入分区,然后以先进先出的顺序读取。**注意,由于一个主题一般包含几个分区,因此无法在整个主题范围内保证消息的顺序,但可以保证消息在单个分区内的顺序。**主题是逻辑上的概念,在物理上,一个主题是横跨多个服务器的。
1.生产者分区策略
生产者写入消息到topic,Kafka将依据不同的策略将数据分配到不同的分区中,支持自定义分区策略
- 如果指定了分区,那么分区器就不会再做任何事情,直接发送到该分区;
- 如果发送时未指定,则默认使用key的hash值指定一个分区;
- 如果发送时未指定消息key,则采用轮询的方式选择一个分区(这就是Kafka默认的分区策略)。
- 2.4版本过后Kafka默认的分区策略改为了sticky策略
轮询策略(Round-robin 默认)
轮询策略,即顺序分配。
- 轮询策略是 Kafka Java 生产者 API 默认提供的分区策略;
- 轮询策略的负载均衡表现非常优秀,总能保证消息最大限度地被平均分配到所有分区上,默认情况下它是最合理的分区策略。
随机策略(Randomness)
随意地将消息放置到任意一个分区上,但是可以从图中看出该分区策略会导致某些分区的消息过多。
如果要实现随机策略版的 partition 方法,很简单,只需要两行代码即可:
List partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());
先计算出该主题总的分区数,然后随机地返回一个小于它的正整数。本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,所以如果追求数据的均匀分布,还是使用轮询策略比较好。事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。
按消息键保序策略(Key-ordering)
说明
- Kafka允许为每条消息定义消息键,简称为Key
- Key可以是一个有明确业务含义的字符串:客户代码、部门编号、业务ID、用来表征消息的元数据等
- 一旦消息被定义了Key,可以保证同一个Key的所有消息都进入到相同的分区里,由于每个分区下的消息处理都是顺序的,所以这个策略被称为按消息键保序策略
实现这个策略的 partition 方法同样简单,只需要下面两行代码即可:
List partitions = cluster.partitionsForTopic(topic);
return Math.abs(key.hashCode()) % partitions.size();
自定义分区策略
如想实现自定义分区策略,直接实现Partitioner接口,重写接口中的方法。
//kafka java api`提供了一个接口,用于自定义分区策略:`org.apache.kafka.clients.producer.Partitioner
public interface Partitioner extends Configurable, Closeable {
/**
* Compute the partition for the given record.
*
* @param topic The topic name
* @param key The key to partition on (or null if no key)
* @param keyBytes The serialized key to partition on( or null if no key)
* @param value The value to partition on or null
* @param valueBytes The serialized value to partition on or null
* @param cluster The current cluster metadata
*/
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
/**
* This is called when partitioner is closed.
*/
public void close();
}
说明
partition()
:计算给定记录的分区
参数说明
参数 | 说明 |
---|---|
topic | 需要传递的主题 |
key | 消息中的键值 |
keyBytes | 分区中序列化过后的key,byte数组的形式传递 |
value | 消息的 value 值 |
valueBytes | 分区中序列化后的值数组 |
cluster | 当前集群的原数据 |
close()
: 继承了Closeable
接口能够实现 close() 方法,在分区关闭时调用。onNewBatch()
: 表示通知分区程序用来创建新的批次
各分区优缺点
-
轮询策略(默认分区策略)
优点:可以提供非常优秀的负载均衡能力,可以保证消息被平均分配到所有分区上。
缺点:无法保证消息的有序性。 -
随机策略
优点:消息的分区选择逻辑简单。
缺点:负载均衡能力一般,也无法保证消息的有序性 -
按消息键保序策略
优点:可以保证相同key的消息被发送到相同的分区,因此可以保证相同key的所有消息之间的顺序性。
缺点:可能会产生数据倾斜 —— 取决于数据中key的分布,以及使用的hash算法。
Kafka Java生产者的默认分区策略:
- 如果指定了Key,采用按消息键保序策略
- 如果没有指定Key,采用轮询策略
参考:https://lilinchao.com/archives/1515.html
2.消费者分区策略
轮询策略
RoundRobin是针对所有topic分区。它是采用轮询分区策略,是把所有的partition和所有的consumer列举出来,然后按照hashcode进行排序,最后再通过轮询算法来分配partition给每个消费者。
当某一个消费者下线后,则重新进行分配。
随机策略
Range策略是kafka默认的消费者分区分配策略,它是针对topic维度的,首先对同一个topic里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序,它可以确保每个消费者消费的分区数量是均衡的。
粘性分配策略
粘性分区定义:可以理解为分配的结果带有“粘性的”。即在执行一次新的分配之前,考虑上一次分配的结果。尽量少的调整分配的变动,可以节省大量的开销。
没有发生rebalance时,Striky粘性分配策略和RoundRobin分配策略类似。
上面如果consumer2崩溃了,此时需要进行rebalance。如果是Range分配和轮询分配都会重新进行分配,例如:
通过上图,我们发现,consumer0和consumer1原来消费的分区大多发生了改变。接下来我们再来看下粘性分配策略。
我们发现,Striky粘性分配策略,保留rebalance之前的分配结果。这样,只是将原先consumer2负责的两个分区再均匀分配给consumer0、consumer1。这样可以明显减少系统资源的浪费,例如:之前consumer0、consumer1之前正在消费某几个分区,但由于rebalance发生,导致consumer0、consumer1需要重新消费之前正在处理的分区,导致不必要的系统开销。(例如:某个事务正在进行就必须要取消了)
kafka副本
副本的ACK参数
在kafka集群中,每个Partition都有多个副本,其中一个副本叫做leader,其他的副本叫做follower,对副本关系较大的就是,生产者(producer)配置的acks参数了, acks参数表示当生产者生产消息的时候,写入到副本的要求严格程度。它决定了生产者如何在性能和可靠性之间做取舍。
Properties props = new Properties();
props.put("bootstrap.servers", "node1.itcast.cn:9092");
// 这里的ack参数有:-1、0、1、all
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
- acks = 0:生产者只管写入,不管是否写入成功,可能会数据丢失。性能是最好的
- acks = 1:生产者会等到leader分区写入成功后,返回成功,接着发送下一条
- acks = -1/all:确保消息写入到leader分区、还确保消息写入到对应副本都成功后,接着发送下一条,性能是最差的
根据业务情况来选择ack机制,是要求性能最高,一部分数据丢失影响不大,可以选择0/1。如果要求数据一定不能丢失,就得配置为-1/all。
leader和follower
- Kafka中的leader和follower是相对分区有意义,不是相对broker
- Kafka在创建topic的时候,会尽量分配分区的leader在不同的broker中,其实就是负载均衡
- leader职责:读写数据
- follower职责:同步数据、参与选举(leader crash之后,会通过zookeeper选举一个follower重新成为分区的leader)
- 注意和ZooKeeper区分
- ZK的leader负责读、写,follower可以读取
- Kafka的leader负责读写、follower只负责副本数据的同步(确保每个消费者消费的数据是一致的),Kafka一个topic有多个分区leader,一样可以实现数据操作的负载均衡
AR
AR(Assigned Replicas) 是指一个topic下为每个分区分配的副本集合。在Kafka中,每个分区可以有多个副本,其中一个副本被选举为leader,其他副本为follower。AR是指包括leader副本在内的所有副本的集合。
ISR
**ISR(In-Sync Replicas)**是指与leader副本保持同步的follower副本集合。只有处于ISR中的副本才会被认为是同步的,其他副本将被视为不可靠的。当follower副本无法及时跟上leader副本的同步进度时,它将被移出ISR,直到它能够追赶上来。ISR机制确保了数据的一致性和可靠性。
OSR
OSR(Out-of-Sync Replicas) 是指与leader副本不同步的follower副本集合。当follower副本无法及时跟上leader副本的同步进度时,它将被移出ISR,并被标记为OSR。OSR副本将尝试追赶上来,一旦追赶上来并与leader副本保持同步,它将被重新添加到ISR中。
AR = ISR + OSR
HW(High Watermark)
HW(High Watermark)表示Kafka分区中已经被确认的最高偏移量。它代表了消费者可以安全地读取的消息位置。消费者只能消费高于HW的消息,确保消息的可靠性。
LEO(Log End Offset)
LEO(Log End Offset)表示Kafka分区中当前的最高偏移量。它代表了分区中最新的消息位置,包括已经写入但尚未被确认的消息。LEO是一个动态的值,随着消息的写入和确认而变化。
HW和LEO在Kafka中用于管理消息的可靠性和一致性。消费者可以通过比较HW和LEO来确定自己的消费进度,并确保不会丢失任何重要的消息。
生产者数据不丢失
生产者连接leader写入数据时,可以通过ACK机制来确保数据已经成功写入
生产者可以采用同步和异步两种方式发送数据
- 同步:发送一批数据给kafka后,等待kafka返回结果
- 异步:发送一批数据给kafka,只是提供一个回调函数。
说明:如果broker迟迟不给ack,而buffer又满了,开发者可以设置是否直接清空buffer中的数据。
消费者数据不丢失
在消费者消费数据的时候,只要每个消费者记录好offset值即可,就能保证数据不丢失。
- At-least once:一种数据可能会重复消费
- Exactly-Once:仅被一次消费
数据积压&数据清理
企业中通过监控系统进行监控,如果发现数据积压就及时进行处理。
数据清理
- Log Deletion(日志删除):如果消息达到一定的条件(时间、日志大小、offset大小),Kafka就会自动将日志设置为待删除(segment端的后缀名会以 .delete结尾),日志管理程序会定期清理这些日志
- 默认是7天过期
- Log Compaction(日志合并)
- 如果在一些key-value数据中,一个key可以对应多个不同版本的value
- 经过日志合并,就会只保留最新的一个版本