Kafka入门到精通

1、kafka0.11.0.0版本之前,Kafka只能支持两种语义:At most once和At least once。Kafka在0.11.0.0版本支持增加了对幂等的支持。幂等是针对生产者角度的特性。幂等可以保证生产者发送的消息,不会丢失,而且不会重复。
要实现 exactly-once 在 Kafka 0.11.0 中有两个官方策略:

  • 幂等
    每个 Producer 在初始化的时候都会被分配一个唯一的 PID,对于每个唯一的 PID,Producer 向指定的Topic 中某个特定的 Partition 发送的消息都会携带一个从 0 单调递增的 Sequence Number。
    启用幂等producer:在producer程序中设置属性enabled.idempotence=true,但不要设置transational_id.注意是不要设置,而不是设置为空字符串。
    幂等性不能跨多个 Topic-Partition,只能保证单个 partition内的幂等性。即单个分区内数据不丟不重。如果要保证多个分区不丢不重,需要以下事务性来实现
  • 事务
    保证操作的原子性,要么全部成功,要么全部失败。
    启用事务支持:在producer程序中设置属性transcational.id为一个指定字符串(你可以认为这是你的事务名称), 同时设置enable.idempotence=true

2、kafka key为空的时候消息发到哪个分区?
kafka定义了一个全局变量,这个变量值是配置参数中的topic.metadata.refresh.interval.ms设置的值,也就是说在这个时间内,key=null的消息都会往缓存起来的这个分区存储,当缓存过时之后,就会重新计算分区号,将计算结果缓存起来。也就是说在key为null的情况下,Kafka并不是每条消息都随机选择一个Partition;而是每隔topic.metadata.refresh.interval.ms才会随机选择一次!
3、kafka底层存储
Partition是以文件的形式存储在文件系统中,比如,创建了一个名为test的topic,其有5个partition,那么在Kafka的数据目录中(由配置文件中的log.dirs指定的)中就会有这样5个目录: test-0,test-1…
partition还可以细分为segment,一个partition物理上由多个segment组成。
segment文件由两部分组成,分别为“.index”文件和“.log”文件,分别表示为segment索引文件和数据文件
4、只要不更改group.id,每次重新消费kafka,都是从上次消费结束的地方继续开始,不论"auto.offset.reset”属性设置的是什么。因为kafka会维护每个group.id消费的偏移量。每次都会从上次消费结束的地方继续开始
5、Kafka保证exactly-once(一般只说producer到broker的exactly-once)
设置Kafka的ack级别为-1,可以保证producer到broker之间不会丢失数据,即at least once。
设置Kafka的ack级别为0,可以保证at most once
Kafka 0.11版本之前,只能通过at least once + 下游消费者全局去重保证exactly-onec。0.11版本后,引入了幂等性:无论producer向broker发送多少重复数据,broker只会持久化一条。所以幂等性利用了at least once,并且在broker端去重,完成exactly-once。
开启幂等性:设置produce的参数enabled.idempotence=true即可
幂等性原理:
开启后,ack就会自动设置为-1。然后每个 Producer 在初始化的时候都会被分配一个唯一的 PID,对于每个唯一的 PID,Producer 向指定的Topic 中某个特定的 Partition 发送的消息都会携带一个从 0 单调递增的 Sequence Number。broker会对这个键做一个缓存,当有相同的键提交时,broker只会存储一条。
但是producer挂掉重启后,pid会发生变化。不同分区partition也不一样。所以幂等只能保证生产者单会话单分区的exactly-once。
为了实现跨分区跨会话的exactly-once,0.11版本后引入事务。一般我们只说produce端的事务,即我们的客户端代码会给一个全局唯一的transactionid,然后将pid和transactionID绑定。这样当produce重启后,transactionID不变,就可以根据transactionID去找挂掉之前的pid,那么当前启动的produce的pid就不会重新生成,还是原来的pid,那么有重复数据到来时,这个键还是一样。假如没有这个transactionid机制,produce挂掉之后再重启,会得到新的PID,这个键就不一样,数据就会重复。
上面只是事务的一个功能,即解决幂等性无法跨生产者会话exactlyonce的问题。除此之外,事务还有很多功能,如

  • 将消息消费和生产封装在一个事务中,形成一个原子操作
  • 写入一批数据到多个分区时,保证所有数据写入到所有分区要么全部成功要么全部失败,没有中间状态,如下面的生产者代码
 Kafka事务代码示例
Properties props = new Properties();
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("client.id", "ProducerTranscationnalExample");
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
//事务ID
props.put("transactional.id", "test-transactional");
KafkaProducer producer = new KafkaProducer(props);
//初始化一个事务
producer.initTransactions();
try {
 String msg = "matt test";
 //开启一个事务
 producer.beginTransaction();
 producer.send(new ProducerRecord(topic, "0", msg.toString()));
 producer.send(new ProducerRecord(topic, "1", msg.toString()));
 producer.send(new ProducerRecord(topic, "2", msg.toString()));
 //提交事务
 producer.commitTransaction();
} catch (ProducerFencedException e1) {
 e1.printStackTrace();
 producer.close();
} catch (KafkaException e2) {
 e2.printStackTrace();
 //中止事务
 producer.abortTransaction();
}
producer.close();

当事务中仅仅存在Consumer消费消息的操作时,它和Consumer手动提交Offset并没有区别。因此单纯的消费消息并不是Kafka引入事务机制的原因,单纯的消费消息也没有必要存在于一个事务中。
7、kakfa有3个broker。创建一个topic,其中有3个partition,2个replication。那么分区和副本分配情况如下:
broker1{partition-0,partition-2} 》broker2 {partition-1,partition-0 }==》broker3{partition-2,partition-1}
创建副本的单位是topic的分区。

8、如何保证kafka全局消息有序?
Kafka只能保证一个分区之内消息的有序性,在不同的分区之间是不可以的,这已经可以满足大部分应用的需求。如果需要topic中所有消息的有序性,那就只能让这个topic只有一个分区,当然也就只有一个consumer组消费它。
9、kafka从0.9版本开始,就有两套消费者API,一个是老的API:需要提供ZK集群信息。一个是新的API:提供的是broker集群信息
10、消费者消费分区的策略
一个消费者组下,每个消费者消费kafka分区的策略:http://blog.csdn.net/wangqyoho/article/details/76169514
每个分区只能由同一个消费组内的一个consumer来消费。每个group中的consumer应该消费哪些分区,这就需要用特定的策略来进行分配。在 Kafka 内部存在两种默认的分区分配策略:Range 和 RoundRobin。两者的目的都是让每个消费者得到的分区数均衡
11、生产者写分区的策略
如果定义了key,那么会按照key进行分区:相同key的记录写到一个分区;
如果没有定义key,则按照轮询round-robin挨个均匀地写分区,即先写分区1一条记录,然后写分区2一条记录,然后写分区3一条记录。。。。
12、Kafka和rabbitmq的区别

  • Kafka
    1.从A系统到B系统的消息没有复杂的传递规则,并且具有较高的吞吐量要求。
    2.需要访问消息的历史记录的场景,因为kafak是持久化消息的,所以可以通过偏移量访问到那些已经被消费的消息(前提是磁盘空间足够,kafka没有将日志文件删除)
    3.流处理的场景。处理源源不断的流式消息
  • rabbit
    1.需要对消息进行更加细粒度的控制,包括一些可靠性方面的特性,比如死信队列。
    2.需要多种消费模式(点对点,广播,订阅发布等)
    3.消息需要通过复杂的路由到消费者。
    4.我们可以通过限制消费者的并发数等于 1 来保证 RabbitMQ 中的消息有序性。

RabbitMQ 内置重试逻辑和死信(dead-letter)交换器,但是 Kafka 只是把这些实现逻辑交给用户来处理。所以rabbitmq在可靠性方面做的更多更好,同时效率也会更低。
当消费者成功消费消息之后,RabbitMQ 就会把对应的消息从存储中删除。这种行为没法修改。相反,Kafka 会给每个主题配置超时时间,只要没有达到超时时间的消息都会保留下来。

