一个单表7000w数据单机离线统计分析的内存优化案例
问题背景
- 产品部署形态:单机JVM,数据库 MySQL,离线部署至客户服务器或者 PC 机
- 需求描述:行为日志表按月分表,针对行为日志,统计某用户某天在滑动窗口内触发某种行为模式的次数、以及在哪一分钟触发过
测试数据
根据项目信息,确定用户数大概在几万这个量级,参照系统的运行情况,推测几万用户每天能产生的日志数量大约为几百万这个量级,故模拟了单月表数据为 7000w 的行为日志数据
行为日志字段数为 10+,每个字段的长度约为 64 bit,平均下来一条日志的大小约为 1 ~ 2k 左右。
根据以上信息,我模拟了以 30000 个用户为基础,三年,每个月为 7000w 数据的 36 张表,每张表的大小约为 10G
测试环境
开发机: 16G 内存,CPU I7 8700
实现
最开始的算法实现的细节如下:
- 从某一月的表中读取一批数据
- 交由滑动窗口算法进行处理,每个用户拥有属于自己的一些窗口,滑动窗口内部会缓存一些状态
- 算法结束时,代表这一个月的数据都已读取完毕,最后遍历每个窗口状态是否满足条件,满足则形成一条触发记录触发明细
问题
由于产品部署形态的限制,必须优化内存的使用效率,让内存需求尽可能地降下来的同时处理速度不能太慢
1. 日志表的无序导致一次必须处理一个月的数据
由于业务的特殊性,日志表中每一条日志并无法保证 id 跟插入时间一样是顺序递增的
第一版优化过的算法是通过 where 日期条件的 sql 指定读取某一天的数据,算法单次只处理某一天的数据,经过测试,这种对日期条件的范围查询比通过 id 顺序读取一遍的方式,慢了不少
2. 滑动窗口状态过多
按最大 30000 用户,一个月 30天,若使用 5 分钟的滑动窗口,这 30000 用户在一个月内的窗口状态数量就最大达 30000 * (1440 - 5) * 30 ~= 九亿个
实际情况肯定不会有太多用户 24 小时产生数据,但按照极端情况来计算,就算一个窗口状态内只是一个整数的数据,这个内存占用量也是很可怕的
3. 触发记录过多
算法记录的触发记录的内容大概是:(用户,某一天,某一分钟)
一个用户在一天内可能会有多条触发记录,这不仅造成了数据冗余,同时由于业务特殊,随着新日志上报上来,进行增量跑批,原先的触发记录就要先擦除再重新插入,这带来了更多的 IO,数据处理速度也就上不去
优化
针对1 2二个问题,经过探索,思考过唯一的解决办法就是提升读取单天数据的速度,简单粗暴地加索引并不能解决这个问题,仔细思考这个问题,本质上是由于随机 IO 导致的处理速度上不去
于是转变思路,可不可以让日志表的数据能按照日期顺序进行存储,从而能进行顺序读?
最终想到一个办法:针对行为日志表,在落库时,进行双写,除了原始的一份数据外,还有一份是以日期为key,通过 hessian 序列化,再以 key 为文件名,进行顺序追加,相同日期的数据都会被存放在一起,其实也就是数仓常用的按日 sharding 思路,这样在读取某一天的数据时,就可以通过顺序读的方式达到一个比较不错的速度
另外一个问题是触发记录过多,这个问题的解法本质是需要进行数据压缩,也就是能不能把一个用户一天内的触发记录压缩成一条?
观察记录的内容,唯一会变的就是在哪一分钟触发过,经过思考,最后选择了使用一个 1440 位的 bitmap 来记录一个用户在一天内的触发明细,比如 1000…. 代表在这一天的第一分钟触发过,1100… 代表第一分钟跟第二分钟都触发过,一个用户一天使用一条记录进行存储明细,通过 MySQL 的 blob 字段来存储这个 bitmap
效果
通过使用顺序文件读替代 sql 查询的方式,数据的处理速度从单月需要约一小时降低到了单月 10 分钟左右的速度
堆内存的占用也从最高 12G 降低到了 2G 上下