Kafka 是高吞吐低延迟的高并发、高性能的消息中间件,在大数据领域有极为广泛的运用。配置良好的 Kafka 集群甚至可以做到每秒几十万、上百万的超高并发写入。可参考这篇文章:页缓存技术 + 磁盘顺序写 + 零拷贝技术
由 Producer(生产),Broker(存储)和Consumer(消费)三部分构成。
底层根本原因:已经消费了数据,但是offset没提交。
在kafka 0.11版本中已经提出,kafka 将对事务和幂等性的支持,使得kafka 端到端exactly once(精确的一次)语义成为可能。幂等性与事务性都是Kafka发展过程中非常重要的。
参考:Kafka幂等性原理及实现剖析
Producer在生产发送消息时,难免会重复发送消息。Producer进行retry时会产生重试机制,发生消息重复发送或者消息乱序。而引入幂等性后,重复发送只会生成一条有效的消息。Kafka作为分布式消息系统,它的使用场景常见与分布式系统中,比如消息推送系统、业务平台系统(如物流平台、银行结算平台等)。以银行结算平台来说,业务方作为上游把数据上报到银行结算平台,如果一份数据被计算、处理多次,那么产生的影响会很严重。
在使用Kafka时,需要确保Exactly-Once语义。分布式系统中,一些不可控因素有很多,比如网络、OOM、FullGC等。在Kafka Broker确认Ack时,出现网络异常、FullGC、OOM等问题时导致Ack超时,Producer会进行重复发送。
Kafka为了实现幂等性,它在底层设计架构中引入了ProducerID和SequenceNumber。那这两个概念的用途是什么呢?
Kafka在引入幂等性之前,Producer向Broker发送消息,然后Broker将消息追加到消息流中后给Producer返回Ack信号值。这是一种理想状态下的消息发送情况,但是实际情况中,会出现各种不确定的因素,比如在Producer在发送给Broker的时候出现网络异常。比如以下这种异常情况的出现:当Producer第一次发送消息给Broker时,Broker将消息(x2,y2)追加到了消息流中,但是在返回Ack信号给Producer时失败了(比如网络异常) 。此时,Producer端触发重试机制,将消息(x2,y2)重新发送给Broker,Broker接收到消息后,再次将该消息追加到消息流中,然后成功返回Ack信号给Producer。这样下来,消息流中就被重复追加了两条相同的(x2,y2)的消息。
生产者要使用幂等性很简单,只需要增加以下配置即可:enable.idempotence=true
与幂等性有关的另外一个特性就是事务。Kafka中的事务与数据库的事务类似,Kafka中的事务属性是指一系列的Producer生产消息和消费消息提交Offsets的操作在一个事务中,即原子性操作。对应的结果是同时成功或者同时失败。
这里需要与数据库中事务进行区别,操作数据库中的事务指一系列的增删查改,对Kafka来说,操作事务是指一系列的生产和消费等原子性操作。
数据传输的事务定义:
Producer提供了五种事务方法,它们分别是:initTransactions()、beginTransaction()、sendOffsetsToTransaction()、commitTransaction()、abortTransaction(),代码定义在org.apache.kafka.clients.producer.Producer
// 初始化事务,需要注意确保transation.id属性被分配
void initTransactions();
// 开启事务
void beginTransaction() throws ProducerFencedException;
// 为Consumer提供的在事务内Commit Offsets的操作
void sendOffsetsToTransaction(Map<TopicPartition, OffsetAndMetadata> offsets,
String consumerGroupId) throws ProducerFencedException;
// 提交事务
void commitTransaction() throws ProducerFencedException;
// 放弃事务,类似于回滚事务的操作
void abortTransaction() throws ProducerFencedException;
数据丢失是一件非常严重的事情事,针对数据丢失的问题我们需要有明确的思路来确定问题所在。首先应该明确数据到底是在什么地方丢失的数据,在kafka之前的环节或者kafka之后的流程丢失?比如kafka的数据是由flume提供的,也许是flume丢失了数据,kafka自然就没有这一部分数据。
如果定位到是kafka环节丢失数据,那么常见的kafka环节丢失数据的原因有:
auto.commit.enable=true
(采用自动提交的机制),当consumer拿到了一些数据但还没有完全处理掉的时候,刚好到auto.commit.interval.ms
(的默认值是 5000,单位是毫秒)发出了提交offset操作,接着consumer挂掉了。这时已经拿到的数据还没有处理完成但已经被commit掉,因此没有机会再次被处理,数据丢失。log.flush.interval.messages
(在消息刷到磁盘之前,日志分区收集的消息数量,long型,默认值是9223372036854775807)和log.flush.interval.ms
(主题中消息在刷到磁盘之前,保存在内存中的最长时间,单位是ms,如果不配置,使用log.flush.scheduler.interval.ms
(单位为毫秒,日志刷新器检查日志是否需要刷到磁盘的频率,long型,默认为9223372036854775807)的配置,long型,默认为null)来配置flush间隔,无论哪个达到都会flush。interval大丢的数据多些,小会影响性能。设置较小定期 flush 的时间,并不能真正保证数据不会丢失,也就是说设置 flush 的时间,不能从根本上保证我们的数据丢失问题。replica.fetch.max.bytes
。这个值应该比message.max.bytes
大,否则broker会接收此消息,但无法将此消息复制出去,从而造成数据丢失。 topic设置多分区,分区自适应所在机器,为了让各分区均匀分布在所在的broker中,分区数要大于broker数。分区是kafka进行并行读写的单位,是提升kafka速度的关键。
broker能接收消息的最大字节数message.max.bytes
,这个值应该比消费端的fetch.message.max.bytes更小才对,否则broker就会因为消费端无法使用这个消息而挂起。Kafka设计的初衷是迅速处理短小的消息,一般10K大小的消息吞吐性能最好,但有时候,我们需要处理更大的消息,比如XML文档或JSON内容,一个消息差不多有10-100M,这种情况下,Kakfa应该如何处理?可参考:kafka发送字节过多引起的发送失败、kafka中处理超大消息的一些考虑、kafka处理超大消息的配置
参数调优(server.properties):
1、网络和IO操作线程配置优化
# broker 处理消息的最大线程数(默认为3)
num.network.threads = cpu核数+1
# broker 处理磁盘IO的线程数
num.io.threads=cpu核数*2
2、log数据文件刷盘策略
# 每当producer写入10000条消息时,刷数据到磁盘
log.flush.interval.messages=10000
# 每间隔1秒钟时间,刷数据到磁盘
log.flush.interval.ms=1000
3、日志保留策略配置
# 保留三天,也可以更短
log.retention.hours=72
4、Replica相关配置
offsets.topic.replication.factor:3
# 这个参数指新创建一个topic时,默认的Replica数量,Replica过少会影响数据的可用性,太多则会白白浪费存储资源,一般建议在2~3为宜。
buffer.memory:33554432 (32m)
#在Producer端用来存放尚未发送出去的Message的缓冲区大小。缓冲区满了之后可以选择阻塞发送或抛出异常,由block.on.buffer.full的配置来决定。
compression.type:none
# 默认发送不进行压缩,推荐配置一种适合的压缩算法,可以大幅度的减缓网络压力和Broker的存储压力。
关闭自动更新offset,等到数据被处理后再手动跟新offset。在消费前做验证前拿取的数据是否是接着上回消费的数据,不正确则return先行处理排错。手动维护Offset可参考:https://blog.csdn.net/qq_20641565/article/details/64440425、https://blog.csdn.net/qq_38483094/article/details/99118140、https://www.cnblogs.com/wh984763176/p/13809346.html
这个是总结出的到目前为止没有发生丢失数据的情况:
//producer用于压缩数据的压缩类型。默认是无压缩。正确的选项值是none、gzip、snappy。压缩最好用于批量处理,批量处理消息越多,压缩性能越好
props.put("compression.type", "gzip");
//增加延迟
props.put("linger.ms", "50");
//这意味着leader需要等待所有备份都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据。这是最强的保证。,
props.put("acks", "all");
//无限重试,直到你意识到出现了问题,设置大于0的值将使客户端重新发送任何数据,一旦这些数据发送失败。注意,这些重试与客户端接收到发送错误时的重试没有什么不同。允许重试将潜在的改变数据的顺序,如果这两个消息记录都是发送到同一个partition,则第一个消息失败第二个发送成功,则第二条消息会比第一条消息出现要早。
props.put("retries ", MAX_VALUE);
props.put("reconnect.backoff.ms ", 20000);
props.put("retry.backoff.ms", 20000);
//关闭unclean leader选举,即不允许非ISR中的副本被选举为leader,以避免数据丢失
props.put("unclean.leader.election.enable", false);
//关闭自动提交offset
props.put("enable.auto.commit", false);
//限制客户端在单个连接上能够发送的未响应请求的个数。设置此值是1表示kafka broker在响应请求之前client不能再向同一个broker发送请求。注意:设置此参数是为了避免消息乱序
props.put("max.in.flight.requests.per.connection", 1);
# 默认内存1个G,生产环境调整为4-6个G,尽量不要超过6个G,因为超过6G的上限后和6G效果一样。
export KAFKA_HEAP_OPTS="-Xms4g -Xmx4g"
扩展:Push vs. Pull:
作为一个messaging system,Kafka遵循了传统的方式,选择由producer向broker push消息并由consumer从broker pull消息。事实上,push模式和pull模式各有优劣。
push模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。
比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。
消费者从 partition 中取出来数据的时候(kafka的消费组的组员 consumer 数量最多增加到和partition数量一致,超过的组员只会占用资源,而不起作用),也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞多个线程来并发处理消息。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。
实际上,Kafka 0.9 开始提供了新版本的 consumer 及 consumer group,位移的管理与保存机制发生了很大的变化——新版本 consumer 默认将不再保存位移到 zookeeper 中,而目前 kafkaoffsetmonitor 还没有应对这种变化(虽然已经有很多人在要求他们改了),所以很有可能是因为你使用了新版本的 consumer 才无法看到的。关于新旧版本,这里统一说明一下:kafka 0.9 以前的 consumer 是使用 Scala 编写的,包名结构是kafka.consumer.*
,分为 high-level consumer
和low-level consumer
两种。我们熟知的 ConsumerConnector
、ZookeeperConsumerConnector
以及 SimpleConsumer
就是这个版本提供的;自 0.9 版本开始,Kafka 提供了 java 版本的 consumer,包名结构是o.a.k.clients.consumer.*
,熟知的类包括 KafkaConsumer
和 ConsumerRecord
等。新版本的 consumer 可以单独部署,不再需要依赖 server 端的代码。
老版本的位移是提交到 zookeeper 中的,目录结构是:/consumers/
,但是 zookeeper 其实并不适合进行大批量的读写操作,尤其是写操作。因此 kafka 提供了另一种解决方案:增加 __consumeroffsets
topic,将 offset 信息写入这个 topic,摆脱对 zookeeper 的依赖(指保存 offset 这件事情)。__consumer_offsets
中的消息保存了每个 consumer group 某一时刻提交的 offset 信息。新版 Kafka 已推荐将 consumer 的位移信息保存在 Kafka 内部的 topic 中,即 __consumer_offsets
topic,并且默认提供了 kafka_consumer_groups.sh
脚本供用户查看 consumer 信息。
__consumers_offsets
topic 配置了 compact 策略,使得它总是能够保存最新的位移信息,既控制了该 topic 总体的日志容量,也能实现保存最新 offset 的目的。compact 的具体原理请参见:Log Compaction
checkpoint:参考:https://www.zhihu.com/question/426382418
“kafka会利用checkpoint机制对offset进行持久化” — 这里的offset不是指消费者的消费位移,而是指其他位移,而持久化应该是指Kafka把位移数据保存到Broker本地磁盘文件这件事。目前,Kafka对三类位移做checkpointing:Log Start Offset;Recovery Point Offset;Replication Offset。
第一个。每个topic partition log对象都有一个重要的位移字段:log start offset,标识分区消息对外部用户或应用可见的最早消息位移。在一些事件发生时Kafka会触发对该值的更新。Kafka对该offset进行checkpointing的初衷是更快地保存分区的元数据,这样下次再初始化Log对象时能够直接加载并初始化log start offset。
第二个是recovery point offset,它保存的是第一条未flush到磁盘的消息。Kafka对它进行checkpointing能够显著加速日志段恢复(recover)的速度,因为直接从recovery point offset所在的日志段开始恢复即可,没必要从头恢复日志段。毕竟生产环境上,分区下的日志段文件可能是非常多的。
第三个是replication offset,保存replication过程中副本的高水位(HW)位移值。通常的场景是当副本重启回来后创建Log对象时直接使用这个文件中的offset对高水位对象进行赋值,省去了读取日志段自行计算HW值的步骤。
总之,checkpointing大体的作用都是将Kafka Broker端重要的日志元数据保存下来,避免后面“书到用时方恨少”的尴尬。
Kafka将消息存储在磁盘中,为了控制磁盘占用空间的不断增加就需要对消息做一定的清理操作。Kafka中每一个分区partition都对应一个日志文件,而日志文件又可以分为多个日志分段文件,这样也便于日志的清理操作。Kafka提供了两种日志清理策略:日志删除(Log Deletion):按照一定的保留策略来直接删除不符合条件的日志分段;日志压缩(Log Compaction):针对每个消息的key进行整合,对于有相同key的的不同value值,只保留最后一个版本。可参考:Kafka日志清理之Log Deletion
我们可以通过broker端参数log.cleanup.policy来设置日志清理策略,此参数默认值为“delete”,即采用日志删除的清理策略。如果要采用日志压缩的清理策略的话,就需要将log.cleanup.policy设置为“compact”,并且还需要将log.cleaner.enable(默认值为true)设定为true。通过将log.cleanup.policy参数设置为“delete,compact”还可以同时支持日志删除和日志压缩两种策略。日志清理的粒度可以控制到topic级别,比如与log.cleanup.policy对应的主题级别的参数为cleanup.policy,为了简化说明,本文只采用broker端参数做陈述,如若需要topic级别的参数可以查看官方文档。