目录
RabbitMQ和Kafka的区别
- RabbitMQ是高并发的。
- Kafka是高吞吐的,通过Kafka的高吞吐量可以在短时间内收集大量的数据。
- Kafka底层使用了零拷贝技术,所以Kafka处理消息的速度是非常快的。
RabbitMQ和Kafka的Ack
- RabbitMQ的Ack表示的是消费端的应答。
- Kafka的Ack表示的是生产者的应答。
应用场景
- Kafka可以用来做分布式架构的日志收集。
- 比如日志写入不需要实时性太高,只需要过几分钟把日志写入到数据库里面就行了,这个时候就需要 Kafka的高吞吐来记录日志。
- 某些公司用Kafka在微服务中做发布订阅,是因为Kafka的吞吐量非常高,而且底层使用了零拷贝技术, 处理消息的速度是非常快的。
ZooKeeper
是什么
- ZooKeeper是一个协调式的数据库,在Kafka中充当调节中枢,Kafka的集群就是由ZooKeeper来进行 管理。
- ZooKeeper可以管理集群的节点信息,从而进行Master的选举,还可以利用集群中的有序节点特性, 来实现Master选举。
- Kafka就是通过 ZooKeeper来实现集群节点的主从选举。
- 总的来说,ZooKeeper 就是分布式数据的一致性解决方案,为分布式应用提供高性能,高可用的分布 式协调服务。
- 它的底层是通过基于Paxos算法演化而来的ZAB协议实现的。
如何实现Leader选举
- ZooKeeper集群节点由三种角色组成,分别是Leader,Follower,Observer。
- Leader负责所有事务请求的处理,以及Kafka过半提交时发起的投票和决策。
- Follower负责接收客户端的非事务请求,另外Follower节点还会参与Leader的选举投票。
- Observer也是负责接收客户端的非事务请求,但是Observer节点不会参与任何选举投票,只是为了扩 展ZooKeeper集群来分担读操作的压力。
- 其次ZooKeeper集群是一种典型的中心化架构,也就是会有一个Leader作为决策节点,专门负责事务 请求的处理和数据的同步。
- 这种架构的好处是可以减少集群架构里面数据同步的复杂度,集群管理会更加简单和稳定。
- 但是会带来Leader选举的一个问题,也就是说,如果Leader节点宕机了,为了保证集群继续提供可靠 的服务,ZooKeeper需要从剩下的Follower节点里面去选举一个新的节点作为Leader节点,也就是 Leader选举。
- 具体的实现是,每一个节点都会向集群里面的其他节点发送一个票据Vote。
- 这个票主要据包括epoch,zxid,myid三种属性。
- epoch是逻辑时钟,用来表示当前票据是否过期。
- zxid是事务id,表示当前节点最新数据的事务编号。
- myid是服务器id,就是在myid文件里面填写的数字。
- 每个节点都会选自己当Leader,所以第一次投票的时候携带的是当前节点的信息。
- 接下来每个节点用收到的票据和自己的节点票据做比较,根据epoch,zxid,myid的顺序逐一比较,以 值最大的一方获胜。
- 比较结束以后这个节点下次再投票的时候,发送的投票请求就是获胜的票据信息。
- 经过了多轮投票以后,每个节点都会去统计当前达成一致的票据,以少数服从多数的方式,最终获得 票据最多的节点成为Leader。
- 选择epoch,zxid,myid作为投票评判依据的原因,我是这么理解的。
- 选择epoch是因为网络通信可能会有延迟,有可能在新一轮的投票里面收到上一轮投票的票据,这种 数据应该丢弃,否则会影响投票的结果和效率。
- 选择zxid是因为zxid越大,就说明这个节点的数据越接近Leader,所以用zxid做判断条件是为了避免 数据丢失的问题。
- 选择myid是因为myid能避免投票的时间过长,直接用myid的最大值来快速选择投票的结果。
数据存储原理
Topic
- 在Kafka中,用来存储消息的队列叫做Topic,是一个逻辑概念,可以理解为一组消息的集合。
- 生产者和Topic以及Topic和消费者的关系都是多对多。一个生产者可以发送消息到多个Topic,一个 消费者也可以从多个Topic获取消息(但是不建议这么做)。
- 生产者发送消息时,如果Topic不存在,Kafka 默认会自动创建。
Partition
- 首先,Kafka为了实现横向扩展,它会把不同的数据存放在不同的Broker上。
- 同时为了降低单台服务器的访问压力,把一个Topic中的数据分隔成多个Partition。在服务器上,每个 Partition都有一个物理目录,Topic名字后面的数字标号即代表分区。比如创建一个名为myTopic的主 题,假设数据目录被分布到了3台机器。
- myTopic-0表示分区A节点,myTopic-1表示分区B节点,myTopic-2表示分区C节点。
Replication
- Kafa为了提高分区的可靠性,又设计了副本机制。
- 我们创建Topic的时候,通过指定replication-factor副本因子,来确定Topic 的副本数。
- 副本因子数必须小于等于节点数,否则会报错。这样就可以保证,绝对不会有一个分区的两个副本分 布在同一个节点上,不然副本机制也失去了备份的意义。
- 假设现在创建了一个3个分区3个副本,然后把它们均匀的分布到了3个Broker节点上,每个Broker 节点互为备份。
- 这些所有的副本分为两种角色,Leader 对外提供读写服务。Follower唯一的任务就是从Leader异步拉 取数据。
- 图中红色的副本为Leader,同时Leader也被均匀的分布在各个节点上,可以保证读写均匀。
Segment
- Kakfa为了防止Log不断追加导致文件过大,导致检索消息效率变低。
- 在一个Partition的Log超出一定大小的时候,就被切割为多个Segment来组织数据。
- 在磁盘上,每个Segment由一个log文件和2个Index文件组成。
- 这三个文件是成套出现的。
- 其中.index是用来存储Consumer的Offset偏移量的索引文件,.timeindex是用来存储消息时间戳的索 引文件,.log文件就是用来存储具体的数据文件。
- 以切割时记录的Offset值作为文件的名字。它的文件结构是这样的,如图所示:
Index
- Index其中一种是偏移量索引文件,记录的是Offset和消息在Log文件中的位置映射关系。
- 还有一种是时间戳索引文件,记录的是时间戳和Offset的关系。
- 为了提高检索效率Kafka并不会为每一条消息都建立索引,而是采用稀疏索引。也就是说每产生一批 消息的时候才产生一条索引记录。
- 可以通过参数来设置索引的稀疏程度。
- 越稠密的索引检索数据越快,但是会消耗更多的存储空间。
- 越稀疏的索引占用存储空间越小,但是插入和删除时维护开销也小。
- 时间戳索引也是采用稀疏索引设计。由于索引文件是以Offset命名的,所以Kafka在检索数据的时候, 是采用二分法查找,效率就非常快。
高效读写
- 批量发送是指线程A把数据都写入到数据池,线程B把数据池里面的数据批量发送给Broker。
- 批量发送不是由Kafka实现的,是由.Net提供的Kafka组件来实现的。
- 当数据池里面的数据达到阈值之后就会向Kafka批量发送,阀值是在客户端配置的。
顺序读写
Kafka是消息队列,数据只有新增没有修改,所以可以连续。Kafka的数据可以预置,默认数据只能存储7天,如果消费端出现了问题超过了7天数据都没有被消费,那么数据也会被删除。如果遇到特殊的情况,可以改变默认值。
零拷贝
- 在实际应用中,如果我们需要把磁盘中的某个文件内容发送到远程服务器上,那么它必须要经过几个 拷贝的过程。
- 从磁盘中读取目标文件内容拷贝到内核缓冲区。
- CPU控制器再把内核缓冲区的数据赋值到用户空间的缓冲区中。
- 接着在应用程序中,调用write方法,把用户空间缓冲区中的数据拷贝到内核中。
- 最后,把在内核模式下的数据赋值到网卡缓冲区,网卡缓冲区再把数据传输到目标服务器上。
- 在这个过程中我们可以发现,数据从磁盘到最终发送出去,要经历4次拷贝。
- 而在这四次拷贝过程中,有两次拷贝是浪费的,分别是:
- 从内核空间赋值到用户空间。
- 从用户空间再次复制到内核空间。
- 除此之外,由于用户空间和内核空间的切换会带来CPU的上下文切换,对于CPU的性能也会造成影响。
- 而零拷贝,就是把这两次多于的拷贝省略掉,应用程序可以直接把磁盘中的数据从内核中直接传输给 Socket,而不需要再经过应用程序所在的用户空间。
- 零拷贝通过DMA技术把文件内容复制到内核空间中的Read Buffer。
- 接着把包含数据的文件描述符加载到Socket Buffer中,DMA引擎直接可以把数据从内核空间中传递给 网卡设备。
- 在这个流程中,数据只经历了两次拷贝就发送到了网卡中,并且减少了2次CPU的上下文切换,对于 效率有非常大的提高。
- 所以说所谓的零拷贝,并不是完全没有数据的赋值,只是相对于用户空间来说,不再进行数据拷贝。
- 对于整个流程来说,零拷贝只是减少了不必要的拷贝次数。
单播/多播/消费者组
- 单播类似于队列,一个消息只能被消费一次,消费过了,其它消费者就不能消费了。
- 多播类似于发布订阅,一个消息可以被多个消费者同时消费。
- 消费者组是指在一个消费者组中,每一个Topic中的消息只能被这个组中的一个消费者消费。
Offset
- Offset就是偏移量,也就是当前数据的唯一ID。
- 自增而且在一个分区里面存在顺序性,但是多个分区里面没有办法保证顺序性,顺序性指的就是偏移量。
同步策略
- 为了保证Producer发送的数据,能可靠的到达指定的Topic,Topic的每个Partition收到Producer发送 的消息后,都需要向Producer发送Ack确认收到,如果Producer收到Ack,就会进行下一轮的发送, 否则重新发送数据。
- 那么何时发送Ack呢?
- 确保有Follow与Leader同步完成,Leader再发送Ack,这样才能保证Leader挂掉之后,能在Follower 中选举出新的Leader。
- 那么有多少个Follower同步完成之后发送Ack呢?
- 第一种方案是,半数以上的Follower同步完成,就可以发送Ack。
- 第二种方案是,全部的Follower同步完成,才可以发送Ack。
Ack的三种应答
- Acks=0是效率最高的,一旦接收到数据就立刻返回Ack,但是Leader挂了或者数据没有落盘的时候, 就有可能丢失数据。
- Acks=1是Leader落盘成功之后返回Ack,但是有可能刚落盘成功Leader就挂了。
- Acks=-1是效率最低的,需要Partition的Leader和Follower全部落盘成功之后才返回Ack,能保证数据 不丢失,但是会存在数据重复的问题。当数据写入Leader和Follower的时候,Leader挂了,然后Follower 成了Leader,但是没有给生产者返回Ack,这个时候生产者补偿重试,又会向Follower写入一条数据, 所以要保证数据的幂等性。原理就是根据客户端的ClientID和当前消息的唯一ID,但是如果客户端也 重启了,那么数据还是会重复,这是因为ClientID变了。
ISR
- 发送到Kafka Broker上面的消息,最终是以Partition的物理形态来存储到磁盘上的。
- 而Kafka为了保证Parititon的可靠性,提供了Paritition的副本机制,然后在这些Partition的副 本集里面。存在Leader Partition和Flollower Partition。
- 生产者发送过来的消息,会先存到Leader Partition里面,然后再把消息复制到Follower Partition。
- 这样设计的好处就是一旦Leader Partition 所在的节点挂了,可以重新从剩余的Partition副本里 面选举出新的Leader。
- 然后消费者可以继续从新的Leader Partition里面获取未被消费的数据。
- 在Partition 多副本设计的方案里面,有两个很关键的需求。 副本数据的同步 新Leader的选举
- 这两个需求都需要涉及到网络通信,Kafka 为了避免网络通信延迟带来的性能问题,以及尽可能的保 证新选举出来的Leader Partition 里面的数据是最新的,所以设计了ISR 这样一个方案。
- ISR全称是in-sync replica,它是一个集合列表,里面保存的是和Leader Parition节点数据最接近 的Follower Partition。
- 如果某个Follower Partition里面的数据落后Leader太多,就会被剔除ISR列表。
- 简单来说,ISR 列表里面的节点,同步的数据一定是最新的,所以后续的Leader选举,只需要从ISR 列表里面筛选就行了。
- 引入ISR 这个方案的原因主要有两个。
- 尽可能的保证数据同步的效率,因为同步效率不高的节点都会被踢出ISR列表。
- 避免数据的丢失,因为ISR里面的节点数据是和Leader副本最接近的。
如何避免被重复消费
- 首先,Kafka的Broker上面存储的消息都有一个Offset标记。
- 然后Kafka的消费者是通过Offset这个标记,来维护当前已经消费的数据。
- 然后消费者每消费一批数据,Kafka的Broker就会更新Offset值,避免重复消费的问题。
- 默认情况下,消息消费完成以后会自动提交Offset值,避免重复消费。
- Kafka的消费端自动提交的时候,会有一个默认5秒的时间间隔。
- 也就是说在5秒之后向Broker去获取消息的时候,来实现Offset的提交。
- 所以Consumer在消费过程中,应用程序强制被kill掉或者宕机的时候,可能会导致Offset没有提交, 从而产生消息重复。
- 在Kafka里面有一个叫Partition Balance的机制,就是把多个Partition均匀分配给多个消费者。
- 那么Consumer端会从默认的Partition里面去消费消息。
- 如果Consumer在默认的5秒钟内,没有办法处理完这一批消息,就会触发Kafka的ReBalance机制, 从而导致Offset自动提交失败。
- 而在重新ReBalance以后,Consumer还是会从之前没有提交的Offset位置开始消费,从而导致重复消 费的一个问题。
- 我们可以提高消费端处理性能,去避免触发Balance,比如可以用异步的方式来处理消息。缩短单个 消息的消费时长。
- 还可以调整消费端消息处理的一个超时时间,我们可以把超时时间设置长一点。
- 还可以减少一次性从Broker上获取的消息条数。
- 还可以针对每一条消息去生成一个MD5的值,然后保存到数据库或者Redis里面,在处理消息之前先 去数据库或者Redis里面判断是否已经存在相同消息的MD5值,如果存在那么就不消费。
如何保证消息不丢失
总结
- 保证消息不丢失可以把服务端的持久化,设置为同步刷盘。
- 还可以把生产端设置为同步投递。
- 还可以把消费端设置为手动提交。
服务端的设置
- 服务端设置Broker中的配置项unclean.Leader.election.enable = false,保证所有副本同步。
- Producer将消息投递到服务器的时候,也需要将消息持久化,同步到磁盘。
- 同步到硬盘的过程中,会有同步刷盘和异步刷盘。如果选择的是同步刷盘,一定会保证消息不丢失。 就算刷盘失败,也可以及时补偿。如果选择的是异步刷盘,消息会有一定概率丢失。
- 网上有一种说法,说Kafka不支持同步刷盘,这种说法也不能说是错的。
- 可以通过参数的配置变成同步刷盘:
- # 达到一定消息数量时,将数据flush到日志文件中。
- #log.flush.interval.messages=10000
- # 达到一定的时间(ms)间隔时,执行一次强制的flush操作。
- interval.ms和interval.messages 无论哪个达到,都会flush。
- #log.flush.interval.ms=1000
生产端的设置
- 生产端的设置就是Producer使用带回调通知的send(msg,callback)方法,并且设置Acks=All。
- Producer要保证消息到达服务器,就需要使用到消息确认机制,也就是说,必须要确保消息投递到服 务端,并且得到投递成功的响应,确认服务器已接收,才会继续往下执行。
- 如果Producer将消息投递到服务端,服务端没来得及接收就已经宕机了,那投递过来的消息岂不是丢 失了。
- 所以在Producer投递消息时,都会记录日志,然后再将消息投递到服务端,就算服务器宕机了,等 服务器重启之后,也可以根据日志信息完成消息补偿,确保消息不丢失。
消费端的设置
- 消费端的设置就是修改enable.auto.commit=false。
- 在Kafka中,消费者消费完成之后,它不会立即删除,而是使用定时清除策略。
- 也就是说,消费者要确保消费成功之后,手动Ack提交。如果消费失败的情况下,要不断地进行重试。
- 所以消费端不要设置自动提交,设置为手动提交才能保证消息不丢失。
消息积压怎么办?
- Kafka一般消息默认过期时间是7天,7天之后没有消费的消息会自动删除。
- 如果是消费能力不足,可以提高对应Topic的分区数,同时提升消费者组的消费者数量。
- 如果是消费间隔较长导致的,可以提高每次拉取数据的数量。
代码演示