前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >慎用BitMap, 小心玩爆你的内存

慎用BitMap, 小心玩爆你的内存

作者头像
时间静止不是简史
发布2024-05-26 15:28:37
770
发布2024-05-26 15:28:37
举报
文章被收录于专栏:Java探索之路Java探索之路

写这篇文章是有感而发, 当时一股脑想了4个标题(头条党狂喜), 但是后来觉得都不错, 索性也不删除了. 放在结尾看看大家觉得觉得哪个标题合适吧!

标题1: 60G的内存占用, 容器敢分配, 服务敢占用. 一个字:绝 标题2: 内存挤爆了. 竟然是因为… 标题3: 内存问题虐我们千百遍 标题4: 慎用BitMap, 小心玩爆你的内存.

1. 出乎意料的开始的开始

最初,没有人在意这场灾难,这不过是一场山火,一次旱灾,一个物种的灭绝,一座城市的消失。 直到这场灾难和每个人息息相关 ——《流浪地球》

这是郭导改编大刘笔下同名科幻小说《流浪地球》里面的一个经典台词, 现在正呼应了我当下所遇到的事情. 在问题的最初, 只是测试环境Redis总是服务超时, 再到开发环境发包以及ssh连接超时, 最后到生产环境达到恐怖的59.99G. 只用了9个月…

下面, 请让我详细叙说这件事

2. 测试环境离奇异常

大概在半年前, 也就是今年上半年的时候, 测试开始偶尔向我反映. Redis经常无缘无故连接失败, 导致我们开发的一个 基于缓存的服务A(后续会出现) 总是启动失败. 因为其他环境正常而且问题原来不好定位, 因此我就没有仔细去分析. 下面是本人凭借着经验进行的初步分析:

  • 首先觉得可能是连接数过多了, 因为此项目非常依赖缓存 于是开始调整该项目Redis参数, 将超时参数以及最大连接数调小, 但发现用处不大
  • 然后考虑到测试环境Redis偶尔失效但其他环境正常, 可能就是测试服务器性能原因 于是打算采用双缓存机制, 即使远程的缓存失效了, 我本地的缓存还能让系统继续运行.
  • 虽然能正常使用了, 但是测试环境Redis依然经常挂掉, 过了段时间干脆无法访问了. 于是, 我将测试环境使用的Redis更换成性能更好的服务器上面的Redis. 问题得以暂时解决(但没有解决测试环境Redis为什么挂掉的问题)

3. 开发环境的炸裂反应

大概在一个月前, 同事开始反映开发环境服务器启动服务的速度变慢了, 而且随着时间的增长, 由原来的1min, 到5min, 到现在的几十分钟. 而且还间接导致开发环境服务器ssh连接很慢, 输入命令执行很慢, 用户登录频繁超时等情况. 由于当时我还忙于其他项目, 因此对此没有太放到心上. 在加上开发环境不仅只有我们部署的这个服务 而是有着大概十几个服务, 很有可能是别的服务出了问题(绝不可能是我们的服务出问题, 哈哈) 心想反正发包了能用就先用着吧

罪恶之源——内存

直到我闲下来腾出手, 梳理和同事的聊天记录并定位问题, 才发现事情没没有那么简单.

ssh连接和输入cmd命令变得很慢
ssh连接和输入cmd命令变得很慢
部署在这个服务器的接口响应开始变得很慢(原来很快)
部署在这个服务器的接口响应开始变得很慢(原来很快)

当时用top 命令查了一下, 原本16GB的内存, 现在只剩四五百兆了, 更恐怖是的交换空间只剩不足2MB了! 这可不妙, 挂不得服务器会那么卡. 通过下面显示的CPU负载和使用情况, 可以确认cpu不是导致这次问题的主要原因. 因此问题最终被定位为内存占用过高(90+), 主要原因:redis-server, 即Redis服务

间接凶手——Redis

在我们搭建的各服务运行环境中, Redis服务是以Docker容器的形式提供的, 这也为后台服务器Redis占用能达到60G埋下隐患! 后续我们将详细说明, 再次不做过多解释 我们都知道, Redis有16个库, 每个库都可以存数据. 通过Redis Desktop Manager可以清除的看到, 这台开发环境服务器Redis的16个库上大概有不到1w条数据.

