前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >规划Redis真的需要预留一半内存?

规划Redis真的需要预留一半内存?

作者头像
richard.xia_志培
发布2022-06-14 14:31:35
1.4K0
发布2022-06-14 14:31:35
举报
文章被收录于专栏:PT运维技术PT运维技术

前段时间,由于太多的因素造成redis故障, 负面影响较大。复盘后决定将内存超出内存一半就需要告警,便于运维人员及时介入处理。 网上这种redis规划内存预留一半的文章汗牛充栋(https://cloud.tencent.com/developer/article/1095192)。真实的情况下,真的需要预留下一半的内存吗? 搞清楚这个问题,需要弄清楚2个事情: 1. Redis bgsave/AOF重写的运行机制。 2. Linux下的进程内存分布以及redis内存管理机制。 先说问题1: 1.redis跟内存相关的运行机制莫过于rdb持久化/AOF重写/内存剔除策略(高版本redis还存在着内存碎片整理的配置选项), 其中AOF重写和rdb持久化都属于fork子进程来完成的。本次就以rdb持久化为例,rdb的持久化可以由持久化的配置策略或者命令行bgsave或者主从全同步触发。redis在做bgsave的时候,fork出子进程来做bgsave。具体的过程如下: rdbSaveBackground()中fork子进程 ---> rdbSave() ---> rdbSaveRio()。fork后子进程拥有和父进程一模一样的进程空间,虽然采用了COW机制(父子进程的虚拟内存指向相同的物理page),但是ps或者top命令中的RSS显示的值都会算成自己进程所占的物理内存,这个可能是很多运维同学/DBA同学经常可以眼见的现象,恐怕这个就是潜意识里需要内存预留一半的重要因素。

再说问题2: 2. Linux下的进程下的地址都是虚拟地址,CPU使用的也是虚拟地址,Linux将每个进程的地址空间人为地分为用户地址空间和内核地址空间,32位下 0-3G为用户地址空间,3-4G为内核地址空间(每个进程都是这样), 64位下,0-128T 为用户地址空间,高位-128T为内核地址空间。进程中的虚拟地址和内存物理地址存在映射关系,这个映射关系由进程的页表pte来维护。虚拟地址和物理地址是多对1或者1对1的关系。Linux默认情况下fork子进程会采用写时复制(Copy On Write)。为了解决默认glibc内存分配器的性能和碎片率问题,redis引入了jemalloc,并成为默认配置。再细说一下Linux的fork COW机制。 在内核层面看,fork 创建一个进程的动作: do_fork()->copy_process()->copy_mm()->dup_mm()-->dump_nmap()-->copy_page_range()->copyp4d_range()->copy_pmd_range()--> copy_pte_range()-->copy_one_pte()-->ptep_set_wrprotect() 大体上的功能就是: 复制当前进程的结构,复制当前进程路径,文件句柄,信号,namespace, 虚拟内存(当然包含页目录和页表),内核栈, CPU体系结构相关信息, 在复制页表的过程中,内核会将物理page权限为设置为只读,一旦父进程 修改物理page的时候,会触发page fault, 内核在异常处理过程中通过pgd_alloc重新分配物理page,将先前物理page中的数据复制到新分配的物理page,同时修改父进程中页表和物理page的映射关系。(参见ULK 2.4和3.3) 从理论上看,redis 在fork bgsave的时候,是不会让内存翻倍的, 那么是不是只要父进程的内存足够,就可以安全地进行bgsave呢?

这个里面有2个因素: 1. 在bgsave期间,业务产生的'update'类数据量(新增/修改)。 2. redis运行过程中rehash产生的内存消耗。 先看因素1, 由于redis默认使用jemalloc, 所以redis由于key过期自动剔除或者业务方发起del xxkey这类的动作所产生的的内存释放,全部会由jemalloc接管,并不会返回给OS, 所以这种类型的动作不会让父进程产生page fault。剩下就是业务方产生的set/add等请求,redis为这些请求分配内存,如果请求set/add所需内存较大,同时jemalloc现有的area/chunk没有的话,就向OS重新申请的,否则从jemalloc空闲内存中分配,不论是那种情况,均会触发父进程page fault 从而分配新的物理page。但现实的情况,基本上没有见过:在bgsave过程中业务请求产生了和redis实例内存大小同等的增量。 再看因素2, redis的rehash装载因子默认值为5,超过就会进行rehash。redis大部分作为缓存场景使用,存在各种数据类型,dict只是其中一种,当然不排除业务发起很小的请求,让redis的dict(内存占用较大)发生rehash,从而造成父进程的内存开销陡增。即使在dict rehash的场景下,也不太可能在bgsave期间,redis产生了和redis实例内存同等大小的增量。何况redis在设计的过程中就考虑到了这点,aof/bgsave子进程运行期间是禁止rehash的。(后面有截图) 以上理论分析完成,证明网上结论只可仅仅参考(除非自己亲自验证),以下实操开干:

(以下的环境统一为:centos7.5 x86-64 redis-3.2.11)

1. 在一台2C4G的虚拟机上部署redis, 开启一个redis实例, 填充测试数据,此时redis的物理内存占用了已超过一半了。

从top命令中查看redis所占内存

2. bgsave时候,bgsave进程和redis父进程的内存开销

3. 导出bgsave进程的物理页帧和父进程的物理页帧, 然后对比两者相同部分.

在这里介绍一个读取物理页帧的工具:(https://github.com/dwks/pagemap,底层原理是分析每个进程/proc/xx/pagemap的内容)。

4. redis的rehash在bgsave子进程运行期间是被禁止的。

代码语言:javascript
复制
   if ((childpid = fork()) == 0) {
        int retval;

        /* Child */
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        retval = rdbSave(filename);
        if (retval == C_OK) {
            size_t private_dirty = zmalloc_get_private_dirty();

            if (private_dirty) {
                serverLog(LL_NOTICE,
                    "RDB: %zu MB of memory used by copy-on-write",
                    private_dirty/(1024*1024));
            }
        }
        exitFromChild((retval == C_OK) ? 0 : 1);
    } else {
        /* Parent *  父进程/
        server.stat_fork_time = ustime()-start;
        server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */
        latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);
        if (childpid == -1) {
            server.lastbgsave_status = C_ERR;
            serverLog(LL_WARNING,"Can't save in background: fork: %s",
                strerror(errno));
            return C_ERR;
        }
        serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
        server.rdb_save_time_start = time(NULL);
        server.rdb_child_pid = childpid;
        server.rdb_child_type = RDB_CHILD_TYPE_DISK;
        updateDictResizePolicy(); //重点部分,reahsh的处理
        return C_OK;
    }
代码语言:javascript
复制
###在子进程运行过程中禁止rehash, 子进程结束后才会开启rehash
void updateDictResizePolicy(void) {
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
        dictEnableResize();
    else
        dictDisableResize();
}

分析+实践 可以不用人云亦云。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-11-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PT运维技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档