原文地址:http://wwangcg.iteye.com/category/171858【good 也有其他方面的内容】
hadoop 源码分析(一) jobClient 提交到JobTracker
- 博客分类:
- hadoop
Hadoop 用了2年多了.从最初一起创业的11人20台服务器集群到后来独立搭建基于hadoop nutch的搜索引擎并商用化 到现在也2年了.这两年来应用了很多新技术也经历了很多,从数据仓库的Hive pig 到mapreduce的编码去解决算法或是etl的问题 等等都离不开hadoop.觉得用了2年多也到了该总结的时候了.故此想重新翻译hadoop 源码,按照不同的类方法的不同作用.也希望读到这篇博客的人和我一起 经历这一段岁月,一起讲hadoop的核心结合实用主义 传递给中国的开源软件使用者。
下图为 jobClient 提交到 Mapreduce 作业到JobTracker 核心逻辑。
1.JobProfile类:
2. JobSubmissionProtocol 接口为 JobClient 和JobTracker 共同执行的接口,因此它是一个可代理的接口
3. 调用 createRPCProxy() 通过远程RPC 调用实现动态代理 JobTracker 类的 submitJob 方法
- private static JobSubmissionProtocol createRPCProxy(InetSocketAddress addr,
- Configuration conf) throws IOException {
- return (JobSubmissionProtocol)[color=red][size=medium] RPC.getProxy[/size][/color](JobSubmissionProtocol.class,
- JobSubmissionProtocol.versionID, addr,
- UserGroupInformation.getCurrentUser(), conf,
- NetUtils.getSocketFactory(conf, JobSubmissionProtocol.class));
- }
打开RPC 类 的 getProxy 方法
- public static VersionedProtocol getProxy(
- Class<? extends VersionedProtocol> protocol,
- long clientVersion, InetSocketAddress addr, UserGroupInformation ticket,
- Configuration conf, SocketFactory factory, int rpcTimeout) throws IOException {
- if (UserGroupInformation.isSecurityEnabled()) {
- SaslRpcServer.init(conf);
- }
- VersionedProtocol proxy =
- (VersionedProtocol)[color=red][size=medium] Proxy.newProxyInstance[/size][/color](
- protocol.getClassLoader(), new Class[] { protocol },
- new Invoker(protocol, addr, ticket, conf, factory, rpcTimeout));
- long serverVersion = proxy.getProtocolVersion(protocol.getName(),
- clientVersion);
- if (serverVersion == clientVersion) {
- return proxy;
- } else {
- throw new VersionMismatch(protocol.getName(), clientVersion,
- serverVersion);
- }
- }
通过RPC 远程调用连接到服务端后.通过传入代理接口 获取到JobTracker 类
代理模式 就详细的讲了,如有必要 可翻看java代理模式
这里 getPoxy() 方法中调用 new Invoker(protocol, addr, ticket, conf, factory, rpcTimeout)) 通过new Invoker() 获得client.该cleintt 先判断ClientCache 是不是已经含有该client 如果有则加+1 如果没有 则new Client 生成一个 代码如下
- private synchronized Client getClient(Configuration conf,
- SocketFactory factory) {
- // Construct & cache client. The configuration is only used for timeout,
- // and Clients have connection pools. So we can either (a) lose some
- // connection pooling and leak sockets, or (b) use the same timeout for all
- // configurations. Since the IPC is usually intended globally, not
- // per-job, we choose (a).
- Client client = clients.get(factory);
- if (client == null) {
- client = new Client(ObjectWritable.class, conf, factory);
- clients.put(factory, client);
- } else {
- client.incCount();
- }
- return client;
- }
- /* 每次 +1 */
- synchronized void incCount() {
- refCount++;
- }
JobTracker 将其加入到job队列中
该过程 完成了 用户通过JobClient 像JobTracker 提交作业的流程,提交的过程中不是通过直接提交而是通过了rpc 通信 创建JobClient 代理 通过代理模式提交
Hadoop 通信机制采用自己编写的RPC. 相比于其他复杂的rpc框架着实清爽了许多.rpc在hadoop中扮演的角色是通信和数据传输在client和server端,以及datanode和namenode 心跳数据以及jobTracker 和taskTracker 通信
1. Client 与 server 端通信采用Writable 序列化形式.因此hadoop中信息的传递 必须继承自writable 接口,writable 接口有两个方法 write 和read
2. Client 端通过调用Call 方法,将消息序列化为writable 形式与server端通信
3. Client 调用sendPing() 到server端.每隔一定时间,Ping时间间隔通过ipc.ping.interval 配置
4. connection方法为多路复用.多个call请求公用一个call方法,通过addCall( ) 将call 加入hash 的call队列中,但是response则单独处理,call 队列 Hashtable<Integer, Call> calls = new Hashtable<Integer, Call>()
5. Server 端通过NIO方式将serveraddress bind到lister.
6. Reader为读入监听到的动作key 交给doRead 去读出来
其实hadoop的RPC 比较简单,无非就是通过wirtable 序列化 在client 和server 端传输数据.其中包括 心跳检测.client 传参数给服务端代理执行器方法等,jobClient 代理直接JobTracker的方法其中传参数的协议就是通过RPC 序列化参数传给服务端
- /** Get a connection from the pool, or create a new one and add it to the
- * pool. Connections to a given ConnectionId are reused. */
- private Connection getConnection(ConnectionId remoteId,
- Call call)
- throws IOException, InterruptedException {
- if (!running.get()) {
- // the client is stopped
- throw new IOException("The client is stopped");
- }
- Connection connection;
- /* we could avoid this allocation for each RPC by having a
- * connectionsId object and with set() method. We need to manage the
- * refs for keys in HashMap properly. For now its ok.
- */
- do {
- synchronized (connections) {
- connection = connections.get(remoteId);
- if (connection == null) {
- connection = new Connection(remoteId);
- connections.put(remoteId, connection);
- }
- }
- } while (!connection.addCall(call));
- //we don't invoke the method below inside "synchronized (connections)"
- //block above. The reason for that is if the server happens to be slow,
- //it will take longer to establish a connection and that will slow the
- //entire system down.
- // setupIOstreams 方法建立IO通道.client和server 建立链接
- connection.setupIOstreams();
- return connection;
- }
1. 进入main方法:
- //执行startTracker 方法
- JobTracker tracker = startTracker(new JobConf());
- //执行offerService()方法
- tracker.offerService();
3.startTracker()方法中
- // new JobTracker 方法
- result = new JobTracker(conf, identifier);
- result.taskScheduler.setTaskTrackerManager(result);
2. JobTracker()构造方法中初始化信息
(1) static constants 变量
(2) 加载调度器 默认为FIFO 调度
/
- / Create the scheduler
- Class<? extends TaskScheduler> schedulerClass
- = conf.getClass("mapred.jobtracker.taskScheduler",
- JobQueueTaskScheduler.class, TaskScheduler.class);
- taskScheduler=(TaskScheduler)ReflectionUtils.newInstance(schedulerClass, conf);
taskScheduler 默认的执行类为JobQueueTaskScheduler ,当启动JobTracker 的时候 调用了 tracker.offerService();该方法执行了父类的 start()方法.该start()方法为 JobQueueTaskScheduler 的start方法:
- @Override
- public synchronized void start() throws IOException {
- super.start();
- taskTrackerManager.addJobInProgressListener(jobQueueJobInProgressListener);
- eagerTaskInitializationListener.setTaskTrackerManager(taskTrackerManager);
- eagerTaskInitializationListener.start();
- taskTrackerManager.addJobInProgressListener(
- eagerTaskInitializationListener);
- }
在这个方法中调用了addJobInProgressListener()将lister 加入到了 jobTracker中,这个过程很绕,可通过下面的流程图梳理清楚
3. 启动jettyServer
- infoServer.addServlet("reducegraph", "/taskgraph", TaskGraphServlet.class);
- infoServer.start();
JobTracker 提交job
1. jobClient()通过代理 调用JobTracker的submit方法提交job
2. submitJob方法中 调用了 addJob()将job添加到job队列中,等待执行
3. addJob方法:
- private synchronized JobStatus addJob(JobID jobId, JobInProgress job)
- throws IOException {
- totalSubmissions++;
- synchronized (jobs) {
- synchronized (taskScheduler) {
- jobs.put(job.getProfile().getJobID(), job);
- // jobInProgressListeners list 在start jobTracker 的时候,在JobQueueTaskScheduler 的start方法中初始化加入了两个listener :
- eagerTaskInitializationListener 和jobQueueJobInProgressListener
- for (JobInProgressListener listener : jobInProgressListeners) {
- listener.jobAdded(job);
- }
- }
- }
- myInstrumentation.submitJob(job.getJobConf(), jobId);
- job.getQueueMetrics().submitJob(job.getJobConf(), jobId);
- LOG.info("Job " + jobId + " added successfully for user '"
- + job.getJobConf().getUser() + "' to queue '"
- + job.getJobConf().getQueueName() + "'");
- AuditLogger.logSuccess(job.getUser(),
- Operation.SUBMIT_JOB.name(), jobId.toString());
- return job.getStatus();
- }
TaskScheduler mapreduce的任务调度器类,当jobClient 提交一个job 给JobTracker 的时候.JobTracker 接受taskTracker 的心跳.心跳信息含有空闲的slot信息等.JobTracker 则通过调用TaskScheduler 的assignTasks()方法类给报告心跳信息中含有空闲的slots信息的taskTracker 分布任务、
TaskScheduler 类为hadoop的 调度器的抽象类。默认继承它作为hadoop调度器的方式为FIFO,当然也有Capacity 和Fair等其他调度器,也可以自己编写符合特定场景所需要的调度器.通过继承TaskScheduler 类即可完成该功能、
下面就 FIFO 调度器进行简单的说明:
JobQueueTaskScheduler 类为FIFO 调度器的实现类.
1. 首先JobQueueTaskSchduler 注册两个监听器类:
JobQueueJobInProgressListener jobQueueJobInProgressListener;
EagerTaskInitializationListener eagerTaskInitializationListener;
JobQueueJobInProgressListener 维护一个job的queue ,其中JobSchedulingInfo 中包含job调度的信息:priority,startTime,id.以及 jobAdd update 等操作jobqueue的方法
EagerTaskInitializationListener 初始化job的listener ,这里所谓的初始化不是初始化job的属性信息,而是针对已经存在jobqueue中 即将被执行job的初始化,
- class JobInitManager implements Runnable {
- public void run() {
- JobInProgress job = null;
- while (true) {
- try {
- synchronized (jobInitQueue) {
- while (jobInitQueue.isEmpty()) {
- jobInitQueue.wait();
- }
- job = jobInitQueue.remove(0);
- }
- threadPool.execute(new InitJob(job));
- } catch (InterruptedException t) {
- LOG.info("JobInitManagerThread interrupted.");
- break;
- }
- }
- LOG.info("Shutting down thread pool");
- threadPool.shutdownNow();
- }
- }
resortInitQueue 按照priority 和starttime 来排序
jobRemoved()
jobUpdated()
jobStateChanged()当priority或是starttime被改变的时候则重新调用resortInitQueue()重新排序
- public EagerTaskInitializationListener(Configuration conf) {
- numThreads = conf.getInt("mapred.jobinit.threads", DEFAULT_NUM_THREADS);
- threadPool = Executors.newFixedThreadPool(numThreads);
- }
在JobTracker 启动的时候 创建 mapred.jobinit.threads 改数量的线程去监控jobqueue.当jobqueue 中含有job的时候 则initjob
- class InitJob implements Runnable {
- private JobInProgress job;
- public InitJob(JobInProgress job) {
- this.job = job;
- }
- //调用run方法 回调TaskTrackerManager
- public void run() {
- ttm.initJob(job);
- }
- }
调度其中核心逻辑在assignTasks()方法中
下面分析分析 FIFO模式下的 assignTasks()
- @Override
- public synchronized List<Task> assignTasks(TaskTracker taskTracker)
- throws IOException {
- TaskTrackerStatus taskTrackerStatus = taskTracker.getStatus();
- ClusterStatus clusterStatus = taskTrackerManager.getClusterStatus();
- //获取集群中TaskTracker 总数
- final int numTaskTrackers = clusterStatus.getTaskTrackers();
- //集群中map slot总数
- final int clusterMapCapacity = clusterStatus.getMaxMapTasks();
- //集群中reduce slot 总数
- final int clusterReduceCapacity = clusterStatus.getMaxReduceTasks();
- Collection<JobInProgress> jobQueue =
- jobQueueJobInProgressListener.getJobQueue();
- //
- // Get map + reduce counts for the current tracker.
- //
- //当前的taskTracker 上map slot 总数
- final int trackerMapCapacity = taskTrackerStatus.getMaxMapSlots();
- //当前的taskTracker 上reduce slot 总数
- final int trackerReduceCapacity = taskTrackerStatus.getMaxReduceSlots();
- //当前的taskTracker上正在运行的 map数目
- final int trackerRunningMaps = taskTrackerStatus.countMapTasks();
- //当前的taskTracker上正在运行的 reduce数目
- final int trackerRunningReduces = taskTrackerStatus.countReduceTasks();
- // Assigned tasks
- List<Task> assignedTasks = new ArrayList<Task>();
- //
- // Compute (running + pending) map and reduce task numbers across pool
- //
- //该taskTracker上剩余的reduce数
- int remainingReduceLoad = 0;
- //该taskTracker 剩余的map数
- int remainingMapLoad = 0;
- synchronized (jobQueue) {
- for (JobInProgress job : jobQueue) {
- if (job.getStatus().getRunState() == JobStatus.RUNNING) {
- remainingMapLoad += (job.desiredMaps() - job.finishedMaps());
- if (job.scheduleReduces()) {
- remainingReduceLoad +=
- (job.desiredReduces() - job.finishedReduces());
- }
- }
- }
- }
- // Compute the 'load factor' for maps and reduces
- //map因子
- double mapLoadFactor = 0.0;
- if (clusterMapCapacity > 0) {
- mapLoadFactor = (double)remainingMapLoad / clusterMapCapacity;
- }
- double reduceLoadFactor = 0.0;
- if (clusterReduceCapacity > 0) {
- reduceLoadFactor = (double)remainingReduceLoad / clusterReduceCapacity;
- }
- //
- // In the below steps, we allocate first map tasks (if appropriate),
- // and then reduce tasks if appropriate. We go through all jobs
- // in order of job arrival; jobs only get serviced if their
- // predecessors are serviced, too.
- //
- //
- // We assign tasks to the current taskTracker if the given machine
- // has a workload that's less than the maximum load of that kind of
- // task.
- // However, if the cluster is close to getting loaded i.e. we don't
- // have enough _padding_ for speculative executions etc., we only
- // schedule the "highest priority" task i.e. the task from the job
- // with the highest priority.
- //
- final int trackerCurrentMapCapacity =
- Math.min((int)Math.ceil(mapLoadFactor * trackerMapCapacity),
- trackerMapCapacity);
- int availableMapSlots = trackerCurrentMapCapacity - trackerRunningMaps;
- boolean exceededMapPadding = false;
- if (availableMapSlots > 0) {
- exceededMapPadding =
- exceededPadding(true, clusterStatus, trackerMapCapacity);
- }
- int numLocalMaps = 0;
- int numNonLocalMaps = 0;
- scheduleMaps:
- for (int i=0; i < availableMapSlots; ++i) {
- synchronized (jobQueue) {
- for (JobInProgress job : jobQueue) {
- if (job.getStatus().getRunState() != JobStatus.RUNNING) {
- continue;
- }
- Task t = null;
- // Try to schedule a node-local or rack-local Map task
- t =
- job.obtainNewNodeOrRackLocalMapTask(taskTrackerStatus,
- numTaskTrackers, taskTrackerManager.getNumberOfUniqueHosts());
- if (t != null) {
- assignedTasks.add(t);
- ++numLocalMaps;
- // Don't assign map tasks to the hilt!
- // Leave some free slots in the cluster for future task-failures,
- // speculative tasks etc. beyond the highest priority job
- if (exceededMapPadding) {
- break scheduleMaps;
- }
- // Try all jobs again for the next Map task
- break;
- }
- // Try to schedule a node-local or rack-local Map task
- t =
- job.obtainNewNonLocalMapTask(taskTrackerStatus, numTaskTrackers,
- taskTrackerManager.getNumberOfUniqueHosts());
- if (t != null) {
- assignedTasks.add(t);
- ++numNonLocalMaps;
- // We assign at most 1 off-switch or speculative task
- // This is to prevent TaskTrackers from stealing local-tasks
- // from other TaskTrackers.
- break scheduleMaps;
- }
- }
- }
- }
- int assignedMaps = assignedTasks.size();
- //
- // Same thing, but for reduce tasks
- // However we _never_ assign more than 1 reduce task per heartbeat
- //
- final int trackerCurrentReduceCapacity =
- Math.min((int)Math.ceil(reduceLoadFactor * trackerReduceCapacity),
- trackerReduceCapacity);
- final int availableReduceSlots =
- Math.min((trackerCurrentReduceCapacity - trackerRunningReduces), 1);
- boolean exceededReducePadding = false;
- if (availableReduceSlots > 0) {
- exceededReducePadding = exceededPadding(false, clusterStatus,
- trackerReduceCapacity);
- synchronized (jobQueue) {
- for (JobInProgress job : jobQueue) {
- if (job.getStatus().getRunState() != JobStatus.RUNNING ||
- job.numReduceTasks == 0) {
- continue;
- }
- Task t =
- job.obtainNewReduceTask(taskTrackerStatus, numTaskTrackers,
- taskTrackerManager.getNumberOfUniqueHosts()
- );
- if (t != null) {
- assignedTasks.add(t);
- break;
- }
- // Don't assign reduce tasks to the hilt!
- // Leave some free slots in the cluster for future task-failures,
- // speculative tasks etc. beyond the highest priority job
- if (exceededReducePadding) {
- break;
- }
- }
- }
- }
- if (LOG.isDebugEnabled()) {
- LOG.debug("Task assignments for " + taskTrackerStatus.getTrackerName() + " --> " +
- "[" + mapLoadFactor + ", " + trackerMapCapacity + ", " +
- trackerCurrentMapCapacity + ", " + trackerRunningMaps + "] -> [" +
- (trackerCurrentMapCapacity - trackerRunningMaps) + ", " +
- assignedMaps + " (" + numLocalMaps + ", " + numNonLocalMaps +
- ")] [" + reduceLoadFactor + ", " + trackerReduceCapacity + ", " +
- trackerCurrentReduceCapacity + "," + trackerRunningReduces +
- "] -> [" + (trackerCurrentReduceCapacity - trackerRunningReduces) +
- ", " + (assignedTasks.size()-assignedMaps) + "]");
- }
- return assignedTasks;
- }
上面方法中真正执行task的方法为:
obtainNewNodeOrRackLocalMapTask 和obtainNewNonLocalMapTask
下一张详细的分析这两个方法
1. 启动 TaskTracker ,执行main方法 new TaskTracker(conf) 启动taskTracker
2. taskTrack 构造方法初始化变量
mapred.tasktracker.map.tasks.maximum taskTracker 可launch 的最大map数 默认是2
mapred.tasktracker.map.tasks.maximum taskTracker 可launch 的最大reduce数 默认是2
mapred.disk.healthChecker.interval 磁盘健康度检查时间间隔 mills 默认是60*1000
构造rpc 链接jobTACKER
tasktracker.http.threads taskTracker 工作线程数,默认是40 ,copy数据的线程数
initialize(); 构造方法,该方法是一个独立于taskTracker的方法,可循环调用,在taskTracker 处于关闭状态时 仍可用
该方法主要用户构造一些 运行时目录 和心跳RPC 信息初始化分布式缓存开发map任务监听线程 初始化new TaskLauncher() 启动map 和reduce 任务抓取线程
- synchronized void initialize() throws IOException, InterruptedException {
- this.fConf = new JobConf(originalConf);
- // 绑定地址.taskTracker 的地址 绑定到rpc中 为传送心跳信息做准备
- String address =
- NetUtils.getServerAddress(fConf,
- "mapred.task.tracker.report.bindAddress",
- "mapred.task.tracker.report.port",
- "mapred.task.tracker.report.address");
- InetSocketAddress socAddr = NetUtils.createSocketAddr(address);
- String bindAddress = socAddr.getHostName();
- int tmpPort = socAddr.getPort();
- //初始化jvm 管理类
- this.jvmManager = new JvmManager(this);
- // RPC 初始化
- int max = maxMapSlots > maxReduceSlots ?
- maxMapSlots : maxReduceSlots;
- //set the num handlers to max*2 since canCommit may wait for the duration
- //of a heartbeat RPC
- //此处taskTracker 汇报的 处理任务的slot 在实际的基础上*2,因为在心跳汇报的阶段传输这段时间会空出来一部分slot.在新的heartbeat 过来的时候 有2倍的slot处理能力
- this.taskReportServer = RPC.getServer(this, bindAddress,
- tmpPort, 2 * max, false, this.fConf, this.jobTokenSecretManager);
- this.taskReportServer.start();
- // 初始化分布式缓存,在写mr代码的时候 讲一个文件写入DistributedCache 的时候, DistributedCache 在这个位置进行初始化
- this.distributedCacheManager = new TrackerDistributedCacheManager(
- this.fConf, taskController);
- // start the thread that will fetch map task completion events
- //在该位置启动 map和reduce任务的处理线程
- this.mapEventsFetcher = new MapEventsFetcherThread();
- mapEventsFetcher.setDaemon(true);
- mapEventsFetcher.setName(
- "Map-events fetcher for all reduce tasks " + "on " +
- taskTrackerName);
- mapEventsFetcher.start();
- //这里 初始化了两个类TaskLauncher reduce 和map 这两个类是具体的 生成map和reduce 的任务类.
- mapLauncher = new TaskLauncher(TaskType.MAP, maxMapSlots);
- reduceLauncher = new TaskLauncher(TaskType.REDUCE, maxReduceSlots);
- mapLauncher.start();
- reduceLauncher.start();
下面详细的讲一下TaskLauncher类,该类为taskTracker 类的内部类,在启动taskTracker 的时候 通过独立的initialize()方法启动.
该类是一个线程类.通过addToTaskQueue() 方法将新的任务添加到 tasksToLaunch list (List<TaskInProgress> tasksToLaunch)中,这个list 很重要,jobTracker将需要job 通过assginTaks 将需要执行的task 通过心跳信息 传给taskTracker,taskTracker 的run()方法调用offerService()解析心跳信息,将解析得来的task执行信息 添加到这个list中, 然后启动run方法 时刻去查看 tasksToLaunch list中是不是有新的 任务放进来.r如果有则去执行,如果没有则调用tasksToLaunch.wait(); 等待.调用startNewTask 方法调用launchTaskForJob() 通过调用 launchTask 去执行map 和reduce任务, launchTask 要判断任务的状态. UNASSIGNED FAILED_UNCLEAN KILLED_UNCLEAN RUNNING
TaskTracker 主要是通过监听jobTracker 通过心跳信息传过来的task任务放在 task的队中 去执行task.
在这个过程中 还会有一些 例如 numFreeSlots 的判断 ,tip 完全是 同步的,等等
jobTracker 通过调用JobQueueTaskScheduler的assginTasks()方法 分配task,两种方式 生成tak
obtainNewNodeOrRackLocalMapTask 和obtainNewNonLocalMapTask
obtainNewNodeOrRackLocalMapTask 即为hadoop 机架感知功能,调度的时候根据location 因素去分配taskTracker
obtainNewNodeOrRackLocalMapTask 非机架感知
hadoop 性能调优
环境:
4台suse 各 4G 内存 1T硬盘 4核cpu
3台 redhat 各 2G内存 500G 硬盘 双核cpu
由于没有真正意义上的服务器,所以当运行大量map reduce任务的时候 map 运行速度还可以接受 但reduce 速度 特别慢,所以开发
对集群进行调优。
hadoop集群调优分两个方面,map和reduce
map调优:
map 任务执行会产生中间数据,但这些中间结果并没有直接IO到磁盘上,而是先存储在缓存(buffer)中,并在缓存中进行一些预排序来优化整个map的性能,该存储map中间数据的缓存默认大小为100M,由io.sort.mb 参数指定.这个大小可以根据需要调整。当map任务产生了非常大的中间数据时可以适当调大该参数,使缓存能容纳更多的map中间数据,而不至于大频率的IO磁盘,当系统性能的瓶颈在磁盘IO的速度上(由于我的是pc 机,因此磁盘读写速度很慢,相信大多数的人都是这样的情况,可以交流),可以适当的调大此参数来减少频繁的IO带来的性能障碍.
由于map任务运行时中间结果首先存储在缓存中,但是不是当整个缓存被填满时才将其写入磁盘,这样会增加map任务的等待,所以默认当 缓存的使用量达到80%(或0.8)的时候就开始写入磁盘,这个过程叫做spill(也叫做磁盘溢出),进行spill的缓存大小可以通过io.sort.spill.percent 参数调整,这个参数可以影响spill的频率.进而可以影响IO的频率.当map任务计算成功完成之后(也可以不成功单个的map)如果map任务有输出.则会产生多个spill。这些文件就是map的输出结果,但
是此时map任务虽然产生了输出结果,但是切记此时map任务仍然没有退出作业,接下来map必须将些spill进行合并,这个过程叫做merge, merge过程是并行处理spill的,每次并行多少个spill是由参数io.sort.factor指定的默认为10个.但是当spill的数量非常大的时候,merge一次并行运行的spill仍然为10个,这样仍然会频繁的IO处理,因此适当的调大每次并行处理的spill数有利于减少merge数因此可以影响map的性能。当map输出中间结果的时候也可以配置压缩,这个在前面的文章中我提到过,这里不再赘述了.
reduce调优:
reduce 运行阶段分为shuflle(copy) sort reduce, shuffle 阶段为reduce 全面拷贝map任务成功结束之后产生的中间结果,如果上面map任务用了压缩的方式,那么reduce 将map任务中间结果拷贝过来的时候首先要做的第一件事情就是解压缩,这一切是在reduce的 缓存中做的,当然也会占用一部分cpu,但是应该不会cpu的性能有影响,我的pc 没有发现cpu 因为这个过程被占用的 过多.为了优化reduce的执行时间,reduce也不是等到所有的map数据都拷贝过来的时候才开始运行reduce任务,而是当job执行完第一个map执行完才开始运行的.reduce 在shuffle阶段 实际上是从不同的并且已经完成的map上去下载属于自己的这个reduce,由于map任务数很多,所有这个copy过程是并行的,既同时有许多个reduce取拷贝map.这个并行 的线程是通过mapred.reduce.parallel.copies 参数指定的默认为5个,也就是说无论map的任务数是多少个,默认情况下一次只能有5个reduce的线程去拷贝map任务的执行结果.所以当map任务数很多的情况下可以适当的调整该参数,这样可以让reduce快速的获得运行数据来完成任务。reduce线程在下载map数据的时候也可能因为各种各样的原因,网络原因,系统原因,存储该map数据所在的datannode 发生了故障,这种情况下reduce任务将得不到该
datanode上的数据了,同时该 download thread 会尝试从别的datanode下载,可以通过mapred.reduce.copy.backoff (默认为30秒)来调整下载线程的下载时间,如果网络不好的集群可以通过增加该参数的值来增加下载时间,以免因为下载时间过长reduce将该线程判断为下载失败。
reduce 下载线程在map结果下载到本地时,由于是多线程并行下载,所以也需要对下载回来的数据进行merge,所以map阶段设置的io.sort.factor 也同样会影响这个reduce的。同map也一样,reduce 将从map下载来的数据也立刻写入磁盘,而是当缓冲区被占用到一定的阀值的时候才写入磁盘,reduce的这个大小mapred.job.shuffle.input.buffer.percent(默认为0.7)来指定,同map一样 该缓冲区大小也不是等到完全被占满的时候才写入磁盘而是默认当当完成0.66的时候就开始写磁盘操作,该参数是通过mapred.job.shuffle.merge.percent 指定的。当reduce 开始进行计算的时候通过:mapred.job.reduce.input.buffer.percent 来指定需要多少的内存百分比来作为reduce读已经sort好的数据的buffer百分比,默认为0.当默认时reduce是全部从磁盘开始读处理数
据
总结:总之map reduce调优的一个原则就是 给shuffle 尽量多的内存,给中间结果多些内存,给并行任务数调大(当然一些都得因人而异,根据自己集群及网络的实际情况来调优,调优的时候可以根据工具ganglia来查看效果)