在消息留存方面,Kafka 仅仅把它当做消息日志来看待,并不关心消费者的消费状态。消费者可以不限次数的消费每条消息,并且他们可以操作分区偏移来“及时”往返的处理这些消息。kafka 会周期的检查分区中消息的留存时间,一旦消息超过设定保留的时长,就会被删除。
业务一般用rmq,大数据用Kafka。
kafka和rabbitmq最大的区别就是在吞吐量上,kafka吞吐量几十万每秒,rabbitmq 几万每秒。kafka要做到全局有序,只能设置成单分区。rabbitmaq要做到全局有序,只能做成单线程生产,单线程消费,顺序性条件比较苛刻

13、Memory Mapped Files(MMAP)
kafka数据不是实时写入磁盘,而是利用操作系统分页存储,利用内存提高IO效率。通过mmap,进程像读写磁盘一样读写内存(虚拟机内存),也不必关心内存的大小(因为有虚拟内存兜底)。使用这种方式,省去了用户空间到内核空间复制的开销。
但这种方式有一个致命缺陷——不可靠,写入mmap的数据并没有真正写入磁盘。所以kafka提供了参数来控制producer是否主动flush到磁盘,写入mmap后立即flush,就是同步,写入mmap后立即返回producer而不flush,就是异步。
zero-copy
传统读取方式为,先复制到内核空间(pagecache),再复制到用户空间,从用户空间重新复制到内核空间(套接字缓冲区),然后发给网卡。
zero-copy直接从内核空间(DMA)到内核空间(socket),然后发给网卡。
14、kafka多线程消费

public class ConsumerRunnable implements Runnable {

    // 每个线程维护私有的KafkaConsumer实例
    private final KafkaConsumer<String, String> consumer;

    public ConsumerRunnable(String brokerList, String groupId, String topic) {
        Properties props = new Properties();
        props.put("bootstrap.servers", brokerList);
        props.put("group.id", groupId);
        this.consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList(topic));   // 本例使用分区副本自动分配策略
    }

    @Override
    public void run() {
        while (true) {
            ConsumerRecords<String, String> records = consumer.poll(200);   
            for (ConsumerRecord<String, String> record : records) {
                // 这里面写处理消息的逻辑
            }
        }
    }
}


public class ConsumerGroup {

    private List<ConsumerRunnable> consumers;
    public ConsumerGroup(int consumerNum, String groupId, String topic, String brokerList) {
        consumers = new ArrayList<>(consumerNum);
		//多个线程执行多个consumer,每个consumer负责一个或者多个分区
		//如果这里consumerNum=1,那么只有一个线程,一个consumer,那么这个线程先去把分区1的数据消费完,再去消费分区2,再去分区3....
        for (int i = 0; i < consumerNum; ++i) {
            ConsumerRunnable consumerThread = new ConsumerRunnable(brokerList, groupId, topic);
            consumers.add(consumerThread);
        }
    }

    public void execute() {
        for (ConsumerRunnable task : consumers) {
            new Thread(task).start();
        }
    }
}
	

