浅谈 Hadoop YARN Linux Container Executor 和 JVM 沙箱

本文简要概述了如何使用 YARN 安全容器中的 Linux Container Executor (LCE),以及如何使用 JavaSandboxLinuxContainerRuntime。参考资料除了官网资料之外,还有YARN-5280与网络资料。

YARN 安全容器

安全集群中的 YARN 容器使用操作系统工具为容器提供执行隔离。安全容器在作业用户的凭据下执行。操作系统对容器实施访问限制。容器必须作为提交应用程序的用户运行。安全容器仅在安全 YARN 集群的上下文中工作。

容器隔离要求

容器执行器必须访问容器所需的本地文件和目录,例如 jars、配置文件、日志文件、共享对象等。虽然它是由 NodeManager 启动的,但容器不应访问 NodeManager 私有文件和配置。不同用户提交的容器运行应用程序应相互隔离,不能互相访问文件和目录。类似的要求适用于其他系统非文件安全对象,如命名管道、临界区、LPC 队列、共享内存等。

Linux Secure Container Executor

在 Linux 环境中,安全容器执行器是 LinuxContainerExecutor。 它使用一个名为 container-executor 的外部程序来启动容器。 该程序设置了 setuid 访问权限标志,允许它以 YARN 应用程序用户的权限启动容器。

这个 container-executorsetuid 二进制文件是 hadoop-yarn 包的一部分。这个容器执行程序仅在 YARN 上使用并仅在 GNU/Linux 上受支持。它要求在启动容器的集群主机上创建所有用户帐户。它使用包含在 Hadoop 发行版中的 setuid 可执行文件,NodeManager 使用这个可执行文件来启动和停止容器。 setuid 可执行文件切换到已提交应用程序并启动或停止容器的用户。

yarn.nodemanager.local-dirsyarn.nodemanager.log-dirs 的配置目录必须由配置的 NodeManager 用户 (yarn) 和组 (hadoop) 拥有。对这些目录设置的权限必须是 drwxr-xr-xcontainer-executor 程序必须由 root 拥有、被运行 YARN 守护进程的用户组拥有(例如hadoop)、被 root:hadoop 拥有(如果你的 YARN 用户组为hadoop)并具有权限集 ---Sr-s---(6050 权限)。

要将 NodeManager 配置为使用 LinuxContainerExecutor,在 conf/yarn-site.xml 中配置:

yarn-site.xml
1
2
3
4
5
6
7
8
<property>
<name>yarn.nodemanager.container-executor.class</name>
<value>org.apache.hadoop.yarn.server.nodemanager.LinuxContainerExecutor</value>
</property>
<property>
<name>yarn.nodemanager.linux-container-executor.group</name>
<value>hadoop</value>
</property>

此外,LCE 需要 container-executor.cfg 文件,该文件由 container-executor 程序读取:

container-executor.cfg
1
2
3
4
5
yarn.nodemanager.linux-container-executor.group=hadoop#configured value of yarn.nodemanager.linux-container-executor.group
banned.users=hdfs,mapred#comma separated list of users who can not run applications
allowed.system.users=yarn#comma separated list of allowed system users
min.user.id=1000#Prevent other super-users
feature.terminal.enabled=1

终端功能 (feature.terminal.enabled) 允许通过 YARN UI2 将受限 shell 放入安全容器中。

除了 $HADOOP_HOME/bin/container-executor 需要 6050 权限(且被 root:hadoop 用户拥有)以外,$HADOOP_HOME 本身需要 755 或更低的权限(不能给与用户组的写权限)。

在实际操作中,官网下载的 Hadoop 其所有者为 1000:1000,在 yarn 用户的 uid=1000hadoop 组的 gid=1000的前提下,个人使用如下配置:

1
2
3
4
5
6
7
8
sudo chmod +x $HADOOP_HOME/etc/hadoop/*.sh
sudo chown root:hadoop $HADOOP_HOME/bin/container-executor
sudo chmod 6050 $HADOOP_HOME/bin/container-executor
sudo chown root:hadoop $HADOOP_HOME/etc/hadoop/container-executor.cfg
sudo chmod 0440 $HADOOP_HOME/etc/hadoop/container-executor.cfg
sudo chown root:hadoop $HADOOP_HOME/etc/hadoop
sudo chown root:hadoop $HADOOP_HOME/etc
sudo chown root:hadoop $HADOOP_HOME

conf/yarn-site.xml 中的可选配置:

yarn-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
<property>
<name>yarn.nodemanager.linux-container-executor.resources-handler.class</name>
<value>org.apache.hadoop.yarn.server.nodemanager.util.DefaultLCEResourcesHandler</value>
</property>
<property>
<name>yarn.nodemanager.linux-container-executor.nonsecure-mode.limit-users</name>
<value>true</value>
</property>
<property>
<name>yarn.nodemanager.linux-container-executor.nonsecure-mode.local-user</name>
<value>yarn</value>
</property>

使用 Java Sandbox 启动应用程序:JavaSandboxLinuxContainerRuntime

JavaSandboxLinuxContainerRuntime 是 YARN 中 JVM 沙箱的一个实现。用户可以给 JVM 程序配置默认的安全策略,或者使用自带的安全策略。用户也可以配置非 JVM 程序禁止或不在安全模式下运行。这个类扩展了 DefaultLinuxContainerRuntime。它的基本原理就是为container生成相应的Java安全策略文件,修改Java命令以应用这些策略。

要开启JavaSandboxLinuxContainerRuntime,需要进行以下配置:

yarn-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<property>
<name>yarn.nodemanager.runtime.linux.allowed-runtimes</name>
<value>default,docker,javasandbox</value>
</property>
<property>
<name>yarn.nodemanager.runtime.linux.type</name>
<value>default</value>
</property>
<property>
<name>yarn.nodemanager.runtime.linux.sandbox-mode</name>
<value>enforcing</value>
</property>
<property>
<name>yarn.nodemanager.runtime.linux.sandbox-mode.local-dirs.permissions</name>
<value>read</value>
</property>
<property>
<name>yarn.nodemanager.runtime.linux.sandbox-mode.policy.group.hadoop</name>
<value>/data/nodemanager/java.policy</value>
</property>

下面进行逐条解释。

首先,yarn.nodemanager.runtime.linux.allowed-runtimes需要添加javasandbox以允许 JVM 沙箱运行。yarn.nodemanager.runtime.linux.type 可以不做改动,因为 JVM 沙箱的运行不是通过这条配置来进行探测的。

对于yarn.nodemanager.runtime.linux.sandbox-mode,配置如下:

  • disabled: 关闭LinuxContainerRuntime
  • permissive: JVM容器会遵循安全策略,非JVM容器正常运行
  • enforcing: JVM容器会遵循安全策略,非JVM容器禁止运行,抛出异常

yarn.nodemanager.runtime.linux.sandbox-mode.local-dirs.permissions默认为read,可以配置为read,write,execute,delete中的一个或多个。

yarn.nodemanager.runtime.linux.sandbox-mode.policy默认为空。它接收一个本地文件系统路径(Java策略文件),作为基础策略。如果不指定,则使用默认的策略文件。相应地,yarn.nodemanager.runtime.linux.sandbox-mode.policy.group.$groupName用于将指定group映射到指定策略。如果一个用户属于多个组,则策略相互叠加。在这里,我们指定hadoop组的默认策略。

yarn.nodemanager.runtime.linux.sandbox-mode.whitelist-group为可选配置,可以指定某个队列上的应用不遵循沙箱策略。

在配置时,推荐给mapred-site增加如下配置,避免找不到JAVA_HOME

mapred-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<property>
<name>yarn.app.mapreduce.am.env</name>
<value>JAVA_HOME=/usr/local/openjdk-8</value>
</property>
<property>
<name>yarn.app.mapreduce.am.command-opts</name>
<value>-Xmx1024m -Djava.ext.dirs=file:$JAVA_HOME/jre/lib/ext </value>
</property>
<property>
<name>mapred.child.env</name>
<value>JAVA_HOME=/usr/local/openjdk-8</value>
</property>
<property>
<name>mapred.child.java.opts</name>
<value>-Djava.ext.dirs=file:$JAVA_HOME/jre/lib/ext </value>
</property>

其中JAVA_HOME需要根据你的本地机器指定。在安全策略中,默认的策略可以在 Hadoop 源码中找到,这里给出其中的关键部分:

java.policy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// Standard extensions get all permissions by default
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};

// default permissions granted to all domains
grant {
permission java.lang.RuntimePermission "accessDeclaredMembers";

permission java.util.PropertyPermission "java.version", "read";
permission java.util.PropertyPermission "java.vendor", "read";
permission java.util.PropertyPermission "java.vendor.url", "read";
permission java.util.PropertyPermission "java.class.version", "read";
permission java.util.PropertyPermission "os.name", "read";
permission java.util.PropertyPermission "os.version", "read";
permission java.util.PropertyPermission "os.arch", "read";
permission java.util.PropertyPermission "file.separator", "read";
permission java.util.PropertyPermission "path.separator", "read";
permission java.util.PropertyPermission "line.separator", "read";

permission java.util.PropertyPermission "java.specification.version", "read";
permission java.util.PropertyPermission "java.specification.vendor", "read";
permission java.util.PropertyPermission "java.specification.name", "read";

permission java.util.PropertyPermission "java.vm.specification.version", "read";
permission java.util.PropertyPermission "java.vm.specification.vendor", "read";
permission java.util.PropertyPermission "java.vm.specification.name", "read";
permission java.util.PropertyPermission "java.vm.version", "read";
permission java.util.PropertyPermission "java.vm.vendor", "read";
permission java.util.PropertyPermission "java.vm.name", "read";

//additional hadoop permissions
permission java.util.PropertyPermission "awt.Toolkit", "read";
permission java.util.PropertyPermission "file.encoding", "read";
permission java.util.PropertyPermission "file.encoding.pkg", "read";
permission java.util.PropertyPermission "hadoop.metrics.log.level", "read";
permission java.util.PropertyPermission "hadoop.root.logger", "read";
permission java.util.PropertyPermission "java.awt.graphicsenv" ,"read";
permission java.util.PropertyPermission "java.awt.printerjob", "read";
permission java.util.PropertyPermission "java.class.path", "read";
permission java.util.PropertyPermission "yarn.app.container.log.dir", "read";
permission java.util.PropertyPermission "yarn.app.container.log.filesize", "read";
permission java.lang.RuntimePermission "loadLibrary.gplcompression";
permission javax.security.auth.AuthPermission "getSubject";
};

以 MapReduce 程序为例,启动脚本会被修改为如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$JAVA_HOME/bin/java 
-Djava.security.manager
-Djava.security.policy==/tmp/hadoop-yarn/nm-sandbox-policies/container_e18_1653552682428_0001_01_000001-java.policy
-Djava.security.debug=all
-Djava.io.tmpdir=$PWD/tmp
-Dlog4j.configuration=container-log4j.properties
-Dyarn.app.container.log.dir=/usr/local/hadoop/logs/userlogs/application_1653552682428_0001/container_e18_1653552682428_0001_01_000001
-Dyarn.app.container.log.filesize=0
-Dhadoop.root.logger=INFO,CLA
-Dhadoop.root.logfile=syslog
-Xmx1024m
-Djava.ext.dirs=file:$JAVA_HOME/jre/lib/ext
org.apache.hadoop.mapreduce.v2.app.MRAppMaster
1>/usr/local/hadoop/logs/userlogs/application_1653552682428_0001/container_e18_1653552682428_0001_01_000001/stdout
2>/usr/local/hadoop/logs/userlogs/application_1653552682428_0001/container_e18_1653552682428_0001_01_000001/stderr

使用 Docker 容器启动应用程序:DockerLinuxContainerRuntime

Linux Container Executor (LCE) 允许 YARN NM 启动 YARN 容器以直接在主机上或在 Docker 容器内运行。请求资源的应用程序可以为每个容器指定它应该如何执行。当 LCE 启动 YARN 容器以在 Docker 容器中执行时,应用程序可以指定要使用的 Docker 镜像。Docker 容器提供了一个自定义的执行环境,应用程序的代码在其中运行,与 NM 和其他应用程序的执行环境隔离。Docker 容器甚至可以运行与 NM 上不同风格的 Linux。YARN 的 Docker 提供一致性(所有 YARN 容器将具有相同的软件环境)和隔离(不干扰物理机上安装的任何内容)。

LCE 要求容器执行器二进制文件由 root:hadoop 拥有并具有 6050 权限。为了启动 Docker 容器,Docker 守护进程必须在所有 NM 主机上运行。Docker 客户端还必须安装在所有相关的 NM 主机上。

有关此功能的相关配置,请参见Cluster Configuration