并且通过server Info 页面可以看到占用的内存在14.5G左右 难道1w条数据就占用了14.5G的内存吗? 抱着怀疑的态度我开始寻找bigKey

于是, 我便使用了分析redis自带的命令 docker exec -it redis redis-cli --bigkeys -h redis的ip -p 6379 -a redis密码(如果有) 当时由于过大直接在分析时, 下面6种数据类型全部是0. 下图占用220MB的是事后我用于测试的一个key(之前的图忘截图了)

直接凶手——服务A

发现通过命令执行查询不到bigkey, 于是我这边又从 Redis Desktop Manager 寻找问题 我当时又联想到测试环境, 对比下和开发环境的区别, 我当时就有种直觉, 很可能就是我们的那个基于缓存的服务A导致的

于是开始着手将开发环境服务A的缓存删除, 在删除之后, 内存占用果然由14.5G变成了490M左右. (大家切勿学习! 因为服务A缓存相关功能我开发的, 在删除之后及时触发数据重刷新机制, 因此不会影响开发环境的正常使用). 进而证实了我的猜想, 凶手是我们的那个基于缓存的服务A(之前还笃定不是我们的问题, 尴尬), 因此我开始对我们服务的缓存进行简单的内存分析.

凶手武器——BitMap

为了验证是哪个key, 我又重新打开了我们服务所测试环境所使用的缓存库, 然后梳理了每种类型的key占用内存的大小. 发现基本类型的数据最大占用也就100KB左右. 直到我看到了BitMap类型的数据 . 发现仅有1天的数据就可以占用100-250MB. 这样算起来, 14G也就56-140天的记录而已.

然后, 根据这个BitMap, 我定位到了具体的方法. 如下代码片

代码语言:javascript
复制
    /**
     * 方法:1 将指定param的值设置为1,{@param param}会经过hash计算进行存储。
     *
     * @param key   bitmap结构的key
     * @param param 要设置偏移的key,该key会经过hash运算。
     * @param value true:即该位设置为1,否则设置为0
     * @return 返回设置该value之前的值。
     */
    public static Boolean setBit(String key, String param, boolean value) {
        return stringRedisTemplate.opsForValue().setBit(key, hash(param), value);
    }

    /**
     * guava依赖获取hash值。
     */
    private static long hash(String key) {
        Charset charset = Charset.forName("UTF-8");
        int abs = Math.abs(Hashing.murmur3_128().hashObject(key, Funnels.stringFunnel(charset)).asInt());
        return abs;
    }

    /**
     * 方法2 将指定offset偏移量的值设置为1;
     *
     * @param key    bitmap结构的key
     * @param offset 指定的偏移量。
     * @param value  true:即该位设置为1,否则设置为0
     * @return 返回设置该value之前的值。
     */
    public static Boolean setBit(String key, Long offset, boolean value) {
        return stringRedisTemplate.opsForValue().setBit(key, offset, value);
    }

然后断点调试发现了其通过 hash方法, 将传入的字符串转成long类型的hash码, 并作为bitmap的偏移量传入. 可以看到传入一个字符串的用户id被映射成 1762177145 (已脱敏).

然后为了确定是hash导致的BitMap变大的问题, 我开始进行验证 调用工具类中第三个方法, 通过手动将hash之后的值作为偏移量传入该方法, RedisTemplateUtil.setBit("testBitMapDay:" + DateTimeUtil.getCurrentDate() , 1762177145L, true); 执行后发现得到的BitMap达到210.07MB(如下图). 与偏移量 1762177145 有什么关系呢?

通过存储单位换算可以验证得到, 所谓的偏移量就是指在Redis占用内存的字节大小!

而如果用户id较小的情况下, 所占用的内存则会较小

例如, 在执行 RedisTemplateUtil.setBit("testBitMapDay:" + DateTimeUtil.getCurrentDate() , 1000L, true); 代码之后, 该BitMap类型数据所占用内存仅站 126(125+1符号位)