15、kafka单分区有序是指按照写入的顺序有序而不是是按照大小排序。比如依次往一个分区写10,5,11那存储的时候是存储10,5,11而不是是5,10,11
16、Partition在服务器上的表现形式就是一个一个的文件夹,每个partition的文件夹下面会有多组segment文件,每组segment文件又包含.index文件、.log文件、.timeindex文件(早期版本中没有)三个文件, log文件就实际是存储message的地方,而index和timeindex文件为索引文件,用于检索消息。kafka就是利用分段+索引的方式来解决查找效率的问题。
18、Kafka底层存储
数据文件的分段
Kafka解决查询效率的手段之一是将数据文件分段,比如有100条Message,它们的offset是从0到99。假设将数据文件分成5段,第一段为0-19,第二段为20-39,以此类推,每段放在一个单独的数据文件里面,数据文件以该段中最小的offset命名。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message在哪个段中。
为数据文件建索引
数据文件分段使得可以在一个较小的数据文件中查找对应offset的Message了,但是这依然需要顺序扫描才能找到对应offset的Message。为了进一步提高查找的效率,Kafka为每个分段后的数据文件建立了索引文件,文件名与数据文件的名字是一样的,只是文件扩展名为.index。
索引文件中包含若干个索引条目,每个条目表示数据文件中一条Message的索引。索引包含两个部分(均为4个字节的数字),分别为相对offset和position。
相对offset:因为数据文件分段以后,每个数据文件的起始offset不为0,相对offset表示这条Message相对于其所属数据文件中最小的offset的大小。举例,分段后的一个数据文件的offset是从20开始,那么offset为25的Message在index文件中的相对offset就是25-20 = 5。存储相对offset可以减小索引文件占用的空间。
position,表示该条Message在数据文件中的绝对位置。只要打开文件并移动文件指针到这个position就可以读取对应的Message了。
index文件中并没有为数据文件中的每条Message建立索引,而是采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。这样避免了索引文件占用过多的空间,从而可以将索引文件保留在内存中。但缺点是没有建立索引的Message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小了。

20、kafka为什么快:
写入快的原因: 顺序写入 和 MMFile

  • 顺序写入:磁盘读写的快慢取决于你怎么使用它,也就是顺序读写或者随机读写。在顺序读写的情况下,某些优化场景磁盘的读写速度可以和内存持平。kafka写入数据的时候它是末尾添加所以速度最优。
  • Memory Mapped Files(后面简称mmap)内存映射文件。也有一个很明显的缺陷:写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。 Kafka提供了一个参数——producer.type来控制是不是主动flush,如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步 (sync);写入mmap之后立即返回Producer不调用flush叫 异步 (async)。

读取快的原因:1、基于sendfile实现Zero copy 2、分区分段,以及分段文件建索引

  • 传统模式下我们从硬盘读取一个文件是这样的:先复制到内核空间(read是系统调用,放到了DMA,所以用内核空间),然后复制到用户空间(1,2);从用户空间重新复制到内核空间(你用的socket是系统调用,所以它也有自己的内核空间),最后发送给网卡(3、4)。Zero Copy中直接从内核空间(DMA的)到内核空间(Socket的),然后发送网卡。

22、kafka 0.10之后,虽然offset不存储在zk上了,但是kafka的安装,启动还是依赖zk。使用/kafka-topics.sh创建topic,查看topic列表,还是要指定zk。消费topic数据不需要指定zk
23、ack设置为1,写到页缓存就会通知客户端写入成功,而不是写到磁盘才通知
24、生产者数据丢失

  • Producer 生产数据默认是先写到内存(PageCache)中的,定期 flush 到磁盘上。设置较小定期 flush的时间,可以降低发生数据的可能性,但不能真正解决数据丢失
  • 将 request.required.acks =all表示生产者发送消息要等到leader以及所有的副本都同步了,才会返回确认消息。但是同时也影响了性能。
    acks=all不一定能百分百防止数据丢失,如果你的Partition只有一个副本,也就是一个Leader,任何Follower都没有,你认为acks=all有用吗?
    当然没用了,因为ISR里就一个Leader,他接收完消息后宕机,也会导致数据丢失。所以说,这个acks=all,必须跟ISR列表里至少有2个以上的副本配合使用,起码是有一个Leader和一个Follower才可以。
    如果要提高数据的可靠性,在设置request.required.acks=-1的同时,也要min.insync.replicas这个参数(可以在broker或者topic层面进行设置)的配合,这样才能发挥最大的功效。
    min.insync.replicas这个参数设定ISR中的最小副本数是多少,默认值为1,当且仅当request.required.acks参数设置为-1时,此参数才生效
    当ack=-1时,只要有一台follower没有与leader同步,生产者就会重新发送消息,这就照成了消息的重复
    解决办法:
    方法1、开启精确一次性,也就是幂等性,
    enable.idempotence=true
    开启后,kafka首先会让producer自动将 acks=-1,再将producer端的retry次数设置为Long.MaxValue,再在集群上对每条消息进行标记去重!
    方法2、事务的producer
    消费者数据丢失
    消费端数据丢失的原因是 offset 的自动提交。解决方法:关闭自动提交,改成手动提交,每次数据处理完后,再提交。

