目的
本文档从较高层面描述了实现YARN Applications 的方法。
概念和流程
一般概念是应用程序提交客户端向YARN ResourceManager(RM)提交应用程序。这可以通过设置YarnClient对象来完成。启动YarnClient后,客户端可以设置应用程序上下文,准备包含ApplicationMaster的应用程序的第一个容器(AM),然后提交申请。您需要提供信息,例如有关运行应用程序需要可用的本地文件 /jars 的详细信息,需要执行的实际命令(使用必要的命令行参数),任何OS环境设置(可选)有效地,您需要描述需要为ApplicationMaster启动的Unix进程。
然后,YARN ResourceManager将在已分配的容器上启动ApplicationMaster(如指定的那样)。ApplicationMaster与YARN集群通信,并处理应用程序执行。它以异步方式执行操作。在应用程序启动期间,ApplicationMaster的主要任务是:
a)与ResourceManager通信以协商和分配未来容器的资源;
b)在容器分配之后,通信YARN NodeManager(NM)以在其上启动应用程序容器。任务a)可以通过AMRMClientAsync对象异步执行,并使用AMRMClientAsync.CallbackHandler中指定的事件处理方法事件处理程序的类型。需要将事件处理程序显式设置为客户端。任务b)可以通过启动一个可运行的对象来执行,然后在分配容器时启动容器。作为启动此容器的一部分,AM必须指定具有启动信息的ContainerLaunchContext,例如命令行规范,环境等。
在执行应用程序期间,ApplicationMaster通过NMClientAsync对象与NodeManager进行通信。所有容器事件都由NMClientAsync.CallbackHandler处理,与NMClientAsync相关联。典型的回调处理程序处理客户端启动,停止,状态更新和错误。ApplicationMaster还通过处理AMRMClientAsync.CallbackHandler的getProgress()方法向ResourceManager报告执行进度。
除异步客户端外,还有某些工作流的同步版本(AMRMClient和NMClient)。
建议使用异步客户端,因为(主观上)更简单的用法,本文将主要介绍异步客户端。
有关同步客户端的更多信息,请参阅AMRMClient和NMClient。
接口
以下是重要的接口:
-
Client<-->ResourceManager
通过使用YarnClient对象。
-
ApplicationMaster<-->ResourceManager
通过使用AMRMClientAsync对象,由AMRMClientAsync.CallbackHandler异步处理事件
-
ApplicationMaster<-->NodeManager
发射容器。通过使用NodeManagers沟通NMClientAsync对象,处理容器事件由NMClientAsync.CallbackHandler
注意
-
YARN应用程序的三个主要协议(ApplicationClientProtocol,ApplicationMasterProtocol 和 ContainerManagementProtocol)仍然保留。3个客户端包装这3个协议,为YARN应用程序提供更简单的编程模型。
-
在极少数情况下,程序员可能希望直接使用3个协议来实现应用程序。但请注意,一般用例不再鼓励此类行为。
编写一个简单的YARN应用程序
写一个简单的客户端
-
客户端需要做的第一步是初始化并启动YarnClient。
YarnClient yarnClient = YarnClient.createYarnClient(); yarnClient.init(conf); yarnClient.start();
-
设置客户端后,客户端需要创建应用程序,并获取其应用程序ID。
YarnClientApplication app = yarnClient.createApplication(); GetNewApplicationResponse appResponse = app.getNewApplicationResponse();
-
YarnClientApplication对新应用程序的响应还包含有关群集的信息,例如群集的最小/最大资源功能。这是必需的,以确保您可以正确设置将启动ApplicationMaster的容器的规范。有关更多详细信息,请参阅GetNewApplicationResponse。
-
客户端的主要关键是设置ApplicationSubmissionContext,它定义了RM启动AM所需的所有信息。客户端需要将以下内容设置到上下文中:
-
Application 信息:id,name
-
队列,优先级信息:将向其提交应用程序的队列,为应用程序分配的优先级。
-
用户:提交申请的用户
-
ContainerLaunchContext:定义将在其中启动和运行AM的容器的信息。ContainerLaunchContext,如先前所提到的,定义了运行该应用程序所需的所有必需的信息,如 Resources (binaries, jars, files etc.), Environment 设置(CLASSPATH等),Command 和安全 Tokens (RECT)。
// set the application submission context
ApplicationSubmissionContext appContext = app.getApplicationSubmissionContext();
ApplicationId appId = appContext.getApplicationId();
appContext.setKeepContainersAcrossApplicationAttempts(keepContainers);
appContext.setApplicationName(appName);
// set local resources for the application master
// local files or archives as needed
// In this scenario, the jar file for the application master is part of the local resources
Map<String, LocalResource> localResources = new HashMap<String, LocalResource>();
LOG.info("Copy App Master jar from local filesystem and add to local environment");
// Copy the application master jar to the filesystem
// Create a local resource to point to the destination jar path
FileSystem fs = FileSystem.get(conf);
addToLocalResources(fs, appMasterJar, appMasterJarPath, appId.toString(),
localResources, null);
// Set the log4j properties if needed
if (!log4jPropFile.isEmpty()) {
addToLocalResources(fs, log4jPropFile, log4jPath, appId.toString(),
localResources, null);
}
// The shell script has to be made available on the final container(s)
// where it will be executed.
// To do this, we need to first copy into the filesystem that is visible
// to the yarn framework.
// We do not need to set this as a local resource for the application
// master as the application master does not need it.
String hdfsShellScriptLocation = "";
long hdfsShellScriptLen = 0;
long hdfsShellScriptTimestamp = 0;
if (!shellScriptPath.isEmpty()) {
Path shellSrc = new Path(shellScriptPath);
String shellPathSuffix =
appName + "/" + appId.toString() + "/" + SCRIPT_PATH;
Path shellDst =
new Path(fs.getHomeDirectory(), shellPathSuffix);
fs.copyFromLocalFile(false, true, shellSrc, shellDst);
hdfsShellScriptLocation = shellDst.toUri().toString();
FileStatus shellFileStatus = fs.getFileStatus(shellDst);
hdfsShellScriptLen = shellFileStatus.getLen();
hdfsShellScriptTimestamp = shellFileStatus.getModificationTime();
}
if (!shellCommand.isEmpty()) {
addToLocalResources(fs, null, shellCommandPath, appId.toString(),
localResources, shellCommand);
}
if (shellArgs.length > 0) {
addToLocalResources(fs, null, shellArgsPath, appId.toString(),
localResources, StringUtils.join(shellArgs, " "));
}
// Set the env variables to be setup in the env where the application master will be run
LOG.info("Set the environment for the application master");
Map<String, String> env = new HashMap<String, String>();
// put location of shell script into env
// using the env info, the application master will create the correct local resource for the
// eventual containers that will be launched to execute the shell scripts
env.put(DSConstants.DISTRIBUTEDSHELLSCRIPTLOCATION, hdfsShellScriptLocation);
env.put(DSConstants.DISTRIBUTEDSHELLSCRIPTTIMESTAMP, Long.toString(hdfsShellScriptTimestamp));
env.put(DSConstants.DISTRIBUTEDSHELLSCRIPTLEN, Long.toString(hdfsShellScriptLen));
// Add AppMaster.jar location to classpath
// At some point we should not be required to add
// the hadoop specific classpaths to the env.
// It should be provided out of the box.
// For now setting all required classpaths including
// the classpath to "." for the application jar
StringBuilder classPathEnv = new StringBuilder(Environment.CLASSPATH.$$())
.append(ApplicationConstants.CLASS_PATH_SEPARATOR).append("./*");
for (String c : conf.getStrings(
YarnConfiguration.YARN_APPLICATION_CLASSPATH,
YarnConfiguration.DEFAULT_YARN_CROSS_PLATFORM_APPLICATION_CLASSPATH)) {
classPathEnv.append(ApplicationConstants.CLASS_PATH_SEPARATOR);
classPathEnv.append(c.trim());
}
classPathEnv.append(ApplicationConstants.CLASS_PATH_SEPARATOR).append(
"./log4j.properties");
// Set the necessary command to execute the application master
Vector<CharSequence> vargs = new Vector<CharSequence>(30);
// Set java executable command
LOG.info("Setting up app master command");
vargs.add(Environment.JAVA_HOME.$$() + "/bin/java");
// Set Xmx based on am memory size
vargs.add("-Xmx" + amMemory + "m");
// Set class name
vargs.add(appMasterMainClass);
// Set params for Application Master
vargs.add("--container_memory " + String.valueOf(containerMemory));
vargs.add("--container_vcores " + String.valueOf(containerVirtualCores));
vargs.add("--num_containers " + String.valueOf(numContainers));
vargs.add("--priority " + String.valueOf(shellCmdPriority));
for (Map.Entry<String, String> entry : shellEnv.entrySet()) {
vargs.add("--shell_env " + entry.getKey() + "=" + entry.getValue());
}
if (debugFlag) {
vargs.add("--debug");
}
vargs.add("1>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/AppMaster.stdout");
vargs.add("2>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/AppMaster.stderr");
// Get final command
StringBuilder command = new StringBuilder();
for (CharSequence str : vargs) {
command.append(str).append(" ");
}
LOG.info("Completed setting up app master command " + command.toString());
List<String> commands = new ArrayList<String>();
commands.add(command.toString());
// Set up the container launch context for the application master
ContainerLaunchContext amContainer = ContainerLaunchContext.newInstance(
localResources, env, commands, null, null, null);
// Set up resource type requirements
// For now, both memory and vcores are supported, so we set memory and
// vcores requirements
Resource capability = Resource.newInstance(amMemory, amVCores);
appContext.setResource(capability);
// Service data is a binary blob that can be passed to the application
// Not needed in this scenario
// amContainer.setServiceData(serviceData);
// Setup security tokens
if (UserGroupInformation.isSecurityEnabled()) {
// Note: Credentials class is marked as LimitedPrivate for HDFS and MapReduce
Credentials credentials = new Credentials();
String tokenRenewer = conf.get(YarnConfiguration.RM_PRINCIPAL);
if (tokenRenewer == null | | tokenRenewer.length() == 0) {
throw new IOException(
"Can't get Master Kerberos principal for the RM to use as renewer");
}
// For now, only getting tokens for the default file-system.
final Token<?> tokens[] =
fs.addDelegationTokens(tokenRenewer, credentials);
if (tokens != null) {
for (Token<?> token : tokens) {
LOG.info("Got dt for " + fs.getUri() + "; " + token);
}
}
DataOutputBuffer dob = new DataOutputBuffer();
credentials.writeTokenStorageToStream(dob);
ByteBuffer fsTokens = ByteBuffer.wrap(dob.getData(), 0, dob.getLength());
amContainer.setTokens(fsTokens);
}
appContext.setAMContainerSpec(amContainer);
- 设置过程完成后,客户端就可以提交具有指定优先级和队列的应用程序。
// Set the priority for the application master
Priority pri = Priority.newInstance(amPriority);
appContext.setPriority(pri);
// Set the queue to which this application is to be submitted in the RM
appContext.setQueue(amQueue);
// Submit the application to the applications manager
// SubmitApplicationResponse submitResp = applicationsManager.submitApplication(appRequest);
yarnClient.submitApplication(appContext);
-
此时,RM将接受该应用程序,并在后台进行分配具有所需规范的容器的过程,然后最终在分配的容器上设置和启动AM。
-
客户端可以通过多种方式跟踪实际任务的进度。
- 它可以与RM通信,并通过YarnClient的getApplicationReport()方法请求应用程序的报告。
// Get application report for the appId we are interested in
ApplicationReport report = yarnClient.getApplicationReport(appId);
从RM收到的ApplicationReport包含以下内容:
常规应用程序信息:应用程序ID,提交应用程序的队列,提交应用程序的用户以及应用程序的开始时间。
ApplicationMaster details:运行AM的主机,侦听客户端请求的rpc端口(如果有)以及客户端需要与AM通信的令牌。
Application tracking information:如果应用程序支持某种形式的进度跟踪,则可以设置跟踪URL,该URL可通过ApplicationReport的getTrackingUrl()方法获得,客户端可以查看该方法以监控进度。
Application status:ResourceManager可以通过ApplicationReport#getYarnApplicationState获得应用程序的状态。如果YarnApplicationState设置为FINISHED,则客户端应引用ApplicationReport#getFinalApplicationStatus来检查应用程序任务本身的实际成功/失败。如果出现故障,ApplicationReport#getDiagnostics可能有助于更好地了解故障。
- 如果ApplicationMaster支持它,客户端可以通过主机直接查询AM本身以获取进度更新:从应用程序报告中获取的rpcport信息。它还可以使用从报告中获取的跟踪网址(如果有)。
- 在某些情况下,如果申请时间过长或由于其他因素,客户可能希望终止申请。YarnClient支持killApplication调用,该调用允许客户端通过ResourceManager向AM发送终止信号。如果设计的ApplicationMaster也可以通过其客户端可能能够利用的rpc层支持中止调用。
yarnClient.killApplication(appId);
编写ApplicationMaster(AM)
-
AM是工作的实际所有者。它将由RM启动,并将通过客户提供有关其负责监督和完成的工作的所有必要信息和资源。
-
由于AM是在一个可能(可能会)与其他容器共享物理主机的容器中启动的,考虑到多租户性质,除了其他问题之外,它不能对预先配置的端口这样的东西进行任何假设,它可以监听上。
-
当AM启动时,通过环境可以使用几个参数。其中包括AM容器的ContainerId,应用程序提交时间以及运行ApplicationMaster的NM(NodeManager)主机的详细信息。参考名称的参考ApplicationConstants。
-
与RM的所有交互都需要ApplicationAttemptId(如果出现故障,每个应用程序可能会有多次尝试)。该ApplicationAttemptId可以从AM的容器ID来获得。有辅助API将从环境中获取的值转换为对象。
Map<String, String> envs = System.getenv();
String containerIdString =
envs.get(ApplicationConstants.AM_CONTAINER_ID_ENV);
if (containerIdString == null) {
// container id should always be set in the env by the framework
throw new IllegalArgumentException(
"ContainerId not set in the environment");
}
ContainerId containerId = ConverterUtils.toContainerId(containerIdString);
ApplicationAttemptId appAttemptID = containerId.getApplicationAttemptId();
- 在AM完全初始化之后,我们可以启动两个客户端:一个到ResourceManager,一个到NodeManager。我们使用自定义的事件处理程序设置它们,我们将在本文后面详细讨论这些事件处理程序。
AMRMClientAsync.CallbackHandler allocListener = new RMCallbackHandler();
amRMClient = AMRMClientAsync.createAMRMClientAsync(1000, allocListener);
amRMClient.init(conf);
amRMClient.start();
containerListener = createNMCallbackHandler();
nmClientAsync = new NMClientAsyncImpl(containerListener);
nmClientAsync.init(conf);
nmClientAsync.start();
- AM必须向RM发出心跳信号,以便让它知道AM已存在且仍在运行。RM的超时到期间隔由可通过YarnConfiguration.RM_AM_EXPIRY_INTERVAL_MS访问的配置设置定义,默认值由YarnConfiguration.DEFAULT_RM_AM_EXPIRY_INTERVAL_MS定义。ApplicationMaster需要使用ResourceManager注册自己以开始心跳。
// Register self with ResourceManager
// This will start heartbeating to the RM
appMasterHostname = NetUtils.getHostname();
RegisterApplicationMasterResponse response = amRMClient
.registerApplicationMaster(appMasterHostname, appMasterRpcPort,
appMasterTrackingUrl);
- 在注册的响应中,如果包括最大资源能力。您可能希望使用它来检查应用程序的请求。
// Dump out information about cluster capability as seen by the
// resource manager
int maxMem = response.getMaximumResourceCapability().getMemory();
LOG.info("Max mem capability of resources in this cluster " + maxMem);
int maxVCores = response.getMaximumResourceCapability().getVirtualCores();
LOG.info("Max vcores capability of resources in this cluster " + maxVCores);
// A resource ask cannot exceed the max.
if (containerMemory > maxMem) {
LOG.info("Container memory specified above max threshold of cluster."
+ " Using max value." + ", specified=" + containerMemory + ", max="
+ maxMem);
containerMemory = maxMem;
}
if (containerVirtualCores > maxVCores) {
LOG.info("Container virtual cores specified above max threshold of cluster."
+ " Using max value." + ", specified=" + containerVirtualCores + ", max="
+ maxVCores);
containerVirtualCores = maxVCores;
}
List<Container> previousAMRunningContainers =
response.getContainersFromPreviousAttempts();
LOG.info("Received " + previousAMRunningContainers.size()
+ " previous AM's running containers on AM registration.");
- 根据任务要求,AM可以要求一组容器来运行其任务。我们现在可以计算出我们需要多少个容器,并请求那些容器。
List<Container> previousAMRunningContainers =
response.getContainersFromPreviousAttempts();
LOG.info("Received " + previousAMRunningContainers.size()
+ " previous AM's running containers on AM registration.");
int numTotalContainersToRequest =
numTotalContainers - previousAMRunningContainers.size();
// Setup ask for containers from RM
// Send request for containers to RM
// Until we get our fully allocated quota, we keep on polling RM for
// containers
// Keep looping until all the containers are launched and shell script
// executed on them ( regardless of success/failure).
for (int i = 0; i < numTotalContainersToRequest; ++i) {
ContainerRequest containerAsk = setupContainerAskForRM();
amRMClient.addContainerRequest(containerAsk);
}
- 在setupContainerAskForRM()中,以下两件事需要一些设置:
资源功能:目前,YARN支持基于内存的资源需求,因此请求应定义需要多少内存。该值以MB为单位定义,并且必须小于群集的最大容量和min能力的精确倍数。内存资源对应于对任务容器施加的物理内存限制。它还将支持基于计算的资源(vCore),如代码所示。
优先级:当询问容器集时,AM可以为每个集定义不同的优先级。例如,Map-Reduce AM可以为Map任务所需的容器分配更高的优先级,为Reduce任务的容器分配较低的优先级。
private ContainerRequest setupContainerAskForRM() {
// setup requirements for hosts
// using * as any host will do for the distributed shell app
// set the priority for the request
Priority pri = Priority.newInstance(requestPriority);
// Set up resource type requirements
// For now, memory and CPU are supported so we set memory and cpu requirements
Resource capability = Resource.newInstance(containerMemory,
containerVirtualCores);
ContainerRequest request = new ContainerRequest(capability, null, null,
pri);
LOG.info("Requested container ask: " + request.toString());
return request;
}
- 在应用程序管理器发送容器分配请求之后,将通过AMRMClientAsync客户端的事件处理程序异步启动容器。处理程序应该实现AMRMClientAsync.CallbackHandler接口。
- 当分配了容器时,处理程序设置一个运行代码以启动容器的线程。这里我们使用名称LaunchContainerRunnable来演示。我们将在本文的以下部分讨论LaunchContainerRunnable类。
@Override
public void onContainersAllocated(List<Container> allocatedContainers) {
LOG.info("Got response from RM for container ask, allocatedCnt="
+ allocatedContainers.size());
numAllocatedContainers.addAndGet(allocatedContainers.size());
for (Container allocatedContainer : allocatedContainers) {
LaunchContainerRunnable runnableLaunchContainer =
new LaunchContainerRunnable(allocatedContainer, containerListener);
Thread launchThread = new Thread(runnableLaunchContainer);
// launch and start the container on a separate thread to keep
// the main thread unblocked
// as all containers may not be allocated at one go.
launchThreads.add(launchThread);
launchThread.start();
}
}
- 在心跳时,事件处理程序报告应用程序的进度。
@Override
public float getProgress() {
// set progress to deliver to RM on next heartbeat
float progress = (float) numCompletedContainers.get()
/ numTotalContainers;
return progress;
}
- 容器启动线程实际上在NM上启动容器。在将容器分配给AM之后,它需要遵循客户端在为将要在分配的Container上运行的最终任务设置ContainerLaunchContext时所遵循的类似过程。一旦定义了ContainerLaunchContext,AM就可以通过NMClientAsync启动它。
// Set the necessary command to execute on the allocated container
Vector<CharSequence> vargs = new Vector<CharSequence>(5);
// Set executable command
vargs.add(shellCommand);
// Set shell script path
if (!scriptPath.isEmpty()) {
vargs.add(Shell.WINDOWS ? ExecBatScripStringtPath
: ExecShellStringPath);
}
// Set args for the shell command if any
vargs.add(shellArgs);
// Add log redirect params
vargs.add("1>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout");
vargs.add("2>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr");
// Get final command
StringBuilder command = new StringBuilder();
for (CharSequence str : vargs) {
command.append(str).append(" ");
}
List<String> commands = new ArrayList<String>();
commands.add(command.toString());
// Set up ContainerLaunchContext, setting local resource, environment,
// command and token for constructor.
// Note for tokens: Set up tokens for the container too. Today, for normal
// shell commands, the container in distribute-shell doesn't need any
// tokens. We are populating them mainly for NodeManagers to be able to
// download anyfiles in the distributed file-system. The tokens are
// otherwise also useful in cases, for e.g., when one is running a
// "hadoop dfs" command inside the distributed shell.
ContainerLaunchContext ctx = ContainerLaunchContext.newInstance(
localResources, shellEnv, commands, null, allTokens.duplicate(), null);
containerListener.addContainer(container.getId(), container);
nmClientAsync.startContainerAsync(container, ctx);
-
该NMClientAsync对象,其事件处理一起,处理容器事件。包括容器启动,停止,状态更新,并发生错误。
-
在ApplicationMaster确定工作完成后,它需要通过AM-RM客户端取消注册,然后停止客户端。
try {
amRMClient.unregisterApplicationMaster(appStatus, appMessage, null);
} catch (YarnException ex) {
LOG.error("Failed to unregister application", ex);
} catch (IOException e) {
LOG.error("Failed to unregister application", e);
}
amRMClient.stop();
常问问题
如何将应用程序的jar分发给需要它的YARN集群中的所有节点?
您可以使用LocalResource向应用程序请求添加资源。这将导致YARN将资源分发到ApplicationMaster节点。如果资源是tgz,zip或jar - 您可以让YARN解压缩它。然后,您需要做的就是将解压缩的文件夹添加到类路径中。例如,在创建应用程序请求时:
File packageFile = new File(packagePath);
URL packageUrl = ConverterUtils.getYarnUrlFromPath(
FileContext.getFileContext().makeQualified(new Path(packagePath)));
packageResource.setResource(packageUrl);
packageResource.setSize(packageFile.length());
packageResource.setTimestamp(packageFile.lastModified());
packageResource.setType(LocalResourceType.ARCHIVE);
packageResource.setVisibility(LocalResourceVisibility.APPLICATION);
resource.setMemory(memory);
containerCtx.setResource(resource);
containerCtx.setCommands(ImmutableList.of(
"java -cp './package/*' some.class.to.Run "
+ "1>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout "
+ "2>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr"));
containerCtx.setLocalResources(
Collections.singletonMap("package", packageResource));
appCtx.setApplicationId(appId);
appCtx.setUser(user.getShortUserName);
appCtx.setAMContainerSpec(containerCtx);
yarnClient.submitApplication(appCtx);
如您所见,setLocalResources命令将名称映射到资源。该名称成为应用程序的cwd中的sym链接,因此您可以使用./package/*来引用内部的工件。
注意:Java的classpath(cp)参数非常敏感。确保您的语法完全正确。
将程序包分发到AM后,只要AM启动新容器(假设您希望将资源发送到容器),就需要执行相同的过程。这个代码是一样的。您只需要确保为AM提供包路径(HDFS或本地),以便它可以将容器ctx与资源URL一起发送。
我如何获得ApplicationMaster的ApplicationAttemptId?
该ApplicationAttemptId将通过环境被传递到AM以及从环境中的值可以被转换成一个ApplicationAttemptId经由ConverterUtils辅助函数的对象。
为什么我的容器被NodeManager杀死了?
这可能是由于高内存使用量超过了您请求的容器内存大小。造成这种情况的原因有很多。首先,查看NodeManager在杀死容器时转储的进程树。您感兴趣的两件事是物理内存和虚拟内存。如果您已超出物理内存限制,则您的应用程序使用的物理内存过多。如果您正在运行Java应用程序,则可以使用-hprof查看占用堆空间的内容。如果已超出虚拟内存,则可能需要增加群集范围配置变量yarn.nodemanager.vmem-pmem-ratio的值。
如何包含本机库?
在启动容器时在命令行上设置-Djava.library.path会导致Hadoop使用的本机库无法正确加载并导致错误。使用LD_LIBRARY_PATH更清晰。
有用的链接
示例代码
YARN分布式shell:在设置开发环境后的hadoop-yarn-applications-distributedshell项目中。
原文链接: https://hadoop.apache.org/docs/r3.2.0/