[源碼解析] 從TimeoutException看Flink的心跳機制

[源碼解析] 從TimeoutException看Flink的心跳機制

目錄

  • [源碼解析] 從TimeoutException看Flink的心跳機制
    • 0x00 摘要
    • 0x01 緣由
    • 0x02 背景概念
      • 2.1 四大模塊
      • 2.2 Akka
      • 2.3 RPC機制
        • 2.3.1 RpcEndpoint:RPC的基類
        • RpcService:RPC服務提供者
        • RpcGateway:RPC調用的網關
      • 2.4 常見心跳機制
    • 0x03 Flink心跳機制
      • 3.1 代碼和機制
      • 3.2 靜態架構
        • 3.2.1 HeartbeatTarget :監控目標抽象
        • 3.2.2 HeartbeatMonitor : 管理heartbeat target的心跳狀態
        • 3.2.3 HeartbeatManager :心跳管理者
        • 3.2.4 HearbeatListener 處理心跳結果
      • 3.3 動態運行機制
        • 3.3.1 HearbeatManagerImpl : Receiver
        • 3.3.2 HeartbeatManagerSenderImpl : Sender
        • 3.3.3 HeartbeatMonitorImpl
        • 3.3.3 HeartbeatServices
    • 0x04 初始化
      • 4.1 心跳服務創建
    • 0x05 Flink中具體應用
      • 5.1 總述
        • 5.1.1 RM, JM, TM之間關係
        • 5.1.2 三者間心跳機制
      • 5.2 初始化過程
        • 5.2.1 TaskExecutor初始化
        • 5.2.2 JobMaster的初始化
        • 5.2.3 ResourceManager初始化
      • 5.3 註冊過程
        • 5.3.1 TM註冊到RM中
          • 5.3.1.1 TM的操作
          • 5.3.1.2 RM的操作
          • 5.3.1.3 返回到TM
        • 5.3.2 TM註冊到 JM
      • 5.4 心跳過程
        • 5.4.1 ResourceManager主動發起
          • 5.4.1.1 Sender遍歷所有監控的Monitor(Target)
          • 5.4.1.2 Target進行具體操作
          • 5.4.1.3 RPC調用
        • 5.4.2 RM通過RPC調用TM
        • 5.4.3 TM 通過RPC回到 RM
      • 5.5 超時處理
        • 5.5.1 TaskManager
        • 5.5.2 ResourceManager
    • 0x06 解決問題
    • 0x07 參考

0x00 摘要

本文從一個調試時候常見的異常 “TimeoutException: Heartbeat of TaskManager timed out”切入,為大家剖析Flink的心跳機制。文中代碼基於Flink 1.10。

0x01 緣由

大家如果經常調試Flink,當進入斷點看到了堆棧和變量內容之後,你容易陷入了沉思。當你發現了問題可能所在,高興的讓程序Resume的時候,你發現程序無法運行,有如下提示:

Caused by: java.util.concurrent.TimeoutException: Heartbeat of TaskManager with id 93aa1740-cd2c-4032-b74a-5f256edb3217 timed out.

這實在是很鬱悶的事情。作為程序猿不能忍啊,既然異常提示中有 Heartbeat 字樣,於是我們就來一起看看Flink的心跳機制,看看有沒有可以修改的途徑。

0x02 背景概念

2.1 四大模塊

Flink有核心四大組件:Dispatcher,JobMaster,ResourceManager,TaskExecutor。

  • Dispatcher(Application Master)用於接收client提交的任務和啟動相應的JobManager。其提供REST接口來接收client的application提交,負責啟動JM和提交application,同時運行Web UI。
  • ResourceManager:主要用於資源的申請和分配。當TM有空閑的slot就會告訴JM,沒有足夠的slot也會啟動新的TM。kill掉長時間空閑的TM。
  • JobMaster :功能主要包括(舊版本中JobManager的功能在新版本中以JobMaster形式出現,可能本文中會混淆這兩個詞,請大家諒解):
    • 將JobGraph轉化為ExecutionGraph(physical dataflow graph,并行化)。
    • 向RM申請資源、schedule tasks、保存作業的元數據。
  • TaskManager:類似Spark的executor,會跑多個線程的task、數據緩存與交換。Flink 架構遵循 Master – Slave 架構設計原則,JobMaster 為 Master 節點,TaskManager 為Slave節點。

這四大組件彼此之間的通信需要依賴RPC實現。

2.2 Akka

Flink底層RPC基於Akka實現。Akka是一個開發併發、容錯和可伸縮應用的框架。它是Actor Model的一個實現,和Erlang的併發模型很像。在Actor模型中,所有的實體被認為是獨立的actors。actors和其他actors通過發送異步消息通信。

Actor模型的強大來自於異步。它也可以顯式等待響應,這使得可以執行同步操作。但是強烈不建議同步消息,因為它們限制了系統的伸縮性。

2.3 RPC機制

RPC作用是:讓異步調用看起來像同步調用。

Flink基於Akka構建了其底層通信系統,引入了RPC調用,各節點通過GateWay方式回調,隱藏通信組件的細節,實現解耦。Flink整個通信框架的組件主要由RpcEndpoint、RpcService、RpcServer、AkkaInvocationHandler、AkkaRpcActor等構成。

RPC相關的主要接口如下:

  • RpcEndpoint
  • RpcService
  • RpcGateway

2.3.1 RpcEndpoint:RPC的基類

RpcEndpoint是Flink RPC終端的基類,所有提供遠程過程調用的分佈式組件必須擴展RpcEndpoint,其功能由RpcService支持。

RpcEndpoint的子類只有四類組件:Dispatcher,JobMaster,ResourceManager,TaskExecutor,即Flink中只有這四個組件有RPC的能力,換句話說只有這四個組件有RPC的這個需求。

每個RpcEndpoint對應了一個路徑(endpointId和actorSystem共同確定),每個路徑對應一個Actor,其實現了RpcGateway接口,

RpcService:RPC服務提供者

RpcServer是RpcEndpoint的成員變量,為RpcService提供RPC服務/連接遠程Server,其只有一個子類實現:AkkaRpcService(可見目前Flink的通信方式依然是Akka)。

RpcServer用於啟動和連接到RpcEndpoint, 連接到rpc服務器將返回一個RpcGateway,可用於調用遠程過程。

Flink四大組件Dispatcher,JobMaster,ResourceManager,TaskExecutor,都是RpcEndpoint的實現,所以構建四大組件時,同步需要初始化RpcServer。如JobManager的構造方式,第一個參數就是需要知道RpcService。

RpcGateway:RPC調用的網關

Flink的RPC協議通過RpcGateway來定義;由前面可知,若想與遠端Actor通信,則必須提供地址(ip和port),如在Flink-on-Yarn模式下,JobMaster會先啟動ActorSystem,此時TaskExecutor的Container還未分配,後面與TaskExecutor通信時,必須讓其提供對應地址。

Dispatcher,JobMaster,ResourceManager,TaskExecutor 這四大組件通過各種方式實現了Gateway。以JobMaster為例,JobMaster實現JobMasterGateway接口。各組件類的成員變量都有需要通信的其他組件的GateWay實現類,這樣可通過各自的Gateway實現RPC調用。

2.4 常見心跳機制

常見的心跳檢測有兩種:

  • socket 套接字SO_KEEPALIVE本身帶有的心跳機制,定期向對方發送心跳包,對方收到心跳包後會自動回復;
  • 應用自身實現心跳機制,同樣也是使用定期發送請求的方式;

Flink實現的是第二種方案。

0x03 Flink心跳機制

3.1 代碼和機制

Flink的心跳機制代碼在:

Flink-master/flink-runtime/src/main/java/org/apache/flink/runtime/heartbeat

四個接口:

HeartbeatListener.java          HeartbeatManager.java      HeartbeatTarget.java  HeartbeatMonitor.java

以及如下幾個類:

HeartbeatManagerImpl.java   HeartbeatManagerSenderImpl.java   HeartbeatMonitorImpl.java
HeartbeatServices.java    NoOpHeartbeatManager.java

Flink集群有多種業務流程,比如Resource Manager, Task Manager, Job Manager。每種業務流程都有自己的心跳機制。Flink的心跳機制只是提供接口和基本功能,具體業務功能由各業務流程自己實現。

我們首先設定 心跳系統中有兩種節點:sender和receiver。心跳機制是sender和receivers彼此相互檢測。但是檢測動作是Sender主動發起,即Sender主動發送請求探測receiver是否存活,因為Sender已經發送過來了探測心跳請求,所以這樣receiver同時也知道Sender是存活的,然後Reciver給Sender回應一個心跳錶示自己也是活着的。

因為Flink的幾個名詞和我們常見概念有所差別,所以流程上需要大家仔細甄別,即:

  • Flink Sender 主動發送Request請求給Receiver,要求Receiver回應一個心跳;
  • Flink Receiver 收到Request之後,通過Receive函數回應一個心跳請求給Sender;

3.2 靜態架構

3.2.1 HeartbeatTarget :監控目標抽象

HeartbeatTarget是對監控目標的抽象。心跳機制在行為上而言有兩種動作:

  • 向某個節點發送請求。
  • 處理某個節點發來的請求。

HeartbeatTarget的函數就是這兩個動作:

  • receiveHeartbeat :向某個節點(Sender)發送心跳回應,其參數heartbeatOrigin 就是 Receiver。
  • requestHeartbeat :向某個節點(Receiver)要求其回應一個心跳,其參數requestOrigin 就是 Sender。requestHeartbeat這個函數是Sender的函數,其中Sender通過RPC直接調用到Receiver。

這兩個函數的參數也很簡單:分別是請求的發送放和接收方,還有Payload載荷。對於一個確定節點而言,接收的和發送的載荷是同一類型的。

public interface HeartbeatTarget<I> {
	/**
	 * Sends a heartbeat response to the target.
	 * @param heartbeatOrigin Resource ID identifying the machine for which a heartbeat shall be reported.
	 */
  // heartbeatOrigin 就是 Receiver
	void receiveHeartbeat(ResourceID heartbeatOrigin, I heartbeatPayload);

	/**
	 * Requests a heartbeat from the target. 
	 * @param requestOrigin Resource ID identifying the machine issuing the heartbeat request.
	 */
  // requestOrigin 就是 Sender
	void requestHeartbeat(ResourceID requestOrigin, I heartbeatPayload);
}

3.2.2 HeartbeatMonitor : 管理heartbeat target的心跳狀態

對HeartbeatTarget的封裝,這樣Manager對Target的操作是通過對Monitor完成,後續會在其繼承類中詳細說明。

public interface HeartbeatMonitor<O> {
  // Gets heartbeat target.
	HeartbeatTarget<O> getHeartbeatTarget();
	// Gets heartbeat target id.
	ResourceID getHeartbeatTargetId();
	// Report heartbeat from the monitored target.
	void reportHeartbeat();
	//Cancel this monitor.
	void cancel();
  //Gets the last heartbeat.
	long getLastHeartbeat();
}

3.2.3 HeartbeatManager :心跳管理者

HeartbeatManager負責管理心跳機制,比如啟動/停止/報告一個HeartbeatTarget。此接口繼承HeartbeatTarget。

除了HeartbeatTarget的函數之外,這接口有4個函數:

  • monitorTarget,把和某資源對應的節點加入到心跳監控列表;
  • unmonitorTarget,從心跳監控列表刪除某資源對應的節點;
  • stop,停止心跳管理服務,釋放資源;
  • getLastHeartbeatFrom,獲取某節點的最後一次心跳數據。
public interface HeartbeatManager<I, O> extends HeartbeatTarget<I> {
	void monitorTarget(ResourceID resourceID, HeartbeatTarget<O> heartbeatTarget);
	void unmonitorTarget(ResourceID resourceID);
	void stop();
	long getLastHeartbeatFrom(ResourceID resourceId);
}

3.2.4 HearbeatListener 處理心跳結果

用戶業務邏輯需要繼承這個接口以處理心跳結果。其可以看做服務的輸出,實現了三個回調函數。

  • notifyHeartbeatTimeout,處理節點心跳超時
  • reportPayload,處理節點發來的Payload載荷
  • retrievePayLoad。獲取對某節點發下一次心跳請求的Payload載荷
public interface HeartbeatListener<I, O> {
	void notifyHeartbeatTimeout(ResourceID resourceID);
	void reportPayload(ResourceID resourceID, I payload);
	O retrievePayload(ResourceID resourceID);
}

3.3 動態運行機制

之前提到Sender和Receiver,下面兩個類就對應上述概念。

  • HeartbeatManagerImpl :Receiver,存在於JobMaster與TaskExecutor中;
  • HeartbeatManagerSenderImpl :Sender,繼承 HeartbeatManagerImpl類,用於周期發送心跳要求,存在於JobMaster、ResourceManager中。

幾個關鍵問題:

  1. 如何判定心跳超時?

    心跳服務啟動后,Flink在Monitor中通過 ScheduledFuture 會啟動一個線程來處理心跳超時事件。在設定的心跳超時時間到達后才執行線程。

    如果在設定的心跳超時時間內接收到組件的心跳消息,會先將該線程取消而後重新開啟,重置心跳超時事件的觸發。

    如果在設定的心跳超時時間內沒有收到組件的心跳,則會通知組件:你超時了。

  2. 何時”調用雙方”發起心跳檢查?

    心跳檢查是雙向的,一方(Sender)會主動發起心跳請求,而另一方(Receiver)則是對心跳做出響應,兩者通過RPC相互調用,重置對方的 Monitor 超時線程。

    以JobMaster和TaskManager為例,JM在啟動時會開啟周期調度,向已經註冊到JM中的TM發起心跳檢查,通過RPC調用TM的requestHeartbeat方法,重置TM中對JM超時線程的調用,表示當前JM狀態正常。在TM的requestHeartbeat方法被調用后,通過RPC調用JM的receiveHeartbeat,重置 JM 中對TM超時線程的調用,表示TM狀態正常。

  3. 如何處理心跳超時?

    心跳服務依賴 HeartbeatListener,當在timeout時間範圍內未接收到心跳響應,則會觸發超時處理線程,該線程通過調用HeartbeatListener.notifyHeartbeatTimeout方法做後續重連操作或者直接斷開。

下面是一個概要(以RM & TM為例):

  • RM : 實現了ResourceManagerGateway (可以直接被RPC調用)

  • TM : 實現了TaskExecutorGateway (可以直接被RPC調用)

  • RM :有一個Sender HM : taskManagerHeartbeatManager,Sender HM 擁有用戶定義的 TaskManagerHeartbeatListener

  • TM :有一個Receiver HM :resourceManagerHeartbeatManager,Receiver HM 擁有用戶定義的ResourceManagerHeartbeatListener。

  • HeartbeatManager 有一個ConcurrentHashMap<ResourceID, HeartbeatMonitor > heartbeatTargets,這個Map是它監控的所有Target。

  • 對於RM的每一個需要監控的TM, 其生成一個HeartbeatTarget,進而被構造成一個HeartbeatMonitor,放置到ResourceManager.taskManagerHeartbeatManager中。

  • 每一個Target對應的Monitor中,有自己的異步任務ScheduledFuture,這個ScheduledFuture不停的被取消/重新生成。如果在某個期間內沒有被取消,則通知用戶定義的listener出現了timeout。

3.3.1 HearbeatManagerImpl : Receiver

HearbeatManagerImpl是receiver的具體實現。它由 心跳 被發起方(就是Receiver,例如TM) 創建,接收 發起方(就是Sender,例如 JM)的心跳發送請求。心跳超時 會觸發 heartbeatListener.notifyHeartbeatTimeout方法。

注意:被發起方監控線程(Monitor)的開啟是在接收到請求心跳(requestHeartbeat被調用后)以後才觸發的,屬於被動觸發。

HearbeatManagerImpl主要維護了

  • 一個心跳監控列表 map : <ResourceID, HeartbeatMonitor<O>> heartbeatTargets;。這是一個KV關聯。

    key代表要發送心跳組件(例如:TM)的ID,value則是為當前組件創建的觸發心跳超時的線程HeartbeatMonitor,兩者一一對應。

    當一個從所聯繫的machine發過來的心跳被收到時候,對應的monitor的狀態會被更新(重啟一個新ScheduledFuture)。當一個monitor發現了一個 heartbeat timed out,它會通知自己的HeartbeatListener。

  • 一個 ScheduledExecutor mainThreadExecutor 負責heartbeat timeout notifications。

  • heartbeatListener :處理心跳結果。

HearbeatManagerImpl 數據結構如下:

@ThreadSafe
public class HeartbeatManagerImpl<I, O> implements HeartbeatManager<I, O> {

	/** Heartbeat timeout interval in milli seconds. */
	private final long heartbeatTimeoutIntervalMs;

	/** Resource ID which is used to mark one own's heartbeat signals. */
	private final ResourceID ownResourceID;

	/** Heartbeat listener with which the heartbeat manager has been associated. */
	private final HeartbeatListener<I, O> heartbeatListener;

	/** Executor service used to run heartbeat timeout notifications. */
	private final ScheduledExecutor mainThreadExecutor;

	/** Map containing the heartbeat monitors associated with the respective resource ID. */
	private final ConcurrentHashMap<ResourceID, HeartbeatMonitor<O>> heartbeatTargets;

	/** Running state of the heartbeat manager. */
	protected volatile boolean stopped;  
}

HearbeatManagerImpl實現的主要函數有:

  • monitorTarget :把一個節點加入到心跳監控列表。
    • 傳入參數有:ResourceId和HearbeatTarget,monitorTarget根據這兩個參數,生成一個HeartbeatMonitor對象,然後把這個對象跟ResrouceId做kv關聯,存入到heartbeatTargets。 一個節點可能參与多個業務流程,因此一個節點參与多個心跳流程,一個節點上運行多個不同類型的HearbeatTarget。所以一個ResourceID可能會跟不同類型的HearbeatTarget對象關聯,分別加入到多個HeartbeatManager,進行不同類型的心跳監控。也因此這個函數入參是兩個參數。
  • requestHeartbeat :Sender通過RPC異步調用到Receiver的這個函數 以要求receiver向requestOrigin節點(就是Sender)發起一次心跳響應,載荷是heartbeatPayLoad。其內部流程如下:
    • 首先會調用reportHeartbeat函數,作用是 通過Monitor 記錄發起請求的這個時間點,然後創建一個ScheduleFuture。如果到期后,requestOrigin沒有作出響應,那麼就將requestOrigin節點對應的HeartbeatMonitor的state設置成TIMEOUT狀態,如果到期內requestOrigin響應了,ScheduleFuture會被取消,HeartbeatMonitor的state仍然是RUNNING。
    • 其次調用reportPayload函數,把requestOrigin節點的最新的heartbeatPayload通知給heartbeatListener。heartbeatListener是外部傳入的,它根據所有節點的心跳記錄做監聽管理。
    • 最後調用receiveHearbeat函數,響應一個心跳給Sender。

3.3.2 HeartbeatManagerSenderImpl : Sender

繼承HearbeatManagerImpl,由心跳管理的一方(例如JM)創建,實現了run函數(即它可以作為一個單獨線程運行),創建后立即開啟周期調度線程,每次遍歷自己管理的heartbeatTarget,觸發heartbeatTarget.requestHeartbeat,要求 Target 返回一個心跳響應。屬於主動觸發心跳請求。

public class HeartbeatManagerSenderImpl<I, O> extends HeartbeatManagerImpl<I, O> implements Runnable {
	public void run() {
		if (!stopped) {
			for (HeartbeatMonitor<O> heartbeatMonitor : getHeartbeatTargets().values()) {
				requestHeartbeat(heartbeatMonitor);
			}
      // 周期調度
			getMainThreadExecutor().schedule(this, heartbeatPeriod, TimeUnit.MILLISECONDS);
		}
	}

  //  主動發起心跳檢查
	private void requestHeartbeat(HeartbeatMonitor<O> heartbeatMonitor) {
		O payload = getHeartbeatListener().retrievePayload(heartbeatMonitor.getHeartbeatTargetId());
		final HeartbeatTarget<O> heartbeatTarget = heartbeatMonitor.getHeartbeatTarget();
    // 調用 Target 的 requestHeartbeat 函數
		heartbeatTarget.requestHeartbeat(getOwnResourceID(), payload);
	}  
}

3.3.3 HeartbeatMonitorImpl

Heartbeat monitor管理心跳目標,它啟動一個ScheduledExecutor。

  • 如果在timeout時間內沒有接收到心跳信號,則判定心跳超時,通知給HeartbeatListener。
  • 如果在timeout時間內接收到心跳信號,則重置當前ScheduledExecutor。
public class HeartbeatMonitorImpl<O> implements HeartbeatMonitor<O>, Runnable {

	/** Resource ID of the monitored heartbeat target. */
	private final ResourceID resourceID;   // 被監控的resource ID

	/** Associated heartbeat target. */
	private final HeartbeatTarget<O> heartbeatTarget; //心跳目標

	private final ScheduledExecutor scheduledExecutor;

	/** Listener which is notified about heartbeat timeouts. */
	private final HeartbeatListener<?, ?> heartbeatListener; // 心跳監聽器

	/** Maximum heartbeat timeout interval. */
	private final long heartbeatTimeoutIntervalMs;

	private volatile ScheduledFuture<?> futureTimeout;
  //  AtomicReference  使用 
	private final AtomicReference<State> state = new AtomicReference<>(State.RUNNING);
   //  最近一次接收到心跳的時間
  private volatile long lastHeartbeat;
  
	// 報告心跳
	public void reportHeartbeat() {
    // 保留最近一次接收心跳時間
		lastHeartbeat = System.currentTimeMillis();
    // 接收心跳后,重置timeout線程
		resetHeartbeatTimeout(heartbeatTimeoutIntervalMs);
	}

  // 心跳超時,觸發lister的notifyHeartbeatTimeout
	public void run() {
		// The heartbeat has timed out if we're in state running
		if (state.compareAndSet(State.RUNNING, State.TIMEOUT)) {
			heartbeatListener.notifyHeartbeatTimeout(resourceID);
		}
	}

  //  重置TIMEOUT
	void resetHeartbeatTimeout(long heartbeatTimeout) {
		if (state.get() == State.RUNNING) {
      //先取消線程,在重新開啟
			cancelTimeout();
      // 啟動超時線程
			futureTimeout = scheduledExecutor.schedule(this, heartbeatTimeout, TimeUnit.MILLISECONDS);

			// Double check for concurrent accesses (e.g. a firing of the scheduled future)
			if (state.get() != State.RUNNING) {
				cancelTimeout();
			}
		}
	}

3.3.3 HeartbeatServices

建立heartbeat receivers and heartbeat senders,主要是對外提供服務。這裏我們可以看到:

  • HeartbeatManagerImpl就是receivers。
  • HeartbeatManagerSenderImpl就是senders。
public class HeartbeatServices {
	// Creates a heartbeat manager which does not actively send heartbeats.
	public <I, O> HeartbeatManager<I, O> createHeartbeatManager(...) {
		return new HeartbeatManagerImpl<>(...);
	}
	// Creates a heartbeat manager which actively sends heartbeats to monitoring targets.
	public <I, O> HeartbeatManager<I, O> createHeartbeatManagerSender(...) {
		return new HeartbeatManagerSenderImpl<>(...);
	}
}

0x04 初始化

4.1 心跳服務創建

心跳管理服務在Cluster入口創建。因為我們是調試,所以在MiniCluster.start調用。

public void start() throws Exception {
				......
				heartbeatServices = HeartbeatServices.fromConfiguration(configuration);
				......
}

HeartbeatServices.fromConfiguration會從Configuration中獲取配置信息:

  • 心跳間隔 heartbeat.interval
  • 心跳超時時間 heartbeat.timeout

這個就是我們解決最開始問題的思路:從配置信息入手,擴大心跳間隔。

public HeartbeatServices(long heartbeatInterval, long heartbeatTimeout) {
		this.heartbeatInterval = heartbeatInterval;
		this.heartbeatTimeout = heartbeatTimeout;
}

public static HeartbeatServices fromConfiguration(Configuration configuration) {
		long heartbeatInterval = configuration.getLong(HeartbeatManagerOptions.HEARTBEAT_INTERVAL);
		long heartbeatTimeout = configuration.getLong(HeartbeatManagerOptions.HEARTBEAT_TIMEOUT);

		return new HeartbeatServices(heartbeatInterval, heartbeatTimeout);
}

0x05 Flink中具體應用

5.1 總述

5.1.1 RM, JM, TM之間關係

系統中有幾個ResourceManager?整個 Flink 集群中只有一個 ResourceManager。

系統中有幾個JobManager?JobManager 負責管理作業的執行。默認情況下,每個 Flink 集群只有一個 JobManager 實例。JobManager 相當於整個集群的 Master 節點,負責整個集群的任務管理和資源管理。

系統中有幾個TaskManager?這個由具體啟動方式決定。比如Flink on Yarn,Session模式能夠指定拉起多少個TaskManager。 Per job模式中TaskManager數量是在提交作業時根據併發度動態計算,即Number of TM = Parallelism/numberOfTaskSlots。比如:有一個作業,Parallelism為10,numberOfTaskSlots為1,則TaskManager為10。

5.1.2 三者間心跳機制

Flink中ResourceManager、JobMaster、TaskExecutor三者之間存在相互檢測的心跳機制:

  • ResourceManager會主動發送請求探測JobMaster、TaskExecutor是否存活。
  • JobMaster也會主動發送請求探測TaskExecutor是否存活,以便進行任務重啟或者失敗處理。

我們之前講過,HeartbeatManagerSenderImpl屬於Sender,HeartbeatManagerImpl屬於Receiver。

  1. HeartbeatManagerImpl所處位置可以理解為client,存在於JobMaster與TaskExecutor中;
  2. HeartbeatManagerSenderImpl類,繼承 HeartbeatManagerImpl類,用於周期發送心跳請求,所處位置可以理解為server, 存在於JobMaster、ResourceManager中。

ResourceManager 級別最高,所以兩個HM都是Sender,監控taskManager和jobManager

public abstract class ResourceManager<WorkerType extends ResourceIDRetrievable>
		extends FencedRpcEndpoint<ResourceManagerId>
		implements ResourceManagerGateway, LeaderContender {
	taskManagerHeartbeatManager = heartbeatServices.createHeartbeatManagerSender
	jobManagerHeartbeatManager = heartbeatServices.createHeartbeatManagerSender
}

JobMaster級別中等,一個Sender, 一個Receiver,受到ResourceManager的監控,監控taskManager。

public class JobMaster extends FencedRpcEndpoint<JobMasterId> implements JobMasterGateway, JobMasterService {
	taskManagerHeartbeatManager = heartbeatServices.createHeartbeatManagerSender
	resourceManagerHeartbeatManager = heartbeatServices.createHeartbeatManager
}

TaskExecutor級別最低,兩個Receiver,分別被JM和RM疾控。

public class TaskExecutor extends RpcEndpoint implements TaskExecutorGateway {
	this.jobManagerHeartbeatManager = return heartbeatServices.createHeartbeatManager
	this.resourceManagerHeartbeatManager = return heartbeatServices.createHeartbeatManager
}

以JobManager和TaskManager為例。JM在啟動時會開啟周期調度,向已經註冊到JM中的TM發起心跳檢查,通過RPC調用TM的requestHeartbeat方法,重置對JM超時線程的調用,表示當前JM狀態正常。在TM的requestHeartbeat方法被調用后,通過RPC調用JM的receiveHeartbeat,重置對TM超時線程的調用,表示TM狀態正常。

5.2 初始化過程

5.2.1 TaskExecutor初始化

TM初始化生成了兩個Receiver HM。

public class TaskExecutor extends RpcEndpoint implements TaskExecutorGateway {
	/** The heartbeat manager for job manager in the task manager. */
	private final HeartbeatManager<AllocatedSlotReport, AccumulatorReport> jobManagerHeartbeatManager;

	/** The heartbeat manager for resource manager in the task manager. */
	private final HeartbeatManager<Void, TaskExecutorHeartbeatPayload> resourceManagerHeartbeatManager;
  
  //初始化函數
	this.jobManagerHeartbeatManager = createJobManagerHeartbeatManager(heartbeatServices, resourceId);
	this.resourceManagerHeartbeatManager = createResourceManagerHeartbeatManager(heartbeatServices, resourceId);
}

生成HeartbeatManager時,就註冊了ResourceManagerHeartbeatListener和JobManagerHeartbeatListener。

此時,兩個HeartbeatManagerImpl中已經創建好對應monitor線程,只有在JM或者RM執行requestHeartbeat后,才會觸發該線程的執行。

5.2.2 JobMaster的初始化

JM生成了一個Sender HM,一個Receiver HM。這裡會註冊 TaskManagerHeartbeatListener 和 ResourceManagerHeartbeatListener

public class JobMaster extends FencedRpcEndpoint<JobMasterId> implements JobMasterGateway, JobMasterService {
  private HeartbeatManager<AccumulatorReport, AllocatedSlotReport> taskManagerHeartbeatManager;
  private HeartbeatManager<Void, Void> resourceManagerHeartbeatManager;

  private void startHeartbeatServices() {
      taskManagerHeartbeatManager = heartbeatServices.createHeartbeatManagerSender(
        resourceId,
        new TaskManagerHeartbeatListener(),
        getMainThreadExecutor(),
        log);

      resourceManagerHeartbeatManager = heartbeatServices.createHeartbeatManager(
        resourceId,
        new ResourceManagerHeartbeatListener(),
        getMainThreadExecutor(),
        log);
  }
}

5.2.3 ResourceManager初始化

JobMaster在啟動時候,會在startHeartbeatServices函數中生成兩個Sender HeartbeatManager。

taskManagerHeartbeatManager :HeartbeatManagerSenderImpl對象,會反覆啟動一個定時器,定時掃描需要探測的對象並且發送心跳請求。

jobManagerHeartbeatManager :HeartbeatManagerSenderImpl,會反覆啟動一個定時器,定時掃描需要探測的對象並且發送心跳請求。

taskManagerHeartbeatManager = heartbeatServices.createHeartbeatManagerSender(
			resourceId,
			new TaskManagerHeartbeatListener(),
			getMainThreadExecutor(),
			log);

jobManagerHeartbeatManager = heartbeatServices.createHeartbeatManagerSender(
			resourceId,
			new JobManagerHeartbeatListener(),
			getMainThreadExecutor(),
			log);  

5.3 註冊過程

我們以TM與RM交互為例。TaskExecutor啟動之後,需要註冊到RM和JM中。

流程圖如下:

 * 1. Run in Task Manager
 *
 *    TaskExecutor.onStart //Life cycle
 *        |
 *        +----> startTaskExecutorServices@TaskExecutor
 *        |     //開始TM服務
 *        |     
 *        +----> resourceManagerLeaderRetriever.start(new ResourceManagerLeaderListener());
 *        |     // 開始連接到RM
 *        |     // start by connecting to the ResourceManager
 *        |    
 *        +----> notifyLeaderAddress@ResourceManagerLeaderListener  
 *        |     // 當RM狀態變化之後,將回調到這裏    
 *        |     // The listener for leader changes of the resource manager.
 *        |        
 *        +----> reconnectToResourceManager@TaskExecutor   
 *        |     // 以下三步調用是漸進的,就是與RM聯繫。
 *        |    
 *        +----> tryConnectToResourceManager@TaskExecutor 
 *        |     
 *        +----> connectToResourceManager()@TaskExecutor
 *        |     // 主要作用是生成了 TaskExecutorToResourceManagerConnection    
 *        |  
 *        +----> start@TaskExecutorToResourceManagerConnection
 *        |     // 開始RPC調用,將會調用到其基類RegisteredRpcConnection的start   
 *        |   
 *        +----> start@RegisteredRpcConnection
 *        |     // RegisteredRpcConnection實現了組件之間註冊聯繫的基本RPC
 *        |      
   
   
 * ~~~~~~~~ 這裡是 Akka RPC
   
 * 2. Run in Resource Manager   
 * 現在程序執行序列到達了RM, 主要是添加一個Target到RM 的 Sender HM;
 *
 *    registerTaskExecutor@ResourceManager
 *        |
 *        +----> taskExecutorGatewayFuture.handleAsync
 *        |     // 異步調用到這裏
 *        |     
 *        +----> registerTaskExecutorInternal@ResourceManager
 *        |     // RM的內部實現,將把TM註冊到RM自己這裏
 *        |      
 *        +----> taskManagerHeartbeatManager.monitorTarget
 *        |     // 生成HeartbeatMonitor,
 *        |      
 *        +---->  heartbeatTargets.put(resourceID,heartbeatMonitor);
 *        |     // 把Monitor放到 HM in TM之中,就是說TM開始監控了RM
 *        |        

 * ~~~~~~~~ 這裡是 Akka RPC
  
 * 3. Run in Task Manager
 * 現在程序回到了TM, 主要是添加一個Target到 TM 的 Receiver HM;
 *
 *    onRegistrationSuccess@TaskExecutorToResourceManagerConnection
 *        |   
 *        |   
 *        +---->  onRegistrationSuccess@ResourceManagerRegistrationListener 
 *        |       // 回調函數
 *        |  
 *        +---->  runAsync(establishResourceManagerConnection) 
 *        |       // 異步執行
 *        |  
 *        +---->  establishResourceManagerConnection@TaskExecutor 
 *        |       // 說明已經和RM建立了聯繫,所以可以開始監控RM了
 *        |  
 *        +---->  resourceManagerHeartbeatManager.monitorTarget 
 *        |     // 生成HeartbeatMonitor,
 *        |      
 *        +---->  heartbeatTargets.put(resourceID,heartbeatMonitor);  
 *        |       // 把 RM 也註冊到 TM了
 *        |       // monitor the resource manager as heartbeat target 

下面是具體文字描述。

5.3.1 TM註冊到RM中

5.3.1.1 TM的操作
  • TaskExecutor啟動之後,調用onStart,開始其生命周期。
  • onStart直接調用startTaskExecutorServices。
  • 啟動服務的第一步就是與ResourceManager取得聯繫,這裏註冊了一個ResourceManagerLeaderListener(),用來監聽RM Leader的變化。
private final LeaderRetrievalService resourceManagerLeaderRetriever;
// resourceManagerLeaderRetriever其實是EmbeddedLeaderService的實現,A simple leader election service, which selects a leader among contenders and notifies listeners.

resourceManagerLeaderRetriever.start(new ResourceManagerLeaderListener());
  • 當得到RM Leader的地址之後,會調用到回調函數notifyLeaderAddress@ResourceManagerLeaderListener,然後調用notifyOfNewResourceManagerLeader。
  • notifyOfNewResourceManagerLeader中獲取到RM地址后,就通過reconnectToResourceManager與RM聯繫。
  • reconnectToResourceManager中間接調用到TaskExecutorToResourceManagerConnection。其作用是建立TaskExecutor 和 ResourceManager之間的聯繫。因為知道 ResourceManagerGateway所以才能進行RPC操作。
  • 然後在 TaskExecutorToResourceManagerConnection中,就通過RPC與RM聯繫。
5.3.1.2 RM的操作
  • RPC調用后,程序就來到了RM中,RM做如下操作:
  • 會註冊一個新的TaskExecutor到自己的taskManagerHeartbeatManager中。
  • registerTaskExecutor@ResourceManager會通過異步調用到registerTaskExecutorInternal。
  • registerTaskExecutorInternal中首先看看是否這個TaskExecutor的ResourceID之前註冊過,如果註冊過就移除再添加一個新的TaskExecutor。
  • 通過 taskManagerHeartbeatManager.monitorTarget 開始進行心跳機制的註冊。
taskManagerHeartbeatManager.monitorTarget(taskExecutorResourceId, new HeartbeatTarget<Void>() {
				public void receiveHeartbeat(ResourceID resourceID, Void payload) {
					// the ResourceManager will always send heartbeat requests to the
					// TaskManager
				}
				public void requestHeartbeat(ResourceID resourceID, Void payload) {
					taskExecutorGateway.heartbeatFromResourceManager(resourceID);
				}
});

當註冊完成后,RM中的Sender HM內部結構如下,能看出來多了一個Target:

taskManagerHeartbeatManager = {HeartbeatManagerSenderImpl@8866} 
 heartbeatPeriod = 10000
 heartbeatTimeoutIntervalMs = 50000
 ownResourceID = {ResourceID@8871} "040709f36ebf38f309fed518a88946af"
 heartbeatListener = {ResourceManager$TaskManagerHeartbeatListener@8872} 
 mainThreadExecutor = {RpcEndpoint$MainThreadExecutor@8873} 
 heartbeatTargets = {ConcurrentHashMap@8875}  size = 1
  {ResourceID@8867} "630c15c9-4861-4b41-9c95-92504f458b71" -> {HeartbeatMonitorImpl@9448} 
   key = {ResourceID@8867} "630c15c9-4861-4b41-9c95-92504f458b71"
   value = {HeartbeatMonitorImpl@9448} 
    resourceID = {ResourceID@8867} "630c15c9-4861-4b41-9c95-92504f458b71"
    heartbeatTarget = {ResourceManager$2@8868} 
    scheduledExecutor = {RpcEndpoint$MainThreadExecutor@8873} 
    heartbeatListener = {ResourceManager$TaskManagerHeartbeatListener@8872} 
    heartbeatTimeoutIntervalMs = 50000
    futureTimeout = {ScheduledFutureAdapter@10140} 
    state = {AtomicReference@9786} "RUNNING"
    lastHeartbeat = 0
5.3.1.3 返回到TM

RM會通過RPC再次回到TaskExecutor,其新執行序列如下:

  • 首先RPC調用到了 onRegistrationSuccess@TaskExecutorToResourceManagerConnection。
  • 然後onRegistrationSuccess@ResourceManagerRegistrationListener中通過異步執行調用到了establishResourceManagerConnection。這說明TM已經和RM建立了聯繫,所以可以開始監控RM了。
  • 然後和RM操作類似,通過resourceManagerHeartbeatManager.monitorTarget 來把RM註冊到自己這裏。
HeartbeatMonitor<O> heartbeatMonitor = heartbeatMonitorFactory.createHeartbeatMonitor 
heartbeatTargets.put(resourceID, heartbeatMonitor);  

當註冊完成后,其Receiver HM結構如下:

resourceManagerHeartbeatManager = {HeartbeatManagerImpl@10163} 
 heartbeatTimeoutIntervalMs = 50000
 ownResourceID = {ResourceID@8882} "96a9b80c-dd97-4b63-9049-afb6662ea3e2"
 heartbeatListener = {TaskExecutor$ResourceManagerHeartbeatListener@10425} 
 mainThreadExecutor = {RpcEndpoint$MainThreadExecutor@10426} 
 heartbeatTargets = {ConcurrentHashMap@10427}  size = 1
  {ResourceID@8886} "122fa66685133b11ea26ee1b1a6cef75" -> {HeartbeatMonitorImpl@10666} 
   key = {ResourceID@8886} "122fa66685133b11ea26ee1b1a6cef75"
   value = {HeartbeatMonitorImpl@10666} 
    resourceID = {ResourceID@8886} "122fa66685133b11ea26ee1b1a6cef75"
    heartbeatTarget = {TaskExecutor$1@10668} 
    scheduledExecutor = {RpcEndpoint$MainThreadExecutor@10426} 
    heartbeatListener = {TaskExecutor$ResourceManagerHeartbeatListener@10425} 
    heartbeatTimeoutIntervalMs = 50000
    futureTimeout = {ScheduledFutureAdapter@10992} 
    state = {AtomicReference@10667} "RUNNING"
    lastHeartbeat = 0

5.3.2 TM註冊到 JM

其調用基本思路與之前相同,就是TM和JM之間互相註冊一個代表對方的monitor:

JobLeaderListenerImpl ----> establishJobManagerConnection

消息到了JM中,做如下操作。

registerTaskManager ----> taskManagerHeartbeatManager.monitorTarget
  // monitor the task manager as heartbeat target

5.4 心跳過程

在任務提交之後,我們就進入了正常的心跳監控流程。我們依然用 TM 和 RM進行演示。

我們先給出一個流程圖。

 * 1. Run in Resouce Manager
 *
 *    HeartbeatManagerSender in RM
 *        |
 *        +----> run@HeartbeatManagerSenderImpl
 *        |     //遍歷所有監控的Monitor(Target),逐一在Target上調用requestHeartbeat
 *        |     
 *        +----> requestHeartbeat@HeartbeatManagerSenderImpl 
 *        |     // 將調用具體監控對象的自定義函數
 *        |     // heartbeatTarget.requestHeartbeat(getOwnResourceID(), payload);
 *        |      
 *        +----> getHeartbeatListener().retrievePayload   
 *        |     // 調用到TaskManagerHeartbeatListener@ResourceManager    
 *        |     // 這裡是return null;,因為RM不會是任何人的Receiver
 *        |        
 *        +----> requestHeartbeat@HeartbeatTarget   
 *        |     // 調用到Target這裏,代碼在ResourceManager這裏,就是生成Target時候賦值的
 *        |    
 *        +----> taskExecutorGateway.heartbeatFromResourceManager
 *        |     // 會通過gateway RPC 調用到TM,這就是主動對TM發起了心跳請求
 *        |      

 * ~~~~~~~~ 這裡是 Akka RPC
   
 * 2. Run in Task Manager   
 * 現在程序執行序列到達了TM, 主要是 1. 重置TM的Monitor線程; 2.返回一些負載信息;
 *
 *    heartbeatFromResourceManager@TaskExecutor
 *        |
 *        +----> resourceManagerHeartbeatManager.requestHeartbeat(resourceID, null);
 *        |     //開始要調用到 Receiver HM in Task Manager
 *        |     
 *        +----> requestHeartbeat@HeartbeatManager in TM 
 *        |     // 在Receiver HM in Task Manager 這裏運行
 *        |      
 *        +----> reportHeartbeat@HeartbeatMonitor   
 *        |     //reportHeartbeat : 記錄發起請求的這個時間點,然後resetHeartbeatTimeout
 *        |      
 *        +----> resetHeartbeatTimeout@HeartbeatMonitor
 *        |     // 如果Monitor狀態依然是RUNNING,則取消之前設置的ScheduledFuture。
 *        |     // 重新創建一個ScheduleFuture。因為如果不取消,則之前那個ScheduleFuture運行時
 *        |     // 會調用HeartbeatMonitorImpl.run函數,run直接compareAndSet后,通知目標函數
 *        |     // 目前已經超時,即調用heartbeatListener.notifyHeartbeatTimeout。
 *        |     // 這裏代表 JM 狀態正常。   
 *        |      
 *        +---->  heartbeatListener.reportPayload
 *        |     // 把Target節點的最新的heartbeatPayload通知給heartbeatListener。 
 *        |     // heartbeatListerner是外部傳入的,它根據所擁有的節點的心跳記錄做監聽管理。
 *        |        
 *        +---->  heartbeatTarget.receiveHeartbeat(getOwnResourceID(), heartbeatListener.retrievePayload(requestOrigin));
 *        |      
 *        |     
 *        +---->  retrievePayload@ResourceManagerHeartbeatListener in TM   
 *        |       // heartbeatTarget.receiveHeartbeat參數調用的
 *        |     
 *        +---->  return new TaskExecutorHeartbeatPayload
 *        |   
 *        |     
 *        +---->  receiveHeartbeat in TM 
 *        |       // 回到 heartbeatTarget.receiveHeartbeat,這就是TM生成Target的時候的自定義函數
 *        |       // 就是響應一個心跳消息回給RM
 *        |     
 *        +----> resourceManagerGateway.heartbeatFromTaskManager  
 *        |     // 會通過gateway RPC 調用到 ResourcManager
 *        |      

 * ~~~~~~~~ 這裡是 Akka RPC
  
 * 3. Run in Resouce Manager
 * 現在程序回到了RM, 主要是 1.重置RM的Monitor線程;2. 上報收到TaskExecutor的負載信息  
 *
 *    heartbeatFromTaskManager in RM
 *        |   
 *        |   
 *        +---->  taskManagerHeartbeatManager.receiveHeartbeat 
 *        |       // 這是個Sender HM
 *        |  
 *        +---->  HeartbeatManagerImpl.receiveHeartbeat  
 *        |  
 *        |     
 *        +---->  HeartbeatManagerImpl.reportHeartbeat(heartbeatOrigin);
 *        | 
 *        |    
 *        +---->  heartbeatMonitor.reportHeartbeat(); 
 *        |      // 這裏就是重置RM 這裏對應的Monitor。在reportHeartbeat重置 JM monitor線程的觸發,即cancelTimeout取消註冊時候的超時定時任務,並且註冊下一個超時檢測futureTimeout;這代表TM正常執行。
 *        |     
 *        +----> heartbeatListener.reportPayload    
 *        |      //把Target節點的最新的heartbeatPayload通知給 TaskManagerHeartbeatListener。heartbeatListerner是外部傳入的,它根據所擁有的節點的心跳記錄做監聽管理。 
 *        |      
 *        +----> slotManager.reportSlotStatus(instanceId, payload.getSlotReport());
 *        |     // TaskManagerHeartbeatListener中調用,上報收到TaskExecutor的負載信息
 *        |        

下面是具體文字描述。

5.4.1 ResourceManager主動發起

5.4.1.1 Sender遍歷所有監控的Monitor(Target)

心跳機制是由Sender主動發起的。這裏就是 ResourceManager 的HeartbeatManagerSenderImpl中定時schedual調用,這裡會遍歷所有監控的Monitor(Target),逐一在Target上調用requestHeartbeat。

// HeartbeatManagerSenderImpl中的代碼

	@Override
	public void run() {
		if (!stopped) {
			for (HeartbeatMonitor<O> heartbeatMonitor : getHeartbeatTargets().values()) {
        // 這裏向被監控對象節點發起一次心跳請求,載荷是heartbeatPayLoad,要求被監控對象回應心跳
				requestHeartbeat(heartbeatMonitor);
			}
			getMainThreadExecutor().schedule(this, heartbeatPeriod, TimeUnit.MILLISECONDS);
		}
	}
}

// 運行時候的變量
this = {HeartbeatManagerSenderImpl@9037} 
 heartbeatPeriod = 10000
 heartbeatTimeoutIntervalMs = 50000
 ownResourceID = {ResourceID@8788} "d349506cae32cadbe99b9f9c49a01c95"
 heartbeatListener = {ResourceManager$TaskManagerHeartbeatListener@8789} 
 mainThreadExecutor = {RpcEndpoint$MainThreadExecutor@8790} 

// 調用棧如下
requestHeartbeat:711, ResourceManager$2 (org.apache.flink.runtime.resourcemanager)
requestHeartbeat:702, ResourceManager$2 (org.apache.flink.runtime.resourcemanager)
requestHeartbeat:92, HeartbeatManagerSenderImpl (org.apache.flink.runtime.heartbeat)
run:81, HeartbeatManagerSenderImpl (org.apache.flink.runtime.heartbeat)
call:511, Executors$RunnableAdapter (java.util.concurrent)
run$$$capture:266, FutureTask (java.util.concurrent)
run:-1, FutureTask (java.util.concurrent)
5.4.1.2 Target進行具體操作

具體監控對象 Target 會調用自定義的requestHeartbeat。

HeartbeatManagerSenderImpl

	private void requestHeartbeat(HeartbeatMonitor<O> heartbeatMonitor) {
		O payload = getHeartbeatListener().retrievePayload(heartbeatMonitor.getHeartbeatTargetId());
		final HeartbeatTarget<O> heartbeatTarget = heartbeatMonitor.getHeartbeatTarget();

    // 這裏就是具體監控對象
		heartbeatTarget.requestHeartbeat(getOwnResourceID(), payload);
	}

heartbeatTarget = {ResourceManager$2@10688} 
 taskExecutorGateway = {$Proxy42@9459} "org.apache.flink.runtime.rpc.akka.AkkaInvocationHandler@6d0c8334"
 this$0 = {StandaloneResourceManager@9458} 

請注意,每一個Target都是由ResourceManager生成的。ResourceManager之前註冊成為Monitor時候就註冊了這個HeartbeatTarget。

這個HeartbeatTarget的定義如下,兩個函數是:

  • receiveHeartbeat :這個是空,因為RM沒有自己的Sender。

  • requestHeartbeat :這個針對TM,就是調用TM的heartbeatFromResourceManager,當然是通過RPC調用。

5.4.1.3 RPC調用

會調用到ResourceManager定義的函數requestHeartbeat,而requestHeartbeat會通過gateway調用到TM,這就是主動對TM發起了心跳請求。

taskManagerHeartbeatManager.monitorTarget(taskExecutorResourceId, new HeartbeatTarget<Void>() {
   @Override
   public void receiveHeartbeat(ResourceID resourceID, Void payload) {
      // the ResourceManager will always send heartbeat requests to the TaskManager
   }

   @Override
   public void requestHeartbeat(ResourceID resourceID, Void payload) {
      //就是調用到這裏
      taskExecutorGateway.heartbeatFromResourceManager(resourceID); 
   }
});

5.4.2 RM通過RPC調用TM

通過taskExecutorGateway。心跳程序執行就通過RPC從RM跳躍到了TM。

taskExecutorGateway.heartbeatFromResourceManager 的意義就是:通過RPC調用回到TaskExecutor。這個是在TaskExecutorGateway就定義好的。

// TaskExecutor RPC gateway interface.
public interface TaskExecutorGateway extends RpcGateway

TaskExecutor實現了TaskExecutorGateway,所以具體在TaskExecutor內部實現了接口函數。

@Override
public void heartbeatFromResourceManager(ResourceID resourceID) {
    //調用到了這裏 ...........
		resourceManagerHeartbeatManager.requestHeartbeat(resourceID, null);
}

TM中,resourceManagerHeartbeatManager 定義如下。

/** The heartbeat manager for resource manager in the task manager. */
private final HeartbeatManager<Void, TaskExecutorHeartbeatPayload> resourceManagerHeartbeatManager;

所以下面就是執行TM中的Receiver HM。在這個過程中有兩個處理步驟:

  1. 調用對應HeartbeatMonitor的reportHeartbeat方法,cancelTimeout取消註冊時候的超時定時任務,並且註冊下一個超時檢測futureTimeout;
  2. 調用monitorTarget的receiveHeartbeat方法,也就是會通過rpc調用JobMaster的heartbeatFromTaskManager方法返回一些負載信息;

具體是調用 requestHeartbeat@HeartbeatManager。在其中會

  • 調用reportHeartbeat@HeartbeatMonitor,記錄發起請求的這個時間點,然後resetHeartbeatTimeout。
  • 在resetHeartbeatTimeout@HeartbeatMonitor之中,如果Monitor狀態依然是RUNNING,則取消之前設置的ScheduledFuture。重新創建一個ScheduleFuture。因為如果不取消,則之前那個ScheduleFuture運行時會調用HeartbeatMonitorImpl.run函數,run直接compareAndSet后,通知目標函數目前已經超時,即調用heartbeatListener.notifyHeartbeatTimeout。
  • 調用 heartbeatListener.reportPayload,把Target節點的最新的heartbeatPayload通知給heartbeatListener。
  • 調用 heartbeatTarget.receiveHeartbeat(getOwnResourceID(), heartbeatListener.retrievePayload(requestOrigin)); 就是響應一個心跳消息回給RM。
	@Override
	public void requestHeartbeat(final ResourceID requestOrigin, I heartbeatPayload) {
		if (!stopped) {
			log.debug("Received heartbeat request from {}.", requestOrigin);

			final HeartbeatTarget<O> heartbeatTarget = reportHeartbeat(requestOrigin);

			if (heartbeatTarget != null) {
				if (heartbeatPayload != null) {
					heartbeatListener.reportPayload(requestOrigin, heartbeatPayload);
				}

				heartbeatTarget.receiveHeartbeat(getOwnResourceID(), heartbeatListener.retrievePayload(requestOrigin));
			}
		}
	}

