使用 MQTT 进行进程间通信的消息设计

2018-01-27

1、为什么需要进程间通信

复杂业务系统的实现大体有两种相对的实现思路,一是做一个大而全的程序,这个程序什么都管,完整地实现了该业务系统的所有功能;二是把复杂系统分解为一系列功能点,再对应做多个程序来共同实现整体功能。后一种思路正是 UNIX 哲学的体现:

程序应该只关注一个目标,并尽可能把它做好。让程序能够互相协同工作。应该让程序处理文本数据流,因为这是一个通用的接口。

那么,这就是进程间通信的背景。多个各自独立运行的程序,为了协作完成系统的整体功能,就必然需要进行数据交互,也即是所谓的进程间通信。

2、MQTT 消息的特点

2.1、基于主题“订阅、发布”的消息通知

这是 MQTT 消息的传递的最大特点。而其带来的一大好处就是使得消息的发送和接收方解除耦合,各自遵循事先定义的主题和消息组装格式即可。特别是在消息多生产者多消费者的复杂场景中,基于主题的订阅与发布机制依然是能够轻松应付的。

2.2、基于文本的数据流

消息的发布需要两个元素,一个是 topic(主题),另一个是 payload(负载)。topic 就是自定义的一个字符串,正是 UNIX 哲学中提到的文本数据流。这就很通用而且十分灵活了。例如常见的就会通过斜杠 / 来在一个 topic 中区分多个级别。下一节中的消息设计就可以看到。

然后 payload 中的数据,我们也可以组织为文本数据流,那就是使用非常轻量而通用的 json 格式。

2.3、其他特点

其他特点如消息封装开销小、传输模式支持多种 QoS 等等,我们这里不是特别关注,不细说。

3、三种角度的消息设计

基于 MQTT 的进程间通信的消息设计,正如 2.2 节中所说,包含 topic 和 payload 两方面的设计。

需要传递的消息 payload 除了数据量非常大的要用到共享内存等方式,其他的基本都可以使用 json 文本来组织传递。在不同的业务系统中,消息内容千差万别,所以 json 的结构设计以后有机会再举例解说。

下面主要说说 topic 的设计。不要小看这个一小段的 topic 字符串的设计,不同的角度设计将会影响到整个系统结构的设计。这里说的“角度”包括三种:基于消息数据的角度,基于消息接收者的角度,以及基于消息发送者的角度。

为了便于讲解,我们先来设定一个系统中有如下几个进程、数据以及他们的关系:

进程/数据 data1 data2
prog_a producer producer
prog_b consumer producer
prog_c consumer consumer

注:producer 即产生数据需要发送给其他进程的进程,而 consumer 只接收其他进程产生的数据的进程。

3.1、基于消息数据的主题设计

基于消息数据的意思是,topic 这个文本串的字面含义正好反映了 payload 中携带的数据内容。

我们设计一个系统中的 MQTT topic 和 payload json 格式,就先要把所有进程需要传输的数据做一个统一梳理,进行分门别类,并分出不同层次。然后就可以通过在每个 topic 中设置对应的字段来表示该 topic 所携带的数据的性质。一目了然。

对于上表中的进程和数据,我们就从数据出发,设计两个 topic,分别是:

/data1

/data2

需要接收数据的 consumer 进程就订阅对应的 topic,producer 进程就负责发布。

具体举例我们的一个项目中传输的数据其实包括配置信息、采集数据,而采集数据又分为实时采集数据和分析后提取的数据。那么我们设计一个 topic 用来传输实时采集数据,就可以设计为 /data/realtime/1 。最后的 1 表示通道一,以与其他通道区分,前面的 data 就与配置的 topic config 进行区分。那么这里 topic 里读出来的字面意思就是描述数据本身的,不关心发送者是怎么样的,不关心谁是接收者。

3.2、基于消息接收者的主题设计

面对上表中同样的进程和数据,从消息接收者角度出发,第一是 prog_b 接收 data1,第二是 prog_c 接收 data1,第三是 prog_c 接收 data2。对应的三个 topic 就分别为:

/prog_b/data1

/prog_c/data1

/prog_c/data2

这样,看 topic 就会知道这条消息谁会接收,接收的内容是什么。

当然,接收的数据内容在 topic 中不会像 3.1 中的角度那样表示。更进一步,可以把后两个 topic 合并为一个,因为都是同一个接收者,data1 和 data2 的区分可以在 payload 的 json 数据组织里区分。简单的做法就是 json 里添加一个 message ID 字段。这在实现的时候就可以把所有数据的接收处理都放在一个 topic 的处理函数里分别处理。

3.3、基于消息发送者的主题设计

从消息发送者角度出发设计,有三个 topic:

/prog_a/data1

/prog_a/data2

/prog_b/data2

这个设计相对来说是比较接近基于数据的设计的。他在描述数据的基础上表明了是谁发送的。一般来说数据生产者很清楚自己生产的是什么数据,却不太确定都有谁关心这些数据。那他就只管生产,只管发布了。

3.4、三种设计方法的对比

第一种在 MQTT 框架中是最正宗、最符合其框架原本设计的 topic 设计角度。抽象层次高,可扩展性好。先考虑好这个系统有哪些数据,就设计对应的 topic,不用关心到底谁会生产这些数据,谁会消费这些数据。生产者和消费者的设计实现可以在后续随意变更,对 topic 的先前设计没有影响。缺点就是抽象层次高,编码实现的时候就会变得有点“无从下手”,对整个系统理解起来也没那么顺畅。

相对来说,第二种从消息接收者角度的设计是三种中耦合度最高的。一个 topic,已经定好了谁会订阅他,而对于数据生产者来说,在发布他生产的数据的时候,也要知道谁在关心这些数据,从而发布对应的 topic 。在系统复杂度较低的时候可以使用这种设计,耦合度是高点,但更具体了,实现的时候 topic 设计和编码就联系紧密。容易理解。

那第三种基于发送者角度的设计,介于前面两者之间,作为一种折中方案。更应该尝试。

2018-01-27