微服务学习总结-> Zookeeper/Kafka/RocketMQ

670 阅读28分钟

参考

基础知识

存储

顺序读写与随机读写的差异

从上图可以看出顺序写磁盘的速度比随机写内存的速度快。

磁盘I/O流程

参考 图解Kafka之核心原理

从编程角度,一般磁盘I/O的场景如下:

  1. 用户调用标准C库进行I/O数据流:
    • 应用程序buffer \rightarrow
    • C库标准IObuffer \rightarrow
    • 文件系统页缓存 \rightarrow
    • 通过具体文件系统到磁盘
  2. 用户调用文件IO数据流:
    • 应用程序buffer \rightarrow
    • 文件系统页缓存 \rightarrow
    • 通过具体文件系统到磁盘
  3. 用户打开文件时使用O_DIRECT,绕过也缓存直接读写磁盘
  4. 用户使用类似dd工具,并使用direct参数,绕过系统cache与文件系统直接写磁盘

发起I/O请求步骤如下(以最长链为例):

  • 写操作
    • 用户调用fwrite把数据写入C库标准IObuffer后就返回,为异步操作
    • 在IObuffer中的数据不会立即刷新磁盘,会将多个小数据量相邻写操作先缓存起来合并,最终调用wirte函数一次性写入(或者将大块数据分解多次write调用)页缓存
    • 页缓存中的数据也不会立即刷新到磁盘,内核有pdflush线程在不停地检测脏页,判断是否要协会磁盘,如果是则发起磁盘I/O请求
  • 读操作
    • 用户调用fread到C库标准IObuffer中读取数据,如果成功则返回,否则继续
    • 到页缓存中读取数据,如果成功则返回,否则继续
    • 发起I/O请求,读取数据后,缓存buffer和C库标准IObuffer并返回,可见读操作是一个同步请求
  • I/O请求处理
    • 通用块层根据I/O请求构造一个或多个bio结构并提交给调度层
    • 调度器将bio结构进行排序和合并组织成队列且确保读写操作尽可能理想:将一个或多个进程的读操作合并到一起读,并尽可能变随机为顺序读

目前Linux系统中的I/O调度策略有4种:NOOP, CFQ, DEADLINE, ANTICIPATORY(默认CFQ)

零拷贝

指将数据直接从磁盘文件复制到网卡设备中,而不需要经过应用程序之手。

  • 零拷贝减少了内核和用户模式之间的上下文切换,大大提高了应用程序的性能

比如下面一种场景: 需要将一个图片展示给用户,首先将图片从磁盘中复制出来放到一个内存buf中,然后将这个buf通过Socket传输给用户,进而用户获得了图片。

正常情况下,文件A展示给用户过程中经历了4次拷贝过程:

  1. 调用read()时,文件A中的内容被复制到了内核模式下的Read Buffer中
  2. CPU控制将内核模式数据复制到用户模式下
  3. 调用write()时,将用户模式下的内容复制到内核模式下的Socket Buffer中
  4. 将内核模式下的Socket Buffer的数据复制到网卡设备中传送

如果采用了零拷贝技术,那么应用程序就可以直接将请求内核把磁盘中的数据传输给Socket,如下图所示:

零拷贝技术通过DMA技术将文件内容复制到内核模式下的Read Buffer中。不过数据没有被复制到Socket Buffer,相反只有包含数据的位置和长度的信息的文件描述符被加到Socket Buffer中。DMA引擎直接将数据从内核模式中传递到网卡设备(协议引擎)。这样数据只经历了两次复制就从磁盘中传输出去了,并且上下文切换变为2次。

  • 零拷贝是针对内核模式而言的,数据在啮合模式下实现了零拷贝。

一致性算法

Paxos

参考: Paxos Wiki Paxos算法 Paxos算法解决的是一个分布式系统如何就某个值(决议)达成一致的问题。

Paxos角色

  • Client
    • Client发起请求,并等待返回。比如在分布式文件系统中写入一个文件
  • Proposer
    • 接受客户请求,尝试说服Acceptors接受,并处理此过程中的各种冲突
    • 只要Proposer发的提案被半数以上Acceptor接受,Proposer就认为该提案里的value被选定
  • Acceptor(Voters)
    • 只要Accepter接受了接受了某个提案,Acceptor就认提案里的value被选定
  • Learner
    • 扮演协议的副本因子
    • Acceptor告诉Leader哪个value被选定,Learner就认为那个value被选定
  • Leader
    • 作为唯一的Proposer去推进流程

Paxos算法两阶段

  • 准Leader确定

    • Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求
    • 如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor已经响应过的所有Prepare请求的编号,那么它就会将它已经接受过的编号最大的提案(如果有)作为响应发给Proposer,同时该Accepter承诺不再接受任何编号小于N的提案
  • Leader确认

    • 如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它会发送一个针对[N, V]提案的Accept请求给半数以上的Acceptor。其中V就是收到响应中编号最大提案的value,如果响应中不包含任何提案,那么V就由Proposer自己决定
    • 如果Acceptor收到一个针对编号为N提案的Accept请求。只要改Acceptor没有对编号大于N的Prepare请求做出过响应,它就接受该提案;否则忽略该请求

注意:Acceptor可以接受多个提案,当另外一个Proposer(没有意识到一个新的value正在确认中),发起了一个更高的N请求。在这种情形下,即使Acceptor之前已经接受了一个value,这个Acceptor还是会接受这个新提案。这些提案可能处于一个不确定的失败状态。但最终Paxos会保证所有的Acceptors达到最终一致性。

常见Basic Paxos流程

  • Basic Paxos:没有失败
Client   Proposer      Acceptor     Learner
   |         |          |  |  |       |  |
   X-------->|          |  |  |       |  |  Request
   |         X--------->|->|->|       |  |  Prepare(1)
   |         |<---------X--X--X       |  |  Promise(1,{Va,Vb,Vc})
   |         X--------->|->|->|       |  |  Accept!(1,V)
   |         |<---------X--X--X------>|->|  Accepted(1,V)
   |<---------------------------------X--X  Response
   |         |          |  |  |       |  |
  • Basic Paxos:Acceptor fails
Client   Proposer      Acceptor     Learner
   |         |          |  |  |       |  |
   X-------->|          |  |  |       |  |  Request
   |         X--------->|->|->|       |  |  Prepare(1)
   |         |          |  |  !       |  |  !! FAIL !!
   |         |<---------X--X          |  |  Promise(1,{Va, Vb, null})
   |         X--------->|->|          |  |  Accept!(1,V)
   |         |<---------X--X--------->|->|  Accepted(1,V)
   |<---------------------------------X--X  Response
   |         |          |  |          |  |
  • Basic Paxos:有redundant Learner失败
Client Proposer         Acceptor     Learner
   |         |          |  |  |       |  |
   X-------->|          |  |  |       |  |  Request
   |         X--------->|->|->|       |  |  Prepare(1)
   |         |<---------X--X--X       |  |  Promise(1,{Va,Vb,Vc})
   |         X--------->|->|->|       |  |  Accept!(1,V)
   |         |<---------X--X--X------>|->|  Accepted(1,V)
   |         |          |  |  |       |  !  !! FAIL !!
   |<---------------------------------X     Response
   |         |          |  |  |       |
  • 带有Proposer失败的Basic Paxos
Client  Proposer        Acceptor     Learner
   |      |             |  |  |       |  |
   X----->|             |  |  |       |  |  Request
   |      X------------>|->|->|       |  |  Prepare(1)
   |      |<------------X--X--X       |  |  Promise(1,{Va, Vb, Vc})
   |      |             |  |  |       |  |
   |      |             |  |  |       |  |  !! Leader fails during broadcast !!
   |      X------------>|  |  |       |  |  Accept!(1,V)
   |      !             |  |  |       |  |
   |         |          |  |  |       |  |  !! NEW LEADER !!
   |         X--------->|->|->|       |  |  Prepare(2)
   |         |<---------X--X--X       |  |  Promise(2,{V, null, null})
   |         X--------->|->|->|       |  |  Accept!(2,V)
   |         |<---------X--X--X------>|->|  Accepted(2,V)
   |<---------------------------------X--X  Response
   |         |          |  |  |       |  |
  • 多个Proposers冲突
Client   Leader         Acceptor     Learner
   |      |             |  |  |       |  |
   X----->|             |  |  |       |  |  Request
   |      X------------>|->|->|       |  |  Prepare(1)
   |      |<------------X--X--X       |  |  Promise(1,{null,null,null})
   |      !             |  |  |       |  |  !! LEADER FAILS
   |         |          |  |  |       |  |  !! NEW LEADER (knows last number was 1)
   |         X--------->|->|->|       |  |  Prepare(2)
   |         |<---------X--X--X       |  |  Promise(2,{null,null,null})
   |      |  |          |  |  |       |  |  !! OLD LEADER recovers
   |      |  |          |  |  |       |  |  !! OLD LEADER tries 2, denied
   |      X------------>|->|->|       |  |  Prepare(2)
   |      |<------------X--X--X       |  |  Nack(2)
   |      |  |          |  |  |       |  |  !! OLD LEADER tries 3
   |      X------------>|->|->|       |  |  Prepare(3)
   |      |<------------X--X--X       |  |  Promise(3,{null,null,null})
   |      |  |          |  |  |       |  |  !! NEW LEADER proposes, denied
   |      |  X--------->|->|->|       |  |  Accept!(2,Va)
   |      |  |<---------X--X--X       |  |  Nack(3)
   |      |  |          |  |  |       |  |  !! NEW LEADER tries 4
   |      |  X--------->|->|->|       |  |  Prepare(4)
   |      |  |<---------X--X--X       |  |  Promise(4,{null,null,null})
   |      |  |          |  |  |       |  |  !! OLD LEADER proposes, denied
   |      X------------>|->|->|       |  |  Accept!(3,Vb)
   |      |<------------X--X--X       |  |  Nack(4)
   |      |  |          |  |  |       |  |  ... and so on ...

Raft

参考: Raft Wiki JAVA核心知识点整理

In Search of an Understandable Consensus Algorithm

Raft算法的头号目标就是容易理解(UnderStandable)。 增强了可理解性,在性能、可靠性、可用性方面不输于Paxos。

Raft是工程上广泛使用的强一致性、去中心化、高可用的分布式协议。

Raft和Paxos一样要保证一半以上的节点能正常提供服务。 Raft把算法流程分为三个子问题:

  • 选举(Leader election)
  • 日志复制(Log replication)
  • 安全性(Safety)

角色

  • Leader(领导者-> 日志管理)
    • 负责日志同步管理,处理客户端请求,与Follower保持心跳联系
  • Follower(追随者-> 日志同步)
    • 刚启动时,所有的节点为Follower状态
    • 如果在一段时间内如果没有收到Leader心跳,则Follower转为Candidate状态,发起选举
    • 如果收到majority的投票(含自己一票)则切换为Leader状态
    • 如果发现其他节点
    • 响应Leader日志请求,响应Candidate的请求
    • 把请求到Folloer的事务转给Leader
  • Candidate(候选者-> 负责选票)
    • 负责选举投票
    • Raft刚启动时,由一个节点从Follower转为Candidate发起选举
    • 选举出Leader后,从Candidate转为Leader状态

Term(任期)

在Raft中,使用了一个可以理解为周期(任期)的概念。

  • 每个Term都是一个连续递增的编号
  • 每一轮选举都是一个Term周期
  • 在一个Term中只能产生一个Leader

如下图所示,term(任期)以选举(election)开始,然后就是一段或长或短的工作周期(normal Operation)。另外term3展示了一中情况,没有选举出leader就结束了

选举(Election)

Raft的选举由定时器来触发,每个节点的选举定时器时间都是不一样的,开始状态都为Follwer,某个节点定时器触发选举后,Term递增,状态由Follower转为Candidate,向其他节点发送heartBeat以保持Leader正常运转

  1. 该RequestVote请求接收到n2+1\frac {n} {2} + 1(过半数)节点投票,从Candidate转为Leader,向其他节点转为Follower,否则保持Candidate拒绝该请求
  2. 在此期间如果收到其他节点发送的AppendEntries RPC请求,如该节点的Term大,则当前节点转为Follower,否则保持Candidate拒绝该请求
  3. Election timeout发生,Term递增,重新发起选举

在一个Term期间,每个节点只能投票一次,所以当有多个Candidate存在时就会出现每个Candidate发起的选举都存在接收投票数不过半的问题,这时每个Candidate都将Term递增,重启定时器,并重新发起选举,由于每个节点中定时器的时间都是随机的,所以不会多次存在多个Candidate同时发起投票的问题

在Raft中,当接收到客户端日志(事务请求)后先把该日志追加到本地的Log中,然后通过heartBeat把该Entry同步给其他Follower,Follower接收到日志后,记录日志,然后向Leader发送ACK,当Leader收到大多数Follower的ACK信息后,将日志设置为已提交并追加到本地磁盘中,通知客户端并在下个heartBeat中Leader将通知所有Follower将该日志存储在自己的本地磁盘中。

安全性(Safety)

安全性是用于保证每个节点都执行相同序列的安全机制如当某个Follower在当前Leader Commit Log时变的不可用了,稍后可能该Follower又会被选举为Leader,这时,新Leader可能会用新的Log覆盖先前已committed的Log,这就是导致节点执行不同序列。

Safety就是用于保证选举出来的Leader一定包含先前Commited Log的机制:

  • 选举安全性(Election Safety):每个Term只能选举出一个Leader
  • Leader完整性(Leader Completeness):指Leader日志的完整性,Raft在选举阶段就使用Term的判断用于保证完整性,当请求投票的该Candidate的Term较大或Term相同Index更大则投票,该节点将容易变为Leader

Zab

见下面的Zookeeper分析

Zookeeper

参考: Zookeeper官网 可能是把 ZooKeeper 概念讲的最清楚的一篇文章

Zookeeper概念

ZooKeeper 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

Zookeepe提供了一个类似于Linux文件系统的树形结构(可以认为是轻量级的内存文件系统,但只适合存储少量信息),同时提供了对于每个节点的监控与通知机制。

  • Zookeeper本身是一个分布式程序(需要半数以上节点存活)
  • 为了保证高可用,最好以集群形态来部署Zookeeper
  • Zookeeper将数据保存在内存中,保证了高吞吐量和低延迟(但内存限制了存储的容量)
  • Zookeeper是高性能的,在“读”多于“写”的应用程序中尤其高性能
  • Zookeeper有临时节点的概念,当创建临时节点的客户端会话一直保持活动,临时节点就一直存在,当会话终结时,临时节点被删除。
  • Zookeeper底层只提供了两个功能:
    • 管理(存储、读取)用户程序提交的数据
    • 为用户程序提交数据节点提供监听服务

Zookeeper特点:

  • 顺序一致性:从同一客户端发起的事务请求,最终会严格地按照顺序应用到Zookeeper中
  • 原子性:所有事务请求的处理结果在整个集群中所有机器人上的应用情况是一致的
  • 单一系统映像:无论客户端连到哪一个Zookeeper服务器上,其看到的服务器数据模型是一致的
  • 可靠性:一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖

Zookeeper数据模型

Zookeeper允许分布式进程通过共享的层次结构命名空间进行相互协调,与标砖文件系统类似。

Znode

Zookeeper中的节点称为Znode。Znode有四种形式的目录节点:

  • PERSISTENT: 持久节点
  • EPHEMERAL: 暂时节点
  • PERSISTENT_SEQUENTIAL: 持久化顺序编号目录节点
  • EPHEMERAL_SEQUENTIAL: 暂时化顺序编号节点

Zookeeper角色

Zookeeper集群是一个基于主从复制(Master/Slave)的高可用集群,每个服务器承担如下三种角色中的一种

Leader

  • 一个Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各个Follower及Observer间的心跳
  • 所有的写操作必须要通过Leader完成,再由Leader将写操作广播给其他服务器。只要有超过半数节点(不包括Observer节点)写入成功,该写操作就会被提交(类2PC协议)

Follower

  • 一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳
  • Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理
  • 负责在Leader处理请求时,对请求进行投票

Observer

  • 与Follower类似,但无投票权。Zookeeper需保证高可用和强一致性,为了支持更多Server
  • Server增多,则投票阶段延迟增大,影响性能
  • 引入Observer,不参与投票
  • Observers接受客户端连接,并将写请求转发给Leader节点
  • 加入更多Observer节点,提高伸缩性,同时不影响吞吐率

Zookeeper工作原理(原子广播)总结

  • Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步,实现这个机制的协议叫Zab协议
  • 当服务启动或者在Leader崩溃后,Zab就进入了恢复模式,当Leader被选举出来,且大多数Server完成了与Leader的状态同步后,恢复模式就结束了
  • 状态同步保证了Leader和Follower Server具有相同的系统状态
  • 一旦Leader已经和多数的Follower进行状态同步后,它就可以开始广播消息了,即进入广播状态。这时候当一个Server加入到Zookeeper服务中,它会在恢复模式下启动,发现Leader,并和Leader进行状态同步。待到同步结束,它也参与消息广播。Zookeeper服务一直维持在Broadcast状态,直到Leader崩溃或者Leader失去了大部分的Follower支持
  • 广播模式需要保证提议(proposal)被按照顺序处理,因此zk采用了递增的事务id号Zxid来保证,所有的提议(proposal)都在被提出的时候加上Zxid
  • 当Leader崩溃或者Leader失去大多数Follower支持,这时候ZK进入恢复模式,恢复模式需要重新选举出一个新的Leader,让所有的Server都恢复到一个正确的状态

Zab协议

Zookeeper并没有完全采用Paxos算法,而是使用了ZAB(Zookeeper Atomic Broadcast)协议作为其保证数据一致性的核心算法。

Zxid 事务编号

  • Zxid(Zookeeper Transaction Id): 对于来自客户端的每个事务请求,Zookeeper都会分配一个全局唯一的递增编号,这个编号反应了所有事务操作的先后顺序。

Zxid是一个64位数字,其中低32位是一个简单的单调递增计数器,而高32位则代表Leader周期epoch的编号。

每个当选产生一个新的Leader服务器,就会从这个Leader服务器上取出本地日志中最大的Zxid,并从中读取epoch值,然后加1作为新的epoch,并将低32位从0开始计数。

  • epoch可理解为当前集群所处的年代或周期,每个Leader像皇帝一样有自己的年号,Leader变更后,会在前一个年底基础上加1。这样就算旧的Leader崩溃恢复后,也没有人听他的了,因为Follower只听命于当前年代的Leader

Zab协议模式

  • 恢复模式(选主)
    • 当服务启动或者在Leader崩溃后,Zab进入恢复模式
    • 当Leader被选举处理,且大多数Server完成与Leader的状态同步以后,恢复模式就结束了。
  • 广播模式(同步)

Zab协议四阶段

  1. Leader election(选举阶段 -> 选出准Leader)
    • 节点一开始都处于选举阶段
    • 只要有一个节点得到超过半数节点的票数,它就可以当选准Leader
    • 只有到达广播阶段(broadcast)准leader才会成为真正的Leader。
    • 这一阶段的目的就是为了选出一个准Leader,然后进入下一个阶段
  2. Discovery(发现阶段 -> 接受提议、生成epoch、接受epoch)
    • Followers跟准Leader进行通信,同步Followers最近接收的事务提议
    • 这一阶段的主要目的是发现当前大多数节点接收的最新提议,并且准Leader生成新的epoch,让Followers接受,更新他们的accpted Epoch
    • 一个Follower只会连接一个Leader,如果有一个节点f认为另一个Follower p是Leader,f在尝试连接p时会被拒绝,f被拒绝之后,就会进入重新选举阶段
  3. Synchronization(同步阶段 -> 同步Follower副本)
    • 利用Leader前一阶段获得的最新提议历史,同步集群中所有的副本
    • 只有当大多数节点都同步完成,准Leader才会成为真正的Ledaer
    • Follower只会接受Zxid比自己的lastZxid大的提议
  4. Broadcast(广播阶段 -> Leader消息广播)
    • 到此阶段,Zookeeper集群才能正式对外提供事务服务,并且Leader可以进行消息广播
    • 如果有新的节点加入,还需要对新的节点进行同步

Zab提交事务不像2PC一样需要全部的Follower都ACK,只需要超过半数的节点ACK就可以了

投票机制

