7.6.2 资源隔离机制
资源隔离是指为不同任务提供可独立使用的计算资源以避免它们相互干扰。当前存在很多资源隔离技术,比如硬件虚拟化、Linux Container[1]等。
前面提到,Hadoop为各个任务启动独立的Java虚拟机以达到资源隔离的目的。然而,考虑到用户应用程序可能会创建其他子进程,如Hadoop Pipes(或者Hadoop Streaming)编写的MapReduce应用程序中每个任务至少由Java进程和C++进程两个进程组成,这难以通过创建单独的虚拟机达到资源隔离的效果。为了弥补这个不足,在Linux环境(其他操作系统暂不支持)中,Hadoop在各个TaskTracker上启动一个TaskMemoryManagerThread线程以监控各个任务内存使用情况,一旦发现某个任务使用内存过量,则直接将其杀死。
1.MapReduce作业的配置参数
由于不同的MapReduce作业对内存的需求不同,因此,Hadoop提供了各种配置参数帮助用户和管理员合理地使用内存资源。这些参数可分为两类,一类是用户配置参数,比如用户根据自己应用程序特点为Map Task和Reduce Task设定的最大内存量;另一类是管理员配置参数,比如为防止用户滥用内存资源,管理员可限定每个任务的最大可用内存量。
(1)用户配置参数
mapred. job.{map|reduce}.memory.mb:作业的Map Task或Reduce Task最多使用的内存量(单位:MB)。
(2)管理员配置参数
❑mapred. cluster.max.{map|reduce}.memory.mb:用户可设置的Map Task或Reduce Task最多使用内存量的上限值(单位:MB)。
❑mapred. job.{map|reduce}.memory.mb:TaskTracker上每个Map slot或Reduce slot代表的内存量(单位:MB)。注意,有些调度器会根据该参数值和任务内存需求为任务分配多个slot,比如Capacity Scheduler。
2.MapReduce作业内存监控
TaskMemoryManagerThread线程每隔一段时间(由参数mapred.tasktracker.taskmemorymanager.monitoring-interval指定,默认是5 s)扫描所有正在运行的任务,并按照以下步骤检查它们使用的内存量是否超过上限值。
步骤1 构造进程树。
在/proc目录下,有大量以整数命名的目录,这些整数是某个正在运行的进程的进程号,而目录下面则是该进程运行时的一些信息。为了更全面地监控一个任务的内存使用量,TaskTracker通过读取/proc/<pid>/stat文件构造出以该任务进程为根的进程树。这样,通过监控该进程树使用的内存总量可严格限制一个任务使用的内存量。
步骤2 判断单个任务内存使用量是否超过内存量最大值。
在Linux系统中,/proc/<pid>/stat文件中包含了进程pid的运行时信息,而TaskTracker正是使用正则表达式从该文件中提取进程的运行时信息,具体包括进程名称、父进程的PID、父进程组号、Session ID在用户态运行的时间(单位:jiffies)、核心态运行的时间(单位:jiffies)、占用虚拟内存大小(单位:page)和占用物理内存大小(单位:page)等。TaskTracker使用的正则表示式为:
^([0-9-]+)\s([^\s]+)\s[^\s]\s([0-9-]+)\s([0-9-]+)\s([0-9-]+)\s([0-9-]+\s){7}([0-9]+)\s([0-9]+)\s([0-9-]+\s){7}([0-9]+)\s([0-9]+)(\s[0-9-]+){15}
文件/proc/<pid>/stat中包含的内存大小单位为page。为了获取以字节为单位的内存信息,TaskTracker通过执行以下Shell命令获取每个page对应的内存量(单位:B):
getconf PAGESIZE
通过以上信息可计算当前每个运行的任务使用的内存总量。但需要注意的是,不能仅凭该内存量是否超过设定的内存最高值决定杀死一个任务。在创建一个子进程时,JVM采用了“fork()+exec()”模型,这意味着进程创建之后、执行之前会复制一份父进程内存空间,进而使得进程树在某一小段时间内存使用量翻倍。为了避免误杀任务,Hadoop赋予每个进程“年龄”属性,并规定刚启动进程的年龄是1,且TaskMemoryManagerThread线程每更新一次,各个进程年龄加1。在此基础上,选择被杀死任务的标准如下:
如果一个任务对应的进程树中所有进程(年龄大于0)总内存超过(用户设置的)最大值的两倍,或者所有年龄大于1的进程总内存量超过(用户设置的)最大值,则认为该任务过量使用内存,直接将其杀死,并将状态标注为FAILED。
步骤3 判断任务总内存使用量是否超过总可用内存量。
计算所有正在运行的任务当前使用的内存总量,如果超过系统可用内存总量(slot数与slot对应内存乘积),则TaskTracker会不断选择进度最慢的任务并将其杀掉,直到内存使用量降到可用内存总量以下。
需要注意的是,在下一代MapReduce(见第12章)中,同时增加了对内存资源和CPU资源的隔离机制,但采用的资源隔离方案不同。对于内存资源,为了能够更灵活地控制内存使用量,它仍采用了本节所述的线程监控的方案。采用这种机制的主要原因是Java中创建子进程采用了“fork()+exec()”的方案,子进程创建瞬间,它使用的内存量与父进程一致,从外面看来,一个进程使用内存量瞬间翻倍,然后又降下来,采用线程监控的方法可防止这种情况下导致swap操作。对于CPU资源,则采用了Cgroups进行资源隔离。具体可参考YARN-3[2]。
[1]这里的内存指的是虚拟内存。从0.21.0版本开始,Hadoop允许设置物理内存,具体可参考:https://issues.apache.org/jira/browse/MAPREDUCE-1221。
[2]https://issues. apache.org/jira/browse/YARN-3