自动备份蚂蚁笔记所有数据到Dropbox

记一次翻车

前些天用服务器下载东西,拖回本地的时候随手开了一个FTP服务器。由于下载比较慢,放在后台让它自己慢慢下载去了。然后就关机回家,完全忘了服务器上有一个临时FTP正在运行的事。

(为了让更多人体会到翻车的乐趣,这里放上只要装了python就能运行的零配置FTP脚本

danger_ftp.bash
import os
from pyftpdlib.servers import FTPServer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.authorizers import DummyAuthorizer
 
address = ("::", 21)  # listen on localhost, port 21
handler = FTPHandler
 
authorizer = DummyAuthorizer()
authorizer.add_anonymous(os.getcwd(),perm='elradfmwM')
 
handler.authorizer = authorizer
 
ftpd = FTPServer(address,handler)
ftpd.serve_forever()

anonymous、写权限、粗心管理员这下凑齐了,这车岂有不翻之理?两个星期后我想起来这事儿的时候,发现蚂蚁笔记打不开了(中间还有插曲,此处不便细说,否则我不至于两个星期后才发现服务端有问题)。进来一看,FTP能接触的每个目录下都被塞了一个Photo.scr,所有的HTML文件都被篡改过……

查了下,知道这貌似是中了利用匿名FTP传播的挖矿病毒。虽然在Linux下Photo.scr不能执行,删掉就好,但所有的HTML都不行了,只好重装蚂蚁笔记。重装时我还庆幸着病毒是全自动执行,没有手动搞点恶意动作,比如删库之类的。知道所有服务运行起来,图片还是打不开时,我才发现,蚂蚁笔记所有的附件都没了……

自动定时备份

后来找到一个之前的备份,总算是没啥大的损失,不过浪费了一整个晚上。当时我就把自动备份系统提上了日程。

其实半年前我就把**自动异地备份**提上过日程,后面的情况可想而知。此番若不是[fengbrute兄](https://github.com/fengbrute)的回帖,没准我又要好了伤疤忘了疼了。

几天之后,当时请教车祸善后问题的issue有了回帖。于是我研究了fengbrute兄的自动备份方案,然而我没有群晖或者同类的东西,只能是结合这篇文章,使用这个5K Star的脚本:Dropbox-Uploader,做了一个自动备份蚂蚁笔记数据,然后自动上传到Dropbox的方案出来。

一般个人站点,有效数据有上百兆就不错了,除非你有拿蚂蚁笔记当相册的习惯。因此,备份到Dropbox的话,空间不会是问题。~~实在不够还可以去马云家买扩容服务嘛。~~废话不说,代码如下:

autoBackup.bash
#!/bin/bash
# 一些配置
# Dropbox 目录,根目录 / 是你已经创建的 app 目录
DROPBOX_DIR=/$(date +%Y-%m-%d)
MONGO_USER="用户名"
MONGO_PASS="密码"
MONGO_DB="数据库名"
LEANOTE_DATA_FILE_PATH=/path/to/leanote/files/dir
 
#临时目录
TEMP_PATH=/path/to/temp/dir/
DB_DUMP_TEMP_PATH=/path/to/temp/dir/db
#可执行文件路径
MONGO_DUMP_EXE=/path/to/mongodb/mongodump
DROPBOX_UPLOADER=/root/leanote/autoBackup/dropbox_uploader.sh
 
# 定义备份文件名
DataBakName=DB_$(date +"%Y-%m-%d").tar.gz
FileBakName=File_$(date +%Y-%m-%d).tar.gz
 
# Dropbox 里 90 天以上的旧数据可以清除
Old_DROPBOX_DIR=/$(date -d -90day +%Y-%m-%d)
 
# 导出 Mongo 数据库,并压缩
echo -ne "Dump database..."
$MONGO_DUMP_EXE -d $MONGO_DB -u $MONGO_USER -p $MONGO_PASS --gzip -o $DB_DUMP_TEMP_PATH
tar zcf $TEMP_PATH/$DataBakName $DB_DUMP_TEMP_PATH/$MONGO_DB
rm -rf $DB_DUMP_TEMP_PATH/*
echo -e "Done"
 
# 备份附件等文件
echo -ne "Backup Leanote files..."
tar zcf $TEMP_PATH/$FileBakName $LEANOTE_DATA_FILE_PATH
echo -e "Done"
 
# 开始上传到 Dropbox
echo -e "Start uploading..."
$DROPBOX_UPLOADER upload $TEMP_PATH/$DataBakName $DROPBOX_DIR/$DataBakName
$DROPBOX_UPLOADER upload $TEMP_PATH/$FileBakName $DROPBOX_DIR/$WebBakName
# 清理 Dropbox 里 90 天前的旧数据,这句可能会失败,没关系,90天以后它就有用了……
$DROPBOX_UPLOADER delete $Old_DROPBOX_DIR/
 
#清理本地文件
rm -rf $TEMP_PATH/$DataBakName
rm -rf $TEMP_PATH/$FileBakName
 
echo -e "All done."

具体步骤:

  1. 按照Dropbox-Uploader的说明,下载之,配置之,直至你可以./dropbox_uploader.sh mkdir lalala并在你Dropbox里面看到这个新建的lalala文件夹。阿里云等访问不通的问题,只能是各显神通了……
  2. 把上面的脚本里配置成自己需要的路径,测试一下,应该直接就看到Dropbox里面刚刚上传的文件了。把文件下载下来打开看看,确认内容和大小正常;
  3. 定时任务:新建cron配置如下。注意路径问题。 shell # 14:00 in US is about 03:00 in China 0 14 * * * /path/to/autoBackup/doBackup.sh > /path/to/backupLog.log 2>&1

理论上,如果都没错的话,就应该每天都能看到新的备份了。

小内存VPS上的`oom`的问题

~~因为穷,~~我是在一个128M的乞丐版OpenVZ VPS上运行这么一套东西。运行上述脚本/定时任务可能遇到的问题是:mongoDB可能在后面tar打包或者上传的时候由于占用内存太多(多于一半,高于任何其他进程)被oom kill掉。

由于mongoDB会自动做内存缓存,在任何机器上他都会默认吃掉一半以上的内存,而这些内存并不是非用不可的,没有他们也就是多几次硬盘IO而已。mongoDB文档上声称这些内存缓存是浮动的,如果有其他程序想吃内存,mongoDB会主动释放一些给他们用。内里的策略就不清楚了,我估计是隔几秒钟看一眼系统空闲内存,如果实在太少了,就吐出来一点还给系统。这里有一个重要前提是:在这个辗转腾挪的过程中,需要有SWAP空间暂时顶上,维持程序不被oom——然而乞丐版OpenVZ显然是不具备SWAP的,于是系统就从吃内存最大的mongoDB开始杀了。这个策略是默认内置的,这年头才不会有人为128M没SWAP的机型专门做优化。

解决方案之一是把mongoDB做成会自动重启的系统服务,或者在脚本末尾检测mongoDB还活着没,死了的话再电它一下。然而这终究不是个让人舒坦的解决办法。

于是我查了半天如何限制mongoDB的内存使用,搜索结果中有一半是解释mongoDB内置的自动释放机制设计的多么好,小白看见RAM被占用了不用担心云云,另一小半是在说用cgroup限制它——且不说再开个cgroup又要多少内存,即使用了,cgroup也就是在超限的时候提前kill、不等系统oom而已,在小鸡上面用处不大。

后来终于翻到了一个选项:--wiredTigerCacheSizeGB ,此参数可以直接设定内存池占多少GB,多少GBGB……虽然这个选项的单位制看得我很不好意思,我还是横下心来在配置文件中把它改成了0.039,也就是大约40M,重启服务,free -h一下,果然内存空出来一大半,再执行脚本试验,毫发无伤,皆大欢喜。

文件积累变多之后,笔记的files目录可能增大到上百兆,此时有可能出现的情况是:上传文件的过程中需要大量的内存。由于dropbox_uploader实际上调用curl进行网络操作,上传过程中会报错curlkill,然后大文件传不上去。出现这种情况的时候,即使调整过上述mongoDB选项,它多半也已经被干掉以便腾出空间给curl了,这就叫同归于尽啊。

dropbox_uploader.sh文件开头有一个迷惑性相当大的选项:CHUNK_SIZE,说明指出这是以M为单位的大文件分块传输时所用的块大小,我把它设置为了5,按理说再大的的文件也该分割成5M的小块分别上传,怎么会内存超限?

搜了下github的issue,果然找到有人遇到了这个问题。作者透露,脚本里面还有个隐藏的选项,控制着体积大于某个数值的文件才会启用分块上传机制,而这个默认值是150M……

这个数值直接写在了if [[ $FILE_SIZE -gt 157286000 ]]; then一行中,以字节为单位。我将它改成了5242880也就是5M,测试一下,完美通过。

PS 1: 开始时我发现备份失败还以为是`tar`打包那一百多兆文件时被搞掉的。这样看来,老牌工具火这么多年果然是有原因的。

> PS 2: 突然想到我之前还用过一个`64M`内存的小鸡,我觉得这种机器就不要挣扎了吧,认命是福……

# 重要修正与补充

2018年02月16日添加

昨天迁移服务器,很幸运或者很悲哀地发现,上面脚本备份的东西是不能用的,原因有两个:

  1. Leanote的迁移必须整个目录迁移,原因是有一些除files目录之外的数据也是动态生成的,所以不能只备份files目录,而应该备份整个leanote文件夹。这个文件夹是可以直接迁移的。如我之前想象的一样“从官网下载个新的leanote,然后把files目录恢复进去就能用”的想法,是错误的。
  2. 在数据逐渐增多时,在超小内存机器上上述脚本会无预警地生成损坏的数据。这一点非常可怕,原因是小内存机器从本地dump数据库的时候,高速读取会造成mongo引擎被OOM,然后留下的部分损坏的数据被“正常地”压缩、上传。大内存机器上是不会出现这个问题的。目前检查数据是否完整的官方方法不多,我想到的方案是时不时检查数据库进程运行时间是否超过一天,超过了就说明上一次备份没有挂掉。

同时,改进了删除老数据机制,每个月15号不删除老数据。新脚本如下:

autoBackup.bash
#!/bin/bash
# 一些配置
DROPBOX_DIR=/$(date +%Y-%m-%d) # Dropbox 目录,根目录 / 是你已经创建的 app 目录
MONGO_USER="用户名"
MONGO_PASS="密码"
MONGO_DB="数据库名"
LEANOTE_DATA_FILE_PATH=/root/leanote/leanote
 
# command prefix to minimize resource usage
#CMD_PREFIX="nice -n19 ionice -c3 nocache"
CMD_PREFIX="" #"ionice -c3 nocache"
 
#临时目录
TEMP_PATH=/root/leanote/autoBackup/temp/
DB_DUMP_TEMP_PATH=/root/leanote/autoBackup/temp/db
#可执行文件路径
MONGO_DUMP_EXE=/root/leanote/mongodb-3.6.2/mongodump
DROPBOX_UPLOADER=/root/leanote/autoBackup/dropbox_uploader.sh
 
# 定义备份文件名
DataBakName=DB_$(date +"%Y-%m-%d").tar.bz2
FileBakName=File_$(date +%Y-%m-%d).tar.bz2
 
# Dropbox 里 90 天以上的旧数据可以清除
Old_DROPBOX_DIR=/$(date -d -90day +%Y-%m-%d)
 
# 导出 Mongo 数据库,并压缩
echo -ne "Dump database..."
$CMD_PREFIX $MONGO_DUMP_EXE -d $MONGO_DB -u $MONGO_USER -p $MONGO_PASS --gzip -o $DB_DUMP_TEMP_PATH
$CMD_PREFIX tar -cjf $TEMP_PATH/$DataBakName $DB_DUMP_TEMP_PATH/$MONGO_DB
rm -rf $DB_DUMP_TEMP_PATH/*
echo -e "Done"
 
# 备份附件等文件
echo -ne "Backup Leanote files..."
$CMD_PREFIX tar -cjf $TEMP_PATH/$FileBakName $LEANOTE_DATA_FILE_PATH
echo -e "Done"
 
 
# 开始上传到 Dropbox
echo -e "Start uploading..."
$CMD_PREFIX $DROPBOX_UPLOADER upload $TEMP_PATH/$DataBakName $DROPBOX_DIR/$DataBakName
$CMD_PREFIX $DROPBOX_UPLOADER upload $TEMP_PATH/$FileBakName $DROPBOX_DIR/$WebBakName
 
# 清理 Dropbox 里 90 天前的旧数据
CurrentDate=$(date +%d)
if [ "$CurrentDate" -ne "15" ];
then
	echo -e "Deleting old data..."
	$CMD_PREFIX $DROPBOX_UPLOADER delete $Old_DROPBOX_DIR/
else
	echo -e "I won't deleted old data today!"
fi
 
#清理本地文件
$CMD_PREFIX rm -rf $TEMP_PATH/*
 
echo -e "All done."
  • 最后更改: 2019/05/29 15:58