病毒消息处理

有害消息是一类已超出向应用程序传递的最大尝试次数的消息。 当基于队列的应用程序由于错误而无法处理消息时,可能会引起这种情况。 为符合可靠性要求,排队的应用程序是在事务中接收消息的。 中止已接收某个排队消息的事务时,该消息仍会保留在队列中,这样当开始一个新事务时,将对该消息重试操作。 如果导致事务中止的问题未得到更正,则直到超出最大传递尝试次数并导致产生病毒消息时,接收应用程序才会中断接收和中止同一消息的循环。

消息变为病毒消息的原因有很多。 最常见的是应用程序特定的原因。 例如,如果某个应用程序从队列中读取消息,并执行某些数据库处理,则该应用程序在获取数据库锁时可能会失败,导致中止事务。 因为数据库事务已中止,所以消息仍保留在队列中,这会导致应用程序再次读取消息,并再次尝试获取数据库锁。 如果消息包含无效信息,则也可能变为病毒消息。 例如,采购订单可能包含无效的客户编号。 这种情况下,应用程序可能会自动中止事务,将该消息强制变为病毒消息。

有时消息可能无法被调度到应用程序,不过,这种情况比较少见。 Windows Communication Foundation (WCF) 层可能会找到消息的问题所在,例如消息有错误帧、附加到消息的消息凭据无效或操作标头无效。 在上述情况下,应用程序绝不会收到消息;不过,消息仍可能变为病毒消息,可以对其进行手动处理。

处理病毒消息

在 WCF 中,有害消息处理提供了一种机制,使接收应用程序可以处理无法调度到应用程序的消息、或那些虽然调度到了应用程序但是由于应用程序特定原因而无法进行处理的消息。 通过每个可用排队绑定中的下列属性配置有害消息处理:

  • ReceiveRetryCount。 一个整数值,指示将某个消息从应用程序队列传递到应用程序的最大重试次数。 默认值为 5。 对于立即重试就可以修复问题(如数据库出现临时死锁)的情况,这个数值已足够了。

  • MaxRetryCycles。 一个整数值,指示最大重试周期数。 一个重试周期包括将消息从应用程序队列传送到重试子队列,在经过可配置的延迟后,从重试子队列将消息传送回应用程序队列以便重新尝试传递。 默认值为 2。 在 Windows Vista 中,消息尝试最多 (ReceiveRetryCount +1) * (MaxRetryCycles + 1) 次。 MaxRetryCycles 在 Windows Server 2003 和 Windows XP 上被忽略。

  • RetryCycleDelay。 重试周期之间的时间延迟。 默认值为 30 分钟。 MaxRetryCyclesRetryCycleDelay 共同提供一个机制,用于解决周期性延迟之后重试可修复问题的问题。 例如,这种机制可以处理 SQL Server 挂起的事务提交中锁定的行集。

  • ReceiveErrorHandling。 一个枚举,指示对在已尝试过最大重试次数后仍无法传递的消息所采取的操作。 可能的值包括“错误”、“删除”、“拒绝”和“移动”。 默认选项为“错误”。

  • 错误。 此选项会向导致 ServiceHost 出现错误的侦听器发送一个错误。 必须利用其他一些外部机制将该消息从应用程序中移除,应用程序才能继续处理队列中的消息。

  • 删除。 此选项删除病毒消息,该消息永远不会再传递到应用程序。 如果该消息的 TimeToLive 属性在此时已过期,那么此消息可能会显示在发送方的死信队列中。 如果不是这种情况,则该消息将不会显示在任何位置。 此选项指示用户尚未指定丢失消息时该如何处理。

  • 拒绝。 此选项仅在 Windows Vista 上可用。 选择此选项会指示消息队列 (MSMQ) 将否定确认发送回发送队列管理器,以说明应用程序无法接收该消息。 该消息会放入发送队列管理器的死信队列中。

  • 移动。 此选项仅在 Windows Vista 上可用。 选择此选项会将病毒消息移动到病毒消息队列,以供以后由病毒消息处理应用程序进行处理。 病毒消息队列是应用程序队列的子队列。 病毒消息处理应用程序可以是将消息从病毒队列中读取出来的 WCF 服务。 病毒队列是应用程序队列的子队列,其地址为 net.msmq://<计算机名称>/应用程序队列;poison,其中 计算机名称 是该队列所驻留的计算机的名称,应用程序队列是应用程序特定队列的名称。

下面是对消息尝试传递的最大次数:

  • 对于 Windows Vista,请用 ((ReceiveRetryCount+1) * (MaxRetryCycles + 1)) 计算。

  • 对于 Windows Server 2003 和 Windows XP,请用 (ReceiveRetryCount + 1) 计算。

注意

对于成功传递的消息,不会再重试传递。

为了跟踪尝试读取消息的次数,Windows Vista 保留了一个持久性消息属性(用于对中止次数进行计数)和一个移动计数属性(用于对消息在应用程序队列和子队列之间移动的次数进行计数)。 WCF 通道使用上述属性计算接收重试计数和重试周期计数。 在 Windows Server 2003 和 Windows XP 上,中止计数是由 WCF 通道在内存中维护的,如果应用程序失败,会重置此计数。 另外,在任意时候,WCF 通道最多只为 256 条消息保留中止计数。 如果读取了第 257 条消息,则时间最早的消息中止计数会被重置。

中止计数和移动计数属性均可用于通过操作上下文进行的服务操作。 下面的代码示例演示如何访问上述两个属性。

MsmqMessageProperty mqProp = OperationContext.Current.IncomingMessageProperties[MsmqMessageProperty.Name] as MsmqMessageProperty;
Console.WriteLine("Abort count: {0} ", mqProp.AbortCount);
Console.WriteLine("Move count: {0} ", mqProp.MoveCount);
// code to submit purchase order ...

WCF 提供两种标准排队绑定:

  • NetMsmqBinding。 这种 .NET Framework 绑定适用于执行与其他 WCF 终结点之间的基于队列的通信。

  • MsmqIntegrationBinding。 这种绑定适用于与现有消息队列应用程序之间的通信。

注意

可以根据 WCF 服务的要求更改上述绑定中的属性。 对于接收应用程序来说,整个病毒消息处理机制都是本地的。 处理过程对于发送应用程序是不可见的,除非接收应用程序最终停止接收并将否定确认发送回发送方。 这种情况下,该消息会移动到发送方的死信队列中。

最佳方案:处理 MsmqPoisonMessageException

当服务确定某个消息是病毒消息时,排队传输会引发一个 MsmqPoisonMessageException,其中包含病毒消息的 LookupId

接收应用程序可以实现 IErrorHandler 接口,以处理应用程序要求处理的任何错误。 有关详细信息,请参阅扩展对错误处理和报告的控制

应用程序可能要求对病毒消息能进行某种自动处理,也就是将病毒消息移动至病毒消息队列,以便服务可以访问队列中的其他消息。 唯一需要使用错误处理程序机制来侦听病毒消息异常情况的情形是 ReceiveErrorHandling 设置被设置为 Fault 时。 Message Queuing 3.0 的病毒消息示例阐释了这一行为。 下面说明了处理病毒消息应执行的步骤,包括最佳方案:

  1. 确保您的病毒消息设置可以反映您的应用程序需求。 在进行设置时,确保对 Windows Vista、Windows Server 2003 和 Windows XP 之间消息队列功能的差异有所了解。

  2. 如有必要,请实现 IErrorHandler 以处理病毒消息错误。 由于将 ReceiveErrorHandling 设置为 Fault 需要一个手动机制以便将病毒消息移出队列或更正外部相关问题,因此当 IErrorHandler 设置为 ReceiveErrorHandling 时,该机制的典型用法就是实现 Fault,如下面的代码中所示。

    class PoisonErrorHandler : IErrorHandler
    {
        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            // No-op -We are not interested in this. This is only useful if you want to send back a fault on the wire…not applicable for queues [one-way].
        }
    
        public bool HandleError(Exception error)
        {
            if (error != null && error.GetType() == typeof(MsmqPoisonMessageException))
            {
                Console.WriteLine(" Poisoned message -message look up id = {0}", ((MsmqPoisonMessageException)error).MessageLookupId);
                return true;
            }
    
            return false;
        }
    }
    
  3. 创建服务行为可以使用的 PoisonBehaviorAttribute。 该行为会在调度程序上安装 IErrorHandler。 请参见下面的代码示例。

    public class PoisonErrorBehaviorAttribute : Attribute, IServiceBehavior
    {
        Type errorHandlerType;
    
        public PoisonErrorBehaviorAttribute(Type errorHandlerType)
        {
            this.errorHandlerType = errorHandlerType;
        }
    
        void IServiceBehavior.Validate(ServiceDescription description, ServiceHostBase serviceHostBase)
        {
        }
    
        void IServiceBehavior.AddBindingParameters(ServiceDescription description, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters)
        {
        }
    
        void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase serviceHostBase)
        {
            IErrorHandler errorHandler;
    
            try
            {
                errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType);
            }
            catch (MissingMethodException e)
            {
                throw new ArgumentException("The errorHandlerType specified in the PoisonErrorBehaviorAttribute constructor must have a public empty constructor", e);
            }
            catch (InvalidCastException e)
            {
                throw new ArgumentException("The errorHandlerType specified in the PoisonErrorBehaviorAttribute constructor must implement System.ServiceModel.Dispatcher.IErrorHandler", e);
            }
    
            foreach (ChannelDispatcherBase channelDispatcherBase in serviceHostBase.ChannelDispatchers)
            {
                ChannelDispatcher channelDispatcher = channelDispatcherBase as ChannelDispatcher;
                channelDispatcher.ErrorHandlers.Add(errorHandler);
            }
        }
    }
    
  4. 确保你的服务已使用病毒行为属性批注过。

另外,如果 ReceiveErrorHandling 设置为 Fault,则 ServiceHost 会在遇到病毒消息时出现错误。 您可以挂钩出错的事件,并关闭服务,采取更正措施,然后重新启动。 例如,可以记下传播到 LookupIdMsmqPoisonMessageException 中的 IErrorHandler,当服务主机出错时,您可以使用 System.Messaging API 从队列接收消息,同时使用 LookupId 从队列中移除该消息,并将该消息存储在外部存储区或其他队列中。 然后,您可以重新启动 ServiceHost 以继续正常处理。 MSMQ 4.0 中的有害消息处理演示了此行为。

事务超时和病毒消息

在排队传输通道和用户代码之间可能会出现一类错误。 这类错误可以在中间层(如消息安全层)或服务调度逻辑中检测到。 例如,无论是在 SOAP 安全层中检测到缺少 X.509 证书,还是缺少操作,都会导致不将消息调度到应用程序。 出现这种情况时,服务模型会删除该消息。 因为该消息是在事务中读取的,并且无法提供此事务的结果,所以事务最终会超时、中止,同时该消息会传送回队列中。 也就是说,对于某一类错误,事务不会立即中止,而是一直等待,直到事务超时。可以使用 ServiceBehaviorAttribute 修改服务的事务超时时间。

若要在计算机范围内更改事务超时时间,请修改 machine.config 文件并设置适当的事务超时时间。需要特别注意,根据在事务中设置的超时时间,事务最终会中止并返回到队列,其中止计数会递增。 最后,消息变为病毒消息,并按照用户的设置进行正确处理。

会话和病毒消息

会话所经历的重试和病毒消息处理过程与单个消息的经历是一样的。 前面列出的病毒消息属性适用于整个会话。 这意味着整个会话将会重试,并前进到最终病毒消息队列或发送方的死信队列(如果消息被拒绝)。

批处理和病毒消息

如果某条消息变为病毒消息,并且是某个批处理的一部分,则整个批处理将回滚,通道会返回到一次读取一条消息的状态。 有关批处理的详细信息,请参阅在事务中对消息进行批处理

对病毒队列中的消息进行病毒消息处理

只要有消息放入病毒消息队列中,病毒消息处理就不会结束。 还必须读取和处理病毒消息队列中的消息。 当从最终病毒子队列读取消息时,可以使用病毒消息处理设置的子集。 适用的设置有 ReceiveRetryCountReceiveErrorHandling。 您可以将 ReceiveErrorHandling 设置为“删除”、“拒绝”或“错误”。 如果 MaxRetryCycles 设置为“移动”,ReceiveErrorHandling 将被忽略并引发异常。

Windows Vista、Windows Server 2003 和 Windows XP 之间的差异

如上所述,并非所有病毒消息处理设置均适用于 Windows Server 2003 和 Windows XP。 下面列出了 Windows Server 2003、Windows XP 和 Windows Vista 上的消息队列在病毒消息处理方面的主要差异:

  • Windows Vista 中的消息队列支持子队列,而 Windows Server 2003 和 Windows XP 不支持子队列。 子队列用于病毒消息处理。 重试队列和病毒队列是应用程序队列的子队列,是基于病毒消息处理设置创建的。 MaxRetryCycles 用于指示要创建的重试子队列的数量。 因此,在 Windows Server 2003 或 Windows XP 上运行时,将忽略 MaxRetryCycles 并且不允许 ReceiveErrorHandling.Move

  • Windows Vista 中的消息队列支持否定确认,而 Windows Server 2003 和 Windows XP 不支持。 来自接收队列管理器的否定确认会致使发送队列管理器将被拒绝的消息放入死信队列。 因此,Windows Server 2003 和 Windows XP 不允许 ReceiveErrorHandling.Reject

  • Windows Vista 中的消息队列支持用于记录消息传递尝试次数的消息属性。 此中止计数属性在 Windows Server 2003 和 Windows XP 上不可用。 WCF 会在内存中维护中止计数,所以当场中的多个 WCF 服务读取同一消息时,此属性包含的值可能不精确。

另请参阅