RabbitMQ 高级指南——实现分布式通信

1,791 阅读10分钟
原文链接: mp.weixin.qq.com

分布式通信

分布式系统是指:通过网络把多个组件连接起来,并提供组件之间消息传递和协作的系统。分布式系统要解决的问题很多,异构、伸缩性、开放、安全、容错等,但是基本问题是——提供组件之间消息通信。没有这个基础其他的都无从谈起,“异构”、“伸缩”、“开放”、“安全”、“容错”其实都是为了更好的通信而要解决的问题。。分布式通信有两种方式

  • 直接通信

通信双方直接直接调用对方接口相互传递数据,Client发送数据到Server,Server响应请求返回数据到Client。我们所接触到的大部分系统都采用这种通信方式,比如REST API、Netty写的服务器或者自己写的Socket服务器。这些系统的区别仅仅是通讯协议不一样,本质上都可以用一幅图表示。


直接通信非常脆弱,主要体现在空间耦合,通信一方一定要知道对方的“地址”才可以通信(比如对端IP地址);时间耦合,通信一方发送数据的对端必须“在线”否则此次通信失败。要解决这个问题其实很简单,伟大先驱David Wheeler说过:

计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,除非这个问题是由于太多中间层引起的。(All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections.)

(我给出的是“完整版”的引用,特别注意“except of course for the problem of too many indirections”,多科学)

  • 间接通信

通信双方不直接调用对方而是通过第三方中转请求,通过中间层来解决“时间耦合”和“空间耦合”。这种方式不常见到,也是本文要介绍的主要内容。

通信的两个误区

  • 世界上没有同步通信

《UNIX网络编程》总结了5种I/O模型,其中一种叫异步I/O,特别是书中最后讨论了POSIX对同步I/O和异步I/O的定义,很容易让人“望文知义”以为会有“同步”。不妨仔细看看这段文字,它仅仅是POSIX的对I/O的定义,它对同步的理解是A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes(导致请求进程阻塞, 直到 IO 操作完成)。如果我们把网络看成I/O,同步就是指把数据包交给网卡而不是指把数据包发送到对端。POSIX做这个定义的原因是由于有些设备支持DMA操作不需要由CPU把数据搬到设备,所以才引入了“同步”、“异步”两个属于来表示两种I/O通讯方式。有DMA的情况下直接由设备取数据,没有DMA的情况下必须由CPU把数据指派给设备。同步是指一个事件的产生对每个系统的影响在时间上是一致、统一的。如果是真正的同步数据产生之后一定是同时作用于两个系统而不涉及到任何“数据传递”过程,这种模型在世界上根本不存在(即便是“同时作用于两个系统”也需要传递,两个系统的网络也有好有坏,时间也会有有长有短不可能完全统一)。

  • 所有的异步操作都要有超时

无论是Http请求一次进程间通讯,所有的异步操作都应该有Timeout否则你的程序很可能永远等待。我们用HttpClient或者Python的Requests的时候如果连接不上对方服务器会抛出异常,其实这个异常不是由于“连接不上”抛出的而是由于“Timeout”引起的。不要相信对方服务器会永远在线,要给所有的异步操作设置上timeout,如果你觉得设置成永远等待没问题那么我劝你“保守”一点设置成50年。

阻塞和非阻塞

直接通信和间接通信是从通信模型上的划分,从通信方式上分为阻塞非阻塞。(世界上没有异步通信同步通信的划分方法,所有的通信都是异步。同步、异步仅仅是针对POSIX中I/O模型的术语,具体原因前面已经解释过了)

  • 阻塞通信,通信一方发送数据后在对端没有回应之前不会发送接下来的数据。

  • 非阻塞通信,通信一方发送数据后不必等待对端回应直接发送接下来的数据。

    阻塞通信因为需要“等待回应”所以它的效率不如非阻塞通信高,非阻塞通信又没有办法保证通信的质量。所以有一种介于二者之间的通信方式——基于滑动窗口。发送方一次发送多个数据然后挨个等待回应,没有收到回应的则重发。上面听起来是不是像计算机网络?其实计算机科学中很多东西都是相互借鉴的,比如上面的滑动窗口的概念你如果有一堆HTTP API需要调用并且要保证所有的API都调用成功,那么用这种批量发送+批量确认的方式是可以提高吞吐率的。

    RabbitMQ实现间接通信

    借助RabbitMQ我们可以实现间接通信,可以通过它实现阻塞和非阻塞两种方式(再次重申,没有异步、同步的说法)。

    非阻塞

    先说简单的非阻塞通信,借组MQ是非常直接的,通信的发送方是Publisher,接收方是Consumer。同时借助RabbitMQ强大的Exchange概念我们可以非常灵活的实现。发送方查看图片
    接收方查看图片
    首先借助间接通信发送方和接收方没有直接依赖关系,发送方在发送数据的时候不需要指定接收方的任何信息,只要指定routingKey(对数据的描述)这实现了空间解耦;发送方在发送数据的时候不要求接收方在线(甚至可以不存在),这实现了时间解耦从接收方看,接收数据的时候不需要知道发送方的任何信息只需要定义自己感兴趣的数据(对数据的描述);也不要求发送方在线,数据世界上是由第三方中转的。这个模型非常符合我们的一般认识,我们要给女朋友写信(好土鳖。。。写信。。)不会自己亲自把信送到女朋友手里而是通过邮局,借助邮局的强大邮政网络帮自己送信。

    阻塞通信

    通信都是非阻塞的,除非我们主动“等待”。所以阻塞通信其实是建立在非阻塞的基础上,过程如下:

  • 发送方临时产生一个Queue,定义routingkey(其实就是queue的名称);

  • 发送方发送数据,在数据头中包括“reply_queue”;本地设置“计时器”(timeout时间)

  • MQ推送数据到接收方

  • 接收方用“reply_queue”作为routingkey发送返回信息给Broker

  • Broker推送数据到发送方

  • 如果发送方在收到数据之前timeout,则停止等待,抛出timeout异常

