65.9K
CodeProject 正在变化。 阅读更多。
Home

使用动态代理实现容错和故障转移

starIconstarIconstarIconstarIconstarIcon

5.00/5 (13投票s)

2009年3月2日

CPOL

6分钟阅读

viewsIcon

34462

downloadIcon

448

如何利用 LinFu(或任何其他动态代理实现)来实现容错和故障转移。

引言

在开发分布式系统时,最终需要处理服务故障。当开发依赖于其他服务的服务时,问题变得更加关键。本文旨在展示使用 LinFu DynamicProxy 实现方便的故障转移解决方案。然而,这种方法可以很容易地利用任何流行的实现(包括,例如,Castle 项目)。

背景

为什么要在代理代码中进行故障转移呢?我们本质上是使用 AOP(面向切面编程)来横切我们对远程服务的引用,并在定义明确的代码段中处理重试和服务解析,而不会用 try...catch 和重试循环逻辑污染业务实现。这使我们能够专注于实现,而不是基础设施问题。

本文选择的 DynamicProxy 实现是 LinFu 项目。

定义服务

为了演示此功能,我选择了一个带有以下接口的简单 .NET Remoting 服务器

public interface IEcho
{
    string Echo( string echo );
}

我们的想法是创建一个 DynamicProxy,它将把远程方法的实际调用包装在一个 try...catch 块中。然后,在 catch 块中,决定这是否是需要通过查找新服务(例如 SocketException 等)来处理的异常,并公开一种方便的方式来更新远程服务的引用,重试调用以向客户端提供透明的故障转移。所有其他异常都可以正常传播回客户端。在所有可能的服务都已尝试且均不可用的情况下,我们需要某种方式来通知客户端。

我基本上实现了两个层。第一个是标准的 LinFu DynamicProxy IInvokeWrapper 实现,它将允许我们拦截远程调用,称为 ExceptionWrapper

public class ExceptionWrapper : IInvokeWrapper
{
...
    public ExceptionWrapper( Object o )
    {
        ...
    }

    public void AddHandler( IExceptionHandler handler )
    {
        ...
    }
...
#region IInvokeWrapper Members

    public void AfterInvoke( InvocationInfo info, object returnValue ) { }

    public void BeforeInvoke( InvocationInfo info ) { }

    public object DoInvoke( InvocationInfo info )
    {
        object result = null;
        bool retry = true;
        int retries = -1;

        while( retry )
        {
            try
            {
                retry = false;
                result = info.TargetMethod.Invoke( _object, info.Arguments );
            }
            catch( Exception e )
            {
                Exception ex = e;
                if( e is TargetInvocationException )
                    ex = e.InnerException;

                Type type = ex.GetType();
                foreach( IExceptionHandler handler in _handlers )
                {
                    if( handler.ExceptionTypes.Contains( type ) )
                    {
                        handler.HandleException( ex, info, ref _object );
                        if( retries == -1 )
                            retries = handler.Retries;

                        if( retries-- > 0 )
                        {
                            retry = true;
                            break;
                        }
                        else
                        {
                            if( handler.RethrowOnFail )
                                throw ex;
                        }
                    }
                }
                if( !retry )
                    throw ex;
            }
        }

        return result;
    }

#endregion

}

此实现的主要部分(catch 块)正在调用第二层,即 IExceptionHandler

public interface IExceptionHandler
{
    IList<Type> ExceptionTypes { get; }
    void HandleException( Exception e, InvocationInfo info, ref object o );
    int Retries { get; }
    bool RethrowOnFail { get; }
}

该接口允许最终用户定义自己的自定义异常处理程序。但是,我们已经实现了一个 DefaultHandler,它可以原样使用,也可以子类型化以扩展它并添加所需的行为。

首先,在接口的方法和属性中,我们有一个此处理程序处理的异常类型列表。其次,我们有一个方法 (HandleException),如果在一个调用期间抛出其中一种异常类型,InvocationWrapper 将调用该方法。最后,有两个属性控制重试逻辑。

由于我的示例(使用 .NET Remoting),异常的实际处理包含一些复杂的逻辑。但是,我认为这说明了对这种类型的抽象的需求。该逻辑只需在一个位置进行测试和调试,而无需在代码的其余部分中复制。由远程处理服务器抛出的异常最终会返回到客户端代理,实际上会将远程异常包装到 TargetInvocationException 中,并将 InternalException 属性设置为原始异常。这意味着,为了用相同的实现处理远程处理和非远程处理异常,我们有一个尴尬的实现,即如果我们实际捕获了 TargetInvocationException,则必须将处理的异常引用设置为 InnerException 属性。此外,正如原始文章的评论之一所指出的那样,使用 throw e; 而不是仅使用 throw; 重新抛出异常意味着我们失去了原始堆栈跟踪。但是,在此实现中,我们重新抛出内部异常,这意味着我们必然会失去原始堆栈。

