繼服務(wù)容器化之后,我們的下一個技術(shù)目標定在了存儲方案上。
各種采集服務(wù)在幾個月內(nèi)產(chǎn)生了數(shù)百萬條數(shù)據(jù),這樣的數(shù)據(jù)規(guī)模,已經(jīng)不再適合用 SQLite 這種單文件數(shù)據(jù)庫存儲。
因此,我們從六月初開始尋找更好的存儲方案,并在七月份將所有數(shù)據(jù)全部轉(zhuǎn)移到 MongoDB 上。
需求分析
促使我們做出這一決定的核心原因是數(shù)據(jù)量。在 SQLite 數(shù)據(jù)庫中,大數(shù)據(jù)量會帶來以下問題:
- 整庫備份時單文件過大不利于傳輸,部分備份時數(shù)據(jù)導(dǎo)出不便
- 由于 SQLite 的全表鎖機制,同一數(shù)據(jù)庫表,同一時間只能進行一個寫操作,帶來了潛在的性能瓶頸
- 數(shù)據(jù)庫的性能會隨著數(shù)據(jù)量增長下降
同時,關(guān)系型數(shù)據(jù)庫的特性,導(dǎo)致我們需要花大量時間編寫相應(yīng)邏輯,將 JSON 展平后存入數(shù)據(jù)庫中。
因此,我們開始尋找一種可以將類似 JSON 的結(jié)構(gòu)直接存儲的數(shù)據(jù)庫,并將范圍縮小到文檔型數(shù)據(jù)庫。
考慮實際使用規(guī)模、相關(guān)參考資料等因素,我們選擇了 MongoDB。
聊聊非關(guān)系型數(shù)據(jù)庫
傳統(tǒng)的關(guān)系型數(shù)據(jù)庫,可以理解為一張巨大的 Excel 表格。
想要在里面存儲數(shù)據(jù),需要先填寫表頭,對應(yīng)到數(shù)據(jù)庫上,就是執(zhí)行一段建表語句,它叫做數(shù)據(jù)庫模式定義語言(DDL)。
數(shù)據(jù)庫中的很多功能都和 Excel 相似,我們簡單說幾個。
首先是排序,Excel 的排序是對整張表格生效的,數(shù)據(jù)庫中的排序則是對單次查詢生效的。
約束,對應(yīng)到 Excel 中就是數(shù)據(jù)有效性校驗,數(shù)據(jù)庫會禁止你寫入不符合約束的數(shù)據(jù),而且它的約束是以列為單位的,不允許某個“單元格”出現(xiàn)特例。
聯(lián)合查詢,Excel 中的 VLOOKUP 等函數(shù)能實現(xiàn)“查找一個表的內(nèi)容,插入到另一個表中”,數(shù)據(jù)庫的查詢也差不多,指定要從哪里查,用什么東西查,查什么,查出來放到哪里。
Excel 的行在數(shù)據(jù)庫中稱為記錄,列在數(shù)據(jù)庫中稱為字段。
數(shù)據(jù)庫沒有“合并單元格”,是嚴格的行列結(jié)構(gòu)。
文檔型數(shù)據(jù)庫的特點
非關(guān)系型數(shù)據(jù)庫有很多種,文檔型是其中的一個分類,還有鍵值對數(shù)據(jù)庫、列存儲數(shù)據(jù)庫、圖數(shù)據(jù)庫等。
顧名思義,文檔型數(shù)據(jù)庫更像 Word 文檔。
一個數(shù)據(jù)庫文檔對應(yīng)一個 Word 文檔,數(shù)據(jù)庫中的文檔大概長這樣:
{
"_id": {
"$oid": "62c83fb59bc80b5ef74856af"
},
"date": {
"$date": {
"$numberLong": "1631923200000"
}
},
"ranking": 1,
"article": {
"title": "幸得君心似我心",
"url": "http://www.itdecent.cn/p/a03adf9d5dd5"
},
"author": {
"name": "雁陣驚寒"
},
"reward": {
"to_author": 3123.148,
"to_voter": 3123.148,
"total": 6246.297
}
}
把它對應(yīng)成 Word 文檔,長這樣:
# 文件名:62c83fb59bc80b5ef74856af
date: 1631923200000
ranking: 1
article:
title: 幸得君心似我心
url: http://www.itdecent.cn/p/a03adf9d5dd5
author:
name: 雁陣驚寒
reward:
to_author: 3123.148
to_voter: 3123.148
total: 6246.297
(其實這是 YAML,一種配置文件格式)
這里的 _id,對應(yīng)到 Word 中是文件名,它在單臺設(shè)備上是唯一的。
date 日期被轉(zhuǎn)換成了整數(shù)格式,準確來說是 UNIX 時間戳,1970/1/1 到該時間經(jīng)過的秒數(shù)。
這是一條簡書文章收益排行榜數(shù)據(jù)。
像這樣的數(shù)據(jù),還有三萬多條,并且正在以每天 100 條的速度增加。
但在文檔型數(shù)據(jù)庫中,一個表————在這里叫做集合(collection)————的文檔,結(jié)構(gòu)可以不同。
你的每篇簡書文章,可以有不同的結(jié)構(gòu),不一定都是序言、正文、后記。
但文章一定要有標題,數(shù)據(jù)庫的文檔也一定要有 id。
其它的數(shù)據(jù)可以隨意填寫,像簡書的文章一樣,任你發(fā)揮。
我們怎么用 MongoDB
之前我寫過另一篇技術(shù)說:技術(shù)說 | Docker 如何幫助我們構(gòu)建面向未來的服務(wù),我們五月份確立容器化目標,六月份完成,MongoDB 自然也用上了 Docker。
認真看過之前文章的小伙伴可能會疑惑,容器是無狀態(tài)的,如果數(shù)據(jù)庫容器重新創(chuàng)建,數(shù)據(jù)不就被刪除了嗎?
Docker 提供了容器數(shù)據(jù)持久化的方案,我們可以使用卷(Volume)保存數(shù)據(jù)庫。
我們先創(chuàng)建三個卷:
- MongoDB:存放數(shù)據(jù)庫
- MongoConfigDB:存放水平擴展需要用到的數(shù)據(jù),現(xiàn)在沒有使用
- MongoLog:存放數(shù)據(jù)庫日志
之后根據(jù) MongoDB 官方文檔,將三個卷掛載到對應(yīng)的目錄,就完成了數(shù)據(jù)持久化配置。
接下來是容器的內(nèi)存限制,數(shù)據(jù)庫會主動緩存熱點內(nèi)容,加快讀寫速度,如果不作限制,數(shù)據(jù)庫將占用大量內(nèi)存,影響其它服務(wù)的正常運行。
對于我們的應(yīng)用場景,數(shù)據(jù)庫內(nèi)存限制為 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
這個配置文件主要做了以下幾件事:
- 設(shè)置服務(wù)端口為
27017,這也是 MongoDB 的默認端口 - 設(shè)置數(shù)據(jù)庫路徑
- 設(shè)置緩存上限為 0.75GB
- 打開數(shù)據(jù)壓縮,設(shè)置壓縮算法為 zstd
- 打開日志功能,防止非正常退出時丟失數(shù)據(jù)
- 重定向數(shù)據(jù)庫日志到文件
- 禁用 JavaScript 執(zhí)行,我們不會使用到這一功能
這里需要特別注意,MongoDB 默認不啟用權(quán)限驗證,任何人都擁有對數(shù)據(jù)庫的操作權(quán)限,我們的服務(wù)器防火墻中禁止了這一端口的連接,但依然建議大家盡量打開權(quán)限驗證功能。
我們強制規(guī)定,一個服務(wù)只能讀寫一個數(shù)據(jù)庫,但可以讀取其它數(shù)據(jù)庫,例如簡書小工具集只能讀寫自己的數(shù)據(jù)庫,但可以從 JFetcher 的數(shù)據(jù)庫中獲取數(shù)據(jù)。
每個數(shù)據(jù)庫中允許建立任意多的集合,并且鼓勵對每個有需求的服務(wù)模塊單獨建立集合。
接下來的一條規(guī)定,是我們保證非關(guān)系型數(shù)據(jù)庫不成為“維護噩夢”的關(guān)鍵:將非關(guān)系型數(shù)據(jù)庫當(dāng)成可嵌套的關(guān)系型數(shù)據(jù)庫使用。
具體來說,我們禁止在同一集合中存放多種不同類型的數(shù)據(jù)。
另外,當(dāng)字段數(shù)據(jù)為空時,一律使用 None 代替,禁止使用對應(yīng)數(shù)據(jù)類型的默認值。
例如,文章排行榜數(shù)據(jù)中,如果無法獲取到文章標題,標題字段依然不允許省略,不允許使用空字符串代替,必須使用 None 填充。
嵌套數(shù)據(jù)出現(xiàn)空值時,不能使用 None 代替,必須填寫空字典作為占位符。
對不穩(wěn)定的數(shù)據(jù)來源,存入數(shù)據(jù)庫前必須使用映射關(guān)系進行處理。任何外部 API、正處于 Beta 階段的服務(wù),都屬于不穩(wěn)定數(shù)據(jù)來源。
即使源數(shù)據(jù)格式與期望的格式完全一致,也必須使用字典映射進行處理。
MongoDB 給我們帶來了什么
首先,基于更完善的數(shù)據(jù)壓縮機制,我們獲得了 30% 以上的空間收益,同時沒有明顯影響數(shù)據(jù)庫的性能。zstd 支持不同壓縮等級,我們目前使用的是默認等級,如果后期數(shù)據(jù)量進一步增大,可能會考慮對部分訪問不頻繁的數(shù)據(jù)使用更高的壓縮等級。
在高負載場景下,數(shù)據(jù)庫導(dǎo)致的性能瓶頸得到了一定程度的改善,對少量熱點數(shù)據(jù)的高頻訪問測試中,效果尤其明顯。
我們從服務(wù)中去除了對 Peewee ORM 庫的依賴,改為依賴更完善的 pymongo 庫,同時,基于異步的 motor 庫為我們的服務(wù)異步化過程帶來了很大幫助。
在數(shù)據(jù)庫操作層面,我們的關(guān)注點從設(shè)計表結(jié)構(gòu)、編寫映射邏輯轉(zhuǎn)換為對數(shù)據(jù)庫索引、數(shù)據(jù)存儲結(jié)構(gòu)的優(yōu)化。
對于簡單的數(shù)據(jù)操作,我們更傾向于使用 MongoDB 的聚合功能完成,這提升了在大規(guī)模數(shù)據(jù)處理中的程序性能,在一些側(cè)重于展示而不是分析的服務(wù)中,我們?nèi)サ袅藢?Pandas 的依賴,間接降低了服務(wù)部署耗時和資源占用。
在數(shù)據(jù)備份中,我們成功實現(xiàn)了數(shù)據(jù)庫向阿里云 OSS 的自動備份,大大提升了數(shù)據(jù)安全性。
mongodump 工具大大降低了數(shù)據(jù)導(dǎo)出的復(fù)雜度,也在一定程度上縮短了數(shù)據(jù)分析的前期準備時間。
總結(jié)
本期內(nèi)容介紹了我們在數(shù)據(jù)庫轉(zhuǎn)型過程中的經(jīng)驗,我們的目標是構(gòu)建更加先進的服務(wù)體系和基礎(chǔ)架構(gòu),讓開發(fā)者將更多精力放到業(yè)務(wù)邏輯上,讓用戶使用性能優(yōu)異、設(shè)計合理的服務(wù)。
技術(shù)說系列將繼續(xù)為大家講解我們的技術(shù)歷程,歡迎大家持續(xù)關(guān)注。