介绍:
CAP理论中的CP模型
特点:
高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, 由多个consumer group 对partition进行consume操作。
可扩展性:kafka集群支持热扩展
持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)
高并发:支持数千个客户端同时读写
作用:
异步处理数据
系统应用解耦
业务流量削峰
缺点:
由于是批量发送,数据并非真正的实时;
对于mqtt协议不支持;
不支持物联网传感数据直接接入;
仅支持统一分区内消息有序,无法实现全局消息有序;
监控不完善,需要安装插件;
依赖zookeeper进行元数据管理;
安装:
wget http://apache.01link.hk/kafka/2.1.0/kafka_2.11-2.1.0.tgz
tar -xzf kafka_2.11-2.1.0.tgz
修改配置
vim config/server.properties
修改zookeeper地址
修改log.dirs
启动kafka
bin/kafka-server-start.sh -daemon config/server.properties
关闭:
bin/kafka-server-stop.sh
常用命令:
创建topic:
bin/kafka-topics.sh
bin/kafka-topics.sh
在集群的任一节点创建topic,会自动在整个server创建,只要指定replication-factor=对应的节点数即可
删除topic:
kafka-topics.sh --zookeeper localhost:2181/myKafka --delete --topic topic_1
修改topic:
kafka-topics.sh
kafka-topics.sh
查看topic生产情况:
bin/kafka-topics.sh --list --zookeeper localhost:2181
bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic test
“leader”是负责给定分区所有读写操作的节点。每个节点都是随机选择的部分分区的领导者。
“replicas”是复制分区日志的节点列表,不管这些节点是leader还是仅仅活着。故障时不移除
“isr”是一组“同步”replicas,是replicas列表的子集,它活着并被指到leader。故障时移除
查看topic offset情况
bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list 172.31.9.195:9092,172.31.9.196:9092 --topic installmentdb_t_cash_loan
生产消息测试:
bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
消费消息测试:
bin/kafka-console-consumer.sh --bootstrap-server 172.31.9.195:9092 --topic test --from-beginning
bin/kafka-console-consumer.sh --bootstrap-server 172.31.9.195:9092 --topic repayment --offset 0
bin/kafka-console-consumer.sh --bootstrap-server 172.31.9.195:9092,172.31.9.196:9092 --topic akuloandb_t_cash_loan --offset 400000 --partition 0|grep 13398793
bin/kafka-console-consumer.sh --bootstrap-server 172.31.25.35:9092,172.31.25.36:9092 --topic cashLoanRepayment --offset 400000 --partition 0|grep 13398793
查看消费详情:
bin/kafka-consumer-groups.sh
设置offset:
bin/kafka-consumer-groups.sh --bootstrap-server 172.31.9.195:9092 --group feature.model_event --reset-offsets --topic foo:0 --to-offset 1 --execute
删除消费组:
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --delete --group my-group
创建集群:
在启动zookeeper和一个kafka单节点的基础上,只需再启动新节点
bin/kafka-server-start.sh config/server-1.properties
bin/kafka-server-start.sh config/server-2.properties
创建topic指定副本replication-factor为3
bin/kafka-topics.sh
单点故障
leader选举更换,Isr移除,但record仍可从头消费
创建Kafka Connect:
概述:
一旦Kafka Connect进程启动,源连接器就开始从 test.txt 读取行并且 将它们生产到主题 connect-test 中,
同时接收器连接器也开始从主题 connect-test 中读取消息, 并将它们写入文件 test.sink.txt 中。
# 连接器一直在处理数据,新添的数据会实时生产到topic中,然后接收器实时消费
命令:
bin/connect-standalone.sh config/connect-standalone.properties config/connect-file-source.properties config/connect-file-sink.properties
connect-standalone.properties:Kafka Connect的配置文件,包含常用的配置,如Kafka brokers连接方式和数据的序列化格式。
connect-file-source.properties:源连接器,用于从输入文件读取行,并将其输入到 Kafka topic。
connect-file-sink.properties:接收器连接器,它从Kafka topic中读取消息,并在输出文件中生成一行。
客户端API:
客户端api:
生产者:
Map<String, Object> configs = new HashMap<>();
KafkaProducer<Integer, String> producer = new KafkaProducer<Integer, String>(configs);
producer.send(record, new Callback() {})
消费者:
KafkaConsumer<Integer, String> consumer = new KafkaConsumer<Integer, String>(configs);
List<String> topics = Arrays.asList("topic_1");
consumer.subscribe(topics, new ConsumerRebalanceListener(){})
ConsumerRecords<Integer, String> records = consumer.poll(3_000);
Iterable<ConsumerRecord<Integer, String>> topic1Iterable = records.records("topic_1");
SprintBoot:
生产者:
@Autowired
private KafkaTemplate template;
ListenableFuture future = template.send(new ProducerRecord<Integer, String>("topic-spring-02",0,1,message));
future对象同步调用get,异步调用addCallback
消费者:
@KafkaListener(topics = "topic-spring-02")
public void onMessage(ConsumerRecord<Integer, String> record) {
消费记录
}
nginx模块作为producer:
1.文档https://github.com/brg-liuwei/ngx_kafka_module
2.下载nginx源码,参照文档编译好librdkafka,git下载ngx_kafka_module
3.nginx编译模块
./configure
make && make install
注意:
遇到error提示,去掉CFLAGS中的-Werror
4.修改nginx配置
在location里添加kafka_topic your_topic;即可
注意:
报找不到 librdkafka.so.1的文件,执行echo "/usr/local/lib" >> /etc/ld.so.conf和ldconfig即可。
5.启动nginx,发起请求curl localhost/your/path/topic -d '{"xxx":yyy}'。响应一般为204
基本概念:
Broker:
概述:
Kafka集群包含一个或多个服务器,这种服务器被称为broker。
Topic:
概述:
每条发布到Kafka集群的消息都有一个类别,这个类别被称为topic。
一个Topic可以认为是一类消息,每个topic将被分成多个partition(区)
存储形式:
每个partition在存储层面是append log文件。
任何发布到此partition的消息都会被直接追加到log文件的尾部,每条消息在文件中的位置称为offset(偏移量),offset为一个long型数字,
它是唯一标记一条消息。它唯一的标记一条消息。kafka并没有提供其他额外的索引机制来存储offset,因为在kafka中几乎不允许对消息进行“随机读写”。
副本:
通过replicationfactor配置副本数。
如果Topic的"replicationfactor"为N,那么允许N-1个kafka实例失效。
命名规则:
由大小写字母、数字、.、-、_(不推荐)组成
不能为空、不能为.、不能为..
长度不能超过249
警告:
创建topic的时候,如果名称中包含.或者_,kafka会抛出警告。原因是:
在Kafka的内部做埋点时会根据topic的名称来命名metrics的名称,并且会将句点号.改成下划线_。
假设遇到一个topic的名称为topic.1_2,还有一个topic的名称为topic_1.2,那么最后的metrics的名称都为topic_1_2,所以就会发生名称冲突。
内部命名规则:
topic的命名不推荐(虽然可以这样做)使用双下划线__开头,因为以双下划线开头的topic一般看作是kafka的内部topic,比如__consumer_offsets和__transaction_state。
Partition分区:
概述:
partition是物理上的概念,每个topic包含一个或多个partition,创建topic时可指定parition数量。
每个分区都是一个队列,每个分区里的records都有着自己的offset。
存储形式:
每个partition对应于一个文件夹,该文件夹下存储该partition的数据和索引文件。
设计最根本原因:
kafka基于文件存储.通过分区,可以将日志内容分散到多个server上,来避免文件尺寸达到单机磁盘的上限,
每个partiton都会被当前server(kafka实例)保存;可以将一个topic切分多任意多个partitions,来消息保存/消费的效率.
此外越多的partitions意味着可以容纳更多的consumer,有效提升并发消费的能力.
增加分区:
1.添加新broker:为新的broker分配一个唯一id,然后在新的服务器上启动Kafka即可。
做到这一步完成新broker的添加,但它只有在创建新的topic时才会参与工作。除非将已有的partition迁移到新的服务器上面。
2.迁移数据到新broker:迁移数据只能手动执行,但整个过程是自动完成的。
Kafka先将新的Server设置为要迁移的partition的follower,让它进行replica。
等它将目标partition的数据完全复制,且这个新的server已经进入in sync的列表后,已有的一个replica就会删除自己的partition数据。
Kafka提供kafka-reassign-partitions.sh工具做添加新broker后的数据迁移。
Producer生产者:
概述:
负责发布消息到Kafka broker。
数据生产流程:
1. Producer创建时,会创建一个Sender线程并设置为守护线程。
2. 生产消息时,内部其实是异步流程;生产的消息先经过拦截器->序列化器->分区器,然后将消息缓存在缓冲区(该缓冲区也是在Producer创建时创建)。
3. 批次发送的条件为:缓冲区数据大小达到batch.size或者linger.ms达到上限,哪个先达到就算哪个。
4. 批次发送后,发往指定分区,然后落盘到broker;如果生产者配置了retrires参数大于0并且失败原因允许重试,那么客户端内部会对该消息进行重试。
5. 落盘到broker成功,返回生产元数据给生产者。
6. 元数据返回有两种方式:一种是通过阻塞直接返回,另一种是通过回调返回。
分区器;
默认方式:
1. 如果record提供了分区号,则使用record提供的分区号
2. 如果record没有提供分区号,则使用key的序列化后的值的hash值对分区数量取模
3. 如果record没有提供分区号,也没有提供key,则使用轮询的方式分配分区号。
1). 会首先在可用的分区中分配分区号
2). 如果没有可用的分区,则在该主题所有分区中分配分区号。
自定义分区器:
实现接口Partitioner
分区分配策略:
Range(默认)和RoundRobin
Range:
按照分区号和消费者号进行排序,如C1-0消费0,1,2,3
拦截器:
概述:
在消息发送前以及Producer回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。
方法:
onSend(ProducerRecord):在消息被序列化以计算分区前调用该方法。多个拦截器时顺序执行该方法
onAcknowledgement(RecordMetadata, Exception):在消息被应答之前或消息发送失败时调用,并且通常都是在Producer回调逻辑触发之前
close:关闭Interceptor,主要用于执行一些资源清理工作。多个拦截器时顺序执行该方法
自定义拦截器:
实现ProducerInterceptor接口
ACK机制:
幂等性:
enable.idempotence=true ,以及ack=all 以及retries > 1
各种情况:
ack=0 不重试。可能会丢消息,适用于吞吐量指标重要性高于数据丢失,例如:日志收集。
ack=1 leader crash生产者发送消息完,只等待Leader写入成功就返回了,Leader分区丢失了,此时Follower没来及同步,消息丢失。
ack=all/-1 ISR列表的副本的数量可能影响吞吐量,不超过5个,一般三个。
如何保证不丢消息?
ack>1
min.insync.replicas消息至少被写入到多少个副本才算是"真正写入",该值默认值为1
推荐副本个数的 N/2 + 1。
unclean.leader.election.enable = false
当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。
replication-factor=3用来设置主题的副本数
Comsumer消费者:
概述:
消费消息。
组:
每个consumer属于一个特定的consuer group(可为每个consumer指定group name,若不指定group name则属于默认的group)。
同一topic的一条消息只能被同一个consumer group内的一个consumer消费,但多个consumer group可同时消费这一消息。
相同组的消费者,负载均衡地分区消费一个话题的多个消息。这种情况和queue模式很像;消息将会在consumers之间负载均衡。
不同组的消费者,通过订阅广播消费相同话题的相同总数量消息。那这就是"发布-订阅";消息将会广播给所有的消费者。
超过topic分区数的consumer将会闲置。
消费流程:
1.拦截器处理
2.消息处理
拦截器:
方法:
onCommit消费者提交偏移量的时候,经过该方法
close()用于关闭该拦截器用到的资源,如打开的文件,连接的数据库等
自定义:
需要实现ConsumerInterceptor接口
重平衡:
触发条件:
1. 消费者组内成员发生变更,这个变更包括了增加和减少消费者,比如消费者宕机退出消费组。
2. 主题的分区数发生变更,kafka目前只支持增加分区,当增加的时候就会触发重平衡
3. 订阅的主题发生变化,当消费者组使用正则表达式订阅主题,而恰好又新建了对应的主题,就会触发重平衡
缺点:
因为重平衡过程中,消费者无法从kafka消费消息,这对kafka的TPS影响极大,而如果kafka集内节点较多,比如数百个,那重平衡可能会耗时极多。
数分钟到数小时都有可能,而这段时间kafka基本处于不可用状态。
解决:
需要保证尽力避免消费者故障。而其他几种触发重平衡的方式,增加分区,或是增加订阅的主题,抑或是增加消费者,更多的是主动控制。
相关配置:
session.timout.ms控制心跳超时时间,设置为6s
heartbeat.interval.ms控制心跳发送频率,设置2s
max.poll.interval.ms控制poll的间隔。推荐为消费者处理消息最长耗时再加1分钟
分区分配策略:
按范围分配:RangeAssigner
按轮询分配:RoundRobinAssigner
粘性分配:StickyAssigner
位移offset:
概述:
标记消息的位移。
存储位置:
早期由zookeeper管理消费组的偏移量。
kafka自维护,减小网络IO
mysql
提交方式:
自动提交enable.auto.commit=true,可能会重复消费。
手动提交
commitSync()阻塞
commitAsync()异步,出现问题不会自动重试
Leader:
一个分区中负责读、写的副本,它的数据一定是>=其它副本的,如果它挂了,通过ZK来选举一个新的Leader;
Follower:
一个分区中负责从Leader副本中同步数据,保持对齐的小弟副本,混的好进ISR,混不好进OSR
AR:
分区的所有副本,AR(All Replications)= ISR + OSR;
ISR:
概述:
in-sync replicas set (ISR),与leader保持同步的follower集合,由leader按照LEO同步情况维护着Leader以及其它同步数据完整性较高的Follower;
如果ISR中Follower的数据同步落后到某个标准,Leader会将其剔出ISR=>OSR。
leader选举:
如果Leader进程挂掉,unclean.leader.election.enable=false 的情况下,会在ISR队列中选择一个服务作为新的Leader。
配置:
参数replica.lag.time.max.ms(默认10000)决定一台服务是否可以加入ISR副本队列。
OSR:
落后同步的follower
LEO:
last end offset
每个副本上下一条数据将被插入的offset
HIGH WATER MARK高水位线:
概述:
水位或水印(watermark)一词,也可称为高水位(high watermark)。
取 partition 对应的 ISR中 最小的 LEO 作为 HW,consumer 最多只能消费到 HW 所在的位置上一条信息。
offset < HW 的日志被认为是 已提交,已备份,对消费者可见。
木桶原理。
作用:
节点重启后,每次从hw截断消息,将leo恢复为hw。不论是老的 Leader 还是新选举的 Leader,Consumer 都能读到一样的数据。
解决了一定程序上的数据不一致问题:
一个消费者从当前 Leader(副本0) 读取并处理了 Message4,这个时候 Leader 挂掉了,选举了副本1为新的 Leader,这时候另一个消费者再去从新的 Leader 读取消息,发现这个消息其实并不存在,这就导致了数据不一致性问题。
更新机制:
1.leader 分区接收到 producer 发送的消息,该分区磁盘中写入消息,LEO++。
2.follower 向 leader fetch消息,携带 fetchOffset=N (当前需要fetch到的消息offset)。更新 leader remote LEO,根据每个 leader remote LEO, leaderHW 更新 leaderHW。
3.leader 发送当前的 leaderHW 以及日志消息响应消息,发送给 follower。follower 该分区写入消息,更新自己的 follower LEO++
4.follower 发送第二轮 fetch,携带当前最新的 fetchOffset = N+1,leader 接收到请求,更新 remote LEO = N+1,更新 leaderHW,同时将 leaderHW 发送给 follower
5.follower 对比当前最新的 LEO 与 leaderHW,取最小的作为新的 followerHW 值。
缺陷:
需要两轮 fetch req 才能完成 leader 对 leaderHW & followerHW 的更新。
数据不一致:
假设ACK全部写入,leader和follwer的leo更新,leader的hw更新,但follwer的hw还没更新。
如果此时follwer重启,leader宕机,foller成为新的leader,leo截断为旧的hw,接收一条生产者消息,leo和hw更新。此时旧的leader重启,hw与新leader一致,不更新。不进行日志截断。
于是,Leader B和Follower A的offset=1存储的消息是不一致的。
数据丢失:
ACK全部写入,leader和follwer的leo更新,hw都还没更新。
此时follwer重启,leader宕机,follwer切换为新leader,不管旧leader是否已经更新hw(因为新leader leo小),那么都会先后截断消息,丢失了一条已经commit的消息。
Lead Epoch:
概述:
记录(epoch, offset)这组键值对数据,这个键值对会被定期写入一个检查点文件。Leader每发生一次变更epoch的值就会加1,offset就代表该epoch版本的Leader写入的第一条日志的位移。
当follower副本需要截断日志时,这个[LeaderEpoch => StartOffset]向量会替代高位水作为其截断操作的参照数据。
解决:
数据丢失:
重启后,当请求获知到 Leader LEO=2 后,B 发现该 LEO 值不比它自己的 LEO 值小,而且缓存中也没有保存任何起始位移值 > 2 的 Epoch 条目,因此 B 无需执行任何日志截断操作。
或者自己成为leader,写入(epoch, offset),也无需截断。
副本是否执行日志截断不再依赖于高水位进行判断。
不能解决由于nclean.leader.election.enable=true带来的数据丢失问题。
数据不一致:
旧leader重启后, 请求新leader的epoch-offset,新leader由于不会丢弃LEO,那么旧leader的leo也不会不一致。
Kafka消息存储机制:
kafka的消息是存储在磁盘的,Producer等待写入磁盘和完全复制好才确认写入完毕
所以数据不易丢失, 如上了解,partition是存放消息的基本单位,那么它是如何存储在文件当中的呢,
如上:topic-partition-id,每个partition都会保存成一个文件,这个文件又包含两部分。 .index索引文件、.log消息内容文件。
index文件结构很简单,每一行都是一个key,value对,
key 是消息的序号offset,value 是消息的物理位置偏移量. index索引文件 (offset消息编号-消息在对应文件中的偏移量)
Index文件中这些编号不是连续的。因为index文件中没有为数据文件中的每条消息都建立索引,而是采用稀疏存储的方式,间隔一定字节的数据建立一条索引。
这样避免了索引未见占用过多的空间,从而可以将索引文件保留在内存中。
但是缺点是没有建立索引的message也不能一次定位到其在数据文件的位置,从而需要做一次顺序扫描,但是这次顺序扫描的范围就很小啦。
事务:
生产者事务:
幂等性发送(单个producer):
背景问题:
1.Broker 保存消息后,发送 ACK 前宕机,Producer 认为消息未发送成功并重试,造成数据重复。
2.前一条消息发送失败,后一条消息发送成功,前一条消息重试后成功,造成数据乱序。
概述:
Kafka 引入了Producer ID(即PID)和Sequence Number。
PID:
每个新的 Producer 在初始化的时候会被TC分配一个唯一的 PID,该 PID 对用户完全透明而不会暴露给用户。
Sequence Number:
对于每个 PID,该 Producer 发送数据的每个<Topic, Partition>都对应一个从 0 开始单调递增的Sequence Number。
实现:
Broker 端也会为每个<PID, Topic, Partition>维护一个序号,并且每次 Commit 一条消息时将其对应序号递增。
对于接收的每条消息,
如果其序号比 Broker 维护的序号(即最后一次 Commit 的消息的序号)大一,则 Broker 会接受它,否则将其丢弃
如果消息序号比 Broker 维护的序号大一以上,说明中间有数据尚未写入,也即乱序,此时 Broker 拒绝该消息,Producer 抛出InvalidSequenceNumber
如果消息序号小于等于 Broker 维护的序号,说明该消息已被保存,即为重复消息,Broker 直接丢弃该消息,Producer 抛出DuplicateSequenceNumber
配置:
enable.idempotence=true
开启后,强制检查以下配置
retries必须大于0,默认为Integer.MAX_VALUE,表示无限重试
max.in.flight.requests.per.connection不大于5,默认为5
ack=-1,开启幂等后默认为-1
事务:
背景:
上述幂等设计只能保证单个 Producer 对于同一个<Topic, Partition>的Exactly Once语义,并不能保证多个写操作的原子性。
概述:
二阶段提交的思想
可使得应用程序将生产数据和消费数据当作一个原子单元来处理,要么全部成功,要么全部失败,即使该生产或消费跨多个<Topic, Partition>。
另外,有状态的应用也可以保证重启后从断点处继续处理,也即事务恢复。
特性:
隔离性:Transaction Marker与PID提供了识别消息是否应该被读取的能力
原子性:二阶段提交(提交or回滚)
持久性:log文件(只要写入,就不会丢失,不存在mysql的内存情况)
一致性:以上三个特性
开启:
1.客户端配置提供transactionalId,通过客户端参数transactional.id来设置
2.事务要求生产者开启幂等性,默认开启。
TransactionCoordinator:
Kafka 集群中运行着多个 TC 服务,每个TC 服务负责事务 topic 的一个分区读写,也就是这个分区的 leader。Producer 根据 transaction id 的哈希值,来决定该事务属于事务 topic 的哪个分区,最后找到这个分区的 leader 位置。
流程:
1.寻找 TC 服务地址。
Producer 会首先从 Kafka 集群中选择任意一台机器,然后向其发送请求,获取 TC 服务的地址。
事务划分是根据 transaction id, 计算出该事务属于哪个分区。这个分区的 leader 所在的机器,负责这个事务的TC 服务地址。
2.事务初始化。
Producer 在使用事务功能,必须先自定义一个唯一的 transaction id。
Kafka 实现事务需要依靠幂等性,而幂等性需要指定 producer id 。所以Producer在启动事务之前,需要向 TC 服务申请 producer id。TC 服务在分配 producer id 后,会将它持久化到事务 topic。
当 Producer 重启,Producer会根据当前事务的 transactionID 获取对应的PID。
3.发送消息
Producer 在接收到 producer id 后,就可以正常的发送消息了。
不过发送消息之前,需要先将这些消息的分区地址,上传到 TC 服务。TC 服务会将这些分区地址持久化到事务 topic。
然后 Producer sender才会真正的发送消息,这些消息与普通消息不同,它们会有一个字段,表示自身是事务消息。
class TransactionMetadata(
val transactionalId: String,
var producerId: Long,
var producerEpoch: Short,
var txnTimeoutMs: Int,
var state: TransactionState,
val topicPartitions: mutable.Set[TopicPartition],
@volatile var txnStartTimestamp: Long = -1,
@volatile var txnLastUpdateTimestamp: Long
)
事务topic处于状态Ongoing。
4.发送提交请求
Producer 发送完消息后,如果认为该事务可以提交了,就会发送提交请求到 TC 服务。Producer 的工作至此就完成了,接下来它只需要等待响应。
这里需要强调下,Producer 会在发送事务提交请求之前,会等待之前所有的请求都已经发送并且响应成功。
5.TC将提交请求持久化
TC服务收到事务提交请求后,会先将提交信息先持久化到事务 topic(appendTransactionToLog方法追加)。
持久化成功后,服务端就立即发送成功响应给 Producer。然后找到该事务涉及到的所有分区,为每个分区生成提交请求,存到队列里等待发送。
响应未返回,超时,TC 服务就会回滚该事务,更新和持久化事务的状态,并且发送事务回滚结果给分区。那么会重新第3步。
响应返回了,就算个别分区提交失败,事务topic处于PrepareCommit阶段,仍然会继续提交该事务。(因为事务 topic包含所有的信息)
6.发送事务结果信息给分区
后台线程会不停的从队列里,拉取请求并且发送到分区。当一个分区收到事务结果消息后,会将结果保存到分区里,并且返回成功响应到 TC服务。
当 TC 服务收到所有分区的成功响应后,会持久化一条事务完成的消息到事务 topic。至此,一个完整的事务流程就完成了。
示例:
producer.beginTransaction();
producer.sendOffsetsToTransaction(currentOffsets(consumer), "my-group-id"); 过程中消费了topic消息。保证提交到同一个事务。
producer.commitTransaction();
投递语义:
at most once:
最多一次。消息可能丢失,但对不会重复。
将request.required.acks设为0,意思就是Producer不等待Leader确认,只管发出即可;最可能丢失消息。如果丢了消息,就是投递0次。如果没丢,就是投递1次。符合最多投递一次的含义。
at least once:
最少一次。消息绝不会丢失,但可能重复。ACK机制 + 没开启幂等和事务,那么发送ACK前宕机,producer就会重新发送。
exactly once:
恰好一次。每条消息肯定会被传输一次且仅传输一次,kafka在0.11.0.0版本之后支持恰好投递一次的语义。幂等性和事务这两个特性。
消费者事务:
概述:
事务能保证的语义相对偏弱。由于以下原因,Kafka并不能保证已提交的事务中所有消息都能够被消费:
1.对采用日志压缩策略的主题而言,事务中的某些消息有可能被清理(相同key的消息,后写入的消息会覆盖前写入的消息)
2.事务中消息可能分布在同一个分区的多个日志分段LogSegment中,当老的日志分段被删除时,对应的消息可能会丢失
3.消费者可以通过seek()方法访问任意offset的消息,从而可能遗漏事务中的部分消息。
4.消费者在消费时可能没有分配到事务内所有分区,如此它就不能读取事务中的所有消息。
隔离级别:
消费者有一个参数 islation.level,这个参数指定的是事务的隔离级别。
它的默认值是 read_uncommitted(未提交读),意思是消费者可以消费未commit的消息。
当参数设置为 read_committed,则消费者不能消费到未commit的消息。
消费语义
如何保证消息最多消费一次?
Producer:满足最多投递一次的语义即可,即只管发消息,不需要等待消息队列返回确认消息。
Message Queue:接到消息后往内存中一放就行,不用持久化存储。
Consumer:拉取到消息以后,直接给消息队列返回确认消息即可。至于后续消费消息成功与否,无所谓的。
即按照以下顺序执行
consumer.poll();
consumer.commit();
processMsg(message);
如何保证消息至少消费一次?
Producer:满足至少投递一次语义即可,即发送消息后,需要等待消息队列返回确认消息。如果超时没收到确认消息,则重发。
Message Queue:接到消息后,进行持久化存储,而后返回生产者确认消息。
Consumer:拉取到消息后,进行消费,消费成功后,再返回确认消息。
即按照如下顺序执行
consumer.poll();
processMsg(message);
consumer.commit();
由于这里Producer满足的是至少投递一次语义,因此消息队列中是有重复消息的。所以我们的Consumer会出现重复消费的情形!
如何保证消息恰好消费一次?
在保证至少消费一次的基础上,processMsg满足幂等性操作即可。
kafka为什么可以扛住这么高的qps?
读取:
分区分段(二分查找就可以定位到该Message在哪个段(segment)中)
索引(采用了稀疏存储的方式,每隔一定字节的数据建立一条索引。)
page cache
利用操作系统自身的内存而不是JVM空间内存。
页缓存是操作系统实现的一种主要的磁盘缓存,以此用来减少对磁盘 I/O 的操作。(不用再读取磁盘)
具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。
零拷贝
linux操作系统 “零拷贝” 机制使用了sendfile方法, 允许操作系统将数据从内核态传输到套接字缓冲区, 这样避免重新复制数据。
将数据直接从磁盘文件复制到网卡设备中
写入:
1.顺序写
磁盘分为顺序读写与随机读写,内存也一样分为顺序读写与随机读写。基于磁盘的随机读写确实很慢,但磁盘的顺序读写性能却很高,一般而言要高出磁盘随机读写三个数量级,一些情况下磁盘顺序读写性能甚至要高于内存随机读写。
Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入
2.批量写batch size
可以避免在网络上频繁传输单个消息带来的延迟和带宽开销。
满足batch.size和ling.ms之一,producer便开始发送消息。
3.批量压缩
把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。
4.ack机制,0,1,-1,会影响速度。
生产相关配置:
buffer.memory默认32MB。如果生产者发送消息的速度超过了将消息发送到broker的速度,或者存在网络问题,send()方法调用会被阻塞max.block.ms参数配置的时常,默认1分钟。
max.block.ms该参数指定了在调用send()方法或使用partitionsFor()方法获取元数据时生产者的阻塞时间。
当生产者的发送缓冲区已满,或者没有可用的元数据时,这些方法就会被阻塞。
linger.ms该参数指定了生产者在发送批次之前等待更多消息加入批次的时间。kafka生产者会在批次填满或linger.ms达到上限时把批次发送出去。
默认为0
batch.size当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算(而不是消息个数)。
当批次被填满,批次里的所有消息会被发送出去。不过生产者井不一定都会等到批次被填满才发送,这取决于linger.ms的配置。
把批次大小设置得很大,也不会造成延迟,只是会占用更多的内存而已。但如果设置得太小,因为生产者需要更频繁地发送消息,会增加一些额外的开销。
max.in.flight.requests.per.connection
该参数指定了生产者在收到服务器晌应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量。
把它设为1可以保证消息是按照发送的顺序写入服务器的,即使发生了重试。
几个重要的配置:
unclean.leader.election.enable:
概述:
是否允许从非ISR集合中选举follower副本称为新的leader。
细节:
某种状态下,follower2副本落后leader副本很多,并且也不在leader副本和follower1副本所在的ISR(In-Sync Replicas)集合之中。
此时follower2副本还在,就会进行新的选举,不过在选举之前首先要判断unclean.leader.election.enable参数的值。
如果unclean.leader.election.enable参数的值为false,那么就意味着非ISR中的副本不能够参与选举,此时无法进行新的选举,此时整个分区处于不可用状态。
总结:Kafka的可用性就会降低。
如果unclean.leader.election.enable参数的值为true,那么可以从非ISR集合中选举follower副本称为新的leader。
此时,原来的leader副本恢复,成为了新的follower副本,准备向新的leader副本同步消息,但是它发现自身的LEO比leader副本的LEO还要大。Kafka中有一个准则,follower副本的LEO是不能够大于leader副本的,所以新的follower副本就需要截断日志至leader副本的LEO处。
新的follower副本需要删除消息4和消息5,之后才能与新的leader副本进行同步。(之前follower2还没同步到HWM的那部分)
原本客户端已经成功的写入了消息4和消息5,而在发生日志截断之后就意味着这2条消息就丢失了,并且新的follower副本和新的leader副本之间的消息也不一致。
总结:有可能发生数据丢失和数据不一致的情况,Kafka的可靠性就会降低;
监控工具:
比较流行的监控工具有:
KafkaOffsetMonitor
KafkaManager
Kafka Web Console
Kafka Eagle