捕獲故障和系統狀態(第一部分)

工程 | Alef Arendsen | 2008年1月7日 | ...

在 The Spring Experience 大會上,我主持了一個關於各方面內容的會議。其中一個是我上週描述的 Hibernate 同步切面。另一個是能夠捕獲首次故障和系統狀態的切面,有時稱為首次故障資料捕獲(First-Failure Data Capture - FFDC)。我主持這個會議是為了展示一些非常有用的切面,但這些切面在實踐中可能還不常見。我經常聽到人們詢問除了日誌、跟蹤、事務管理和安全之外的其他切面。我認為 Hibernate 同步切面和 FFDC 切面都是很好的例子。

引言

FFDC 的目標是在發生錯誤時捕獲儘可能多的當前系統狀態資訊。下面的條目解釋了這個切面如何工作以及如何在您自己的應用程式中使用它。

讓我們設定以下兩個目標

  • 當故障從業務服務中逃逸時,我們希望記錄呼叫上下文,這意味著在該業務服務執行的上下文中發生的所有呼叫。
  • 當故障從業務服務中逃逸時,我們希望記錄故障的根本原因,這意味著不僅要記錄最頂層的異常(從方法中逃逸的異常),還要記錄可能被包裝、吞沒或重新丟擲的*首次*丟擲的異常。

為了做到這一點,首先讓我們設計一個能夠為我們跟蹤我們想要記錄的資料的類。我們將這個類稱為 CallContext。我省略了實際的實現。我會在下一篇文章中釋出程式碼,這裡的實現並不重要,而且除此之外,它只是一個非常直接的資料持有者。


public class CallContext {

	/**
	 * Registers the root call of this call context.
	 * We want to distinguish between the root call
	 * and all subsequent calls issued in the context
	 * of the root call.
	 */
	public void setRootCall(JoinPoint rootCall) { ... }
	
	/**
	 * Registers a call at a certain depth.
	 * @param the call to register
	 * @param the depth of the call
	 */ 
	public void registerCall(JoinPoint call, int depth) { ... }
	
	/**
	 * Registers the first failure of this call context.
	 * A first failure might already have occurred in which
	 * case subsequent registrations of the same or different
	 * failures will be ignored.
	 */
	public void setFirstFailure(Throwable t) { ... }
	
	/**
	 * Log the entire call context (i.e. output it to
	 * System.out).
	 */
	public void log(Throwable t) { ... }
}

如您所見,我們使用 AspectJ 的 JoinPoint 型別來識別程式中發生的事件。

定義四種場景

好了,我們資料已準備就緒。接下來,讓我們稍微重述一下之前設定的兩個目標,並列出我們希望在程式中發生的事情。
  • 在呼叫業務服務之前,我們希望向當前呼叫上下文註冊根呼叫。
  • 在業務服務上下文中進行呼叫之前,我們希望向當前呼叫上下文註冊該呼叫(以及當前深度)。
  • 當業務服務內部發生異常時,將其註冊為當前呼叫上下文中的首次故障。
  • 當異常從業務服務中逃逸後,我們希望記錄當前呼叫上下文。

如您所見,我只是稍微對事物進行了切分,以便出現“在*某事*發生之前/之後,做*某事*”形式的句子。剩下的唯一事情就是確定這兩個*某事*,然後我們就完成了。讓我們分別處理這三個不同的邏輯片段。

在業務服務之前,註冊根呼叫到當前上下文

使用 AspectJ,這相對容易做到。假設業務服務可以透過新增到方法或類上的 @BusinessService 註解來標識。如果新增到類上,則該類的所有方法都是業務服務。如果新增到方法上,則只有該方法是業務服務。換句話說:業務服務是*一個在由 @BusinessService 註解的類中定義的方法*,或者*一個本身由 @BusinessService 註解的方法*。在 AspectJ 中,這歸結為以下內容(有關 AspectJ 切入點表示式語言精確語法的更多資訊,請參閱 http://www.eclipse.org/aspectj//doc/released/progguide/semantics-pointcuts.html)。

pointcut businessService() : call(* (@BusinessService *..*).*(..)) || call(@BusinessService * *(..));

現在我們已經識別出業務服務,可以完成第一個場景了。


public aspect FirstFailureDataCapturer {

	public CallContext callContext = new CallContext();
	
	pointcut businessService() : call(@BusinessService *..*).*(..)) || 
			call(@BusinessService * *(..));
	
	before() : businessService() {
		// 'thisJoinPoint' is an implicit variable (just like 'this')
		// that represents the current join point
		this.callContext.setRootCall(thisJoinPoint);
	}
}

在業務服務上下文中的呼叫之前,註冊該呼叫到當前呼叫上下文

