继服务容器化之后,我们的下一个技术目标定在了存储方案上。
各种采集服务在几个月内产生了数百万条数据,这样的数据规模,已经不再适合用 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
工具大大降低了数据导出的复杂度,也在一定程度上缩短了数据分析的前期准备时间。
总结
本期内容介绍了我们在数据库转型过程中的经验,我们的目标是构建更加先进的服务体系和基础架构,让开发者将更多精力放到业务逻辑上,让用户使用性能优异、设计合理的服务。
技术说系列将继续为大家讲解我们的技术历程,欢迎大家持续关注。