最後會通過resourceManagerGateway.heartbeatFromTaskManager 調用到 ResourcManager。

5.4.3 TM 通過RPC回到 RM

JobMaster在接收到rpc請求后調用其heartbeatFromTaskManager方法,會調用taskManagerHeartbeatManager的receiveHeartbeat方法,在這個過程中同樣有兩個處理步驟:

  1. 調用對應HeartbeatMonitor的reportHeartbeat方法,cancelTimeout取消註冊時候的超時定時任務,並且註冊下一個超時檢測futureTimeout;
  2. 調用TaskManagerHeartbeatListener的reportPayload方法,上報收到TaskExecutor的負載信息

至此一次完成心跳過程已經完成,會根據heartbeatInterval執行下一次心跳。

5.5 超時處理

5.5.1 TaskManager

首先,在HeartbeatMonitorImpl中,如果超時,會調用Listener。

public void run() {
   // The heartbeat has timed out if we're in state running
   if (state.compareAndSet(State.RUNNING, State.TIMEOUT)) {
      heartbeatListener.notifyHeartbeatTimeout(resourceID);
   }
}

這就來到了ResourceManagerHeartbeatListener,會嘗試再次連接RM。

private class ResourceManagerHeartbeatListener implements HeartbeatListener<Void, TaskExecutorHeartbeatPayload> {

   @Override
   public void notifyHeartbeatTimeout(final ResourceID resourceId) {
      validateRunsInMainThread();
      // first check whether the timeout is still valid
      if (establishedResourceManagerConnection != null && establishedResourceManagerConnection.getResourceManagerResourceId().equals(resourceId)) {
         reconnectToResourceManager(new TaskManagerException(
            String.format("The heartbeat of ResourceManager with id %s timed out.", resourceId)));
      } else {
         .....
      }
   }

5.5.2 ResourceManager

RM就直接簡單粗暴,關閉連接。

private class TaskManagerHeartbeatListener implements HeartbeatListener<TaskExecutorHeartbeatPayload, Void> {

   @Override
   public void notifyHeartbeatTimeout(final ResourceID resourceID) {
      validateRunsInMainThread();
      closeTaskManagerConnection(
         resourceID,
         new TimeoutException("The heartbeat of TaskManager with id " + resourceID + "  timed out."));
   }
}  

0x06 解決問題

心跳機制我們講解完了,但是我們最初提到的異常應該如何解決呢?在程序最開始生成環境變量時候,通過設置環境變量的配置即可搞定:

Configuration conf = new Configuration();
conf.setString("heartbeat.timeout", "18000000");
final LocalEnvironment env = ExecutionEnvironment.createLocalEnvironment(conf);

0x07 參考

[flink-001]flink的心跳機制

Flink中心跳機制

flink1.8 心跳服務

你有必要了解一下Flink底層RPC使用的框架和原理

flink RPC(akka)

弄清Flink1.8的遠程過程調用(RPC)

Apache Flink源碼解析 (七)Flink RPC的底層實現

flink源碼閱讀第一篇—入口

flink-on-yarn 基礎架構和啟動流程

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

【asp.net core 系列】13 Identity 身份驗證入門

0. 前言

通過前兩篇我們實現了如何在Service層如何訪問數據,以及如何運用簡單的加密算法對數據加密。這一篇我們將探索如何實現asp.net core的身份驗證。

1. 身份驗證

asp.net core的身份驗證有 JwtBearer和Cookie兩種常見的模式,在這一篇我們將啟用Cookie作為身份信息的保存。那麼,我們如何啟用呢?

在Startup.cs 的ConfigureServices(IServiceCollection services) 方法里添加如下:

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
                {
                    Configuration.Bind("CookieSettings",options);
                });

此時可以啟動一個權限驗證,當用戶訪問需要驗證的頁面或接口時,如果沒有登錄,則會自動跳轉到:

https://localhost:5001/Account/Login?ReturnUrl=XXXX

其中ReturnUrl指向來源頁。

1.1 設置驗證

當我們在Startup類里設置啟用了身份驗證后,並不是訪問所有接口都會被跳轉到登錄頁面。那麼如何設置訪問的路徑需要身份驗證呢?asp.net core為我們提供了一個特性類:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeAttribute : Attribute, IAuthorizeData
{
    public string Policy { get; set; }
    public string Roles { get; set; }
    public string AuthenticationSchemes { get; set; }
}

可以看的出,這個特性類允許設置在類、方法上,可以設置多個,允許子類繼承父類的特性。所以可以在控制器上設置[Authorize],當在控制器上設置以後訪問控制器里所有的Action都會要求驗證身份;也可以單獨設置在Action上,表示該Action需要驗證身份,控制器里的其他方法不需要驗證。

1.2 設置忽略

我們在開發過程中,會遇到這樣的一組鏈接或者頁面:請求地址同屬於一個控制器下,但其中某個地址可以不用用戶登錄就可以訪問。通常我們為了減少重複代碼以及復用性等方面的考慮,會直接在控制器上設置身份驗證要求,而不是在控制器里所有的Action上添加驗證要求。

那麼,我們如何放開其中的某個請求,可以允許它不用身份驗證。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AllowAnonymousAttribute : Attribute, IAllowAnonymous
{
}

仔細觀察,可以看得出這個特性可以設置在類、方法上,不允許多次設置,允許子類繼承父類的特性。

這個特性的使用沒啥可說的,不過需要注意的是,不要與AuthorizeAttribute一起使用。雖然編譯上沒啥問題,但實際上會對程序員的邏輯照成一定程度的誤導。

2.保存身份

有身份驗證,就必然需要保存身份。當我們從數據庫中或者其他的三方服務中獲取到用戶信息后,我們需要將用戶信息保存起來,而不是每次都向用戶或者服務提供方索求信息。

在asp.net core中,Controller類里有一個屬性:

public HttpContext HttpContext { get; }

HttpContext 提供了一個擴展方法,可以用來保存用戶信息:

public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal);

暫時忽略這個方法的返回類型,它接受了一個ClaimsPrincipal類型的參數。我們來看下這個類的基本情況吧:

public class ClaimsPrincipal : IPrincipal
{

    public ClaimsPrincipal();
    public ClaimsPrincipal(IEnumerable<ClaimsIdentity> identities);
    public ClaimsPrincipal(BinaryReader reader);
    public ClaimsPrincipal(IIdentity identity);
    public ClaimsPrincipal(IPrincipal principal);
    
    public static ClaimsPrincipal Current { get; }
    public static Func<ClaimsPrincipal> ClaimsPrincipalSelector { get; set; }
    public static Func<IEnumerable<ClaimsIdentity>, ClaimsIdentity> PrimaryIdentitySelector { get; set; }
    public virtual IIdentity Identity { get; }
    public virtual IEnumerable<ClaimsIdentity> Identities { get; }
    public virtual IEnumerable<Claim> Claims { get; }
    public virtual void AddIdentities(IEnumerable<ClaimsIdentity> identities);
    public virtual void AddIdentity(ClaimsIdentity identity);
    public virtual ClaimsPrincipal Clone();
    public virtual IEnumerable<Claim> FindAll(Predicate<Claim> match);
    public virtual IEnumerable<Claim> FindAll(string type);
    public virtual Claim FindFirst(string type);
    public virtual Claim FindFirst(Predicate<Claim> match);
    public virtual bool HasClaim(Predicate<Claim> match);
    public virtual bool HasClaim(string type, string value);
    public virtual bool IsInRole(string role);
    public virtual void WriteTo(BinaryWriter writer);
}

方法和屬性有點多,那麼我們重點關注一下構造函數以及可以AddXXX開頭的方法。

這裡有一個竅門,對於一個陌生的類來說,構造函數對於類本身是個很重要的特徵,我們可以通過構造函數分析出這個類需要哪些基礎數據。

所以,通過簡單的分析,我們需要繼續了解這兩個類:

public class ClaimsIdentity : IIdentity
{
    public ClaimsIdentity();
    public ClaimsIdentity(string authenticationType);
    public ClaimsIdentity(IIdentity identity);
    public ClaimsIdentity(IEnumerable<Claim> claims);
    public ClaimsIdentity(IEnumerable<Claim> claims, string authenticationType);
    public ClaimsIdentity(IIdentity identity, IEnumerable<Claim> claims);
    public ClaimsIdentity(string authenticationType, string nameType, string roleType);
    public ClaimsIdentity(IEnumerable<Claim> claims, string authenticationType, string nameType, string roleType);
    public ClaimsIdentity(IIdentity identity, IEnumerable<Claim> claims, string authenticationType, string nameType, string roleType);
    
}

public class Claim
{
    public Claim(BinaryReader reader);
    public Claim(BinaryReader reader, ClaimsIdentity subject);

    public Claim(string type, string value);
    public Claim(string type, string value, string valueType);
    public Claim(string type, string value, string valueType, string issuer);

    public Claim(string type, string value, string valueType, string issuer, string originalIssuer);
    public Claim(string type, string value, string valueType, string issuer, string originalIssuer, ClaimsIdentity subject);
    protected Claim(Claim other);
    protected Claim(Claim other, ClaimsIdentity subject);
    public string Type { get; }
    public ClaimsIdentity Subject { get; }
    public IDictionary<string, string> Properties { get; }
    public string OriginalIssuer { get; }
    public string Issuer { get; }
    public string ValueType { get; }
    public string Value { get; }
    protected virtual byte[] CustomSerializationData { get; }
    public virtual Claim Clone();
    public virtual Claim Clone(ClaimsIdentity identity);
    public override string ToString();
    public virtual void WriteTo(BinaryWriter writer);
    protected virtual void WriteTo(BinaryWriter writer, byte[] userData);
}

所以,看到這裏就會發現,我們可以通過以下方式保存信息:

List<Claim> claims = null;
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme,new ClaimsPrincipal(identity));

這時候,數據就可以保存在Cookie里了,那麼如何在控制器中獲取到數據呢:

public ClaimsPrincipal User { get; }

在控制器中,提供了這樣一個屬性,當然如果想要正確獲取到值的話,需要在 Startup.cs類中的添加如下配置:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ……省略其他配置
    app.UseAuthorization();
    app.UseAuthentication();
    // ……省略其他配置
}

3. 總結

在這一篇中,簡單介紹了asp.net core的identity,下一篇將從實際上帶領大家設置不一樣的identity以及Authorize驗證。

更多內容煩請關注我的博客《高先生小屋》

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

最詳細教學–win10 + frp + rdpwrap + 阿里雲服務器 –實現win10 多用戶同時遠程登錄內網機

概述:

  使用win10 專業版 + frp + RDPwrap + 阿里雲服務器 的組合實現win10 多用戶同時遠程登錄內網機。使用frp 做內網穿透,將內網機的指定端口暴露在外網,通過ip+port 來實現遠程登錄。再使用rdpwrap 來破解win10 不能同時多用戶登錄的問題。

 

 

設想一下場景

  我是一個建築工程師。經常出差,需要經常畫3D圖和展示建築圖紙,所以買了一台性能非常強的筆記本工作站。筆記本重量大概3.9kg,充電器0.5kg,一個本子,一個書包,全部加起來接近10斤的重量。每天背着10斤重的東西跑來跑去出差,想想都累!!!

  筆記本工作站不僅重,價格也很貴,非常不方便用於出差,那簡直是折磨。。。。。。

   被折磨幾個月後,他開始向他的一個朋友訴苦,這真的是太t*d痛苦了,我能不能背着一台輕薄本筆記本出差啊,可是性能又要很好才行,怎麼辦???

然後就有了這篇文章。

 

以↑純屬扯淡……

—————————————————我是完美分隔符—————————————————

一、先實現單用戶遠程登錄內網機

 1.為什麼要實現內網穿透

    繼續對話:

    你怎麼在互聯網裡面找到你家裡面的電腦,是不是要把你的電腦與互聯網對接上。

    是啊,對接上啦,我的電腦不是連着網線嗎……

    (我語文水平有問題……。)兩個條件:一是你的電腦與互聯網對接上,二是讓互聯網知道你家在哪裡,不,你的電腦在哪裡。

    明白,好的。那什麼是內網穿透啊?

    額,(心想:md還要給你解釋內網外網……還要幫你弄,還免費的,我還有一堆事要忙啊,si建築的)。

    enenen……我幫你弄好就行了,你看百度吧,給個鏈接你:https://baike.baidu.com/item/%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F

    (這不是本文的重點)

 2.要準備什麼呢

電腦若干台……
雲服務器(比如阿里雲服務器)
frp 反向代理工具(免費簡單高效)

 資源下載路徑>>>

   window端frp下載:https://github.com/fatedier/frp/releases/download/v0.30.0/frp_0.30.0_windows_amd64.zip

   linux端frp 下載:https://github.com/fatedier/frp/releases/download/v0.30.0/frp_0.30.0_linux_amd64.tar.gz

   資源解壓后的模樣>>>

      windows 解壓后                                                               linux  解壓后

                               

  

先上草圖過過目: 

 

 3.配置過程

      單用戶遠程登錄內網機需要配置的東東:

  1. windows下的frp客戶端:frpc
  2. linux的frp服務端:frps;
  3. 阿里雲服務器端口開放;
  4. windows的遠程登錄配置;
  5. 啟動window中的frpc 客戶端的命令:客戶端連接服務;
  6. 開啟另外一台windows電腦,準備遠程連接測試

 

   1) win 配置frpc.ini 文件(是 frpc ,是客戶端,別配錯了)

[common]
server_addr = 11.11.11.11
server_port = 7000

# trace, debug, info, warn, error
log_level = trace

#遠程桌面
[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 3389
remote_port = 6000

 

  2)Linux 配置frps.ini 文件 (服務端)

[common]
bind_port = 7000
vhost_http_port=8080

  啟動linux中的frps 服務的命令:啟動服務

當前窗口啟動,關閉窗口失效: ./frps -c frps.ini
後台啟動,關閉窗口依然有效:nohup ./frps -c frps.ini &

  啟動成功的效果分別是>>>

   

   

          第二張圖片里的 [1] 2374 是什麼???   

           輸入命令:kill 2374   就知道了……

 

  3)打開阿里雲服務器端口

    進入:安全組配置

 

   進入:安全組列表>配置規則

 

    開放端口:6000和7000

 

   阿里雲實例:重啟

   

  4)配置windows 遠程登錄用戶

     右鍵“我的電腦”進入這個界面    選擇“遠程設置”

     

 

      允許遠程連接

      

   

      點“添加”進入, >>>  “高級”  >>> “立即查找”  >>> 選擇用戶,用於遠程登錄

     

 

    PS: 對於win10 家庭版的用戶,遠程設置的界面是這樣子的

    

     對於win家庭版的用戶,將破解多用戶遠程登錄提前先做: 跳轉

 

  5)啟動window中的frpc 客戶端的命令:客戶端連接服務

打開cmd窗口:
先cd 到:frpc.exe 執行程序的目錄
再執行: frpc.exe -c frpc.ini

  啟動成功的效果圖>>> 簡單的描述一下

       

 

   6)開啟另外一台windows電腦,準備遠程連接測試

     快速打開遠程連接窗口:win + R   >>>  輸入:mstsc      再確定

     

 

     最激動人心的時刻到了

      

      遠程登錄輸入用戶密碼

      

 

     有這個界面說明成功了       點進去

      

 

 

     到目前為止單用戶遠程登錄已完成!!!

      

比如一台高性能的電腦只能同時給一個人用,那太浪費了;

又比如另一個人要用你賬號登錄時還要問你:親,你在在用XXX電腦嗎;

再比如多個人同時用一個賬號遠程登錄時:哪個親在用,不用的人不出聲回復你,正在用的人可能沒聽到,你就尷尬吧/(ㄒoㄒ)/~~

最後比如有個人正在使用,你一聲不吭登錄了,咔嚓,m蛋那個gou兒子登錄不說…………………………

繼續學習,解決問題>>> 

二、多用戶同時遠程登錄內網機

       多用戶同時遠程登錄內網機需要做的那些事:

  1. 單用戶遠程登錄成功;
  2. 新建一個windows登錄用戶;
  3. 配置windows遠程登錄用戶;
  4. 編輯本地組策略:配置關於遠程登錄的東東;
  5. 解決windows多用戶同時登錄的問題;
  6. 測試遠程登錄。

   

1)單用戶遠程登錄成功

      前提:在你 單用戶遠程登錄成功后再做多用戶同時遠程登錄。

 

2)新建一個windows登錄用戶

    直接搜索:“用戶”,進入“創建標準用戶賬戶”

       

 

 

 

 輸入新用戶信息:ccccc  隨便輸入你喜歡的

 

 

   ps:當你點擊創建時,輸入框數據會被清空,其實已經創建好了,只是win10 沒有自動幫你關窗口,也許是方便創建多個用戶吧,個人覺得體驗感很差。

   雙擊打開:用戶  >>> 可以看到有啦

  

 

  