我們已經完成了第一個場景,接下來處理第二個場景。我們已經確定了業務服務的含義。我們希望捕獲業務服務上下文中的所有呼叫。任意呼叫可以按如下方式標識:

pointcut methodCall() : call(* *(..));

如果我們使用這個切入點,我們會將該場景應用於*所有方法*,但我們只想將其應用於業務服務內部的方法。因此,我們需要限制此切入點的範圍。我們可以透過使用*cflow* 切入點指示符來做到這一點。*cflow 切入點指示符* 接受另一個切入點,並將其限制為該切入點上下文中的事件。讓我們看看如何使用它來解決當前問題。請這樣理解:'業務服務中的方法呼叫是 方法呼叫(參見上面定義的切入點) 同時(並且)在業務服務的 控制流 中(參見前面定義的另一個切入點)。'


pointcut methodCallInBusinessService() : methodCall() && cflow(businessService());

讓我們進一步說,我們不想註冊*所有*方法呼叫,而只是一部分有限的呼叫。以下定義了一個可追蹤的方法,只標識了我認為相關的那些方法。它還排除了在切面自身中定義的方法(或在切面的控制流中)。後者防止了無限迴圈的發生。讓我們也把它讀出來:可追蹤的方法是 業務服務中的方法呼叫(參見上面定義的切入點) 同時不(並且不)在 FirstFailureDataCapturer 中定義的正在執行的通知 的 控制流 中,並且它也不應該是對 equals()、hashCode() 或 getClass() 的呼叫。


pointcut traceableMethod() : 
	methodCallInBusinessService() &&
	!cflow(within(FirstFailureDataCapturer) && adviceexecution()) &&
	!call(* equals(Object)) && !call(* hashCode()) && !call(* getClass());

讓我們使用這個切入點來實現我們確定的第二個場景。在上面對場景的描述中,我們沒有指定還需要跟蹤當前深度。我們使用一個 before 通知來記錄當前呼叫。讓我們也使用同一個通知來跟蹤深度,並使用一個 after 通知來將深度重置回其先前的狀態。


public aspect FirstFailureDataCapturer {

	public CallContext callContext = new CallContext();
	
	public int currentDepth = 0;
	
	// other pointcuts and advices omitted

	pointcut methodCallInBusinessService() : methodCall() && cflow(businessService());
	
	pointcut traceableMethod() : 
		methodCallInBusinessService() &&
		!cflow(within(FirstFailureDataCapturer) && adviceexecution())) &&
		!call(* equals(Object)) && !call(* hashCode()) && !call(* getClass());
		
	before() : traceableMethod() {
		currentDepth++;
		callContext.registerCall(thisJoinPoint, currentDepth);
	}
	
	after() : traceableMethod() {
		currentDepth--;
	}
}

當業務服務內部發生異常時,將其註冊到當前呼叫上下文

現在我們完成了第二個場景,我們已經捕獲了我們想要捕獲的幾乎所有狀態。我們想要捕獲的最後一件事是發生在該業務服務上下文中的第一個異常。

潛在的故障點是 a) 從方法中逃逸的異常 或 b) 方法內部的異常處理器(然後被包裝、吞沒、可能重新丟擲等等)。讓我們使用這個定義來實現我們的第三個場景。第一個切入點只是使用可追蹤方法切入點來識別潛在的故障點。我們稍後會使用 after throwing 通知來完成我們場景的一部分。第二個更有趣一些。它定義了一個切入點,用於識別業務服務控制流中的異常處理器(catch 塊)。使用這個切入點,我們可以識別例如被捕獲、包裝和重新丟擲的異常(或被捕獲和吞沒的異常)。


pointcut potentialFailurePoint() : traceableMethod();
	
pointcut exceptionHandler(Throwable t) : handler(*) && args(t) && cflow(businessService());

我們將使用 before 和 after 通知來完成第三個場景。首先:在異常處理器之前,記錄異常


public aspect FirstFailureDataCapturer {

	private CallContext context = new CallContext();

	// other members omitted

	before(Throwable t) : exceptionHandler(t) {
		this.callContext.setFirstFailure(t);
	}
}

現在,讓我們定義另一個通知


public aspect FirstFailureDataCapturer {

	private CallContext context = new CallContext();

	// other members omitted

	after() throwing(Throwable t) : potentialFailurePoint() {
		this.callContext.setFirstFailure(t);
	}
}

當異常從業務服務中逃逸後,記錄當前呼叫上下文

我們需要做的最後一件事是在業務服務執行導致異常時記錄當前呼叫上下文。我們已經擁有了所有必需的要素(切入點),可以直接跳到通知的定義,所以讓我們開始吧。