然后我开始复习了一下BitMap的相关知识 在BitMap在设置偏移量时, redis就会在内存空间开辟出相同偏移量大小的内存空间, 用于存放BitMap位图. 而偏移量数值的大小, 取决于字符串被hash过的值,

因为我们的服务中, 使用的是钉钉的userId, 而钉钉的userId又是采用字符串+数字的形式, 因此只能使用上面代码片中的方法1(即传入userId, 将其进行hash然后作为偏移量放入到BitMap). 进而导致被hash出来的值很大, 因此偏移量就很大, 进而在Redis中占用的缓存就变大.

时间的增加, 每天大概会多用200mb左右内存, 因为在不同环境服务器硬件性能不同, 达到内存最大的时间不同. 所以开发和测试环境出现问题的时间和效果也不同, 但本质都是由于内存占用过大而导致的

4. 生产环境的恐怖情况

然后, 我打开了生产环境的redis, 发现了我最感慨的一件事情.

一个Redis服务, 竟然用了我60G的内存. 更让我震惊的是, 生产环境的服务器竟然至少有64G的内存, 真的是小刀拉屁股, 让我开了眼了! 想到这里, 我变开始构思如何去解决这个问题.

5. 解决措施

下面有几个我想到的思路

  1. 放弃使用BitMap, 转而使用数据库来进行日活统计(最简单, 但需要入库)
  2. 使用BitMap统计当日的数据, 然后结合定时任务在每天00:00将上一天的数据入库(较为简单, 且应用了BitMap对用户活跃数统计)
  3. 将所有用户i钉钉中的所有用户入库, 与数据库中int类型主键建立关系. 通过主键id来作为偏移量进行日活统计(稍微麻烦, 需要将钉钉上所有用户id同步到数据库, 但依然会占用内存)

综合考虑用户活跃数统计的实时性要求不高, 以及开发服务器内存较少的情况.我决定采取第二种方式. 具体步骤如下:

首先, 通过建表来存储每日日活信息. 月活取的是当月日活的最大值 然后, 通过定时任务获取上一天日活信息 最后, 通过Java脚本将以往日期的所有日活数从Redis中自动存储倒数据库中即可

6. 反思

使用Redis作为缓存服务前, 一定要检查 Redis 最大内存设置情况

考虑到生产环境60G内存的占用情况, 结合开发和测试环境的问题, 让我意识到了设置Redis允许最大内存的重要性. 在使用Redis镜像文件中也要规定镜像文件的大小, 如果没有在镜像制作前配置, 也可以通过下面命令补救

代码语言:javascript
复制
# 获取maxmemory配置参数的大小
127.0.0.1:6379> config get maxmemory
# 设置maxmemory参数为2048mb
127.0.0.1:6379> config set maxmemory 2048mb

如果超过设值的最大内存, Redis 则会抛出下面错误

慎用BitMap

慎用BitMap, 特别是在统计日活越活的业务下. BitMap虽被推荐作为统计用户在线情况统计, 但是一定要明白. 使用BitMap存储用户日活信息是需要内存的. 即使使用很小的数值作为偏移量, 将时间拉长了之后也会在占用系统较大的内存 因为这种统计方式本质意图不是在利用缓存的读取快的特点, 而是将缓存作为数据持久化的工具. 可以考虑我解决方案2的做法, 采取数据库持久化+BitMap同时使用的方式来进行用户在线数统计的工作.


最后补一张文心一言关于bitmap在日活统计场景下是否合适的回答

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-03-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 出乎意料的开始的开始
  • 2. 测试环境离奇异常
  • 3. 开发环境的炸裂反应
    • 罪恶之源——内存
      • 间接凶手——Redis
        • 直接凶手——服务A
          • 凶手武器——BitMap
          • 4. 生产环境的恐怖情况
          • 5. 解决措施
          • 6. 反思
            • 使用Redis作为缓存服务前, 一定要检查 Redis 最大内存设置情况
              • 慎用BitMap
              相关产品与服务
              云数据库 Redis
              腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档