3)配置windows遠程登錄用戶

    將剛才新建的用戶添加到遠程登錄(上面已經講過了)

   

    添加成功:

   

 

4)編輯本地組策略

     win + R  >>>  輸入 ”gpedit.msc“ 

     

       打開本地組策略

      

 

      進入到遠程登錄配置

     

 

       配置連接數和同時遠程登錄的信息  

     

        PS: windows 雖然允許設置多用戶同時遠程登錄,但不允許你這麼遠程連接……

        配置完這個后的效果:先遠程登錄一個用戶,再遠程登錄另外一個用戶時會提示等待前一個用戶退出。

         可自行驗證,在這裏就不演示了。

        

 

 5)解決windows多用戶同時登錄的問題

     先下載:RDPWrap-v1.6   https://github.com/stascorp/rdpwrap/releases/tag/v1.6.2

   

 

      解壓后:

     

    1. 先再cmd 下 執行“install.bat” 安裝RDPWrap ;安裝成功后,在“C:\Program Files\RDP Wrapper” 目錄下有

     

 

    2. 再嘗試執行下update.bat 。更新不了配置信息,需要手動來配置

      在配置rdpwrap.ini 之前先看下電腦版本:win + r   ,接着輸入:ver (細心的小夥伴會看到,打開cmd其實就已經看到了版本:10.0.18362.53 )

     

 

     接着打開 C:\Program Files\RDP Wrapper\rdpwrap.ini ,在文本末尾添加面的配置信息(不同版本的配置不一樣)     配置之間有空行,最後的空行也不要漏了

[10.0.18362.53]
LocalOnlyPatch.x86=1
LocalOnlyOffset.x86=B7D06
LocalOnlyCode.x86=jmpshort
LocalOnlyPatch.x64=1
LocalOnlyOffset.x64=82FB5
LocalOnlyCode.x64=jmpshort
SingleUserPatch.x86=1
SingleUserOffset.x86=50535
SingleUserCode.x86=nop
SingleUserPatch.x64=1
SingleUserOffset.x64=DBFC
SingleUserCode.x64=Zero
DefPolicyPatch.x86=1
DefPolicyOffset.x86=50269
DefPolicyCode.x86=CDefPolicy_Query_eax_ecx
DefPolicyPatch.x64=1
DefPolicyOffset.x64=1FE15
DefPolicyCode.x64=CDefPolicy_Query_eax_rcx
SLInitHook.x86=1
SLInitOffset.x86=5A77A
SLInitFunc.x86=New_CSLQuery_Initialize
SLInitHook.x64=1
SLInitOffset.x64=22DDC
SLInitFunc.x64=New_CSLQuery_Initialize

[10.0.18362.53-SLInit]
bInitialized.x86      =D577C
bServerSku.x86        =D5780
lMaxUserSessions.x86  =D5784
bAppServerAllowed.x86 =D578C
bRemoteConnAllowed.x86=D5790
bMultimonAllowed.x86  =D5794
ulMaxDebugSessions.x86=D5798
bFUSEnabled.x86       =D579C

bInitialized.x64      =F6A8C
bServerSku.x64        =F6A90
lMaxUserSessions.x64  =F6A94
bAppServerAllowed.x64 =F6A9C
bRemoteConnAllowed.x64=F6AA0
bMultimonAllowed.x64  =F6AA4
ulMaxDebugSessions.x64=F6AA8
bFUSEnabled.x64       =F6AAC

 

    3. 管理員啟動RDPWrap.exe

    

   ps:不配置第2步,或者版本不對的效果

   

 

  4. 執行:RDPCheck.exe 檢測是否破解成功

   

  對於已經全部綠色了,但提示“訪問拒絕”的,應該是用了當前的用戶(相同用戶)登錄了。

 

    PS:啟動RDPConf.exe 前要 重啟 “遠程桌面服務”:Remote Desktop Services 如圖

    

 

6)測試遠程登錄

    快速打開遠程登錄窗口:win+r  >>> 輸入:mstsc   

    多用戶遠程同時登錄內網機測試效果:  

    

  最後附上不同版本的rdpwrap.ini的配置信息(先看裏面有沒有合適的版本,沒有就沒必要下載了):https://gitee.com/RDNGL/rdpwrap

 

後面純屬扯淡,可以不看。

三、拓展學習

  到目前為止,多用戶同時遠程登錄內網機已完成。

       擴展其應用:內網穿透后,外網可以訪問內網,內網的服務可以被互聯網訪問,也即是可以在內網發布web服務,ftp服務等等–內網穿透的應用。

    外網可以遠程連接內網機器,多個人可以同時使用一台電腦。

    拿着一台可以聯網的筆記本便可以擁有巨大的計算資源。

       延伸為雲計算:

    趨勢:現在很多東西都開始“雲”化,這也是未來的發展方向。未來的趨勢:不需要再買固定配置的電腦,升級不僅麻煩,而且還不方便攜帶。未來只需要購買連接器設備,再開個雲計算機服務,隨時隨地升級配置,便可擁有巨大的計算資源。

         “雲”趨勢帶來的影響:硬件配置將面臨企業集中式採購,而零售數量下降。好像扯遠了……

 

 

    到目前為止全部講完了,歡迎來評論區打唾沫戰。

 

學習會讓人視野開闊,站在頂端才能仰望未來。

轉載請指明出處:https://www.cnblogs.com/dennyLee2025/p/13168408.html

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

【Spring】AOP的代理默認是Jdk還是Cglib?,【DP-動態代理】JDK&Cglib

菜瓜:你覺得AOP是啥

水稻:我覺得吧,AOP是對OOP的補充。通常情況下,OOP代碼專註功能的實現,所謂面向切面編程,大多數時候是對某一類對象的方法或者功能進行增強或者抽象

菜瓜:我看你這個理解就挺抽象的

水稻:舉個栗子!我要在滿足開閉原則的基礎下對已有功能進行擴展

  • 我現在想對很多個功能增加日誌功能,但是代碼已經打好包了,不想改。又或者有時候方法調用很慢,想定位問題
  • low一點的方法就是每個方法調用之前記錄調用開始,之後記錄調用結束

菜瓜:你說的這個low一點的方法怎麼好像是在說我???

水稻:建議看一下動態代理設計模式【DP-動態代理】JDK&Cglib,我當然知道你不會看,所以我還準備了自定義註解的栗子

  • package com.hb.merchant.config.aop;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    /**
     * @author QuCheng on 2020/6/23.
     */
    @Configuration
    @EnableAspectJAutoProxy
    public class AopConfig {
    }
    
    package com.hb.merchant.config.aop;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author QuCheng on 2020/6/23.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface OperatorLog {
    }
    
    
    package com.hb.merchant.config.aop;
    
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.stereotype.Component;
    
    /**
     *
     * @author QuCheng on 2020/6/23.
     */
    @Aspect
    @Component
    @Slf4j
    public class OperatorAspect {
    
        @Around("@annotation(OperatorLog)")
        public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
            //獲取要執行的方法
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            //記錄方法執行前日誌
            log.info("startLog: {} 開始了。。。" , methodSignature.getName());
            //獲取方法信息
            String[] argNames = methodSignature.getParameterNames();
            // 參數值:
            final Object[] argValues = joinPoint.getArgs();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < argNames.length; i++) {
                String value = argValues[i] == null ? "null" : argValues[i].toString();
                sb.append(argNames[i]).append("=").append(value).append(",");
            }
            String paramStr = sb.length() > 0 ? sb.toString().substring(0, sb.length() - 1) + "]" : "";
            log.info("參數信息為:[{}", paramStr);
    
            //執行方法
            Object result;
            try {
                result = joinPoint.proceed();
            } catch (Exception e) {
                log.error("errorLog", e);
                return null;
            }
    
            //記錄方法執行後日志
            log.info("endLog: {} 結束了。。。" , methodSignature.getName());
            return result;
        }
    
    }
    
    
    
    package com.hb.merchant.controller.icbc.item.oc;
    
    import com.hb.merchant.config.aop.OperatorLog;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.util.Assert;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * @author QuCheng on 2020-06-23.
     */
    @RestController
    @RequestMapping("/item")
    @Slf4j
    public class ItemOcController {
    
        @OperatorLog
        @GetMapping("/delete")
        public String delete(Long itemId) {
            Assert.notNull(itemId,"itemId不能為空");
            return "delete finished ...";
        }
    }

    // 後台打印
    startLog: delete 開始了。。。
    參數信息為:[itemId=1]
    endLog: delete 結束了。。。

菜瓜:這個自定義註解又是怎麼實現的呢?

水稻:不愧是你,沒有源碼看來是滿足不了你的好奇心了!!不知道你是否還記得我們之前有聊到過bean創建完畢後會調用一些PostProcessor對其進一步操作

菜瓜:有印象,@PostConstruct註解就是InitDestroyAnnotationBeanPostProcessor在這裏調用的,還自定義過BeanPostProcessorT對象打印輸出過bean信息

水稻:你猜Spring是怎麼操作的

菜瓜:let me try try。結合剛剛的栗子和提示,大膽猜測應該是用PostProcessor在bean創建完成之後生成代理對象。實際調用代理的invoke方法實現對被代理bean的增強

水稻:思路正確。看脈絡

  • 入口在AbstractAdvisorAutoProxyCreator#initializeBean
  • protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
            。。。
                // BeanNameAware BeanFactoryAware ...
                invokeAwareMethods(beanName, bean);
        。。。    
                // BeanPostProcessorBefore  @PostConstruct
                wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
        。。。
                // initMethod InitializingBean接口
                invokeInitMethods(beanName, wrappedBean, mbd);
                。。。
            if (mbd == null || !mbd.isSynthetic()) {
                // aop
                wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
            }
            return wrappedBean;
        }
  • 從aop入口跟下去
  • protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
            。。。
            // 收集切面信息匹配被代理對象
            Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
            if (specificInterceptors != DO_NOT_PROXY) {
                this.advisedBeans.put(cacheKey, Boolean.TRUE);
          // 如果符合切面 創建代理,被代理對象被代理引用
                Object proxy = createProxy(
                        bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
                this.proxyTypes.put(cacheKey, proxy.getClass());
                return proxy;
            }
    
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }
  • 跟createProxy方法 -> DefaultAopProxyFactory#createAopProxy
  • @Override
    public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
       if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
          Class<?> targetClass = config.getTargetClass();
          if (targetClass == null) {
             throw new AopConfigException("TargetSource cannot determine target class: " +
                   "Either an interface or a target is required for proxy creation.");
          }
          if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            // jdk動態代理類
             return new JdkDynamicAopProxy(config);
          }
          // cglib
          return new ObjenesisCglibAopProxy(config);
       }
       else {
          return new JdkDynamicAopProxy(config);
       }
    }
  • 此處省略了切面類搜集和匹配的過程。可以簡單理解成搜集到所有的切面類信息獲取pointcut的目錄或者註解信息,匹配當前bean是否屬於pointcut目標範圍
  • 另外我們可以看到最後返回的bean已經不是原始bean了,而是代理對象。也就是說getBean(“xxx”)返回的對象實際是代理對象,被代理對象被其成員變量直接引用

菜瓜:然後代理類中都有invoke方法,那些advice(@Around,@Before…)在invoke中找到適當時機調用對吧

水稻:是的,這裏我想結合@Transactional註解會更容易理解,你肯定用過這個註解吧,它其實。。。

菜瓜:停。。。今天獲取的知識量已經夠了,我下去自己斷點走一趟再熟悉熟悉。下次請結合Transactional註解再敲打我吧

水稻:也好,我下去再給你準備幾個栗子

 

總結:

  • AOP提供了在不侵入代碼的前提下動態增強目標對象的途徑,讓OOP更加專註於實現自己的邏輯
  • 而Spring的實現還是老套路,利用PostProcessor在類初始化完成之後替需要的bean創建代理對象
  • 這裏還有一些細節沒有照顧到,譬如說AOP解析類是什麼時候註冊到IOC容器的(偷偷告訴你從@EnableAspectJAutoProxy註解下手)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

現代化編程模式(1)-快

感謝大家購買我的書

首先十分感謝大家購買我的書,在Tommy和Julisa購買我的書的時候,我承諾會開Sharing給大家。我開Sharing的目標就是讓像Tommy和Julisa這樣的基礎比較低的人都能夠學會編程。所以我就以Tommy和Julisa為目標制定sharing課程,不再按照書籍章節順序一章一章的講。

現代化編程模式核心 – 快

我這本書最值得學習的地方不是.NET,所以這節sharing我故意不用.NET來演示,而是採用JavaScript來演示。

我這本書最值得學習的地方也不是併發。

我這本書最值得學習的地方是“現代化的編程模式”。也就是如下標黃處

 

 

 

現代化編程模式核心只有一個字,就是 – 快!

為了显示出快,我故意挑選了最慢的JavaScript來做這次sharing的演示語言!

現代化編程模式適用於所有現代化的編程語言,包括.NET/(C#和F#),Java,Python,JavaScript,Scala,Clojure,Swift。所以無論是我書中的C#和F#,還是我這次sharing用的JavaScript,都是通用的。最典型的就是本書第6章所講的Rx,它就有多個編程語言的版本:Java,JavaScript,.NET,Scala,Clojure,Swift。以下截圖取自Rx的官網 http://reactivex.io/

 

 

 

快主要來自兩個方面:

  • 程序運行速度快。
  • 編程速度快。

程序運行速度快

從大家的反饋來看,大家不太關注這點,所以我這次sharing先不講,以後再講。

編程速度快

所有技術的進步趨勢都是越來越容易,門檻越來越低,編程也同樣如此!

現在我回首十九年前(2001年)我用C語言寫出第一個程序的時候,當時的編程門檻是相當的高。當時的Turbo C 2.0還是跑在Dos上的,與今天相比真是天壤之別。編程門檻越來越低,編程工具環境越來越完善。這些基礎設施的完善決定了我們編程速度會越來越快。所以大家不需要再對編程帶有恐懼感,覺得編程難!現在編程的難度已經是你讀書時代的幾十分之一了!如果你掌握的是現代化的編程模式而不再是傳統古老的編程模式的話。

在這次sharing我只講大家最容易理解的幾點:

  • 調試診斷快
  • 不用動腦跟着感覺走導致行動快
  • 不用等待他人協作導致自身行動快

調試診斷快

在傳統的編程模式中,我們是這樣編程的:

  1. 啪啪啪敲一堆鍵盤之後,程序寫完了。
  2. 跑起來試一下,oh! No! 出錯了!(耗時幾分到十幾分鐘)
  3. 這時候就加斷點調試,先進一點的就不加斷點,而是看log來診斷。(至少耗時一分鐘起)

大家可以看到,第二步和第三步耗時都是以分鐘為單位的。如果unlucky,第三步可能要按小時甚至天為單位。為了加深大家的印象,sharing的時候我會視時間多少而現場演示一遍這個痛苦的傳統編程模式。

但是現代化的編程模式是以秒為單位的,比以分為單位的傳統編程模式快了一個數量級!!!

Sharing的時候我以JavaScript為編程語言,Visual Studio Code為編程工具,框架選karma + jasmine + AngularJS(我故意沒選最近的Angular而是選AngularJS,也是為了證明現代化編程模式是通用的)來演示現代化編程模式是如何以秒為單位進行調試診斷的。

現代化編程模式以秒為單位進行調試診斷的要點在於:

  1. 每Save一次就會把所有BDD Test cases在一兩秒內跑完!
  2. 基於上一點,也就是說,與傳統的編程模式相比,我馬上就能知道結果,而不是要等幾分鐘。
  3. 基於上一點,就可以每改一點代碼,就馬上Save,同時就馬上知道結果。如果此時發現BDD Test Case變紅了,馬上就知道剛才改的哪一處代碼有錯誤。馬上就能知道原因,馬上就能夠Fix!這裏的每改一點代碼可以理解成:每改一行代碼,每改一個字符,等等。
  4. 因為每次修改代碼的粒度是如此之小,小到每一行和每一個字符,可謂:一步一個腳印。並且每次改動都能保證程序是能BDD Test Case跑通過的,自然也就不需要加斷點調試和看log來診斷了。
  5. 然而人總是會犯錯的,程序出錯時,現代化編程模式不需要通過加斷點調試和看log診斷這種這麼古老的方式,而是通過不斷的加BDD Test Case和縮小Scope來定位問題發生的地方以及去解決。

這部分實操內容也是這次Sharing的大頭。

不用動腦跟着感覺走導致行動快

傳統的編程模式需要程序員去動腦,去思考程序代碼的流程,所以需要寫if/else/switch/for/foreach等語句。然而動腦是很容易累的,同時又是很耗時間的,所以傳統的編程模式自然是快不起來的!

“動腦是不可能動腦的啦,做生意又不會,只能跟着感覺走寫寫代碼才能生活這樣子”

竊-格瓦拉

現代化編程模式改變了思路,不再去思考程序代碼的流程:

 

傳統的編程模式

現代化編程模式

程序員所要做的

需要動腦思考如何做,需要明確的指出每一步該怎麼做。

只需要告訴計算機你的目標即可!

相關編程語句和庫

if/else/switch/for/foreach

LINQ,Reactive Extensions, Promises (不應該再出現if/else/switch/for/foreach)

專業術語

命令式編程

聲明式編程

 

 

 

補充多一句:聲明式編程語言通常用作解決人工智能和約束滿足問題。哈哈,看到這裏大家都懂的啦。

從上面的表格對比中可以看出,“只需要告訴計算機你的目標”的現代化編程模式明顯就比“還需要明確的指出每一步該怎麼做。”的傳統編程模式節省腦子很多!!!

大家可以看看 https://baike.baidu.com/item/%E5%A3%B0%E6%98%8E%E5%BC%8F%E7%BC%96%E7%A8%8B/9939512 來預習一下

 

 

 

不用等待他人協作導致自身行動快

自從進入移動互聯網時代之後,因為客戶端設備的多樣化,前後端分離已經成了大趨勢。同時也帶來了一個大問題:前後端程序員協作的問題!

接着前面“調試診斷快”一節中所share的內容,傳統的編程模式對前後端程序員聯調的依賴性比較高。而現代化編程模式因為可以通過mock的方式來模擬絕大部分後端的響應,前端程序員不再需要等待後端程序員的工作了,從而節約了這部分時間,導致自身在項目進度中更快更有保障。

在這次sharing中我所使用的Karma(BDD框架)和Jasmine(包含了httpbackend mock庫的unit testing框架)在其他編程語言中都有對應的框架和庫,再次體現了現代化編程模式是在各種編程語言中是通用的!!!

總結和動手口訣

在這次sharing之前,我請了兩位老夥計参觀了一下,都得到了好評,希望參加的有經驗的程序員也同樣會一聲驚嘆!

當然,非程序員還是不可能只通過這一次sharing就馬上學會編程的,所以對於你們來說,這次sharing的目的應該是:你能夠識別和分辨出哪些才是編程里正確的道路,從而找到先進的編程資料和避免誤讀落後的編程資料。關於這點,我總結了口訣如下:

  1. 快是王道!
    凡是要一分鐘以上才能看到編程運行結果的都是落後的編程模式,現代化模式是一兩秒就能看到運行結果的。凡是要通過debug和看log這麼耗時的行為,我們都要思考是否通過BDD來減少時間。
  2. 節省腦力是王道!
    現代化編程模式已經可以實現了只需要告訴計算機你的目標即可!如果還需要費腦子去想if/else/switch/for/foreach。那你就落後了。
  3. 能夠獨立完成任務,不依賴別人是王道!
    Mock+BDD已經可以讓你不需要過多等待和依賴其他程序員的工作了。

大家下周sharing見!

風險提示:

我的blog文章和我所翻譯和所寫的書籍不一樣:

    • 沒有像書籍一樣經過三審三校。所以不像書籍一樣嚴謹和全面。
    • 都有當時特定的閱讀對象,然而每篇blog的閱讀對象都不一樣,這點和書籍十分不一樣。所以如果你讀起來覺得怪怪的,那很有可能是你與該篇blog閱讀對象差異很大。
    • 所有文章全部不構成任何投資建議。如因採納這些文章而進行投資所造成的虧損,我不負任何責任。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

【原創】Linux中斷子系統(四)-Workqueue

背景

  • Read the fucking source code! –By 魯迅
  • A picture is worth a thousand words. –By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

  • Workqueue工作隊列是利用內核線程來異步執行工作任務的通用機制;
  • Workqueue工作隊列可以用作中斷處理的Bottom-half機制,利用進程上下文來執行中斷處理中耗時的任務,因此它允許睡眠,而Softirq和Tasklet在處理任務時不能睡眠;

來一張概述圖:

  • 在中斷處理過程中,或者其他子系統中,調用workqueue的調度或入隊接口后,通過建立好的鏈接關係圖逐級找到合適的worker,最終完成工作任務的執行;

2. 數據結構

2.1 總覽

此處應有圖:

  • 先看看關鍵的數據結構:
    1. work_struct:工作隊列調度的最小單位,work item;
    2. workqueue_struct:工作隊列,work item都掛入到工作隊列中;
    3. worker:work item的處理者,每個worker對應一個內核線程;
    4. worker_pool:worker池(內核線程池),是一個共享資源池,提供不同的worker來對work item進行處理;
    5. pool_workqueue:充當橋樑紐帶的作用,用於連接workqueue和worker_pool,建立鏈接關係;

下邊看看細節吧:

2.2 work

struct work_struct用來描述work,初始化一個work並添加到工作隊列后,將會將其傳遞到合適的內核線程來進行處理,它是用於調度的最小單位。

關鍵字段描述如下:

struct work_struct {
	atomic_long_t data;     //低比特存放狀態位,高比特存放worker_pool的ID或者pool_workqueue的指針
	struct list_head entry; //用於添加到其他隊列上
	work_func_t func;       //工作任務的處理函數,在內核線程中回調
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

圖片說明下data字段:

2.3 workqueue

  • 內核中工作隊列分為兩種:

    1. bound:綁定處理器的工作隊列,每個worker創建的內核線程綁定到特定的CPU上運行;
    2. unbound:不綁定處理器的工作隊列,創建的時候需要指定WQ_UNBOUND標誌,內核線程可以在處理器間遷移;
  • 內核默認創建了一些工作隊列(用戶也可以創建):

    1. system_mq:如果work item執行時間較短,使用本隊列,調用schedule[_delayed]_work[_on]()接口就是添加到本隊列中;
    2. system_highpri_mq:高優先級工作隊列,以nice值-20來運行;
    3. system_long_wq:如果work item執行時間較長,使用本隊列;
    4. system_unbound_wq:該工作隊列的內核線程不綁定到特定的處理器上;
    5. system_freezable_wq:該工作隊列用於在Suspend時可凍結的work item;
    6. system_power_efficient_wq:該工作隊列用於節能目的而選擇犧牲性能的work item;
    7. system_freezable_power_efficient_wq:該工作隊列用於節能或Suspend時可凍結目的的work item;

struct workqueue_struct關鍵字段介紹如下:

struct workqueue_struct {
	struct list_head	pwqs;		/* WR: all pwqs of this wq */   //所有的pool_workqueue都添加到本鏈表中
	struct list_head	list;		/* PR: list of all workqueues */    //用於將工作隊列添加到全局鏈表workqueues中

	struct list_head	maydays;	/* MD: pwqs requesting rescue */    //rescue狀態下的pool_workqueue添加到本鏈表中
	struct worker		*rescuer;	/* I: rescue worker */  //rescuer內核線程,用於處理內存緊張時創建工作線程失敗的情況

	struct pool_workqueue	*dfl_pwq;	/* PW: only for unbound wqs */

	char			name[WQ_NAME_LEN]; /* I: workqueue name */

	/* hot fields used during command issue, aligned to cacheline */
	unsigned int		flags ____cacheline_aligned; /* WQ: WQ_* flags */
	struct pool_workqueue __percpu *cpu_pwqs; /* I: per-cpu pwqs */     //Per-CPU都創建pool_workqueue
	struct pool_workqueue __rcu *numa_pwq_tbl[]; /* PWR: unbound pwqs indexed by node */    //Per-Node創建pool_workqueue
    ...
};

2.4 worker

  • 每個worker對應一個內核線程,用於對work item的處理;
  • worker根據工作狀態,可以添加到worker_pool的空閑鏈表或忙碌列表中;
  • worker處於空閑狀態時並接收到工作處理請求,將喚醒內核線程來處理;
  • 內核線程是在每個worker_pool中由一個初始的空閑工作線程創建的,並根據需要動態創建和銷毀;

關鍵字段描述如下:

struct worker {
	/* on idle list while idle, on busy hash table while busy */
	union {
		struct list_head	entry;	/* L: while idle */     //用於添加到worker_pool的空閑鏈表中
		struct hlist_node	hentry;	/* L: while busy */ //用於添加到worker_pool的忙碌列表中
	};

	struct work_struct	*current_work;	/* L: work being processed */   //當前正在處理的work
	work_func_t		current_func;	/* L: current_work's fn */                  //當前正在執行的work回調函數
	struct pool_workqueue	*current_pwq; /* L: current_work's pwq */   //指向當前work所屬的pool_workqueue

	struct list_head	scheduled;	/* L: scheduled works */    //所有被調度執行的work都將添加到該鏈表中

	/* 64 bytes boundary on 64bit, 32 on 32bit */

	struct task_struct	*task;		/* I: worker task */    //指向內核線程
	struct worker_pool	*pool;		/* I: the associated pool */    //該worker所屬的worker_pool
						/* L: for rescuers */
	struct list_head	node;		/* A: anchored at pool->workers */  //添加到worker_pool->workers鏈表中
						/* A: runs through worker->node */
    ...
};

2.5 worker_pool

  • worker_pool是一個資源池,管理多個worker,也就是管理多個內核線程;
  • 針對綁定類型的工作隊列,worker_pool是Per-CPU創建,每個CPU都有兩個worker_pool,對應不同的優先級,nice值分別為0和-20;
  • 針對非綁定類型的工作隊列,worker_pool創建後會添加到unbound_pool_hash哈希表中;
  • worker_pool管理一個空閑鏈表和一個忙碌列表,其中忙碌列表由哈希管理;

關鍵字段描述如下:

struct worker_pool {
	spinlock_t		lock;		/* the pool lock */
	int			cpu;		/* I: the associated cpu */     //綁定到CPU的workqueue,代表CPU ID
	int			node;		/* I: the associated node ID */ //非綁定類型的workqueue,代表內存Node ID
	int			id;		/* I: pool ID */
	unsigned int		flags;		/* X: flags */

	unsigned long		watchdog_ts;	/* L: watchdog timestamp */

	struct list_head	worklist;	/* L: list of pending works */  //pending狀態的work添加到本鏈表
	int			nr_workers;	/* L: total number of workers */    //worker的數量

	/* nr_idle includes the ones off idle_list for rebinding */
	int			nr_idle;	/* L: currently idle ones */

	struct list_head	idle_list;	/* X: list of idle workers */   //處於IDLE狀態的worker添加到本鏈表
	struct timer_list	idle_timer;	/* L: worker idle timeout */
	struct timer_list	mayday_timer;	/* L: SOS timer for workers */

	/* a workers is either on busy_hash or idle_list, or the manager */
	DECLARE_HASHTABLE(busy_hash, BUSY_WORKER_HASH_ORDER);   //工作狀態的worker添加到本哈希表中
						/* L: hash of busy workers */

	/* see manage_workers() for details on the two manager mutexes */
	struct worker		*manager;	/* L: purely informational */
	struct mutex		attach_mutex;	/* attach/detach exclusion */
	struct list_head	workers;	/* A: attached workers */   //worker_pool管理的worker添加到本鏈表中
	struct completion	*detach_completion; /* all workers detached */

	struct ida		worker_ida;	/* worker IDs for task name */

	struct workqueue_attrs	*attrs;		/* I: worker attributes */
	struct hlist_node	hash_node;	/* PL: unbound_pool_hash node */    //用於添加到unbound_pool_hash中
    ...
} ____cacheline_aligned_in_smp;

2.6 pool_workqueue

  • pool_workqueue充當紐帶的作用,用於將workqueue和worker_pool關聯起來;

關鍵字段描述如下:

struct pool_workqueue {
	struct worker_pool	*pool;		/* I: the associated pool */    //指向worker_pool
	struct workqueue_struct *wq;		/* I: the owning workqueue */   //指向所屬的workqueue

	int			nr_active;	/* L: nr of active works */     //活躍的work數量
	int			max_active;	/* L: max active works */   //活躍的最大work數量
	struct list_head	delayed_works;	/* L: delayed works */      //延遲執行的work掛入本鏈表
	struct list_head	pwqs_node;	/* WR: node on wq->pwqs */      //用於添加到workqueue鏈表中
	struct list_head	mayday_node;	/* MD: node on wq->maydays */   //用於添加到workqueue鏈表中
    ...
} __aligned(1 << WORK_STRUCT_FLAG_BITS);

2.7 小結

再來張圖,首尾呼應一下:

3. 流程分析

3.1 workqueue子系統初始化

  • workqueue子系統的初始化分成兩步來完成的:workqueue_init_early和workqueue_init。

3.1.1 workqueue_init_early

  • workqueue子系統早期初始化函數完成的主要工作包括:
    1. 創建pool_workqueue的SLAB緩存,用於動態分配struct pool_workqueue結構;
    2. 為每個CPU都分配兩個worker_pool,其中的nice值分別為0和HIGHPRI_NICE_LEVEL,並且為每個worker_pool從worker_pool_idr中分配一個ID號;
    3. 為unbound工作隊列創建默認屬性,struct workqueue_attrs屬性,主要描述內核線程的nice值,以及cpumask值,分別針對優先級以及允許在哪些CPU上執行;
    4. 為系統默認創建幾個工作隊列,這幾個工作隊列的描述在上文的數據結構部分提及過,不再贅述;

從圖中可以看出創建工作隊列的接口為:alloc_workqueue,如下圖:

  • alloc_workqueue完成的主要工作包括:
    1. 首先當然是要分配一個struct workqueue_struct的數據結構,並且對該結構中的字段進行初始化操作;
    2. 前文提到過workqueue最終需要和worker_pool關聯起來,而這個紐帶就是pool_workqueue,alloc_and_link_pwqs函數就是完成這個功能:1)如果工作隊列是綁定到CPU上的,則為每個CPU都分配pool_workqueue並且初始化,通過link_pwq將工作隊列與pool_workqueue建立連接;2)如果工作隊列不綁定到CPU上,則按內存節點(NUMA,參考之前內存管理的文章)來分配pool_workqueue,調用get_unbound_pool來實現,它會根據wq屬性先去查找,如果沒有找到相同的就創建一個新的pool_workqueue,並且添加到unbound_pool_hash哈希表中,最後也會調用link_pwq來建立連接;
    3. 創建工作隊列時,如果設置了WQ_MEM_RECLAIM標誌,則會新建rescuer worker,對應rescuer_thread內核線程。當內存緊張時,新創建worker可能會失敗,這時候由rescuer來處理這種情況;
    4. 最終將新建好的工作隊列添加到全局鏈表workqueues中;

3.1.2 workqueue_init

workqueue子系統第二階段的初始化:

  • 主要完成的工作是給之前創建好的worker_pool,添加一個初始的worker;
  • create_worker函數中,創建的內核線程名字為kworker/XX:YY或者kworker/uXX:YY,其中XX表示worker_pool的編號,YY表示worker的編號,u表示unbound;

workqueue子系統初始化完成后,基本就已經將數據結構的關聯建立好了,當有work來進行調度的時候,就可以進行處理了。

3.2 work調度

3.2.1 schedule_work

以schedule_work接口為例進行分析:

  • schedule_work默認是將work添加到系統的system_work工作隊列中;

  • queue_work_on接口中的操作判斷要添加work的標誌位,如果已經置位了WORK_STRUCT_PENDING_BIT,表明已經添加到了隊列中等待執行了,否則,需要調用__queue_work來進行添加。注意了,這個操作是在關中斷的情況下進行的,因為工作隊列使用WORK_STRUCT_PENDING_BIT位來同步work的插入和刪除操作,設置了這個比特后,然後才能執行work,這個過程可能被中斷或搶佔打斷;

  • workqueue的標誌位設置了__WQ_DRAINING,表明工作隊列正在銷毀,所有的work都要處理完,此時不允許再將work添加到隊列中,有一種特殊情況:銷毀過程中,執行work時又觸發了新的work,也就是所謂的chained work;

  • 判斷workqueue的類型,如果是bound類型,根據CPU來獲取pool_workqueue,如果是unbound類型,通過node號來獲取pool_workqueue;

  • get_work_pool獲取上一次執行work的worker_pool,如果本次執行的worker_pool與上次執行的worker_pool不一致,且通過find_worker_executing_work判斷work正在某個worker_pool中的worker中執行,考慮到緩存熱度,放到該worker執行是更合理的選擇,進而根據該worker獲取到pool_workqueue;

  • 判斷pool_workqueue活躍的work數量,少於最大限值則將work加入到pool->worklist中,否則加入到pwq->delayed_works鏈表中,如果__need_more_worker判斷沒有worker在執行,則喚醒worker內核線程執行;

  • 總結:

    1. schedule_work完成的工作是將work添加到對應的鏈表中,而在添加的過程中,首先是需要確定pool_workqueue;
    2. pool_workqueue對應一個worker_pool,因此確定了pool_workqueue也就確定了worker_pool,進而可以將work添加到工作鏈表中;
    3. pool_workqueue的確定分為三種情況:1)bound類型的工作隊列,直接根據CPU號獲取;2)unbound類型的工作隊列,根據node號獲取,針對unbound類型工作隊列,pool_workqueue的釋放是異步執行的,需要判斷refcnt的計數值,因此在獲取pool_workqueue時可能要多次retry;3)根據緩存熱度,優先選擇正在被執行的worker_pool;

3.2.2 worker_thread

