源宝导读:明源云天际-移动建模平台是一个快速生成多端移动应用的PaaS平台,元数据是移动应用设计与运行的核心数据结构,本文将从元数据存储这个视角分享我们的技术思考与实践。

一、什么是元数据(Metadata)?

这个问题要先从移动建模平台的定位说起。移动建模平台是一个高效的应用搭建、管理平台,用户可以通过拖拉拽的方式,自定义快速生成多端移动应用的PaaS平台。

目前主流的移动应用开发大都是基于H5为主的前端技术,元数据是对移动应用内部结构的一种数据抽象,用于描述应用所使用的组件和配置,是整个移动应用设计阶段和运行阶段的核心数据,也是移动建模平台生成的重要产物之一。本文主要从元数据这个视角去讨论移动建模平台在元数据存储方面的一些实践。

如果把移动建模平台比作一个汽车生产线的话,那么移动应用就好比这条生产线生产的汽车,元数据就好比汽车的配置,消费者可以基于汽车的原厂配置进行个性化改装,也就有了个性化元数据,改装完成最终验车上牌也就有了运行时元数据。

设计阶段通过一个Web版的在线设计器,设计器初始化会加载元数据进行页面渲染,元数据数据结构如下:

设计器加载完成后可以通过设计器进行应用的设计和页面配置,保存设计就会产生新的元数据:

二、元数据存储架构演进过程概览

移动建模平台元数据存储的演进过程大致可以分为三个阶段:

2.1、单体应用阶段

这个阶段元数据表和其他业务数据表在同一个数据库中,按照上图的逻辑结构主要分成四张表来存储:

在项目初期数据量并不大,这种结构也是最容易实现和最容易想到的。但随着业务的发展,各种组件越来越丰富,单个应用的元数据也由最初的普遍几十KB发展到几M,同时伴随着页面增多,页面之间的拷贝、复制、更新等操作也变得越来越缓慢。

从上图的表结构可以看出,metadata字段是使用字符串来存储json的,并且设计时和运行时元数据存储在同一张表中。很快这种设计方案的弊端就显现出来,主要有几方面问题:

  1. 元数据可能会有几M,对元数据的每次读写操作都需要对元数据进行序列化和反序列化,网络IO和内存消耗大,程序执行时间过长

  2. 即使要修改元数据中很小的一部分内容也必须将元数据全部取出,修改后再序列化为字符串存入数据库。由于元数据的特殊性,缓存方案也无法使用

  3. 页面、文件夹数量比较多时对页面的复制、删除等操作需要涉及到多表事务,事务执行效率低。一个租户升级操作可能需要十几分钟。

  4. 当PHP按照数组方式来处理后导致空对象和空数组转换问题,会导致元数据损坏无法还原,前端页面渲染出错

  5. 由于涉及到多张表的操作,多表查询会让业务逻辑变得极其复杂,程序很难维护

2.2、服务拆分阶段

针对以上出现的一些问题,开始采取一些局部的优化手段,主要有几下几方面:

  1. 采用服务化的方式将原有的元数据操作相关的逻辑从单体应用中剥离出来,有了元数据服务。

  2. 数据表结构增加了一些冗余字段,并针对索引进行了相应调整,提高了查询性能。

  3. 在写入操作比较多的地方将以前的单条insert改为了一次性多条insert插入,优化写入性能。

  4. 对元数据的结构进行优化,精简冗余部分,减小元数据的体积。

  5. PHP操作元数据禁止使用数组方式来处理,统一转为对象。

这个阶段采用了以上一些优化方法,虽然性能得到一些改善,但是都没有从根本上解决问题,根本问题出在存储层,团队也有讨论过使用NOSQL,比如MongoDB,但是由于元数据和其他模块严重耦合,数据层的拆分难度很大。加之如果改为MongoDB,新的数据模型如何设计,旧的数据如何迁移等问题还没最佳实践。所以这个阶段的一些改进仅限于应用层的拆分,不过对于后续重构提供了参考。

2.3、微服务化阶段

这个阶段也是移动PaaS2.0阶段,在2.0中元数据相关的能力完全抽离出来成为单独的服务,并且使用golang进行重构,数据库也独立出来,使用MongoDB进行重新建模设计。为什么要选用MongoDB来作为数据库存储,主要基于以下几个方面:

  1. 元数据本来就是json结构,而MongoDB的使用BSON作为数据交换格式,以文档方式组织数据,非常符合元数据的结构特点。

  2. MongoDB4.0之后同样支持事务操作,在一些需要事务的场景下依然能够保证数据的一致性。

  3. 通过性能对比,MongoDB在读写性能上有明显优势。

  4. JSON 格式存储最接近真实对象模型,对开发者友好,方便快速开发迭代。对于测试人员来说,可以直观的看到元数据的数据结构,对测试更加友好。

  5. 能够极大的简化目前的应用层开发,减少大量的多表查询操作。

  6. 可以按需修改元数据文档的某个节点,而不需要读取整个元数据文档。

  7. 高可用复制集满足数据高可靠、服务高可用的需求,运维简单,故障自动切换。

  8. 可扩展分片集群,面对未来海量元数据存储,可以很方便的支持水平扩展。

  9. 强大的aggregation & mapreduce,可以将复杂的查询分解为一个个小的步骤。

下图是在4核8G的同一台虚拟机上做的一个MySQL和MongoDB的性能对比测试,可以看出随着插入元数据的数量增加,MySQL和MongoDB所花费的时间的差距也越来越大。

使用MongoDB重新设计后的元数据结构:

{"_id": ObjectId("5f3de7507cda70000e433ca2"),"workspaceId": "26043287605354496","common": {"style": {"globalBgColor": "#FFFFFF","primaryColor": "#FF543D","secondaryColor": "#FF6954"},"body": {"header": {"hide": false}}},"configs": [{"_id": ObjectId("5f3de7507cda70000e433ca3"),"type": "role","name": "游客","alias": "default","isGuest": true,"remark": "用户未登录时所使用...","position": 1.59789243277115e+18,"viewIds": [ObjectId("5f3e45c62ef1d50013b3303e")],"metadata": {"tabs": {"items": [{"isDefault": true,"text": "123","activeIcon": "appicon-house","href": {"name": "bde68663-6f93-2206-0b29-cf910711f71e"},"icon": "appicon-house","iconClass": "appicon"}]}}},{"_id": ObjectId("5f3de7507cda70000e433ca4"),"type": "role","name": "已登录用户","alias": "default-login","isGuest": false,"remark": "用户登录时所使用...","position": 1.59789243277116e+18,"viewIds": [ ],"metadata": { }},{"_id": ObjectId("5f470ced59221f0014d2a144"),"type": "page","ancestors": [ObjectId("5f3de7507cda70000e433ca4")],"name": "login","routeName": "ef214890-b3e6-9a24-9dd8-80d12343f76c","routePath": "/ef214890-b3e6-9a24-9dd8-80d12343f76c","remark": "","design": { },"metadata": {"name": "ef214890-b3e6-9a24-9dd8-80d12343f76c","path": "/ef214890-b3e6-9a24-9dd8-80d12343f76c","body": {"header": {"title": "login","items": [ ]},"content": {"items": [ ]}}},"position": 1.59849188522867e+18,"viewIds": [ ]}],"createdAt": ISODate("2020-08-20T03:00:32Z"),"updatedAt": ISODate("2020-08-20T03:00:32Z")
}

从新的结构可以看出之前的元数据中的配置变成了一个内嵌数组configs,configs下包含了角色配置、文件夹、页面。三者之间的关系由以前的层次关系被打平后变成了并列关系。那么如何实现他们之前的那种上下级关系呢?仔细看就能发现configs中的每一个对象里都有一个ancestors字段,这个字段用于记录祖先节点,也就是通过这个节点就可以轻松找到当前项有几个上级,只需要增加一个索引字段就可以高效的得到一个树状结构。根据ancestors创建索引:

xxxxxxxxxx
db.metadata.createIndex({"configs.ancestors": 1
})

如图所示,在1.0中,如果想要按照箭头所指的方向移动往往需要配合数据库中的

这两个字段,更新这两个字段来标注页面的位置。

在新的数据库当中,由于页面、文件夹、配置是平等关系,所以只需要一个 "position": 1.59849188522867e+18字段来记录就行了,当需要移动上下页面时候只需要取相邻两个元素的position的平均值,最后结果按照position来排序,性能得到很大提升。

通过前后数据结构的对比,可以很明显发现,在使用MySQL存储时,为了要保证元数据节点之间的关系,往往需要设计多张表,而使用MongoDB后,只要一个集合就能搞定设计时元数据的存储,这样带来的直接好处就是性能提升和应用程序开发的简化。

元数据服务端使用了golang代替之前的php,其实也是为了方便元数据的操作和提升性能,由于配置、文件夹和页面的差异被抹平,三者被统一抽象为配置,于是就很方便的提供统一的元数据操作API,golang结构体可以完美的将元数据的结构映射到MongoDB的文档模型中,开发者可以清楚的看到数据库中元数据结构和代码中是完全一致的,这对新人理解元数据结构会有很大帮助。

//元数据结构体
type Metadata struct {Model       `bson:"-"`Id          bson.ObjectId `bson:"_id" json:"id"`WorkspaceId string        `bson:"workspaceId" comment:"工作区ID"`Common      bson.M        `bson:"common" comment:"公共配置"`Configs     []Config      `bson:"configs" comment:"配置信息"`IsPublished bool          `bson:"isPublished" comment:"是否发布"`CreatedAt   time.Time     `bson:"createdAt"`UpdatedAt   time.Time     `bson:"updatedAt"`
}type Config interface {Add(metadataId string, data interface{}) errorEdit(metadataId, configId string, data interface{}) errorDelete(metadataId, configId string) errorGetType() string
}

解决了存储问题后,需要返回树状结构给前端,这就需要应用端重新组装数据。

type PageListResponse []TreeNode//统一定义菜单树的数据结构
type TreeNode struct {Id        string                 `json:"id"`                  //节点idParentId  string                 `json:"-"`                   //父idType      string                 `json:"type"`                //类型Name      string                 `json:"name"`                //节点名字RouteName string                 `json:"routeName,omitempty"` //标识RoutePath string                 `json:"routePath,omitempty"` //路径Leaf      bool                   `json:"leaf,omitempty"`      //叶子节点IsGuest   bool                   `json:"isGuest,omitempty"`   //是否是游客配置IsLogin   bool                   `json:"isLogin,omitempty"`   //是否是登录页面Ancestors []string               `json:"ancestors,omitempty"` //祖先节点Remark    string                 `json:"remark,omitempty"`    //备注Position  string                 `json:"position"`            //位置Design    map[string]interface{} `json:"design,omitempty"`    //组件属性Metadata  map[string]interface{} `json:"metadata,omitempty"`  //元数据Children  []TreeNode             `json:"children,omitempty"`  //子节点
}// GenerateTree 自定义的结构体实现 TreeNode 接口后调用此方法生成树结构
// nodes 需要生成树的节点
func GenerateTree(nodes []TreeNode) (trees []TreeNode) {trees = []TreeNode{}// 定义顶层根和子节点var roots, childs []TreeNodefor _, v := range nodes {if len(v.ParentId) <= 0 {// 判断顶层根节点roots = append(roots, v)}childs = append(childs, v)}for _, v := range roots {childTree := &v// 递归recursiveTree(childTree, childs)// 递归之后,根据子节确认是否是叶子节点childTree.Leaf = (len(childTree.Children) == 0)trees = append(trees, *childTree)}return
}// recursiveTree 递归生成树结构
// tree 递归的树对象
// nodes 递归的节点
func recursiveTree(tree *TreeNode, nodes []TreeNode) {for _, v := range nodes {if len(v.ParentId) <= 0 {// 如果当前节点是顶层根节点就跳过continue}if tree.Id == v.ParentId {childTree := &vrecursiveTree(childTree, nodes)// 递归之后,根据子节确认是否是叶子节点childTree.Leaf = (len(childTree.Children) == 0)tree.Children = append(tree.Children, *childTree)}}
}

这个阶段golang结构体处理json的便利性凸显出来,omitempty可以将空的节点数据忽略掉,这就有效的降低了元数据的体积,降低了网络I/O。

设计时的元数据存储性能和逻辑复杂的问题解决了,剩下的就是运行时元数据的问题了。元数据在运行时阶段其实是不会变动的,在1.0当中,移动应用在运行时需要动态请求元数据的服务,从元数据服务接口中拉取运行时元数据来渲染页面,显然如果访问量大后元数据服务会成为性能的瓶颈。针对这个问题结合元数据的业务特点,最终运行时元数据就采用静态json文件的方式存储在OSS上,不仅消除了后端服务访问压力问题,同时也提高了运行时元数据加载的稳定性。最终生成的路径其实访问的是一个真实存在的json问题。

xxxxxxxxxx
https://xxxxxx.com/_assets/mobile_three/demo/exp/1.0.12/meta/default.json

三、总结

好了,以上就是本次分享的移动建模平台元数据存储的演进过程,当然实际演进过程远比本次讲述的要复杂得多,分享的内容也是挑选几个比较重要的场景展开,后续可以分享一些MongoDB设计模式方面的内容,总结一下从开发选型角度大致有以下几点实践经验:

  1. 使用MySQL和MongoDB同时进行数据建模,对比两者之间的优劣,在表关系比较复杂时可能涉及到多表关联查询较多的场景下利用MongoDB内嵌文档、内嵌数组等灵活的文档数据结构往往能设计出结构更清晰、性能更好的存储方案。

  2. 小心MongoDB单个文档16M的存储限制,对于那种可能无限增长的数据不适合直接使用内嵌方式存储,可改为内嵌引用方式。

  3. 尽量不要使用ORM框架来操作MongoDB,往往会误把MongoDB当成MySQL来使用,同时不能很好的使用MongoDB强大的API。

  4. Golang和MongoDB的结合能在提升性能的同时,带来开发上的便利。

  5. MongoDB 4.0以后已经支持多文档事务,扩展了MongoDB的使用场景,越来越多的场景其实是可以使用MongoDB代替MySQL。如果没有特别的必要和限制,采用MongoDB往往会给程序设计带来更大的灵活性,提高数据库开发效率,更好的满足快速迭代开发的需求。

  6. MongoDB不能简单理解为一个json文档存储所有数据,同时要结合具体的业务场景考虑读写操作是否方便来设计文档模型。

------ END ------

作者简介

段同学: 研发工程师,目前负责天际-移动平台产品的研发工作。

也许您还想看

基于 Go 的微服务运行情况监控实践

在明源云客,一个全新的服务会经历什么?

云客后台优化的“前世今生”(一)

云客后台优化的“前世今生”(二)

回归统计在DMP中的实战应用

移动建模平台元数据存储架构演进相关推荐

  1. 京东到家评价系统存储架构演进

    前言 一.业务场景 1. 评价生成 2. 评价处理 二 架构演进 1. 系统初创 2. 存储多元化 3. 架构再升级 三 展望 四 总结 前言 京东到家作为即时零售的电商平台,致力于将万千好物即时送到 ...

  2. 云上应用系统数据存储架构演进

    简介: 回顾过去二十年的技术发展,整个应用形态和技术架构发生了很大的升级换代,而任何技术的发展都与几个重要的变量相关.本文将会给大家分享应用系统数据架构的演进以及云上的架构最佳实践. 作者 | 木洛 ...

  3. 【存储知识学习】第十章- 存储架构演进过程《大话存储》阅读笔记

    10.5.1 第一阶段:全整合阶段 所有的部件和模块都在同一个机箱当中,是DAS结构. 10.5.2  第二阶段:磁盘外置阶段 是将磁盘置于服务器机箱外部的情况.这种架构依然属于DAS架构,因为存储系 ...

  4. vivo 云服务海量数据存储架构演进与实践

    一.写在开头 vivo 云服务提供给用户备份手机上的联系人.短信.便签.书签等数据的能力,底层存储采用 MySQL 数据库进行数据存储. 随着 vivo 云服务业务发展,云服务用户量增长迅速,存储在云 ...

  5. 电商平台微服务架构演进

    一 初始架构 引入 nacos 后的基础架构图. 二 加入 Ribbon 后的架构 引入多个微服务,每个微服务通过 Ribbon 进行相互调用. 三 引入 Feign 后的架构 Feign 底层还是会 ...

  6. 360数据处理平台的架构演进及优化实践

    本文根据DBAplus社群第153期线上分享整理而成 讲师介绍 王素梅 360大数据开发经理 目前在360大数据中心担任大数据开发经理,拥有7年以上大数据行业从业经验,专注大数据处理.大数据平台开发, ...

  7. 分析视角下银行业数据平台架构演进及实现

    当前,数据成为驱动银行业数字化转型的关键生产要素.如何从海量的数据中识别有效的价值数据,实现业务与数据的深度融合,激活数据要素潜能.深挖数据资产价值,成为银行业持续探索的重要课题. 随着云计算.大数据 ...

  8. 从 ClickHouse 到 Apache Doris,腾讯音乐内容库数据平台架构演进实践

    导读:腾讯音乐内容库数据平台旨在为应用层提供库存盘点.分群画像.指标分析.标签圈选等内容分析服务,高效为业务赋能.目前,内容库数据平台的数据架构已经从 1.0 演进到了 4.0 ,经历了分析引擎从 C ...

  9. IOT(27)---国内物联网平台的发展、技术架构演进

    国内物联网平台的发展.技术架构演进 本文基于两年来在物联网方面的研发积累,先跟大家探讨国内物联网平台的发展和技术架构演进,再提出作者的物联网完整解决方案. 一.国内物联网平台的发展特点 1.    国 ...

最新文章

  1. mysql 导入一个数据库_mysql导入一个数据库
  2. Python之配置日志模块logging
  3. sqoop动态分区导入mysql,sqoop 导入数据到hive分区表(外表,内表) 指定分区 指定数据库 指定表...
  4. 2018-2019 20165227《信息安全系统设计基础》第三周学习总结
  5. java的mybatis批量更新_mybatis批量更新的问题
  6. 广联达2018模板算量步骤_广联达钢结构算量软件可以和广联达量筋合一GTJ2018互导吗?...
  7. Toonz开源,Apple开源CareKit,以及更多新闻
  8. Linux下 fio磁盘压测笔记
  9. 机器视觉光源学习总结——侧部背光源
  10. java流程控制结构不包括_以下各项中不属于Java语言流程控制结构的是()。
  11. Torch7框架学习资料整理
  12. php生成html文件方法总结
  13. 读Ext之九(事件管理)
  14. DSP技术是利用计算机或,DSP技术是什么?
  15. c语言空气污染指数代码,空气质量指数API是怎么算出来的
  16. 计算机一级插入页码,计算机一级WPS考试:WPS文字中页码插入及排版技巧
  17. 自动化测试 selenium 模块 webdriver使用02
  18. js实现京东购物放大镜和选项卡效果
  19. Ubuntu 18.04 go语言环境搭建
  20. Vue移动网页开发调试过程(第二篇)——weinre

热门文章

  1. JDK8之Stream新特性
  2. 设计模式(1)--简单工厂模式、策略模式
  3. 2015计算机应用基础平时作业答案,2015秋《计算机应用基础》第一次作业
  4. php v9 上传_phpcms v9 表单添加文件上传字段
  5. python数据结构与算法第六讲_Python 学习 -- 数据结构与算法 (六)
  6. java对象实例化的方式
  7. android进程间通信:使用AIDL
  8. From Apprentice To Artisan 翻译 19
  9. 2013-10-10
  10. 【.NET 日常开发技巧】一个性能强悍的HttpClient 库