多线程环境内存数据安全持久化到磁盘

2018-11-08 15:33:21 +08:00
 firebroo

问题

最近一个项目,对计算效率时实性要求比较高,我把所有数据都放到内存里面操作计算,这个时候如果进程被意外终止(人为 kill 或者说 oom kill 都可能),数据就会全部丢失,所以需要定时的把内存数据备份到磁盘(数据可能几十 M,也可能十几个 G ),这样进程重启就可以读取备份文件恢复数据到内存,丢失几分钟的数据还是可以接受的;还有一个需求是需要有个触发器线程,会在一定情况触发去同步内存数据到硬盘。

思考

总的来说就是一个定时线程,一个触发器线程,有几率会出现多线程写一个文件情况,多线程写文件比较好解决,首先想到的就是给文件上锁,确实可以解决多线程环境写的问题,但是无法解决在写的时候,进程被意外 kill,写到一半操作被终止,数据就会被损坏,这个时候比较尴尬的就是直接在原文件基础操作的,数据完全被损坏,进程重启找不到完整的数据去恢复,于是想到每次备份的时候写到不同的文件里面,这个时候面临的问题就是如果数据很大,就会产生 n 个备份文件,极端情况也无法接受,毕竟磁盘也是钱阿。所以抛出的问题就是多线程环境内存数据安全持久化到磁盘。

解决方案

寻找一种原子级别的备份操作,备份成功则数据更新,备份失败保留原始数据,由于是原子操作,也不存在多线程的竞争问题

具体实现

首先将内存数据写入一个随机的 tmp 文件,然后使用 rename 函数将 tmp 文件更新为备份文件名字,rename 的 manpage

If newpath already exists it will be atomically replaced (subject to a few conditions; see ERRORS below), so that there is no point at which another process attempting to access newpath will find it missing.

重点就是只要操作系统不 crash,rename 操作就是原子的。

bool 
concurrent_safe_backup()
{
    ofstream ofs; 
    std::string tmp = "tmp";

    static default_random_engine e(time(0));
    //random filename
    tmp += to_string(e());

    //open
    ofs.open(tmp);
    if (!ofs) {
        return false;
    }

    std::string contentString = "data";
    ofs << contentString;
    ofs.close();

    int ret = rename(tmp.c_str(), "memory.bak");
    if (ret == -1) {
        return false;
    }
    //done
    return true;
}
4132 次点击
所在节点    C
44 条回复
feverzsj
2018-11-08 19:37:45 +08:00
@firebroo 知不知道什么叫事务性啊?我根本不需要了解你那些小儿科的场景应用
firebroo
2018-11-08 19:43:12 +08:00
@429839446 可以可以,就是类似这种,mmkv 这样的实现就完全丢失不了数据了。
xylophone21
2018-11-08 19:47:34 +08:00
@muntoya 说到问题的关键了,但感觉楼主还在一些不重要的细枝末节上出不来,或者没找到重点,不解释。
firebroo
2018-11-08 19:47:46 +08:00
@feverzsj 我讨论 redis 这种内存数据库是如何实现安全持久化内存数据到磁盘的,然后自己实现个类似的,你让我把这种事情交给 redis 去搞,你是扛精。。?
feverzsj
2018-11-08 19:53:15 +08:00
@firebroo 我根本没提过 redis,你说说你想实现的东西,有什么不能用 sqlite 实现的? sqlite 能在保证事务性的基础上达到良好的 io 效率,你自己用标注库 io 能做到?
firebroo
2018-11-08 19:59:32 +08:00
@feverzsj redis 和 sqlite 有差吗,就是把这件事交给数据库去完成,某些场景确实需要,不然微信为啥有 mmkv 这种轮子不用 sqlite
feverzsj
2018-11-08 20:03:31 +08:00
@firebroo mmkv 是非常低层次的 kvstore,根本不管你数据死活,sqlite 是完整的关系型数据库,根本不是一个层面的
liuxu
2018-11-08 20:10:16 +08:00
能不能写到 ramdisk 呢,然后一个线程定期刷到硬盘呢,记录好日志,这样进程即使死了 ramdisk 数据依然不会丢失,重启进程分析日志继续写入到硬盘
firebroo
2018-11-08 20:14:24 +08:00
@feverzsj 不抬杠了,太累,下次我用 sqlite
firebroo
2018-11-08 20:18:59 +08:00
@xylophone21 科普一下,我确实没有看懂 3 楼回复,redis 的实现没看过
firebroo
2018-11-08 20:32:04 +08:00
@muntoya
@xylophone21 原来是你好像真的没看懂问题是啥。。我刚去翻了下 redis 持久化的源码
```c
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
if (rename(tmpfile,filename) == -1) {
serverLog(LL_WARNING,"Error moving temp append only file on the final destination: %s", strerror(errno));
unlink(tmpfile);
return C_ERR;
}
```
msg7086
2018-11-09 09:11:26 +08:00
没看懂问题是啥。
写临时文件然后 rename 算是基本操作,rsync 之类的软件也是这么跑的。
值得注意的是你这里写完临时文件以后需要 flush 一下底层的文件系统,确保文件内容已经刷入磁盘了,再 rename 比较好。
Monad
2018-11-09 10:03:33 +08:00
@msg7086 #32 只考虑 kill -9 进程的话 操作系统应该可以保证落盘的吧
firebroo
2018-11-09 10:22:17 +08:00
@msg7086 我 rename 之前已经 close 了 tmp 文件,会自动 flush 吧
msg7086
2018-11-09 10:29:22 +08:00
@Monad @firebroo kill -9 不影响。主要是考虑断电的问题。
firebroo
2018-11-09 10:38:42 +08:00
@msg7086 真实环境,会可能断电。。
msg7086
2018-11-09 10:44:25 +08:00
@firebroo 会断电的话还是查一下文件系统的文档吧,看看什么时候强刷。保险起见可以手动强刷再 RENAME。
firebroo
2018-11-09 10:46:53 +08:00
@msg7086 fflush:是把 C 库中的缓冲调用 write 函数写到磁盘[其实是写到内核的缓冲区]。fsync:是把内核缓冲刷到磁盘上。 这个吧?学习了。
no1xsyzy
2018-11-09 11:10:27 +08:00
@firebroo #31 https://github.com/antirez/redis/blob/2f8f29aa0e63a198aa628296ce617214b3ae1575/src/aof.c#L1540
Redis 实现 rewriteAppendOnlyFileBackground() 的时候用了 fork() 而在子进程中断开监听端口,调用 rewriteAppendOnlyFile() 进行写入。
不过 Redis 是单线程的,多线程的话 fork 要处理锁,因为这时候系铃人可能消失了,解不了铃。
no1xsyzy
2018-11-09 11:21:11 +08:00
@firebroo #14 全量写入也可以追加文件末尾啊,虽然又碰上数据量过大的问题了。有没有想过上磁带?
或者保证只有 2 份文件存在。写 A 不碰 B,写 B 不碰 A。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/505805

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX