Spark:Container exited with a non-zero exit code 137

Spark:Container exited with a non-zero exit code 137,第1张

        最近公司用户量暴涨,领导笑嘻嘻,搬砖的心里***,服务器压力骤然上升,最近大数据集群在跑Spark任务时老是报“Container exited with a non-zero exit code 137”这样的错误,详细日志如下:

WARN YarnSchedulerBackend$YarnSchedulerEndpoint: Requesting driver to remove executor 4 for reason Container marked as failed: container_e52_1650941597513_0076_01_000007 on host: host0. Exit status: 137. Diagnostics:Container killed on request. Exit code is 137
Container exited with a non-zero exit code 137. 
Killed by external signal

虽然日志级别是warn,而且yarn会重启一个新的container来重新执行先前失败的task,但是毕竟有的时候重启次数太多了会导致任务执行时间很长甚至于整个任务失败,所以这个问题还是不容忽视的。那到底是因为什么导致容器被killed了呢?是谁把容器给kill了呢?怎么解决这个问题呢?

        最开始的时候百度了一下找到一篇文章(Spark 中的“Container killed on request.Exit code is 137”)讲了这个问题,但是折腾半天不好使,后面仔细分析一下问题,才发现是Spark executor运行时占用内存太多而物理内存不足,Linux内核开启了oom killer机制(Linux内核OOM机制的详细分析),Linux内核会根据一定机制选择内存占用过大的进程kill掉。而错误日志里的“Killed by external signal”中的“external signal”指的是“kill -9”(SIGKILL)这个信号(Linux Signal信号详解),而这个信号对应的进程退出码(Linux and Unix exit code tutorial with examples)就是137。而且在Spark报137错误时,能够在被killed 掉的容器所在的主机上的日志文件/var/log/messages能看到如下的日志:

host0 kernel: Out of memory: Kill process 12290 (java) score 152 or sacrifice child
host0 kernel: Killed process 12290 (java), UID 1011, total-vm:23715088kB, anon-rss:8738028kB, file-rss:0kB, shmem-rss:20kB
host0 kernel: java invoked oom-killer: gfp_mask=0x201da, order=0, oom_score_adj=0

日志里面我们会看到“Out of memory”、“java invoked oom-killer”字样。

         现在知道这个问题是我们的服务器内存不足导致的,但是怎么解决呢?把oom killer机制关了?这个属于掩耳盗铃。申请升级服务器?不好意思预算紧张。总不能用我的工资升级服务器吧,所以还得自己想其他办法节省内存来规避这个问题。对于Spark要节省内存无非一下几个办法:

        1.使用Kryo序列化。

        Spark默认使用Java序列化,而使用Kryo会大大减少对象序列化后数据的大小,当在代码中使用了RDD持久化时,会大大减少存储空间的使用。

        2.调整RDD持久化。

        通过观察Spark UI中Storage页面可以看到持久化的具体统计数据,项目代码里StorageLevel原本设置为MEMORY_ONLY,这会占用大量的内存,可以在启动参数中把--executor-memory适当调小,并在代码中把StorageLevel设置为 MEMORY_AND_DISK或者MEMORY_AND_DISK_SER(MEMORY_AND_DISK_SER会占用更少的存储空间,但是会占用更多的CPU时间)甚至于DISK_ONLY。虽然这样使一部分数据持久化在磁盘里,但是总比容器被killed然后有重启付出的代价少,而且现在很多云服务器可以用SSD,相比HDD也是鸟q换了炮。

        3.调整并发度与并行度

        可以通过调大spark.sql.shuffle.partitions、spark.default.parallelism来增加RDD的分区数来减少每个分区的数据量;同时可以减少spark.executor.cores,减少同时运行的task的数量以减少内存的使用。

         4.祛除数据倾斜。

        内存占用过高还有一种情况就是数据倾斜,如果老是某些节点的container被killed掉,很可能就是存在数据倾斜,关于数据倾斜具体可参考:Spark性能优化指南

         经过实践最有用的是前两种办法,基本上就可以解决这个问题。还有一点就是在配置yarn资源时,不要把内存上限设置得太满,留一定的冗余也不会出现这个问题,我们的集群就是没有注意这个问题,yarn.nodemanager.resource.memory-mb设置得太高了。

        最后还想说一点就是在分析这个问题的时候发现Spark on YARN在生成运行executor的脚本文件(就是launch_container.sh)时,最后生成运行executor的shell命令如下:

exec /bin/bash -c "LD_LIBRARY_PATH="/usr/hdp/current/hadoop-client/lib/native:/usr/hdp/current/hadoop-client/lib/native/Linux-amd64-64:$LD_LIBRARY_PATH" $JAVA_HOME/bin/java -server -Xmx10240m '-XX:+UseNUMA' -Djava.io.tmpdir=$PWD/tmp '-Dspark.history.ui.port=18081' '-Dspark.driver.port=37119' -Dspark.yarn.app.container.log.dir=/hadoop/yarn/log/application_1650941597513_0025/container_e52_1650941597513_0025_01_000003 -XX:OnOutOfMemoryError='kill %p' org.apache.spark.executor.CoarseGrainedExecutorBackend --driver-url spark://CoarseGrainedScheduler@had3:37119 --executor-id 2 --hostname had3 --cores 3 --app-id application_1650941597513_0025 --user-class-path file:$PWD/__app__.jar 1>/hadoop/yarn/log/application_1650941597513_0025/container_e52_1650941597513_0025_01_000003/stdout 2>/hadoop/yarn/log/application_1650941597513_0025/container_e52_1650941597513_0025_01_000003/stderr"

 其中-Xmx10240m中的数值就对应的是--executor-memory的设置,而且会默认添加一个参数:-XX:OnOutOfMemoryError='kill %p',意思是当出现OutOfMemory时会执行kill命令将当前的进程杀死,至于Spark为什么要这样做源代码解释如下:

  /**
   * Kill if OOM is raised - leverage yarn's failure handling to cause rescheduling.
   * Not killing the task leaves various aspects of the executor and (to some extent) the jvm in
   * an inconsistent state.
   * TODO: If the OOM is not recoverable by rescheduling it on different node, then do
   * 'something' to fail job ... akin to blacklisting trackers in mapred ?
   *
   * The handler if an OOM Exception is thrown by the JVM must be configured on Windows
   * differently: the 'taskkill' command should be used, whereas Unix-based systems use 'kill'.
   *
   * As the JVM interprets both %p and %%p as the same, we can use either of them. However,
   * some tests on Windows computers suggest, that the JVM only accepts '%%p'.
   *
   * Furthermore, the behavior of the character '%' on the Windows command line differs from
   * the behavior of '%' in a .cmd file: it gets interpreted as an incomplete environment
   * variable. Windows .cmd files escape a '%' by '%%'. Thus, the correct way of writing
   * '%%p' in an escaped way is '%%%%p'.
   */
  private[yarn] def addOutOfMemoryErrorArgument(javaOpts: ListBuffer[String]): Unit = {
    if (!javaOpts.exists(_.contains("-XX:OnOutOfMemoryError"))) {
      if (Utils.isWindows) {
        javaOpts += escapeForShell("-XX:OnOutOfMemoryError=taskkill /F /PID %%%%p")
      } else {
        javaOpts += "-XX:OnOutOfMemoryError='kill %p'"
      }
    }
  }

最开始的怀疑kill的问题跟这个有关,但是其实不是的,因为kill对于的退出码是143,而137对应的是“kill -9”,应该是在jvm出现OutOfMemoryError之前就已经被Linux给kill掉了,如果你的日志里面出现的exit code是143,那么大概率是出现了OutOfMemoryError。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/872098.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存