写这篇文章是有感而发, 当时一股脑想了4个标题(头条党狂喜), 但是后来觉得都不错, 索性也不删除了. 放在结尾看看大家觉得觉得哪个标题合适吧!
标题1: 60G的内存占用, 容器敢分配, 服务敢占用. 一个字:绝 标题2: 内存挤爆了. 竟然是因为… 标题3: 内存问题虐我们千百遍 标题4: 慎用BitMap, 小心玩爆你的内存.
最初,没有人在意这场灾难,这不过是一场山火,一次旱灾,一个物种的灭绝,一座城市的消失。 直到这场灾难和每个人息息相关 ——《流浪地球》
这是郭导改编大刘笔下同名科幻小说《流浪地球》里面的一个经典台词, 现在正呼应了我当下所遇到的事情. 在问题的最初, 只是测试环境Redis总是服务超时, 再到开发环境发包以及ssh连接超时, 最后到生产环境达到恐怖的59.99G. 只用了9个月…
下面, 请让我详细叙说这件事
大概在半年前, 也就是今年上半年的时候, 测试开始偶尔向我反映. Redis经常无缘无故连接失败, 导致我们开发的一个 基于缓存的服务A(后续会出现) 总是启动失败. 因为其他环境正常而且问题原来不好定位, 因此我就没有仔细去分析. 下面是本人凭借着经验进行的初步分析:
大概在一个月前, 同事开始反映开发环境服务器启动服务的速度变慢了, 而且随着时间的增长, 由原来的1min, 到5min, 到现在的几十分钟. 而且还间接导致开发环境服务器ssh连接很慢, 输入命令执行很慢, 用户登录频繁超时等情况. 由于当时我还忙于其他项目, 因此对此没有太放到心上. 在加上开发环境不仅只有我们部署的这个服务 而是有着大概十几个服务, 很有可能是别的服务出了问题(绝不可能是我们的服务出问题, 哈哈) 心想反正发包了能用就先用着吧
直到我闲下来腾出手, 梳理和同事的聊天记录并定位问题, 才发现事情没没有那么简单.
当时用top 命令查了一下, 原本16GB的内存, 现在只剩四五百兆了, 更恐怖是的交换空间只剩不足2MB了! 这可不妙, 挂不得服务器会那么卡. 通过下面显示的CPU负载和使用情况, 可以确认cpu不是导致这次问题的主要原因. 因此问题最终被定位为内存占用过高(90+), 主要原因:redis-server, 即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(之前的图忘截图了)
发现通过命令执行查询不到bigkey, 于是我这边又从 Redis Desktop Manager 寻找问题 我当时又联想到测试环境, 对比下和开发环境的区别, 我当时就有种直觉, 很可能就是我们的那个基于缓存的服务A导致的
于是开始着手将开发环境服务A的缓存删除, 在删除之后, 内存占用果然由14.5G变成了490M左右. (大家切勿学习! 因为服务A缓存相关功能我开发的, 在删除之后及时触发数据重刷新机制, 因此不会影响开发环境的正常使用). 进而证实了我的猜想, 凶手是我们的那个基于缓存的服务A(之前还笃定不是我们的问题, 尴尬), 因此我开始对我们服务的缓存进行简单的内存分析.
为了验证是哪个key, 我又重新打开了我们服务所测试环境所使用的缓存库, 然后梳理了每种类型的key占用内存的大小. 发现基本类型的数据最大占用也就100KB左右. 直到我看到了BitMap类型的数据 . 发现仅有1天的数据就可以占用100-250MB. 这样算起来, 14G也就56-140天的记录而已.
然后, 根据这个BitMap, 我定位到了具体的方法. 如下代码片
/**
* 方法: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左右内存, 因为在不同环境服务器硬件性能不同, 达到内存最大的时间不同. 所以开发和测试环境出现问题的时间和效果也不同, 但本质都是由于内存占用过大而导致的
然后, 我打开了生产环境的redis, 发现了我最感慨的一件事情.
一个Redis服务, 竟然用了我60G的内存. 更让我震惊的是, 生产环境的服务器竟然至少有64G的内存, 真的是小刀拉屁股, 让我开了眼了! 想到这里, 我变开始构思如何去解决这个问题.
下面有几个我想到的思路
综合考虑用户活跃数统计的实时性要求不高, 以及开发服务器内存较少的情况.我决定采取第二种方式. 具体步骤如下:
首先, 通过建表来存储每日日活信息. 月活取的是当月日活的最大值 然后, 通过定时任务获取上一天日活信息 最后, 通过Java脚本将以往日期的所有日活数从Redis中自动存储倒数据库中即可
考虑到生产环境60G内存的占用情况, 结合开发和测试环境的问题, 让我意识到了设置Redis允许最大内存的重要性. 在使用Redis镜像文件中也要规定镜像文件的大小, 如果没有在镜像制作前配置, 也可以通过下面命令补救
# 获取maxmemory配置参数的大小
127.0.0.1:6379> config get maxmemory
# 设置maxmemory参数为2048mb
127.0.0.1:6379> config set maxmemory 2048mb
如果超过设值的最大内存, Redis 则会抛出下面错误
慎用BitMap, 特别是在统计日活越活的业务下. BitMap虽被推荐作为统计用户在线情况统计, 但是一定要明白. 使用BitMap存储用户日活信息是需要内存的. 即使使用很小的数值作为偏移量, 将时间拉长了之后也会在占用系统较大的内存 因为这种统计方式本质意图不是在利用缓存的读取快的特点, 而是将缓存作为数据持久化的工具. 可以考虑我解决方案2的做法, 采取数据库持久化+BitMap同时使用的方式来进行用户在线数统计的工作.
最后补一张文心一言关于bitmap在日活统计场景下是否合适的回答