发送方查看图片
接收方查看图片
上面的过程有两个不易察觉的问题

  • 发送方超时的时候Reply Queue由谁来删除

AMQP中Declare Queue的时候可以设置autoDelete,这个表示,该Queue会在消费者首次连接后如果没有任何消费者则删除。简单理解就是专门针对某个consumer生成的Queue,如果consumer断开则Queue会被Broker删除。默认的情况下Java API中queueDeclare这个标识为True

  • 再次上线的接收方做“无用功”

因为时间解耦,发送方发送数据的时候接收方可能不在线(甚至过了很久以后接收方才上线)。当接收方再次上线数据可能已经失去了“时效性”(发送者已经产生了timeout异常,reply queue也被从broker删除)。为了解决这个问题我们可以给“消息”设置一个TTL查看图片
这条数据会在Queue中存在2秒如果没有被消费则会被删除

MQ是通信的简洁之道

分布式系统中主流的通信方案有两种

  • 以HTTP为代表的REST。这个方案几乎是主流,很多微服务系统也都采用这种架构,但是缺点非常明显不支持双向通信(没有长连接)

  • 自己设计直接基于TCP/IP协议开发的RPC。比如gRPC、或者自己用Netty写的RPC Framework。这种架构是非常难以适合“异构”环境,必须有一个庞大的客户端生态圈(比如像gRPC一样)

这两种通信方式都是原始通信方式,我们使用TCP/IP协议、私有协议或者HTTP都非常痛苦,必须处理很多细节(时间耦合的问题、扩展问题、速度问题、双向通信问题…………)。现代化的通信方式应该提供一层“封装”,可以忽略底层网络让我们只关注应用本身,构建应用层通信协议RabbitMQ通信模型属于间接通信而ZeroMQ通信模型属于直接通信,从设计上来说它更加纯粹,纯粹到直接用Socket这种方式提示别人——“老子是通信协议不是MQ!!!”。但是这种提醒好像是徒劳的,我看到过很多人在吐槽“ZeroMQ丢消息”。这背后隐藏的是——对ZeroMQ力量的一无所知。它解决的是通信问题,而且是直接通信,所以“丢消息”是它的正常反应(想要可靠?你得自己实现!!!)。

微服务???

微服务的本质是分布式系统,它的要解决的问题更加突出——通信。协调各个组件的之间的通信,为各个组件提供运行环境,组件专注于“应用层”的事情。所以我觉得微服务架构和ESB(或者WebService)最大的区别应该是——微服务提供的一个“交互式架构”它的基础应该是分布式组件,每个组件不但可以调用别的组件也可以被别的组件调用。ESB则不行,每个服务仅仅是“服务”它暴露的是endpoint。如果整个系统是一个“由积木”堆成的那么这种理想是可以被实现的。而现实情况是系统通常是“错综复杂的交互”,每个“服务”不但是服务的提供者也是服务的消费者。所以通信必然是双向的否则就会犯ESB一样的“理想主义错误”。

Pieter Hintjen

Pieter Hintjens是ZeroMQ的作者,也是AMQP协议最早的发起者之一。2016年4月他写下最后一篇Blog《A Protocol for Dying》选择“安乐死”,他的twitter定格在10月4日下午一点。(如果你经常看IT新闻的话应该能看到这篇《死亡协议》)查看图片
ZeroMQ和《ZeroMQ指南》是他留着这个世界上宝贵财富,他对分布式系统的间接非常值得我们学习和研究。


欢迎关注公众账号了解更多信息“写程序的康德——思考、批判、理性”

查看图片