public aspect FirstFailureDataCapturer {

	private CallContext context = new CallContext();

	// other members omitted

	after() throwing(Throwable t) : businessService() {
		this.callContext.log(t);
	}
}

以 CarPlant 為例

在 The Spring Experience 大會上的會議中,我使用了我(臭名昭著的)CarPlant 示例來展示 FirstFailureDataCapturer。CarPlant 是一個相對較小的系統,能夠製造汽車。製造汽車是一個兩步過程:1) 從 CarPartsInventory 系統獲取零件,2) 要求 CarAssemblyLine 將零件組裝成一輛 Car。CarPlant 本身

@BusinessService public Car manufactureCar(CarModel model) {
	Set <Part> parts = inventory.getPartsForModel(model);
	
	return assemblyLine.assembleCarFromParts(model, parts);
}

在這個示例中,CarPartsInventory 是一個樁(stub),並沒有真正做任何有用的事情。


public Set<Part> getPartsForModel(CarModel model) {
	return new HashSet<Part>();
}

有趣的部分是 CarAssemblyLine。正如您在下面的程式碼中看到的,CarAssemblyLine 中有一些奇怪的程式碼。它首先丟擲一個異常,自己捕獲它,然後將其重新丟擲為一個相當 MeaninglessException(無意義異常)。


public Car assembleCarFromParts(CarModel model, Set<Part> parts) {
		
	try {
		throw new OnStrikeException("The workers are on strike!");
	} catch (OnStrikeException e) {
		throw new MeaninglessException();
	}
}

顯然,在正常情況下,這個問題的真正原因(根本原因)在這種情況下永遠不會被識別出來(它被捕獲了,沒有記錄...丟擲了另一個異常,並且根本原因沒有傳遞),而且我們永遠無法*精確地*在真實且首次故障(OnStrikeException)發生的那個時間點註冊系統狀態。幸運的是,現在我們有了 FirstFailureDataCapturer,我們可以註冊根本原因並記錄它。下面您可以找到一個序列圖和我執行測試的一些輸出。如您所見,我們不僅獲得了呼叫堆疊,還獲得了**在此業務服務執行上下文中發生的所有呼叫**,換句話說:完整的呼叫樹。

ffdc.png

捕獲系統狀態

如果您仔細看,可以看到第一個被 Dump 的異常是 MeaninglessException。然而,在 MeaninglessException 被 Dump 之後,有一條訊息說有一個根本原因與 MeaninglessException 不同,然後 Dump 出了真正的異常。堆疊跟蹤還提到真正的異常發生在第 18 行,而 MeaninglessException 源自第 20 行。

現在我們已經識別出故障發生的*真實*點,我們也可以開始捕獲系統狀態了。可以想象,CarPlant 第 18 行的系統狀態可能與 CarPlant 第 20 行的系統狀態不同,而我們的 FirstFailureDataCapturer 允許我們在*正確的時間點*註冊系統狀態。

那麼,系統狀態到底是什麼呢?這完全取決於執行時、您的特定應用程式以及您感興趣的內容。幾個例子:

  • 當前登入的使用者
  • 當前的汽車製造請求
  • 任何技術系統狀態(執行緒數量、快取統計資訊等)
  • 發生此異常的節點

捕獲系統狀態現在非常容易,我們可以在 CallContext.setFirstFailure() 方法內部進行,例如。

那第二部分呢??

這個切面還沒有完成!完整的切面第一次出現時是按如下方式編碼的:

public aspect FirstFailureDataCapturer {

	public CallContext callContext = new CallContext();
	
	pointcut businessService() : call(@BusinessService * *(..)) || call(* (@BusinessService *..*).*(..)) || call(@BusinessService * *(..));
	
	before() : businessService() {
		// 'thisJoinPoint' is an implicit variable (just like 'this')
		// that represents the current join point
		this.callContext.setRootCall(thisJoinPoint);
	}
}

如您所見,Call Context 在 FirstFailureDataCapturer 例項化時被例項化。當然,現在的問題是:*FirstFailureDataCapturer 何時以及例項化多少次*?而且,當您回答了這個問題後,可能會想到另一個問題:*如果在多執行緒環境中使用此切面會發生什麼*?在下一部分中,我將討論所有這些問題,並對切面進行一些其他更改以使其更完善。與此同時,您當然可以在評論中嘗試回答這些問題 :)!我也將在下一部分提供該切面的原始碼。

獲取 Spring 電子報

訂閱 Spring 電子報保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲取支援

Tanzu Spring 透過一份簡單的訂閱提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

即將舉辦的活動

檢視 Spring 社群即將舉辦的所有活動。

檢視全部