技術(shù)說 | MongoDB 與我們的存儲解決方案

繼服務(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)注。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容