导读:本文将结合一个常见的物联网场景分享其存储引擎架构和设计。通过对团队在过去一段时间承接的一些设备元数据案例进行总结,结合他们的需求和存储架构的演进,介绍在存储引擎层的一些沉淀和思考。
今天的介绍会围绕下面四部分展开:
业务场景介绍
存储引擎迭代历程
自研新存储
总结和展望
然后会开始介绍这个场景下用户选型的存储部分的历代架构,也会展开说说这些架构的优缺点,中间会涉及到一些存储引擎的原理。第三部分会介绍下我们通过自研新存储引擎如何承接这个场景,并解决之前架构遇到的各项问题。最后做一个总结和展望。
--
场景介绍
首先介绍下业务场景,以及这里会有一些什么问题需要去优化和解决。
我们的场景是一个物联网设备元数据管理平台,需要接入多样化的设备数据,比如汽车终端设备、无人贩售机等。设备数据中很重要的一环就是设备元数据。设备元数据主要维护设备的基础信息,包括设备的id、设备类型、设备状态等,还可能会根据不同类型有一些扩展属性,例如车联网中的设备需要存储他的当前地理坐标等。设备元数据与设备时序数据不同,一个设备通常仅保留有限条记录,尤其是最新的状态信息。
在上述场景下,最基础的需求就是存储需求,把元数据进行统一规整的存储。我们需要接入不同类型的设备。随着物联网的发展,设备数量级会有爆发性增长,可能会达到亿级。同时,这些设备状态可能会在线高频更新,比如地理位置等信息可能会频繁更新。在累积了海量数据后,会有一些拓展需求,比如去做一些复杂的设备检索和圈选,还会有一些活动带来业务访问量的激增。
基于这些需求,我们的业务架构大体如下:
在各种设备会有一个我们的客户端,定期上传设备元数据信息。在云上,有一个接入网关服务器,接入各类设备的原始消息数据,经过处理我们会使用各类存储进行数据存储。通常会用消息队列做消息的写入。数据库做设备元数据的长期存储,缓存承载一些热数据的高频查询。下文将重点展开介绍数据库存储这部分的设计。
--
储存引擎迭代
. 第一代
第一代架构比较简单和直观。使用 MySQL 单库存储我们的设备元数据,CRUD 都基于 SQL 接口进行访问。在活跃设备数量级万级别,终端类型个位数级别,峰值读写百 QPS 级别,也就是访问量不是很大的情况下,这套架构可以很好地满足我们的需求。整套架构的优势就是开发便捷,架构简单。
但是随着业务发展,尤其是我们定位做一个设备统一管理平台,接入不同类型的设备。在万物互联时代,设备的量级可能突破亿级别甚至更高。随着存储量级增长,活跃设备带来的更新操作也会大幅增长,峰值读写可能达到k+ QPS。这时单机的MySQL就很难满足我们的需求了,我们很自然的想到一个架构的升级,即基于 MySQL 进行分库分表。
MySQL分库分表可以在一定程度上满足我们的需求,但整体来说这样一套架构在物联网场景还是有些笨重,也存在一些问题。
当面临业务改造,我们可以借助一些相对成熟的 MySQL 中间件来降低改造开销,但依然存在维护这样一个分布式MySQL集群的运维成本。在物联网设备场景下,通常对完整的事务能力要求没那么高,随着数据的写入规模增长,这套架构可能难以满足弹性的诉求。同时,在遇到一些复杂查询和分析需求时,比如前文提到的设备圈选,在海量设备中基于一些“组合条件”选中部分设备,并拉取这些设备的一些信息。这种场景在MySQL分库分表的架构也很难满足需求。架构还需要进一步演进。
. 第二代架构
业务会引入一个开源搜索引擎 ElasticSearch,形成一个MySQL+ES组合的方式,MySQL承接一些基础的查询,而ES负责复杂的检索和查询。ES 擅长全文检索,多字段组合查询等,并且 ES 本身是分布式的架构对亿到数十亿的设备存储并不是一件难事。这个架构拓展了 MySQL 的查询能力,但是本身并没有解决分库分表的维护成本。由于 ES 本身并不是一个写入即可见的存储系统,通常不太能直接替换MySQL 做整个业务的主存储。因此业务方不得不同时维护两套存储系统。
有些业务方会进一步优化,把分库分表的MySQL替换成 Hbase,分布式 NoSQL 数据库。替换的原因主要在于,MySQL是关系模型,而物联网设备云存储对关系模型并没有很强的依赖,这时Hbase 相对灵活的存储模型(schema free)就更有优势。Hbase天然是分布式的架构,并且采用存储计算分离的架构,做计算节点或者存储节点扩容都相对 MySQL 要容易很多。Hbase 加上 ES 的组合可以在扩展性和功能上很好的满足我们这一场景的需求,业务的能力也可以提升到存储亿级别设备,k+的峰值读写请求,大量复杂查询也可以得到支持。我们再来仔细对比一下已经提到的三个存储系统,MySQL,Hbase 和 ES。
MySQL是单机的架构,采用B+树索引,有着丰富的生态,以及内嵌的索引,提供SQL查询的能力。它擅长中小数据集合下的CRUD,对一致性、事务关系模型要求比较高的情况下,MySQL是一个比较好的选择。
Hbase是分布式架构,采用了LSM的索引结构,有着自己的大数据生态,没有原生SQL引擎,相比于MySQL最大的特点就是存储计算分离,在存储或计算达到瓶颈时可以分别去做扩容,这样可以大大降低成本。同时它采用Range分片,这样可以做灵活的切片和快捷的定位查找。它还具有写入能力强的特点。Hbase擅长海量数据的存储和简单查询。
ES也是分布式架构,使用Lucene倒排,有着自己的ELK生态,同样没有原生SQL。它虽然使用分布式架构,但并非存储计算分离,而是用本地磁盘读写,因此要保证数据可靠性,可能要维护多个副本。数据分片采用Hash策略。同时基于Lucene特点,它的更新能力相对Hbase较弱,它擅长的场景是全文索引和复杂查询。
再进一步对比Hbase和ES:
Hbase和ES分区方式不同。以数据ID列-为例,Hbase采用Range分区,分区承载了-,分区是剩下的-。而ES采取Hash方式,数据不是连续排布,而是离散分开的。同时,Hbase的数据组织方式是连续排序的方式,如果查询-,那么可以很快定位到分区,找到是哪一个文件,把文件中的数据通过连续定位扫描出来。ES文件存储采用倒排方式,通过倒排列组合去查询出数据。
通过以上对比,可以看出Hbase和ES是互补的,Hbase 擅长高并发的简单读写,ES 更新能力大不如 Hbase,但是复杂查询能力,全文检索等能力也是 Hbase 所不能做的。因此用户往往需要结合Hbase和ES。
下表是对前述架构优缺点的一个总结。
那 Hbase + ES 是不是就已经满足所有需求了呢?其实还是存在一些问题的。
Hbase 和 ES 不论那个存储组件单拿出来都已经需要不小的运维能力,同时 Hbase 到 ES 也没有非常成熟的数据同步方案,前面也提到 ES 的写入能力和 Hbase 不太对等,所以 ES 峰值写入可能扛不住。我们在做查询的时候也很难实现统一查询,需要分别去两套系统里面做对应的查询,想原生使用 SQL 也更是奢求。因此我们希望通过自研一套架构,充分利用这些存储引擎的优势,并解决存在的问题。
--
自研新储存
. 架构设计
我们所面对的业务需求可以总结为三大方面:海量设备数据接入、高频更新,以及圈选、复杂检索和分析。
基于这些需求,我们希望这样去设计架构:
底层是DFS分布式文件系统,脱离本地盘,并将计算和存储分离,解决成本和扩展的问题。在计算层,分区策略吸取Hbase和ES各自优势,同时支持Range分区和Hash分区。为了同时满足简单查询和复杂查询,会有两种不同的存储类型,主存储和索引节点。为了应对业务激增,我们还会有只读节点的扩展能力。由于维护两套存储系统,可能会用两套系统分别去查询,查询接口可能并不很友好,所以我们希望提供原生API和SQL的方式给用户使用。
基于以上设计,我们的架构就会演进成如下图所示:
如果希望保留MySQL,可以通过MySQL的binlog复制到我们的存储引擎中来,由我们的系统承担所有的查询能力。或者可以直接用我们的存储来替换掉MySQL,实现统一的元数据存储。
为了满足我们的设计需求,存储引擎需要包括以下核心功能:
首先是采用分布式架构,并且存储与计算分离;
使用LSM存储引擎,可以承载更高并发的写入能力,同时支持CDC数据订阅能力,解决数据同步问题;
自动分区扩展,当业务量上涨,快速增加计算节点,需要做分区扩容,对一些高并发的读场景,做一些只读副本的扩展;
多元索引,包括二级索引、倒排索引、多维度空间索引等。
介绍了这些概念和设计。我们再来深入的就海量设备圈选这一场景,看一下我们在自研存储引擎时遇到的问题,以及如何进一步优化。
. 场景分析
上图中的SQL是场景的一个样例,比如查询最近一小时都没有活跃过的设备,并且设备版本号>xxx,状态和类型满足某些条件的设备是哪些。实现时,首先从接入层传入,通过索引获取设备ID,再通过主存储去查询设备的其它属性,返回数据。当查询量很大时,可能无法一次返回,就需要循环迭代的去翻页查询。循环过程如上图所示:
根据条件构造请求
基于条件拉取过滤命中数据,开发返回第一批 ID 和 nexttoken 用来翻页
根据 ID 获取反查获取所需所有列,返回数据和 nexttoken
根据nexttoken 循环进入
亿到数十亿级别设备中,根据条件进行圈选,召回的数据从百级别到千万级别,希望做到近实时,对于千万级别数据,吞吐可以达到 w/s,同时不影响线上其他访问。而实际上假设我们一次查询拉取条数据需要耗时~ms,一秒的吞吐数据也就在几万行级别,性能远不及预期。
我们不难想到除了一些过滤优化降低查询延时等常见手段外,我们最直接获取更高吞吐的方式就是加并发。我们可以让业务设计一个分桶key,所有的数据都带上这个key,在查询的时候我们可以带上这个key 让不同分桶的数据可以并发。比如有个桶,吞吐就可以翻倍。
理论上确实如此,但是我们很快发现这样仍然达不到百万级别,原因是什么呢?是因为我们的查询会有读扩散,每次查询需要路由到很多节点查询,并发高了以后整个集群的请求量会随着一个索引的节点数以及请求并发数倍数增长。这时候不仅上限很容易触发,其他在线读写也会受到影响。我们需要理解存储原理做一些优化。这里就引入了一个概念“routing key”,即路由key,虽然整个索引的数据是 hash 打散的,但是我们可以定制一些打散路由规则,类似前面分桶,我们在存储层也基于一个规则进行分部,每次查询的时候分桶key 变成这个路由key,单一请求只会路由到一个分区节点,这样大大减少了读扩散带来的开销。我们的吞吐此时可以达到百万级别/s。下图分别是优化前和优化后。
优化以后,我们发现性能已经接近满足业务需求。但是随着吞吐百万数据,对我们的内存,cpu的开销还是大幅增长。如何再进一步优化单次请求对服务的压力开销呢?我们进一步剖析存储的原理不难发现,一次请求会访问多个文件,从多个文件中捞取我们要的数据,但是因为涉及到单次访问数据的限制,很多命中的数据不能一次返回,我们需要反复交互进行读取。但是这种读取是没有上下文的,每一次请求过来都要重新去构造中间的数据结构,也没有保留哪些文件访问过哪些没有访问过,存储引擎需要重新过滤数据,获取对应的完整结果。这也正是导致CPU和内容开销大的原因。
我们要进一步优化的方式就是增加上下文的能力。
我们提供了一个针对这种海量数据圈选的接口ParallelScan。在接入层和存储节点进行交互,生成上下文ID,在每次请求时都会带上这个ID。同时在存储节点中会维护这些ID,保留当前有哪些文件需要被查询,也会保留每次查询命中的文件,记录这些上下文。这样业务方不再需要自己去设置分桶或routing key去实现并发,而是天然的通过系统去实现并发,我们会根据分片数,以及每个分片有多少数据去切分并发,一方面让切割更加均衡,同时也做到对业务方的无感知。这样的设计经过我们实测会减少大约%的开销。
最后再来总结对比一下我们的自研存储引擎架构和传统架构:
--
总结和展望
以上通过一个具体场景介绍了我们自研的存储系统,并与传统开源系统进行了对比。统一的存储引擎带来了架构简化,满足了多存储引擎的需求,在解决了数据的基础存储的同时,支持高并发读写,以及复杂查询。