每个Server首先给自己投票,然后用自己的选票和其他的Server选票对比,权重大的胜出,使用权重较大的更新自身的选票箱。具体选举流程:

  1. 每个Server启动后都询问其他的Server他要投票给谁。对于其他Server的询问,Server每次根据自己的状态都回复自己推荐的Leader的id和上一次处理事务的Zxid(系统启动时,每个server都会推荐自己)
  2. 收到Server的回复后,就计算出Zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server
  3. 计算过程中获得票数最多的Server为获胜者,如果获胜者票数超过半数,则改Server被选为Leader,否则,继续这个过程,直到Leader被选举出来
  4. Leader就会开始等待Server连接
  5. Follower连接Leader,将最大的Zxid发送给Leader
  6. Leader根据Follower的Zxid确定同步点,至此选举阶段完成
  7. 选举阶段完成Leader同步后,通知Follower已经成功uptodate状态
  8. Follower收到uptodate消息后,又可重新接受client的请求,进行服务了

示例,假如有5台服务器,每台服务器均没有数据,选举过程如下:

  1. 服务器1启动,给自己投票,然后发投票信息,由于其他机器还没有启动,所以它收不到反馈信息,服务器1的状态一直处于Looking
  2. 服务器2启动,给自己投票,同时与之前启动好的服务器1交换结果,由于服务器2的编号大,所以服务器2胜出,但此时投票数没有大于半数,两个服务器的状态依然是Looking
  3. 服务器3启动,给自己投票,同时与服务器1、2交换信息,由于服务器3的编号大,所以服务器3胜出,此时投票数正好大于半数,所以服务器3称为Leader,服务器1、2称为Follower
  4. 服务器4启动,给自己投票,与服务器1、2、3交换信息,尽管服务器4的编号大,但是之前服务器3已经胜出,所以服务器4也称为Follower
  5. 服务器5启动,与服务器4一样的逻辑,称为Follower

Kafka

参考: Kafka官网 图解Kafka之核心原理

Kafka是一种高吞吐量、分布式、基于发布/订阅的消息系统,最初由LinkedIn公司开发,使用Scala编写,目前是Apache开源项目。

概念

  • Broker:Kafka服务器,负责消息存储与转发
  • Topic:消息类别,Kafka按照topic分类消息
  • Partition:topic的分区,一个消息可以有多个partition,消息保存在多个partition上
  • Offset:消息在日志中的位置,可以理解为消息在partition上的偏移量,也是代表该消息的唯一序号
  • Producer:消息生产者
  • Consumer:消息消费者
  • Consumer Group:消息者分组,每个消费者必属于一个group
  • Zookeeper:保存着集群broker、topic、partition等meta数据;另外,还负责broker故障发现,partition leader选举,负载均衡等功能

Kafka存储设计

Kafka中的消息是以主题Topic为基本单位进行归类的,各个主题在逻辑上相互独立。

Partition的数据文件

每个主题可以分为一个或多个分区(Partition),每条消息在发送的时候,会根据分区规则追加到指定的分区中。 Parition每条Message包含以下三个属性:

  • Offset:Message在这个Parition中的偏移量,但不是该Message在partition实际存储位置,而是逻辑上的一个值,它唯一确定了parition中的一个Message的id
  • MessageSize:表示消息内容的data的大小
  • Data:Message具体内容

如果分区规则设置的合理,则所有的消息可以均匀地分不到不同的分区中,实现水平扩展。

数据文件分段

不考虑副本(Replica)的情况,一个分区对应一个日志(Log),为防止Log过大, Partition物理上由多个LogSegment文件组成,每个segment大小相等,顺序读写。这样一个巨型文件被平均分配为多个较小的文件,方便消息的维护和清理。

每个LogSegment对应磁盘上一个日志文件和两个索引文件,以及可能的其他文件(比如以".txnindex"为后缀的事务索引文件)。Log对应一个命名形式为<topic>-<partiton>的文件夹。

向Log中追加消息时是顺序写入的,之后最后一个LogSegment才能执行写入操作。

为了便于消息的检索,每个LogSegment中的日志文件(以".log"为文件后缀)都有对应的两个索引文件:

  • 偏移量索引文件:以".index"为文件后缀
  • 时间戳索引文件:以".timeindex"为文件后缀

每个LogSegment都有一个基准偏移量baseOffset,用来表示当前LogSegment中的第一条消息的offset。偏移量是一个64位的长整数,日志文件和两个索引文件都是根据基准偏移量(baseOffset)命名的。这样在查找指定offset的Message的时候,用二分查找就可以定位到该Message的哪个LogSegment数据文件中。

偏移量索引

Kafka为每个分段后LogSetment的数据文件建立了索引文件,文件名与数据文件名一样,只是扩展名为.index。index文件中并没有为数据文件中的每条Message建立索引,而是采用稀疏存储方式,每隔一定字节数据建立一条索引。这样避免索引文件占用过多空间,从而可以将索引文件保留在内存中

索引文件的格式:

  • relativeOffset:相对偏移量,表示消息相对于baseOffset的偏移量,占用4个字节
  • postion:物理地址,4个字节

时间戳索引

每个索引项占用12个字节,格式如下:

  • timestamp: 当前日志分段最大的时间戳,8个字节
  • relativeOffset:时间戳所对应的消息的相对偏移量,4个字节

由指定时间戳查找消息的路由过程如下:

  1. 定位目标日志分段(LogSegment):将目标时间戳与每个日志分段中的最大时间戳largestTimeStamp逐一对比,直到找到不小于目标时间戳的日志分段。
  2. 定位时间戳索引文件相对偏移量(RelativeOffset):根据目标时间戳,在时间戳文件中使用二分查找找到不大于该时间戳的最大偏移量
  3. 定位偏移量索引相对偏移量(RelativeOffset):在偏移量索引中使用二分查找找到不大于目标相对偏移量的最大相对偏移量。
  4. 定位日志文件中的消息:从步骤1找到的日志分段文件(LogSegment)中的步骤3得到的相对偏移量位置开始查找目标偏移量的消息。

磁盘存储设计

  • Kafka在设计时采用了文件追加的方式写入消息:
  • 只能在日志文件的尾部追加新的消息
  • 不允许修改已写入的消息

这种方式属于典型的顺序写盘的操作。除此之外,Kafka在存储性能上还做了很多优化。

页缓存

页缓存是操作系统实现的一种主要的磁盘缓存,可以减少对磁盘的I/O操作。 Kafka中大量使用了页缓存,这是Kafka实现高吞吐的重要因素之一。消息写入流程如下:

  1. 消息先被写入页缓存
  2. 操作系统负责具体的刷盘任务,但在Kafka中同样提供了同步刷盘或间断性强制刷盘(fsync)功能。

同步刷盘可以提高消息的可靠性,防止掉电异常。

零拷贝

生产者设计

负载均衡

由于消息topic由多个partition组成,且partition会均衡到不同的broker上。Producer可以通过随机或者Hash等方式,将消息负载均衡到多个partition上。

批量发送

Producer可以在内存中合并多条消息后,以一次请求的方式发送了批量的消息给broker,从而减少了broker存储消息的IO操作次数。

但也一定程度上影响了消息的实时性,以时延的代价,换取更好的吞吐量。

压缩

Producer端通过GZIP或Snappy格式对消息集合进行压缩,在Consumer端进行解压,减少数据的传输量,减轻了网络传输的压力。

在对大数据的处理上,瓶颈往往是在网络,而不是CPU(解压缩会耗掉部分的CPU资源)。

消费者设计

  • 同一个Consumer Group中的多个Consumer实例,不同时消费同一个partition,等效于队列模式

  • Partition内的消息是有序的,Consumer通过pull方式消费消息。

  • Kafka不删除已消费的消息。

  • 对于partition,顺序读写磁盘数据,以时间复杂度O(1)O(1)方式提供消息持久化能力。

RocketMQ

参考:《浅入浅出》-RocketMQ RocketMQ官网

角色

NameServer

  • NameServer主要负责对于源数据的管理,包含了对于Topic和路由信息的管理。
  • NameServer压力不会太大,平时主要开销在维持心跳和提供Topic-Broker关系数据
  • Broker向NameServer发心跳时,会带上当前自己所负责的所有的Topic信息,如果Topic个数太多(万级别),会导致一次心跳中,就Topic的数据就有几十M,网络差的话,会导致心跳失败,导致NameServer误认为Broker心跳失败
  • NameServer被设计成几乎无状态的,可以横向扩展,节点相互之间无通信,通过部署多台机器来标记自己是要给伪集群
  • 每个Broker在启动的时候会到NameServer注册,Producer在发送消息前会根据Topic到NameServer获取到Broker的路由信息,Consumer也会定时获取Topic的路由信息。

