如何处理适配器故障

通常,适配器应暂停无法处理的消息。 例如,遇到提交失败的接收适配器通常应暂停消息,尽管此决定取决于适配器的用途。 还存在有关处理失败的安全注意事项。 例如,如果适配器自动挂起所有失败的消息,则适配器可能会容易遭受拒绝服务攻击,导致 BizTalk Server 挂起队列填满。 某些适配器(如 HTTP)可以向客户端返回故障代码,指示请求已被拒绝。 对于这些类型的适配器,返回失败代码而不是挂起消息通常有意义。 通常,发送适配器在耗尽主传输和辅助传输的所有重试次数后,才会暂停消息发送。

将错误处理与单个操作相关联,而不是与包含该操作的批处理相关联

适配器中的消息批处理应该对适配器的用户不可见。 这意味着批处理中一个操作的失败不应以任何形式影响其他任何操作。 但是,批处理操作是不可分割的,因此任意一条消息的失败会导致批处理出错,并且不会处理任何操作。

编写负责处理错误、重新提交成功消息以及挂起失败消息的代码。 幸运的是,BizTalk Server 提供了详细的错误结构,使适配器能够确定失败的特定操作。 它允许创建新的批次,其中成功的操作会重新提交,而未成功的操作将被暂停。

操作的最终状态不应受到适配器内批处理的影响。

使用 SetErrorInfo 向 BizTalk Server 报告失败

如果您正在暂停消息,则必须从以前的消息上下文向 BizTalk Server 提供故障信息。 BizTalk Server 在 IBaseMessageITransportProxy 接口上使用 SetErrorInfo 方法提供错误报告功能。 可以按如下所示报告错误:

  • 处理消息时发生故障时,使用 SetErrorInfo(Exception e) 在要挂起的消息 (IBaseMessage) 上设置异常。 这样,引擎就可以保留错误消息,以便以后进行诊断,并将其记录到事件日志,以向管理员发出警报。

  • 如果在初始化或内部记账期间(而非在消息处理期间)遇到错误,你应该在初始化时传递给你的 ITransportProxy 指针上调用 SetErrorInfo(Exception e)。 如果适配器基于 BaseAdapter 实现,应始终有权访问此指针。 否则,您应该确保缓存它。

    报告其中任一方法的错误会导致将错误消息写入事件日志。 重要的是,如果你能做到的话,请务必将错误与相关消息关联起来。

处理 Database-Offline 条件

如果其中一个 BizTalk Server 数据库脱机,BizTalk 服务将自行回收。 消息传送引擎尽力在回收服务之前关闭所有接收位置。 如果此时间超过 60 秒,服务将终止。 由于引擎已事务化,因此不会导致数据丢失。

可以在注册表中使用键值来调整此超时:

DWORD   
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\BTSSvc{Host Guid}\MessagingDBFailoverShutdownTimeLimit  

对于独立适配器,由于 BizTalk Server 不拥有该过程,因此当其中一个 BizTalk Server 数据库脱机时,将禁用接收位置。 数据库重新联机后,重新启用这些接收位置。

写入事件日志

适配器可以通过 IBTTransportProxy 接口将异常传入,从而写入事件日志条目。 本机代码中开发的适配器需要传入 IErrorInfo 接口 IBTTransportProxy.SetErrorInfo(异常e)。

消息传送引擎代表适配器写入事件日志中的事件,例如当适配器在传输失败后重试消息、将消息移动到其备份传输或挂起消息时。 对于此类作,适配器只需在调用 API 之前对消息设置异常。 以下代码片段演示了这一点:

IBaseMessage msg;  
...  
// Set exception on msg to indicate why transmission failed...  
msg.SetErrorInfo(  
 new ApplicationException(  
 "The TCP connection was closed by the destination"));  

处理 Receive-Specific 批处理错误

处理接收失败

当适配器将操作(或一批操作)提交到 BizTalk Server 时,可能会有各种导致失败的原因。 两个最重要的是:

  • 接收管道失败。

  • 发布消息时发生路由失败。

    消息传送引擎在遇到接收管道故障时,会自动尝试挂起消息。 挂起操作可能并不总是成功。 例如,如果消息传送引擎在发布消息时遇到路由失败,发动机甚至不会尝试将消息暂停。

    消息总有可能失败。 在这种情况下,适配器应显式调用 MoveToSuspendQ API,并尝试将消息挂起。 当适配器尝试挂起消息时,以下情况之一应为真:

  • 适配器提交的相同消息对象(建议)应挂起。

  • 如果适配器必须创建新消息,则应使用指向最初提交的消息的消息上下文的指针来设置新消息的消息上下文。 这是因为消息的消息上下文包含有关消息和失败的大量有价值的信息。 为了调试失败的消息,需要此信息。

注释

如果适配器创建新的消息对象并挂起它,适配器应将错误消息从旧消息对象复制到新消息对象。

某些适配器,例如 BizTalk Server 提供的 HTTP 适配器,并不要求对消息进行挂起处理。 这些适配器可以将错误返回给客户端。

失败原因

失败的简单原因是可能发生在构造批处理时或调用IBTTransportBatch::Done时的错误。

  • 提交未成功 提交调用可能由于有限的原因而失败,所有这些调用都是致命的。 其原因包括:

  • BizTalk Server 进程空间内发生的内存不足错误。

  • 架构程序集已从项目部署中移除。 在这种情况下, 提交 失败并出现神秘错误。 在 MQSeries 适配器中,捕获 BizTalk Server 的常规故障异常,并在系统事件日志中写入扩展错误消息。 此消息表明,错误的可能原因之一是架构程序集已从部署中删除。

    通常,如果 提交 失败,则应尝试使用同一事务暂停消息。

  • IBTTransportBatch::执行失败。 由于几个原因之一,IBTTransportBatch::Done 调用可能会失败。 通常,您应该始终尝试进行一次挂起操作,仅当该挂起操作失败时才结束事务。 从 IBTTransportBatch::Done 方法失败时您可能收到的错误代码之一是 BizTalk Server 正在尝试关闭。 在这种情况下,应只结束事务并保留该事务,因为 终止 调用可能同时发生。 当你成功构造批处理并成功执行IBTTransportBatch::Done时,会出现其他场景。 在这些情况下,错误会在 BatchComplete 中返回,适配器必须决定如何处理它们。 本部分的其余部分将处理此案例。

处理 BatchComplete 错误信息

BatchComplete 是由 BizTalk Server 调用的适配器提供的回调,用于指示批处理作的完成状态。

传递给 BatchComplete 的最重要参数是批处理状态 hResult。 这表示批处理的成功或失败。 如果批处理失败,则表示批处理中没有任何操作成功。 适配器将经历批处理状态结构并确定哪些消息失败(这称为 筛选批处理)。

非事务性批量完成错误

对于非事务适配器,如果 SubmitMessage/SubmitRequestMessageSubmitResponseMessage 操作发生故障,您必须选择响应。 通常适配器通过调用 MoveToSuspendQ 来挂起消息。

以下操作始终应能通过:DeleteMessageMoveToSuspendQResubmitMessage。 如果这些操作失败,通常意味着适配器中存在漏洞。 在这些情况下,无需编写代码来处理故障。 但是,如果批处理因为另一个操作失败而失败,则必须在新的批处理中重新执行这些操作。

如果适配器调用 MovetoBackupTransport 且失败(因为没有备份传输),则适配器应调用 MoveToSuspendQ 来暂停消息。

事务批处理完成错误

