捕获故障和系统状态(第一部分)

工程 | 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 社区即将举办的所有活动。

查看全部