1. 先搞清全局:JobTracker与TaskTracker在早期Hadoop里的位置
很多人学大数据,一上来就啃HDFS、MapReduce原理,结果被一堆名词绕晕。如果让我给新人指一条最快建立体系感的路,我会建议先从Hadoop经典计算引擎入手,把JobTracker与TaskTracker这两个核心组件彻底吃透。它们俩是整个MapReduce计算框架的“大脑”和“四肢”,也是后来理解YARN、Spark调度器的地基。
先放一张宏观图景:在Hadoop 1.x时代,一个集群里同时跑着两套独立的主从架构。一套是存储体系,由NameNode和DataNode组成,负责把大文件切块存到多台机器上;另一套就是计算体系,由JobTracker和TaskTracker组成,负责把用户写的计算逻辑分发到各台机器上执行,再把结果汇总回来。前者管“数据放哪”,后者管“逻辑跑哪”,两套体系互不干扰,又紧密配合。
JobTracker运行在主节点上,和NameNode通常部署在同一台机器。它的角色可以类比成一个项目总包:接收客户端提交的作业,把作业拆成一个个可以并行执行的小任务,再根据每台执行机的实时负载,把任务派下去,全程盯着进度,谁跑挂了就安排重跑。TaskTracker则运行在每一台数据节点上,角色类似施工队:老老实实接收JobTracker下发的任务,在本机启动进程执行,并定期向JobTracker汇报“我这边还闲着多少工位”。
这里要特别提醒一个新手很容易混淆的点:JobTracker和TaskTracker是计算框架的组件,不负责存储;所有中间结果和最终结果,要么写在本地磁盘,要么写回HDFS。理解了这两个组件各自的分工边界,后面看调度、看容错、看数据本地性都不会迷路。
2. 调度中枢JobTracker:职责拆解与关键参数
JobTracker被无数文章称为“单点大脑”,这个说法很形象,但它实际管的活远比“派任务”三个字复杂。我拆开来讲。
2.1 JobTracker到底管了什么
第一件事是接收作业。客户端通过JobClient提交作业时,会把打好的jar包、XML配置和输入分片信息分别上传到HDFS的临时目录,然后向JobTracker发起一个RPC请求。JobTracker拿到请求后,会为作业分配一个全局唯一的作业ID,创建对应的JobInProgress对象,把它放入调度队列。从这一刻起,这个作业就归JobTracker管了。
第二件事是拆任务。一个作业能拆成多少个Map任务,取决于输入路径被切成了多少个InputSplit;Reduce任务的数量则由用户配置制定,通常根据业务需求手动指定。JobTracker并不是真正执行切分的角色,切分信息在客户端就已经算好,但JobTracker需要基于这些分片信息去规划任务列表,并决定每个任务应该优先派给哪台机器。
第三件事是收集集群资源视图。每一台TaskTracker都会通过心跳上报自己当前有多少个可用的Map槽位、Reduce槽位,以及健康状态。JobTracker把这些信息汇总成一张全局资源表,调度器才能知道“现在谁能接活、谁手上还有余量”。
第四件事是监控与容错。作业跑起来之后,所有状态变化都会通过心跳和任务汇报传到JobTracker。它需要跟踪每个Map任务和Reduce任务的进度,发现某个任务反复失败、或某台TaskTracker长时间没有心跳,就要触发重新调度或把任务转移到其他节点。
2.2 三种调度器与调度相关的核心参数
JobTracker内置的调度器,在1.x时代主要有三个选择。
默认的是FIFO调度器,先把最早到达的作业排在队列最前面,一个作业占不满集群资源时,后面的作业也能跟着用空闲资源。优点是实现简单,缺点是后提交的小作业可能被大作业长时间堵住,典型的一支独大局面。
后来社区陆续加入了Fair Scheduler和Capacity Scheduler。公平调度的思路是让所有作业轮流使用资源,短作业能快速挤进来;容量调度则是把集群资源按比例分给多个队列,每个队列内部再独立调度。这也是现在生产集群里最常见的资源管理理念,可以说JobTracker时期就把多用户资源隔离的雏形磨出来了。
调度相关的几个参数,生产环境里我几乎每次都要调,列在下面供参考。
| 参数 | 默认值 | 作用说明 |
|---|---|---|
| mapred.jobtracker.maxtaskpernode | 2 | 限制每台节点同时运行的最大任务数,防止单机过载 |
| mapred.reduce.slowstart.completed.maps | 0.05 | 控制Reduce任务在多少比例的Map任务完成后才启动,默认5%就开始拉数据,大作业常调到0.7以上 |
| mapred.jobtracker.handler.count | 10 | JobTracker处理RPC请求的线程数,作业并发高时要调大 |
| mapred.max.map.failures.percent | 0 | 允许失败的Map任务占总任务数的百分比,超过则整个作业失败 |
| mapred.tasktracker.expiry.interval | 600000 | TaskTracker心跳超时判定时间,单位毫秒,默认10分钟 |
比如reduce.slowstart这个参数,很多人忽视。Reduce任务启动早,确实能提前拉取Map输出,但如果太早,Reduce进程会白白占用槽位却等不到数据;调高了又可能让尾盘堵塞。实际经验是,如果集群Map阶段普遍较长,把它从默认的0.05调到0.6到0.8,资源利用率会明显改善。
2.3 JobTracker的重负载与单点隐患
把上述所有职责堆到一个进程里,性能压力自然可想而知。每个作业的JobInProgress和每个任务的TaskInProgress都要常驻内存,作业一多,光对象引用就够喝一壶,频繁Full GC是那个年代主节点最常见的死法。
更麻烦的是单点故障。JobTracker挂了,整个集群的计算能力直接瘫痪,所有已提交作业卡死;等进程重启后,内存里的作业状态全部丢失,新提交的作业也只能重新排队。这种设计在几十台节点的小集群里还能将就,规模一旦上千,就成了最让人头痛的瓶颈。后来社区把JobTracker拆成ResourceManager和ApplicationMaster,很大程度上就是为了解决这个“大脑袋”的问题。
3. 执行节点TaskTracker:任务落地与心跳汇报
如果说JobTracker是大脑,那TaskTracker就是最前线干活的工人。但“干活”这两个字背后,也藏着不少后来被YARN吸收的设计思路。
3.1 TaskTracker的核心职责
TaskTracker最核心的工作,是接收JobTracker分配来的任务、在本地启动JVM进程执行、再汇报结果和进度。每个任务不是直接在主进程里跑的,而是由TaskTracker派生出一个新的JVM进程去运行。这样做的好处是任务之间的依赖和崩溃隔离能力更强,坏一个任务不至于拖垮整台机器的TaskTracker。
它的另一个职责是维持心跳通信。TaskTracker默认每隔3秒向JobTracker发送一次心跳,心跳包里除了“我活着”之外,还装着本机的可用槽位数量、正在执行的任务进度、完成任务的数量、本地磁盘剩余空间等关键信息。JobTracker正是根据这些心跳内容,决定要不要给这台机器派新任务、派什么任务。
很多人会误以为TaskTracker只负责执行,其实它还承担了清理工作。任务执行产生的中间文件、日志碎片,都需要TaskTracker定期清理,否则本地磁盘很快会被中间结果堆满,这个问题在长时间跑批的场景里尤其明显。
3.2 槽位机制与资源隔离真相
旧版Hadoop里的资源概念只有一个,叫做槽位,英文Slot。它分为Map Slot和Reduce Slot两种,互不通用。TaskTracker默认配置为同时运行2个Map任务和2个Reduce任务,对应参数是mapred.tasktracker.map.tasks.maximum和mapred.tasktracker.reduce.tasks.maximum。
这里有个特别容易踩的认知误区:槽位不等于CPU核数,也不是内存大小,它只是一个最简单的并发计数凭证。一台8核32G的机器,默认也只能跑2个Map加2个Reduce任务,后面的任务排着队等槽位释放。反过来,如果把这俩参数调大,任务确实能并发上去,但旧版Hadoop并没有真正的CPU内存隔离机制,多个任务同时开跑后,CPU争抢、内存溢出、互相干扰的问题很快会冒出来。
JVM重用参数也是这个阶段特有的优化手段。默认情况下每个Map或Reduce任务跑完就销毁JVM,频繁创建进程开销不小;把mapred.job.reuse.jvm.num.tasks设成大于1,可以让同一个JVM连续跑多个任务。但副作用很隐蔽,JVM里残留的静态变量可能污染下一个任务,排查起来相当费劲。我的建议是,除非单个任务本身极短极小,否则不要轻易开启重用,省的那点进程开销,远不够填后面调静态变量脏数据埋的坑。
3.3 数据本地性与HDFS配合
TaskTracker和数据节点的绑定,是这套架构能跑得快的关键因素之一。HDFS会把一个文件块复制成多份,存放在不同机器上;JobTracker在调度Map任务时,会优先选择待处理数据块副本所在的TaskTracker,这就是所谓的数据本地性调度。
这个设计背后的道理很好懂:把计算推到数据所在的机器上,任务进程直接从本地磁盘读块,走的是本机IO,速度快、不占网络带宽;如果非要反过来把数据传来传去,那MapReduce最庞大的中间传输量会把集群网络彻底打爆。
但在实操中,数据本地性并不是百分之百能保证的。比如某台机器上槽位全满,或者副本所在节点心跳异常被标记为不可用,JobTracker只能退而求其次,选择同机架上的其他TaskTracker。Hadoop的机架感知功能就是为了让调度器能判断“哪些机器离得近”,减少跨机架的数据搬移。配置机架感知需要自定义机架脚本,这一环节在很多老集群里其实没有配全,直接导致Map任务大量走远程读数据,性能白扔一截。
4. 一探到底:MapReduce作业从提交到产出的完整旅程
光知道两个组件各自干什么还不够,把一条作业的完整执行链路串起来,才是真正看懂经典计算引擎的标志。下面这条主线,我按实际运行的先后顺序来讲。
4.1 客户端提交与作业初始化
当用户执行一条hadoop jar命令时,客户端先连接HDFS的NameNode,往临时目录写入三个关键东西:job.jar(用户程序及依赖)、job.xml(配置参数)、job.split(输入分片元信息)。传输完成后,客户端向JobTracker请求创建作业。
JobTracker收到请求,为作业生成作业ID,然后从HDFS上把分片信息读回来,根据分片数量确定Map任务数,结合用户配置的Reducer数量生成任务列表,最后把整个作业封装成JobInProgress放进调度队列。从客户端视角看,提交作业这件事到这就完成了,接下来进度条开始滚动,背后的一切都由JobTracker代管。
4.2 任务分配:JobTracker根据什么决定把任务给谁
前文说过,TaskTracker通过心跳上报可用槽位。JobTracker的调度器在每个心跳周期里,遍历一遍当前待执行的任务,把适合这个TaskTracker的任务塞进心跳响应里下发。判断“适合”主要看三点:数据本地性、槽位类型、任务优先级。
数据本地性是第一优先级的考量。TaskTracker尤其是距离数据块最近的节点时,对应的Map任务会被优先分配;如果一个分片有多个副本,调度器会按心跳顺序匹配任一副本所在节点。Reduce任务没有数据本地性概念,因为Reduce的输入是全局的,它需要从所有Map任务的输出里拉取数据,所以分配时只看哪里有空闲Reduce槽位。
4.3 Map端执行与溢写细节
任务下发到TaskTracker后,由子进程里的MapTask开始执行。每个Map任务从对应InputSplit里逐行读取键值对,调用用户的map函数处理后,把输出先写进内存中的环形缓冲区。缓冲区默认大小100MB,达到阈值后会触发溢写,把数据按Partitioner分区分好,在分区内部按键排序后写入本地磁盘的小文件。
这里有一个提升性能的关键改动:在溢写之前可以跑一次Combiner,相当于在Map端先做一次局部聚合,能显著减少写到磁盘和网络传输的数据量。多轮溢写会产生多个中间文件,Map任务结束前会把它们合并成一个大文件,同时建成一个索引文件,方便后续Reduce来拉取对应分区的数据。
Map端执行完不马上退出,它还得等Reduce端把数据全部拉完才能清理本地文件。这也是为什么有时候能看到Map进度停留在100%很长时间,其实是在等Reduce完成最后的拉取和清理。
4.4 Shuffle、Reduce端执行与结果回收
Shuffle是MapReduce最核心也最容易被面试官反复拷问的一段。从Reduce任务角度看,它有两大阶段:先按需从各个MapTask所在节点拉取属于自己的那份分区数据,拉取到本地后做归并排序;然后把合并后的数据断成一串有序键值对交给用户的reduce函数逐键处理。
Reduce端启动时机很讲究,过早启动会干等数据,占用槽位浪费资源;过晚启动又会让Map端堆积大量待拉取数据,拖长作业尾屏时间。这个平衡由slowstart参数控制,前面提过的mapred.reduce.slowstart.completed.maps就是干这个用的。Reduce端能拉数据的并发线程数也有限制,参数mapred.reduce.parallel.copies默认5个线程,网络带宽不足的集群里调这个参数比无脑调内存更有效。
全部Reduce任务完成后,每个Reducer把结果写到HDFS,默认写到一个名为part-r-xxxxx的文件里。此时JobTracker更新作业状态为成功,整个计算链路才算闭环。
4.5 任务失败与推测执行
分布式环境下任务失败是常态,不是例外。TaskTracker在任务执行过程中如果发现异常,会先把任务状态标记为失败,并通过心跳通知JobTracker。JobTracker收到失败消息后,将这个任务重新调度,默认重试4次,连续超过限制才会让整个作业失败。
还有一种失败是被动发现的。如果某台TaskTracker超过mapred.tasktracker.expiry.interval没有心跳,JobTracker会判定该节点失联,把它标记为不可用,并把正在它上面运行的所有任务重新分配到别的节点。这要求任务支持从头重跑,所以MapReduce任务每一步都必须做记录和容错恢复,这也是它写得“重”的原因之一。
推测执行是老架构里很有特色的容错策略。当某个任务运行速度明显慢于同类任务时,JobTracker会启动一个同样的任务在别的节点上并行跑,谁先完成用谁的结果。出发点很好,但实际项目中我经常直接把它关掉,因为同一个作业里任务之间的进度差异往往取决于数据倾斜而非机器性能,盲目开“备份任务”只会让集群资源雪上加霜。参数是mapred.map.tasks.speculative.execution和mapred.reduce.tasks.speculative.execution,出现数据倾斜的作业建议设为false。
5. 实操复盘:手工搭建迷你集群并跑通WordCount
理论讲再多,不如亲手跑一遍。这一节我会带大家把一个双节点的小集群跑起来,然后把JobTracker和TaskTracker的配置逐项说透。这里用的都是Hadoop 1.x的老版本,新版本里已经找不到这两个组件了。
5.1 版本选择与环境准备
要体验原汁原味的JobTracker,只能选择Hadoop 1.x分支。建议直接下载Hadoop 1.2.1的稳定发行包,放到一台Linux服务器或虚拟机上,另外准备几台机器模拟集群。准备阶段需要做四件事:
- 所有节点统一安装JDK 1.6或1.7,配置JAVA_HOME环境变量;
- 配置主节点到所有从节点的SSH免密登录,脚本启动时需要通过SSH拉起远程进程;
- 解压Hadoop包到统一目录,比如/home/hadoop/hadoop-1.2.1;
- 编辑conf/hadoop-env.sh,把JAVA_HOME路径写进去,别依赖默认探测。
有多台机器时,只需完整配置主节点一份,再把整个目录拷贝到各从节点即可。环境变量、目录权限这些细节统一起来,后面排查问题能省一半时间。
5.2 mapred-site.xml配置逐项解读
核心配置全部集中在conf/mapred-site.xml。下面这份配置是我个人在实验环境里常用的模板,加上注释讲解每项含义。
<?xml version="1.0"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <name>mapred.job.tracker</name> <value>master:9001</value> <description>JobTracker的地址和端口,所有TaskTracker和客户端都靠这个地址找到它</description> </property> <property> <name>mapred.tasktracker.map.tasks.maximum</name> <value>2</value> <description>每台TaskTracker同时运行的Map槽位数量,单机内存小就保持默认</description> </property> <property> <name>mapred.tasktracker.reduce.tasks.maximum</name> <value>2</value> <description>每台TaskTracker同时运行的Reduce槽位数量</description> </property> <property> <name>mapred.reduce.slowstart.completed.maps</name> <value>0.7</value> <description>当70%的Map任务完成后,才开始调度Reduce任务</description> </property> <property> <name>mapred.local.dir</name> <value>/home/hadoop/hadoop-1.2.1/mapred/local</value> <description>中间结果本地目录,建议配置多块磁盘目录,用逗号分隔,分散IO压力</description> </property> <property> <name>mapred.system.dir</name> <value>/tmp/hadoop/mapred/system</value> <description>JobTracker保存作业相关数据的HDFS路径,注意需要是HDFS路径</description> </property> </configuration>需要强调一点:mapred.local.dir千万不要只配一个目录,生产经验是把多块磁盘的目录都写进去,轮询使用能明显分散Map溢写带来的本地IO压力。另外,这个目录如果残留上次运行的数据,节点重启时可能出现任务状态错乱,所以脚本启动前清空这个目录是老运维的常规操作。
5.3 启动服务与验证
启动顺序有讲究,老版本里推荐先启动HDFS,再启动MapReduce服务。
bin/start-dfs.sh bin/start-mapred.sh如果配置正确,主节点上执行jps能看到NameNode、JobTracker两个主进程,从节点上能看到DataNode、TaskTracker两个进程。JobTracker默认提供Web页面,地址是http://master:50030,可以直接在浏览器里看到集群中注册的TaskTracker数量、空闲槽位数、已提交作业列表和每个作业的任务进度。TaskTracker自己的页面在50060端口,能看到本机槽位占用情况、本地目录使用量、日志信息。
验证计算能力最直接的方法,是跑官方自带的WordCount示例:
hadoop fs -mkdir -p /input hadoop fs -put /home/hadoop/test.txt /input/ hadoop jar hadoop-examples-1.2.1.jar wordcount /input /output hadoop fs -cat /output/part-r-00000第一次跑通时,建议同时盯着JobTracker的Web界面,观察Map进度、Reduce进度、任务分配在本机还是远程机器,再对照日志目录里TaskTracker执行任务的过程。这一步做完,对两个组件的理解就不是纸面概念了。
5.4 日志排查与典型问题
老集群跑起来之后,日志才是第一排查工具。JobTracker日志在logs/hadoop-用户名-jobtracker-主机名.log,TaskTracker日志在logs/hadoop-用户名-tasktracker-主机名.log。下面三个问题是我在仿真环境里真实踩过的坑。
第一个问题是TaskTracker总是不上报可用槽位,界面上所有槽位显示为0。原因通常是本地目录权限错误或者磁盘空间不足,TaskTracker认为自己无法执行任务,就在心跳里上报0。翻开日志会看到初始化失败或磁盘检查不通过之类的记录,清理磁盘、修正目录权限即可恢复。
第二个问题是任务一直处于“Pending”状态不执行。多半是mapred.job.tracker地址配错,或者TaskTracker连不上JobTracker。检查方式很直接:在主节点用netstat查看9001端口是否监听,在从节点telnet主节点9001端口通不通。
第三个问题让人最恼火,作业失败报错提示找不到某个本地路径,但去检查时路径明明存在。这种情况几乎都是mapred.local.dir配置了绝对路径,但各节点Hadoop安装目录不一致,TaskTracker在相对路径下找不到文件。解决办法是统一所有节点Hadoop包的部署路径,或者直接把本地目录全部改成相同绝对路径。
6. 经典架构的谢幕与遗产:从JobTracker/TaskTracker到YARN
既然这套架构这么经典,为什么现在的新手几乎接触不到JobTracker和TaskTracker?因为它在生产环境中暴露出了几个想捂住都捂不住的大问题。
6.1 JobTracker/TaskTracker的四个致命短板
第一个是单点故障。整个集群的计算调度系于JobTracker一身,它一挂,所有计算任务瘫痪。虽然可以把JobTracker进程用脚本拉起做简单重启,但作业状态已经丢了,恢复时间很长。
第二个是调度和资源管理耦合太深。JobTracker既要管作业排队调度,又要管理每台机器上的资源分配,同时还要处理任务容错,所有状态都放在内存里,压力全集中在一个进程。
第三个是槽位机制太粗糙。不能用CPU、内存精确描述资源,只能数槽位,资源利用率很低。而且Map槽位和Reduce槽位互不相通,即使计算高峰期Map槽位占满、Reduce槽位空闲,Map任务也不可能借用Reduce槽位。
第四个是根本无法支撑多种计算框架。这套架构是MapReduce专用引擎,流式计算、图计算等其他框架想复用资源管理能力,完全没有接口。这也是后来YARN出现的根本动机:把资源管理从计算框架里抽离出来,让所有引擎共享一个通用的资源调度层。
6.2 与YARN核心概念的映射关系
把旧架构和YARN摆在一起对照,会发现演进并不是推倒重来,而是一次精准拆分。
| 旧架构组件 | 对应YARN组件 | 职责变化 |
|---|---|---|
| JobTracker | ResourceManager + ApplicationMaster | 前者只管全局资源分配,后者负责单个作业的任务协调 |
| TaskTracker | NodeManager | 从执行MapReduce任务变成通用容器管理进程,只负责启动和监控容器 |
| 槽位(Slot) | 容器(Container) | 从粗粒度计数单位变成带CPU、内存上限的资源单元 |
| 固定Map/Reduce进程 | 任意类型的ApplicationMaster | 计算框架不再绑定MapReduce,流计算、交互式查询都能跑 |
这个拆分最核心的价值在于,ApplicationMaster把原来堆积在JobTracker身上的作业管理逻辑吓到了每个作业自己头上。全局调度器只负责决定每份资源给谁用,至于这份资源拿到手之后怎么安排具体任务,由作业自己的Master进程说了算。集群规模一上来,这种去中心化的作业管理方式明显更扛得住。
6.3 旧架构在现代大数据面试与实战中的价值
那是不是说JobTracker和TaskTracker就完全没有学习价值了?恰恰相反。我在带新人时,一定会让他们先看一套旧版MapReduce的完整执行流程,再看YARN设计。
原因很简单:分布式系统的核心概念重叠度极高。心跳保活、主从拓扑、任务重试、数据本地性调度、慢任务推测执行,这些思想在Spark、Flink、甚至各种自研调度系统里都能看到影子。你在JobTracker里理解了“调度器根据心跳汇报的空闲资源决策任务下发”,再看YARN的ResourceManager和NodeManager交互、看Spark的Driver与Executor通信,全部是一脉相承的逻辑。
对正在准备面试的人来说,JobTracker、TaskTracker的职责区别,槽位机制的问题,JobTracker为什么是瓶颈,数据本地性是如何实现的,都是面试官特别喜欢追问的基础题。能把这些讲清楚,同时说出“这套设计后来如何演进成YARN的RM和NM”,会比只会背概念强很多。
我个人在实际带环境时最深的一个感受是:技术架构可以换代,但设计思想不会过时。Hadoop经典计算引擎虽然退出了生产舞台,它身上承载的主从调度、心跳容错、数据感知分配这些思路,仍然是理解整个大数据生态最顺手的教材。如果你有条件,我建议抽出半天时间把一套老版本环境搭起来,亲手观测一次任务从提交到完成的全过程。那种把概念落到实处的通透感,是看多少篇文档都换不来的。