25、Kafka 的核心源码分为两部分:客户端源码和服务端源码,客户端又分为生产者和消费者。

kafka生产者流程
1:一条消息过来首先会被封装成为一个ProducerRecord对象。
2:接下来要对这个对象进行序列化,因为Kafka的消息需要从客户端传到服务端,涉及到网络传输,所以需要实现序列。Kafka 提供了默认的序列化机制,也支持自定义序列化。
3:消息序列化完了以后,对消息要进行分区,分区的时候需要获取集群的元数据。
4:分好区的消息不是直接被发送到服务端,而是放入了生产者的一个缓存里面。在这个缓存里面,多条消息会被封装成为一个批次(batch),默认一个批次的大小是 16K。
5:Sender 线程启动以后会从缓存里面去获取可以发送的批次。
6:Sender 线程把一个一个批次发送到服务端

生产者高级设计之内存池设计
刚刚我们看到 batches 里面存储的是批次,批次默认的大小是 16K,整个缓存的大小是 32M,生产者每封装一个批次都需要去申请内存,正常情况下如果一个批次发送出去了以后,那么这 16K 的内存就等着 GC 来回收了。但是如果是这样的话,就可能会频繁的引发 FullGC,故而影响生产者的性能,所以在缓存里面设计了一个内存池(类似于我们平时用的数据库的连接池),一个 16K 的内存用完了以后,把数据清空,放入到内存池里,下个批次用的时候直接从里面获取就可以。这样大大的减少了 GC 的频率,保证了生产者的稳定和高效。
首先这个acks参数,是在KafkaProducer,也就是生产者客户端里设置的。然后这个参数实际上有三种常见的值可以设置,分别是:0、1 和 all。
第一种选择是把acks参数设置为0,意思就是我的KafkaProducer在客户端,只要把消息发送出去,不管那条数据有没有在哪怕Partition Leader上落到磁盘,我就不管他了,直接就认为这个消息发送成功了。
第二种选择是设置 acks = 1,意思就是说只要Partition Leader接收到消息而且写入本地磁盘了,就认为成功了,不管他其他的Follower有没有同步过去这条消息了。这种设置其实是kafka默认的设置

26、kafka多消费者
消费者组ConsumerGroup

  • 1、一个分区只能被消费者组中的其中一个消费者去消费,组员之间不能重复消费
  • 2、Consumer Group 下可以有一个或多个 Consumer实例。这里的实例可以是一个单独的进程,也可以是同一进程下的线程。在实际场景中,使用进程更为常见一些。
    一个消费者组(groupid)可以有多个consumer,每个分区只能被一个consumer消费。如果一个消费者组只有一个consumer,那么topic所有分区都会被这个consumer消费。
    不同消费者组下的consumer互不影响,也就是假如有两个消费者组,每个组有只有一个消费者,那么每个组的消费者能消费到所有分区数据。
    一个groupid下创建多个consumer的方式(核心是"group.id"相同):
    1)只启动一个消费应用,即只有一个进程。但是在这个进程下启动多线程去启动多个消费者
    2)启动多个相同的消费应用,即多进程。每个进程一个线程去启动一个消费者
  • 3、在0.9以前的client api中,consumer是要依赖zk的。因为同一个consumer group中的所有consumer需要靠zk进行协同,进行下面所讲的rebalance。但是因为zk的“split brain”,导致一个group里面,不同的consumer拥有了同一个partition,进而会引起消息的消费错乱。
    为此,在0.9中,消费时不再用zk,而是Kafka集群内部的broker组件Coordinator协调consumer之间的同步。
    某个Cosnumer Group的Coordinator负责在该Consumer Group的成员变化或者所订阅的Topic的Partititon变化时协调Rebalance操作。
    Rebalance 本质上是一种协议,主要作用是为了保证消费者组(Consumer Group)下的所有消费者(Consumer)消费的主体分区达成均衡。
    比如:我们有10个分区,当我们有一个消费者时,该消费者消费10个分区,当我们增加一个消费者,理论上每个消费者消费5个分区,这个分配的过程我们成为Rebalance(重平衡)。
    触发条件:消费者组的消费者数量有变化;topic的分区有变化
    缺点:Rebalance时所有消费者无法消费数据;Rebalance速度慢

