在上面一节我们分析了JobTracker调用JobQueueTaskScheduler进行任务分配,JobQueueTaskScheduler又调用JobInProgress按照一定顺序查找任务的流程,获得了任务之后,将任务封装为TaskTrackerAction数组返回的整个过程。TaskTracker通过心跳响应接收到了这个数组。本节我们继续分析,TaskTracker拿到了这个数组之后,如何对任务进行处理的。

1,TaskTracker在其方法offerService中,将得到的任务加入队列:

  1. TaskTrackerAction[] actions = heartbeatResponse.getActions();
  2. 。。。。。。。
  3. if (actions != null){
  4. for(TaskTrackerAction action: actions) {
  5. if (action instanceof LaunchTaskAction) {
  6. addToTaskQueue((LaunchTaskAction)action);
  7. } else if (action instanceof CommitTaskAction) {
  8. CommitTaskAction commitAction = (CommitTaskAction)action;
  9. if (!commitResponses.contains(commitAction.getTaskID())) {
  10. commitResponses.add(commitAction.getTaskID());
  11. }
  12. } else {
  13. addActionToCleanup(action);
  14. }
  15. }
  16. }

可见,返回的TaskTrackerAction对象有多种可能,最典型的是LaunchTaskAction,即启动一个任务;另外还有CommitTaskAction等。TaskTrackerAction是一个抽象类,其实现总共有5个,如下所示:

  1. public static enum ActionType {
  2. /** Launch a new task. */
  3. LAUNCH_TASK,
  4.  
  5. /** Kill a task. */
  6. KILL_TASK,
  7.  
  8. /** Kill any tasks of this job and cleanup. */
  9. KILL_JOB,
  10.  
  11. /** Reinitialize the tasktracker. */
  12. REINIT_TRACKER,
  13.  
  14. /** Ask a task to save its output. */
  15. COMMIT_TASK
  16. };

LaunchTaskAction表示启动任务,内部包含一个Task对象,即要启动的任务对象;

CommitTaskAction表示让一个正在执行中的任务提交其输出结果,其内部包含一个TaskAttemptID对象,对于某些Job来说,只有Map阶段而没有Reduce阶段,此时可以让其直接提交结果;

KillJobAction表示杀死属于该Job的任意任务,内部包含一个JobID;

KillTaskAction表示杀死一个任务,内部包含一个TaskAttemptID;

ReinitTrackerAction表示让TaskTracker重新初始化,无需带什么参数,只要随着心跳响应返回,对应的TaskTracker自然知道应该重新初始化。

我们对最典型的LaunchTaskAction进行分析,其内部的Task是一个抽象类,主要有MapTask和ReduceTask两种实现。在上面的代码中,addToTaskQueue将LaunchTaskAction按照Map或Reduce类型分别加入到不同的队列:

  1. private void addToTaskQueue(LaunchTaskAction action) {
  2. if (action.getTask().isMapTask()) {
  3. mapLauncher.addToTaskQueue(action);
  4. } else {
  5. reduceLauncher.addToTaskQueue(action);
  6. }
  7. }

mapLauncher和reduceLauncher都是TaskLauncher类型的对象,顾名思义用于Task的启动。TaskLauncher继承了Thread类,因此是一个线程类。其加入队列的代码为:

  1. public void addToTaskQueue(LaunchTaskAction action) {
  2. synchronized (tasksToLaunch) {
  3. TaskInProgress tip = registerTask(action, this);
  4. tasksToLaunch.add(tip);
  5. tasksToLaunch.notifyAll();
  6. }
  7. }

tasksToLaunch是一个任务链表:LinkedList<TaskInProgress>(),可见,我们再一次又看见了TaskInProgress,心跳响应时,双方交互的实际上是Task对象,自己内部维护的是TaskInProgress对象。通过registerTask这个方法,将其转换回来。其实其转换代码也简单:

  1. private TaskInProgress registerTask(LaunchTaskAction action,
  2. TaskLauncher launcher) {
  3. Task t = action.getTask();
  4. LOG.info("LaunchTaskAction (registerTask): " + t.getTaskID() +
  5. " task's state:" + t.getState());
  6. TaskInProgress tip = new TaskInProgress(t, this.fConf, launcher);
  7. synchronized (this) {
  8. tasks.put(t.getTaskID(), tip);
  9. runningTasks.put(t.getTaskID(), tip);
  10. boolean isMap = t.isMapTask();
  11. if (isMap) {
  12. mapTotal++;
  13. } else {
  14. reduceTotal++;
  15. }
  16. }
  17. return tip;
  18. }

在上面的注册代码中,创建了一个TaskInProgress对象,之后加入到两个映射表中:

  1. Map<TaskAttemptID, TaskInProgress> tasks = new HashMap<TaskAttemptID, TaskInProgress>();
  2. /**
  3. * Map from taskId -> TaskInProgress.
  4. */
  5. Map<TaskAttemptID, TaskInProgress> runningTasks = null;

映射表的索引是目前的任务ID,值即为刚刚创建的TaskInProgress对象。

之后,加入到tasksToLaunch对象中,并执行notifyAll方法,唤醒TaskLauncher线程(该线程可能处于睡眠等待中)处理队列里面的任务。

接下来,另一个线程,即TaskLauncher的run方法会处理这个对象。心跳线程即TaskTracker的主线程返回,继续等待执行后续的心跳。

2,TaskLauncher的run方法接着执行:

run方法的前半部分是从队列中获取任务:

  1. synchronized (tasksToLaunch) {
  2. while (tasksToLaunch.isEmpty()) {
  3. tasksToLaunch.wait();
  4. }
  5. //get the TIP
  6. tip = tasksToLaunch.remove(0);
  7. task = tip.getTask();
  8. LOG.info("Trying to launch : " + tip.getTask().getTaskID() +
  9. " which needs " + task.getNumSlotsRequired() + " slots");
  10. }

如果为空,则调用wait进行等待;如果不为空,自然有人会通知自己(即notifyAll),之后,取出第一个任务并删除队列中的第一个元素。wait和notifyAll是JAVA任意一个对象都会具有的方法。

取出任务之后,需要判断是否有资源可用,使用IntWritable numFreeSlots进行判断,如果没有足够资源,则一直会等待:

  1. while (numFreeSlots.get() < task.getNumSlotsRequired()) {
  2. 。。。。。。。。。。。。。
  3. numFreeSlots.wait();
  4. }

而numFreeSlots的更新是TaskLauncher另一个方法中进行的:

  1. public void addFreeSlots(int numSlots) {
  2. synchronized (numFreeSlots) {
  3. numFreeSlots.set(numFreeSlots.get() + numSlots);
  4. assert (numFreeSlots.get() <= maxSlots);
  5. LOG.info("addFreeSlot : current free slots : " + numFreeSlots.get());
  6. numFreeSlots.notifyAll();
  7. }
  8. }

而该方法又被另一个方法调用:

  1. private synchronized void releaseSlot() {
  2. if (slotTaken) {
  3. if (launcher != null) {
  4. launcher.addFreeSlots(task.getNumSlotsRequired());
  5. }
  6. slotTaken = false;
  7. } else {
  8. // wake up the launcher. it may be waiting to block slots for this task.
  9. if (launcher != null) {
  10. launcher.notifySlots();
  11. }
  12. }
  13. }

releaseSlot表示释放资源,又是在kill和reportTaskFinished方法中调用:

  1. void reportTaskFinished(boolean commitPending) {
  2. if (!commitPending) {
  3. taskFinished();
  4. releaseSlot();
  5. }
  6. notifyTTAboutTaskCompletion();
  7. }

也就是说,当任务被杀掉或者执行完毕时,会释放资源,进而发出通告。

如果获得了足够的资源,则该变量会变化:

  1. numFreeSlots.set(numFreeSlots.get() - task.getNumSlotsRequired());

之后,如果任务可以执行(如果不可以执行还需要回收资源),进入startNewTask方法。在该方法里,会创建一个新线程进行任务的启动工作,上一个线程只是从队列里取出任务,判断资源是否充足等功能。启动了新线程后,它的使命结束了,返回去继续等待队列LinkedList<TaskInProgress>()中是否出现了新的TaskInProgress。

3,startNewTask创建的线程继续执行:

  1. void startNewTask(final TaskInProgress tip) throws InterruptedException {
  2. Thread launchThread = new Thread(new Runnable() {
  3. @Override
  4. public void run() {
  5. try {
  6. RunningJob rjob = localizeJob(tip);
  7. tip.getTask().setJobFile(rjob.getLocalizedJobConf().toString());
  8. launchTaskForJob(tip, new JobConf(rjob.getJobConf()), rjob);
  9. } catch (Throwable e) {
  10. 。。。。。
  11. try {
  12. tip.kill(true);
  13. tip.cleanup(false, true);
  14. } catch (IOException ie2) {
  15. 。。。。。。
  16. }
  17. }
  18. });
  19. launchThread.start();
  20. }

该线程中,首先执行localizeJob方法。这个方法用于初始化Job的目录。首先,执行addTaskToJob方法,将任务加入到正在运行作业的对象中,runningJobs是TaskTracker中的一个变量,记录了当前这个TaskTracker上面运行了哪些Job。

  1. Map<JobID, RunningJob> runningJobs = new TreeMap<JobID, RunningJob>();

runningJobs里面保存了Job ID和RunningJob之间的对应关系,RunningJob是一个类,记录了当前正在初始化的任务的信息。主要包含以下信息:

  1. private JobID jobid;
  2. private JobConf jobConf;
  3. private Path localizedJobConf;
  4. // keep this for later use
  5. volatile Set<TaskInProgress> tasks;
  6. //the 'localizing' and 'localized' fields have the following
  7. //state transitions (first entry is for 'localizing')
  8. //{false,false} -> {true,false} -> {false,true}
  9. volatile boolean localized;
  10. boolean localizing;
  11. boolean keepJobFiles;
  12. UserGroupInformation ugi;
  13. FetchStatus f;
  14. TaskDistributedCacheManager distCacheMgr;

即Job ID、Job Conf即配置参数、Job配置文件路径、该Job包含的任务集合(当前TaskTracker内的)以及一些用户权限等信息。

接下来需要对Job所在目录进行定位,使用initializeJob方法:

上一节我们在分析任务调度的时候,有一个obtainNewMapTaskCommon方法,该方法通过查找任务后,执行addRunningTaskToTIP方法,并调用addRunningTask方法,即创建MapTask或ReduceTask对象。此时会将任务的目录信息传进来,而最终一个任务的目录信息是在TaskInProgress的构造方法中传进来的。我们再回顾下Job的初始化过程,有以下代码:

  1. maps = new TaskInProgress[numMapTasks];
  2. for(int i=0; i < numMapTasks; ++i) {
  3. inputLength += splits[i].getInputDataLength();
  4. maps[i] = new TaskInProgress(jobId, jobFile,
  5. splits[i],
  6. jobtracker, conf, this, i, numSlotsPerMap);
  7. }

这里的jobFile即为任务所在的目录,实际上任务所在目录来源于作业所在目录,因此一个JobInProgress对象在创建的时候就会将目录信息记录下来,实际上,最终的文件目录是在Job初始化时的submitJobInternal方法传进来的。我们回顾以下关键代码:

  1. Path jobStagingArea = JobSubmissionFiles.getStagingDir(JobClient.this,
  2. jobCopy);
  3. JobID jobId = jobSubmitClient.getNewJobId();
  4. Path submitJobDir = new Path(jobStagingArea, jobId.toString());

这个目录即Staging目录,是根据一定规则自动生成的,最终位于HDFS的某个路径中。

我们回到initializeJob这个方法。因为Job提交时其实已经确定了目录,所以以下代码就是找到这个目录对应的文件系统:

  1. final JobID jobId = t.getJobID();
  2. final Path jobFile = new Path(t.getJobFile());
  3. final Configuration conf = getJobConf();
  4. FileSystem userFs = getFS(jobFile, jobId, conf);

这个文件系统就是HDFS中作业所在的Staging目录。Staging是集结地、驻留区的意思。用户在提交作业的时候,会将所需的jar文件、配置文件(conf、xml)、其它依赖的lib包、要处理数据的Split信息文件等全部上传到这个地方。

获得了这个目录后,接下来,TaskTracker的任务初始化过程会将上述处于HDFS中的文件拷贝到本地(如Linux)文件系统。使用localizeJobConfFile方法:

  1. FileStatus status = null;
  2. long jobFileSize = -1;
  3. try {
  4. status = userFs.getFileStatus(jobFile);
  5. jobFileSize = status.getLen();
  6. } catch(FileNotFoundException fe) {
  7. jobFileSize = -1;
  8. }
  9. Path localJobFile = lDirAlloc.getLocalPathForWrite(getPrivateDirJobConfFile(user,jobId.toString()), jobFileSize, fConf);
  10.  
  11. // Download job.xml
  12. userFs.copyToLocalFile(jobFile, localJobFile);

lDirAlloc是一个LocalDirAllocator对象,主要用于本地目录的操作。getLocalPathForWrite方法:

  1. public Path getLocalPathForWrite(String pathStr, long size,
  2. Configuration conf,
  3. boolean checkWrite) throws IOException {
  4. AllocatorPerContext context = obtainContext(contextCfgItemName);
  5. return context.getLocalPathForWrite(pathStr, size, conf, checkWrite);
  6. }

AllocatorPerContext对象通过 obtainContext方法获得。这里面涉及到一个参数contextCfgItemName,该参数是LocalDirAllocator对象在创建的时候传入的:该对象本身是TaskTracker的一个变量:

  1. this.localDirAllocator = new LocalDirAllocator("mapred.local.dir");
  2.  
  3.   public LocalDirAllocator(String contextCfgItemName) {
        this.contextCfgItemName = contextCfgItemName;
      }

之后调用 context.getLocalPathForWrite方法,首先执行confChanged方法。在该方法里,有以下代码:

  1. String newLocalDirs = conf.get(contextCfgItemName);
  2. if (!newLocalDirs.equals(savedLocalDirs)) {
  3. String[] localDirs = conf.getStrings(contextCfgItemName);
  4. localFS = FileSystem.getLocal(conf);

此处,通过调用文件系统相关方法,会创建"mapred.local.dir"所指定的本地目录。

创建好了本地目录后,执行文件拷贝操作copyToLocalFile:

  1. public void copyToLocalFile(boolean delSrc, Path src, Path dst)
  2. throws IOException {
  3. FileUtil.copy(this, src, getLocal(getConf()), dst, delSrc, getConf());
  4. }

即job.xml文件被从HDFS中拷贝到TaskTracker所在的本地文件系统。该目录由配置文件"mapred.local.dir"指定。

另外,在这之前还有一些拷贝关于用户操作权限等文件:localizeJobTokenFile(与localizeJobConfFile过程类似)。

接下来对拷贝到本地的配置文件进行解析,得到JobConf对象:

  1. final JobConf localJobConf = new JobConf(localJobFile);

并将这个信息加入到缓存中,因为localJobConf是根据某个任务拷贝过来的job.xml解析的,如果后面需要再用,则可以使用该缓存信息。TaskTracker里面维护了一个TrackerDistributedCacheManager distributedCacheManager对象,该对象里面维护了一个任务缓存:

  1. private Map<JobID, TaskDistributedCacheManager> jobArchives =
  2. Collections.synchronizedMap(
  3. new HashMap<JobID, TaskDistributedCacheManager>());

以及一个TaskController taskController。

首先将上面解析的任务配置参数加入到缓存中:

  1. public TaskDistributedCacheManager newTaskDistributedCacheManager(JobID jobId,
  2. Configuration taskConf) throws IOException {
  3. TaskDistributedCacheManager result = new TaskDistributedCacheManager(this, taskConf);
  4. jobArchives.put(jobId, result);
  5. return result;
  6. }

TrackerDistributedCacheManager以及TaskDistributedCacheManager 有什么意义呢?每个TaskTracker创建时会创建一个TrackerDistributedCacheManager对象,其主要作用是管理该机器上所有任务的Cache文件。而当任务在初始化时,会由TrackerDistributedCacheManager产生一个TaskDistributedCacheManager对象(如上面代码示例),用于管理本任务的Cache文件。有什么好处呢?比如,一个TaskTracker上可能会有一个Job的多个Task,这样,某个TaskDistributedCacheManager已经获取处理了某些Cache文件,则其他任务无需再处理。这里所谓的Cache文件,指的是从HDFS文件系统拷贝到本地文件系统的那些文件,本地的Linux系统相对于HDFS来说可以看做一种缓存,因为不能每次要访问某个参数,或者一个机器上的所有任务要访问Job信息都去HDFS中去获取和解析吧,这就是Distributed Cache分布式缓存的含义,因为有很多机器会拷贝这个文件。对于某个TaskTracker而言,通过拷贝到本地,不仅加速了后面的处理时间,也为其他任务做出了贡献。只是需要注意的是,如何维护本地的缓存信息和HDFS中的一致是需要解决的问题。Hadoop里设计了一个CacheFile类,里面包含了时间戳等信息,只要比较一下该时间戳以及HDFS中文件时间戳是否一致即可。

获得了缓存对象后,本地的Job配置会有一些更新,更新后写回到本地的配置文件:

  1. // Set some config values
  2. localJobConf.set(JobConf.MAPRED_LOCAL_DIR_PROPERTY,
  3. getJobConf().get(JobConf.MAPRED_LOCAL_DIR_PROPERTY));
  4. if (conf.get("slave.host.name") != null) {
  5. localJobConf.set("slave.host.name", conf.get("slave.host.name"));
  6. }
  7. resetNumTasksPerJvm(localJobConf);
  8. localJobConf.setUser(t.getUser());
  9.  
  10. // write back the config (this config will have the updates that the
  11. // distributed cache manager makes as well)
  12. JobLocalizer.writeLocalJobFile(localJobFile, localJobConf);

接下来进入到taskController的initializeJob方法,进行后续的初始化。TaskController是一个抽象类,其实现是DefaultTaskController和LinuxTaskController。DefaultTaskController是默认的任务控制器,LinuxTaskController在Linux系统上使用,在用户权限设置等方面有些不同,可能是利用Linux的用户管理等等进行任务管理,以后有时间再研究,现在先来看默认的DefaultTaskController。

其initializeJob方法声明如下,可以看出,该方法从TaskTracker的私有空间拷贝证书文件到Job的私有空间;创建Job工作目录,下载job.jar文件并解压得到要执行的任务的.class文件,并更新配置。之后,创建一个JobConf配置对象,设置分布式缓存和用户日志目录等

  1. /**
  2. * This routine initializes the local file system for running a job.
  3. * Details:
  4. * <ul>
  5. * <li>Copies the credentials file from the TaskTracker's private space to
  6. * the job's private space </li>
  7. * <li>Creates the job work directory and set
  8. * {@link TaskTracker#JOB_LOCAL_DIR} in the configuration</li>
  9. * <li>Downloads the job.jar, unjars it, and updates the configuration to
  10. * reflect the localized path of the job.jar</li>
  11. * <li>Creates a base JobConf in the job's private space</li>
  12. * <li>Sets up the distributed cache</li>
  13. * <li>Sets up the user logs directory for the job</li>
  14. * </ul>
  15. * This method must be invoked in the access control context of the job owner
  16. * user. This is because the distributed cache is also setup here and the
  17. * access to the hdfs files requires authentication tokens in case where
  18. * security is enabled.
  19. * @param user the user in question (the job owner)
  20. * @param jobid the ID of the job in question
  21. * @param credentials the path to the credentials file that the TaskTracker
  22. * downloaded
  23. * @param jobConf the path to the job configuration file that the TaskTracker
  24. * downloaded
  25. * @param taskTracker the connection to the task tracker
  26. * @throws IOException
  27. * @throws InterruptedException
  28. */
  29. @Override
  30. public void initializeJob(String user, String jobid,
  31. Path credentials, Path jobConf,
  32. TaskUmbilicalProtocol taskTracker,
  33. InetSocketAddress ttAddr
  34. )

其输入参数主要有:用户、JobID、证书目录、Job配置文件目录、一个TaskUmbilicalProtocol对象,以及TaskTracker的IP地址信息,这里的TaskUmbilicalProtocol对象需要细说一下,TaskUmbilicalProtocol是和InterTrackerProtocol等等类似的一个RPC接口,我们前面分析过,再来总结一下:

1)JobClient和JobTracker之间存在一个RPC接口JobSubmissionProtocol,JobClient调用这个接口,JobTracker实现了这个接口。

2)JobTracker和TaskTracker之间存在一个RPC接口InterTrackerProtocol,TaskTracker调用这个接口,JobTracker实现了这个接口。

TaskUmbilicalProtocol也类似,不过是TaskTracker实现的,那么,谁来掉用呢?

Umbilical大致是脐带的意思,直观含义是连接胎儿和母体之间的重要通道。TaskTracker要启动一个Map或Reduce任务的时候,是通过启动另外一个JAVA虚拟机去运行这个任务,用户提交作业时,就是JobClient和JobTracker在打交道,JobTracker对这个作业初始化的过程会创建HDFS目录,将jar文件、分析处理的文件(即FileInputFormat.addInputPath里面指定的),生成Split信息文件等统统放到这个目录。在某个TaskTracker与自己心跳时,从作业队列挑选一些合适的任务,让TaskTracker运行,两者之间交互了Task的信息,TaskTracker接收到这个信息后,去HDFS目录里面将Job文件拷贝到本地,同时jar文件也拷贝下来,进行解压以后怎么运行呢?TaskTracker会启动一个JAVA虚拟机,这个JAVA虚拟机当然也是一个进程,不过称为TaskTracker进程的子进程,这个子进程和TaskTracker进程之间也需要交互一些信息,比如汇报状态等等,此时就导致了TaskUmbilicalProtocol这个RPC接口的出现。我们看看他有哪些方法:

  1. JvmTask getTask(JvmContext context) throws IOException;
  2.  
  3. boolean statusUpdate(TaskAttemptID taskId, TaskStatus taskStatus,
  4. JvmContext jvmContext) throws IOException, InterruptedException;
  5.  
  6. void reportDiagnosticInfo(TaskAttemptID taskid, String trace,
  7. JvmContext jvmContext) throws IOException;
  8.  
  9. void reportNextRecordRange(TaskAttemptID taskid, SortedRanges.Range range,
  10. JvmContext jvmContext) throws IOException;
  11.  
  12. boolean ping(TaskAttemptID taskid, JvmContext jvmContext) throws IOException;
  13.  
  14. void done(TaskAttemptID taskid, JvmContext jvmContext) throws IOException;
  15.  
  16. void commitPending(TaskAttemptID taskId, TaskStatus taskStatus,
  17. JvmContext jvmContext) throws IOException, InterruptedException;
  18.  
  19. boolean canCommit(TaskAttemptID taskid, JvmContext jvmContext) throws IOException;
  20.  
  21. void shuffleError(TaskAttemptID taskId, String message, JvmContext jvmContext) throws IOException;
  22.  
  23. void fsError(TaskAttemptID taskId, String message, JvmContext jvmContext) throws IOException;
  24.  
  25. void fatalError(TaskAttemptID taskId, String message, JvmContext jvmContext) throws IOException;
  26.  
  27. MapTaskCompletionEventsUpdate getMapCompletionEvents(JobID jobId,
  28. int fromIndex,
  29. int maxLocs,
  30. TaskAttemptID id,
  31. JvmContext jvmContext) throws IOException;
  32.  
  33. void updatePrivateDistributedCacheSizes(org.apache.hadoop.mapreduce.JobID jobId,long[] sizes) throws IOException;

可见,主要是状态更新、错误信息上报、周期地ping、任务完成通知等等。

所以,现在可以理解,上面initializeJob方法中传入的TaskUmbilicalProtocol对象其实就是TaskTracker.this。

我们接下里分析initializeJob方法。

这个方法的主要功能上面已经进行了简单描述,来看一下细节。首先需要做的就是将TaskTracker从HDFS拷贝过来的文件进行二次拷贝,拷贝到作业所在的目录。这里使用了JobLocalizer类,这个类会创建以下目录:

  1. /**
  2. * Internal class responsible for initializing the job, not intended for users.
  3. * Creates the following hierarchy:
  4. * <li>$mapred.local.dir/taskTracker/$user</li>
  5. * <li>$mapred.local.dir/taskTracker/$user/jobcache</li>
  6. * <li>$mapred.local.dir/taskTracker/$user/jobcache/$jobid/work</li>
  7. * <li>$mapred.local.dir/taskTracker/$user/jobcache/$jobid/jars</li>
  8. * <li>$mapred.local.dir/taskTracker/$user/jobcache/$jobid/jars/job.jar</li>
  9. * <li>$mapred.local.dir/taskTracker/$user/jobcache/$jobid/job.xml</li>
  10. * <li>$mapred.local.dir/taskTracker/$user/jobcache/$jobid/jobToken</li>
  11. * <li>$mapred.local.dir/taskTracker/$user/distcache</li>
  12. */

主要的代码是:

  1. FileSystem localFs = FileSystem.getLocal(getConf());
  2. JobLocalizer localizer = new JobLocalizer((JobConf)getConf(), user, jobid);
  3. localizer.createLocalDirs();
  4. localizer.createUserDirs();
  5. localizer.createJobDirs();
  6. JobConf jConf = new JobConf(jobConf);
  7. localizer.createWorkDir(jConf);
  8. //copy the credential file
  9. Path localJobTokenFile = lDirAlloc.getLocalPathForWrite(
  10. TaskTracker.getLocalJobTokenFile(user, jobid), getConf());
  11. FileUtil.copy(localFs, credentials, localFs, localJobTokenFile, false, getConf());
  12. //setup the user logs dir
  13. localizer.initializeJobLogDir();
  14. // Download the job.jar for this job from the system FS
  15. // setup the distributed cache
  16. // write job acls
  17. // write localized config
  18. localizer.localizeJobFiles(JobID.forName(jobid), jConf, localJobTokenFile, taskTracker);

这里比较重要的最后一行:localizeJobFiles,jar文件就是在这里处理的,进去看看。

执行的第一行代码就是localizeJobJarFile,前面已经把job.xml等文件拷贝到本地了,jar文件还没拷贝,此处就是做这个工作的。

主要代码为:

  1. // Here we check for five times the size of jarFileSize to accommodate for
  2. // unjarring the jar file in the jars directory
  3. Path localJarFile = lDirAlloc.getLocalPathForWrite(JARDST, 5 * jarFileSize, ttConf);
  4. //Download job.jar
  5. userFs.copyToLocalFile(jarFilePath, localJarFile);
  6. localJobConf.setJar(localJarFile.toString());
  7. // Also un-jar the job.jar files. We un-jar it so that classes inside
  8. // sub-directories, for e.g., lib/, classes/ are available on class-path
  9. RunJar.unJar(new File(localJarFile.toString()),
  10. new File(localJarFile.getParent().toString()));
  11. FileUtil.chmod(localJarFile.getParent().toString(), "ugo+rx", true);

在获得了本地的jar目录后,从HDFS中进行拷贝,并利用RunJar的unJar方法进行解压。JAVA里有JarFile这个类用于JAR打包和解压。顺便说一下,jar文件采用的就是ZIP格式,JarFile其实就是利用ZIP的解压方法,而JAVA中本身也有ZIP的解压方法,ZIP利用的是LZ系列字典压缩算法以及Huffman、游程编码等算法的融合。JarFile继承于java.util.zip.ZipFile这个类。

至此,TaskController的initializeJob方法结束,返回TaskTracker的initializeJob方法,并继续返回localizeJob方法。RunningJob这个对象初始化完毕,改变下面的值:

  1. rjob.localized = true;

并向其他线程执行通告:

  1. synchronized (rjob) {
  2. if (rjob.localizing) {
  3. rjob.localizing = false;
  4. rjob.notifyAll();
  5. }
  6. }
  7.  
  8. synchronized (runningJobs) {
  9. runningJobs.notify(); //notify the fetcher thread
  10. }

localizeJob方法执行完毕,之后返回startNewTask方法。接下来执行launchTaskForJob方法:

  1. protected void launchTaskForJob(TaskInProgress tip, JobConf jobConf,
  2. RunningJob rjob) throws IOException {
  3. synchronized (tip) {
  4. jobConf.set(JobConf.MAPRED_LOCAL_DIR_PROPERTY,
  5. localStorage.getDirsString());
  6. tip.setJobConf(jobConf);
  7. tip.setUGI(rjob.ugi);
  8. tip.launchTask(rjob);
  9. }
  10. }

首先是设置一些任务参数,之后调用launchTask方法,在launchTask方法里,因为前面涉及的都是Job相关的配置信息,到了此时,Task本身的配置信息需要进行设置:

  1. /**
  2. * Localize the given JobConf to be specific for this task.
  3. */
  4. public void localizeConfiguration(JobConf conf) throws IOException {
  5. conf.set("mapred.tip.id", taskId.getTaskID().toString());
  6. conf.set("mapred.task.id", taskId.toString());
  7. conf.setBoolean("mapred.task.is.map", isMapTask());
  8. conf.setInt("mapred.task.partition", partition);
  9. conf.set("mapred.job.id", taskId.getJobID().toString());
  10. }

比如Task ID、是否是Map任务等等。

设置Task的状态为运行状态:

  1. if (this.taskStatus.getRunState() == TaskStatus.State.UNASSIGNED) {
  2. this.taskStatus.setRunState(TaskStatus.State.RUNNING);
  3. }

接下来会创建一个TaskRunner对象,TaskRunner 对象本身是TaskInProgress中的一个变量,其创建则是在MapTask或ReduceTask中实现的。

  1. @Override
  2. public TaskRunner createRunner(TaskTracker tracker,
  3. TaskTracker.TaskInProgress tip,
  4. TaskTracker.RunningJob rjob
  5. ) throws IOException {
  6. return new MapTaskRunner(tip, tracker, this.conf, rjob);
  7. }

TaskRunner是一个抽象类,主要有MapTaskRunner和ReduceTaskRunner两种实现。该抽象类继承于Thread,是一个线程类,并且在TaskRunner中实现了run方法。MapTaskRunner在创建过程中,利用父类构造方法,将一系列参数传进去:

  1. public TaskRunner(TaskTracker.TaskInProgress tip, TaskTracker tracker,
  2. JobConf conf, TaskTracker.RunningJob rjob
  3. ) throws IOException {
  4. this.tip = tip;
  5. this.t = tip.getTask();
  6. this.tracker = tracker;
  7. this.conf = conf;
  8. this.mapOutputFile = new MapOutputFile();
  9. this.mapOutputFile.setConf(conf);
  10. this.jvmManager = tracker.getJvmManagerInstance();
  11. this.localdirs = conf.getLocalDirs();
  12. taskDistributedCacheManager = rjob.distCacheMgr;
  13. }

比如Map任务之后的目录等,这里还有一个jvmManager对象,该对象是TaskTracker中的一个对象,每个TaskTracker一个,在TaskTracker初始化的时候会创建出来:

  1. public JvmManager(TaskTracker tracker) {
  2. mapJvmManager = new JvmManagerForType(tracker.getMaxCurrentMapTasks(), true, tracker);
  3. reduceJvmManager = new JvmManagerForType(tracker.getMaxCurrentReduceTasks(), false, tracker);
  4. }

其中:JvmManagerForType的构造方法如下,第一个参数是要管理的JAVA虚拟机的个数,而tracker.getMaxCurrentMapTasks()获得的就是为本TaskTracker配置的最大Map槽数。

  1. public JvmManagerForType(int maxJvms, boolean isMap,
  2. TaskTracker tracker) {
  3. this.maxJvms = maxJvms;
  4. this.isMap = isMap;
  5. this.tracker = tracker;
  6. sleeptimeBeforeSigkill =
  7. tracker.getJobConf().getLong(DELAY_BEFORE_KILL_KEY,
  8. DEFAULT_SLEEPTIME_BEFORE_SIGKILL);
  9. }

创建好了TaskRunner之后,即启动新线程:

  1. setTaskRunner(task.createRunner(TaskTracker.this, this, rjob));
  2. this.runner.start();

launchTask这个方法启动了TaskRunner之后,则返回launchTaskForJob方法,继续返回startNewTask方法。对tasksToLaunch中的其他TaskInProgress方法重复执行上面的初始化操作。startNewTask创建的局部线程已经结束。如果有新的任务到达,TaskLauncher会启动新的线程来执行上面的工作,下面进入到TaskRunner的run方法执行。

4,TaskRunner的run方法接力执行:

这个方法为JVM的启动提供命令行生成、环境变量、类库路径等信息。首先创建Task工作目录,注意上面的JobLocalizer类创建了一系列Job目录,这里创建的是Task相关的目录。

  1. final File workDir =
  2. new File(new Path(localdirs[rand.nextInt(localdirs.length)],
  3. TaskTracker.getTaskWorkDir(t.getUser(), taskid.getJobID().toString(),
  4. taskid.toString(),
  5. t.isTaskCleanupTask())).toString());
  6.  
  7. String user = tip.getUGI().getUserName();

之后调用getClassPaths方法获取解压后的.class文件路径

  1. private static List<String> getClassPaths(JobConf conf, File workDir,
  2. TaskDistributedCacheManager taskDistributedCacheManager)
  3. throws IOException {
  4. // Accumulates class paths for child.
  5. List<String> classPaths = new ArrayList<String>();
  6. boolean userClassesTakesPrecedence = conf.getBoolean(MAPREDUCE_USER_CLASSPATH_FIRST,false);
  7. if (!userClassesTakesPrecedence) {
  8. // start with same classpath as parent process
  9. appendSystemClasspaths(classPaths);
  10. }
  11. // include the user specified classpath
  12. appendJobJarClasspaths(conf.getJar(), classPaths);
  13. // Distributed cache paths
  14. classPaths.addAll(taskDistributedCacheManager.getClassPaths());
  15. // Include the working dir too
  16. classPaths.add(workDir.toString());
  17. if (userClassesTakesPrecedence) {
  18. // parent process's classpath is added last
  19. appendSystemClasspaths(classPaths);
  20. }
  21. return classPaths;
  22. }

总之,一个JAVA程序运行需要很多库目录,比如本地目录、JRE目录等,首先加入系统目录:

  1. private static void appendSystemClasspaths(List<String> classPaths) {
  2. for (String c : System.getProperty("java.class.path").split(
  3. SYSTEM_PATH_SEPARATOR)) {
  4. classPaths.add(c);
  5. }
  6. }

appendJobJarClasspaths方法加入到Job的jar文件所在的本地目录(已经从HDFS中拷贝下来了)。

之后,创建JAVA虚拟机运行所需要的参数:

  1. // Build exec child JVM args.
  2. Vector<String> vargs = getVMArgs(taskid, workDir, classPaths, logSize);

说白了,就是产生一堆“java xxx -D ...”等等这个命令,比如一些参数示例,虚拟机内存占多大等等:

  1. // <property>
  2. // <name>mapred.map.child.java.opts</name>
  3. // <value>-Xmx 512M -verbose:gc -Xloggc:/tmp/@taskid@.gc \
  4. // -Dcom.sun.management.jmxremote.authenticate=false \
  5. // -Dcom.sun.management.jmxremote.ssl=false \
  6. // </value>
  7. // </property>

首先找到JAVA虚拟机那个可执行文件在哪个目录(安装目录/bin),然后加进去,这自然是第一步。

  1. File jvm = // use same jvm as parent
  2. new File(new File(System.getProperty("java.home"), "bin"), "java");
      vargs.add(jvm.toString());

另外,就是一些库文件目录等等,比如:

  1. Path childTmpDir = createChildTmpDir(workDir, conf, false);
  2. vargs.add("-Djava.io.tmpdir=" + childTmpDir);
  3.  
  4. // Add classpath.
  5. vargs.add("-classpath");
  6. String classPath = StringUtils.join(SYSTEM_PATH_SEPARATOR, classPaths);
  7. vargs.add(classPath);

关键的是把要执行的class的类名写上:

  1. vargs.add(Child.class.getName()); // main of Child

可以看出,执行的是Child这个类,这个类有一个Main方法。我们后面再看。

获得了JAVA运行参数后,继续获取环境变量:

  1. Map<String, String> env = new HashMap<String, String>();
  2. errorInfo = getVMEnvironment(errorInfo, user, workDir, conf, env, taskid,
  3. logSize);

比如LD_LIBRARY_PATH等等这些环境变量。

之后,执行launchJvmAndWait方法:

  1. void launchJvmAndWait(List <String> setup, Vector<String> vargs, File stdout,
  2. File stderr, long logSize, File workDir)
  3. throws InterruptedException, IOException {
  4. jvmManager.launchJvm(this, jvmManager.constructJvmEnv(setup, vargs, stdout,
  5. stderr, logSize, workDir, conf));
  6. synchronized (lock) {
  7. while (!done) {
  8. lock.wait();
  9. }
  10. }
  11. }

里面的核心方法是jvmManager.launchJvm:

  1. public void launchJvm(TaskRunner t, JvmEnv env
  2. ) throws IOException, InterruptedException {
  3. if (t.getTask().isMapTask()) {
  4. mapJvmManager.reapJvm(t, env);
  5. } else {
  6. reduceJvmManager.reapJvm(t, env);
  7. }
  8. }

mapJvmManager和reduceJvmManager都是一个JvmManagerForType对象,前面提过,在TaskTracker初始化的时候会进行创建。执行JvmManagerForType的reapJvm方法,该方法中,有一个变量spawnNewJvm记录了是否应该启动一个新的JVM。spawn是产卵的意思,该标志的含义很明显,如果不满足一些条件,那么是不能启动虚拟机的,那么有哪些条件呢?

JvmManagerForType对象里面有几个映射表:

  1. //Mapping from the JVM IDs to running Tasks
  2. Map <JVMId,TaskRunner> jvmToRunningTask = new HashMap<JVMId, TaskRunner>();
  3. //Mapping from the tasks to JVM IDs
  4. Map <TaskRunner,JVMId> runningTaskToJvm = new HashMap<TaskRunner, JVMId>();
  5. //Mapping from the JVM IDs to Reduce JVM processes
  6. Map <JVMId, JvmRunner> jvmIdToRunner = new HashMap<JVMId, JvmRunner>();
  7. //Mapping from the JVM IDs to process IDs
  8. Map <JVMId, String> jvmIdToPid = new HashMap<JVMId, String>();

jvmToRunningTask记录了JVMID和TaskRunner对象的关系;runningTaskToJvm则反过来记录;jvmIdToRunner记录了JVMID和JvmRunner的关系。注意这里有几个不同的对象:TaskRunner代表了要运行的任务;JvmRunner代表了要启动的虚拟机。

在判断spawnNewJvm变量时,首先获得当前已经运行的JVM:

  1. int numJvmsSpawned = jvmIdToRunner.size();

如果numJvmsSpawned < maxJvms,那么表明目前启动的JVM还没达到最大值,可以启动:spawnNewJvm=true;

如果numJvmsSpawned >= maxJvms,则表明目前的数量已经至少等于最大可允许的JVM数量(=配置的Map或Reduce Slot数目),这种情况下按理来说可能不能启动了,不过也不一定。这里会对每一个JVM进行逐一判断,找找看,看哪个JVM适合处理这个任务。这是什么逻辑呢?有两种可能,一种可能是利用已经启动的JVM,判断代码如下:

  1. //look for a free JVM for this job; if one exists then just break
  2. if (jId.equals(jobId) && !jvmRunner.isBusy() && !jvmRunner.ranAll()){
  3. setRunningTaskForJvm(jvmRunner.jvmId, t); //reserve the JVM
  4. LOG.info("No new JVM spawned for jobId/taskid: " +
  5. jobId+"/"+t.getTask().getTaskID() +
  6. ". Attempting to reuse: " + jvmRunner.jvmId);
  7. return;
  8. }

如果一个JVM内的任务与要启动任务的Job ID一样(jId.equals(jobId)),且已经不忙了,即处于空闲状态(!jvmRunner.isBusy()),并且所有任务没有执行完毕(!jvmRunner.ranAll()),此时则将该任务(TaskRunner代表了要运行的任务)直接让该虚拟机执行,执行setRunningTaskForJvm方法:

  1. synchronized public void setRunningTaskForJvm(JVMId jvmId, TaskRunner t) {
  2. jvmToRunningTask.put(jvmId, t);
  3. runningTaskToJvm.put(t,jvmId);
  4. jvmIdToRunner.get(jvmId).setBusy(true);
  5. }

除了上面这种情况,还有一种情况就是看看能不能杀掉一个JVM,判断代码如下:

  1. if ((jId.equals(jobId) && jvmRunner.ranAll()) ||
  2. (!jId.equals(jobId) && !jvmRunner.isBusy())) {
  3. runnerToKill = jvmRunner;
  4. spawnNewJvm = true;
  5. }

即两者Job ID相同且JVM已经执行完毕了,此时可以杀掉这个JVM来执行新任务;或者两者Job ID不同且那个JVM处于空闲状态。这说明,一个新的任务并不是一定要启动一个新的JVM,可能会利用已有的JVM继续运行。

如果spawnNewJvm=true,则表示可以启动新的JVM:

  1. if (spawnNewJvm) {
  2. if (runnerToKill != null) {
  3. LOG.info("Killing JVM: " + runnerToKill.jvmId);
  4. killJvmRunner(runnerToKill);
  5. }
  6. spawnNewJvm(jobId, env, t);
  7. return;
  8. }

接下来我们看看spawnNewJvm这个方法,其代码较简单:

  1. private void spawnNewJvm(JobID jobId, JvmEnv env,
  2. TaskRunner t) {
  3. JvmRunner jvmRunner = new JvmRunner(env, jobId, t.getTask());
  4. jvmIdToRunner.put(jvmRunner.jvmId, jvmRunner);
  5. jvmRunner.setDaemon(true);
  6. jvmRunner.setName("JVM Runner " + jvmRunner.jvmId + " spawned.");
  7. setRunningTaskForJvm(jvmRunner.jvmId, t);
  8. LOG.info(jvmRunner.getName());
  9. jvmRunner.start();
  10. }

首先创建一个JvmRunner对象,表示要运行的JVM,将该JVM记录下来与JVMID与JVMRunner的关系。JVMID是一个JobID加上随机生成的整数组合成的一个对象。因为JvmRunner对象是一个线程类,所以最后一句启动了一个新线程:JVMRunner。此时,TaskRunner线程类的工作结束。

5,JVMRunner的run方法继续执行,其run方法也很简单,就一句话:

  1. @Override
  2. public void run() {
  3. try {
  4. runChild(env);
  5. } catch (InterruptedException ie) {
  6. return;
  7. } catch (IOException e) {
  8. 。。。。。。。。。。。
  9. } catch (Throwable e) {
  10. 。。。。。。。。。
  11. } finally {
  12. jvmFinished();
  13. }
  14. }

在方法runChild里,关键的一句是:

  1. exitCode = tracker.getTaskController().launchTask(user,
  2. jvmId.jobId.toString(), taskAttemptIdStr, env.setup,
  3. env.vargs, env.workDir, env.stdout.toString(),
  4. env.stderr.toString());

于是进入DefaultTaskController的launchTask方法,该方法的作用是创建所有Task所需的目录,启动子JVM。

里面关键的代码是:

  1. // get the JVM command line.
  2. String cmdLine =
  3. TaskLog.buildCommandLine(setup, jvmArguments,
  4. new File(stdout), new File(stderr), logSize, true);
  5.  
  6. // write the command to a file in the
  7. // task specific cache directory
  8. // TODO copy to user dir
  9. Path p = new Path(allocator.getLocalPathForWrite(
  10. TaskTracker.getPrivateDirTaskScriptLocation(user, jobId, attemptId),
  11. getConf()), COMMAND_FILE);
  12.  
  13. String commandFile = writeCommand(cmdLine, rawFs, p);
  14. rawFs.setPermission(p, TaskController.TASK_LAUNCH_SCRIPT_PERMISSION);

cmdLin即生成要启动JVM的命令行字符串;第二句话是创建一个文件COMMAND_FILE,其值是COMMAND_FILE = "taskjvm.sh",即创建一个脚本文件,之后将命令行字符串写入该脚本文件,并设置权限。

最后,做好了这些准备工作后,执行脚本:

  1. shExec = new ShellCommandExecutor(new String[]{
  2. "bash", commandFile},
  3. currentWorkDirectory);
  4. shExec.execute();

ShellCommandExecutor是一个脚本命令的执行类,execute方法将执行以下代码:

  1. /** Execute the shell command. */
  2. public void execute() throws IOException {
  3. this.run();
  4. }
  5.  
  6. /** check to see if a command needs to be executed and execute if needed */
  7. protected void run() throws IOException {
  8. if (lastTime + interval > System.currentTimeMillis())
  9. return;
  10. exitCode = 0; // reset for next run
  11. runCommand();
  12. }

因此,进入runCommand方法,该方法使用了JAVA中提供的一个类:java.lang.ProcessBuilder,这个类用于创建操作系统进程。

首先设置环境变量等:

  1. if (environment != null) {
  2. builder.environment().putAll(this.environment);
  3. }
  4. if (dir != null) {
  5. builder.directory(this.dir);
  6. }

然后到了启动JVM的最关键一步,启动进程:

  1. process = builder.start();

等待进程运行:

  1. // wait for the process to finish and check the exit code
  2. exitCode = process.waitFor();

接下来就是新进程的运行,启动的是Child这个类的Main方法:

  1. public static void main(String[] args) throws Throwable {
  2. LOG.debug("Child starting");
  3. 。。。。。。

这个执行较长,我们留作下一节分析。

总结一下,本节主要分析了JobTracker将任务调度给TaskTracker后,主要执行了5步操作,对应于5个线程:

1)TaskTracker调用addToTaskQueue加入TaskLauncher(mapLauncher和reduceLauncher)的队列。在这个过程中,会创建对应的TaskInProgress对象加入到队列List<TaskInProgress> tasksToLaunch中,然后返回(这一部分操作是由TaskTracker的心跳线程执行的);

2)TaskLauncher这个线程类的run会不停地等待新的TaskInProgress,如果出现新的任务,进入startNewTask方法,startNewTask会创建一个新线程,TaskLauncher这个线程类返回去等待新的TaskInProgress任务;

3)startNewTask中的局部线程:从HDFS拷贝相应文件至本地等操作,该线程还会创建一个TaskRunner线程类,接着由其run方法执行;

4)TaskRunner线程类的run方法,负责获取JVM所需的参数信息,又启动JvmRunner这个线程类的run方法;

5)JvmRunner线程类的run方法,负责将JVM启动所需的命令写入一个脚本文件,执行该脚本文件,在另外一个进程中执行了Child的Main方法。

至于该Main进行了什么操作,留作后续分析。

MapReduce剖析笔记之六:TaskTracker初始化任务并启动JVM过程的更多相关文章

  1. MapReduce剖析笔记之四:TaskTracker通过心跳机制获取任务的流程

    上一节分析到了JobTracker把作业从队列里取出来并进行了初始化,所谓的初始化,主要是获取了Map.Reduce任务的数量,并统计了哪些DataNode所在的服务器可以处理哪些Split等等,将这 ...

  2. MapReduce剖析笔记之七:Child子进程处理Map和Reduce任务的主要流程

    在上一节我们分析了TaskTracker如何对JobTracker分配过来的任务进行初始化,并创建各类JVM启动所需的信息,最终创建JVM的整个过程,本节我们继续来看,JVM启动后,执行的是Child ...

  3. MapReduce剖析笔记之八: Map输出数据的处理类MapOutputBuffer分析

    在上一节我们分析了Child子进程启动,处理Map.Reduce任务的主要过程,但对于一些细节没有分析,这一节主要对MapOutputBuffer这个关键类进行分析. MapOutputBuffer顾 ...

  4. MapReduce剖析笔记之三:Job的Map/Reduce Task初始化

    上一节分析了Job由JobClient提交到JobTracker的流程,利用RPC机制,JobTracker接收到Job ID和Job所在HDFS的目录,够早了JobInProgress对象,丢入队列 ...

  5. MapReduce剖析笔记之二:Job提交的过程

    上一节以WordCount分析了MapReduce的基本执行流程,但并没有从框架上进行分析,这一部分工作在后续慢慢补充.这一节,先剖析一下作业提交过程. 在分析之前,我们先进行一下粗略的思考,如果要我 ...

  6. MapReduce剖析笔记之五:Map与Reduce任务分配过程

    在上一节分析了TaskTracker和JobTracker之间通过周期的心跳消息获取任务分配结果的过程.中间留了一个问题,就是任务到底是怎么分配的.任务的分配自然是由JobTracker做出来的,具体 ...

  7. MapReduce剖析笔记之一:从WordCount理解MapReduce的几个阶段

    WordCount是一个入门的MapReduce程序(从src\examples\org\apache\hadoop\examples粘贴过来的): package org.apache.hadoop ...

  8. Python源代码剖析笔记3-Python运行原理初探

    Python源代码剖析笔记3-Python执行原理初探 本文简书地址:http://www.jianshu.com/p/03af86845c95 之前写了几篇源代码剖析笔记,然而慢慢觉得没有从一个宏观 ...

  9. 【Visual C++】游戏编程学习笔记之六:多背景循环动画

    本系列文章由@二货梦想家张程 所写,转载请注明出处. 本文章链接:http://blog.csdn.net/terence1212/article/details/44264153 作者:ZeeCod ...

随机推荐

  1. ImageView缩放选项

    ImageView.ScaleType 将图片边界缩放到所在view边界时的缩放选项. Options for scaling the bounds of an image to the bounds ...

  2. jsp前端实现分页代码

    前端需要订一page类包装,其参数为 private Integer pageSize=10; //每页记录条数=10 private Integer totalCount; //总记录条数 priv ...

  3. HTML5 语义元素(一)页面结构

    本篇主要介绍HTML5增加的语义元素中关于页面结构方面的,包含: <article>.<aside>.<figure>.<figcaption>.< ...

  4. Android权限管理之Permission权限机制及使用

    前言: 最近突然喜欢上一句诗:"宠辱不惊,看庭前花开花落:去留无意,望天空云卷云舒." 哈哈~,这个和今天的主题无关,最近只要不学习总觉得生活中少了点什么,所以想着围绕着最近面试过 ...

  5. H5项目开发分享——用Canvas合成文字

    以前曾用Canvas合成.裁剪.图片等<用H5中的Canvas等技术制作海报>.这次用Canvas来画文字. 下图中"老王考到驾照后"这几个字是画在Canvas上的,与 ...

  6. java单向加密算法小结(1)--Base64算法

    从这一篇起整理一下常见的加密算法以及在java中使用的demo,首先从最简单的开始. 简单了解 Base64严格来说并不是一种加密算法,而是一种编码/解码的实现方式. 我们都知道,数据在计算机网络之间 ...

  7. [原] KVM 环境下MySQL性能对比

    KVM 环境下MySQL性能对比 标签(空格分隔): Cloud2.0 [TOC] 测试目的 对比MySQL在物理机和KVM环境下性能情况 压测标准 压测遵循单一变量原则,所有的对比都是只改变一个变量 ...

  8. 走进缓存的世界(三) - Memcache

    系列文章 走进缓存的世界(一) - 开篇 走进缓存的世界(二) - 缓存设计 走进缓存的世界(三) - Memcache 简介 Memcache是一个高性能的分布式内存对象缓存系统,用于动态Web应用 ...

  9. 面向组合子设计Coder

    面向组合子 面向组合子(Combanitor-Oriented),是最近帮我打开新世界大门的一种pattern.缘起haskell,又见monad与ParseC,终于ajoo前辈的几篇文章. 自去年9 ...

  10. Visual Studio Code 配置指南

    Visual Studio Code (简称 VS Code)是由微软研发的一款免费.开源的跨平台文本(代码)编辑器.在我看来它是「一款完美的编辑器」. 本文是有关 VS Code 的特性介绍与配置指 ...