一、kafka简介
由Scala和Java编写,Kafka是一种高吞吐量的分布式发布订阅消息系统
Kafka是最初由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的(replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、storm/Spark流式处理引擎,web/nginx日志、访问日志,消息服务等等。
1.1 kafka特性
- 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。
- 可扩展性:kafka集群支持热扩展
- 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
- 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)
- 高并发:支持数千个客户端同时读写
1.2 Kakfa Broker Leader的选举
Kakfa Broker集群受Zookeeper管理。所有的Kafka Broker节点一起去Zookeeper上注册一个临时节点,因为只有一个Kafka Broker会注册成功,其他的都会失败,所以这个成功在Zookeeper上注册临时节点的这个Kafka Broker会成为Kafka Broker Controller
,其他的Kafka broker叫Kafka Broker follower。(这个过程叫Controller在ZooKeeper注册Watch)。这个Controller会监听其他的Kafka Broker的所有信息,如果这个kafka broker controller宕机了,在zookeeper上面的那个临时节点就会消失,此时所有的kafka broker又会一起去 Zookeeper上注册一个临时节点,因为只有一个Kafka Broker会注册成功,其他的都会失败,所以这个成功在Zookeeper上注册临时节点的这个Kafka Broker会成为Kafka Broker Controller,其他的Kafka broker叫Kafka Broker follower 。例如:一旦有一个broker宕机了,这个kafka broker controller会读取该宕机broker上所有的partition在zookeeper上的状态,并选取ISR列表中的一个replica作为partition leader(如果ISR列表中的replica全挂,选一个幸存的replica作为leader; 如果该partition的所有的replica都宕机了,则将新的leader设置为-1,等待恢复,等待ISR中的任一个Replica“活”过来,并且选它作为Leader;或选择第一个“活”过来的Replica(不一定是ISR中的)作为Leader),这个broker宕机的事情,kafka controller也会通知zookeeper,zookeeper就会通知其他的kafka broker。
Kafka判断一个节点是否活着有两个条件:
1、节点必须可以维护和ZooKeeper的连接,Zookeeper通过心跳机制检查每个节点的连接。
2、如果节点是个follower,他必须能及时的同步leader的写操作,延时不能太久。
符合以上条件的节点准确的说应该是“同步中的(in sync)”,而不是模糊的说是“活着的”或是“失败的”。Leader会追踪所有“同步中”的节点,一旦一个down掉了,或是卡住了,或是延时太久,leader就会把它移除。至于延时多久算是“太久”,是由参数replica.lag.max.messages决定的,怎样算是卡住了,怎是由参数replica.lag.time.max.ms
决定的。
1.3 分布式
每个分区在Kafka集群的若干服务中都有副本
,这样这些持有副本的服务可以共同处理数据和请求,副本数量是可以配置的。副本使Kafka具备了容错能力。
每个分区都由一个服务器作为“leader”,零或若干服务器作为“followers”,leader负责处理消息的读和写,followers则去复制leader.如果leader down了,followers中的一台则会自动成为leader。集群中的每个服务都会同时扮演两个角色:作为它所持有的一部分分区的leader,同时作为其他分区的followers,这样集群就会据有较好的负载均衡。
二、基本组成
-
Broker
: Kafka节点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。Kafka集群包含一个或多个服务器,这种服务器被称为broker。 -
Topic
: 每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上,但用户只需指定消息的Topic即可。生产或消费数据而不必关心数据存于何处) -
Partition
: Partition是物理上的概念,每个Topic包含一个或多个Partition. -
Producer
: 负责发布消息到Kafka broker -
Consumer
: 消息消费者,向Kafka broker读取消息的客户端。 -
Consumer Group
: 一个Consumer Group包含多个consumer。每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)。partition中的每个message只能被组(Consumer group ) 中的一个consumer(consumer 线程 )消费,如果一个message可以被多个consumer(consumer 线程 ) 消费的话,那么这些consumer必须在不同的组 -
Leader
:每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的partition。 -
Follower
:Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。如果Leader失效,则从Follower中选举出一个新的Leader。当Follower与Leader挂掉、卡住或者同步太慢,leader会把这个follower从“in sync replicas”(ISR)列表中删除,重新创建一个Follower。
三、 分区
https://www.cnblogs.com/listenfwind/p/12465409.html
分区机制是kafka实现高吞吐的秘密武器,但这个武器用得不好的话也容易出问题,今天主要就来介绍分区的机制以及相关的部分配置。
首先,从数据组织形式来说,kafka有三层形式,kafka有多个主题,每个主题有多个分区,每个分区又有多条消息
。
而 每个分区可以分布到不同的机器上,这样一来,从服务端来说,分区可以实现高伸缩性,以及负载均衡,动态调节的能力
。
当然 多分区就意味着每条消息都难以按照顺序存储
,那么是不是意味着这样的业务场景kafka就无能为力呢?不是的,最简单的做法可以使用单个分区,单个分区,所有消息自然都顺序写入到一个分区中,就跟顺序队列一样了。而复杂些的,还有其他办法,那就是使用按消息键,将需要顺序保存的消息存储的单独的分区,其他消息存储其他分区,这个在下面会介绍。
我们可以通过replication-factor
指定创建topic时候所创建的副本数
。
3.1 partition分区个数选择
既然分区效果这么好,是不是越多分区越好呢?显而易见并非如此。
分区越多,所需要消耗的资源就越多。甚至如果足够大的时候,还会触发到操作系统的一些参数限制
。比如linux中的文件描述符限制,一般在创建线程,创建socket,打开文件的场景下,linux默认的文件描述符参数,只有1024,超过则会报错。
看到这里有读者就会不耐烦了,说这么多有啥用,能不能直接告诉我分区分多少个比较好?很遗憾,暂时没有。
因为每个业务场景都不同,只能结合具体业务来看。假如每秒钟需要从主题写入和读取1GB数据,而消费者1秒钟最多处理50MB的数据,那么这个时候就可以设置20-25个分区,当然还要结合具体的物理资源情况。
而如何无法估算出大概的处理速度和时间,那么就用基准测试来测试吧。创建不同分区的topic,逐步压测测出最终的结果。如果实在是懒得测,那比较无脑的确定分区数的方式就是partition数量 = broker机器数量的2~3倍。
3.2 分区写入策略
所谓分区写入策略,即是生产者将数据写入到kafka主题后,kafka如何将数据分配到不同分区中的策略。
常见的有三种策略,轮询策略,随机策略,和按键保存策略
。其中轮询策略是默认的分区策略,而随机策略则是较老版本的分区策略,不过由于其分配的均衡性不如轮询策略,故而后来改成了轮询策略为默认策略。
轮询策略
所谓轮询策略,即按顺序轮流将每条数据分配到每个分区中。
举个例子,假设主题test有三个分区,分别是分区A,分区B和分区C。那么主题对接收到的第一条消息写入A分区,第二条消息写入B分区,第三条消息写入C分区,第四条消息则又写入A分区,依此类推。
轮询策略是默认的策略,故而也是使用最频繁的策略
,它能最大限度保证所有消息都平均分配到每一个分区。除非有特殊的业务需求,否则使用这种方式即可。
随机策略
随机策略,也就是每次都随机地将消息分配到每个分区。其实大概就是先得出分区的数量,然后每次获取一个随机数,用该随机数确定消息发送到哪个分区。
在比较早的版本,默认的分区策略就是随机策略,但其实使用随机策略也是为了更好得将消息均衡写入每个分区。但后来发现对这一需求而言,轮询策略的表现更优,所以社区后来的默认策略就是轮询策略了。
按键保存策略
按键保存策略,就是当生产者发送数据的时候,可以指定一个key,计算这个key的hashCode值,按照hashCode的值对不同消息进行存储。
至于要如何实现,那也简单,只要让生产者发送的时候指定key就行。欸刚刚不是说默认的是轮询策略吗?其实啊,kafka默认是实现了两个策略,没指定key的时候就是轮询策略,有的话那激素按键保存策略了。
上面有说到一个场景,那就是要顺序发送消息到kafka。前面提到的方案是让所有数据存储到一个分区中,但其实更好的做法,就是使用这种按键保存策略。
让需要顺序存储的数据都指定相同的键,而不需要顺序存储的数据指定不同的键,这样一来,即实现了顺序存储的需求,又能够享受到kafka多分区的优势,岂不美哉。、
3.3 实现自定义分区
kafka提供了两种让我们自己选择分区的方法:
- 在发送producer的时候,在
ProducerRecord
中直接指定,但需要知道具体发送的分区index
,所以并不推荐。 - 实现
Partitioner.class
类,并重写类中的partition
(String topic, Object key, byte[] keyBytes,Object value, byte[] valueBytes, Cluster cluster) 方法。后面在生成kafka producer客户端的时候直接指定新的分区类就可以了。
package kafkaconf;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
public class MyParatitioner implements Partitioner {
@Override
public void configure(Map<String, ?> configs) {
}
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
//key不能空,如果key为空的会通过轮询的方式 选择分区
if(keyBytes == null || (!(key instanceof String))){
throw new RuntimeException("key is null");
}
//获取分区列表
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
//以下是上述各种策略的实现,不能共存
//随机策略
return ThreadLocalRandom.current().nextInt(partitions.size());
//按消息键保存策略
return Math.abs(key.hashCode()) % partitions.size();
//自定义分区策略, 比如key为123的消息,选择放入最后一个分区
if(key.toString().equals("123")){
return partitions.size()-1;
}else{
//否则随机
ThreadLocalRandom.current().nextInt(partitions.size());
}
}
@Override
public void close() {
}
}
然后需要在生成kafka producer客户端的时候指定该类就行:
val properties = new Properties()
......
props.put("partitioner.class", "kafkaconf.MyParatitioner"); //主要这个配置指定分区类
......其他配置
val producer = new KafkaProducer[String, String](properties)
四、副本(leader、follow)
https://blog.csdn.net/qq_39907763/article/details/82697452
https://www.cnblogs.com/listenfwind/p/12465409.html
说完了分区,再来说说副本。先说说副本的基本内容,在kafka中,每个主题可以有多个分区,每个分区又可以有多个副本。这多个副本中,只有一个是leader,而其他的都是follower副本。仅有leader副本可以对外提供服务
。
多个follower副本通常存放在和leader副本不同的broker中。通过这样的机制实现了高可用,当某台机器挂掉后,其他follower副本也能迅速”转正“
,开始对外提供服务。
这里通过问题来整理这部分内容。
leader副本
:响应客户端的读写请求follow副本
:备份leader的数据,不进行读写操作ISR副本集合
:leader副本和所有能够与leader副本保持基本同步的follow副本,如 果follow副本和leader副本数据同步速度过慢,该follow将会被T出ISR副本
4.1 ISR集合中的副本必须满足的条件
-
副本所在的节点与zk相连
-
副本的最后一条消息和leader副本的最后一条消息的差值不能超过阈值replica.lag.time.max.ms。如果该follower在此时间间隔之内没有追上leader,则该follower将会被T出ISR
4.2 副本同步时的两个重要概念
- LEO(Last end offset) 记录了该副本底层日志中的下一条消息的offset,例如LEO为10,那么当前的offset为9
- HW (High water)标记着可消费的消息,对于同一个副本而言HW不会大于LEO,小于等于HW的消息将会被认为是已备份的。
4.3 副本协同机制
producer将消息发送到该partition的leader上,leader会把消息写入其本地log,每个follower都从leader pull数据。在follower收到消息并且将消息写入本地log之后会向leader发送ack,一旦leader收到了ISR中所有replica的ACK,该消息就被认为已经commit了,leader会增加HW并向producer发送ACK
4.4 kafka的副本都有哪些作用?
在kafka中,实现副本的目的就是冗余备份,且仅仅是冗余备份,所有的读写请求都是由leader副本进行处理的。follower副本仅有一个功能,那就是 从leader副本拉取消息,尽量让自己跟leader副本的内容一致
。
4.5 follower副本为什么不对外提供服务?
这个问题本质上是对性能和一致性的取舍。试想一下,如果follower副本也对外提供服务那会怎么样呢?首先,性能是肯定会有所提升的。但同时,会出现一系列问题。类似数据库事务中的幻读,脏读。
比如你现在写入一条数据到kafka主题a,消费者b从主题a消费数据,却发现消费不到,因为消费者b去读取的那个分区副本中,最新消息还没写入。而这个时候,另一个消费者c却可以消费到最新那条数据,因为它消费了leader副本。
看吧,为了提高那么些性能而导致出现数据不一致问题,那显然是不值得的。
4.6 leader副本挂掉后,如何选举新副本?
如果你对zookeeper选举机制有所了解,就知道zookeeper每次leader节点挂掉时,都会通过 内置id,来选举处理了最新事务的那个follower节点。
从结果上来说,kafka分区副本的选举也是类似的,都是选择最新的那个follower副本,但它是通过一个In-sync(ISR)副本集合实现。
kafka会将与leader副本保持同步的副本放到ISR副本集合中。当然,leader副本是一直存在于ISR副本集合中的,在某些特殊情况下,ISR副本中甚至只有leader一个副本
。
当leader挂掉时,kakfa通过zookeeper感知到这一情况,在ISR副本中选取新的副本成为leader,对外提供服务。
但这样还有一个问题,前面提到过,有可能ISR副本集合中,只有leader,当leader副本挂掉后,ISR集合就为空,这时候怎么办呢?这时候如果设置unclean.leader.election.enable参数为true,那么kafka会在非同步,也就是不在ISR副本集合中的副本中,选取出副本
成为leader,但这样意味这消息会丢失,这又是可用性和一致性的一个取舍了。
4.7 ISR副本集合保存的副本的条件是什么?
上面一直说ISR副本集合中的副本就是和leader副本是同步的,那这个同步的标准又是什么呢?
答案其实跟一个参数有关:replica.lag.time.max.ms。
前面说到follower副本的任务,就是从leader副本拉取消息,如果持续拉取速度慢于leader副本写入速度,慢于时间超过replica.lag.time.max.ms后,它就变成“非同步”副本,就会被踢出ISR副本集合中
。但后面如何follower副本的速度慢慢提上来,那就又可能会重新加入ISR副本集合中了。
4.8 producer的acks
acks决定了生产者如何在性能与数据可靠之间做取舍。
配置acks的代码其实很简单,只需要在新建producer的时候多加一个配置:
val properties = new Properties()
......
props.put("acks", "0/1/-1"); //配置acks,有三个可选值
......其他配置
val producer = new KafkaProducer[String, String](properties)
acks这个配置可以指定三个值,分别是0,1和-1。我们分别来说三者代表什么:
acks为0:这意味着producer发送数据后,不会等待broker确认,直接发送下一条数据,性能最
快
acks为1:为1意味着producer发送数据后,需要等待leader副本确认接收后,才会发送下一条数据,性能中等
acks为 -1:即all。意味着发送的消息写入所有的ISR集合中的副本后,才会发送下一条数据。如果说Partition Leader刚接收到了消息,但Follower没有收到消息,此时Leader宕机了,那么客户端会感知到这个消息没发送成功,他会重试再次发送消息过去。性能最慢,但可靠性最强
acks=all 就可以代表数据一定不会丢失了吗?
当然不是,如果你的Partition只有一个副本,也就是一个Leader,任何Follower都没有,你认为acks=all有用吗?
当然没用了,因为ISR里就一个Leader,他接收完消息后宕机,也会导致数据丢失。
所以说,这个acks=all,必须跟ISR列表里至少有2个以上的副本配合使用,起码是有一个Leader和一个Follower才可以。
这样才能保证说写一条数据过去,一定是2个以上的副本都收到了才算是成功,此时任何一个副本宕机,不会导致数据丢失。
五、Consumer
5.1 Consumer 和Partition关系
上图详解:一个consumer group下,无论有多少个consumer,这个consumer group一定回去把这个topic下所有的partition都消费了。
- 当consumer group里面的
consumer数量 < topic下的partition数量
时,如下图groupA,groupB,就会出现一个conusmer thread消费多个partition的情况,总之是这个topic下的partition都会被消费。 - 当 consumer group里面的
consumer数量 = topic下的partition数量时
,如下图groupC,此时效率是最高的,每个partition都有一个consumer thread去消费。 - 当 consumer group里面的
consumer数量 > topic下的partition数量时
,如下图GroupD,就会有一个consumer thread空闲。
因此,我们在设定consumer group的时候,只需要指明里面有几个consumer数量即可,无需指定对应的消费partition序号,consumer会自动进行rebalance。
Kafka不支持一个partition中的message由两个或两个以上的同一个consumer group下的consumer thread来处理,除非再启动一个新的consumer group。所以如果想同时对一个topic做消费的话,启动多个consumer group就可以了。
当启动一个consumer group去消费一个topic的时候,无论topic里面有多个少个partition,无论我们consumer group里面配置了多少个consumer thread,这个consumer group下面的所有consumer thread一定会消费全部的partition;即便这个consumer group下只有一个consumer thread,那么这个consumer thread也会去消费所有的partition。因此,最优的设计就是,consumer group下的consumer thread的数量等于partition数量,这样效率是最高的,类似左上图的Group C。
同一partition的一条message只能被同一个Consumer Group内的一个Consumer消费。不能够一个consumer group的多个consumer同时消费一个partition。
因此,线上的分布式多个service服务,每个service里面的kafka consumer数量都小于对应的topic的partition数量,但是所有服务的consumer数量之和(total consumer)=partition的数量!!!!!!!!!!!!!!!!!!!! |
5.2 Consumer Rebalance的触发条件
- Consumer增加或删除
- Broker的增加或者减少
六、如何保证顺序消费
对于单个分区的消息消费,Kafka会确保按照消息的顺序进行消费
。
1、单个分区消费:创建一个单独的消费者实例来消费一个分区的消息。这样可以确保在单个分区内的消息按顺序消费。但是需要注意,如果有多个分区,不同分区的消息仍可能以并发方式进行消费。
2、指定分区消费:通过指定消费者订阅的特定分区,可以确保只消费指定分区的消息。这样,可以通过将相关消息发送到同一个分区来保证消息的顺序消费。
3、按键分区:Kafka允许根据消息的键(key)来决定将消息发送到哪个分区。如果消息的键是相同的,Kafka会将它们发送到同一个分区。因此,可以根据消息的键来保证消息的顺序消费。
七 、编程指引
7.1 ProducerFactoryBuilder 生产者工厂
@Configuration
public class ProducerFactoryBuilder {
@Autowired
private KafkaConfiguration kafkaConfiguration;
@Autowired
private KafkaProducerConfiguration kafkaProducerConfiguration;
/**
* 生产者配置
*
* @return 配置
*/
@Bean
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>(11);
//kafka server地址
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaConfiguration