Yarn源碼剖析(一) --- RM與NM服務啓動以及心跳通信

前言

Yarn源碼剖析(零) --- spark任務提交到yarn的流程中介紹了Yarn任務提交的流程,按照此篇的閱讀指導,該篇文章將會介紹Yarn中ResourceManager、NodeManager的啓動,以及兩者之間的心跳通信。

Yarn的啓動

1. 對於任務的yarn服務的啓動當然要從它的啓動腳本start-yarn.sh中進行分析,可以看到分別執行了yarn-daemon.sh、yarn-daemons.sh

 # start resourceManager
 "$bin"/yarn-daemon.sh --config $YARN_CONF_DIR start resourcemanager
 # start nodeManager
 "$bin"/yarn-daemons.sh --config $YARN_CONF_DIR start nodemanager

2. 仔細閱讀腳本(本文就不對腳本的細節部分做過多的講解了),不難找到在腳本中有這麼一句話,可知實際上執行的是bin目錄下yarn這一個文件

nohup nice -n $YARN_NICENESS "$HADOOP_YARN_HOME"/bin/yarn --config $YARN_CONF_DIR $command "$@" > "$log" 2>&1 < /dev/null &

3. 打開這個文件,去找傳進去的resourcemanager參數和nodemanager參數對應的執行類,能夠清楚的觀察到分別執行了NodeManager和ResourceManager

 elif [ "$COMMAND" = "nodemanager" ] ; then
     CLASSPATH=${CLASSPATH}:$YARN_CONF_DIR/nm-config/log4j.properties
     CLASS='org.apache.hadoop.yarn.server.nodemanager.NodeManager'
     YARN_OPTS="$YARN_OPTS -server $YARN_NODEMANAGER_OPTS"
elif [ "$COMMAND" = "resourcemanager" ] ; then
     CLASSPATH=${CLASSPATH}:$YARN_CONF_DIR/rm-config/log4j.properties
     CLASS='org.apache.hadoop.yarn.server.resourcemanager.ResourceManager'
     YARN_OPTS="$YARN_OPTS $YARN_RESOURCEMANAGER_OPTS"

ResourceManager

那我們直接從ResourceManager(後文簡稱RM)的mian方法來分析RM的啓動,main方法中很容易就能觀察到兩個重要的方法

//會實現本類中的serviceInit方法
resourceManager.init(conf);
resourceManager.start();

實際上此處是調用父類AbstractService的init()方法,再內部實現serviceInit方法,再有匹配的實現類來達到初始化的目的。AbstractService的init()方法非常重要,基本所有重要服務的初始化方法都是調用該方法實現的。
那我們就來看一下這個方法:

public void init(Configuration conf) {
    //服務配置是否爲空
    if (conf == null) {
        throw new ServiceStateException("Cannot initialize service "
        + getName() + ": null configuration");
    }
    
    //服務是否已經初始化
    if (isInState(STATE.INITED)) {
        return;
    }
    synchronized (stateChangeLock) {
        /*相當於對服務做一次校驗,確保服務初始化成功*/
        if (enterState(STATE.INITED) != STATE.INITED) {
            setConfig(conf);
            try {
                //服務初始化,會進入子類的同名函數
                serviceInit(config);
                if (isInState(STATE.INITED)) {
                    notifyListeners();
                }
            } catch (Exception e) {
             noteFailure(e);
             ServiceOperations.stopQuietly(LOG, this);
             throw ServiceStateException.convert(e);
            }
        }
    }
}

再來看一下start方法它同init()方法一樣,都是去調用父類AbstractService的start()方法,然後調用內部的serviceStart()方法由對應的實現類來實現的,與init()方法一樣,許多重要服務的start方法都是採用這樣的模式實現的。

public void start() {
    if (isInState(STATE.STARTED)) {
        return;
    }
    synchronized (stateChangeLock) {
        if (stateModel.enterState(STATE.STARTED) != STATE.STARTED) {
            try {
                startTime = System.currentTimeMillis();
                if (isInState(STATE.STARTED)) {
                    serviceStart();
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("Service " + getName() + " is started");
                    }
                    notifyListeners();
                }
            } catch (Exception e) {
                noteFailure(e);
                ServiceOperations.stopQuietly(LOG, this);
                throw ServiceStateException.convert(e);
            }
        }
    }
}

那我們去看一下具體實現的方法先從ResourceManager.serviceInit方法看起,充分理解服務啓動時各個成員變量以及相關服務的初始化對於我們分析整篇yarn相關的內容是很有幫助的。在初始化方法中有一個很重要的方法createAndInitActiveServices(),具體的作用在下面的代碼塊中做了註釋。該方法內部實現了許多重要服務的初始化,在這裏我們就不做這些服務一一介紹了,在後文分析中遇到使用該類的地方時,會順帶介紹初始化的相關方法。其實通過研究RM的初始化代碼不難發現RM就像一個由許多子服務類組成的大服務類,所以RM就通過這些子服務來管理整個Yarn。

// load core-site.xml
 // load yarn-site.xml
 //前面的代碼關鍵就做了初始化配置文件,讀取配置文件,設置配置文件上下文這類的事情,就不多做分析了。

// 註冊了一個調度器,用於內部事件調度處理
rmDispatcher = setupDispatcher();
addIfService(rmDispatcher);
rmContext.setDispatcher(rmDispatcher);

//這個方法內部實現了許多重要的服務初始化的過程,其實真正需要分析的就是這個方法。
//這是由RM的一個內部類RMActiveServices實現的。
createAndInitActiveServices(); 
super.serviceInit(this.conf);

在本文最後一節會介紹一個關鍵類的初始化,這是NM與RM保持心跳的關鍵類,它的初始化方法也是一些配置參數的初始化

resourceTracker = createResourceTrackerService();
addService(resourceTracker);
rmContext.setResourceTrackerService(resourceTracker);
接下來看一下ResourceManager.serviceStart()
protected void serviceStart() throws Exception {
    if (this.rmContext.isHAEnabled()) {
        transitionToStandby(true);
    } else {
        transitionToActive();
    }
    
    startWepApp();
    if (getConfig().getBoolean(YarnConfiguration.IS_MINI_YARN_CLUSTER,false)) {
        int port = webApp.port();
        WebAppUtils.setRMWebAppPort(conf, port);
    }
    super.serviceStart();
}

本文不分析HA環境,所以我們直接看transitionToActive(),順着該方法往裏走,最終調用的是AsyncDispatcher.serviceStart(),內部新建了一個死循環線程,用來處理阻塞隊列中的各個事件,最終會調用createThread()

 Runnable createThread(){
    return new Runnable(){
        @Override 
        public void run(){
            while(!stopped&&!Thread.currentThread().isInterrupted()){
                drained=eventQueue.isEmpty();
                if(blockNewEvents){
                    synchronized(waitForDrained){
                        if(drained){
                            waitForDrained.notify();
                        }
                    }
                }
                
                Event event;
                try{
                    event=eventQueue.take();
                }catch(InterruptedException ie){
                    if(!stopped){
                        LOG.warn("AsyncDispatcher thread interrupted",ie);
                    }
                    return;
                }
                if(event!=null){
                    dispatch(event);
                }
            }
        }
    }
}

最終看super.serviceStart(),內部就是將每一個service啓動,至此RM的啓動就完畢了。(每一個具體服務的啓動在後文介紹的時候會在使用到的地方再做解釋)

protected void serviceStart() throws Exception {
    List<Service> services = getServices();
    if (LOG.isDebugEnabled()) {
        LOG.debug(getName() + ": starting services, size=" + services.size());
    }
    for (Service service : services) {
        service.start();
    }
    super.serviceStart();
}

NodeManager

先來看一下main()方法

public static void main(String[] args) throws IOException {
    Thread.setDefaultUncaughtExceptionHandler(new YarnUncaughtExceptionHandler());
    StringUtils.startupShutdownMessage(NodeManager.class, args, LOG);
    
    NodeManager nodeManager = new NodeManager();
    Configuration conf = new YarnConfiguration();
    new GenericOptionsParser(conf, args);
    nodeManager.initAndStartNodeManager(conf, false);
}

調用了initAndStartNodeManager(conf, false),其實這個方法就是執行了init(),以及start(),所以我們先來看serviceInit()方法,同RM一樣,做了許多重要方法的初始化,此篇文章要着重介紹NodeStatusUpdaterImpl,可以看到該初始化方法中new了一個NodeStatusUpdaterImpl,一起在RM章節中我們已經介紹了,後文的start方法會逐一啓動這些初始化過的服務,所以不再贅述,我們直接去看NodeStatusUpdaterImpl的初始化方法和啓動方法

nodeStatusUpdater =createNodeStatusUpdater(context, dispatcher, nodeHealthChecker);

protected NodeStatusUpdater createNodeStatusUpdater(Context context,
        Dispatcher dispatcher, NodeHealthCheckerService healthChecker) {
    return new NodeStatusUpdaterImpl(context, dispatcher, healthChecker,metrics);
}

大致瀏覽一下初始化方法,大多都是配置文件的讀取(都是關於節點資源信息的配置文件),而後去關注重點的start方法

this.nodeId = this.context.getNodeId();
this.httpPort = this.context.getHttpPort();
this.nodeManagerVersionId = YarnVersionInfo.getVersion();

