hadoop 學習

Hadoop 的文件系統,最重要是 FileSystem 類,以及它的兩個子類 LocalFileSystem 和 DistributedFileSystem。 這裏先分析 FileSystem。
抽象類 FileSystem,提高了一系列對文件/目錄操作的接口,還有一些輔助方法。分別說明一下:
1. open,create,delete,rename等,非abstract,部分返回 FSDataOutputStream,作爲流進行處理。
2. openRaw,createRaw,renameRaw,deleteRaw等,abstract,部分返回 FSInputStream,可以隨機訪問。
3. lock,release,copyFromLocalFile,moveFromLocalFile,copyToLocalFile 等abstract method,提供便利作用,從方法命名可以看出作用。
特別說明,Hadoop的文件系統,每個文件都有一個checksum,一個crc文件。因此FileSystem裏面的部分代碼對此進行了特別的處理,比如 rename。
LocalFileSystem 和 DistributedFileSystem,理應對用戶透明,這裏不多做分析,和 FSDataInputStream,FSInputStream 結合一起說明一下。
查看兩個子類的 getFileCacheHints 方法,可以看到 LocalFileSystem 是使用'localhost'來命名,這裏暫且估計兩個FileSystem都是通過網絡進行數據通訊,一個是Internet,一個是Intranet。
LocalFileSystem 裏面有兩個內部類 LocalFSFileInputStream和LocalFSFileOutputStream,查看代碼可以看到它是使用 FileChannel進行操作的。另外 lock和release 兩個方法使用了TreeMap來保存文件和對應的鎖。
DistributedFileSystem 代碼量少於 LocalFileSystem,但是更加複雜,它裏面使用了 DFSClient 來進行分佈式文件系統的操作:
    public DistributedFileSystem(InetSocketAddress namenode, Configuration conf) throws IOException
    {
      super(conf);
      this.dfs = new DFSClient(namenode, conf);
      this.name = namenode.getHostName() + ":" + namenode.getPort();
    }
DFSClient 類接收一個InetSocketAddress 和Configuration 作爲輸入,對網絡傳輸細節進行了封裝。DistributedFileSystem中絕大多數方法都是調用DFSClient進行處理,它只是一個 Warpper。下面着重分析DFSClient。
DFSClient中,主要使用RPC來進行網絡的通訊,而不是直接在內部使用Socket。如果要詳細瞭解傳輸細節,可以查看 org.apache.hadoop.ipc 這個包裏面的3個Class。
DFSClient 中的路徑,基本上都是UTF8類型,而非String,在DistributedFileSystem中,通過getPath和getDFSPath來轉換,這樣做可以保證路徑格式的標準和數據傳輸的一致性。
DFSClient 中的大多數方法,也是直接委託ClientProtocol類型的namenode來執行,這裏主要分析其它方法。
LeaseChecker 內部類。一個守護線程,定期對namenode進行renewLease操作,註釋說明:
Client programs can cause stateful changes in the NameNode that affect other clients. A client may obtain a file and neither abandon nor complete it. A client might hold a series of locks that prevent other clients from proceeding. Clearly, it would be bad if a client held a bunch of locks that it never gave up. This can happen easily if the client dies unexpectedly. So, the NameNode will revoke the locks and live file-creates for clients that it thinks have died. A client tells the NameNode that it is still alive by periodically calling renewLease(). If a certain amount of time passes since the last call to renewLease(), the NameNode assumes the client has died.
作用是對client進行心跳監測,若client掛掉了,執行解鎖操作。
DFSInputStream 和 DFSOutputStream,比LocalFileSystem裏面的更爲複雜,也是通過 ClientProtocol 進行操作,裏面使用到了 org.apache.hadoop.dfs 包中的數據結構,如DataNode,Block等,這裏不對這些細節進行分析。

對FileSystem的分析(1)到此結束,個人感覺它的封裝還是做的不錯的,從Nutch項目分離出來後,比原先更爲清晰。 下面就接着進行MapReduce的第二部分分析,從MapReduce如何進行分佈式
 

###############################################################################

之前的MapReduce Demo只能在一臺機器上運行,現在是時候讓它分佈式運行了。在對MapReduce的運行流程和FileSystem進行了簡單研究之後,現在嘗試從配置着手,看看怎樣讓Hadoop在兩臺機器上面同時運行MapReduce。
首先看回這裏
      String tracker = conf.get("mapred.job.tracker", "local");
      if ("local".equals(tracker)) {
        this.jobSubmitClient = new LocalJobRunner(conf);
      } else {
        this.jobSubmitClient = (JobSubmissionProtocol)
          RPC.getProxy(JobSubmissionProtocol.class,
                       JobTracker.getAddress(conf), conf);
      }
當tracker地址不爲local,則tracker爲Remote Client的 JobTracker 類,這裏重點分析。
JobTracker有一個main函數,註釋顯示它僅僅用於調試,正常情況是作爲DFS Namenode進程的一部分來運行。不過這裏我們可以先從它着手開始分析。
          tracker = new JobTracker(conf);   //構造
構造函數先獲取一堆常量的值,然後清空'systemDir',接着啓動RPC服務器。
        InetSocketAddress addr = getAddress(conf);
        this.localMachine = addr.getHostName();
        this.port = addr.getPort();
        this.interTrackerServer = RPC.getServer(this, addr.getPort(), 10, false, conf);
        this.interTrackerServer.start();
啓動TrackInfoServer:
        this.infoPort = conf.getInt("mapred.job.tracker.info.port", 50030);
        this.infoServer = new JobTrackerInfoServer(this, infoPort);
        this.infoServer.start();
TrackInfoServer 提供了通過HTTP方式獲取JobTracker信息的方式,可以方便用於監測工作任務的進度。
啓動三個守護線程:
        new Thread(this.expireTrackers).start();  //Used to expire TaskTrackers that have gone down
        new Thread(this.retireJobs).start();  //Used to remove old finished Jobs that have been around for too long
        new Thread(this.initJobs).start();  //Used to init new jobs that have just been created
三個線程的用處已經註釋,這裏不作分析。下面開始分析 JobTracker.submitJob()
之前已經分析過 LocalJobRunner.submitJob(),它實例化內部類Job,在裏面實現MapReduce流程。JobTracker就複雜一些,它實例化 JobInProgress,然後將這個Job提交到隊列:
        JobInProgress job = new JobInProgress(jobFile, this, this.conf);
        synchronized (jobs) {
            synchronized (jobsByArrival) {
                synchronized (jobInitQueue) {
                    jobs.put(job.getProfile().getJobId(), job);
                    jobsByArrival.add(job);
                    jobInitQueue.add(job);
                    jobInitQueue.notifyAll();
                }
            }
        }
此時RetireJobs線程開始處理超時和出錯的Job,JobInitThread線程初始化工作任務: job.initTasks();
開始分析 JobInProgress
在構造函數中,Tracker從發起端的DFS獲取任務文件(xml和jar),然後保存到本地目錄下面
        JobConf default_job_conf = new JobConf(default_conf);
        this.localJobFile = default_job_conf.getLocalFile(JobTracker.SUBDIR,
            jobid + ".xml");
        this.localJarFile = default_job_conf.getLocalFile(JobTracker.SUBDIR,
            jobid + ".jar");
        FileSystem fs = FileSystem.get(default_conf);
        fs.copyToLocalFile(new File(jobFile), localJobFile);

        conf = new JobConf(localJobFile);
        this.profile = new JobProfile(conf.getUser(), jobid, jobFile, url,
                                      conf.getJobName());
        String jarFile = conf.getJar();
        if (jarFile != null) {
          fs.copyToLocalFile(new File(jarFile), localJarFile);
          conf.setJar(localJarFile.getCanonicalPath());
        }

這裏要注意jarFile,JobConf的構造函數:
  public JobConf(Configuration conf, Class aClass) {
    this(conf);
    String jar = findContainingJar(aClass);
    if (jar != null) {
      setJar(jar);
    }
  }
