微服务(三)


目录

  • 微服务(三)
  • Spring Cloud Bus 消息总线
  • Rabbit MQ
  • 到底什么时候该用MQ?
  • 什么时候使用MQ?
  • 消息总线的必达
  • 消息总线的幂等
  • 延迟消息


Spring Cloud Bus 消息总线

在微服务架构系统中,通常会使用轻量级的消息代理来构建一个公用的消息主题 ,让系统中所有微服务实例都连接起来。由于该主题中产生的消息都会被各个监听实例消费,因此称为消息总线。消息总线可用用于配置信息的变更和统一操作管理,它的使用范围很广,也是微服务架构中的必备组件,通常可以配合spring cloud config 实现微服务应用配置信息的动态更新。

spring cloud bus架构

企业服务总线和微服务 服务总线 微服务_执行时间


在我目前看来,rabbitMQ就是用来动态更新配置信息。

企业服务总线和微服务 服务总线 微服务_执行时间_02


1、config(分布式配置中心):更改配置信息

2、actuator(监控中心): 监控配置信息、手动调用 client1的actuator/bus-refresh方法,刷新client1中配置参数

3、bus (消息总线):client1发现配置参数有变、将消息发送到消息总线,消息总线收到消息后将此消息散发到所有client,client将更新配置,而不需要再手动去调用actuator中的refresh方法

Rabbit MQ

首先来了解两个东西:

MQ:Message Queue 消息队列,是应用程序和应用程序之间的通信方法

AMQP:Advanced Message Queuing Protocol 高级消息队列协议

Erlang:面向并发的编程语言

一共有五种常用队列:

企业服务总线和微服务 服务总线 微服务_执行时间_03


work模式:能者多劳

一个生产者只能被多个消费者中的一个消费,消费掉就没了

publish/subscribe订阅模式:

企业服务总线和微服务 服务总线 微服务_解耦_04


解读:

1、1个生产者,多个消费者

2、每一个消费者都有自己的一个队列

3、生产者没有将消息直接发送到队列,而是发送到了交换机

4、每个队列都要绑定到交换机

5、生产者发送的消息,经过交换机,到达队列,实现,一个消息被多个消费者获取的目的

注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费

Routing 路由模式:

企业服务总线和微服务 服务总线 微服务_企业服务总线和微服务_05


主题模式:

企业服务总线和微服务 服务总线 微服务_消息总线_06

到底什么时候该用MQ?

并不是什么时候有可以用MQ的、MQ作为互联网分层架构中的解耦器,可以进行“逻辑解耦+物理解耦”

使用场景:上下游消息通信服务,上游发送消息到MQ、下游通过MQ接收消息,上下游不存在逻辑依赖关系或者说调用方不是实时离开执行结果的业务场景可以使用MQ来进行解耦

企业服务总线和微服务 服务总线 微服务_解耦_07

什么时候使用MQ?

场景一:数据驱动的任务依赖
什么是任务依赖,举个栗子,互联网公司经常在凌晨进行一些数据统计任务,这些任务之间有一定的依赖关系,比如:

1)task3需要使用task2的输出作为输入

2)task2需要使用task1的输出作为输入

这样的话,tast1, task2, task3之间就有任务依赖关系,必须task1先执行,再task2执行,载task3执行。

企业服务总线和微服务 服务总线 微服务_执行时间_08


对于这类需求,常见的实现方式是,使用cron人工排执行时间表:

1)task1,0:00执行,经验执行时间为50分钟

2)task2,1:00执行(为task1预留10分钟buffer),经验执行时间也是50分钟

3)task3,2:00执行(为task2预留10分钟buffer)

这种方法的坏处是:

1)如果有一个任务执行时间超过了预留buffer的时间,将会得到错误的结果,因为后置任务不清楚前置任务是否执行成功,此时要手动重跑任务,还有可能要调整排班表

2)总任务的执行时间很长,总是要预留很多buffer,如果前置任务提前完成,后置任务不会提前开始

3)如果一个任务被多个任务依赖,这个任务将会称为关键路径,排班表很难体现依赖关系,容易出错

4)如果有一个任务的执行时间要调整,将会有多个任务的执行时间要调整 

企业服务总线和微服务 服务总线 微服务_解耦_09


优化方案是,采用MQ解耦:

1)task1准时开始,结束后发一个“task1 done”的消息

2)task2订阅“task1 done”的消息,收到消息后第一时间启动执行,结束后发一个“task2 done”的消息

3)task3同理

采用MQ的优点是:

1)不需要预留buffer,上游任务执行完,下游任务总会在第一时间被执行

2)依赖多个任务,被多个任务依赖都很好处理,只需要订阅相关消息即可

3)有任务执行时间变化,下游任务都不需要调整执行时间

需要特别说明的是,MQ只用来传递上游任务执行完成的消息,并不用于传递真正的输入输出数据。

场景二:上游不关心执行结果
上游需要关注执行结果时要用“调用”,上游不关注执行结果时,就可以使用MQ了。

举个栗子,58同城的很多下游需要关注“用户发布帖子”这个事件,比如招聘用户发布帖子后,招聘业务要奖励58豆,房产用户发布帖子后,房产业务要送2个置顶,二手用户发布帖子后,二手业务要修改用户统计数据。

企业服务总线和微服务 服务总线 微服务_解耦_10


对于这类需求,常见的实现方式是,使用调用关系:

帖子发布服务执行完成之后,调用下游招聘业务、房产业务、二手业务,来完成消息的通知,但事实上,这个通知是否正常正确的执行,帖子发布服务根本不关注。

这种方法的坏处是:

1)帖子发布流程的执行时间增加了

2)下游服务当机,可能导致帖子发布服务受影响,上下游逻辑+物理依赖严重

3)每当增加一个需要知道“帖子发布成功”信息的下游,修改代码的是帖子发布服务,这一点是最恶心的,属于架构设计中典型的依赖倒转,谁用过谁痛谁知道(采用此法的请评论留言)

企业服务总线和微服务 服务总线 微服务_消息总线_11


优化方案是,采用MQ解耦:

1)帖子发布成功后,向MQ发一个消息

2)哪个下游关注“帖子发布成功”的消息,主动去MQ订阅

采用MQ的优点是:

1)上游执行时间短

2)上下游逻辑+物理解耦,除了与MQ有物理连接,模块之间都不相互依赖

3)新增一个下游消息关注方,上游不需要修改任何代码

场景三:上游关注执行结果,但执行结果很长

有时候上游需要关注执行结果,但执行结果时间很长(典型的是调用离线处理,或者跨公网调用),也经常使用回调网关+MQ来解耦。

举个栗子,微信支付,跨公网调用微信的接口,执行时间会比较长,但调用方又非常关注执行结果,此时一般怎么玩呢?

企业服务总线和微服务 服务总线 微服务_解耦_12


一般采用“回调网关+MQ”方案来解耦:

1)调用方直接跨公网调用微信接口

2)微信返回调用成功,此时并不代表返回成功

3)微信执行完成后,回调统一网关

4)网关将返回结果通知MQ

5)请求方收到结果通知

这里需要注意的是,不应该由回调网关来调用上游来通知结果,如果是这样的话,每次新增调用方,回调网关都需要修改代码,仍然会反向依赖,使用回调网关+MQ的方案,新增任何对微信支付的调用,都不需要修改代码啦。

消息总线的必达

保证消息的必达就是为了防止消息重复发送所引发的问题.

消息必达在架构设计上需要关注的点:

消息收到先落地 (先存入数据库)

消息超时 重传 确认 保证消息必达

企业服务总线和微服务 服务总线 微服务_企业服务总线和微服务_13


上图是一个MQ的核心架构图,基本可以分为三大块:

(1)发送方 -> 左侧粉色部分

(2)MQ核心集群 -> 中间蓝色部分

(3)接收方 -> 右侧黄色部分

粉色发送方又由两部分构成:业务调用方与MQ-client-sender

其中后者向前者提供了两个核心API:

SendMsg(bytes[] msg)

SendCallback()

蓝色MQ核心集群又分为四个部分:MQ-server,zk,db,管理后台web

黄色接收方也由两部分构成:业务接收方与MQ-client-receiver

其中后者向前者提供了两个核心API:

RecvCallback(bytes[] msg)

SendAck()

MQ是一个系统间解耦的利器,它能够很好的解除发布订阅者之间的耦合,它将上下游的消息投递解耦成两个部分,如上述架构图中的1箭头和2箭头:

(1)发送方将消息投递给MQ,上半场

(2)MQ将消息投递给接收方,下半场

四、MQ消息可靠投递核心流程

MQ既然将消息投递拆成了上下半场,为了保证消息的可靠投递,上下半场都必须尽量保证消息必达。

企业服务总线和微服务 服务总线 微服务_企业服务总线和微服务_14

MQ消息投递上半场,MQ-client-sender到MQ-server流程见上图1-3:

(1)MQ-client将消息发送给MQ-server(此时业务方调用的是API:SendMsg)

(2)MQ-server将消息落地,落地后即为发送成功

(3)MQ-server将应答发送给MQ-client(此时回调业务方是API:SendCallback)

MQ消息投递下半场,MQ-server到MQ-client-receiver流程见上图4-6:

(1)MQ-server将消息发送给MQ-client(此时回调业务方是API:RecvCallback)

(2)MQ-client回复应答给MQ-server(此时业务方主动调用API:SendAck)

(3)MQ-server收到ack,将之前已经落地的消息删除,完成消息的可靠投递
如果消息丢了怎么办?

MQ消息投递的上下半场,都可以出现消息丢失,为了降低消息丢失的概率,MQ需要进行超时和重传。
上半场的超时与重传

MQ上半场的1或者2或者3如果丢失或者超时,MQ-client-sender内的timer会重发消息,直到期望收到3,如果重传N次后还未收到,则SendCallback回调发送失败,需要注意的是,这个过程中MQ-server可能会收到同一条消息的多次重发。
下半场的超时与重传

MQ下半场的4或者5或者6如果丢失或者超时,MQ-server内的timer会重发消息,直到收到5并且成功执行6,这个过程可能会重发很多次消息,一般采用指数退避的策略,先隔x秒重发,2x秒重发,4x秒重发,以此类推,需要注意的是,这个过程中MQ-client-receiver也可能会收到同一条消息的多次重发。
MQ-client与MQ-server如何进行消息去重,如何进行架构幂等性设计,下一次撰文另述,此处暂且认为为了保证消息必达,可能收到重复的消息。

消息总线的幂等

上半场

MQ-client生成inner-msg-id,保证上半场幂等。

这个ID全局唯一,业务无关,由MQ保证。

下半场

业务发送方带入biz-id,业务接收方去重保证幂等。

这个ID对单业务唯一,业务相关,对MQ透明。

结论:幂等性,不仅对MQ有要求,对业务上下游也有要求。

延迟消息

环形队列是一个实现“延时消息”的好方法,开源的MQ好像都不支持延迟消息,不妨自己实现一个简易的“延时消息队列”,能解决很多业务问题,并减少很多低效扫库的cron任务。