27、Coordinator(协调者)介绍
Consumer端应用程序在提交offset时,其实是向Coordinator所在的Broker提交。同样地,当Consumer应用启动时,也是向Coordinator所在的Broker发送各种请求,然后由Coordinator负责执行消费者组的注册、成员管理记录等元数据管理操作。
每个Broker都有各自的Coordinator组件。Group Coordinator 是一个服务,每个Broker在启动的时候都会启动一个该服务。每个consumer group都会被分配一个这样的coordinator用于组管理和位移管理。
当新版本consumer group的第一个consumer启动的时候,它会去和kafka server确定谁是它们组的coordinator。之后该group内的所有成员都会和该coordinator进行协调通信。
Group Coordinator 的作用是用来存储 Group 的相关 Meta 信息,并将对应 Partition 的 Offset 信息记录到 Kafka 内置Topic(__consumer_offsets) 中。
Kafka在0.9之前是基于zk来存储 Partition的Offset信息,因为 Zookeeper 并不适用于频繁的写操作,所以在0.9之后通过内置Topic的方式来记录对应Partition的Offset。
如何进行分区和消费者的Rebalance?
Kafka提供的再平衡策略主要有三种:Range,Round Robin和Sticky,默认使用的是Range。
Range范围分区
Range 范围分区策略是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。假如现在有 10 个分区,3 个消费者,排序后的分区将会是0,1,2,3,4,5,6,7,8,9;消费者排序完之后将会是C1-0,C2-0,C3-0。通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。
例如,10/3 = 3 余 1 ,除不尽,那么 消费者 C1-0 便会多消费 1 个分区,最终分区分配结果如下:
C1-0 消费 0,1,2,3 分区
C2-0 消费 4,5,6 分区
C3-0 消费 7,8,9 分区(如果有11 个分区的话,C1-0 将消费0,1,2,3 分区,C2-0 将消费4,5,6,7分区 C3-0 将消费 8,9,10 分区)
Range 范围分区的弊端:
如上,只是针对 1 个 topic 而言,C1-0消费者多消费1个分区影响不是很大。如果有 N 多个 topic,那么针对每个 topic,消费者 C1-0 都将多消费 1 个分区,topic越多,C1-0 消费的分区会比其他消费者明显多消费 N 个分区。这就是 Range 范围分区的一个很明显的弊端了
由于 Range 范围分区存在的弊端,于是有了RoundRobin 轮询分区策略
RoundRobinAssignor 轮询分区
RoundRobin 轮询分区策略,是把所有的 partition 和所有的 consumer 都列出来,然后按照 hascode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者。
使用RoundRobin策略时,如果要达到分配均匀。必须满足条件:每个消费者订阅的主题必须相同(比如都订阅了topc1和topic2,而不是一个订了1一个订了2)。
如果不满足这个条件,RoundRobin也无法满足分配均匀
28、数据一致性
kafka的副本分为leader副本和follower副本;每个副本其实就是一个分区,其实也就是leader分区和follower分区。 kafka是leader副本负责读写,follower副本只负责备份。
而 zookeeper是 leader机器(进程)节点读写,follower机器(进程)节点负责读。

你可能感兴趣的:(笔记)