为了方便客户,增加客户粘性,公司设计了一套某行业专用的网盘系统,主要保存图片素材,设计文件。
某些头部客户一个月就有上万个文件,某行业第一在系统迁移的时候一次性导入了15T的各类素材。
随着数据量增加,大文件夹的移动、复制异常逐步暴露出来,由于是和行业应用深度绑定,我们的网盘不仅保存文件的基本信息(文件名、文件父子关系)还有文件目录权限、文件特性等业务信息。方便客户在网盘中使用自己保存的文件。
某日系统报警,发现生产环境某个节点频繁重启,同事前方客服传来消息,某个重量级客户在操作文件夹复制的时候莫名报错了,赶紧找运维拿了dump文件,经过分析果然是OOM了,在文件移动的逻辑中,所有文件信息都被读进去内存,某个结果Result Lsit 中持续写入需要移动的文件信息,导致jvm 出现了OOM ,同时docker 容器重启。
定位问题之后分析文件移动逻辑,发现复制也是如此
而且复制还更复杂。
网盘的文件移动其实很简单,抛去文件的权限、深度等信息,只要要移动的文件父节点和子节点关系更新掉就好,由于我们的网盘深度绑定了业务,导致需要诸暨递归更新所有的文件信息,包含权限、深度,同时移动的时候做了目录深度、广度、重名等逻辑处理,这就导致内存中90% 的信息其实和文件移动逻辑没关系,由于文件的ID 查找走了ES,所有在遍历 LIST ids 从MySQL 数据库中批量拿文件详情的时候oom。
这个时候我便整理好思路,文件移动逻辑只是处理文件移动相关的信息,文件特征信息不读到内存,这样正在递归处理文件树的时候,只需要把移动后的文件权限、深度等信息更新到MySQL 数据 之后同步到ES 即可。
这个方案评审之后,我便交给一个组员开始实现。其中我还写好了主流程。
文件复制比较复杂的是 我需要把文件特征值也复制一份,这样的话就避免不了内存过大,同事文件数量多的时候 比如10万 就有一个过程,就像平时windows 系统内的文件夹 复制 、剪切,其实剪切的时候我们都不会进行操作,只是硬盘中数据移动,如果是是复制,windows 的策略是不锁定被复制对象,你可以继续操作被复制对象,如果你在编辑一个Word 复制过程会提示 是否跳过或者你关掉Word 编辑器,继续复制。
我们系统产品都没想到会有这个过程,所以给了一个临时方案 限制复制数量,保证一次性复制成功。
由于测试环境数据量不大,所以测试一切顺利,上了灰度,可以操作用户生产数据了,结果炸了,文件移动时候超过2000 马上内存主键升高,直到docker 容器重启。
查看监测数据,发现
1.移动的时候 网络IO 陡升,从50k 到了50mb。
2.CPU 持续高位利用率 疯狂的计算直到容器被重启
3.内存迅速升高直到容器重启
4.一分钟内产生大量日志文件,被分段策略分出很多压缩日志。
由于业务逻辑日志打印了从ES 查询的返回结果,发现日志刷屏了,公司的发布系统性能一般,根部看不了大日志,根据关键字筛选,发现Es 返回了数据,并未数据超过2000个文件。
接着怀疑是Java 内存泄露,但看了代码发现根本没执行到很多逻辑,打印ES 返回的数据之后 日志就没了,范围缩小到了调用ES 返回结果的前后,接着看日志,发现请求ES 的参数有问题,这之前根本没想到这种分页参数会有问题
上代码
try {
int from = 0;
int size = context.defaultSizeIfNull(sourceFile.getId());
SearchCondition condition = esSearchConditionBuilder.searchDescendantsTreeInfo(sourceFile, context);
while (true) {
condition.setFrom(from);
condition.setSize(size);
List<ResourceIndex> results = esSearchService.search(condition);
List<ResourceMoveDto> resourceMoveDtoList = ResourceMoveDtoConverter.convert(results);
log.info("搜索结果[{}],搜索条件[{}]", JSON.toJSONString(results), JSON.toJSONString(condition));
if (CollectionUtils.isEmpty(results)) {
break;
}
//保存子文件下深度信息
List<Integer> filesDeepInfo = getResourceLevel(results);
context.addChildrenDeepInfo(sourceFile.getId(), filesDeepInfo);
//保存子文件下所有文件信息
context.addChildrenFile(sourceFile.getId(), resourceMoveDtoList);
if (context.enough(sourceFile.getId()) || results.size() < size) {
context.addTotal(sourceFile.getId(), results.size());
break;
}
from++;
}
} catch (Exception e) {
log.error("XXXXX", e);
throw new Exception("查询文件列严重异常");
}
这个分页查询ES 的代码竟然是每次起始点+1.
一切清晰,如果文件树超过2000,那么代码就是频繁请求ES ,由于每次是2000条数据,那么ES 会迅速返回,这时候逻辑开始计算返回数据,导致CPU升高,内存主键升高。
最后docker 容器因为内存不足重启,如果是不足2000文件那么不会出现异常,
修改代码之后,重新测试 4000个文件移动需要3秒,内存基本没有波动。