利用Git来增量备份中小型数据库
2017-12-21 17:42:39 +08 字数:1810 标签: Git数据库的备份与还原,是网络服务的重要一环。 现在各大云平台都推出了独立的数据库服务,包揽了这个问题。 但是对于中小型数据库来说,用这样的独立服务并不划算,所以还是得自己面对这个问题。
本文以SQLite为例,介绍一种用Git进行数据库增量备份的野路子。
思路 ¶
最简单的备份思路是,复制一份,这就是镜像备份。 这种做法简单有效,但有一个致命问题——历史丢了。 如果需要恢复到一段时间以前,而非上一次备份,就无法做到。
数据库备份一般的思路是,压缩打包,标上时间戳——这是全量备份。 全量备份的缺点是,占用的空间过大,往往需要定期删除。 假如数据库需要恢复到很久以前的版本,这种做法就失效了。
在此基础上,对空间占用进行优化,就是增量备份。 而无论以什么方法做增量,都会有一个问题:假如某一天的备份丢失了,那以后数据都无法恢复了。 而且,恢复的时间往往很长,因为需要从第一天备份开始合并。
在全量备份基础上,对恢复时间进行优化,就是差分备份。 以第一个备份为基础,其它备份都是这个备份的差分。 这降低了恢复的时间,因为只需要合并一次,但空间占用比增量备份大,甚至与全量备份相差无几。
还有一个方案是对以上思路的结合。 大周期进行全量备份,中周期进行差分备份,小周期进行增量备份。 这样可以尽量扬长避短。
有没有什么工具是既增量存储,又可以快速合并成任何时间节点,甚至能增量传输的?
——Git!
数据库有没有办法变成纯文本文件存储?
——SQL!
这个野路子的来历,就是这么简单。
备份 ¶
以SQLite为例,而非泛泛而谈,是因为有一个与数据库特性相关的功能,必须具体。 孤不是在说dump,而是说backup。
SQLite从db文件到sql文件很简单,就是一个dump。
sqlite3 some.db .dump > backup.sql
然而,当数据库在高频读写时,直接做dump操作可能会出一些问题。 因为dump相当于高频读操作,会用read-lock抢占数据库资源。 dump的核心逻辑,详见附录。
所以,需要先备份一份,再做dump。 对于Online Backup,SQLite不仅提供了命令,也有API,详见《SQLite Backup API》。 由于有250ms的备份间隔,所以对正常使用的影响极小。 当然,单看时间,比dump或copy要慢得多。
dump完成后,需要提交到某个准备好的Git库中,并且推送到远程仓库,也即备份位置。 完整脚本示例如下:
#!/bin/bash
set -ex
cd $BACKUP_REPO
sqlite3 $TARGET_DB ".backup backup.db"
sqlite3 backup.db .dump > backup.sql
rm backup.db
git add backup.sql
if git commit -m 'Update backup.sql'
then
git push
fi
这样,本地有一个备份,远程有一个备份。 历史清楚,信息详实。
还原 ¶
#!/bin/bash
set -ex
cd $BACKUP_REPO
sqlite3 $TARGET_DB < backup.sql
通常,需要还原的数据库,应该已经停止工作了。
如果需要一边工作一边还原,这种开飞机换引擎的事,SQLite当然……也支持。
这时需要使用.restore
。
#!/bin/bash
set -ex
cd $BACKUP_REPO
sqlite3 backup.db < backup.sql
sqlite3 $TARGET_DB ".restore backup.db"
rm backup.db
总结 ¶
对于SQLite以外的其它数据库,只要能够dump出来内容的SQL,并且SQL还是规整有序的,都可以使用这个方法。 只是在细节上,需要做出调整。
野路子之所以野,不是因为不好用,而是因为适用的情况不够广泛,在特殊情况下,也不够可靠。 并且,由于经过了dump,所以被还原的数据库与原先那个,已经不算同一个了。 比如,孤曾把一个540MB的数据库,还原成213MB。 看上去内容似乎一样,甚至还瘦身了! 至于到底有哪些差异,会不会有负面影响,则只有天知道。
无论如何,这个方法对中小企业、个人开发者来说,还是很合适的。
参考 ¶
- 《[Incremental《 backup - Wikipedia]》
- 《SQLite Backup API》
- 《Command Line Shell For SQLite》
- 《SQLite的Online Backup备份处理》
附录 ¶
以下代码源于sqlite源码的ext/misc/dbdump.c
文件,是dump操作的核心逻辑。
/*
** Convert an SQLite database into SQL statements that will recreate that
** database.
*/
int sqlite3_db_dump(
sqlite3 *db, /* The database connection */
const char *zSchema, /* Which schema to dump. Usually "main". */
const char *zTable, /* Which table to dump. NULL means everything. */
int (*xCallback)(const char*,void*), /* Output sent to this callback */
void *pArg /* Second argument of the callback */
){
DState x;
memset(&x, 0, sizeof(x));
x.rc = sqlite3_exec(db, "BEGIN", 0, 0, 0);
if( x.rc ) return x.rc;
x.db = db;
x.xCallback = xCallback;
x.pArg = pArg;
xCallback("PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\n", pArg);
if( zTable==0 ){
run_schema_dump_query(&x,
"SELECT name, type, sql FROM \"%w\".sqlite_master "
"WHERE sql NOT NULL AND type=='table' AND name!='sqlite_sequence'",
zSchema
);
run_schema_dump_query(&x,
"SELECT name, type, sql FROM \"%w\".sqlite_master "
"WHERE name=='sqlite_sequence'", zSchema
);
output_sql_from_query(&x,
"SELECT sql FROM sqlite_master "
"WHERE sql NOT NULL AND type IN ('index','trigger','view')", 0
);
}else{
run_schema_dump_query(&x,
"SELECT name, type, sql FROM \"%w\".sqlite_master "
"WHERE tbl_name=%Q COLLATE nocase AND type=='table'"
" AND sql NOT NULL",
zSchema, zTable
);
output_sql_from_query(&x,
"SELECT sql FROM \"%w\".sqlite_master "
"WHERE sql NOT NULL"
" AND type IN ('index','trigger','view')"
" AND tbl_name=%Q COLLATE nocase",
zSchema, zTable
);
}
if( x.writableSchema ){
xCallback("PRAGMA writable_schema=OFF;\n", pArg);
}
xCallback(x.nErr ? "ROLLBACK; -- due to errors\n" : "COMMIT;\n", pArg);
sqlite3_exec(db, "COMMIT", 0, 0, 0);
return x.rc;
}
不看不知道,一看吓一跳。 dump操作其实是几个外部SQL操作,和其它C的实现比,性能可想而知。 如果在生产环境上一边高频读写,一边dump……想想也是好可怕!