背景
Kafka 是为了解决 LinkedIn 数据管道问题应运而生的。它的设计目的是提供一个高性能的消息系统,可以处理多种数据类型,并能够实时提供纯净且结构化的用户活动数据和系统度量指标,可以实时访问数据,并通过横向扩展来处理大量的消息。
设计目的:
- 使用推送和拉取模型解耦生产者和消费者;
- 为消息传递系统中的消息提供数据持久化,以便支持多个消费者;
- 通过系统优化实现高吞吐量;
- 系统可以随着数据流的增长进行横向扩展。
所以,Kafka 是一个分布式的、高吞吐量的、可持久性的、自动负载均衡的消息队列,同时 Kafka 从一定意义上来说具有横向易扩展性,通过 Kafka 也可以降低系统间的耦合度。
介绍
整体架构图
(来源:
Kafka系列第1篇:Kafka是什么?它能干什么?xie.infoq.cn)
相关组件介绍
Producer
消息发布者;即主要作用是生产数据,并将产生的数据推送给 Kafka 集群。
Consumer
消息消费者;即主要作用是 kafka 集群中的消息,并将处理结果推送到下游或者是写入 DB 资源等。
Zookeeper Cluster
存储 Kafka 集群的元数据信息,比如记录注册的 Broker 列表,topic 元数据信息,partition 元数据信息等等。
Broker
Kafka 集群由多台服务器构成,每台服务器称之为一个 Broker 节点。
Topic
主题,表示一类消息,consumer 通过订阅 Topic 来消费消息,一个 Broker 节点可以有多个 Topic,每个 Topic 又包含 N 个 partition(分区或者分片)。
Partition
partition 是一个有序且不可变的消息序列,它是以 append log 文件形式存储的,partition 用于存放 Producer 生产的消息,然后 Consumer 消费 partition 上的消息,每个 partition 只能被一个 Consumer 消费。
根据架构图,可以看到,整个Kafka架构主要关注的点主要为3个:
- 生产者将消息从客户端发送到Broker的生产模式;
- 消息队列在Broker上的流转和存储模式;
- 消费者从Broker获取数据的消费模式。
1、客户端生产模式
在介绍这块之前,先介绍一下broker。
一个独立的Kafka服务器被称为broker。broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。根据特定的硬件及其性能特征,单个broker可以轻松处理数千个分区以及每秒百万级的消息量。
broker是集群的组成部分。每个集群都有一个broker 同时充当了集群控制器 的角色(自动从集群的活跃成员中选举出来。控制器负责管理工作,包括将分区分配给broker和监控 broker。在集群中,一个分区从属于一个broker,该 broker被称为分区的 首领。一个分区可以分配给多个broker,这个时候会发生分区复制。这种复制机制为分区提供了消息冗余,如果有一个 broker失效,其他broker可以接管领导权。不过,相关的消费者和生产者都要重新连接到新的首领。
然后在介绍一下topic和partitions。
Kafka的消息通过topic进行分类。主题就好比数据库的表,或者文件系统里的文件夹。topic可以被分为若干个partitions,一个partitions就是一个提交日志。消息以追加的方式写入partitions,然后以先入先出的顺序读取。要注意,由于一个topic一般包含几个partitions,因此无法在整个topic范围内保证消息的顺序,但可以保证消息在单个partition内的顺序。
Kafka 通过partition来实现数据冗余和伸缩性。partition可以分布在不同的broker上,也就是说,一个topic可以横跨多个broker,以此来提供更强大的性能。
其中注意,消息在同一个partition里面是有序的,但是在消费者全局获取的时候是无序的。
一、为什么只保证单partition有序,如果Kafka要保证多个partition有序,不仅broker保存的数据要保持顺序,消费时也要按序消费。假设partition1堵了,为了有序,那partition2以及后续的分区也不能被消费,这种情况下,Kafka 就退化成了单一队列,毫无并发性可言,极大降低系统性能。因此Kafka使用多partition的概念,并且只保证单partition有序。这样不同partiiton之间不会干扰对方。
二、Kafka如何保证单partition有序?producer发消息到队列时,通过加锁保证有序现在假设两个问题broker leader在给producer发送ack时,因网络原因超时,那么Producer 将重试,造成消息重复。先后两条消息发送。t1时刻msg1发送失败,msg2发送成功,t2时刻msg1重试后发送成功。造成乱序。
解决重试机制引起的消息乱序为实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。对于每个PID,该Producer发送消息的每个<Topic, Partition>都对应一个单调递增的Sequence Number。同样,Broker端也会为每个<PID, Topic, Partition>维护一个序号,并且每Commit一条消息时将其对应序号递增。对于接收的每条消息,如果其序号比Broker维护的序号)大一,则Broker会接受它,否则将其丢弃:如果消息序号比Broker维护的序号差值比一大,说明中间有数据尚未写入,即乱序,此时Broker拒绝该消息,Producer抛出InvalidSequenceNumber如果消息序号小于等于Broker维护的序号,说明该消息已被保存,即为重复消息,Broker直接丢弃该消息,Producer抛出DuplicateSequenceNumberSender发送失败后会重试,这样可以保证每个消息都被发送到broker。
所以,客户端存储模型是:
- 一条消息首先需要确定要被存储到哪个 partition 对应的双端队列上(消息的分发);
- 其次,存储消息的双端队列是以批的维度存储的,即 N 条消息组成一批,一批消息最多存储 N 条,超过后则新建一个组来存储新消息;
- 其次,新来的消息总是从左侧写入,即越靠左侧的消息产生的时间越晚;
- 最后,只有当一批消息凑够 N 条后才会发送给 Broker,否则不会发送到 Broker 上。
注意虽然在理论上是分bach来发送到broker上的,但是实际上,我们在写数据到Kafka的时候,很明显的可以发现是写一条消费一条,不存在等待一会出现一批,这是为什么呢,因为在kafka producer端有一个配置:
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("retries", 0);
props.put("batch.size", 16384);
props.put("linger.ms", 1);
props.put("buffer.memory", 33554432);
props.put("compresstion.type","snappy");
props.put("partitioner.class", "org.apache.kafka.clients.producer.internals.DefaultPartitioner");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
1、producer端ack应答机制,本demo中ack设置为all,表示生产者会等待所有副本成功写入该消息,
这种方式是最安全的,能够保证消息不丢失,但是延迟也是最大的,默认是1;
2、retries设置标示消息发送失败,生产者可以自动重试,但是此刻设置为0标示不重试;
这个参数需要结合retry.backoff.ms(重试等待间隔)来使用,
建议总的重试时间比集群重新选举群首的时间长,这样可以避免生产者过早结束重试导致失败;
3、batch.size参数标示生产者为每个分区维护了一个未发送记录的缓冲区,
这个缓冲区的大小由batch.size配置指定,配置的很大可能会导致更多的批处理,
也需要更多的内存(但是对于每个活动分区,我们通常都有一个这样的缓冲区),默认是16384Bytes;
4、linger.ms 指定生产者在发送批量消息前等待的时间,当设置此参数后,
即便没有达到批量消息的指定大小,到达时间后生产者也会发送批量消息到broker.
默认情况下,生产者的发送消息线程只要空闲了就会发送消息,即便只有一条消息.设置这个参数后,发送线程会等待一定的时间,这样可以批量发送消息增加吞吐量,但同时也会增加延迟;
5、buffer.memory控制生产者可用于缓冲的内存总量;
消息的发送速度超过了它们可以传输到服务器的速度,那么这个缓冲空间将被耗尽.
6、compresstion.type:默认情况下消息是不压缩的,
这个参数可以指定使用消息压缩,参数可以取值为snappy、gzip或者lz4
其中:
batch.size(默认16K)和linger.ms(默认0ms),当这两个参数同时设置的时候,
只要两个条件中满足一个就会发送。比如说batch.size设置16kb,linger.ms设置50ms,
那么当消息积压达到16kb就会发送,如果没有到达16kb,那么在第一个消息到来之后的50ms之后消息将会发送。
所以,在默认情况下linger.ms=0ms,消息会来一条写入1条,消费一条。
2、消息的分发模式
一条消息由如下三部分构成:
- OffSet:偏移量,消息在客户端发送前将相对偏移量存储到该位置,当消息存储到 LogSegment 前,先将其修改为绝对偏移量在写入磁盘。
- Size:本条 Message 的内容大小
- Message:消息的具体内容,其具体又由 7 部分组成,crc 用于校验消息,Attribute 代表了属性,key-length 和 value-length 分别代表 key 和 value 的长度,key 和 value 分别代表了其对应的内容。
消息写入流程主要分为如下几步:
- 客户端消息收集器收集属于同一个分区的消息,并对每条消息设置一个偏移量,且每一批消息总是从 0 开始单调递增。比如第一次发送 3 条消息,则对三条消息依次编号 [0,1,2],第二次发送 4 条消息,则消息依次编号为 [0,1,2,3]。注意此处设置的消息偏移量是相对偏移量。
- 客户端将消息发送给服务端,服务端拿到下一条消息的绝对偏移量,将传到服务端的这批消息的相对偏移量修改成绝对偏移量。
- 将修改后的消息以追加的方式追加到当前活跃的 LogSegment 后面,然后更新绝对偏移量。
- 将消息集写入到文件通道。
- 文件通道将消息集 flush 到磁盘,完成消息的写入操作。
每条消息在被实际存储到磁盘时都会被分配一个绝对偏移量后才能被写入磁盘。在同一个分区内,消息的绝对偏移量都是从 0 开始,且单调递增;在不同分区内,消息的绝对偏移量是没有任何关系的。
Kafka分区的作用个人觉得就是提供一种负载均衡的能力,或者说对数据进行分区的主要原因,就是为了实现系统的高伸缩性(Scalability),所以在生产者端,也会有专门的分区策略,将获取到的消息写入对应的分区。
所谓分区策略是决定生产者将消息发送到哪个分区的算法。Kafka 为我们提供了默认的分区策略,同时它也支持你自定义分区策略。
从上面的消息的组成模式中可以看出,Kafka的消息提供了很多的信息,就是希望让你能够充分地利用这些信息对消息进行分区,计算出它要被发送到哪个分区中。只要你自己的实现类定义好了partition 方法,同时设置partitioner.class参数为你自己实现类的 Full Qualified Name,那么生产者程序就会按照你的代码逻辑对消息进行分区。虽说可以有无数种分区的可能,但比较常见的分区策略也就那么几种,下面我来详细介绍一下。
轮询策略
也称 Round-robin 策略,即顺序分配。比如一个主题下有 3 个分区,那么第一条消息被发送到分区 0,第二条被发送到分区 1,第三条被发送到分区 2,以此类推。当生产第 4 条消息时又会重新开始,即将其分配到分区 0,就像下面这张图展示的那样。
这就是所谓的轮询策略。轮询策略是 Kafka Java 生产者 API 默认提供的分区策略。轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。
随机策略
也称 Randomness 策略。所谓随机就是我们随意地将消息放置到任意一个分区上,如下面这张图所示。
先计算出该主题总的分区数,然后随机地返回一个小于它的正整数。本质上看随机策略也是力求将数据均匀地打散到各个分区,但从实际表现来看,它要逊于轮询策略,事实上,随机策略是老版本生产者使用的分区策略,在新版本中已经改为轮询了。
按消息key保存策略
也称 Key-ordering 策略,自定义策略的一种,也是最常用的一种,大部分情况是为了保证消费到不同分区的数据也可以保持顺序的实现方案。
Kafka的消息中都含有key。它可以是一个有着明确业务含义的字符串,比如客户代码、部门编号或是业务 ID 等;也可以用来表征消息元数据。特别是在 Kafka 不支持时间戳的年代,在一些场景中,工程师们都是直接将消息创建时间封装进 Key 里面的。一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略。
Kafka 默认分区策略实际上同时实现了两种策略:如果指定了 Key,那么默认实现按消息Key保序策略;如果没有指定 Key,则使用轮询策略。
注意,一切的分区策略都是为了实现负载均衡以及高吞吐量的关键,所以一定要在生产者这一端就要考虑好合适的分区策略,避免造成消息数据的“倾斜”,使得某些分区成为性能瓶颈,从而导致下游数据消费的性能下降的问题。
3、消息的消费模式
Kafka 消费端确保一个 Partition 在一个消费者组内只能被一个消费者消费。即:
- 在同一个消费者组内,一个 Partition 只能被一个消费者消费。
- 在同一个消费者组内,所有消费者组合起来必定可以消费一个 Topic 下的所有 Partition。
- 在同一个消费组内,一个消费者可以消费多个 Partition 的信息。
- 在不同消费者组内,同一个分区可以被多个消费者消费。
- 每个消费者组一定会完整消费一个 Topic 下的所有 Partition。
为什么存在消费者组,原因:
1、在实际生产中,对于同一个 Topic,可能有 A、B、C 等 N 个消费方想要消费。比如一份用户点击日志,A 消费方想用来做一个用户近 N 天点击过哪些商品;B 消费方想用来做一个用户近 N 天点击过前 TopN 个相似的商品;C 消费方想用来做一个根据用户点击过的商品推荐相关周边的商品需求。对于多应用场景,就可以使用消费组来隔离不同的业务使用场景,从而达到一个 Topic 可以被多个消费组重复消费的目的。
2、消费组与 Partition 的消费进度绑定。当有新的消费者加入或者有消费者从消费组退出时,会触发消费组的 Repartition 操作(后面会详细介绍 Repartition);在 Repartition 前,Partition1 被消费组的消费者 A 进行消费,Repartition 后,Partition1 消费组的消费者 B 进行消费,为了避免消息被重复消费,需要从消费组记录的 Partition 消费进度读取当前消费到的位置(即 OffSet 位置),然后在继续消费,从而达到消费者的平滑迁移,同时也提高了系统的可用性。
所谓 Repartition 即 Partition 在某些情况下重新被分配给参与消费的消费者。在调整Topic的分区个数或者调整消费者组的消费者个数的时候,都会触发Repartition。
注意消费者和分区的对应关系:
- 如果consumer比partition多,是浪费,因为kafka的设计是在一个partition上是不允许并发的, 所以consumer数不要大于partition数,会造成消费者过剩。
- 如果consumer比partition少,一个consumer会对应于多个partitions,这里主要合理分配 consumer数和partition数,否则会导致partition里面的数据被取的不均匀。最好partiton数目是 consumer数目的整数倍,所以partition数目很重要,比如取24,就很容易设定consumer数目。
- 如果consumer从多个partition读到数据,不保证数据间的顺序性,kafka只保证在一个partition 上数据是有序的,但多个partition,根据你读的顺序会有不同。
- 增减consumer,broker,partition会导致rebalance并触发Repartition,所以rebalance后consumer对应的 partition会发生变化。
同一个group中的消费者对于一个topic中的多个partition,存在一定的分区分配策略。在kafka中,存在三种分区分配策略:Range(默认)、 RoundRobin(轮询)、StickyAssignor(粘性)。
RangeAssignor(范围分区)
Range策略是对每个topic而言的,首先对同一个topic里面的分区按照序号进行排序,并对消费者按照字 母顺序进行排序。
- 假设n = 分区数/消费者数量
- m= 分区数%消费者数量
- 那么前m个消费者每个分配n+1个分区,后面的(消费者数量-m)个消费者每个分配n个分区。
假设我们有两个topic分别10个分区,3个消费者,排完序的分区将会是:
C1-0 将消费 T1的0, 1, 2, 3 分区和T2的0, 1, 2, 3 分区
C2-0 将消费 T1的4, 5, 6 分区和T2的4, 5, 6分区
C3-0 将消费 T1的7, 8, 9 分区和T2的7, 8, 9 分区
RoundRobinAssignor(轮询分区)
轮询分区策略是把所有partition和所有consumer线程都列出来,然后按照hashcode进行排序。最后通过轮询算法分配partition给消费线程。如果所有consumer实例的订阅是相同的,那么partition会均匀分布。
具体的实现方案和写入的轮询分区策略基本相同。
假设消费组有3个消费者:C0,C1,C2,它们分别订阅了4个Topic(t0,t1,t2,t3),并且每个主题有两个分 区(p0,p1),也就是说,整个消费组订阅了8个分区:t0p0 、 t0p1 、 t1p0 、 t1p1 、 t2p0 、 t2p1 、t3p0 、 t3p1 。
那么最终的分配场景结果为
C0: t0p0、t1p1 、 t3p0
C1: t0p1、t2p0 、 t3p1
C2: t1p0、t2p1
StrickyAssignor(粘滞策略)
kafka在0.11.x版本支持了StrickyAssignor, 翻译过来叫粘滞策略,它主要有两个目的:
- 分区的分配尽可能的均匀
- 分区的分配尽可能和上次分配保持相同
例如上面的轮询策略中,如果C1挂了,会造成重新分区,如果是轮询,那么结果应该是:
C0: t0p0、t1p0、t2p0、t3p0
C2: t0p1、t1p1、t2p1、t3p1
但是strickyAssignor它是一种粘滞策略,所以它会满足分区的分配尽量和上一次的分配一样,所以,分配结果变成:
C0: t0p0、t1p1、t3p0、t2p0
C2: t1p0、t2p1、t0p1、t3p1
也就是说,C0和C2保留了上一次是的分配结果,并且把原来C1的分区分配给了C0和C2。 这种策略的好处是 使得分区发生变化时,由于分区的“粘性,减少了不必要的分区移动。
消费模式
当消费者分区确定后,就可以消费消息了,消息的消费模型有两种:推送模型(push)和拉取模型(pull)。
推送模型(push):基于推送模型(push)的消息系统,由消息代理记录消费者的消费状态,消息代理在将消息推送到消费者后,标记这条消息为已消费,但这种方式无法很好地保证消息被处理,比如,消息代理把消息发送出去后,当消费进程挂掉或者由于网络原因没有收到这条消息时,就有可能造成消息丢失(因为消息代理已经把这条消息标记为已消费了,但实际上这条消息并没有被实际处理),如果要保证消息被处理,消息代理发送完消息后,要设置状态为“已发送”,只有收到消费者的确认请求后才更新为“已消费”,这就需要消息代理中记录所有的消费状态,这种做法显然是不可取的。
拉取模型(pull):Kafka采用拉取模型,由消费者自己记录消费状态,每个消费者互相独立地顺序读取每个分区的消息,有两个消费者(不同消费者组)拉取同一个主题的消息,消费者A的消费进度是3,消费者B的消费进度是6,消费者拉取的最大上限通过最高水位(watermark)控制,生产者最新写入的消息如果还没有达到备份数量,对消费者是不可见的,这种由消费者控制偏移量的优点是:消费者可以按照任意的顺序消费消息,比如:消费者可以重置到旧的偏移量,重新处理之前已经消费过的消息;或者直接跳到最近的位置,从当前的时刻开始消费。
Kafka高级消费API
优点:
- 高级API 写起来简单
- 不需要自行去管理offset,系统通过zookeeper自行管理。
- 不需要管理分区,副本等情况,系统自动管理。
- 消费者断线会自动根据上一次记录在zookeeper中的offset去接着获取数据(默认设置1分钟更新一下zookeeper中存的offset)。
- 可以使用group来区分对同一个topic 的不同程序访问分离开来(不同的group记录不同的offset,这样不同程序读取同一个topic才不会因为offset互相影响)。
缺点:
- 不能自行控制offset(对于某些特殊需求来说)
- 不能细化控制如分区、副本、zk等
Kafka低级消费API
优点:
- 能够让开发者自己控制offset,想从哪里读取就从哪里读取
- 自行控制连接分区,对分区自定义进行负载均衡
- 对zookeeper的依赖性降低(如:offset不一定非要靠zk存储,自行存储offset即可,比如存在文件或者内存中)
缺点:
太过复杂,需要自行控制offset,常见的将offset保存到HDFS、Redis、Mysql等等。
kafka消息保证机制
1、至少一次。即一条消息至少被消费一次,消息不可能丢失,但是可能会被重复消费。
消费者读取消息,先处理消息,在保存消费进度。消费者拉取到消息,先消费消息,然后在保存偏移量,当消费者消费消息后还没来得及保存偏移量,则会造成消息被重复消费。如下图所示:
2、至多一次。即一条消息最多可以被消费一次,消息不可能被重复消费,但是消息有可能丢失。
消费者读取消息,先保存消费进度,在处理消息。消费者拉取到消息,先保存了偏移量,当保存了偏移量后还没消费完消息,消费者挂了,则会造成未消费的消息丢失。
3、正好一次。即一条消息正好被消费一次,消息不可能丢失也不可能被重复消费。
正好消费一次的办法可以通过将消费者的消费进度和消息处理结果保存在一起。只要能保证两个操作是一个原子操作,就能达到正好消费一次的目的。通常可以将两个操作保存在一起,比如 HDFS 中。
结论:
Q:Kafka 为什么这么快
A:Kafka的消息是保存或缓存在磁盘上的,一般认为在磁盘上读写数据是会降低性能的,一般来说磁盘读取,会比较耗时,但是
- 顺序写入,通过只做Sequence I/O的限制,规避了磁盘访问速度低下对性能可能造成的影响。
- Memory Mapped Files(内存映射文件),Kafka的数据并不是实时的写入硬盘 ,而是直接利用操作系统的Page来实现文件到物理内存的直接映射。使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销。Kafka重度依赖底层操作系统提供的PageCache功能,当读操作发生时,先从PageCache中查找,如果发生缺页才进行磁盘调度,最终返回需要的数据。实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用。同时如果有其他进程申请内存,回收PageCache的代价又很小,所以现代的OS都支持PageCache。
- 零拷贝,Sendfile技术,直接在内核区完成数据拷贝,Kafka将消息先写入页缓存,如果消费者在读取消息的时候如果在页缓存中可以命中,那么可以直接从页缓存中读取,这样又节省了一次从磁盘到页缓存的copy开销,避免了传统网络IO四步流程。
- 批量发送和数据压缩,系统的瓶颈不是CPU或磁盘,而是网络IO,对于需要在广域网上的数据中心之间发送消息的数据流水线尤其如此。进行数据压缩会消耗少量的CPU资源,不过对于kafka而言,网络IO更应该需要考虑。批量的消息可以通过压缩的形式传输并且在日志中也可以保持压缩格式,直到被消费者解压缩。
Kafka速度的秘诀在于,它把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。
Q:Kafka消息数据积压,Kafka消费能力不足怎么处理
A:如果是 Kafka 消费能力不足,则可以考虑增加 Topic 的分区数,并且同时提升消费 组的消费者数量,消费者数=分区数。(两者缺一不可) 如果是下游的数据处理不及时:提高每批次拉取的数量。批次拉取数据过少(拉取 数据/处理时间<生产速度),使处理的数据小于生产的数据,也会造成数据积压。
不过,Partition的数量并不是越多越好,Partition的数量越多,平均到每一个Broker上的数量也就越多。考虑到Broker宕机(Network Failure, Full GC)的情况下,需要由Controller来为所有宕机的Broker上的所有Partition重新选举Leader,再进一步,如果挂掉的Broker是整个集群的Controller,那么首先要进行的是重新任命一个Broker作为Controller,这样Partition越多,重启的时间越长。
Q:Kafka数据为什么可能出现重复消费
A:重复数据一般是在消费者重启后发生(一般是消费系统宕机、rebalance的时候或者消费者速度很慢,导致一个session周期内未完成消费会出现),消费者不是说消费完一条数据就立马提交 offset的,而是定时定期提交一次 offset。消费者如果再准备提交 offset,但是还没提交 offset的时候,消费者进程重启了,那么此时已经消费过的消息的 offset并没有提交,kafka也就不知道你已经消费了,就会导致重复消费。
解决方案:
- 生产者(ack=all 代表至少成功发送一次) 。
- offset手动提交,业务逻辑成功处理后,提交offset。
- 主键或者唯一索引的方式落库,避免重复数据。
- 代码逻辑判断,如果存在虽然消费但是不写入。
Q:Kafka数据如何保证数据不丢失
A:丢失数据两种情况,
第一:消费端丢失,主要发生在消费者重启的时候,消费者消费到数据后写到一个内存 queue里缓存下,消息自动提交 offset,重启了系统,结果会导致内存 queue 里还没来得及处理的数据丢失。
解决方法:kafka会自动提交 offset,那么只要关闭自动提交 offset,在处理完之后自己手动提交,可以保证数据不会丢。但是此时确实还是会重复消费,比如刚好处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次 ,做好幂等即可。
第二:Kafka丢失,kafka 某个 broker 宕机,然后重新选举 partition 的 leader时,此时其他的 follower 刚好还有一些数据没有同步,结果此时 leader挂了,然后选举某个 follower成 leader之后,就丢掉了之前leader里未同步的数据。
解决方案:
kafka的ack机制,在kafka发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到。
思考:
kafka的容错,也就是在Kafka的副本策略(即Kafka副本的复制协议)。
Kafka 的复制是以分区为粒度的,分区的预写日志被复制到 n 个服务器。在 n 个副本中,一个副本作为 leader,其他副本成为 followers。这允许 Kafka 在集群服务器发生故障时自动切换到这些副本,以便在出现故障时消息仍然可用。所以在producer 往 broker 发送消息,消息先写入到对应 leader 分区上,然后复制到这个分区的所有副本中,这样保障在leader挂掉的时候,能够快速的将followers选举为新的 leader。
但问题是,这个机制能够生效的前提是:这个 follower 必须是跟上 leader 写进度的。
逻辑:
每个分区的 leader 会维护一个 in-sync replica(同步副本列表,又称 ISR)。当 producer 往 broker 发送消息,消息先写入到对应 leader 分区上,然后复制到这个分区的所有副本中。只有将消息成功复制到所有同步副本(ISR)后,这条消息才算被提交。换句话说,是因为这个 follower 是跟上 leader 写进度的。由于消息复制延迟受到最慢同步副本的限制,因此快速检测慢副本并将其从 ISR 中删除非常重要。
一个副本与 leader 失去同步的原因有很多,主要包括:
- 慢副本(Slow replica):follower replica 在一段时间内一直无法赶上 leader 的写进度。造成这种情况的最常见原因之一是 follower replica 上的 I/O瓶颈,导致它持久化日志的时间比它从 leader 消费消息的时间要长;
- 卡住副本(Stuck replica):follower replica 在很长一段时间内停止从 leader 获取消息。这可能是以为 GC 停顿,或者副本出现故障;
- 刚启动副本(Bootstrapping replica):当用户给某个主题增加副本因子时,新的 follower replicas 是不同步的,直到它跟上 leader 的日志。
当副本落后于 leader 分区时,这个副本被认为是不同步或滞后的。在 Kafka 0.8.2 中,副本的滞后于 leader 是根据 replica.lag.max.messages 或 replica.lag.time.max.ms 来衡量的;前者用于检测慢副本(Slow replica),而后者用于检测卡住副本(Stuck replica)。
replica.lag.max.messages 的目标 : 能够检测始终与 leader 不同步的副本。假设现在这个主题的流量由于峰值而增加,生产者最终往 foo 发送了一批包含4条消息,等于 replica.lag.max.messages = 4 的配置值。此时,两个 follower 副本将被视为与 leader 不同步,并被移除 ISR。
replica.lag.max.messages 参数的核心问题是,用户必须猜测如何配置这个值,因为我们不知道 Kafka 的传入流量到底会到多少,特别是在网络峰值的情况下。