try {
    //註冊啓動以保證ContainerManager可以獲得每一臺NM的標記
    this.resourceTracker = getRMClient();
    //配置必要配置信息,和安全認證操作利用Hadoop RPC遠程調用RM端ResourcesTrackerService下的registerNodeManager()方法
    // 詳細見後面ResourcesTrackerService下的registerNodeManager()代碼分析
    registerWithRM();
        
    super.serviceStart();
    // 創建一個線程,然後啓動,所有操作都在運行while的循環中
    //設置、獲取和輸出必要配置信息,其中比較重要的調用getNodeStatus()方法,
    // 獲取本地Container和本地Node的狀態,以供後面的nodeHeartbeat()方法使用
    //通過Hadoop RPC遠程調用RM端ResourcesTrackerService下的nodeHeartbeat()函數,
    // 用while循環以一定時間間隔向RM發送心跳信息,心跳操作見下面ResourcesTrackerService下的nodeHeartbeat()函數
    //nodeHeartbeat()將返回給NM信息,根據返回的response,
    // 根據response返回的信息標記不需要的Container和Application發送相應的
    // FINISH_CONTAINERS和 FINISH_APPS給ContainerManager,進行清理操作----詳細見後面的代碼分析
    startStatusUpdater();
} catch (Exception e) {
    String errorMessage = "Unexpected error starting NodeStatusUpdater";
    LOG.error(errorMessage, e);
    throw new YarnRuntimeException(e);
}

可歸納出兩個重要的方法registerWithRM(),startStatusUpdater(),這兩個方法會通過RPC遠程調用上文說過的等待被調用ResourceTracker中的兩個接口 registerNodeManager()和nodeHeartbeat()


NM與RM間的心跳通信

從上文可知registerWithRM()會通過rpc通信調用ResourceTracker中的接口 registerNodeManager(),所以我們直接去看這個 registerNodeManager(),內部做了一些節點健康信息的檢查,資源信息的檢查,從下列代碼中可以看到還會檢查該node是否是新的node,如果是就會觸發STARTRED時間,是,如果不是在觸發RECONNECTED事件

RMNode oldNode=this.rmContext.getRMNodes().putIfAbsent(nodeId,rmNode);
if(oldNode==null) {
    this.rmContext.getDispatcher().getEventHandler().handle(
            new RMNodeStartedEvent(nodeId,request.getNMContainerStatuses(),
        request.getRunningApplications()));
} else {
    LOG.info("Reconnect from the node at: "+host);
    this.nmLivelinessMonitor.unregister(nodeId);
    oldNode.resetLastNodeHeartBeatResponse();
    this.rmContext
        .getDispatcher()
        .getEventHandler()
        .handle(
                new RMNodeReconnectEvent(nodeId,rmNode,request
        .getRunningApplications(),request.getNMContainerStatuses()));
}

那我們此次分析的是NM的啓動,自然去分析STARTED事件,狀態發生了轉移觸發AddNodeTransition函數,該函數內顯示的添加了active nodes,並向調度器發送了一個事件NODE_ADDED,告訴調度器有節點增加,用於計算集羣中總共可調用的資源,到此節點就已經註冊成功了。

//資源判斷
root.updateClusterResource(clusterResource, new ResourceLimits(
        clusterResource));
int numNodes = numNodeManagers.incrementAndGet();
updateMaximumAllocation(schedulerNode, true);

然後代碼往後走,調用了ResourceTracker中的接 口nodeHeartbeat(),心跳方法中最終要的就是提交了一個STATUS_UPDATE事件

// 4. Send status to RMNode, saving the latest response.
 this.rmContext.getDispatcher().getEventHandler().handle(
 new RMNodeStatusEvent(nodeId, remoteNodeStatus.getNodeHealthStatus(),
 remoteNodeStatus.getContainersStatuses(), 
 remoteNodeStatus.getKeepAliveApplications(), nodeHeartBeatResponse));


從下面代碼中可以看出該事件對應的方法

.addTransition(NodeState.RUNNING,
 EnumSet.of(NodeState.RUNNING, NodeState.UNHEALTHY),
 RMNodeEventType.STATUS_UPDATE, new StatusUpdateWhenHealthyTransition())


在沒有任務提交時,心跳方法內部就是彙報了自己節點資源的可用信息和已用信息以及彙報健康狀態,最後返回RUNNING狀態,表示NM正常運行中,整個心跳方法是卸載while循環裏的,沒有異常就不會退出

//TODO:對container進行分組,分爲a)剛申請到還未使用的 b)運行完畢需要回收的
rmNode.handleContainerStatus(statusEvent.getContainers());
if(rmNode.nextHeartBeat) {
    rmNode.nextHeartBeat = false;
    rmNode.context.getDispatcher().getEventHandler().handle(
            new NodeUpdateSchedulerEvent(rmNode));
}

// Update DTRenewer in secure mode to keep these apps alive. Today this is
// needed for log-aggregation to finish long after the apps are gone.
if (UserGroupInformation.isSecurityEnabled()) {
    rmNode.context.getDelegationTokenRenewer().updateKeepAliveApplications(
            statusEvent.getKeepAliveAppIds());
}

return NodeState.RUNNING;


小結

本文粗淺的介紹了RresourceManager、NodeManager的啓動,也能知道RM與NM的心跳是在NM啓動後就會創建一個心跳線程與RM保持通信,在下篇文章中將介紹spark端任務通過spark-submit提交任務到啓動的過程,詳情參見Yarn源碼剖析(二) --- spark-submit


作者:蛋撻

日期:2018.08.05

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章