安全 | 践行:MongoDB数据备份与方案选择

464 阅读7分钟

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

文章首发:fujia.site

前言

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

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

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

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

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

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

方案选型

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

MongoDB Dump

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

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

环境说明:

  • 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-tools

mongodump --uri="mongodb://[用户名]:[密码]@[IP]/[数据库名]" -o [输出目录] --forceTableScan

备注:为什么使用--forceTableScan,见:fujia.site/articles/60…

备份完成后,使用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安装。

实现原理

  1. 使用shell做一些自动化处理,见代码实现:
  • 将项目拷贝从本地拷贝到服务上;
  • 执行定时任务。
  1. 使用crontab制定定时任务,见代码实现。

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

代码实现

  1. 使用下面的命令在本地新建一个npm项目:
mkdir tencent-schedule;cd $_;npm init -y

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

  1. 创建两个脚本文件:

scp.sh

#!/bin/bash

ssh -tt -p [端口] [用户名]@[ip] << EOF

if [ ! -d "/home/ubuntu/schedules/dump/mongodb" ]; then
  mkdir -p /home/ubuntu/schedules/dump/mongodb
else 
  rm -rf /home/ubuntu/schedules/dump/mongodb/lib
  rm -rf /home/ubuntu/schedules/dump/mongodb/bin
fi

exit
EOF

npm run build

CUR_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/bash

source /etc/profile; mongo-dump --start

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

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

PATH=$PATH:/home/ubuntu/.nvm/versions/node/v16.14.2/bin
export PATH
  1. 修改package.json,只列出部分:
{
 // ...
 "main": "./lib/index.js",
  "bin": {
    "mongo-dump": "./bin/index.js"
  },
  "scripts": {
    "build": "tsc",
    "test": "jest"
  },
	// ...
}
  1. 新建bin/index.js
#!/usr/bin/env node

require('../lib');
  1. 新建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;
  1. 执行scp.sh脚本将项目拷贝到服务器上。

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

  3. 使用下面命令编辑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 .

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

小结

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

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

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

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

参考资料

  1. nvm - github.com/nvm-sh/nvm .

  2. shell - www.imooc.com/course/list… .

  3. crontab - www.imooc.com/learn/1009 .

大家加油 :)