原文难以找到,请原作者和读者见谅
当 hadoop集群规模很小的时候,比如100台,200台,300台的时候,可能 一切看上去都很好,jobtracker分配task到计算槽位非常高效,集群的槽位资源在计算多的时候基本能够打满,所以集群的利用率非常高,一切看上 去都运转良好。在这种情况下,当计算越来越多,提交作业的人越来越多,集群的计算槽位逐渐无法满足需求的时候,大多数人第一个想到的解决办法就是:加机 器。的确,hadoop设计上的优秀和可扩展性可以方便的让集群管理员对集群增删机器,所以当集群计算资源紧缺,又有空闲的机器可用时,集群管理员很容易 想到给集群加机器来解决这个问题。当加入的机器不多,比如给原本300台的集群再加50台,可能仍然没有问题,集群的计算槽位增多 了,jobtracker能调度的槽位也多了,集群里能并行的map数和reduce数也增多了。但是,当集群规模扩大到一定程度,比如1200台,再往 上加机器,会发现计算作业没有增多,本应该运行的更快的作业并没有比预期的快,有时候甚至跟加机器前跑的一样,集群的槽位是变多了,但是被调度用来跑 task的槽位总是用不满,jobtracker的cpu使用率始终保持100%,但是集群的计算槽位总是达不到饱和,即使集群在最繁忙的时候,槽位的使 用率也只能达到比如60%,每一个时刻总有一部分的计算槽位是空闲的但是无法往上分配task任务。这就不得不让人怀疑hadoop的程序实现到了这个规 模的时候,触发了某些瓶颈,造成这种现象。
发 现了这种情况后,在一个1100台机器的集群jobtracker上每隔一秒钟输出一次的 jobtracker进程jstack日志,持续输出一分钟,只要能获取jobtracker内每秒钟都在处理哪些调用,哪些是被BLOCK住的,就能发 现jobtracker中的调用瓶颈了。根据所有60份 jstack日志的统计可以看出如下现象:
- 从所有jstack日志中,每一秒钟由于 jobtracker对象锁而被BLOCK住的heartbeat调用有 41 ~ 42次 , 而由于tasktracker汇报心跳的时间间隔是jobtracker根据集群 规模计算出来的,在我的hadoop员jobtracker程序中计算方法为:ceil(cluster_size / 50),所以jobtracker计算出来的tasktracker平均心跳间隔为 : ceil(1100 / 50) = 22秒钟 。而按照集群现有的规模1100台机 器,1100 / 21 = 52 次 。这就说明,平均每一秒钟会有52次左右的 tasktracker的heartbeat调用发送到jobtracker,而平均每秒钟被jobtracker对象锁BLOCK住的为42次。因此在 现有集群规模和计算规模下, 平均每秒jobtracker只能处理10个heartbeat , 也就是为 10台 tasktracker分配task任务,而其他的都被BLOCK住了。
- 同 样,每个tasktracker上都会有一个 MapEventsFetcherThread来从jobtracker获取map完成的信 息,以决定是否获取map完成后的output数据,这最终会调用jobtracker中的getTaskCompletionEvents2方法,该方 法也是对jobtracker加同步锁的方法,从jstack日志统计,平均没秒钟有 56次 getTaskCompletionEvents 的调用由于jobtracker的对象锁原因被BLOCK住,造成的后果是这些调用所来自的tasktracker上的reduce task无法开始shuffle数据,task运行进程被托慢。
- 同 样,jobtracker中还有多个方法调用 都存在该对象的对象锁问题,造成jobtracker本身的scalability无法提高, 而如果jobtracker的这种synchronized方法中还涉及到hdfs文件的读写访问,效率就会更加低,甚至会出现jobtracker对象 锁由于hdfs访问发生问题而无法释放,导致jobtracker无法正常调度的问题,前几天出现的tasktracker集体下线经检查也非常有可能就 是这种原因造成的。
所以,jobtracker本身的scalability存在严重的性能瓶颈,这肯定是 jobtracker目前调度任务低效,槽位无法用满的原因之一。
同 时,有些jobtracker的synchronized方法内部还会有hfds文件的读写操 作,一旦某个时刻hdfs发生异常,而此时刚好jobtracker进入到这种调用(如submitJob),那么就会导致jobtracker在被 synchronized以后,等待hdfs访问的返回,这中间的时间jobtracker无法处理heartbeat,也就无法调度task任务,如果 tasktracker自动下线时间比较短(默认为10分钟),比如3分钟,甚至有可能发生tasktracker大批量自动下线的情况。
所 以从cpu相关的指标来 看,jobtracker的cpu在平常的调度中单核一直是处于99%的状态,且jobtracker并没有把多核用包合,很多 的时候都block在了一个核上,cpu的使用效率不高。这也是制约集群扩容的一个非常大的瓶颈所在,如果集群继续扩大到2000左右,那么 jobtracker每秒被BLOCK住的heartbeat和getTaskCompletionEvents2以及其他同步锁操作会更多,调度的效率 会更加底下,介时可能更多的计算节点的加入意义就不大了。
知 道了原因之后,接下来就该考虑如何解决。要解决jobtracker的scalability 问题,最大的修改之处就在于JobTracker.java类中许多类方法的synchronized加锁力度过大,有的调用甚至不需要进行同步,最严重 的地方为两处,就是上面提到的:
- JobTracker.submitJob
- JobTracker.getTaskCompletionEvents
这 两处调用中不仅对jobtracker加锁, 而且还有hdfs文件的读写访问,最容易造成jobtracker的使用率下降甚至jobtracker服务延迟。而submigJob中需要对 JobInProgress进行初始化,该初始化中涉及到了hdfs文件的读写,效率很慢,对jobtrakcer的加锁时间就变得很长。
另 外,还有 getMapTaskReports(), getReduceTaskReports(), getCleanupTaskReports(), getSetupTaskReports(),这些调用,当job还没有被init的时候,是无须进行接下来的操作的,由于这些方法也都是 jobtracker的synchronized方法,所以当job != null时,增加对job.inited()的判断也能够提高jobtracker的同步锁效率。
- 将这些方法的锁力度细化
- 去掉某些不必要的同步锁
- 将存在hdfs访问的操作移出 jobtracker的同步锁之外
通过这一系列的优化,jobtracker的调度效率应该可以大大提高,具体对比数据有待进一步 测试。jobtracker的调度效率提高,意味这可以为集群加入更多的机器而不会造成资源浪费,这可都是成本啊