一、问题背景

在一个遍历服务中,同时运行 5 个设备执行遍历任务时,快速出现如下错误

容器ExitCode 137, 原因OOMKilled

该服务是通过 python 程序唤起执行 Java 任务

二、资源占用

既然是 OOM,得确认资源占用情况

在这里插入图片描述

发现系统中总内存使用较低,且 python 和 Java 进程的内存使用不高,那为什么会出现 OOM 的情况呢?
注:其实这里查看的是整个服务器的内存占用,该服务器内容总共 25G,所以内存占用不高

在这里插入图片描述

容器的最大内存 500M
但是从这些信息中并未得到较高的内存占用

三、python 内存分析

使用 memory_profiler 分析 python 的内存使用

from memory_profiler import profile

@profile
def compatibility_task(task_id):
    # do compatibility job

python task.py

获取具体的执行信息
在这里插入图片描述

内存占用 200M 以内,应该问题不大 (这里其实有个大坑,导致后面折腾一通)

四、Java 内存分析

既然不是 python 的性能问题,那么接下来分析一下 java 的内存
使用命令获取 hprof 文件

jmap -dump:format=b,file=/app/temp/test11.hprof pid

很开心的使用MAT 工具分析dump 文件在这里插入图片描述
发现内存占用正常没有问题

配置 OOM 时自动触发 headdump

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/temp/heapdump.hprof

在 OOMKilled 时并未触发生成 dump 文件,那是否是因为并不是 Java 程序的 OOM 呢?

五、进程内存使用分析

好吧。还是需要详细分析究竟是哪个进程的内存占用的问题

同时执行 5 个设备遍历,在运行时获取各个进程的内存使用

ps aux | awk '{print $6/1024 " MB\t\t" $11}' | sort -n

得到信息

0 MB            COMMAND
0.00390625 MB           /pause
0.667969 MB             sleep
0.972656 MB             awk
2.29688 MB              sort
3.34766 MB              /bin/bash
3.41406 MB              /bin/bash
3.66016 MB              ps
4.05469 MB              top
9.15625 MB              java
53.5156 MB              java
487.68 MB               python3.6

竟然是 python 的程序内存撑满导致容器直接 OOM

六、python 源码分析

回到 python 内存分析这一步,为什么在内存分析中并未找到问题呢?
问题在于获取内存数据是在本地调试执行,且执行的是 1 个设备执行遍历任务

还是不能偷懒,先撸一遍遍历时 python 的执行逻辑吧,简化的相关逻辑如下:

  1. 从 redis 拉取任务
  2. 监测需要执行的任务数
  3. 最高拉起 20 个线程同时执行

比如任务列表中需要执行的任务数 10,那么接下来 10 个线程每个线程都处理的任务如下:

  1. 解析任务信息
  2. 执行各自下载 apk 操作
  3. 初始化环境操作
  4. 下发安装操作
  5. cmd 执行遍历服务 jar 包
  6. 处理并获取报告
  7. 环境清理处理

从 python 内存分析中也可以看到

Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
   137  58.6406 MiB  58.6406 MiB           1       @profile(precision=4, stream=open('memory_profiler_traverse_test.log', 'w+'))

   ......

   184  59.1328 MiB   0.0000 MiB           1               if not app_config_info:
   185                                                         Logger.logger.info('不存在该应用配置,已跳过,请在配置页面添加该应用配置')
   186                                                         return
   192                                         
   193 193.1602 MiB 134.0273 MiB           1               install_result, install_time = self.app_install_exec(package_name, apk_url)
   194 193.1602 MiB   0.0000 MiB           1               print("install_time: ", install_time)
   195                                                     # 测试应用启动时间
   196 193.1602 MiB   0.0000 MiB           1               start_result, app_start_time = self.get_app_start_time(package_name, app_config_info.get("main_activity"))
   197 193.1602 MiB   0.0000 MiB           1               self.yaml_file_name = compatibility_yaml_name.format(self.device_id + "_" + str(app_config_info.get("id")))
   198 193.1602 MiB   0.0000 MiB           1               Logger.logger.info("yaml file: {}".format(self.yaml_file_name))
   199 193.1641 MiB   0.0039 MiB           1               app_performance = AppPerformance(self.subtask_id, self.device_id, package_name)

   ......

在安装操作self.app_install_exec(package_name, apk_url)时内存飙升,那么着重看一下安装的代码逻辑:

def app_install(self, download_url):
    get_response = requests.get(download_url, timeout=60)
    if get_response:
        app_content = get_response.content
        res = requests.post(
            "http://{}:{}/mobile/{}/installApp".format(self.agent_ip, self.agent_port, self.device_id),
            files={
                "app": app_content
            }
        )

OMG!安装的过程是直接 requests.get 将数据包下载下来,然后再将包内容 content 直接下发给手机设备 mobile 执行安装操作
这种处理方式太诡异了,有几个问题:

  1. 多线程执行时包其实基本是同样的,但是都执行了同样的下载操作,为什么不做合并处理?
  2. 下载时直接将 apk 包 load 在内存对象中然后再转发给 mobile,如果包较大且多个同时下载时内存是否可以支持?
  3. 有链接地址,为什么不传链接地址让 mobile 直接下载安装呢?

七、问题修复

其实现在问题已经很明显了,后面验证了 5 台、10 台、20 台设备时再次验证了从源码中的分析的原因

问题修复方式只要将 apk 包的链接地址传给 mobile,mobile 自行下载然后执行安装逻辑即可


↙↙↙阅读原文可查看相关链接,并与作者交流