如果 aClass 是在一個jar裏面,那麼setJar(jar);就會被執行,這個jar會被copy到 LocalJobRunner 或是 JobTracker 的工作目錄下面。所以這裏有一個原則: 將要執行的MapReduce操作的所有class打包到一個jar中,這樣才能執行分佈式的MapReduce計算
再看 JobInProgress.initTasks()
先從Jar中加載InputFormat
        String ifClassName = jd.get("mapred.input.format.class");
        InputFormat inputFormat;
        if (ifClassName != null && localJarFile != null) {
          try {
            ClassLoader loader =
              new URLClassLoader(new URL[]{ localJarFile.toURL() });
            Class inputFormatClass = loader.loadClass(ifClassName);
            inputFormat = (InputFormat)inputFormatClass.newInstance();
          } catch (Exception e) {
            throw new IOException(e.toString());
          }
        } else {
          inputFormat = jd.getInputFormat();
        }
接下來對文件塊的大小進行排序
創建對應的Map任務
        this.numMapTasks = splits.length;
        // create a map task for each split
        this.maps = new TaskInProgress[numMapTasks];
        for (int i = 0; i < numMapTasks; i++) {
            maps = new TaskInProgress(jobFile, splits, jobtracker, conf, this);
        }
創建Reduce任務
        this.reduces = new TaskInProgress[numReduceTasks];
        for (int i = 0; i < numReduceTasks; i++) {
            reduces = new TaskInProgress(jobFile, maps, i, jobtracker, conf, this);
        }
最後對於每Split的信息進行緩存,並且創建狀態類
        for (int i = 0; i < maps.length; i++) {
            String hints[][] = fs.getFileCacheHints(splits.getFile(), splits.getStart(), splits.getLength());
            cachedHints.put(maps.getTIPId(), hints);
        }

        this.status = new JobStatus(status.getJobId(), 0.0f, 0.0f, JobStatus.RUNNING);
現在輪到 TaskInProgress,它將Job裏面的Map和Reduce操作進行了封裝,但是JobInProgress.initTasks()僅僅對task進行了初始化,並沒有執行Task,經過一番跟蹤,發現Task的執行,是由 TaskTracker 來處理。
TaskTracker,實現了TaskUmbilicalProtocol接口。在之前的文章中,LocalJobRunner的內部類Job也實現了這個接口,這裏對比一下:
接口 JobSubmissionProtocol:   LocalJobRunner <---> JobTracker
接口 TaskUmbilicalProtocol:    LocalJobRunner.Job <---> TaskTracker
下面對TaskTracker進行分析,首先也是從main入口開始。
TaskTracker實現了Runnable,main實例化TaskTracker對象,然後執行run()方法。
在構造函數中,主要進行初始化
        this.mapOutputFile = new MapOutputFile();
        this.mapOutputFile.setConf(conf);
        initialize();
initialize()裏面,初始化一些變量值 ,然後初始化RPC服務器:
        while (true) {
            try {
                this.taskReportServer = RPC.getServer(this, this.taskReportPort, maxCurrentTasks, false, this.fConf);
                this.taskReportServer.start();
                break;
            } catch (BindException e) {
                LOG.info("Could not open report server at " + this.taskReportPort + ", trying new port");
                this.taskReportPort++;
            }
        
        }
        while (true) {
            try {
                this.mapOutputServer = new MapOutputServer(mapOutputPort, maxCurrentTasks);
                this.mapOutputServer.start();
                break;
            } catch (BindException e) {
                LOG.info("Could not open mapoutput server at " + this.mapOutputPort + ", trying new port");
                this.mapOutputPort++;
            }
        }
mapOutputServer使用一個循環來嘗試各個端口綁定。
最後一句
        this.jobClient = (InterTrackerProtocol) RPC.getProxy(InterTrackerProtocol.class, jobTrackAddr, this.fConf);
這裏有一個新的接口InterTrackerProtocol,是TaskTracker和中央JobTracker通訊用的協議。通過這個接口, TaskTracker可以用來執行JobTracker中的Task了。接下來分析TaskServer的主流程,run()函數。
run()中, 有兩個while循環。在內部while循環裏面,執行 offerService() 方法。它裏面也是一個while循環,開始幾段代碼用於JobTracker的心跳監測。接下來,它通過協議接口調用JobTracker,獲取Task並執行:
            if (mapTotal < maxCurrentTasks || reduceTotal < maxCurrentTasks) {
                Task t = jobClient.pollForNewTask(taskTrackerName);
                if (t != null) {
                    TaskInProgress tip = new TaskInProgress(t, this.fConf);
                    synchronized (this) {
                      tasks.put(t.getTaskId(), tip);
                      if (t.isMapTask()) {
                          mapTotal++;
                      } else {
                          reduceTotal++;
                      }
                      runningTasks.put(t.getTaskId(), tip);
                    }
                    tip.launchTask();
                }
            }
