ActiveMQ:
1 很是成熟,功能强大,在业内大量的公司以及项目中都有应用,
2 偶尔会有较低几率丢失消息,
3 社区以及国内应用都愈来愈少,官方社区如今对ActiveMQ 5.x维护愈来愈少,几个月才发布一个版本,并且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用日志
RabbitMQ:
1 erlang语言开发,性能极其好,延时很低;
2 吞吐量到万级,MQ功能比较完备,并且开源提供的管理界面很是棒,用起来很好用,
3 社区相对比较活跃,几乎每月都发布几个版本分在国内一些互联网公司近几年用rabbitmq也比较多一些,可是问题也是显而易见的,
4 RabbitMQ确实吞吐量会低一些,这是由于他作的实现机制比较重。并且erlang开发,国内有几个公司有实力作erlang源码级别的研究和定制?若是说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复bug。并且rabbitmq集群动态扩展会很麻烦,不过这个我以为还好。其实主要是erlang语言自己带来的问题。很难读源码,很难定制和掌控。
RocketMQ:
1 接口简单易用,并且毕竟在阿里大规模应用过,有阿里品牌保障,
2 日处理消息上百亿之多,能够作到大规模吞吐,性能也很是好,分布式扩展也很方便,社区维护还能够,可靠性和可用性都是ok的,还能够支撑大规模的topic数量,支持复杂MQ业务场景,
3 阿里出品都是java系的,咱们能够本身阅读源码,定制本身公司的MQ,能够掌控,社区活跃度相对较为通常,不过也还能够,文档相对来讲简单一些,而后接口这块不是按照标准JMS规范走的有些系统要迁移须要修改大量代码,还有就是阿里出台的技术,你得作好这个技术万一被抛弃,社区黄掉的风险,那若是大家公司有技术实力我以为用RocketMQ挺好的
Kafka:
1 提供超高的吞吐量,ms级的延迟,
2 极高的可用性以及可靠性,并且分布式能够任意扩展,同时kafka最好是支撑较少的topic数量便可,保证其超高吞吐量
3 kafka惟一的一点劣势是有可能消息重复消费,那么对数据准确性会形成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响能够忽略这个特性自然适合大数据实时计算以及日志收集。
Producer:Producer 即生产者,消息的产生者,是消息的入口
Broker:Broker 是 kafka 一个实例,每个服务器上有一个或多个 kafka 的实例,简单的理解就是一台 kafka 服务器,kafka cluster表示集群的意思
Topic:消息的主题,可以理解为消息队列,kafka的数据就保存在topic。在每个 broker 上都可以创建多个 topic 。
Partition:Topic的分区,每个 topic 可以有多个分区,分区的作用是做负载,提高 kafka 的吞吐量。同一个 topic 在不同的分区的数据是不重复的,partition 的表现形式就是一个一个的文件夹!
Replication:每一个分区都有多个副本,副本的作用是做备胎,主分区(Leader)会将数据同步到从分区(Follower)。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为 Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本
Message:每一条发送的消息主体。
Consumer:消费者,即消息的消费方,是消息的出口。
Consumer Group:我们可以将多个消费组组成一个消费者组,在 kafka 的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量!
Zookeeper:kafka 集群依赖 zookeeper 来保存集群的的元信息,来保证系统的可用性。
简而言之,kafka 本质就是一个消息系统,与大多数的消息系统一样,主要的特点如下:
使用推拉模型将生产者和消费者分离
为消息传递系统中的消息数据提供持久性,以允许多个消费者
提供高可用集群服务,主从模式,同时支持横向水平扩展
与 ActiveMQ、RabbitMQ、RocketMQ 不同的地方在于,它有一个分区Partition的概念。
这个分区的意思就是说,如果你创建的topic有5个分区,当你一次性向 kafka 中推 1000 条数据时,这 1000 条数据默认会分配到 5 个分区中,其中每个分区存储 200 条数据。
这样做的目的,就是方便消费者从不同的分区拉取数据,假如你启动 5 个线程同时拉取数据,每个线程拉取一个分区,消费速度会非常非常快!
分区器,我们发送的每条消息都会被发送到那个Broker上,路由的策略是什么?
Producer 我们需要关心的是消息如何负载到不同的分区上去。就是生产者分区的负载均衡策略。
分区(Partition)的作用就是提供了系统的负载均衡的能力,实现了系统的高伸缩性,系统的水平扩展能力很强
一些主流的中间件都用到了分区的概念,比如ElasticSearch,MongDB等等。
策略1 默认的策略就是,如果消息中传的有key就会按照对key求hash,进行路由
策略2 如果没传key就会按照轮询的策略负载消息
.ProducerRecord
含义: 发送给Kafka Broker的key/value 值对
.内部数据结构:
– Topic (名字)
– PartitionID ( 可选)
– Key[( 可选 )
– Value
提供三种构造函数形参:
– ProducerRecord(topic, partition, key, value)
– ProducerRecord(topic, key, value)
– ProducerRecord(topic, value)
RecordAccumulator
1 producer的一部分,在创建producer的时候初始化
2 一个deque对应一个topic-partition,当从partitioner拿到分配好partition的batch后,append到相应的deque中,没有对应的deque则去getOrCreateDeque()
3 从内存池拿内存,用完放回内存池
Sender线程
1.不断轮询缓冲区中达到要求的batch
2.按照Broker进行分类。
3.建立socket连接发送给不同的Broker
4.根据消息的回掉函数,进行响应。
NetworkClient
NetworkClient是kafka的网络层,也就是真正发生网络I/O的地方,是一个通用的网络客户端实现,不只用于生产者消息的发送,也用于消费者消费消息以及服务端Broker之间的通信。
selector的poll方法-NIO
同步发送:get()
同步发送 API
同步发送的意思就是,一条消息发送之后,会阻塞当前线程,直至返回 ack。由于 send 方法返回的是一个 Future 对象,根据 Futrue 对象的特点,我们也可以实现同 步发送的效果,只需在调用 Future 对象的 get 方发即可。
异步发送:callback()
带回调函数的 API
回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是 RecordMetadata 和 Exception,如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败。
注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。
KAFKA 集群的多个服务代理节点(节点,Broker)中,有一个 Broker 会被选举为控制器(Controller)。Controller 负责管理整个集群中所有分区和副本的状态变化,并时刻检测集群状态的变化,例如 Broker 故障、分区的 Leader 副本选举和分区重分配。每个 KAFKA 集群只有一个 Controller,以维护集群的单一一致视图。虽然 Controller 成为单点,但 KAFKA 有处理单点故障的机制。
初始化:
Controller 在选举成功后会读取 ZK 中各节点的数据来完成一系列的初始化工作,包括:
初始化上下文信息(从ZK读取ControllerEpoch、Topic、分区、副本相关的各种元数据信息)、
启动并管理分区和副本的状态机、
定义监听器触发事件、
定义周期性任务触发事件(如分区自平衡)。
不管是监听器触发的事件,还是周期性任务触发事件,都会读取或更新 Controller 中的上下文信息
1、读取 /controller_epoch 的值到 ControllerContext(上下文信息)
readControllerEpochFromZooKeeper()
2、递增 ControllerEpoch,并更新到 /controller_epoch
incrementControllerEpoch()
3、注册 ZK 监听器触发事件(逻辑上基于事件队列模型实现,并非真正在ZK上增加Watcher)
// - Broker 相关变化的事件处理器
// - Topic 相关变化的事件处理器
// - ISR 集合变更的事件处理器
// - 优先副本选举的事件处理器
// - 分区重分配的事件处理器
4、初始化上下文信息,ControllerContext用于维护Topic、分区和副本相关的各种元数据,
// 读取 /brokers/ids,初始化可用 Broker 集合
controllerContext.liveBrokers = zkClient.getAllBrokersInCluster.toSet
// 读取 /broker/topics,初始化集群中所有 Topic 集合
controllerContext.allTopics = zkClient.getAllTopicsInCluster.toSet
// 所有 Topic 对应ZK中 /brokers/topics/ 节点添加PartitionModificationsHandler,用于监听Topic中的分区分配变化
registerPartitionModificationsHandlers(controllerContext.allTopics.toSeq)
// 读取 /broker/topics//partitions,初始化每个Topic每个分区的AR集合
controllerContext.partitionReplicaAssignment = mutable.Map.empty ++
zkClient.getReplicaAssignmentForTopics(controllerContext.allTopics.toSet)
controllerContext.partitionLeadershipInfo = new mutable.HashMap[TopicPartition, LeaderIsrAndControllerEpoch]
controllerContext.shuttingDownBrokerIds = mutable.Set.empty[Int]
// 所有可用 Broker 对应ZK中 /brokers/ids/ 节点添加BrokerModificationsHandler,用于监听Broker增减的变化
registerBrokerModificationsHandler(controllerContext.liveBrokers.map(_.id))
// 初始化每个分区的 Leader、ISR 集合等信息
updateLeaderAndIsrCache()
// 启动 ControllerChannelManager
startChannelManager()
// 读取 /admin/reassign_partitions,初始化需要进行副本重分配的分区
initializePartitionReassignment()
//controller:存放 Controller 的信息。记录 Controller 所在的 brokerid,节点类型是临时节点,与ZK会话失效时自动删除。ControllerChangeHandler 监听 Controller 的变更事件;
/brokers/ids:存放集群中所有可用的 Broker。BrokerChangeHandler 负责处理 Broker 的增减事件;
/brokers/topics:存放集群中所有的 Topic。TopicChangeHandler 负责处理 Topic 的增减事件;
/admin/delete_topics:存放删除的 Topic。TopicDeletionHandler 负责处理 Topic 的删除事件;
/brokers/topics//partitions:存放 Topic 的分区。PartitionModificationsHandler 负责处理分区分配事件;
/brokers/topics//partitions//state:存放的是分区的 Leader 副本和 ISR 集合信息。PartitionReassignmentIsrChangeHandler 负责在副本分配到分区时,需要等待新副本追上Leader 副本后才能执行后续操作;
/admin/reassign_partitions:存放重新分配AR的路径,通过命令修改AR时会写入此路径。PartitionReassignmentHandler 负责执行分区重新分配;
/admin/preferred_replica_election:存放需要选举副本 Leader 的分区信息。PreferredReplicaElectionHandler 负责对指定分区进行有限副本选举。
Controller 的选举流程
选举 Controller:
Kafka 节点启动,加入集群会进行以下两个操作
第一步,通常会成功,如果失败,就说明 Kafka 集群中有两个节点的 broker.id 设置冲突了,修改 server.properties 配置文件中的 broker.id 配置。
第二步,将当前节点注册为集群的控制器,/controller 节点是一个临时节点,如果该节点不存在,则创建成功,如果该节点已存在,则创建失败,并创建一个 Watch,一旦该节点被删除,所有注册 Watch 的节点就会尝试重新创建 /controller 节点,当然只有一个会成功。
case object Startup extends ControllerEvent {
...
override def process(): Unit = {
// 1、/controller 节点添加ControllerChangeHandler,监听Controller的变更事件
zkClient.registerZNodeChangeHandlerAndCheckExistence(controllerChangeHandler)
// 2、选举Controller
elect()
}
当broker0故障,超时没有收到心跳,controller零食节点则会被删除
其他watch到零食家电变化们就会争抢注册临时节点,成功就将controller epoch+1,读取上下文context,初始化工作
KafkaController 触发选举的时机有三个:
脑裂问题:
短暂的时间内可能由于 Leader 的 GC 时间过长或者 ZooKeeper 节点间网络抖动导致心跳超时(Leader 假死),ZooKeeper 与 Leader 的会话超时随后 ZooKeeper 通知剩余的 Follower 重新竞选,Follower 中就有一个成为了“新Leader”,但“旧Leader”并未故障,此时可能有一部分的客户端已收到 ZooKeeper 的通知并连接到“新Leader”,有一部分客户端仍然连接在“旧Leader”,如果同时两个客户端分别对新旧 Leader 的同一个数据进行更新,就会出现很严重问题。
为来解决脑力额问题,kafka增加了“控制器纪元”(ZooKeeper 中是持久节点,路径是 /controller_epoch),Integer 类型,用于跟踪并记录 Controller 发生变更的次数,表示当前的 Controller 是第几代控制器。controller_epoch 的初始值为 1,当 Controller 发生变更,每选出一个新的 Controller 就将该字段自增。每个和 Controller 交互的请求都会携带
controller_epoch 字段:
若请求的 controller_epoch 值小于内存中的 epoch 值,则认为这个请求是发送给已过期的 Controller,那么这个请求会被认为是无效;
若请求的 controller_epoch 值大于内存中的 epoch 值,则说明已经有新的 Controller 当选;旧的controller就要退位,
由此可见,KAFKA 通过 controller_epoch 来保证 Controller 的唯一性;
ocntroller相关代码
1 Broker注册
Broker是分布式部署并且相互之间相互独立,但是需要有一个注册中心对整个集群的Broker进行管理,此时就使用了Zookeeper。在Zookeeper上会有一个专门用来记录Broker服务器列表的节点:/brokers/ids
每个Broker在启动时,都会在Zookeeper上进行注册,即到**/brokers/ids下创建属于自己的节点,如/brokers/ids/[0…N]。**
Kafka使用了全局唯一的数字来指代每个Broker服务器,不同的Broker必须使用不同的Broker ID进行注册,创建完节点后,每个Broker就会将自己的IP地址和端口信息记录到该节点中去
2 Topic注册
在kafka中,用户可以自定义多个topic,每个topic又被划分为多个分区,每个分区存储在一个独立的broker上。这些分区信息及与Broker的对应关系都是由Zookeeper进行维护
3 生产者负载均衡
同一个Topic消息会被分区并将其分布在多个Broker上。由于每个Broker启动时,都会在Zookeeper上进行注册,生产者会通过该节点的变化来动态地感知到Broker服务器列表的变更,这样就可以实现动态的负载均衡
4 消费者负载均衡
与生产者类似,Kafka中的消费者同样需要进行负载均衡来实现多个消费者合理地从对应的Broker服务器上接收消息,每个消费者分组包含若干消费者,每条消息都只会发送给分组中的一个消费者,不同的消费者分组消费自己特定的Topic下面的消息,互不干扰。
每个消费者都需要关注所属消费者分组中其他消费者服务器的变化情况,即对/consumers/[group_id]/ids节点注册子节点变化的Watcher监听,一旦发现消费者新增或减少,就触发消费者的负载均衡。还对Broker服务器变化注册监听。消费者需要对/broker/ids/[0-N]中的节点进行监听,如果发现Broker服务器列表发生变化,那么就根据具体情况来决定是否需要进行消费者负载均衡。
5 分区与消费者的关系
消费者组 Consumer group 下有多个 Consumer(消费者)。
对于每个消费者组 (Consumer Group),Kafka都会为其分配一个全局唯一的Group ID,Group 内部的所有消费者共享该 ID。订阅的topic下的每个分区只能分配给某个 group 下的一个consumer(当然该分区还可以被分配给其他group)。同时,Kafka为每个消费者分配一个Consumer ID。
在Kafka中,规定了每个消息分区 只能被同组的一个消费者进行消费,因此,需要在 Zookeeper 上记录 消息分区 与 Consumer 之间的关系,每个消费者一旦确定了对一个消息分区的消费权力,需要将其Consumer ID 写入到 Zookeeper 对应消息分区的临时节点上,例如:
/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]
其中,**[broker_id-partition_id]**就是一个 消息分区 的标识,节点内容就是该消息分区上消费者的Consumer ID。
、
6 记录消息消费的进度Offset
在消费者对指定消息分区进行消息消费的过程中,需要定时地将分区消息的消费进度Offset记录到Zookeeper上,以便在该消费者进行重启或者其他消费者重新接管该消息分区的消息消费后,能够从之前的进度开始继续进行消息消费。Offset在Zookeeper中由一个专门节点进行记录,其节点路径为:
/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]
节点内容就是Offset的值。
7 Consumer注册
注册新的消费者分组
当新的消费者组注册到zookeeper中时,zookeeper会创建专用的节点来保存相关信息,其节点路径为 /consumers/{group_id},其节点下有三个子节点,分别为**[ids, owners, offsets]**。
ids节点:记录该消费组中当前正在消费的消费者;
owners节点:记录该消费组消费的topic信息;
offsets节点:记录每个topic的每个分区的offset;
8 辅助leader选举
ISR中先向controller注册的的foller赢得选举
1 启动–在zookeeper中注册
2选举leader
3 controller与zookeeper同步broker节点信息 state
4接收消息,应答消息 ACKs
5 leader-follower同步消息 HW LEO
6 消息实体是segment文件,包含log文件和index索引文件
所谓协调者,在 Kafka 中对应的术语是 Coordinator,它专门为 Consumer Group 服务,负责Group Rebalance 以及提供位移管理和组成员管理等。
Consumer 端应用程序在提交位移时,其实是向 Coordinator 所在的 Broker 提交位移。同样地,当 Consumer 应用启动时,也是向 Coordinator 所在的 Broker 发送各种请求,然后由 Coordinator 负责执行消费者组的注册、成员管理记录等元数据管理操作
所有 Broker 在启动时,都会创建和开启相应的 Coordinator 组件。也就是说,所有 Broker 都有各自的 Coordinator 组件。那么,Consumer Group 如何确定为它服务的 Coordinator 在哪台 Broker 上呢
不同的group id会被哈希到不同的分区上,从而不同的broker能充当不同group的Coordinator
1 Coordinator的确定与分区分配
前面我们说到一个问题,那就是一个group内部,1个parition只能被1个consumer消费,其实看到这里我们就知道应该有这样一个组件来负责partition的分配,而且前面学习消费者组机制的时候还提到过分区的三种分配策略。
对于每一个consumer group,Kafka集群为其从broker集群中选择一个broker作为其coordinator。因此,第1步就是找到这个coordinator。也就是说1个consumer group对应一个coordinattor
1)GroupCoordinatorRequest 请求
第 1 步:确定由位移主题的哪个分区来保存该 Group 数据:
partitionId=Math.abs(groupId.hashCode() % offsetsTopicPartitionCount)。
第 2 步:找出该分区 Leader 副本所在的 Broker,该 Broker 即为对应的 Coordinator。
其实这个过程是发送了一个GroupCoordinatorRequest定位请求去寻找coordinator
首先,Kafka 会计算该 Group 的 group.id 参数的哈希值。比如你有个 Group 的 group.id 设置成了“test-group”,那么它的 hashCode 值就应该是 627841412。其次,Kafka 会计算 __consumer_offsets 的分区数,通常是 50 个分区,之后将刚才那个哈希值对分区数进行取模加求绝对值计算,即 abs(627841412 % 50) = 12。
此时,我们就知道了位移主题的分区 12 负责保存这个 Group 的数据。有了分区号,算法的第 2 步就变得很简单了,我们只需要找出位移主题分区 12 的 Leader 副本在哪个 Broker 上就可以了。
这个 Broker,就是我们要找的 Coordinator
2)JoinGroup
所有consumer都往coordinator发送JoinGroup消息之后,coordinator会指定其中一个consumer作为leader,并把组成员信息以及订阅信息发给leader
其他consumer作为follower,然后由这个leader进行partition分配
3)SyncGroup
leader开始分配消费方案,即哪个consumer负责消费哪些topic的哪些partition
一旦完成分配,leader会将这个方案封装进SyncGroup请求中发给Coordinator,Coordinator给它返回null
follower发送 null的 SyncGroupRequest 给Coordinator,Coordinator回给它partition分配的结果。
2 coordinator的类
1)handleFindCoordinatorRequest
这里先判断这个请求是处理事务的还是消费者组的
后面就是和我们前面介绍的就一样了(groupCoordinator.partitionFor(findCoordinatorRequest.data.key), GROUP_METADATA_TOPIC_NAME 方法里面计算出了partition信息,其实就是那个partition,这个方法的实现的话,大致如下
Utils.abs(groupId.hashCode) % groupMetadataTopicPartitionCount
找到了partition之后就开始找这个partition的lead 分区了,也就是我们下面图上的第二段代码。这样我们的Coordinator节点就找到了
2)handleJoinGroupRequest
下面这个代码其实就比较简单了,其实就是一个consumer 注册的过程,调用了groupCoordinator.handleJoinGroup() 的方法,将我们的consumer加入到我们的消费组里面去了
同时将确定了consumer group 的leader consumer,然后返回给了客户端,也就是我们的sendResponseCallback 方法。
3)handleSyncGroupRequest
下面就是comsumer 同步分区的分配信息,最后返回给客户端的assignmentMap,就是分配的结果
4)handleOffsetCommitRequest
这个就是处理consumer 客户端提交offset 的方法了,这个代码有点长,我这里就帖几处比较重要的地方了
这里就是获取我们提交的offset 信息了
所有的consumer线程要先向coordinator注册,由coordinator选出leader, 然后由leader来分配state。 从group memeber里选出来一个做为leader,由leader来执行性能开销大的协调任务, 这样把负载分配到client端,可以减轻broker的压力,支持更多数量的消费组。
所有group member(指的是consumer线程)都需要发心跳给coordinator,这样coordinator才能确定group的成员。
对于Kafka consumer,它的实际上必须跟coordinator保持连接,因为它还需要提交offset给coordinator。所以coordinator实际上负责commit offset,那么,即使leader来确定状态的分配,但是每个partition的消费起始点,还需要coordinator来确定。
3.Consumer消费者的工作过程:
4 . Coordinator协调节点的工作过程
在分布式系统中,由于网络问题你不清楚没接收到心跳,是因为对方真正挂了还是只是因为负载过重没来得及发生心跳或是网络堵塞。所以一般会约定一个时间,超时即判定对方挂了。而在kafka消费者场景中,session.timout.ms参数就是规定这个超时时间是多少。
还有一个参数,heartbeat.interval.ms,这个参数控制发送心跳的频率,频率越高越不容易被误判,但也会消耗更多资源。
此外,还有最后一个参数,max.poll.interval.ms,消费者poll数据后,需要一些处理,再进行拉取。如果两次拉取时间间隔超过这个参数设置的值,那么消费者就会被踢出消费者组。也就是说,拉取,然后处理,这个处理的时间不能超过 max.poll.interval.ms 这个参数的值。这个参数的默认值是5分钟,而如果消费者接收到数据后会执行耗时的操作,则应该将其设置得大一些。
三个参数:
这里给出一个相对较为合理的配置,如下:
session.timout.ms:设置为6s
heartbeat.interval.ms:设置2s
max.poll.interval.ms:推荐为消费者处理消息最长耗时再加1分钟
5.Coordinator存储的信息有什么
对于每个Consumer Group,Coordinator会存储以下信息:
对每个存在的topic,可以有多个消费组group订阅同一个topic(对应消息系统中的广播)
对每个Consumer Group,元数据如下:
订阅的topics列表
Consumer Group配置信息,包括session timeout等
组中每个Consumer的元数据。包括主机名,consumer id
每个正在消费的topic partition的当前offsets
Partition的ownership元数据,包括consumer消费的partitions映射关系
独立消费者,没有规则限制,一对多 多对一
以组去消费pantition的时候,只能由一个group中的一个consumer消费一个partition
消费一条消息后,提交offset到对应的topic
2 ConsumerNetWorkClient 提交send到Broker,有数据回调OnSuccess抓取数据
3 Consumer从completedFetch的队列获取数据,进行反序列化以及本地化处理
创建一个topic为second,16个partition,3个副本
kafuka的一条消息 即一个record
HW(High Watermark):俗称高水位,它标识了一个特定的消息偏移量(offset),消费者只能拉取到这个 offset 之前的消息。分区 ISR 集合中的每个副本都会维护自身的 LEO(Log End Offset):俗称日志末端位移,而 ISR 集合中最小的 LEO 即为分区的 HW,对消费者而言只能消费 HW 之前的消息。
LEO、HW流程
1.数据写到leader的partition上
2.leader更新自己的leo,
3.follower带上自己的leo,
4.leader更新follower列表每个follower对应的leo,即remot leo
5.尝试更新ISR
6.取follower的leo列表最小值做hw并尝试更新,
7.将hw返回follower,
8.follower更新自己的leo
9.follower取自己的hw和leader返回的hw做对比取最小值更新。
查看Kafka的broker配置
log.flush.interval.messages,默认值为9223372036854775807,该参数的含义是刷新进磁盘累计的消息个数阈值。
log.flush.interval.ms的默认值为null,表示多长时间间隔把内存中的消息刷新进磁盘,如果没有设置,则使用
log.flush.scheduler.interval.ms ,这个参数的默认值9223372036854775807,含义是检查日志被刷新进磁盘的频率。
从上述三个参数及默认值来看,如果遵从这些配置,如果发生broker宕机或者服务crash,则会丢失大量数据,观察实际环境显然不是这样的。那kafka日志时如何刷盘的呢?
首先我们先来了解一下kafka日志的结构:每个topic的partition对应一个broker上一个目录,目录中的文件以日志的大小(log.segment.bytes)和时间(log.roll.hours)来roll。我们看到的kafka v0.10.2的日志文件包括三个部分,分别是
xxxxxxxxxxxxxxxxxxxx.index、
xxxxxxxxxxxxxxxxxxxx.log
xxxxxxxxxxxxxxxxxxxx.timeindex,
其中xxxxxxxxxxxxxxxxxxxx代表的是offset,20位,从0开始。Kafka没有采用uuid的形式,为每个message分配一个message.id,而是通过offset来标记message,offset并不是消息在文件中的物理编号,而是一个逻辑编号,通过追加方式,每次加1。那通过offset如何查找消息的呢?
通过offset查找文件位置,分为3步:
1)对xxxxxxxxxxxxxxxxxxxx.log进行排序,通过二分查找,得到所在的xxxxxxxxxxxxxxxxxxxx.log文件;
2) 对应的xxxxxxxxxxxxxxxxxxxx.index文件,通过二分查找,找到对应的条目,也就是offset到position的映射;
3)拿到这个position,就可直接定位xxxxxxxxxxxxxxxxxxxx.log的位置。然后从这个位置顺序扫描,得到实际的消息。
这里index文件的目的,就是为了加快查找速度。如果没有index文件,通过扫描log文件,从头扫描,也可以找到位置。
一条消息在磁盘的存储格式如下:
offset : 8 bytes
message length: 4 bytes (value: 4 + 1 + 1 + 8(if magic value > 0) + 4 + K + 4 + V)
crc : 4 bytes
magicvalue : 1 byte
attributes : 1 byte
timestamp : 8 bytes (Only exists when magic valueis greater than zero)
key length : 4 bytes
key : K bytes
valuelength : 4 bytes
value : V bytes
在Linux系统中,当我们把数据写入文件系统之后,其实数据在操作系统的pagecache里面,并没有刷到磁盘上。如果操作系统挂了,数据就丢失了。一方面,应用程序可以调用fsync这个系统调用来强制刷盘,另一方面,操作系统有后台线程,定时刷盘。频繁调用fsync会影响性能,需要在性能和可靠性之间进行权衡。实际上,官方不建议通过上述的三个参数来强制写盘,认为数据的可靠性通过replica来保证,而强制flush数据到磁盘会对整体性能产生影响。
所谓的幂等,简单就是说对接口的多次调用所产生的结果和调用一次是一样的。生产者在进行重试的时候有可能会重复写入消息,而使用kafka的幂等性功能之后就可以避免这种情况
开启幂等性的方法,设置生产者客户端参数
enable.idempotence=true
序列号
为了实现生产者的幂等性,kafka引入了produce id和序列号(sequence number)这两个概念。每个新的生产者在实例初始化的时候都会被分配一个PID,这个PID对用户完全透明。对于每个PID,消息发送到的每一个分区都有对应的序列号,这些序列号从0开始单调递增。生产者每发送一条消息就会将
broker端会在内存中为每一对
引入序列号来实现幂等性也只是针对每一对
幂等性并不能跨越多个分区运作,而事务可以弥补这个缺陷。事务可以保证对多个分区写入操作的原子性。操作原子性指多个操作要么成功要么失败,不存在部分成功,部分失败的可能。
对流式应用而言, 一个典型的应用模式为consumer-transformer-produce。在这种模式下的消费者和生产者并存:应用程序从某个主题中消费消息,然后经过一系列转换后写入另外一个主题,消费者可能在提交消费位移的过程中出现问题而导致重复消费,也有可能生产者重复生产消息。kafka的事务可以使用应用程序将消息消息、生产消息、提交消息位移当做原子操作来处理,同时成功或失败,即使该生产或消费位移会跨多个分区。
为了实现事务,应用程序必须提供一个唯一的transcationalId,这个事务id通过客户端参数来显示设置,如下
properties.put(“transactional.id”,“transcationId”)
事务要求生产者开启幂等特性,因此通过将transactional.id 参数设置为非空从而开启事物特性的同时需要将****enable.idempotence为true
transactionalId与PID 一一对应,两者之间不同的是,transactionalId 是用户显式设置的,而PID是由kafka内部分配的。另外,为了保证新的生产者启动后具有相同的transactionalId的旧生产者能够立即失效,每个生产者通过transactionalId获取PID的同时,还会获取一个单调递增的producer epoch。
如果使用同一个transactionalId开启两个生产者,那么前一个开启的生产者会报错。
从生产者的角度分析,通过事务,kafka可以保证跨生产者会话的消息幂等发送,以及跨生产者会话的事务恢复。前者表示具有相同的transactionalId的新生产者实例被创建且工作的时候,旧的且拥有相同transactionalId的生产者实例将不能工作。后者指当某个生产者实例宕机后,新的生产者实例可以保证任何未完成的旧事务要么被提交,要么被终止。如此可以是新的生产者实例从一个正常的状态开始工作。
而从消费者的角度分析,事务能保证的语义相对偏弱。处于以下原因,kafka并不能保证已经提交的事务中的所有消息都能被消费:
对采用日志压缩策略的topic而言,事务中的某些消息有可能被清理
事务中的消息可能分布在同一个分区的多个日志分段中,当老的日志分段被删除中,对应的消息可能会丢失。
消费者可以通过seek方法访问任意offset的消息,从而可能遗漏事务中的部分消息
消费者在消费者时可能没有分配到事务内的所有分区,如此它也就不能读取事务中的所有消息
Kafka producer关于事务的5个方法
initTransactions()
beginTransaction()
sendOffsetsToTransaction()
commitTransaction()
abortTransaction()
事务协调器(TransactionCoordinator)
为了实现事务的功能,Kafka还引入了事务协调器来负责处理事务,这一点类比组协调器(GroupCoordinator)。每一个生产者都会被指派一个特定的TransactionCoordinator来负责实施的。TransactionCoordinator会将事务状态持久化到内部主题__transaction_state中
事务实现过程原理
1)查找TransactionCoordinator
TransactionCoordinator负责分配PID和管理事务,因此生产者要做的第一件事情就是找出对应的TransactionCoordinator所在的 broker节点。和查找GroupCoordinator节点一样,也是通过发送FindCoordinatorRequest请求来实现的。
kafka在收到FindCoordinatorRequest后,返回TransactionCoordinator节点的node_id,host和port。具体查找方式是根据transactionalId的哈希值计算主题__transaction_state的分区编号
2)获取PID
在找到TransactionCoordinator节点后,就需要为当前的生产者分配一个PID了,凡是开启了幂等性功能的生产者都必须执行这个操作,不需要考虑该生产者是否还开启了事务。生产者节点请求协调器发送一个PID
3)保存PID
生产者的请求会被发送给TransactionCoordinator,如果没有开启事物而只开启了幂等性,那么请求可以发送给任意的broker。当TransactionCoordinator第一次收到包含该transcationalId的请求,它会把transcationalId和对应的PID以消息的形式保存到topic__transaction_state中,这样可以保证transaction_id,pID的对应关系被持久化,保证即使TransactionCoordinator宕机也不会丢失。
总结:
高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒
可扩展性:kafka集群支持热扩展
持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
容错性:允许集群中节点故障(若副本数量为n,则允许n-1个节点故障)
高并发:支持数千个客户端同时读写
日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer
消息系统:解耦生产者和消费者、缓存消息等
用户活动跟踪:kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后消费者通过订阅这些topic来做实时的监控分析,亦可保存到数据库
运营指标:kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告
流式处理:比如spark streaming和storm;
传统的随机读写,需要磁头不断的寻址,读到数据再返回给内存
顺序写-基于硬盘存储,数据堆积量大,kafka的写入是append操作,partion是有序的,而且是批量操作
顺序读-连续的按顺序读,每次读多页,放在内存中,所以实际看起来就像从内盘读取一样快速
用户态进程想要访问内核态的磁盘或网卡数据,需要切换到内核态,内核从内核空间读取数据后copy到用户空间,
kafka从磁盘读到数据,直接把数据给内核读缓冲区,缓冲区将数据发到网卡,
Kafka将一个分区的文件是按照片段来存储的,一个片段的默认大小为1GB,可以在server.properties配置文件中修改片段大小,并且同时维护了index索引文件。
Partition:topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列
Segment:partition物理上由多个segment组成,每个Segment存着message信息
这种partition-segment的分区分段,在并发的时候可以分段加锁,提高的效率
offset:表示的是相对于该分区的记录偏移量,指的是第几条记录,比如0代表第一条记录。
position:表示该记录相对于当前片段文件的偏移量。
CreateTime:记录创建的时间。
isvalid:记录是否有效。
keysize:表示key的长度。
valuesize:表示value的长度
magic:表示本次发布kafka服务程序协议版本号。
compresscodec:压缩工具。
producerId:生产者ID(用于幂等机制)。
sequence:消息的序列号(用于幂等机制)。
payload:表示具体的消息
批量压缩,降低传输消耗
java写入磁盘,需要先创建对象,通过JVM序列化的写入磁盘
kafka避开了java的JVM,直接写入磁盘的page,避免了对象的创建,gc的耗时,读写速度高,进程重启也不会丢缓存
Kafka Broker 集群接收到数据后会将数据进行持久化存储到磁盘,为了提高吞吐量和性能,采用的是「异步批量刷盘的策略」,也就是说按照一定的消息量和间隔时间进行刷盘。首先会将数据存储到 「PageCache」 中,至于什么时候将 Cache 中的数据刷盘是由「操作系统」根据自己的策略决定或者调用 fsync 命令进行强制刷盘,
kafak生产一条消息后,没有直接发给broker,而是先缓存起来,返回给业务ok,待消息达到一定数量,再批量发送给broker,
好处:减少网络Io,提高吞吐率
坏处:如果生产者宕机,可能造成消息丢失,牺牲可靠性
首先需要保证本地事务的原子性
比较容易出现笑死丢失的四个环节:
1、主线程KafkaProducer创建消息,然后通过生产者拦截器
2、生产者拦截器对消息的key ,value做一定的处理,交给序列化器,
3、序列化器对消息key和vlue做序列化处理,然后给分区器
4、分区器给消息分配分区、并发送给消息收集器
5、一条ProducerRecord,添加到RecordAccumulator,首先会根据分区确定对应分区所在的双端队列,在双端队列获取尾部的一个ProducerBatch对象,查看该ProducerBatch是否可以写入该ProducerRecord消息,如果可以则写入,不能的话,会在双端队列末尾在创建一个ProducerBatch对象,创建时会评估这条消息是否是否超过batch.size参数的大小。如果不超过,就以batch.size参数的大小来创建ProducerBatch对象。通过sender线程发送消息
6、sender线程获取RecordAccumulator中的消息,需要将原本的<分区,Deque>形式再次封装成
7、sender线程发往Kafka之前还会保存到InFlightRequest中,InFlightRequest保存对象的具体形式为Map
8、sender将Request交给Selector准备发送。
9、Selector将Request发送到对应的kafka节点(Broker)。
10、Selector相应结果反馈给InFlightRequest。
11、主线程清理RecordAccumulator已经发送完毕的消息。
kafka的ack机制默认设置为ack=1,这就可能造成消息丢失
ack机制:
acks = 0:由于发送后就自认为发送成功,这时如果发生网络抖动, Producer 端并不会校验 ACK 自然也就丢了,且无法重试。
acks = 1:消息发送 Leader Parition 接收成功就表示发送成功,这时只要 Leader Partition 不 Crash 掉,就可以保证 Leader Partition 不丢数据,但是如果 Leader Partition 异常 Crash 掉了, Follower Partition 还未同步完数据且没有 ACK,这时就会丢数据。
acks = -1 或者 all: 消息发送需要等待 ISR 中 Leader Partition 和 所有的 Follower Partition 都确认收到消息才算发送成功, 可靠性最高, 但也不能保证不丢数据,比如当 ISR 中只剩下 Leader Partition 了, 这样就变成 acks = 1 的情况了。
选举机制:
AR-分区中的所有副本统称为 AR (Assigned Replicas)
ISR-所有与leader副本保持一定程度同步的副本(包括leader副本在内)组成 ISR (In Sync Replicas)。
OSR-于leader副本同步滞后过多的副本(不包括leader副本)将组成 OSR (Out-of-Sync Replied)
解决:
1 ack=-1/all
2 Leader Partition选举, 从ISR选,不从OSR选
3 重试次数 retries:>0, 这样Producer 端就会一直进行重试直到 Broker 端返回 ACK 标识
同步副本数min.insync.replices>1
Kafka Broker 集群接收到数据后会将数据进行持久化存储到磁盘,为了提高吞吐量和性能,采用的是「异步批量刷盘的策略」,也就是说按照一定的消息量和间隔时间进行刷盘。首先会将数据存储到 「PageCache」 中,至于什么时候将 Cache 中的数据刷盘是由「操作系统」根据自己的策略决定或者调用 fsync 命令进行强制刷盘,如果此时 Broker 宕机 Crash 掉,且选举了一个落后 Leader Partition 很多的 Follower Partition 成为新的 Leader Partition,那么落后的消息数据就会丢失。
既然 Broker 端消息存储是通过异步批量刷盘的,那么这里就可能会丢数据的!!!
由于 Kafka 中并没有提供「同步刷盘」的方式,所以说从单个 Broker 来看还是很有可能丢失数据的。
kafka 通过「多 Partition (分区)多 Replica(副本)机制」已经可以最大限度的保证数据不丢失,如果数据已经写入 PageCache 中但是还没来得及刷写到磁盘,此时如果所在 Broker 突然宕机挂掉或者停电,极端情况还是会造成数据丢失。
解决:保证消息落到磁盘,受动调用fsync,减小刷盘间隔
Consumer的工作可大致理解为两部分:
1 获取元数据并从 Kafka Broker 集群拉取数据。
2 处理消息,并标记消息已经被消费,提交 Offset 记录。
解决:
1因此正确的做法是:拉取数据、业务逻辑处理、提交消费 Offset 位移信息。
2 我们还需要设置参数 enable.auto.commit = false, 采用手动提交位移的方式
3 另外对于消费消息重复的情况,业务自己保证幂等性, 保证只成功消费一次即可–(sequence序列号?)
分布式事务:业务相关的多个操作,保证他们同时成功或者同时失败
最终一致性:与之对应的就是强一致性
MQ中要保证事务的最终一致性,就需要做到两点
1.生产者要保证100%的消息投递。事务消息机制
2.消费者这一端需要保证幂等消费。唯一ID+业务自己实现的幂等
分布式MQ的三种语义:
at least once
at most once
exactly once:
exactly-once:即使producer重试发送消息,消息也会保证最多一次地传递给最终consumer。该语义是最理想的,但也难以实现,这是因为它需要消息系统本身与生产和消费消息的应用程序进行协作。例如如果在消费消息成功后,将Kafka consumer的偏移量rollback,我们将会再次从该偏移量开始接收消息。这表明消息传递系统和客户端应用程序必须配合调整才能实现excactly-once
at-least-once:如果producer收到来自Kafka broker的确认(ack)或者acks = all,则表示该消息已经写入到Kafka。但如果producer ack超时或收到错误,则可能会重试发送消息,客户端会认为该消息未写入Kafka。如果broker在发送Ack之前失败,但在消息成功写入Kafka之后,此重试将导致该消息被写入两次,因此消息会被不止一次地传递给最终consumer,这种策略可能导致重复的工作和不正确的结
at least once 最少一次,消息不会丢失,但是可能会重复
exactly once 恰好一次。消息有且仅会被传输一次
RocketMQ并不能保证exactly once。商业版本当中提供了exactly once的实现机制。
Kafka:在最新版本的源码当中,提供了exactly once的demo。
RabbitMQ:erlang天生就成为了一种屏障