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

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

各种采集服务在几个月内产生了数百万条数据,这样的数据规模,已经不再适合用 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 与我们的存储解决方案)