多线程环境内存数据安全持久化到磁盘 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
firebroo
V2EX    C

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

  •  
  •   firebroo 2018-11-08 15:33:21 +08:00 4532 次点击
    这是一个创建于 2527 天前的主题,其中的信息可能已经有所发展或是发生改变。

    问题

    最近一个项目,对计算效率时实性要求比较高,我把所有数据都放到内存里面操作计算,这个时候如果进程被意外终止(人为 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; } 
    第 1 条附言    2018-11-09 14:53:46 +08:00

    经过大神指点,参考redis实现,最后我修改代码大概是这样,调用fflush和fsync强制数据刷入磁盘。

    bool concurrent_safe_backup() { ofstream ofs; std::string tmp("tmp"); //random filename static default_random_engine e(time(0)); tmp += to_string(e()); //open FILE* fp = fopen(tmp.c_str(), "w"); if (!fp) { return false; } std::string contentString("dataaaaaa\n"); fwrite(contentString.c_str(), 1, contentString.length(), fp); /* Make sure data will not remain on the OS's output buffers */ if (fflush(fp) == EOF) return false; if (fsync(fileno(fp)) == -1) return false; if (fclose(fp) == EOF) return false; /* Use RENAME to make sure the DB file is changed atomically only * if the generate DB file is ok. */ int ret = rename(tmp.c_str(), "memory.bak"); if (ret == -1) { return false; } //done return true; } 
    44 条回复    2018-11-12 13:38:00 +08:00
    polythene
        1
    polythene  
       2018-11-08 15:54:08 +08:00
    Write Ahead Log?
    feverzsj
        2
    feverzsj  
       2018-11-08 16:09:25 +08:00
    把中间数据提交到数据库不就可以了
    muntoya
        3
    muntoya  
       2018-11-08 16:14:21 +08:00   1
    fork 以后在子进程里安心写磁盘就行了,子进程的内存保持不变,redis 就这么做的
    huhu3312
        4
    huhu3312  
       2018-11-08 16:19:01 +08:00
    天池大赛啊。。。。key-value 数据库复赛
    yzmm
        5
    yzmm  
       2018-11-08 16:20:38 +08:00
    hi...
    iiusky
        6
    iiusky  
       2018-11-08 16:23:31 +08:00
    hi,二狗
    petelin
        7
    petelin  
       2018-11-08 17:24:20 +08:00
    推荐看下#3 的方式去写数据. 可以保证内存数据不变.另外你可以创建文件名是 data_{utcnano}.txt 的文件不就不会覆盖之前的了吗. 写失败的你就删了就得了.
    ooo3o
        8
    ooo3o  
       2018-11-08 17:35:17 +08:00
    先分线程各自写一个, 再跑一个慢慢归并.
    firebroo
        9
    firebroo  
    OP
       2018-11-08 18:00:13 +08:00 via Android
    @huhu3312 看了下,还真的是。。不过我是实际场景遇到
    firebroo
        10
    firebroo  
    OP
       2018-11-08 18:04:28 +08:00 via Android
    @muntoya 问题不在于内存变不变。。而是解决写的时候写操作被 kill -9 中断导致写文件损坏
    firebroo
        11
    firebroo  
    OP
       2018-11-08 18:05:54 +08:00 via Android
    @petelin 离题。。我文章里面写明了多个文件会消耗磁盘,pass 掉
    leavan
        12
    leavan  
       2018-11-08 18:29:15 +08:00
    一般来说如果是追加到文件末尾的话,即使 kill 掉也只会导致文件末尾没写上...
    feverzsj
        13
    feverzsj  
       2018-11-08 18:31:40 +08:00
    你让 sqlite 之类的嵌入式数据库帮你代理读写就可以了,你想原子就原子,你想质子就质子
    firebroo
        14
    firebroo  
    OP
       2018-11-08 18:59:53 +08:00
    @leavan 这里的数据具有,不能增量写,只能全量重新写入。
    firebroo
        15
    firebroo  
    OP
       2018-11-08 19:00:26 +08:00
    @firebroo 具有完整性
    firebroo
        16
    firebroo  
    OP
       2018-11-08 19:07:40 +08:00
    @feverzsj 数据库场景不合适,十几个 G 数据全量变化更新,不然我就不把数据放内存里面了,再次也用 redis 这种内存数据库,测试 redis 没满足性能需要
    lihongjie0209
        17
    lihongjie0209  
       2018-11-08 19:09:53 +08:00
    重新发明一个数据库
    feverzsj
        18
    feverzsj  
       2018-11-08 19:14:07 +08:00
    @firebroo 了解,你原来根本搞不清楚问题在哪,写入完整性本来就是依赖副本交换实现的,十几 G 根本不算大,sqlite 完全能胜任
    429839446
        19
    429839446  
       2018-11-08 19:15:34 +08:00 via Android
    可以看微信开源的 mmkv ?
    firebroo
        20
    firebroo  
    OP
       2018-11-08 19:36:27 +08:00
    @feverzsj 你没测试就说可以胜任我的场景,我说的是数据有完整性,不能单一的一条条更新,必须整体更新,不是写入完整性。
    feverzsj
        21
    feverzsj  
       2018-11-08 19:37:45 +08:00
    @firebroo 知不知道什么叫事务性啊?我根本不需要了解你那些小儿科的场景应用
    firebroo
        22
    firebroo  
    OP
       2018-11-08 19:43:12 +08:00
    @429839446 可以可以,就是类似这种,mmkv 这样的实现就完全丢失不了数据了。
    xylophone21
        23
    xylophone21  
       2018-11-08 19:47:34 +08:00
    @muntoya 说到问题的关键了,但感觉楼主还在一些不重要的细枝末节上出不来,或者没找到重点,不解释。
    firebroo
        24
    firebroo  
    OP
       2018-11-08 19:47:46 +08:00
    @feverzsj 我讨论 redis 这种内存数据库是如何实现安全持久化内存数据到磁盘的,然后自己实现个类似的,你让我把这种事情交给 redis 去搞,你是扛精。。?
    feverzsj
        25
    feverzsj  
       2018-11-08 19:53:15 +08:00
    @firebroo 我根本没提过 redis,你说说你想实现的东西,有什么不能用 sqlite 实现的? sqlite 能在保证事务性的基础上达到良好的 io 效率,你自己用标注库 io 能做到?
    firebroo
        26
    firebroo  
    OP
       2018-11-08 19:59:32 +08:00 via Android
    @feverzsj redis 和 sqlite 有差吗,就是把这件事交给数据库去完成,某些场景确实需要,不然微信为啥有 mmkv 这种轮子不用 sqlite
    feverzsj
        27
    feverzsj  
       2018-11-08 20:03:31 +08:00
    @firebroo mmkv 是非常低层次的 kvstore,根本不管你数据死活,sqlite 是完整的关系型数据库,根本不是一个层面的
    liuxu
        28
    liuxu  
       2018-11-08 20:10:16 +08:00
    能不能写到 ramdisk 呢,然后一个线程定期刷到硬盘呢,记录好日志,这样进程即使死了 ramdisk 数据依然不会丢失,重启进程分析日志继续写入到硬盘
    firebroo
        29
    firebroo  
    OP
       2018-11-08 20:14:24 +08:00 via Android
    @feverzsj 不抬杠了,太累,下次我用 sqlite
    firebroo
        30
    firebroo  
    OP
       2018-11-08 20:18:59 +08:00 via Android
    @xylophone21 科普一下,我确实没有看懂 3 楼回复,redis 的实现没看过
    firebroo
        31
    firebroo  
    OP
       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
        32
    msg7086  
       2018-11-09 09:11:26 +08:00
    没看懂问题是啥。
    写临时文件然后 rename 算是基本操作,rsync 之类的软件也是这么跑的。
    值得注意的是你这里写完临时文件以后需要 flush 一下底层的文件系统,确保文件内容已经刷入磁盘了,再 rename 比较好。
    Monad
        33
    Monad  
       2018-11-09 10:03:33 +08:00
    @msg7086 #32 只考虑 kill -9 进程的话 操作系统应该可以保证落盘的吧
    firebroo
        34
    firebroo  
    OP
       2018-11-09 10:22:17 +08:00 via Android
    @msg7086 我 rename 之前已经 close 了 tmp 文件,会自动 flush 吧
    msg7086
        35
    msg7086  
       2018-11-09 10:29:22 +08:00
    @Monad @firebroo kill -9 不影响。主要是考虑断电的问题。
    firebroo
        36
    firebroo  
    OP
       2018-11-09 10:38:42 +08:00 via Android
    @msg7086 真实环境,会可能断电。。
    msg7086
        37
    msg7086  
       2018-11-09 10:44:25 +08:00
    @firebroo 会断电的话还是查一下文件系统的文档吧,看看什么时候强刷。保险起见可以手动强刷再 RENAME。
    firebroo
        38
    firebroo  
    OP
       2018-11-09 10:46:53 +08:00
    @msg7086 fflush:是把 C 库中的缓冲调用 write 函数写到磁盘[其实是写到内核的缓冲区]。fsync:是把内核缓冲刷到磁盘上。 这个吧?学习了。
    no1xsyzy
        39
    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
        40
    no1xsyzy  
       2018-11-09 11:21:11 +08:00
    @firebroo #14 全量写入也可以追加文件末尾啊,虽然又碰上数据量过大的问题了。有没有想过上磁带?
    或者保证只有 2 份文件存在。写 A 不碰 B,写 B 不碰 A。
    firebroo
        41
    firebroo  
    OP
       2018-11-09 13:52:00 +08:00
    @no1xsyzy 啊哈,上磁带是啥意思?
    no1xsyzy
        42
    no1xsyzy  
       2018-11-09 16:04:31 +08:00
    @firebroo 磁带+RAID 0,单位容量价格极低,顺序读写速度堪比 SSD,缺点是基本只能顺序,随机查找 IOPS 极低,但你这个情景用磁带做持久,之后用磁带还原状态,也就顺序……要是 VPS 上跑就当我没说
    firebroo
        43
    firebroo  
    OP
       2018-11-09 16:08:54 +08:00
    @no1xsyzy 擦。。感觉太复杂,不是 vps,线上环境。
    pythonCoder
        44
    pythonCoder  
       2018-11-12 13:38:00 +08:00
    爬山心得
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5666 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 06:29 PVG 14:29 LAX 23:29 JFK 02:29
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86