美文网首页
技术说 | MongoDB 与我们的存储解决方案

技术说 | MongoDB 与我们的存储解决方案

作者: 初心不变_叶子 | 来源:发表于2022-08-16 11:17 被阅读0次

    继服务容器化之后,我们的下一个技术目标定在了存储方案上。

    各种采集服务在几个月内产生了数百万条数据,这样的数据规模,已经不再适合用 SQLite 这种单文件数据库存储。

    因此,我们从六月初开始寻找更好的存储方案,并在七月份将所有数据全部转移到 MongoDB 上。

    需求分析

    促使我们做出这一决定的核心原因是数据量。在 SQLite 数据库中,大数据量会带来以下问题:

    • 整库备份时单文件过大不利于传输,部分备份时数据导出不便
    • 由于 SQLite 的全表锁机制,同一数据库表,同一时间只能进行一个写操作,带来了潜在的性能瓶颈
    • 数据库的性能会随着数据量增长下降

    同时,关系型数据库的特性,导致我们需要花大量时间编写相应逻辑,将 JSON 展平后存入数据库中。

    因此,我们开始寻找一种可以将类似 JSON 的结构直接存储的数据库,并将范围缩小到文档型数据库。

    考虑实际使用规模、相关参考资料等因素,我们选择了 MongoDB。

    聊聊非关系型数据库

    传统的关系型数据库,可以理解为一张巨大的 Excel 表格。

    想要在里面存储数据,需要先填写表头,对应到数据库上,就是执行一段建表语句,它叫做数据库模式定义语言(DDL)。

    数据库中的很多功能都和 Excel 相似,我们简单说几个。

    首先是排序,Excel 的排序是对整张表格生效的,数据库中的排序则是对单次查询生效的。

    约束,对应到 Excel 中就是数据有效性校验,数据库会禁止你写入不符合约束的数据,而且它的约束是以列为单位的,不允许某个“单元格”出现特例。

    联合查询,Excel 中的 VLOOKUP 等函数能实现“查找一个表的内容,插入到另一个表中”,数据库的查询也差不多,指定要从哪里查,用什么东西查,查什么,查出来放到哪里。

    Excel 的行在数据库中称为记录,列在数据库中称为字段。

    数据库没有“合并单元格”,是严格的行列结构。

    文档型数据库的特点

    非关系型数据库有很多种,文档型是其中的一个分类,还有键值对数据库、列存储数据库、图数据库等。

    顾名思义,文档型数据库更像 Word 文档。

    一个数据库文档对应一个 Word 文档,数据库中的文档大概长这样:

    {
      "_id": {
        "$oid": "62c83fb59bc80b5ef74856af"
      },
      "date": {
        "$date": {
          "$numberLong": "1631923200000"
        }
      },
      "ranking": 1,
      "article": {
        "title": "幸得君心似我心",
        "url": "https://www.jianshu.com/p/a03adf9d5dd5"
      },
      "author": {
        "name": "雁阵惊寒"
      },
      "reward": {
        "to_author": 3123.148,
        "to_voter": 3123.148,
        "total": 6246.297
      }
    }
    

    把它对应成 Word 文档,长这样:

    # 文件名:62c83fb59bc80b5ef74856af
    
    date: 1631923200000
    ranking: 1
    article:
        title: 幸得君心似我心
        url: https://www.jianshu.com/p/a03adf9d5dd5
    author:
        name: 雁阵惊寒
    reward:
        to_author: 3123.148
        to_voter: 3123.148
        total: 6246.297
    

    (其实这是 YAML,一种配置文件格式)

    这里的 _id,对应到 Word 中是文件名,它在单台设备上是唯一的。

    date 日期被转换成了整数格式,准确来说是 UNIX 时间戳,1970/1/1 到该时间经过的秒数。

    这是一条简书文章收益排行榜数据。

    像这样的数据,还有三万多条,并且正在以每天 100 条的速度增加。

    但在文档型数据库中,一个表————在这里叫做集合(collection)————的文档,结构可以不同。

    你的每篇简书文章,可以有不同的结构,不一定都是序言、正文、后记。

    但文章一定要有标题,数据库的文档也一定要有 id。

    其它的数据可以随意填写,像简书的文章一样,任你发挥。

    我们怎么用 MongoDB

    之前我写过另一篇技术说:技术说 | Docker 如何帮助我们构建面向未来的服务,我们五月份确立容器化目标,六月份完成,MongoDB 自然也用上了 Docker。

    认真看过之前文章的小伙伴可能会疑惑,容器是无状态的,如果数据库容器重新创建,数据不就被删除了吗?

    Docker 提供了容器数据持久化的方案,我们可以使用卷(Volume)保存数据库。

    我们先创建三个卷:

    • MongoDB:存放数据库
    • MongoConfigDB:存放水平扩展需要用到的数据,现在没有使用
    • MongoLog:存放数据库日志

    之后根据 MongoDB 官方文档,将三个卷挂载到对应的目录,就完成了数据持久化配置。

    接下来是容器的内存限制,数据库会主动缓存热点内容,加快读写速度,如果不作限制,数据库将占用大量内存,影响其它服务的正常运行。

    对于我们的应用场景,数据库内存限制为 1GB。

    这是我们的 Docker Compose 文件:

    version: "3"
    
    volumes:
      MongoDB:
      MongoConfigDB:
      MongoLog:
    
    networks:
      mongodb:
        external: true
    
    services:
      mongodb:
        image: mongo:5.0.9
        command: --config /etc/mongod.conf
        ports:
          - "27017:27017"
        networks:
          - mongodb
        volumes:
          - "MongoDB:/data/db"
          - "MongoConfigDB:/data/configdb"
          - "MongoLog:/var/log/mongodb/"
          - "./mongod.conf:/etc/mongod.conf"
        deploy:
          resources:
            limits:
              memory: 1G
          restart_policy:
            condition: on-failure
            delay: 5s
            max_attempts: 3
    

    之后,我们还需要编写一个配置文件。

    net:
      port: 27017
    storage:
      dbPath: /data/db
      wiredTiger:
        engineConfig:
          cacheSizeGB: 0.75
          journalCompressor: zstd
        collectionConfig:
          blockCompressor: zstd
        indexConfig:
          prefixCompression: true
      journal:
        enabled: true
    systemLog:
      quiet: true
      destination: file
      path: "/var/log/mongodb/mongod.log"
      logAppend: false
    security:
      javascriptEnabled: false
    

    这个配置文件主要做了以下几件事:

    • 设置服务端口为 27017,这也是 MongoDB 的默认端口
    • 设置数据库路径
    • 设置缓存上限为 0.75GB
    • 打开数据压缩,设置压缩算法为 zstd
    • 打开日志功能,防止非正常退出时丢失数据
    • 重定向数据库日志到文件
    • 禁用 JavaScript 执行,我们不会使用到这一功能

    这里需要特别注意,MongoDB 默认不启用权限验证,任何人都拥有对数据库的操作权限,我们的服务器防火墙中禁止了这一端口的连接,但依然建议大家尽量打开权限验证功能。

    我们强制规定,一个服务只能读写一个数据库,但可以读取其它数据库,例如简书小工具集只能读写自己的数据库,但可以从 JFetcher 的数据库中获取数据。

    每个数据库中允许建立任意多的集合,并且鼓励对每个有需求的服务模块单独建立集合。

    接下来的一条规定,是我们保证非关系型数据库不成为“维护噩梦”的关键:将非关系型数据库当成可嵌套的关系型数据库使用。

    具体来说,我们禁止在同一集合中存放多种不同类型的数据。

    另外,当字段数据为空时,一律使用 None 代替,禁止使用对应数据类型的默认值。

    例如,文章排行榜数据中,如果无法获取到文章标题,标题字段依然不允许省略,不允许使用空字符串代替,必须使用 None 填充。

    嵌套数据出现空值时,不能使用 None 代替,必须填写空字典作为占位符。

    对不稳定的数据来源,存入数据库前必须使用映射关系进行处理。任何外部 API、正处于 Beta 阶段的服务,都属于不稳定数据来源。

    即使源数据格式与期望的格式完全一致,也必须使用字典映射进行处理。

    MongoDB 给我们带来了什么

    首先,基于更完善的数据压缩机制,我们获得了 30% 以上的空间收益,同时没有明显影响数据库的性能。zstd 支持不同压缩等级,我们目前使用的是默认等级,如果后期数据量进一步增大,可能会考虑对部分访问不频繁的数据使用更高的压缩等级。

    在高负载场景下,数据库导致的性能瓶颈得到了一定程度的改善,对少量热点数据的高频访问测试中,效果尤其明显。

    我们从服务中去除了对 Peewee ORM 库的依赖,改为依赖更完善的 pymongo 库,同时,基于异步的 motor 库为我们的服务异步化过程带来了很大帮助。

    在数据库操作层面,我们的关注点从设计表结构、编写映射逻辑转换为对数据库索引、数据存储结构的优化。

    对于简单的数据操作,我们更倾向于使用 MongoDB 的聚合功能完成,这提升了在大规模数据处理中的程序性能,在一些侧重于展示而不是分析的服务中,我们去掉了对 Pandas 的依赖,间接降低了服务部署耗时和资源占用。

    在数据备份中,我们成功实现了数据库向阿里云 OSS 的自动备份,大大提升了数据安全性。

    mongodump 工具大大降低了数据导出的复杂度,也在一定程度上缩短了数据分析的前期准备时间。

    总结

    本期内容介绍了我们在数据库转型过程中的经验,我们的目标是构建更加先进的服务体系和基础架构,让开发者将更多精力放到业务逻辑上,让用户使用性能优异、设计合理的服务。

    技术说系列将继续为大家讲解我们的技术历程,欢迎大家持续关注。

    相关文章

      网友评论

          本文标题:技术说 | MongoDB 与我们的存储解决方案

          本文链接:https://www.haomeiwen.com/subject/jrvrgrtx.html