Broker

消息中转,负责存储消息,转发消息

  • 单个Broker节点与所有的NameServer节点保持长连接及心跳,并会定时将Topic信息注册到NameServer,底层的通信、连接是基于Netty实现的
  • Broker负责消息存储,以Topic为维度支持轻量级别的队列,单机可以支撑上万队列规模,支持消息push/pull模式
  • 具有上亿级消息堆积能力,同时可严格保证消息的有序性

Producer

消息由Producer通过多种负载均衡模式发送到Broker集群,提供了三种发送方式:

  • 同步发送
    • 发送方在发出数据后,在收到接收方的响应后才发下一个数据包。
  • 异步发送
    • 发送方在发出数据后,不等接收方响应,接着发下一个数据包
  • 单向发送
    • 发送方只负责发送消息,而不等待接收方响应且没有回调函数触发。用于可靠性不高的场景,比如日志收集

Consumer

支持Push和Pull两种消费模式,支持集群消息和广播消息,提供实时消息的订阅机制

  • Pull:主动从消息服务器拉取信息,只要拉取到消息,启动消费过程。
  • Push:推送型消费者封装了消息的拉取、消费进度和其他内部维护工作,将消息到达时执行回调接口留给用户应用程序实现。故称为被动类型消费。但从实现上看还是从消息服务器中拉取消息,不同于Pull的是Push首先要注册消费监听器,当监听器触发后才开始消费。

消费领域模型

Message

  • 一条消息必须有一个主题
  • 一条消息可以拥有一个可选的标签(Tag)和额外的键值对。比如可以设置一个业务key,可以方便在Broker上定位问题

Topic

  • Topic与生产者消费者的关系比较松散,一个Topic可以有0个、1个或多个生产者向其发送消息,一个生产者也可以同时向不同的Topic发送消息
  • 一个Topic也可以被0个、1个或多个消费者消费订阅

Tag

  • 可以看做为子主题。
  • 使用标签,同一业务不同目的的消息可以用同一Topic,不同Tag标识。
  • 一条消息里可以没有Tag

Group

  • 一个组可以订阅多个Topic
  • 分为消费者组ConsumerGroup,生产者组ProducerGroup。代表一类的生产者和消费者。

Queue

在Kafka中叫Parition

  • 每个Queue内部都是有序的
  • RocketMQ中分为读、写两种队列,读写队列数量一致

Message Queue

  • 消息队列,Topic被划分为一个或多个子主题,即消息队列。
  • RocketMQ会轮询该Topic下的所有队列将消息发出去
  • Queue的引入使得消息的存储可以分布式集群化,具备了水平扩展能力

Offset

  • 在RocketMQ中,所有消息队列都是持久化
  • 使用Offset来访问其中的存储单元

消费者模式

  • Clustering(集群消费)
    • 默认是集群消费
    • 一个消费者集群共同消费一个Topic的多个队列
    • 一个队列只会被一个消费者消费
    • 如果某个消费者挂掉,分组内其他消费者会接替挂掉的消费者继续消费
  • Broadcasting(广播消费)
    • 广播消费消息会发给消费者组中的每一个消费者进行消费

Message Order

  • Orderly(顺序消费)
    • 表示消息消费的顺序,同生产者为每个消费队列发送的顺序一致
    • 如果处理全局顺序是强制性的,则需要确保Topic只有一个消息队列
  • Concurrently(并行消费)
    • 并行消费不再保证消息顺序,消费的最大并行数量受每个消费者客户端指定的线程池限制

与Kafka对比

功能RocketMQKafka
可靠性同步刷盘/异步刷盘异步刷盘,丢数据概率高
横向扩展支持支持
选举
注册中心NameSrvZK
消费模型Push/PullPull
定时消息只支持18个固定的Level不支持
事务消息不支持不支持
顺序消息支持支持
消息堆积查询支持不支持
消息回溯支持不支持
消息重试支持不支持
死信队列支持不支持
性能(常规)十万级QPS百万级QPS
性能(万级Topic场景)十万级QPS
性能(海量消息堆积场景)十万级QPS