Kafka是什么
Kafka是一个分布式的基于发布/订阅模式的消息队列,它主要应用于大数据实时处理领域。它的主要设计目标如下
Kafka的特性
● 高性能
○ 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间的访问性能
● 高吞吐、低延迟
○ 在很廉价的机器上也能做到单机支持每秒几十万条消息的传输,并保持毫秒级延迟
● 持久性、可靠性
○ 消息最终被持久化到磁盘,且提供数据备份机制防止数据丢失
● 容错性
○ 支持集群节点故障容灾恢复,即使某一台Kafka服务节点宕机,也不会影响到整个系统的功能
● 高并发
○ 可以支撑数千个客户端同时进行读写操作
kafka的适用场景
● 日志收集方向
● 消息系统方向
● 大数据实时计算方向
Kafka体系架构
● Producer:生产者,生产者负责创建消息,然后将其投递到Kafka中
● Consumer:消费者,消费者连接到Kafka上接收消息,进而进行相应的业务逻辑处理
● Broker:服务代理节点,对于Kafka而言,Broker可以简单的看作一个独立的Kafka服务节点或Kafka服务实例。一个或多个Broker组成了一个Kafka集群
● Zookeeper:Kafka集群能够正常工作需要依赖于Zookeeper,包括:
○ 集群元数据管理
○ 集群成员管理
○ Controller选举
○ 其他管理类任务
Kafka基础知识
● Producer:消息生产者
● Consumer:消息消费者
● Consumer Group:消费者组,消费者组内的每个消费者负责消费不通分区的数据,以提高消费能力
● Broker:一台Kafka机器就是一个Broker。一个集群是由多个Broker组成的,且一个Broker可以容纳多个Topic
● Topic:主题,可以简单理解为队列,Topic将消息分类,生产者和消费者面相的都是同一个Topic
● Partiton:分区,为了实现Topic的扩展性,提高并发能力,一个非常大的Topic可以分不到多个Broker上,一个Topic可以分为多个Partition进行存储,每个Partition都是一个有序的队列
● Replica:副本,副本之间是“一主多从的”的关系,其中Leader副本负责处理读写请求,Follower副本只负责与Leader副本的消息同步。副本处于不同的Broker中,当Leader副本出现故障时,从Follower副本中重新选举新的Leader副本对外提供服务。Kafka通过多副本机制来实现故障的自动转移,当Kafka集群中的某个Broker失效时仍然能保证服务可用。
○ Leader副本:每个分区多个副本的主副本
○ Follower副本:每个分区多个副本的从副本
● AR:分区中所有副本的统称,AR = ISR + OSR
● ISR:所有与Leader副本保持一定程度同步的副本
● OSR:与leader副本同步滞后过多的副本集合
● HW(High Watermark)高水位:标识一个特定的消息偏移量,消费者只能拉取这个offset之前的消息
○ 分区的高水位就是其Leader副本的高水位
○ HW作用
■ 用来表示分区下哪些消息是可以被消费者消费的
■ 协助Kafka完成副本数据同步
● LSO(LogStartOffset):第一条消息的offset
● LEO(LogEndOffset):标识当前日志文件中下一条待写入消息的offset。LEO大小相当于当前日志分区中最后一条消息的offset + 1
○ LEO作用
■ 如果Follower和Leader的LEO数据同步了,那么HW就可以更新了
■
● Controller:Kafka Controller,其实就是Kafka集群中的一台Broker,它的主要作用是在Zookeeper集群的帮助下管理和协调整个Kafka集群
Kafka的三高架构设计
Kafka的高可用
● Leader选举机制,触发Leader选举的几种场景:
○ Offline:创建新分区或分区失去现有Leader
○ Reassign:用户执行重分配操作
○ PrefferredReplica:将Leader迁移回首选副本
○ ControlledShutdown:分区现有的Leader即将下线
● 副本机制
● ISR机制
○ 每个分区都有一个ISR列表,用于维护所有同步的、可用的副本。Leader副本必然是同步副本、而对于Follower副本来说,它需要满足以下条件才能被认为是同步副本
■ 必须定时向Zookeeper发送心跳
■ 在规定的时间内从Leader副本“低延迟”地获取过消息
如果副本不满足上面条件的话,就会被从ISR列表中移除,直到满足条件才会被再次加入(默认10s,即只要一个Follower副本落后Leader副本的时间不连续超过10s,Kafka就认为两者是同步的)
● ACK机制:
○ ACK用来执行分区中必须要有多少个副本收到这条消息,之后生产者才会认为这条消息是成功写入的。
■ acks = 0
● Producer不会等待Broker的反馈;每个记录返回的offset总是被设置为-1.这个模式下Kafka的吞吐量最大、并发最高,但是数容易丢失,通常适用于记录应用日志,对数据要求不高的场景
■ acks = 1
● 默认值为1,生产者发送消息后,只要分区的Leader副本成功写入消息,那么它就会收到来自服务端的成功响应
■ acks = -1 或 acks = all
● 生产者发送消息后,需要等待ISR中的所有副本都成功写入消息之后才能够收到来自服务端的成功响应
Kafka的高性能
● Kafka SocketServer是基于Java NIO开发的,采用了Reactor的模式。Kafka Reactor包含三种角色:
○ Acceptor
○ Processor
○ Handler
● 顺序写磁盘 + OS Cache(生产数据的角度)
○ Kafka为了保证磁盘写入性能,通过基于操作系统的页缓存来实现文件写入的。操作系统本身有一层缓存,叫做page cache,是在内存里的缓存,我们也可以称之为OS Cache,意思就是操作系统自己管理的缓存。那么在写磁盘文件的时候,就可以直接写入os cache中,接下来由操作系统自己决定什么时候把os cache里的数据刷入到磁盘中
○ Kafka在写数据的时候是以磁盘顺序写的方式来进行落盘的,即将数据追加到文件的末尾,而不是在文件的随机位置来修改数据
● “零拷贝”技术(消费数据的角度)
○ 从Kafka消费数据,在消费的时候实际上就是从Kafka的磁盘文件读取数据然后发送给下游的消费者,大概过程如下:
■ 先检查要读取的数据是否在os cache中,如果不在的话,就从磁盘文件读取数据据后放入os cache中
■ 接着从os cache里面copy数据到应用程序进程的缓存里面,再从应用程序进程的缓存里copy数据到操作系统层面的socket缓存里面,最后再从socket缓存里面读取数据后发送到网卡,最后从网卡发送到下游的消费者
○ 其中,“从操作系统的os cache拷贝数据到应用程序进程的缓存”和“接着从应用进程缓存拷贝数据到操作系统的socket缓存”这两次拷贝是没必要的缓存,相对来说比较耗性能,所以Kafka为了解决这个问题,在读取数据的时候引入了“零拷贝”技术。即让操作系统的os cache中的数据直接发送到网卡后传出给下游的消费者,中间跳过了两次拷贝数据的步骤,从而减少了拷贝的CPU开销,减少用户态内核态上下文切换次数,从而优化了数据的传输性能,而Socket缓存中仅仅会拷贝一个描述符过去,不会拷贝数据到socket缓存
○ Kafka主要使用了mmap和sendfile的方式来实现零拷贝,对应Java里的MappedByteBuffer和FileChannel.transferIO
● 压缩传输:压缩有助于提高吞吐量,降低延迟,并提高磁盘利用率,默认情况下,在生产者端不开启压缩,三端要使用相同的压缩算法
○ Producer段压缩
○ Broker端保持
○ Consumer端姐压缩
● 服务端的内存池设计
○ RecordAccumulator主要用来缓存消息以便sender线程可以批量发送,从而减少网络传输的资源消耗以提升性能,默认大小是32M。
○ 在RecordAccumulator的内部为每个分区都维护了一个双端队列,队列中的内容就是ProducerBatch,消息写入时,追加到双端队列的尾部;读取消息时,从双端队列的头部读取
○ 一个ProducerBatch可以包含一个或多个ProducerRecord。ProducerRecord指的是生产者创建的消息。而ProducerBatch指的是一个消息批次
Kafka的高并发
这里我们将 Kafka 的网络架构抽象成如上图所示的三层架构, 整个请求流转的路径如下:
客户端发送请求过来, 在Kafka 服务端会有个Acceptor线程, 这个线程上面绑定了OP_ACCEPT事件, 用来监听发送过来的请求, 下面有个while死循环会源源不断的监听Selector是否有请求发送过来, 接收到请求链接后封装成socketchannel, 然后将socketChannel发送给网络第一层架构中。
在第一层架构中有3个一模一样的Processor线程, 这个线程的里面都有一个连接队列,里面存放socketchannel, 存放规则为轮询存放, 随着请求的不断增加, 连接队列里面就会有很多个socketchannel, 这个时候socketchannel就会在每个selector上面注册OP_READ事件, 参考上图第一层的第三个Processor线程, 即每个线程里面还有一个while循环会遍历每个socketchannel, 监听到事件后就会接收到客户端发送过来的请求, 这个时候Processor线程会对请求进行解析(发送过来的请求是二进制的, 上面已经说过, 跨网络传输需要进行序列化) , 并解析封装成Request对象发送到上图所示的网络第二层架构中。
在第二层架构中会有两个队列, 一个RequestQueue(请求队列), 一个是ResponseQueue(返回队列), 在请求队列中会存放一个个Request请求, 起到缓冲的作用, 这个时候就到了网络第三层架构中。
在第三层架构中有个RequestHandler线程池, 里面默认有8个RequestHandler线程, 这8个线程启动后会不断的从第二层的RequestQueue队列中获取请求, 解析请求体里面的数据, 通过内置工具类将数据写入到磁盘
写入成功后还要响应客户端, 这个时候会封装一个Response对象, 会将返回结果存放到第二层的ResponseQueue队列中, 此时默认有3个小的Response队列, 这里面的个数是同第一层架构中的Processor线程一一对应的。
这个时候第一层的Processor线程中while循环就会遍历Response请求, 遍历完成后就会在selector上注册OP_WRITE事件, 这个时候就会将响应请求发送回客户端。
在整个过程中涉及到2个参数:num.network.threads = 3 和 num.io.threads = 8 如果感觉默认参数性能不够好的话, 可以对这2个参数进行优化, 比如将num.network.threads = 9, num.io.threads = 32(和CPU个数要一致), 每个RequestHandler线程可以处理2000QPS, 2000 * 8 = 1.6万QPS , 扩容后可以支撑6.4万QPS, 通过扩容后Kafka可以支撑6万QPS, 可以看出通过上面的架构讲解, kafka是可以支撑高并发的请求的
Kafka的存储架构设计
Kafka的存储实现方案
基于顺序追加写日志 + 稀疏索引
Kafka的日志清理机制
Kafka日志清理策略
● 日志删除:按照一定的保留策略直接删除不符合条件的日志分段
○ log.cleanup.policy=delete
● 日志压缩:针对每个消息的key进行整合,对于有相通key的不通value值,只保留最后一个版本
○ log.cleanup.policy=compact log.cleaner.enable=true
Kafka调优
对于Kafka而言,性能一般指吞吐量和延迟。所以【高吞吐量】和【低延时】是我们调优Kafka集群的主要目标
Broker端调优
● 保证服务器端和客户端版本一致
● 合理地设置Broker端参数值
○ num.network.threads:创建Processor处理网络请求的线程个数,建议设置为Broker当前CPU核心数 * 2,这个值太低经常出现网络空闲太低而缺失副本
○ num.io.threads:创建KafkaRequestHandler处理具体请求线程个数,建议设置为Broker磁盘个数 * 2
○ num.replica.fetchers:建议设置为CPU核心数/4,适当提高可以提升CPU利用率及Follower同步Leader数据并行度
○ compression.type:建议采用Iz4压缩类型,压缩可以提升CPU利用率同时可以减少网络传输数据量
○ log.flush.xxx:这几个参数表示日志数据刷新到磁盘的策略,应该保持默认配置,刷盘策略让操作系统去完成
■ log.flush.scheduler.interval.ms
■ log.flush.interval.ms
■ log.flush.interval.messages
○ auto.leader.rebalance.enable:表示是否开启leader自动负载均衡,默认true;应该把设个参数设置为false,因为自动负载均衡不可控,可能影响集群性能和稳定
Kafka处理消息丢失
消息传递语义
消息传递语义是 Kafka 提供的 Producer 和 Consumer 之间的消息传递过程中消息传递的保证性
● at most once(最多一次):消息可能会丢失,但绝不会重复传递
● at least once(至少一次):消息绝不会丢,但可能会重复传递
● exactly once(精确传递一次):消息被处理且只会被精确的处理一次。不丢失不重复就一次
Producer消息丢失
Producer端消息丢失更多是因为消息根本没有发送到Kafka Broker端,导致Producer端消息没有发送成功的原因如下:
● 网络原因:由于网络抖动导致数据根本没发送到Broker端
● 数据原因:消息体太大超出Broker承受范围而导致Broker拒收消息
● Producer端发送数据有ACK机制,这里也可能存在消息丢失
○ acks = 0:发送后就自认为成功,如果发生网络抖动,Producer端并不会校验ack,自然也就丢了,且无法重试
○ acks = 1:消息发送Leader Partiton接收成功就表示发送成功,这里如果Leader Partition发生Crash掉,且Follower Partition还未同步完数据且没有ACK,就会丢失数据
○ acks = -1 或 acks = all:消息发送需要等待ISR中Leader Partition和所有的Follower Partition都确认收到消息才算发生成功。但是如果ISR中只剩下Leader Partition,这样就编程了acks = 1的情况了
Broker端消息丢失
● 由于Kafka中并没有提供同步刷盘的方式,所以说从单个Broker来看还是很可能丢失数据的
● Kafka通过【多分区多副本】机制已经可以最大限度保证数据不丢失,如果数据已经写入PageCache中但是还没有来得及刷写到磁盘,此时如果所在Broker突然宕机挂掉或者停电,极端情况下还是会造成数据丢失
Consumer端丢失数据
● 可能使用【自动提交offset的方式】
● 拉取消息后,【先提交offset,后处理消息】:如果处理消息的时候异常宕机,由于offset已经宕机,当Consumer重启的时候会从之前已提交的offset的下一个位置重新开始消费,此时会出现消息丢失
● 拉去消息后,【先处理消息,再进行提交offset】:不会出现消息丢失,但是会出现消息重复
消息丢失解决方案
Producer端解决方案
1、更换调用方式
● 使用带回调通知的方法发送消息,替换发后立即返回的方式,这样一旦消息发送失败,就可以针对性的处理
● 网络抖动导致消息丢失,Producer端可以进行重试
● 消息大小不合格,可以进行适当调整,符合Broker承受范围再发送
2、ACK确认机制
● 需要将request.required.acks设置为 -1/all,配合参数:replication.factor >=2 min.insync.replicas > 1
3、设置重试次数
● 需要将retries设置为大于0的数,如果需要保证发送消息的顺序性,需要再设置参数:max.in.flight.requests.per.connection = 1
4、设置重试时间
● 设置retry.backoff.ms,即消息发送超时后两次重试之间的间隔时间,避免无效的频繁重试,默认100ms,推荐设置为300ms
Broker端解决方案
● unclean.leader.election.enable = false
● replication.factor >= 3
● min.insync.replicas > 1
● 另外还需要确保replication.factor > min.insync.replicas,推荐设置为 replication.factor = min.insync.replicas + 1
Consumer端解决方案
● 为了不丢数据,正确做法是,拉取数据、业务逻辑处理、提交消息offset位移信息
● 另外需要设置参数:enable.auto.commit = false,即采用手动提交位移的方式
● 对于消费消息重复的情况,业务自己保证幂等,保证只成功消费一次即可
MS
生产者有哪些发消息的模式
● 发送即忘-发送模式(吞吐量最高,消息最不可靠的一种方式)
○ 它只管发送消息,并不需要关心消息是否发送成功。本质上也是一种异步发送的方式,消息现存储在缓冲区中,达到设定条件后再批量进行发送
● 同步-发送模式
○ 调用send()方法会返回一个Future对象,再通过调用Future对象的get()方法等待结果返回,根据返回的结果可以判断消息是否发送成功
● 异步-发送模式
○ 在调用send()方法的时候指定一个callback函数,当Broker接收到返回的时候,该callback函数就会被触发执行,只有回调函数执行完毕生产者才会结束,否则一直阻塞
Kafka为什么设计分区
如果不进行分区的话,就如同MySQL单表存储一样,发消息就会被集中存储,这样就会导致某台Kafka服务器存储Topic消息过多,如果在写消息压力很大的情况下,最终会导致这台Kafka服务器吞吐量出现瓶颈,因此Kafka设计了分区的概念,同时也带来了【负载均衡】和【横向扩展】的能力
生产者发送消息时如何选择分区的
选择分区策略
● 轮询策略
● 消息key指定分区策略
● 随机策略
● 自定义策略
Kafka如何合理设置分区,越多越好吗?
合适的分区数,需要根据每个分区的生产者和消费者的目标吞吐量进行估计,可以遵循一定步骤来计算确定分区数:
分区并不是越多越好,通常情况下Kafka集群中的分区总量过大或者单个Broker节点的分区过多,都可能会对系统的可用性和消息延迟带来负面的影响
● 在使用内存方面,如果分区越多,那么相应的Broker端、Producer端和Consumer端内存占用也会更多,成本更大
● 在Kafka的日志文件目录中,每个日志数据段都会分配三个文件,两个索引文件和一个数据文件,每个Broker会为每个日志段文件打开两个index文件句柄和一个log数据文件句柄,因此,若随着分区的增多,所需要保持打开状态的文件句柄数也就越多,最终可能会超过底层操作系统配置的文件句柄数量限制
● 分区越多可能会影响端到端的延迟
如何保证Kafka中的消息是有序的
在Kafka中,并不能保证消息全局有序,但是可以保证分区有序性,分区与分区之间是无序,保证消息有序,可以从三个方面分析:
● 对于生产端,如果要保证严格的消息有序,首先要考虑用同步的方式来发送消息,两种同步方式如下:
○ 设置消息响应参数acks = all & max.in.flight.requests.per.connection = 1
○ 同步发送方式,即调用send方法后,通过返回的Future对象的get方法阻塞等待,只有当get返回数据,才会继续下一条消息的发送
● 对于Broker端,如果要严格保证业务全局有序,那么就要设置Topic为单分区的方式
● 对于消费端,我们可以通过写多个内存队列的方式,将相通key的消息写入到同一个队列中,然后对于多个线程,每个线程分别消费一个队列即可保证消息顺序
Kafka为什么不支持读写分离
我们知道,Kafka为了避免数据不一致,而采用了通过主节点来统一提供服务的方式,不支持读写分离,主要有两点原因: