安全_践行_MongoDB数据备份与方案选择

记录一次MongoDB数据备份与技术方案的抉择,了解一下「全栈」的魅力:)

文章首发:fujia.site

前言

随着最近几次大的更新,fujia.site这个个人站点终于可以见人了,主要更新包括:支持GitHub登录、分页组件的优化、添加广告以及开启HTTP2等等。一切看上去还不错,一个维护了5年的个人站点真正有点东西了。

让小编感到不安是什么时候呢?是随着博文的页码数突破10的时候,这意味着小编一点点已经积攒了100多篇文章。你开始不得不去思考一个问题,对一个内容网站(或任何网站)来说,真正的核心是什么?对,是数据。

小编知道,网站崩溃或服务器数据被清空的可能性微乎其微,但是,万一呢?万一网站受到某个萌萌哒的黑客入侵了,删除了所有的数据。那么真正的损失是什么?你如何快速(如:10分钟)恢复站点运营?

损失的是源码吗?不是的,源码已经托管在gitee的私库上了,站点部署也不过是一条命令的事情。多说一句,大部分源码是3年前写的,如今去看总觉得有些辣眼睛,每次改动都忍着恶心一点点重构。当然,要小编整体重构是不存在的,将就着用吧!

真正的损失是那一篇篇小编一个字一个字敲出来的,然后变成了一条条数据的文章。 一旦损失,基本很难复原,只能哭了。

站点安全下的数据安全保障就成了一个势在必行的行动,只有有了基础的数据安全上的保障,即数据库备份,这个站点才具备基础的完备性。

方案选型

在讨论方案之前,我们先来看下实现后的结果(如下图),站点会在每周三和周日的凌晨3点做一次备份,备份完成后将数据自动上传到腾讯云的OSS上。

事实上,在做数据备份之前,小编一度以为这是一个很简单的功能,可以很快实现。但当真正操作起来,各种各样的问题都冒出来了。

了解下服务端的环境和架构:

环境说明:

server OS: Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-72-generic x86_64);

docker: version 20.10.2;

mognodb: version 4.4;

egg.js: version 2.29.1。

架构说明:

数据库:MongoDB+docker swarm搭建了一个简单的一主二从的复制集;

后台服务:egg.js+docker swarm搭建了两个节点的服务集群;

代理服务:nginx+docker swarm搭建了两个节点的代理服务集群。

后台服务和数据库之间通过docker network连通,并使用了docker secret对数据库密码加密处理。

基于此,我们来聊聊可行的方案,如下:

1. eggjs自带的schedule

eggjs schedule 

最简单的方式是使用eggjs自带的schedule,它可以简单、快速地实现定时任务。

一种方式是是在后台服务中启用定时任务,这样做的好处是:

实现简单,且相关的逻辑都放在了后台服务中,便于管理;

定时任务和后台服务是一致的,即当后台服务终止时,定时任务也清除了,这样就不必花费精力去管理定时任务。

那实现的难点是什么呢?

我们知道数据库和后台服务都包裹在docker container中,需要通过跨容器通信。额...,听着有点难!

一种折中的方法是在服务器使用egg.js起一个本地服务来做数据库的备份,方法是可行的,但我们的期望是所有的应用服务都包裹在docker中,节省服务器资源。

2. 手动备份

在站点早期或网站PV较少的情况下,如果想做数据备份,这也是一个不错的选择,手动备份虽然麻烦,但是因为没有必要频繁的备份操作,也可以接受。

MongoDB备份命令如下:

# ubuntu 下安装mongo-tools,若已安装则跳过sudo apt install mongo-toolsmongodump --uri="mongodb://[用户名]:[密码]@[IP]/[数据库名]" -o [输出目录] --forceTableScan

备注:为什么使用--forceTableScan,见:;

备份完成后,使用scp命令将文件从服务器上拷贝到本地宿主机,使用man scp查看scp的使用手册。

3. 使用shell + crontab

一般来说,数据备份是「运维工程师」的职责,这也是他们采用的方法,但对小编来说,简单的shell编写还行,复杂点的话,就需要系统深入的学习了,成本太高,这里就不展开了。

4. 使用shell + crontab + node.js

这是目前我们使用的方案:

shell:做一些自动化的处理;

crontab:执行定时任务;

node.js:实现业务逻辑,如:备份、上传OSS以及删除备份等。

这种方案的好处是:

不需要太高的学习成本,核心逻辑使用node.js实现;

多种技术的最佳组合,对前端来说,有很大的想象空间。

不好的地方是:

仍需要学习shell和crontab,相关资料见「参考资料」部分;

需要自行管理定时任务,如:服务已停止,需要手动清理定时任务。

对前端工程师来说,将业务功能使用node.js来实现,再与其它技术相结合,可以有很多的玩法,进一步拓宽前端的边界。

实现

服务器需要安装node,推荐使用nvm 安装。

实现原理

使用shell做一些自动化处理,具体的见代码实现:

将项目拷贝从本地拷贝到服务上;

执行定时任务。

使用crontab制定定时任务,见代码实现。

使用node .js完成业务功能,见代码实现。

代码实现

使用下面的命令在本地新建一个npm项目:

mkdir tencent-schedule;cd $_;npm init -y

说明:下面所有的文件和文件夹都是相对tencent-schedule目录来说的。

创建两个脚本文件:

scp.sh

#!/bin/bashssh -tt -p [端口] [用户名]@[ip] << EOFif [ ! -d "/home/ubuntu/schedules/dump/mongodb" ]; thenmkdir -p /home/ubuntu/schedules/dump/mongodbelse rm -rf /home/ubuntu/schedules/dump/mongodb/librm -rf /home/ubuntu/schedules/dump/mongodb/binfiexitEOFnpm run buildCUR_DIR=$(pwd)scp -P[端口] -r $CUR_DIR/lib $CUR_DIR/bin $CUR_DIR/package.json $CUR_DIR/dump.sh [用户名]@[ip]:/home/ubuntu/schedules/dump/mongodb

dump.sh

#!/bin/bashsource /etc/profile; mongo-dump --start

编辑/etc/profile,在最后加上下面的语句。

为什么呢?Crontab 有自己的运行环境(/etc/crontab),会自动设置可构成最小环境的环境变量, 不会自动加载当前用户的环境变量!这点很重要,小编在这个地方卡很久,需要注意下。

PATH=$PATH:/home/ubuntu/.nvm/versions/node/v16.14.2/binexport PATH

修改package.json,只列出部分:

