分布式消息队列:kafka
高可用性,可靠性 ,可扩展性,高吞吐量
消息队列中间件
消息队列作为中间件,通常应用在分布式系统中;分布式系统需要保证消息的持久化;
同时解决幂等问题:网络发生抖动时,消息队列能对同样的数据进行去重,数据排序(能让consumer收到顺序消息)
应用场景
异步处理
注册系统,客户发送的邮件注册内容会放在一个数据库,请求会放在一个消息队列。
服务器有空的时候处理请求,发送消息回去
注册流程
客户端提起请求给服务器,服务器根据客户端的数据操作DB(把客户端的注册请求写入到数据库),
然后服务器调用其它接口发送验证信息,注册中间件完成任务后返回服务器,服务器返回调用客户端。
以上第一步是紧迫的,第二步是非紧迫的,可以推迟异步处理-------mq的介入。
系统解耦
分布式系统通过RPC或者restful API交互,RPC严重增加成本(需要知道调用函数名字和参数),restful API过于不安全(http),因此可以通过消息队列进行解耦,一方只提出需要什么数据,不需要考虑数据是如何存储和实现的方法。
流量削锋
当做缓存用,适用于请求大于处理能力时。
日志处理
日志处理紧急度不高,而且需要持久化存储,利用Kalfa比数据库更快
操作数据库的复杂度是B+树的复杂度O(h*log2n),操作消息队列的时间复杂度是O(1),因为消息队列每次都会记录上一个记录的offset,插入在队尾。
Kalfa体系结构
一个Kalfa集群内有多个borker,类比于服务器进程,负责处理读写请求,通过zookeeper选举出一个主broker作为controller。
一个broker进程内有自己的数据,这些数据存储在不同的partition分区内,由进程自己选出一个主分区,数据的读写都针对这个分区进行,其他的复制分区只在数据恢复时使用。
zookeeper除了用来选举主broker,还存储每个broker的主分区位置,用于读写。
分布式系统的选举方式:
zookeeper进行选举
集群初始化时,选择leader。
-
每个服务器节点发起一个投票,广播一个(myId,votes),对i号节点就是(i,0)。
-
每个服务器节点检查接受到的投票的有效性。
-
处理投票。对自己发出的投票和收到的投票进行比较,更新自己的投票,然后把更新后的投票再广播出去。
- 优先选择votes多的节点作为leader
- votes相同,优先选择myId大的节点作为leader
-
对于ZK1而言,它的投票是(1, 0),接收ZK2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时ZK2的myid最大,于是ZK2胜。ZK1更新自己的投票为(2, 0),并将投票重新发送给ZK2。
-
统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于ZK1、ZK2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出ZK2作为Leader。
-
一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。当新的Zookeeper节点ZK3启动时,发现已经有Leader了,不再选举,直接将直接的状态从LOOKING改为FOLLOWING。
集群运行期间,重新选择leader时。(只要leader挂掉,zookeeper停止服务,重选leader)
- 变更状态。Leader挂后,余下的非Observer服务器都会讲自己的服务器状态变更为LOOKING,然后开始进入Leader选举过程。
- 每个Server会发出一个投票。在运行期间,每个服务器上的votes可能不同,此时假定ZK1的votes为124,ZK3的votes为123;在第一轮投票中,ZK1和ZK3都会投自己,产生投票(1, 124),(3, 123),然后各自将投票发送给集群中所有机器。
- 接收来自各个服务器的投票。与启动时过程相同。
- 处理投票。与启动时过程相同,由于ZK1事务ID大,ZK1将会成为Leader。
- 统计投票。与启动时过程相同。
- 改变服务器的状态。与启动时过程相同。
redis的哨兵集群选举
少部分节点作为哨兵节点,不断和节点通信,了解节点状态;当主节点宕机,哨兵节点选择较新的一个从数据库作为新的主数据库
去中心化选举
raft选举(etcd)
Kalfa生产消费模式
点对点(1生产者—消息队列—1消费者)
发布订阅(n生产者—消息队列—n消费者)
读写流程
写流程
消息写到哪个分区?
- 主分区(leader partition),复制分区用于选举和故障恢复。写进程先连接zookeeper,拿到各个节点的主分区位置信息,然后选择一个节点进行写入。
- 使用Kalfa API:高级API是已经定义了一些解决方案的API,低级API只提供了Kalfa的基本接口。
- 高级API例子:
- 1.随机写入一个节点。
- 2.hash方式,每个消息都是key+message,对key%partitions.length,存储到对应的分区。
- 3.轮询方式,需要记录上次写入的位置,每次写入上一次写入的下一个位置。
如何确保消息的可靠到达?
- 请求回应方式,数据写入请求携带一个ack字段。
-
ack=0,表示请求不需要返回,写没写如我不关心。(20w条/s)
-
ack=1,表明数据写入成功的时候返回。(可能刚写入数据的节点宕机了,其他副本还没有更新,所以仍然是不可靠的)
-
ack=-1,表明数据写入之后,还需要等待不同broker的数据同步完成才返回。(最可靠,效率最低)(10w条/s)
-
上图是ack=-1时的流程,生产者先获得所有节点主分区的位置,然后用某种策略写入一个节点的主分区,然后更新其他节点的副本,在最后第4步返回一个ack
-
kalfa的订阅发布模式和redis数据库的是不同的
- redis是推数据,某节点更新了,广播自己更新的消息;
- kalfa是拉,和consumer流程是一样的,主动监听其他副本,有变更时改变自己
-
读流程
消息从哪个分区读取?
- 点对点模式,消费者指定partition,直接取数据。
- 发布订阅模式,存在多个partition。
- 因此需要知道每个分区已经消费到哪个位置了,从那个位置开始消费。(offset而已)
- 0.9以前,每个分区的消费信息存储在zk中,但是zk是一个高可用性,强一致性(同步与等待)的kv存储,节点负担大。zk只有一个主节点,采用paxos算法。
- 0.9之后,采用一个额外的主题(包含多个partition)用来存储分区被消费的位置信息。
- 因此需要知道每个分区已经消费到哪个位置了,从那个位置开始消费。(offset而已)
消费者组如何消费?
- 消费者组和partition如何对应(上图包含多个consumer和多个partition)
- 消息生产时,是按顺序存储的(末尾添加,每个Topic内核broker内都是顺序的)
- 因此,尽量保证消费消息有序执行,至少一个分区内部的消费要有序
- 一个分区只能有一个消费者,一个消费者可以对应多个分区
- 当消费者数量大于分区数量,多出来的消费者是多余的
- 但是这不是全局有序,因为消息是按不同策略写入不同分区的,所以只能做到局部有序
如何确保消息被消费
- 消息没存进kalfa
- 消息没执行对应职能(消息从kalfa被取出,但没有被消费者收到,offset就后移了)
consumer和partition的对应策略:高级API的rebalance策略(修正对应关系)。
- 因为consumer和partition都可能宕机,会导致对应关系的破坏和更新。
- *方案一,range平均策略,按节点数分成n组,第i组给第i个consumer
- *方案二,轮询策略,轮询consumer,每次分配一个partition给当前询问的consumer
- 方案三,粘性方式(尽量维持原来的对应关系)没有rebalance时采用轮询方式,发生rebalance时,尽量维持原来的关系,对被破坏的对象采用轮询方式。
只有方案三是正确的。因为consumer或partition数量变化会导致整个集群停止服务,rebalance,再对外提供服务。因此要减少rebalance的代价。
对应关系是一个socket连接,难以重现建立。
如何确保消息被消费?
使用低级API
-
提交策略:自动提交 x 手动提交 √
- 自动提交是在数据库操作返回值后自动增加offset值,如果数据库操作失败,数据没有写入,offset后移,那么数据就丢失了;所以设置手动提交(offset不再存储在额外主题,而是其他外部数据库中)。
-
事务
- 在数据写入DB时成功,但是在DB操作返回consumer时,consumer宕机,数据没有提交到broker,出现数据不一致(重复消费/消息丢失)
- 因此把offset存储在mysql,把数据提交和存储放在mysql支持的一个事务里就行了。
应用:分布式延时队列
消费者可以在定时结束后执行消费任务。
kalfa
- 创建额外的主题
- 创建额外的定时进程,监测这个主题,把过期的消息转存到另一个消息队列中;消费区去另一个具体的消息队列取消息。
rabbitmq
redis集群
go语言实例
第一行等价于ack=-1
第二行选择分区函数是轮询(roundRobin)
第三行表明执行成功后是否需要返回。
- 创建额外的定时进程,监测这个主题,把过期的消息转存到另一个消息队列中;消费区去另一个具体的消息队列取消息。
rabbitmq
redis集群
go语言实例
[外链图片转存中…(img-JL5u3W0M-1653474782166)]
第一行等价于ack=-1
第二行选择分区函数是轮询(roundRobin)
第三行表明执行成功后是否需要返回。