work添加到工作隊列后,最終的執行在worker_thread函數中:

  • 在創建worker時,創建內核線程,執行函數為worker_thread;

  • worker_thread在開始執行時,設置標誌位PF_WQ_WORKER,調度器在進行調度處理時會對task進行判斷,針對workerqueue worker有特殊處理;

  • worker對應的內核線程,在沒有處理work的時候是睡眠狀態,當被喚醒的時候,跳轉到woke_up開始執行;

  • woke_up之後,如果此時worker是需要銷毀的,那就進行清理工作並返回。否則,離開IDLE狀態,並進入recheck模塊執行;

  • recheck部分,首先判斷是否需要更多的worker來處理,如果沒有任務處理,跳轉到sleep地方進行睡眠。有任務需要處理時,會判斷是否有空閑內核線程以及是否需要動態創建,再清除掉worker的標誌位,然後遍歷工作鏈表,對鏈表中的每個節點調用process_one_worker來處理;

  • sleep部分比較好理解,沒有任務處理時,worker進入空閑狀態,並將當前的內核線程設置成睡眠狀態,讓出CPU;

  • 總結:

    1. 管理worker_pool的內核線程池時,如果有PENDING狀態的work,並且發現沒有正在運行的工作線程(worker_pool->nr_running == 0),喚醒空閑狀態的內核線程,或者動態創建內核線程;
    2. 如果work已經在同一個worker_pool的其他worker中執行,不再對該work進行處理;

work的執行函數為process_one_worker:

  • work可能在同一個CPU上不同的worker中運行,直接退出;
  • 調用worker->current_func(),完成最終work的回調函數執行;

3.3 worker動態管理

3.3.1 worker狀態機變換

  • worker_pool通過nr_running字段來在不同的狀態機之間進行切換;
  • worker_pool中有work需要處理時,需要至少保證有一個運行狀態的worker,當nr_running大於1時,將多餘的worker進入IDLE狀態,沒有work需要處理時,所有的worker都會進入IDLE狀態;
  • 執行work時,如果回調函數阻塞運行,那麼會讓worker進入睡眠狀態,此時調度器會進行判斷是否需要喚醒另一個worker;
  • IDLE狀態的worker都存放在idle_list鏈表中,如果空閑時間超過了300秒,則會將其進行銷毀;
  1. Running->Suspend
  • 當worker進入睡眠狀態時,如果該worker_pool沒有其他的worker處於運行狀態,那麼是需要喚醒一個空閑的worker來維持併發處理的能力;
  1. Suspend->Running
  • 睡眠狀態可以通過wake_up_worker來進行喚醒處理,最終判斷如果該worker不在運行狀態,則增加worker_pool的nr_running值;

3.3.2 worker的動態添加和刪除

  1. 動態刪除
  • worker_pool初始化時,註冊了timer的回調函數,用於定時對空閑鏈表上的worker進行處理,如果worker太多,且空閑時間太長,超過了5分鐘,那麼就直接進行銷毀處理了;
  1. 動態添加
  • 內核線程執行worker_thread函數時,如果沒有空閑的worker,會調用manage_workers接口來創建更多的worker來處理工作;

參考

Documentation/core-api/workqueue.rst
http://kernel.meizu.com/linux-workqueue.html

洗洗睡了,收工!

歡迎關注公眾號,不定期分享Linux內核機制文章

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

比亞迪拉丁美洲第一座電動車工廠誕生 投27.1億定位巴西

比亞迪股份有限公司將投資2億巴西雷亞爾(約合新臺幣27.1億元)在巴西投資建廠。這家工廠將落戶聖保羅州坎皮納市(Campinas),是比亞迪在拉丁美洲的第一座工廠。   比亞迪巴西工廠的產品是電動公車、電池和太陽能板。今年1月,比亞迪曾表示其年電動公車最大產能將達到4000輛。其生產的電動公車可能在巴西和拉美地區銷售。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

一起玩轉微服務(12)——揭密starter

介紹

Spring Boot的starter主要用來簡化依賴用的,對於企業級開發中的與第三方的集成,可以通過一段簡單的配置來完成,這樣開發人員無需再對包依賴的問題頭疼。Spring Boot為我們提供了簡化企業級開發的絕大多數場景的starter pom,只需要指定需要配置的starter,Spring Boot會自動為我們提供配置好的bean。

通常流程

通常我們要搭建一個基於Spring的Web應用,我們需要做以下一些工作:

pom文件中引入相關jar包,包括spring、springmvc、redis、mybaits、log4j、mysql-connector-java 等等相關jar …

配置web.xml,Listener配置、Filter配置、Servlet配置、log4j配置、error配置 …

配置數據庫連接、配置spring事務

配置視圖解析器

開啟註解、自動掃描功能

配置完成後部署tomcat、啟動調試

……

花在搭建一個初始項目,可能一個小時就過去了或者半天就過了,但是用了SpringBoot之後一切都會變得非常便捷。

常用的starter

Spring Boot常用的starter(啟動器)包括:

  • Spring-boot-starter-logging :使用 Spring Boot 默認的日誌框架 Logback。
  • Spring-boot-starter-log4j :添加 Log4j 的支持。
  • Spring-boot-starter-web :支持 Web 應用開發,包含 Tomcat 和 Spring-mvc。
  • Spring-boot-starter-tomcat :使用 Spring Boot 默認的 Tomcat 作為應用服務器。
  • Spring-boot-starter-jetty :使用 Jetty 而不是默認的 Tomcat 作為應用服務器。
  • Spring-boot-starter-test :包含常用的測試所需的依賴,如 Junit、Hamcrest、Mockito 和 Spring-test 等。
  • Spring-boot-starter-AOP :包含 Spring-AOP 和 AspectJ 來支持面向切面編程(AOP)。
  • Spring-boot-starter-security :包含 Spring-security。
  • Spring-boot-starter-jdbc :支持使用 JDBC 訪問數據庫。
  • Spring-boot-starter-redis :支持使用 Redis。
  • Spring-boot-starter-data-mongodb :包含 Spring-data-mongodb 來支持 MongoDB。
  • Spring-boot-starter-data-jpa :包含 Spring-data-jpa、Spring-orm 和 Hibernate 來支持 JPA。
  • Spring-boot-starter-amqp :通過 Spring-rabbit 支持 AMQP。
  • Spring-boot-starter-actuator : 添加適用於生產環境的功能,如性能指標和監測等功能。

當然,如果有必要,也可以定製自己的starter。

起步依賴

在我們的pom文件裏面引入以下jar:

spring-boot-starter-web包自動幫我們引入了web模塊開發需要的相關jar包。

 

mybatis-spring-boot-starter幫我們引入了dao開發相關的jar包。 spring-boot-starter-xxx是官方提供的starter,xxx-spring-boot-starter是第三方提供的starter。

截圖看一下我們的mybatis-spring-boot-starter

可以看出mybatis-spring-boot-starter並沒有任何源碼,只有一個pom文件,它的作用就是幫我們引入其它jar。

 

得益於starter的作用,使用SpringBoot確實方便,但對剛剛上手SpringBoot的人來說,可能只知道配置屬性是在application.xml或application.yml中添加,但他們各自的屬性都有哪些,具體怎麼配置,卻無從下手。這裏先解決SpringBoot-starter中各屬性的配置問題。

Mybatis的配置是怎麼生效的?查看示例工程的pom依賴:

注意到mybatis-spring-boot-starter幫我們自動依賴了Mybatis所需jar包,其中有一個負責自動配置的mybatis-spring-boot-autoconfigure.jar,緊接着打開此jar,如下:

META-INF/spring-configuration-metadata.json中便是Mybatis在SpringBoot中的所有配置屬性和介紹。

SpringBoot-starter自動配置bean

現在已得知jar包是怎麼樣自動依賴進來,以及他們的配置屬性,那麼接下來該考慮Mybatis所需的bean(如必需的sqlSessionFactory、sqlSessionTemplate等)是如何被自動加載的?

理所應當地,我們繼續去查看mybatis-spring-boot-autoconfigure.jar,注意到裏面有一個自動配置的類MybatisAutoConfiguration:

(1)@Configuration:被掛上@Configuration註解,表明它是一個配置類,作用等同於xml配置,裏面有被@Bean註解的方法,也等同於xml配置的各種。

(2)@ConditionalOnClass/@ConditionalOnBean:自動配置條件註解,用於在某一部分配置中,將另一模塊部分的配置自動加載進來,因為隨着系統越來越大,配置內容越來越多,我們應當將Mybatis的配置放在一處,將log4j的配置放在一處,將SpringBoot自身的配置放在一處,當他們需要互相依賴時,可通過這類註解進行自動配置,如下:

@ConditionalOnClass @ConditionalOnMissingClass
@ConditionalOnBean @ConditionalOnMissingBean
@ConditionalOnProperty
@ConditionalOnResource
@ConditionalOnWebApplication @ConditionalOnNotWebApplication
@ConditionalOnExpression
 
@AutoConfigureAfter @AutoConfigureBefore @AutoConfigureOrder(指定順序)
 

(3)@EnableConfigurationProperties:啟用對@ConfigurationProperties註解的bean的支持,這裏對應了配置屬性類MybatisProperties,它裏面定義了Mybatis的所有配置。

(4)@AutoConfigureAfter:應在其他指定的自動配置類之後應用自動配置。即org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration被自動配置后,才會接着自動配置MybatisAutoConfiguration。這裏也解釋了為什麼我們在application.xml中只配置了數據源,而沒有配置Mybatis,但是Mybatis可以正常查庫的原因,就是因為它們配置之間的依賴關係。

到這裏,差不多明白了starter自動配置bean的方式,但是如若再去深究,各種starter的bean是如何被自動加載的,猜想會不會是項目啟動后,SpringBoot自動掃描裏面所有的jar包,再去掃描所有的類,從而將各個bean放置IOC容器中。從結果來看,肯定是SpringBoot在啟動時確確實實地自動加載了數據源和Mybatis相關的bean,不然他們無法正常工作。

回想在我們啟動示例工程時,SpringBoot會自動掃描啟動類所在包下的所有類,而如果還去掃描所有的jar包的話,又是具體怎麼做到的?不妨從入口類調試一把,在SpringApplication.run(DemoApplication.class, args)打斷點,一直追蹤到getSpringFactoriesInstances這塊:

查看SpringFactoriesLoader.loadFactoryNames的方法註釋:

 

使用給定的類加載器從META-INF / spring.factories加載給定類型的工廠實現的完全限定類名。

這裏的spring.factories剛好也存在於mybatis-spring-boot-autoconfigure.jar中,

繼續調試,進入SpringFactoriesLoader.loadFactoryNames,

這裏用類加載器得到工程中所有jar包中的META-INF/spring.factories文件資源,進而通過此文件得到了一些包括自動配置相關的類的集合,有各種工廠類、監聽器、處理器、過濾器、初始化器等等,如下:

最後的org.springframework.boot.autoconfigure.EnableAutoConfiguration集合中當然包括了org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration和org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration。接着必然是將實例化的各個bean放進IOC容器中。

至此我們便明白了SpringBoot是如何自動配置starter裏面的bean的。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

那些年,我們用過的服務器軟件

引言

看過這部電影的人都老了。。。

很多人都知道我是一名後端開發底層小碼農,平時打交道最多的就是服務器,而關於服務器,又有一堆名詞,看起來好像是一個意思,仔細想想又好像不對。

不信?

先放三個名詞「Web 服務器」、「HTTP 服務器」、「應用服務器」。這三種服務器有誰能現在立刻馬上區分開么。

反正我是區分不開。這個問題要是扔到一堆程序員中間,指不定還能引發一場菜雞爭奪戰。

雖然我不會,不會可以度娘啊,希望經常看我文章同學可以有我這種敢於承認自己菜的精神,沒啥好丟人的,不會可以學,學完了下次就會了,總比不會裝會死鴨子嘴硬,下次還不會要強。

經過我一翻度娘 + 整理后,基本上這三個名詞解釋有了:

「Web 服務器」它一般指的是網站服務器,可以向瀏覽器( PC 端或者移動端)等 Web 客戶端提供服務,供請求數據或者下載數據。

而由於 Web 服務器主要支持的協議就是 HTTP 或者 HTTPS ,所以通常情況下 Web 服務器和 HTTP 服務器是等同的,這兩種服務器之間是可以畫上等號的。

而應用服務器是一個很大的概念,微軟對它的定義是「我們把應用程序服務器定義為:作為服務器執行共享業務應用程序的底層的系統軟件。 就像文件服務器為很多用戶提供文件一樣,應用程序服務器讓多個用戶可以同時使用應用程序(通常是客戶創建的應用程序)」。

講的通俗一點就是一種特定應用的承載容器,一般來講,它需要有運行時環境的支持,比如說在 Java 領域,比較常用的應用服務器 Tomcat ,它就必須要 Java 的環境支持。

站在我的角度上,一名 Java 底層碼農從業人員的角度上來看,「Web 服務器」、「HTTP 服務器」、「應用服務器」之間的界限是非常模糊的,因為同樣一個 Tomcat 服務器,我叫它「Web 服務器」或者「HTTP 服務器」沒有問題,叫它「應用服務器」也沒有半毛錢問題。

常見的「Web 服務器」有 Nginx 、 Apache 、 IIS (這個做 .Net 的同學應該不陌生),常見的「應用服務器」軟件包括 WebLogic、JBoss,前者更輕量級,後者更重量級。

靜態服務和動態服務

接下來科普另一個概念:「靜態服務」和「動態服務」。

「靜態服務」返回的是一些靜態資源,比如圖片、HTML 、 CSS 、 JavaScript 等資源,這些靜態資源有一個顯著的特點是在我們的電腦上,只要路徑寫對了,可以在瀏覽器裏面直接訪問。

比如我在電腦上新建一個文件,把後綴改成 html ,裏面使用 html 隨便寫點內容:

<html>
    <h1>Hello World!</h1>
</html>

靜態服務就是每個人訪問,得到的內容都是一樣的,而動態服務就比較牛逼了,能做到每個人訪問,得到的內容都是不一樣的。

最直接的例子就是經常用的淘寶京東這些網站,登錄以後,訪問自己賬號的個人中心,肯定每個人得到的結果都是不一樣的結果。

還有就是我的博客,最開始我的博客是使用 Hexo 搭建的靜態博客,託管在騰訊雲的文件服務上,後來開了一台雲服務器,就換成了使用 WordPress 構建的動態博客。

Nginx

Nginx 是一款輕量級的 Web 服務器 / 反向代理服務器及电子郵件( IMAP / POP3 )代理服務器。

不查還真不知道,原來 Nginx 還提供了 IMAP / POP3 / SMTP 服務,設置過郵箱客戶端的同學對這三個名詞應該不陌生。

關於 Nginx ,比較令人遺憾的一件事是,它的作者伊戈爾·賽索耶夫進了監獄。

Nginx 的特點是佔有內存少,併發能力強,在同類型的網頁服務器中表現較好,國內比較有名的公司,比如說百度、京東、新浪、網易、騰訊、淘寶等都在使用。

Nginx 現在用途最多的應該是作為反向代理服務器在使用,因為它的特性穩定、佔用系統資源少、併發能力強,一般都直接放在直面用戶的最外層應對用戶流量。

用戶的訪問請求先落到 Nginx 上,由 Nginx 進行代理轉發,負載均衡到後續的 Tomcat 應用服務器上,盡可能的提升系統的穩定性。

至於 Nginx 如何複雜均衡到後面的應用服務器上,這就是另一個問題了, Nginx 有很多種的負載均衡方案,這裏我就不展開介紹了。

Nginx 是一個典型的靜態服務,把圖片等內容放在 Nginx 上,可以通過固定的鏈接直接訪問。

不過現在通過 Lua 的加持,我們也可以在 Nginx 做一些動態服務才能做的事情,這就是大名鼎鼎的 OpenResty 。

至於 Nginx 安裝或者是 OpenResty 的安裝以及簡單的使用,大家可以訪問各自的官網查看,我就不演示介紹了(反正都不難)。

Tomcat

在我的碼農生涯中,使用最多的莫過於 Tomcat ,沒有之一。

Tomcat 啟動成功的話,訪問它的首頁,正常情況下是能看到一隻貓的,雖然這隻貓長得實在是有點抽象,但人家確實是只貓。

Tomcat 是 Apache 軟件基金會( Apache Software Foundation )的 Jakarta 項目中的一個核心項目,由 Apache 、 Sun 和其他一些公司及個人共同開發而成。

Tomcat 服務器是一個免費的開放源代碼的 Web 應用服務器,這也是為什麼它可以風靡全球的重要原因。

由於這個項目有了 Sun 公司的參与和支持,所以 Tomcat 一般都會支持最新版的 Servlet 和 JSP 規範。這也是為什麼 Java 初學者接觸到的第一個 Web 服務總會是 Tomcat 。

但是 Tomcat 並未支持 EJB 和 JMS ,所以說 Tomcat 是一款輕量級的 Web 容器。

GlassFish

Sun 公司為 Java 提供了商業兼容的 Web 容器: Glassfish ,不過說實話,我沒怎麼用過這個容器,這個 Web 容器僅存在於我上大學的時候的課本上以及課後大作業上。

GlassFish 達到產品級質量,可免費用於開發、部署和重新分發。開發者可以免費獲得源代碼,還可以對代碼進行更改。

Glassfish 既是 EJB 容器也是 WEB 容器,它支持最新版的 Java EE 標準。

而剛才前面介紹的 Tomcat 則僅僅只是一個 Web 容器。

Jboss

Jboss 是一個基於 Java EE 的開放源代碼的應用服務器。 JBoss 代碼遵循 LGPL 許可,可以在任何商業應用中免費使用。