一个示例

为了用一个示例将所有内容联系起来,我已将 IEcho 接口作为控制台应用程序中的服务器实现。它接受一个命令行参数,即用于服务器 TcpChannel 的端口。服务器还在 Echo() 方法的每第 7 次调用时抛出 ApplicationException,以演示未捕获的异常将透明地重新抛出给客户端,而不会调用 ExceptionHandler

在测试客户端时,您可以启动服务器的多个实例,每个实例都使用不同的端口(例如,9001、9002、9003)。客户端接受多个命令行参数,这些参数是正在运行的可用服务的端口。

客户端代码以一个有效的服务 URL 字符串列表开始,该列表由命令行参数填充

foreach( string port in args )
    _urls.Add( string.Format( "tcp://127.0.0.1:{0}/EchoServer", 
                              int.Parse( port ) ) );

设置客户端 TcpChannel 后,我们使用 ExceptionWrapper 创建一个动态代理,并将客户端代理包装到我们 URL 列表中的第一个服务

ProxyFactory factory = new ProxyFactory();
ExceptionWrapper wrapper = 
  new ExceptionWrapper( Activator.GetObject( typeof( IEcho ), 
                        _urls[ _currentUrl ] ) );
IEcho echoProxy = factory.CreateProxy<IEcho>( wrapper );

接下来,我们设置一个 ExceptionHandler,以循环方式遍历可用的服务 URL

DefaultHandler handler = new DefaultHandler();
handler.Retries = _urls.Count - 1;
handler.ExceptionTypes.Add( typeof( SocketException ) );
handler.ExceptionTypes.Add( typeof( TargetInvocationException ) );
handler.OnHandledException += 
	new OnHandledExceptionDelegate( handler_OnHandledException );
wrapper.AddHandler( handler );

我们将处理程序的 Retries 属性设置为 URL 数量减 1。这允许第一个成为主要服务,在失败时,第一次重试将用于第二个服务,依此类推。我们注册两种异常类型并注册一个事件委托来处理给定的异常之一。最后,我们将处理程序添加到调用包装器。

我们可以选择使用相同的委托处理多种异常类型,或者添加多个处理程序实例,每个实例处理一种异常类型。该设计还允许 IExceptionHandler 接口的自定义实现,或 DefaultHandler 的子类型化(所有有趣的方法都声明为 virtual)。但是,由于 OnHandledException 事件的存在,大多数情况都可以原样处理。

OnHandledException 事件中最需要注意的重要部分是 object 参数(即内部远程服务引用)使用 ref 关键字传入,这允许委托更新引用。

最后,我们有 OnHandledException 事件的实现

static void handler_OnHandledException( IExceptionHandler handler, Exception e, 
                                   InvocationInfo info, ref object o )
{
    o = Activator.GetObject( typeof( IEcho ), _urls[ ++_currentUrl % _urls.Count ] );
}

每当调用此服务时发生我们两种异常类型中的一种时,我们的处理程序委托都会被调用。然后我们滚动到列表中的下一个 URL 并重新获取服务引用。

我们将只调用委托处理程序的 Retries 属性中定义的次数(在此示例中为 2)。如果在那之后我们仍然失败,那么如果 RethrowOnFail 为 true,捕获的异常将重新抛出给客户端。否则,将返回 null。在我们三个服务的示例中,如果我们在最终失败之前想要循环两次,我们将 Retries 属性设置为 5。

客户端代码的其余部分只是一个 while 循环,反复调用 Echo 方法。周围有一个 try...catch,但仅用于最终故障或其他未捕获的异常类型。

示例

要运行示例,只需使用不同的端口多次启动服务器。然后,运行客户端,在命令行上引用这些端口。客户端将连接到第一个服务并按预期工作。杀死第一个服务并发送更多回显字符串将显示下一次调用会自动故障转移到下一个服务。杀死所有服务将显示捕获的异常最终如何重新抛回客户端。

历史

  • 2009 年 3 月 4 日 - 我更新了文章和示例代码,以反映更准确的 .NET Remoting 异常处理。(感谢那些评论原始文章的人!)我还添加了更多的控制台输出,使其更清晰地显示发生了什么。

[我最初发布这篇文章时,它确实按照预期工作,但几乎是偶然的。事实上,我偏离得太远了,以至于在理解了我的误解之后,我基本上重写了示例代码。抱歉,那些尝试使用原始代码的人。顺便说一句,这个承认并不意味着我的想法是错误的,只是我的实现是错误的。感谢阅读。]

© . All rights reserved.