1.Mnesia:Erlang语言中的分布式数据库
在EMQX 5.x版本之前,集群数据存储采用的是Erlang/OTP自带的实时分布式数据库管理系统——Mnesia。Mnesia是用Erlang语言实现的,并且与Erlang紧密耦合,这也造就了它的独特之处,它几乎将Erlang变成了一种数据库编程语言。Mnesia可以说是专为用Erlang编写的工业级电信应用程序而设计的,并提供了支持高容错电信级系统所必需的常用功能。它试图解决典型电信系统所需的所有数据管理问题,并具有许多传统DBMS通常不具备的功能。其提供的特性主要包括:
- 快速实时的键值查找;
- 复杂的非实时性查询;
- 分布式数据支持;
- 高容错性;
- 复杂的数据对象。
Mnesia通常支持两种数据访问模式:本地模式和远程模式。本地模式采用的是全连接、点对点的复制模式,即节点中的数据表会复制到集群所有节点中;而在远程模式中,当要访问的表没有本地副本时,会通过RPC调用读取远程具有数据表副本的节点。本地模式访问的缺点是集群扩展性差,且存在脑裂的风险,但优点也显而易见,因为集群中每个节点都拥有集群全量的数据,故而可以通过本地查询来提高检索效率。
相对于远程模式的网络操作而言,本地读取数据的延迟要比远程模式的网络延迟小几个数量级。另外,这种实现方式也能提高集群的分布式容错能力,只要保证集群中仍有存活的节点,集群数据就是全量的、安全的。所以在早期的EMQX实现中,默认使用的就是本地模式。尤其是在消息分发时,通过本地查询Mnesia数据库中的路由表数据快速定位到消息要投递的节点,可以实现个位数毫秒的高效、低延时的消息分发操作。
2.Mnesia的弊端:复制带来的开销
如前文所述,由于Mnesia集群使用全网状的连接架构,集群中每个节点都会与其它所有的节点建立连接,每个节点产生的事务也都被会复制到集群中的所有节点上。这就导致集群的整体可扩展性差:首先,集群中每增加一个节点,集群数据同步的开销也会随之增大,且由于网络问题导致的集群脑裂的风险也会增加。其次,集群中每个节点都要能够承载全量的集群数据,相对于Mnesia这种经常将数据存放在内存中的应用场景来说,服务器资源的投入也会跟着集群规模的扩展而增加,对机器配置和性能的要求也会越来越高。集群节点间的数据复制成本和服务器资源投入这两个问题一直是限制集群扩展性的核心问题。
Mnesia 网状拓扑架构
3.Mria:从全网状到单复制
为了解决Mnesia全网状复制带来的问题,EMQX 5.x版本中引入了新的数据层解决方案实现——Mria。Mria对Mnesia进行了封装,其核心诉求是在实现数据的本地读写的基础上,尽可能地减少集群节点复制的开销。
Mria将原有的全网状复制的Mnesia节点扩展成两种不同的角色节点——核心节点(Core)和复制节点(Replicant)。核心节点与传统的Mnesia节点行为类似,仍旧采用全网状的复制模式,所有核心节点之间的事务仍会复制到其它核心节点上。复制节点则不直接参与Mnesia事务处理,而是连接到集群中某个核心节点上,被动地复制来自核心节点的数据更新。为此,核心节点还同时拥有另外一项重要的工作,即处理连接到自身的所有复制节点的数据处理。
由于复制节点不再参与集群中事务的同步工作,只有少数的核心节点会实时地同步事务,而复制节点只是复制对应核心节点的数据,所以这种实现模式在复制节点可以拥有集群全量数据以实现高效的本地数据检索的前提下,同时能够减少整个集群的事务同步开销。借助于复制节点的特性,当更多的设备需要接入到集群中时,只需要相应地扩展复制节点的数量,让这些节点承载设备连接,而又不会直接增加核心节点写操作的延时,从而达到扩展集群规模的效果。
Mria 单复制拓扑架构
但是Mria这种架构实现也不是银弹,虽然它可以解决全网状复制带来的数据同步问题,但是依然无法很好地处理所有节点都要承载集群全量数据的问题。
另外需要特别注意的是,为了提高Mria架构的复制效率,EMQX官方在Erlang/OTP实现的基础上引入了一个叫做post-commit钩子的实现。如果要应用新的Mria架构,需要使用有此补丁的Erlang/OTP库,否则集群会自动降级到Mnesia的实现模式。遗憾的是,到目前为止,该新特性并未合并到Erlang/OTP官方仓库中,需要研发人员自己构建带有此补丁的依赖库。
PR: mnesia: Add post-commit hook #5926
4.AMQ 2.0:基于角色的路由分发
AMQ是中国移动智慧家庭运营中心自研的基于开源EMQX实现的物联网连接中间件。为了增加集群的扩展能力,我们在2.0版本中引入了Mria开源实现的新特性,用于解决集群节点复制的开销问题。同时,为了解决所有节点需要承载集群全量数据的问题,我们设计了新的集群数据复制实现——连接分发引擎:一种基于节点角色进行订阅/复制的路由分发机制。
路由数据是物联网连接集群中的核心数据,它存储设备订阅主题与集群节点的映射关系,在消息发布时根据消息主题信息查找所有匹配的节点,用于集群内节点间的消息派发。在EMQX的实现中,路由数据存在于集群中的所有节点上。客户端的主题订阅数据,则只保存在连接所在的节点上,用于节点内部派发消息到客户端。当客户端连接到集群某个节点订阅某个新的主题时,就会生成一条路由数据,该数据最终会同步到集群所有节点上,每个节点都可以通过本地查询找到任意主题对应的订阅节点列表。当客户端发布消息时,连接所在节点会根据消息主题检索路由数据得到所有订阅节点的信息,然后将消息派发到这些节点上。
Mria实现的一个问题就在于,集群中很多节点复制了本身就不需要的路由数据。设想这样一种场景:一个智能门锁和一个智能台灯分别连接到集群中的NodeA和NodeB节点上,并且分别订阅了主题TopicA和TopicB。由于EMQX实现的特性,这两个节点都会存储一条包含TopicA和TopicB的路由数据。但由于门锁和台灯之间不会直接互相发布消息,对这两个节点来说,他们都存储了一条永远也不会用到的路由数据。同理,当集群中接入的设备越来越多时,每个节点上都会存在大量无用的路由数据记录。这不仅会增加服务器资源的投入,还会导致查询性能的降低,另外在新节点加入集群时,还会导致数据复制时间的增加,降低节点的接入效率。
在AMQ 2.0实现的路由分发机制中,每个节点都有一个数据复制角色:DB(Database),SVC(Service)或者CONN(Connection)。其中只有少数的DB角色节点才拥有全量的集群数据,在集群中承担“数据中心”的角色。DB节点负责根据可配置的订阅策略将路由数据分发给对应的SVC或CONN节点。另外,SVC和CONN节点并不会简单地复制DB节点的所有路由数据,而是根据可配置的角色策略选择性地复制自己所需要的数据。这样,这些节点存储的数据就是有限的,并不会随着集群数据量的增加而增加,并且仍旧采取本地查询的方式检索数据,不会影响消息派发时数据检索的效率。此外,由于不需要同步集群全量数据,每个SVC和CONN节点都可以做到快速接入、快速完成存量数据的复制。
AMQ基于角色的路由分发拓扑架构
5.总结
无论是EMQX 4.x的Mnesia实现,还是EMQX 5.x的Mria实现,亦或是AMQ 2.0的路由分发实现,目的都是一样的:在确保数据读写效率的前提下,尽可能地扩展集群的规模。