Jboss 和上面的 Glassfish 一樣,同樣是企業級的 Web 容器,並且在 2004 年 6 月, JBoss 公司宣布, JBoss 應用服務器通過了 Sun 公司的 J2EE 認證后,一直在緊跟最新的 J2EE 規範,而且在某些技術領域引領 J2EE 規範的開發。

因此,無論在商業領域,還是在開源社區, JBoss 成為了第一個通過 J2EE 1.4 認證的主流應用服務器。 JBoss 應用服務器已經真正發展成企業級應用服務器。

之後好景不長,在 2006 年, JBoss 被 Red Hat 以三億五千萬美金併購。

之後在 2019 年,也就是去年, Red Hat 為 JBoss Application Server 換了一個新的名字,即: WildFly 。

因為 JBoss 本身是開源免費的,而 Red Hat 的企業產品 JBoss EAP 是一個收費產品,Red Hat 為了使這兩個產品差異化,避免用戶混淆,而進行更名。

JBoss 版本:

  • 社區版:JBoss AS(Application Server) -> WildFly
  • 企業版:JBoss EAP(Enterprise Application Server)

JBoss 核心服務不包括支持 servlet / JSP 的 WEB 容器,一般與 Tomcat 綁定使用, JBoss 的 Web 容器使用的是 Tomcat 。

Apache

如果不是 IT 行業,如果說起來 Apache ,是不是大多數人想到的是這個東西:

不過顯然我要說的不是這個,而是這個小羽毛:

說起來慚愧,我在剛入門的很長一段時間中,一直以為 Apache 就是 Tomcat , 傻傻分不清楚。

後來接觸到 PHP 以後,才知道他們倆完全不同, Logo 就不同(這不是廢話)。

Apache 一般是指 Apache HTTP Server,是 Apache 軟件基金會(和 Tomcat 同屬一家基金會,並且 Apache 服務和 Apache 基金會名字都一樣,新人能分清才見鬼了)下的一個網頁服務器軟件。

由於其跨平台和安全性,被廣泛使用,是最流行的 Web 服務器軟件之一。它快速、可靠並且可通過簡單的 API 擴展。

我現在的博客站使用的就是 Apache 的服務,當時搞 WordPress 的時候着實坑了我一把,完全沒想到一個 PHP 環境這麼難搞,後來在網上找問題的搜索的時候才知道有 LAMP 這麼個東西。

  • Linux,操作系統。
  • Apache,網頁服務器。
  • MySQL,數據庫管理系統(或者數據庫服務器)。
  • PHP 、 Perl 或 Python,腳本語言。

不過還可以使用 Nginx 替換 Apache ,這個新的組合叫 LNMP 。

Jetty

Jetty 和 Tomcat 有很多相似之處,比如說可以為 JSP 和 Servlet 提供運行時環境。Jetty 是 Java 語言編寫的,它的 API 以一組 JAR 包的形式發布。

相比較 Tomcat 而言, Jetty 更加的輕量級,因為 Tomcat 除了遵循 Servlet 規範以外,自身還擴展了大量 Java EE 特性以滿足企業級應用的需求。

但對於大量普通的互聯網應用而言,並不需要用到 Tomcat 其他高級特性,所以在這種情況下,使用 Tomcat 是很浪費資源的。

而這時換成 Jetty ,每個應用服務器省下那幾 MB 內存,對於大的分佈式環境則是節省大量資源。

Jetty 可以同時處理大量鏈接並且長時間的保持這些鏈接,例如,一些 Web 聊天應用非常適合用 Jetty 服務器。

Jetty 的架構比較簡單,它有一個基本數據模型,這個數據模型就是 Handler,所有可以被擴展的組件都可以作為一個 Handler,添加到 Server 中,Jetty 就是幫我們管理這些 Handler 的。

Resin

最後一個放出來的是 Resin ,不知道有多少人聽說過這個 Web 容器。

Resin 是 CAUCHO 公司的產品,也是一個 Web 容器,對 Servlet / JSP 提供了良好的支持,性能也比較優良, Resin 自身採用 JAVA 語言開發。

基於百度百科的介紹是說 Resin 是一個非常流行的 Web 容器。

請恕我直言,這個容器真的這麼流行么,如果我不是因為維護一個公司的老系統,還真不知道還有這麼個 Web 容器。

可能 Resin 流行的年代比較久遠了,從我入行以後就不流行了。

其他

再說幾個只聽過沒接觸過的容器:

  • Undertow: JFinal 框架的默認容器切換成了 Undertow 。
  • WebLogic: 甲骨文出品,這個我沒接觸過,不過聽朋友講用這個大多數都是銀行,據說買一買挺貴的。
  • WebSphere: IBM 出品,這個和上面那個 WebLogic 一樣,只聽說過銀行在用。

JFinal 是基於 Java 語言的極速 WEB + ORM 框架,其核心設計目標是開發迅速、代碼量少、學習簡單、功能強大、輕量級、易擴展、Restful。用於一些小項目還是很合適的。

WebLogic 和 WebSphere 一看出品方都挺 NB 的,據說還有後台界面可以操作使用,發布程序都是點鼠標完成的。

不過現在發布程序大多數都開始用 Jenkins 了,其實也方便了很多,包括很多公司可能都上線了 DevOps 系統,程序發布只會越來越簡單。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

什麼是技術債,為什麼要還技術債?

先說我的結論就是:技術債要還,還不還技術債,決定你所在的公司是不是尊重科學尊重技術,觀點主要有一下三個:

  • 技術債是什麼,對產品和項目有什麼影響
  • 技術債對開發環境和技術氛圍的影響
  • 技術債和技術價值觀

技術棧是什麼,對產品和項目有什麼影響

既然叫技術債,那麼他本質是一種“債”,所以我們先脫離所謂的技術,單獨聊聊什麼是債?債是一個金融上的術語,代表你的負資產,說人話就是代表你欠了別人的錢,在著名美劇《冰與火之歌》裏面的蘭尼斯特家族有一句名言就是:有債必嘗

  1. 那麼生活中有哪些跟債相關的事情呢?我們日常接觸的債有哪些? 花唄,信用卡,透支下個月工資,貸款,高利貸 等等

  2. 債有什麼特點?債本身是一種透支行為,是你犧牲未來滿足自己當下的某種需求,而且所有的債都有一個共同的特點,就是利息,而且跟隨時間~利滾利

  3. 那麼債是怎麼產生的?大家可以想想你什麼時候會用信用卡,用花唄去購物,或者去借高利貸?當你渴望得到某一個東西,但是你本身還不具備購買能力的時候,你會去借債對吧,例如在你財務狀況還很差的情況下,你想買名牌包包,你想買最新款性能最好的蘋果電腦,你想買豪華轎車等等,通過透支未來,來滿足當下的需求,技術債為什麼叫債,就是通過借債,透支系統的擴展性,安全性,來達到快速上線功能目的,借債很容易上癮,為什麼?因為它可以讓你快速滿足慾望(物質,快速變現),嘗到甜頭

  4. 要麼有債要不要還?還債重不重要?:我覺得這其實是看你個人的選擇而已,你借錢也可以不還,可以賴賬,可以能拖一年是一年,甚至也可以忘記你借過的債或者否認它,這可以讓你獲得一些短期利益,讓你嘗到一些甜頭,例如技術上你也可以通過欠債,來快速的實現功能,但是不知道大家是否在意自己的信用和口碑,但在如今的文明社會正在構建就是個人的信用體系,國家徵信中心,支付寶的芝麻信用,微信的支付積分,都是在評價你的個人信用,你的還債的及時和履行契約的能力,最終都是體現在你的個人信用積分上,所以說有債不還也是可以的,這取決與你是否在意你的個人信用和口碑,但如果是一個信用不好的人那麼在一個信用體系如此完善的現代社會裡面是很艱難的,別人不敢跟你做生意,你做什麼時候都必須要先交押金,出行乘坐交通工作,信用好的可以走安全通道,你就必須過安檢和全身掃描,所以在不在債務,還不還債務,其實取決於你想不想做一個講信用的人,做一個用誠信為本去安身立命的人,如果你想做一個誠信為本的人,那麼就要放棄短期利益,把目光放的更加長遠一些,記得我曾經看過一個報道,是講京東創業的故事,京東的企業家劉強東對記者說,如果我們想要賺錢,那麼很簡單,我們有很多捷徑可以走,例如我們不給員工買交社保和五險一金,把大量人員全部轉去外包公司,那麼我們每年營業額馬上就會多十幾個億,可以馬上賺很多的錢,但是他沒有選擇這樣做,這樣通過透支的做事方式確實短期可以獲得一些利益,但是長期來看,你失去的人心,失去了企業的誠信

技術債對開發環境和技術氛圍的影響

產品的迭代就像一個運動員在跑步,汽車在前進,技術債就像運動員消耗的體力,汽車在運行當中所出現的各種問題,沒油,爆胎,熄火等等,還債就是給運動員補水,給汽車加油一樣,是為了可以讓運動員和汽車跑的更遠,不至於因為累積技術債而掛掉,為什麼要重視技術債和細節?因為魔鬼藏在細節當中,再舉幾個和生活息息相關的:

  1. 為什麼我們大樓每天檢修消防和安全設備,為什麼消防要經常做演習?在這些沒有真正產出的事情上耗費精力,難道不能等到真正發生火災發生後去撲滅和搶救嗎 ?
  2. 飛機是在起飛前,為什麼需要做那麼多的安全和檢查措施?確保沒有風險后,然後再執行起飛,難道不能先讓飛機起飛,等到出現問題后再去補救和修復嗎 ?
  3. 為什麼我們提倡每天鍛煉身體,健康飲食?為什麼每年要去醫院體檢?難道不應該等到你的身體已經出現問題,或者發出警報后,你再去看醫生嗎?

說到這裏,技術債的重要性毋庸置疑,重視技術債,就是重視於未然,已最低的成本或者零成本,防止未來的災難發生,還不還技術債很多時候是一種選擇,這些選擇決定了你有沒有預先判斷和解決問題的能力,那麼什麼樣的產品不用還技術債?一次性產品,例如一次性杯子,一次性手套用完就扔掉,所以如果產品長期的可持續的發展,那麼技術債的重要性是毋庸置疑的,對方辯友可能會說我們不是不還技術債,我們只是等做完緊急需求等到空閑時間再還技術債,但是經常做項目的同事應該了解,哪有什麼空閑時間?我們在項目衝刺的時候怎麼可能還會有空閑時間,大部分時間所謂的稍後處理,其實就是不處理,屬於掩耳盜鈴,當技術債被遺忘后就成為項目的定時炸彈埋在那裡了,而且技術債的特性前面也說了,所謂的稍後處理,就是讓它利滾利,拖延時間越長,還債的成本越高,而且人們還債的意願就越低,誰也不敢去碰它,例如,你身體出現問題,你不去看醫院檢查和修復問題,而是一直繼續使用和消耗你的身體,拖到最後實在不能動的,你沒辦法去醫院一查,癌症晚期,那時候神仙也沒救了, 而且技術債不單單是技術債,它就像一個垃圾堆一樣,久而久之不處理,慢慢周圍就會產生更多的垃圾,因此產生的“破窗效應”更加是會對未來的項目環境造成很大的影響,大家也會逐漸喪失維護環境的信心,所以我們在討論技術債的時候不僅僅是討論技術債本身,技術債對團隊追求質量的信心,對大家維護環境整潔的积極性都會造成很大的影響,所以我方觀點是,技術債,有債必嘗,越拖成本越高,最好是在發現的時候馬上處理它,不要讓乾淨的房間出現垃圾堆,只有在乾淨的環境下大家才能持續的高效的去創造,一個需求捏着鼻子做,兩個需求捏着鼻子做,久而久之代碼中就散發出臭味,對於大家的工作體驗和項目質量都會產生巨大的影響,如果連工作都不開心,那還談什麼夢想?沒有良好的技術環境企業就無法吸收和留住高質量的技術人才,人才是現代企業的核心競爭力,沒有人才的企業在瞬息萬變的市場上是難以做出快速反應的

技術債和技術價值觀

不重視技術債就是不重視技術,不尊重科學發展,不能客觀的認識和理解技術的複雜性和軟件工程帶來的價值和意義,我們國家近幾年就因為不重視技術吃了不少虧,比如去年的中興通訊公司被制裁,因為沒有自己的技術,芯片被斷供製裁后卻毫無還手之力,國產目前的大多手機廠商看似繁榮,但手機行業的 8,9 成利潤被都被掌握技術的蘋果公司賺走,打開現在的智能手機裏面你會看到,美國的芯片和谷歌的安卓操作系統,日本的鏡頭和相機模組,三星的屏幕,還要在微薄的利潤上繳納高通的芯片稅,實際上國內大多廠商做的都是代加工和組裝的臟活累活,沒有技術的公司,就會受制於人,不僅賺不到錢,而且公司的命運都是由掌握核心技術的公司決定,再比如一個近期的新聞,哈工大的建模軟件被斷供等等例子,不勝枚舉,那麼技術有多重要?我們就用華為來舉例,華為為什麼是一家值得尊重的科技公司,因為他打破了中國自從第二次工業革命以來,但是因為長期技術落後長期受制於人的客觀事實,中國以前的代號叫做世界工廠,只適合做一些勞動密集型產業,但華為讓中國企業在先進的技術領域,同樣是被美國制裁,為什麼華為活的比中興好很多?因為華為重視技術,從海思芯片到5G 再到操作系統,自己擁有產業供應鏈,有自己的的核心技術,才能掌握自己的命運,而且在取得商業上的成功后,也得到的大家的尊重,相同還有最近處於風口的台灣的芯片製造廠商台積電公司,全球唯二掌握 7納米芯片製造技術的芯片公司,因為自主的核心技術在擁有可以在國際上和英特爾平起平坐資本,綜上所述,不重視技術雖然也可以生存,但是重視技術,掌握核心技術,才能走的更遠,我們都知道技術的目的是要體現商業價值,但前提是要擁有核心技術才配擁有商業價值,沒有技術壁壘的企業和人隨時都可能被人替換,而且幾乎沒有什麼成本,重視技術公司才能發展的更遠,才不會受制於人,才能成為頭部玩家,收割行業90%的利潤,才有可能成為一家偉大並且受人尊重的公司,不然你去想想蘋果公司為什麼不放棄技術,微軟和谷歌為什麼不放棄技術,英特爾和高通為什麼不放棄技術,技術很重要,可以讓個人和企業提升競爭力,不容易被淘汰,對於國家和社會,二次工業革命以來,技術改變了我們的生產效率,從而改變我們社會的運行方式,技術幫助解決了困擾我們幾千年的《馬爾薩斯陷阱》,我們國家經歷過近代史的幾百年技術落後的屈辱后,更加的尤為重視技術,我們在1960 年代大家都吃不飽的情況下我們就研發出自己的原子彈,我們國家級的戰略目標《中國製造 2025》就包含的“芯片,人工智能,區塊鏈,機器人,新能源”等等高精尖產業,目的就是讓我們脫離低端製造業,脫離勞動密集型產業,因為沒有技術含量的重複性的勞動工作未來都將被機器和 人工智能 取代,在未來很難被取代就是人類特有的豐富的想象力和創造力。

最後我想再引用 一個真實的故事,是來源於 NASA 的著名文章《為什麼要探索太空?》,文章的背景是來源於 1970年,贊比亞修女 Mary Jucunda 給 NASA 科學家 Ernst Stuhlinger 博士寫了一封信,信中,Mary Jucunda 修女問道:目前地球上還有這麼多小孩子吃不上飯,他怎麼能捨得為遠在火星的項目花費數十億美元。Ernst Stuhlinger 在回信中寫到一個真實的故事如下:

那是在400年前,德國某小鎮里有一位伯爵。他是個心地善良的人,他將自己收入的一大部分捐給了鎮子上的窮人。這十分令人欽佩,因為中世紀時窮人很多,而且那時經常爆發席捲全國的瘟疫。一天,伯爵碰到了一個奇怪的人,他家中有一個工作台和一個小實驗室,他白天賣力工作,每天晚上的幾小時的時間專心進行研究。他把小玻璃片研磨成鏡片,然後把研磨好的鏡片裝到鏡筒里,用此來觀察細小的物件。伯爵被這個前所未見的可以把東西放大觀察的小發明迷住了。他邀請這個怪人住到了他的城堡里,作為伯爵的門客,此後他可以專心投入所有的時間來研究這些光學器件。然而,鎮子上的人得知伯爵在這麼一個怪人和他那些無用的玩意兒上花費金錢之後,都很生氣。“我們還在受瘟疫的苦,”他們抱怨道,“而他卻為那個閑人和他沒用的愛好亂花錢!”伯爵聽到后不為所動。“我會盡可能地接濟大家,”他表示,“但我會繼續資助這個人和他的工作,我確信終有一天會有回報。”果不其然,他的工作(以及同時期其他人的努力)贏來了豐厚的回報:顯微鏡。顯微鏡的發明給醫學帶來了前所未有的發展,由此展開的研究及其成果,消除了世界上大部分地區肆虐的瘟疫和其他一些傳染性疾病。伯爵為支持這項研究發明所花費的金錢,其最終結果大大減輕了人類所遭受的苦難,這回報遠遠超過單純將這些錢用來救濟那些遭受瘟疫的人。

綜上所述,重視技術債就是重視技術,重視技術就是重視細節和未來,魔鬼存在細節當中,細節決定成敗

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案