这个话题源自于我司一个项目的技术架构升级,由原来的模块编程调整为微服务编程,鹏哥所在的研发部负责提供升级的解技术支持。之前系统由4个模块组成,约定4个模块是通过Activity MQ解耦,但是模块之间难免会有一些需要等待对方返回才能下一步的调用,之前的解决方案是使用Activity MQ 的同步调用,现在系统升级改为使用RabbitMQ代替原来的ActivityMQ,异步的替换没有啥问题,但是同步的时候负责的开发就有点懵了,因为我的同事并没有提供rabbit mq 同步调用的方案,项目升级因此中断而造成了延误。

中午正好跟项目的负责人一起吃饭,他一直在跟我吐槽我们研发部对他们项目升级支持不够,没有提供MQ同步方法的支持,鹏哥听了火冒三丈,脑子一热直接怼了过去:MQ 产生的意义是什么,MQ的出现就是为了解耦,你们把他用在同步调用上,还怎么实现解耦,同步调用直接rest api 调用不就行了,使用MQ多此一举。对面项目经理瞬间无话,至此切换下一个话题。

吃完饭回来,冷静下来之后,想想MQ支持同步其实也不是没有好处,比如:

  • 多实例间负载均衡
  • 服务发现治理
  • 快速服务横向扩容

幸亏当时对方经理对rabbitMQ不是很熟,不然丢人估计就丢大了,哈哈。

好了废话不多说,我们言归正卷。rabbit 实现同步调用其实依赖与其私有队列和发送确认,既利用声明一个不指定名称的队列,靠rabbit 来生成一个唯一的队列名,同时将这个队列名赋值给消息头里面的reply_to字段,以告诉生产者,这是一个返回信息。

原理很简单,我们来看一个具体的案例。我们先假设服务A需要调用服务B来完成A的业务逻辑,加入服务B有两个节点。

多实例间负载均衡

我们先来想一下,模块编程时代,我们是如何实现负载均衡的:我们会在服务B外层架一个负载均衡服务器,如nginx 等,然后再A调用B的时候,其实是通过nginx 代理的方式,路由到其中一个B服务的节点上,如下图:

ad261af9da5f3ac0c13b668796f9d7fd.png

这种行业的标准解决方案已经被业界无数次的验证可行性,这里就不多赘述了。现在我们来看一下MQ是怎么实现负载均衡的。这其实MQ天然的特性,并不是因为使用了同步调用才有的。在多个消费者监听同一个队列的时候,rabbit mq 使用简单的轮训以实现消费者的均衡,这种方式巧妙的将负载粗暴的平均到每一个消费实例上。rabbit mq在同步调用案例中,负载均衡的示意图如下:

33bcd7b7debc2b2dbc8a71c0c5ca3b4b.png

服务发现治理

在微服务模式下,我们来想一下eureka的作用。服务A需要调用服务B的一个接口,但是它并不知道这个服务B在那台服务上,也不知道服务B有多少台可以工作的实例,这个时候eureka就出场了。首先服务A和服务B都注册到eureka服务器上,并把eureka的服务列表copy一份到服务器的本地,这样当A需要调用服务B的接口的时候,就可以根据服务路由表快速的找到可以提供服务的实例。
那模块编程时代,这个功能靠什么实现呢?rabbit mq 有一次拯救了我们。服务A在调用B的接口的时候,并不需要知道提供服务的是谁,只需要往对应的queue上发送消息即可,rabbit mq 负责将消息推送给监听这个queue的消费者。

快速服务横向扩容

扩容这个问题,如果是使用Nginx 作为代理服务器的话,每次扩容都需要修改Nginx的配置,将新的服务器信息添加到Nginx配置信息中去。这种方式给每一次的扩容带来工作量和风险。rabbit mq 在对待这个问题上也是天然的支持,当我们发现服务B 当前实例不能满足现在的请求压力的时候,我们只需要将B新部署实例连上对应的队列即可,无需修改任何配置。当我们不需要的这么多实例的时候,也可以直接停掉即可,也不用修改任何配置。

一个基于Spring boot的例子

说了这么理论,现在我们来实现我们吹过的牛。使用Spring boot + rabbit 来实现一个分布式系统的雏形。

新建一个client的项目,配置DirectExchange 和一个Queue,并绑定在一起。

d17747ad67cea840603e3aa1de78d62f.png

最基本的定义方法。

14c4bb42f0046015ad923ef94f9f0b57.png

为了方便测试,我们提供一个rest api 负责发送和接受同步返回的消息。发送MQ的时候我用使用convertSendAndReceive 代替convertAndSend 来实现同步的调用,并接收返回值。

dbde64fc7f807e8039270f89875f9f0d.png

好了。client 端就完成了,现在我们来编写服务端。

536bba945752e24a018dd3ef8b81279f.png

编写一个receiver,负责监听client端定义的queue。并将Process 方法添加返回值。

71317962585a966f700f76cdd9a1a84a.png

你也许会好奇,为什么我没有配置rabbit的任何信息,这是因为,你要你在POM中引入了 rabbit的jar,并且没有修改默认的rabbit的用户名和密码,Spring boot 会默认帮你链接到rabbit上的。

c6a42adc89074326ac219f63ebdfdca6.png

e4b5b2bbc62a2e496917cfdd811d93d9.png

使用docker 启动一个 MQ的实例,如果不会Docker 启动MQ 鹏哥会提供另外一篇博客来简单介绍一下。

df9aa69c7c46cc203f9cf85420bd7080.png

分别启动 client 和Server

bb114ab6aa8a55bbaae1cb1ae4a83adc.png

使用Postman 请求一下client 的API来模拟一个请求,同时client 会去向 Server 发送一个同步请求。

a5cfdc1e786afb1b13566543778374c8.png

client端日志:

7623987dc8f1ade80a470cfee29c1bab.png

server 端日志:

f5293faef94085d4f5093b5d120f806a.png

至此我们使用 rabbit mq 模拟了一个同步的调用,下边我们来模拟我们的高可用和可伸缩。

修改服务端的启动配置,以便我们可以启动多个实例。

5bd5641b731f21889d6a8fff4f103f78.png

在启动完一个实例之后,修改端口号,再次启动,你会发现已经起了两个实例。

我们来启动三个服务端实例,来模拟负载均衡。

2c9b3e72075ebf1ecfbef5575c4d385d.png

使用postman 请求6次刚才的接口,我们在client 端 的日志里面看到了6个请求的返回值。

0fe984b1e7c23c601c63b02a76750560.png

同时均匀的在服务端的3个实例上,分别承载了两次调用。

cbd0bfbae7b9988aa9b253063614dc9a.png
eecc288faf6adffb189d1b218af098f1.png
eac6ef8b386d622b384d5d0957093b92.png

至此我们演示完了这个简易分布式系统的所有的功能。

项目代码地址:https://github.com/linghuxiong/spring-boot-demo/tree/master/spring-boot-rabbit-rpc

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