文章目录
1、Exactly Once
1)、引入幂等性
如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复
0.11版本的Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指Producer不论向Server发送多少次重复数据,Server端都只会持久化一条。幂等性结合At Least Once语义,就构成了Kafka的Exactly Once语义。即:
At Least Once + 幂等性 = Exactly Once
2)、如何启用?
要启用幂等性,只需要将Producer的参数中enable.idompotence设置为true即可。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的Producer在初始化的时候会被分配一个PID,发往同一Partition的消息会附带Sequence Number。而Broker端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。
3)、缺点
PID重启就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区跨会话的Exactly Once。
2、消费方式
consumer采用pull(拉)模式从broker中读取数据
1)、为什么不用push方式?
push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。
2)、pull方式的不足之处以及解决方法
如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。针对这一点,Kafka的消费者在消费数据时会传入一个时长参数timeout,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长即为timeout。
3、kafka消费者分区分配策略
1)、当consumer的数量比partition的数量小的时候怎么分配?
Kafka有两种分配策略,一是roundrobin,一是range。
2)、roundrobin策略
可以理解为发牌时候的按序分发(你一张我一张)
3)、range策略
也相当于发牌,但是提前看几张牌几个人分配好每个人对应的牌的数量(三个人,7张牌,第一个人三张后面两人一人两张,直接把三张给第一人,后面每人两张)
4、offset的维护
1)、引入
由于consumer在消费过程中可能会出现断电宕机等故障,consumer恢复后,需要从故障前的位置的继续消费,所以consumer需要实时记录自己消费到了哪个offset,以便故障恢复后继续消费。
2)、解决
Kafka 0.9版本之前,consumer默认将offset保存在Zookeeper中,从0.9版本开始,consumer默认将offset保存在Kafka一个内置的topic中,该topic为__consumer_offsets。
补充:zookeeper毕竟是外部框架,而且存储在zookeeper不好优化,放在Kafka内方便优化(自己动手丰衣足食)
5、Kafka高效读写数据
1)、顺序写磁盘
Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到到600M/s,而随机写只有100k/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
2)、页面缓存(Pagecache)
①、定义
页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘I/O的操作。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。
②、pagecache具体操作
当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据所在的页(page)是否在页缓存(page cache)中,如果存在(命中)则直接返回数据,从而避免了对物理磁盘I/O操作;如果没有命中,则操作系统会向磁盘发起读取请示并将读取的数据页写入页缓存,之后再将数据返回进程。同样,如果一个进程需要将数据写入磁盘,那么操作系统也会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入对应的页。被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以操作数据的一致性。
③、联系kafka
Kafka数据持久化是直接持久化到Pagecache中
④、好处
1、 I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能
2、I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间
3、 充分利用所有空闲内存(非 JVM 内存)。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担
4、读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据
5、如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用
尽管持久化到Pagecache上可能会造成宕机丢失数据的情况,但这可以被Kafka的Replication机制解决。如果为了保证这种情况下数据不丢失而强制将 Page Cache 中的数据 Flush 到磁盘,反而会降低性能。
3)、零复制技术
①、传统IO流程
1、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2、第二次:将内核缓冲区的数据,copy到application应用程序的buffer;
3、第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。
②、kafka的做法
数据直接在内核完成输入和输出,不需要拷贝到用户空间再写出去。
kafka数据写入磁盘前,数据先写到进程的内存空间。
③、对比
思考传统IO方式,会注意到实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。
显然,第二次和第三次数据copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的意义。
④、零拷贝局限
零拷贝运用于读取磁盘文件后,不需要做其他处理,直接用网络发送出去。试想,如果读取磁盘的数据需要用程序进一步处理的话,必须要经过第二次和第三次数据copy,让应用程序在内存缓冲区处理
6、Zookeeper在Kafka中的作用
1)、Controller解释
Kafka集群中有一个broker会被选举为Controller,负责管理集群broker的上下线,所有topic的分区副本分配和leader选举等工作。
2)、partition的leader选举过程
①、brokers在启动的时候会在zookeeper里面注册一个临时节点(用来告知zookeeper),其中有一个broker会被选为Controller
②、Controller会监听图中ids目录下的子节点,每当我们新建立一个话题topic,Controller就负责分区、副本的存放。
③、对于每个话题的每个分区会在zookeeper的/brokers/topics/话题名/parttions/0/state目录下记录leader是谁,以及isr是谁,这件事由Controller来完成。
④、假设broker0挂了,ids里面0就会消失(临时节点),Controller就会监听此变化,并且选举出新的leader(如图中broker1),再更新state目录下的内容
7、Kafka事务
1)、Producer事务
事务的唯一标识(手起)Transaction ID,当启用事务以后,kafka就会开启一个内部话题Transaction state,这个话题负责存储Transaction ID、Producer ID(PID)、消息的状态连同信息(比如消息有100条我才写到50条这就是状态),以上的存储操作就是由Transaction Coordinator负责
Producer就是通过和Transaction Coordinator交互获得Transaction ID对应的任务状态。由于Transaction state存储了事务的状态,即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。
2)、Consumer事务
①、server对consumer几乎没有控制能力,consumer想回去拉就可以回去拉数据
②、回滚可能会失败,回滚的数据已经被kafka清理
8、Producer API
1)、消息发送流程
Kafka的Producer发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main线程和Sender线程,以及一个线程共享变量——RecordAccumulator。main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker。
batch.size:只有数据积累到batch.size之后,sender才会发送数据。
linger.ms:如果数据迟迟未达到batch.size,sender等待linger.time之后就会发送数据。
2)、异步发送API
①、导入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>1.0.0</version>
</dependency>
②、Producer.java
public class Producer {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1、实例化kafka集群
Properties properties = new Properties();
properties.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("acks", "all");
properties.setProperty("bootstrap.servers", "test:9092");
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//2、用集群对象发送是数据
for (int i = 0; i < 10; i++) {
Future<RecordMetadata> future = producer.send(new ProducerRecord<String, String>(
"first",
Integer.toString(i),
"Value" + i
),
//回调函数
new Callback() {
/**
* 当我们的send收到服务器的ack以后,会调用onCompletion方法
* @param metadata 消息发送到那个分区,传递的元数据的返回
* @param exception 发送失败返回exception
*/
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception == null) {
System.out.println(metadata);
}
}
});
RecordMetadata recordMetadata = future.get();//同步,不加就是异步
System.out.println("发完了" + i + "条");
}
//3、关闭资源
producer.close();
}
}
9、Consumer API
1)、自动提交Offset的Consumer
①、consumer1.properties
key.deserializer=org.apache.kafka.common.serialization.StringDeserializer
value.deserializer=org.apache.kafka.common.serialization.StringDeserializer
bootstrap.servers=test:9092
enable.auto.commit=true
group.id=test
auto.offset.reset=earliest
②、Consumer.java
public class Consumer {
public static void main(String[] args) throws IOException, InterruptedException {
//1、实例化consumer对象
Properties properties = new Properties();
properties.load(Consumer.class.getClassLoader().getResourceAsStream("consumer1.properties"));
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
//2、用这个对象接收消息
//Collections.singleton单例集合
consumer.subscribe(Collections.singleton("first"));
while(true){
//从订阅的话题中拉取数据,后接拉取的时间(这边设置超过2s失败)
ConsumerRecords<String, String> poll = consumer.poll(2000);
if(poll.count()==0){
Thread.sleep(100);
}
//消费拉取的数据
for (ConsumerRecord<String, String> record : poll) {
System.out.println(record);
}
}
//3、关闭consumer
//consumer.close();
}
}
2)、手动提交Offset的Consumer
//consumer.commitSync();//同步提交
consumer.commitAsync();//异步提交
确保消费和提交原子性,要么同时成功,要么同时失败
但是无论是异步还是同步还是有可能造成数据重读,因此要自定义保存offset