{ // ... "main": "./lib/index.js","bin": {"mongo-dump": "./bin/index.js"},"scripts": {"build": "tsc","test": "jest"},// ...}

新建bin/index.js

#!/usr/bin/env noderequire(../lib);

新建src目录,并创建index.ts和config.ts文件,实现如下:

index.ts

import { CommonSpawnOptions, spawn } from child_process;import path from path;import process from process;import COS from cos-nodejs-sdk-v5;import { pathExist } from @fujia/check-path;import conf from ./config;const SERVER_DUMP_DIR = /home/ubuntu/data/mongodb/dump;const DB_NAME = fujiaSite;const spawnAsync = (command: string,args: readonly string[],options: CommonSpawnOptions): Promise<number | null> => {return new Promise((resolve, reject) => {const cp = spawn(command, args, options);cp.on(error, (err) => {reject(err);});cp.on(exit, (chunk) => {resolve(chunk);});});};const args = process.argv.slice(2);const isStart = args.includes(--start) ? true : false;const isDev = args.includes(--dev) ? true : false;if (!isStart) {console.warn(warning: ,invalid options! you should provide "-start" option.);process.exit(0);}async function dumpMongoData() {const cmd = isDev ? touch : mongodump;const descDir = path.join(SERVER_DUMP_DIR);const cmdArgs = isDev? [`${SERVER_DUMP_DIR}/test.txt`]: [--uri="mongodb://[用户名]:[密码]@[IP]/[数据库名]",`-o ${descDir}`,--forceTableScan,];isDev && console.log(info: , `Starting run ${cmd} command`);const execCode = await spawnAsync(cmd, cmdArgs, {stdio: inherit,shell: true,cwd: descDir,});if (execCode === 0) {console.log(dump mongo data successful!);await zipData();}}async function zipData() {const now = Date.now();const tarName = isDev ? `test_${now}.tar.gz` : `${DB_NAME}_${now}.tar.gz`;const sourceDir = path.join(SERVER_DUMP_DIR, DB_NAME);const execCode = await spawnAsync(tar, [-zcvf, tarName, sourceDir], {stdio: inherit,shell: true,cwd: SERVER_DUMP_DIR,});if (execCode === 0) {console.log(zip mongo data successful.);await uploadTencentOSS(tarName);await delHostData();}}async function uploadTencentOSS(fileName: string) {const filePath = path.join(SERVER_DUMP_DIR, fileName);const { cos: cosConf } = conf;const { datadump } = cosConf;const { Bucket, Region, uploadDir } = datadump;if (!(await pathExist(filePath))) return;const cos = new COS({SecretId: conf.cos.SecretId,SecretKey: conf.cos.SecretKey,});return new Promise((resolve, reject) => {cos.uploadFile({Bucket /* 填入您自己的存储桶,必须字段 */,Region /* 存储桶所在地域,例如ap-beijing,必须字段 */,Key: `${uploadDir}/${fileName}` /* 存储在桶里的对象键(例如1.jpg,a/b/test.txt),必须字段 */,FilePath: filePath /* 必须 */,SliceSize:1024 * 1024 * 5 /* 触发分块上传的阈值,超过5MB使用分块上传,非必须 */,onTaskReady: function (taskId) {/* 非必须 */console.log(taskId);},onProgress: function (progressData) {/* 非必须 */console.log(JSON.stringify(progressData));},onFileFinish: function (err, data, options) {console.log(options.Key + 上传 + (err ? 失败 : 完成));},},function (err, data) {if (err) {console.log(err);reject(err);return;}isDev && console.log(upload data successful!);resolve(data);});});}async function delHostData() {const execCode = await spawnAsync(rm, [-rf, `${SERVER_DUMP_DIR}/*`], {stdio: inherit,shell: true,});if (execCode === 0) {isDev && console.log(deleted data successful.);}}function main() {if (isStart) {console.log(Starting dump mongodb data...);dumpMongoData();}}main();

config.ts

const defaultConfig = {cos: {SecretId: ,SecretKey: ,datadump: {Bucket: ,Region: ,uploadDir: ,},},};export default defaultConfig;

执行scp.sh脚本将项目拷贝到服务器上。

进入服务器,在项目根目录下执行npm link,使mongo-dump(在package.json中定义的)命令全局可用。

使用下面命令编辑crontab定时器

crontab -e# 插入下面的定时任务0 3 * * 0,3 bash /home/ubuntu/schedules/dump/mongodb/dump.sh >> /home/ubuntu/dump.out# 查看定时任务crontab -l

备注:如果定时命令命令未生效,可以重启下服务sudo service crontab restart .

如此,一个简单的定时数据库备份任务就完成了,整体的逻辑并不复杂。

小结

对前端工程师来说,掌握好技术深度和技术广度之间的平衡是非常重要的,稍不小心就会走很大的弯路,这一点,小编是深有体会且吃过大亏。

说实话,纯粹的技术前端天花板是相对较低的,作为跟终端最近的工程师,做一个功能完备的产品是大有可能的。但是,这里也不是建议你去走「全栈」的路子,坑是真的多,你需要认真思考,自己技术的前行路径究竟是什么?时间真的过的很快。

建议学好node.js,这样就有了和其它技术组合的基础,从而衍生出不同技术玩法,可想象的空间也大得多。

不要自我设限,大胆想象,元宇宙和web3.0正在路上,作为前端工程师的我们,且行且看。最重要的是,积极参与并做好准备。

参考资料

nvm - ; .

shell - ?c=linux  .

crontab - ; .

大家加油 🙂