使用适配器创建的事务提交至 BizTalk Server 的批处理时,您可以选择以下两个操作方案之一:

  • 使用单消息批处理。 将单个消息批发送到 BizTalk Server。 如果单个消息失败,那么您可以在同一事务下合法地向 BizTalk Server 发送第二批消息,但必须将有问题的消息移至挂起队列,而不是重新提交该消息。 删除失败的消息后,第二批的提交应成功。 这个流程完成后,在 BizTalk Server 确认第二批成功时就可以提交事务。 如果第二批失败,适配器必须中止事务,或找到其他位置来放置该消息。 在此情境中,由于事务回滚处理,你立即遭遇显著的性能下降。

    有一些技术可用于提高适配器的性能。 例如,MQSeries 适配器在运行时动态调整其方法。 它以每批 100 条消息的方式运行。 遇到错误时,必须结束批处理,但在短时间内系统会切换到单独处理消息,并在处理完错误消息后继续批处理。 然后,它将恢复到 100 条消息批处理。 如果再次遇到错误,它将再次变慢。

  • 使用预先挂起。 构造一个包含多个消息的批次,其中错误消息被预先暂停。 该批包含SubmitMoveToSuspendQ操作的组合,并且是该事务下的第一批也是唯一的一批。 它应该成功,因为错误的数据被抢先挂起,并且事务可以提交(等待从 BizTalk Server 接收确认后)。

    这似乎需要研究未来,但此技术已在 MSMQ 适配器中使用。 这取决于具有可靠的唯一消息 ID。 此适配器构造一批消息。 如果出现任何故障,它将回滚事务(因此是批处理),但会记住临时数据结构中的消息 ID。 (为了防止此结构无限期增长,在一些固定的时间延迟后,会删除其中的项。在提交每个批处理之前,适配器会检查错误消息 ID 的列表。 如果看到消息,它知道该消息将失败(因为它在过去失败了一次),并抢先暂停它,而不是尝试提交它。

    并非每个适配器都有可靠的唯一消息 ID,而事务性存储更不太可能具备这种 ID。 因此,许多事务适配器仅限于发送单消息批处理。

处理其他错误

在所有其他的情况下(如消息挂起失败),适配器必须结束事务。 任何其他结果都会导致消息重复或删除。

每当适配器可以时,如果批处理失败,它应中止事务。 但是,在某些情况下,适配器无法中止事务。 在这种情况下,它应使用相同的事务挂起消息。

事务性接收处理错误

常见的事务处理模式是在发生错误时结束事务。 在这种情况下,所有内容都将返回到其以前的状态,并且不会丢失任何数据。 但是,如果要使用事务源中的数据(例如,从数据库中的临时表一次拉取一行,或者从 MQSeries 或 MSMQ 等队列产品一次拉取一条消息),则这可能是不够的。 如果只是结束事务并再次返回并选取相同的数据,则可能发生相同的错误,并且系统陷入重复循环。

在早期版本的 BizTalk Server 中,SQL 适配器带有这种行为。 但是,发布后不久,适配器行为就被更改为尝试暂停失败的消息并提交事务。 将消息移到同一事务下的挂起队列,然后提交事务能够避免数据丢失,并且还允许适配器跳过不良数据。

当适配器的接收部分接收到响应提交消息操作的错误消息时,适配器应处理该错误,并将消息移动到挂起队列。

如果适配器创建了事务对象并在事务下提交消息,则适配器应在发生故障时将消息合理地移到同一事务下的挂起队列中。 该事务可确保不会删除数据,甚至导致错误的数据也不应删除。

管理未订阅的消息

如果没有定义接受消息的订阅,BizTalk Server 不接受在其 MessageBox 数据库中发布的消息。 订阅由编排或发送端口注册。 可以定义多个订阅,在这种情况下,消息会发送到多个目标。 如果没有订阅,BizTalk Server 将拒绝该消息,并且不会尝试暂停该消息。 如果适配器未处理此错误并显式挂起消息,则会删除该消息,并且其数据可能会丢失。 当然,事务适配器可能会结束事务并将消息返回到其目标。

支持使用接收流进行查找

接收端流必须支持Seek方法,以便 BizTalk Server 能够在管道故障时挂起消息。 如果消息流不可查找,则 BizTalk Server 在尝试运行 Seek 时会生成错误。

在许多情况下,支持 Seek 并不容易。 例如,从网络流式传输数据时,可能很难返回到网络资源并再次请求数据。

多个随 BizTalk Server 提供的适配器在 BizTalk Server 读取数据时,将消息数据同时存储到磁盘上的文件中。 这使适配器在遇到错误时可以在该文件上使用Seek(例如,在消息数据的管道处理中)。 在内部,适配器使用 ReadOnlySeekableStream 类,该类包装传入的不可查找流并在达到可配置的大小阈值时溢出到磁盘。 对于小于阈值大小的消息,磁盘永远不会命中。

请考虑 User-Configurable Error-Handling 选项

有时一个错误没有唯一正确的响应。 在这种情况下,应考虑用户可配置的选项以在行为之间进行选择。 MQSeries 适配器执行此作。

适配器在检测到错误时挂起消息的问题在于,BizTalk Server 中的挂起队列就像一个“黑洞”。消息进入队列相对简单,但要再次取出就更加困难。

适配器的某些用户可能不希望有任何内容在挂起队列中。 例如,对于 MQSeries 适配器,向用户提供一个配置选项以执行以下动作之一:

  • 将适配器设置为结束当前事务,并在看到错误时禁用自身。

  • 暂停失败的消息并提交事务。 即使 BizTalk Server 已成功挂起消息,适配器也会继续进行这一操作。 此操作满足客户要求,即使它会导致事件日志不完全准确。

使用单个线程并等待 BatchComplete 实现接收排序

BizTalk Server 的接口旨在通过支持并发实现性能和横向扩展的能力。 但是,如果需要严格有序地接收消息(有时需要从 MQSeries 或 MSMQ 等消息队列产品接收消息),则必须在适配器中执行一些额外的工作来禁用某些并发。 这可以通过两个步骤完成:

  1. 必须为适配器中的所有数据处理使用单个线程。

  2. 必须等待 BizTalk Server 完全处理每个批处理。 此要求非常重要,可以使用 .NET 线程同步基元来实现。 例如,使用 AutoResetEvent,可以:

    • 声明可由主工作线程和 BatchComplete 回调对象访问的事件对象。

    • 在主工作线程上,像往常一样将消息提交到批处理,但在调用批处理IBTTransportBatch::Done之前,对事件对象调用AutoResetEvent.Reset

    • 从同一线程对事件对象调用 AutoResetEvent.WaitOne 。 这会导致主工作线程阻塞。 在 BizTalk Server 的 BatchComplete 回调中,对同一事件对象调用 AutoResetEvent.Set 解除工作线程的阻塞,使其准备好处理另一条消息。

    强烈建议将这样的 接收顺序 配置为可配置,因为它会导致显著的性能下降。 许多(如果不是大多数)用户方案不需要对消息进行排序。 暂停消息还可以中断排序。 在这种情况下,具体做法是与应用程序相关,因此适配器最好的办法是为用户提供一个配置点。

    在有序方案中,一些客户表示他们宁愿停止处理,即禁用适配器,而不是中断排序。 支持有序接收的 MQSeries 适配器向用户提供此选项。

处理 Send-Specific 批处理错误

处理发送重试和批处理

下面是发送端批处理的典型示例:

  • BizTalk Server 向适配器提供一批消息。

  • 当适配器确定它已将消息正确提供给其目标时,它会在 BizTalk Server 上执行删除操作,指示它已完成。 (照常,可以任意批处理多个删除消息以提高性能。

    如果发送方适配器无法处理消息,则可能使用该消息执行以下几个作之一:

  • 适配器应告知 BizTalk Server 它想要重试消息。 BizTalk Server 不会自动重试消息。 BizTalk Server 保留重试的计数,可以在消息上下文中看到此计数。

  • 适配器可以确定它无法处理消息。 在这种情况下,适配器可能会将消息移动到下一个传输。 适配器使用 Batch 对象的 MoveToNextTransport 调用执行此作。

  • 适配器可将消息移动到暂停队列。

    适配器确定如何处理消息。 但是,建议确保适配器行为一致,因为这会使 BizTalk Server 安装更易于支持。

    强烈建议适配器的行为应如下所述。 BizTalk Server 附带的适配器的行为如下所示。

发送适配器接收一些消息并将其提交到 BizTalk Server。

对于每个成功的消息,适配器应删除 BizTalk Server 上的该消息。 所有发回 BizTalk Server 的通信都是通过批处理完成的,并且可以对删除进行批处理。 它们不必与 BizTalk Server 在适配器上创建的一批相同。 如果有任何响应消息(如在 SolicitResponse 方案中),则应将其返回提交到 BizTalk Server(使用 SubmitResponse),以及执行删除操作。

  • 如果适配器中的消息处理失败,请检查重试计数。

    • 如果未超过重试计数,请将消息重新提交到 BizTalk Server,记得在消息上设置重试时间。 消息上下文提供重试计数,以及适配器应使用的重试间隔。

    • 如果超过了重试计数,则适配器应尝试使用 MoveToNextTransport 移动消息。 重新提交和 MoveToNextTransport 消息可以与同一批中的删除消息混合回 BizTalk Server。 这不是必需的,但可能是一个有用的步骤。

  • 重新提交和 MoveToNextTransport 是适配器处理故障的方法。 但是,在处理失败的过程中可能会出现失败。 在这种情况下,在处理 BizTalk Server( BatchComplete 方法)的响应时,适配器必须针对 BizTalk Server 创建另一个批处理,以指示如何处理该失败。

    处理另一故障过程中发生的失败时,请按照以下步骤:

    • 如果重新提交失败,请使用 MoveToNextTransport

    • 如果 MoveToNextTransport 失败,请使用 MoveToSuspendQ

    您必须在 BizTalk Server 上继续创建批处理,直到在 BizTalk Server 上收到成功操作的反馈。

消息上下文属性的序列化

分配给消息上下文属性的所有对象都必须可序列化。 否则,消息引擎将引发 E_NOINTERFACE类型的异常。 此返回值不明确表示尝试为消息上下文分配的非可序列化对象。