tip.launchTask(); 開始執行這個Task,在方法內部:
            this.runner = task.createRunner(TaskTracker.this);
            this.runner.start();
Task 有兩個子類 MapTask和ReduceTask,它們的createRunner()方法都會創建一個TaskRunner的子類,TaskRunner繼承Thread,run()方法中:
      String sep = System.getProperty("path.separator");
      File workDir = new File(new File(t.getJobFile()).getParent(), "work");
      workDir.mkdirs();
               
      StringBuffer classPath = new StringBuffer();
      // start with same classpath as parent process
      classPath.append(System.getProperty("java.class.path"));
      classPath.append(sep);
      JobConf job = new JobConf(t.getJobFile());
      String jar = job.getJar();
      if (jar != null) {                      // if jar exists, it into workDir
        unJar(new File(jar), workDir);
        File[] libs = new File(workDir, "lib").listFiles();
        if (libs != null) {
          for (int i = 0; i < libs.length; i++) {
            classPath.append(sep);            // add libs from jar to classpath
            classPath.append(libs);
          }
        }
        classPath.append(sep);
        classPath.append(new File(workDir, "classes"));
        classPath.append(sep);
        classPath.append(workDir);
      }

獲取工作目錄,獲取classpath。然後解壓工作任務的jar包。
      //  Build exec child jmv args.
      Vector vargs = new Vector(8);
      File jvm =                                  // use same jvm as parent
        new File(new File(System.getProperty("java.home"), "bin"), "java");

      vargs.add(jvm.toString());
      String javaOpts = handleDeprecatedHeapSize(
          job.get("mapred.child.java.opts", "-Xmx200m"),
          job.get("mapred.child.heap.size"));
      javaOpts = replaceAll(javaOpts, "@taskid@", t.getTaskId());
      int port = job.getInt("mapred.task.tracker.report.port", 50050) + 1;
      javaOpts = replaceAll(javaOpts, "@port@", Integer.toString(port));
      String [] javaOptsSplit = javaOpts.split(" ");
      for (int i = 0; i < javaOptsSplit.length; i++) {
         vargs.add(javaOptsSplit);
      }

      // Add classpath.
      vargs.add("-classpath");
      vargs.add(classPath.toString());
      // Add main class and its arguments
      vargs.add(TaskTracker.Child.class.getName());  // main of Child
      vargs.add(tracker.taskReportPort + "");        // pass umbilical port
      vargs.add(t.getTaskId());                      // pass task identifier
      // Run java
      runChild((String[])vargs.toArray(new String[0]), workDir);
這裏是構造啓動Java進程的classpath和其它vm參數,最後在 runChild 中開一個子進程來執行這個Task。感覺夠複雜的。
最後分析TaskTracker的內部類Child。它就是上面子進程執行的類。在main函數中
          TaskUmbilicalProtocol umbilical =
            (TaskUmbilicalProtocol)RPC.getProxy(TaskUmbilicalProtocol.class,
                                                new InetSocketAddress(port), conf);
            
          Task task = umbilical.getTask(taskid);
          JobConf job = new JobConf(task.getJobFile());

          conf.addFinalResource(new File(task.getJobFile()));
可見該子進程也是通過RPC跟TaskTracker進行通訊。
          startPinging(umbilical, taskid);        // start pinging parent
開一個進程,對TaskTracker進行心跳監測。
              String workDir = job.getWorkingDirectory();
              if (workDir != null) {
                FileSystem file_sys = FileSystem.get(job);
                file_sys.setWorkingDirectory(new File(workDir));
              }
              task.run(job, umbilical);           // run the task
這裏才真正開始執行Task。

分析到此告一段落,下面開始構造一個分佈式執行的環境。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章