这样每台机器就可以处理符合其当前水位的连接。
在现实开发中,我们可能不仅仅满足于一个如此简单的消息系统,我们可能想要有离线消息,数据统计,数据缓存,限流等一系列操作,所以我们还可以再优化一下架构:
将整体架构划分成业务逻辑层和数据存储层;
数据存储层又可以根据存储数据类型的不同来进一步划分;
前端可以单独划分一个网络接入层;
数据包的流向可以用 MQ 来串联;
这样我们可以得到以下的图 3:
在这个模型中,网络接入层和消息业务逻辑层整体上应该是一个 stateless 的模块,可以较为轻松地做横行扩展。存储层作为一个有状态的模块,想要做到横行扩展是一件很不容易的事情。如果撇开这点来看,至此,这个模型理论上在应对海量用户的场景下应该是有效的。
通信协议和技术栈的选择
做一个消息系统,不可避免地要涉及到对通信协议的选择。我们在对通信协议的选择上,遵循以下几个原则:
协议尽可能精简轻量,因为在系统设计之初我们就考虑了对物联网的支持,省电,节约流量都是目标之一;
通用性好,扩展性强,方便后期做特性开发;
协议在业界被广泛认可,且尽可能多的有不同语言的开源实现,以方便不同技术栈的客户做集成;
综上,我们没有重新自定义一份通信协议,而是选择了基于 长连接 的 MQTT 。从很多角度来看,MQTT 非常适合做消息总线的通信协议,而且协议栈也足够轻巧和易于实现。云巴实时消息系统传输的消息体积较小(一般小于 4 KB),比如控制信号,普通聊天信息等。就这点上,针对物联网设计的 MQTT 有着天然的优势。后面,在不断地研究中我们又发现,MQTT 其实不仅仅适用于物联网场景,在很多要求低延迟高稳定性的非物联网场景也同样适用(比如手机端 app 推送,IM,直播弹幕等)。
从前面几个章节我们看到,云巴消息系统是一个典型的 IO 密集型系统。在出于开发效率和稳定的考虑下,我们选了 Erlang/OTP 作为主力开发语言。Erlang/OTP 作为一门小众开发语言(无论是国内还是国际),在应付这类 IO 密集型系统上,有着得天独厚的优势(可参考 RabbitMQ 这个基于 Erlang/OTP 的著名开源项目):
基于 actor 的进程创建模型,可以为每个数据包创建一个 Erlang 处理进程,充分利用多核;
OTP 的开发框架抽象了分布式开发的许多细节,使得开发者在很小的心智负担下就能轻松快速地开发出功能原型;
Erlang/OTP 充分运用了容错思想,应对异常不是防,而是容,很多时候我们写出一些安全逻辑上有漏洞的代码,在 Erlang/OTP 上居然也能工作得好好的;
随着不断深入地使用 Erlang/OTP, 其性能问题也渐渐凸显出来。我们发现,当客户端请求量增加的时候,用 Erlang/OTP 写出的模块轻而易举地就可以将 CPU 跑满,从而让当前实例超负荷运转。很多时候出于成本上的考量,我们无法选择更多核数的机器来提升 Erlang 虚拟机运行的性能(此点未明确验证过),所以只好选择适度增加服务处理实例来缓解压力。
不过,通过对业务模块更细粒度的划分,我们可以将一些核心的小模块用 C/C++ 语言改写,在一定范围的复杂度内,可以有效提升整体处理性能。这也是我们接下来优化核心系统的思路之一。
MQTT 的 Pub/Sub 模型与高可用 KV 存储
MQTT 协议采用的是 Pub/Sub 的编程模型。其中有三个比较关键的动作: publish, subscribe 和 unsubsribe 。通过前面几个章节的讨论,我们又可以得到这么一个场景:
假如存在一个订阅量巨大的 topic(百万级),如何在单次 publish 中保证实时性 ?
其实,解决思路跟之前的场景是一致的: 分而治之 。我们必须通过某种策略对 topic 进行分片,然后将分片分发到不同的 publish 模块上进行处理。在一定的算法复杂度下,这个问题理论上是可以被有效解决的。于是,topic 的分片策略就成了高性能 publish 的关键。其实,如果想采用 MQTT 做海量消息系统,订阅关系的管理一定是无法绕开的大问题。它主要有以下几个设计难点: