本文翻译自hadoop官方文档:Hadoop MapReduce Next Generation – Writing YARN Applications
原文地址:http://www.rigongyizu.com/hadoop-mapreduce-next-generation-writing-yarn-applications/
目的
本文在一个比较高的层面上描述了如何在YARN上开发一个新的应用程序。
概念和流程
一般的概念就是“Application Submission Client”提交一个”Application”到YARN的Resource Manager。客户端(client)与ResourceManager之间通过”ClientRMProtocol”协议进行通信。如果有需要,客户端通过 ClientRMProtocol#getNewApplication 调用来获得一个新的“ApplicationId”,接着通过调用 ClientRMProtocol#submitApplication 来提交任务(Application)。作为 ClientRMProtocol#submitApplication 调用的一部分,客户端需要提供足够的信息给ResourceManager来启动应用程序的第一个container,即ApplicationMaster。客户端需要提供的信息包括任务运行所需要的本地文件,jar包,真正需要执行的命令(和必需的命令行参数),以及unix环境设置(可选)等。事实上,你需要为启动ApplicationMaster提供Unix进程信息。
YARN的ResourceManager接着会在RM分配的第一个container上启动指定的ApplicationMaster。ApplicationMaster与ResourceManager之间会通过‘AMRMProtocol’协议通信。首先,ApplicationMaster需要将自己注册到ResourceManager上。为了完成分配的任务,ApplicationMaster接着会通过 AMRMProtocol#allocate 协议请求请求和接受containers。如果分配到了container,ApplicationMaster就会通过 ContainerManager#startContainer 协议与NodeManager通信,来启动container。作为启动container的一部分,ApplicationMaster需要指定类似于ApplicationSubmissionContext的ContainerLaunchContext,里面包含了启动container所需的信息,比如命令行,环境变量等等。一旦任务完成,ApplicationMaster就会通过 AMRMProtocol#finishApplicationMaster 协议告知ResourceManager任务完成了。
同时,客户端可通过查询ResourceManager来监控应用的状态,或者如果ApplicationMaster支持这种调用服务也可以直接从ApplicationMaster来查询信息。如果有必要,客户端通过 ClientRMProtocol#forceKillApplication 也能杀死应用。
接口
你最需要关心的接口有:
- ClientRMProtocol – ClientResourceManager
这是客户端和ResourceManager之间的通信协议,可以用来启动一个新的应用(如ApplicationMaster),检查应用状态和杀死应用。例如,在gateway机器上提交job的客户端一般使用这个协议。 - AMRMProtocol – ApplicationMasterResourceManager
这是ApplicationMaster和ResourceManager之间的通信协议,ApplicationMaster通过这个协议可以向ResourceManager注册和注销自己,还能从Scheduler处请求资源以完成任务。 - ContainerManager – ApplicationMasterNodeManager
这是ApplicationMaster和NodeManager直接的通信协议,ApplicationMaster通过它来告诉NodeManager启动/停止container,如果有需要能从NodeManager处获取container的任务状态更新信息。
编写一个简单的Yarn应用程序
编写一个简单的客户端
- 客户端第一步需要做的是要连接到ResourceManager,具体连接的是ResourceManager的ApplicationsManager(AsM)接口。
ClientRMProtocol applicationsManager; |
YarnConfiguration yarnConf = new YarnConfiguration(conf); |
InetSocketAddress rmAddress = |
NetUtils.createSocketAddr(yarnConf.get( |
YarnConfiguration.RM_ADDRESS, |
YarnConfiguration.DEFAULT_RM_ADDRESS)); |
LOG.info( "Connecting to ResourceManager at " + rmAddress); |
configuration appsManagerServerConf = new Configuration(conf); |
appsManagerServerConf.setClass( |
YarnConfiguration.YARN_SECURITY_INFO, |
ClientRMSecurityInfo. class , SecurityInfo. class ); |
applicationsManager = ((ClientRMProtocol) rpc.getProxy( |
ClientRMProtocol. class , rmAddress, appsManagerServerConf)); |
- 一旦ASM的handler获取之后,客户端需要向ResourceManager请求一个新的ApplicationId。
GetNewApplicationRequest request = |
Records.newRecord(GetNewApplicationRequest. class ); |
GetNewApplicationResponse response = |
applicationsManager.getNewApplication(request); |
LOG.info( "Got new ApplicationId=" + response.getApplicationId()); |
- 从ASM返回的接口包括了集群的信息,例如最小/最大资源容易等。有了这些信息才能正确的设置container的参数,以启动ApplicationMaster。可以参考GetNewApplicationResponse以获取更多的信息。
- 客户端的关键工作是设置ApplicationSubmissionContext,它定义了ResourceManager所需要的启动ApplicationMaster的所有信息:
1). Application Info: id, name
2). Queue, Priority info: 应用将要被提交的队列,以及应用要被赋予的优先级。
3). User: 提交应用的用户
4). ContainerLaunchContext: 定义了启动container需要的信息,ApplicationMaster会在这个container上运行。ContainerLaunchContext正如前面所描述的,定义了所有启动ApplicationMaster需要的信息,例如本地资源(二进制文件,jar包,文件等),, security tokens, 环境变量 (CLASSPATH etc.) 和被执行的命令。
ApplicationSubmissionContext appContext = |
Records.newRecord(ApplicationSubmissionContext. class ); |
appContext.setApplicationId(appId); |
appContext.setApplicationName(appName); |
ContainerLaunchContext amContainer = |
Records.newRecord(ContainerLaunchContext. class ); |
Map<String, LocalResource> localResources = |
new HashMap<String, LocalResource>(); |
FileStatus jarStatus = fs.getFileStatus(jarPath); |
LocalResource amJarRsrc = Records.newRecord(LocalResource. class ); |
amJarRsrc.setType(LocalResourceType.FILE); |
amJarRsrc.setVisibility(LocalResourceVisibility.APPLICATION); |
amJarRsrc.setResource(ConverterUtils.getYarnUrlFromPath(jarPath)); |
amJarRsrc.setTimestamp(jarStatus.getModificationTime()); |
amJarRsrc.setSize(jarStatus.getLen()); |
localResources.put( "AppMaster.jar" , amJarRsrc); |
amContainer.setLocalResources(localResources); |
Map<String, String> env = new HashMap<String, String>(); |
String classPathEnv = "$CLASSPATH:./*:" ; |
env.put( "CLASSPATH" , classPathEnv); |
amContainer.setEnvironment(env); |
"${JAVA_HOME}" + /bin/java" + |
" 1>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout" + |
" 2>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr" ; |
List<String> commands = new ArrayList<String>(); |
amContainer.setCommands(commands); |
Resource capability = Records.newRecord(Resource. class ); |
capability.setMemory(amMemory); |
amContainer.setResource(capability); |
appContext.setAMContainerSpec(amContainer); |
- 在设置完进程信息后,客户端最后准备好了提交任务到ASM。
SubmitApplicationRequest appRequest = |
Records.newRecord(SubmitApplicationRequest. class ); |
appRequest.setApplicationSubmissionContext(appContext); |
applicationsManager.submitApplication(appRequest); |
- 这时,ResourceManager将会接受这个任务,在后台根据获取的参数分配一个container,并且在这个container上启动ApplicationMaster。
- 客户端有多种方法可以监控任务的实际进度。
1). 客户端可以通过 ClientRMProtocol#getApplicationReport 与ResourceManager通信来请求获取任务的状态。
GetApplicationReportRequest reportRequest = |
Records.newRecord(GetApplicationReportRequest. class ); |
reportRequest.setApplicationId(appId); |
GetApplicationReportResponse reportResponse = |
applicationsManager.getApplicationReport(reportRequest); |
ApplicationReport report = reportResponse.getApplicationReport(); |
从ResourceManager获取的任务状态报告ApplicationReport包括如下信息:
(1.1). 一般性的任务信息: ApplicationId, ApplicationId,application被提交到的queue,提交application的user,application开始的时间
(1.2). ApplicationMaster的详细信息: ApplicationMaster运行的主机,提供给client连接的rpc端口(如果有),以及client与ApplicationManager通讯需要的一个令牌(token).
(1.3). Application的监控信息: 如果任务支持某种类型的进程监控,它可以设置监控的url,客户端可以通过 ApplicationReport#getTrackingUrl 来获取url,并通过这个url来监控progress的状态.
(1.4). ApplicationStatus: ResourceManager能够看到的一些任务的状态,可以通过 Application#getYarnApplicationState 得到是否YarnApplicationState被设置为FINISHED,客户端可以通过 ApplicationReport#getFinalApplicationStatus 来check 任务的成功/失败。在失败时,ApplicationReport#getDiagnostics 可以提供一些关于失败的信息。
2). 如果ApplicationMaster支持,客户端可以直接通过ApplicationReport中包含的host:rpcport来查询ApplicationMaster以获得进程更新信息。如果能得到racking url,也能用于获取状态信息。
- 在特定条件下,如果任务花费了太长时间或者其他因素,客户端可能希望终止任务。ClientRMProtocol协议支持forceKillApplication调用,允许客户端通过ResourceManager给ApplicationMaster发送一个kill消息。ApplicationMaster也可以通过设计为客户端提供abort调用,那么客户端就能通过rpc调用来终止任务了。
KillApplicationRequest killRequest = |
Records.newRecord(KillApplicationRequest. class ); |
killRequest.setApplicationId(appId); |
applicationsManager.forceKillApplication(killRequest); |
编写ApplicationMaster
- ApplicationMaster是任务的实际拥有者。它由客户端通过ResouceManager启动,客户端提供了job运行需要的所有必要的信息和资源。ApplicationMaster负责任务的监控和相关工作的完成。
- 启动于某个container内的ApplicationMaster在多用户环境下可能与其他container运行在相同的物理主机上,因此它无法使用预先配置的端口来监听。
- 当ApplicationMaster启动时,可以通过环境变量来获得一些参数,例如:ApplicationMaster所在container的ContainerId,任务提交的时间,以及运行 ApplicationMaster的NodeManger主机的详细信息,可以查阅ApplicationConstants来获得参数名称。
- 所有与ResouceManager的交互需要一个ApplicationAttemptId(如果任务失败可能会有多次重试)。ApplicationAttemptId能够通过ApplicationMaster的containerId来获得。有些辅助的API可以将从环境变量获得的值转换为对象。
Map<String, String> envs = System.getenv(); |
String containerIdString = |
envs.get(ApplicationConstants.AM_CONTAINER_ID_ENV); |
if (containerIdString == null ) { |
throw new IllegalArgumentException( |
"ContainerId not set in the environment" ); |
ContainerId containerId = ConverterUtils.toContainerId(containerIdString); |
ApplicationAttemptId appAttemptID = containerId.getApplicationAttemptId(); |
- ApplicationMaster初始化完成后,可以通过 ARMRMProtocol#registerApplicationMaster 来向ResourceManager注册。ApplicationMaster经常通过ResouceManager的Scheduler接口与之通讯。
YarnConfiguration yarnConf = new YarnConfiguration(conf); |
InetSocketAddress rmAddress = |
NetUtils.createSocketAddr(yarnConf.get( |
YarnConfiguration.RM_SCHEDULER_ADDRESS, |
YarnConfiguration.DEFAULT_RM_SCHEDULER_ADDRESS)); |
LOG.info( "Connecting to ResourceManager at " + rmAddress); |
AMRMProtocol resourceManager = |
(AMRMProtocol) rpc.getProxy(AMRMProtocol. class , rmAddress, conf); |
RegisterApplicationMasterRequest appMasterRequest = |
Records.newRecord(RegisterApplicationMasterRequest. class ); |
appMasterRequest.setApplicationAttemptId(appAttemptID); |
appMasterRequest.setHost(appMasterHostname); |
appMasterRequest.setRpcPort(appMasterRpcPort); |
appMasterRequest.setTrackingUrl(appMasterTrackingUrl); |
RegisterApplicationMasterResponse response = |
resourceManager.registerApplicationMaster(appMasterRequest); |
- ApplicationMaster需要发出心跳给ResouceManager,表示ApplicationMaster还活着且正在运行。在ResouceManager端设置的超时时间可以通过YarnConfiguration.RM_AM_EXPIRY_INTERVAL_MS来访问,缺省值为YarnConfiguration.DEFAULT_RM_AM_EXPIRY_INTERVAL_MS。对ResouceManager的 AMRMProtocol#allocate 调用可以作为心跳,它还支持发送进度更新信息。因此,一次不请求任何container和不包含进度更新信息的allocate调用,对ResourceManager来说,是一种有效的发送心跳方式。
- 按照任务的需求,ApplicationMaster可以申请一系列containers来运行任务。ApplicationMaster使用ResouceRequest类来指定container的规格:
1). hostname:如果container需要host在特定的rack或主机上,需要设定这个参数,其中“*”代表container可以分配在任何主机上。
2). Resouce capability:目前的YARN版本只支持基于内存的资源分配,因此资源请求只需要定义任务需要多少内存。内存的值以MB为单位,必须小于集群的最大容量,且是最小容量的整数倍。内存资源是以子任务的物理内存使用来设定限制的。
3). Priority:当申请到一些container时,ApplicationMaster可以给不同组的container设置不同的优先级,例如,对于Map-Reduce任务来说,ApplicationMaster可以给map任务的container指定比较高的优先级,而给reduce任务的container指定比较低的优先级。
ResourceRequest rsrcRequest = Records.newRecord(ResourceRequest. class ); |
rsrcRequest.setHostName( "*" ); |
Priority pri = Records.newRecord(Priority. class ); |
pri.setPriority(requestPriority); |
rsrcRequest.setPriority(pri); |
Resource capability = Records.newRecord(Resource. class ); |
capability.setMemory(containerMemory); |
rsrcRequest.setCapability(capability); |
rsrcRequest.setNumContainers(numContainers); |
- 在定义了container的资源请求对象requirement以后,ApplicationMaster需要构建AllocateRequest发送到ResourceManager。AllocateRequest包括:
1). Requested containers:container的说明和ApplicationMaster从ResourceManager处申请的container的数量
2). Released containers:在某些情况下,ApplicationMaster可能申请了过多的container或者由于运行失败,决定使用其他已经分配给它的containers,这时它可以返还那些不用的container给ResourceManager,这些container可以分配给其他的应用使用。
3). ResponseId:在allocate调用时保持在response当中的response id
4). Progress update information:ApplicationMaster可以发送进度更新信息给ResourceManager(取值范围在0到1直接)。
List<ResourceRequest> requestedContainers; |
List<ContainerId> releasedContainers |
AllocateRequest req = Records.newRecord(AllocateRequest. class ); |
req.setResponseId(rmRequestID); |
req.setApplicationAttemptId(appAttemptID); |
req.addAllAsks(requestedContainers); |
req.addAllReleases(releasedContainers); |
req.setProgress(currentProgress); |
AllocateResponse allocateResponse = resourceManager.allocate(req); |
- ResourceManager返回的AllocateResponse通过AMResponse对象包含了下面这些信息:
1). Reboot flag(重启标志):针对ApplicationMaster失去了和ResourceManager同步的场景
2). Allocated containers:分配给ApplicationMaster的containers
3). Headroom:整个集群的资源上限。基于这个信息和自身的资源需求,ApplicationMaster可以灵活的调整子任务的优先级以充分利用已经获得的containers,或者在无法获得资源时,能够尽快的脱离困境。
4). Completed containers:当ApplicationMaster启动了一个获得的container后,当这个container完成后,它将接收到来自ResourceManager的更新信息。ApplicationMaster能够查看完成的container的状态信息,并采取适当的策略,比如重试某个失败的任务。
有一点需要注意的是,container不一定会立即分配给ApplicationMaster。这不意味着ApplicationMaster需要持续不断的请求没有获得的containers。一旦allocate request被发送了,在考虑到集群容量、优先级和调度策略的条件下,ApplicationMaster最终会获得container。ApplicationMaster只有在它原有的请求数量有变化,需要新增container时,才需要再次发送资源请求。
AMResponse amResp = allocateResponse.getAMResponse(); |
List<Container> allocatedContainers = amResp.getAllocatedContainers(); |
for (Container allocatedContainer : allocatedContainers) { |
LOG.info( "Launching shell command on a new container." |
+ ", containerId=" + allocatedContainer.getId() |
+ ", containerNode=" + allocatedContainer.getNodeId().getHost() |
+ ":" + allocatedContainer.getNodeId().getPort() |
+ ", containerNodeURI=" + allocatedContainer.getNodeHttpAddress() |
+ ", containerState" + allocatedContainer.getState() |
+ ", containerResourceMemory" |
+ allocatedContainer.getResource().getMemory()); |
LaunchContainerRunnable runnableLaunchContainer = |
new LaunchContainerRunnable(allocatedContainer); |
Thread launchThread = new Thread(runnableLaunchContainer); |
launchThreads.add(launchThread); |
Resource availableResources = amResp.getAvailableResources(); |
List<ContainerStatus> completedContainers = |
amResp.getCompletedContainersStatuses(); |
for (ContainerStatus containerStatus : completedContainers) { |
LOG.info( "Got container status for containerID= " |
+ containerStatus.getContainerId() |
+ ", state=" + containerStatus.getState() |
+ ", exitStatus=" + containerStatus.getExitStatus() |
+ ", diagnostics=" + containerStatus.getDiagnostics()); |
int exitStatus = containerStatus.getExitStatus(); |
if (- 100 != exitStatus) { |
numCompletedContainers.incrementAndGet(); |
numFailedContainers.incrementAndGet(); |
numRequestedContainers.decrementAndGet(); |
numCompletedContainers.incrementAndGet(); |
numSuccessfulContainers.incrementAndGet(); |
- 当一个container分配给ApplicationMaster以后,ApplicationMaster需要做和Client类似的过程来为最终运行的task设置ContainerLaunchContext,使得task能够在已分配的container上运行。一旦ContainerLaunchContext定义好了,ApplicationMaster就能够与ContainerManager进行通信和启动已分配的container。
String cmIpPortStr = container.getNodeId().getHost() + ":" |
+ container.getNodeId().getPort(); |
InetSocketAddress cmAddress = NetUtils.createSocketAddr(cmIpPortStr); |
(ContainerManager)rpc.getProxy(ContainerManager. class , cmAddress, conf); |
ContainerLaunchContext ctx = |
Records.newRecord(ContainerLaunchContext. class ); |
ctx.setContainerId(container.getId()); |
ctx.setResource(container.getResource()); |
ctx.setUser(UserGroupInformation.getCurrentUser().getShortUserName()); |
} catch (IOException e) { |
"Getting current user failed when trying to launch the container" , |
Map<String, String> unixEnv; |
ctx.setEnvironment(unixEnv); |
Map<String, LocalResource> localResources = |
new HashMap<String, LocalResource>(); |
LocalResource shellRsrc = Records.newRecord(LocalResource. class ); |
shellRsrc.setType(LocalResourceType.FILE); |
shellRsrc.setVisibility(LocalResourceVisibility.APPLICATION); |
ConverterUtils.getYarnUrlFromURI( new URI(shellScriptPath))); |
shellRsrc.setTimestamp(shellScriptPathTimestamp); |
shellRsrc.setSize(shellScriptPathLen); |
localResources.put( "MyExecShell.sh" , shellRsrc); |
ctx.setLocalResources(localResources); |
String command = "/bin/sh ./MyExecShell.sh" |
+ " 1>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout" |
+ " 2>" + ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr" ; |
List<String> commands = new ArrayList<String>(); |
ctx.setCommands(commands); |
StartContainerRequest startReq = Records.newRecord(StartContainerRequest. class ); |
startReq.setContainerLaunchContext(ctx); |
cm.startContainer(startReq); |
- 正如前面提到的,ApplicationMaster通过AMRMProtocol#allocate调用的返回信息,能够得到任务的完成进度信息,它也能够通过查询ContainerManager的状态来主动监测已经启动的containers。
GetContainerStatusRequest statusReq = |
Records.newRecord(GetContainerStatusRequest. class ); |
statusReq.setContainerId(container.getId()); |
GetContainerStatusResponse statusResp = cm.getContainerStatus(statusReq); |
LOG.info( "Container Status" |
+ ", id=" + container.getId() |
+ ", status=" + statusResp.getStatus()); |
FAQ
我如何将我的应用的jar包放到YARN集群的所有节点上?
你可以使用LocalResource将所需要的资源添加到你应用的资源请求中。这将使YARN分发这些资源到ApplicationMaster的节点。如果资源的类型是 tgz, zip或者jar包,你可以让YARN去解压它。所有你需要做的只是将未压缩的文件夹添加到你的classpath中。例如,像下面这样创建你的应用的资源请求:
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) |
request.setApplicationSubmissionContext(appCtx) |
applicationsManager.submitApplication(request) |
正如你所看到的,setLocalResources方法建立了一个名字到资源的映射,名字成为一个软链接链接到你应用的当前目录,因此通过使用 ./package*.,你就可以访问这些资源了。
注意:Java的classpath参数是很敏感的。务必保证你使用的语法完全正确。
一旦你的资源包被分发到ApplicationMaster节点,无论任何时候当ApplicationMaster启动一个新的container时,你只需要遵循这个相同的过程(假设你是希望资源被分发到你的container节点的)。完全可以重用这段代码,你只需要给ApplicationMaster资源包路径(无论是在HDFS上或者本地路径),这样资源的URL就可以随着container的ctx一起发送过去。
我如何获取ApplicationMaster的ApplicationAttemptId?
ApplicationAttemptId会作为环境变量发送给ApplicationMaster,因此可以从环境变量中得到它的值,此外通过辅助函数ConverterUtils还能将其转化为ApplicationAttemptId对象。
我的container被NodeManager杀掉了
这可能是因为比较高的内存使用超出了你的container的内存大小。有一系列的原因可能产生这种现象,首先可以产看当container被kill时,node manager dump出来的进程树。你需要关注的两个参数是物理内存和虚拟内存。如果你超出了物理内存限制,说明你的应用使用了太多的物理内存。如果你运行的是一个Java应用程序,你可以使用 -hprof 来查看是什么占用了堆的空间。如果你超出了虚拟内存的限制,你需要增大针对集群的配置 yarn.nodemanager.vmem-pmem-ratio。
我如何包含本地库?
当你启动container时,通过命令行参数 -Djava.library.path 会导致hadoop使用的本地库无法正常加载而导致失败。较明智的做法是使用LD_LIBRARY_PATH。
有用的链接
下一代MapReduce的架构
下一代MapReduce的调度