Azure设计模式之管道过滤器模式

管道过滤器模式


将复杂任务分解为一系列可重用的独立元素。可通过将任务处理单元进行独立部署和伸缩来提高性能、可伸缩性和可重用性。


问题背景
应用程序需要执行不同复杂任务,每个任务包含不同的信息。一种不太灵活的实现方式是将应用程序作为一个整体模块来执行。但是如果应用程序中的其他部分需要相同的处理逻辑,这种方法就会减少重用的机会。下图说明了在单机结构下处理数据的问题。应用程序包含接收和处理来自两个源的数据。在将结果传递给应用程的业务逻辑之前,通过一个单独的模块处理每个数据源中的数据,并执行一系列任务来转换这些数据。



单机结构所执行的一些任务在功能上非常相似,但模块是各自单独设计的。如果将这些代码紧密耦合在同一模块中,就可能导致开发时很少或根本不考虑代码重用与系统的可伸缩性。但是,每个模块执行的处理任务或每个任务的部署需要都会随着业务需求的更新而变化。有些任务可能是计算密集型的,可以从强大的硬件上获益,而另一些则可能不需要如此昂贵的资源。另外,每个任务可能都会有后续需求变更,或者是任务的执行顺序发生变化。需要一个方案来解决这些问题,并提高代码重用性。


解决方案
将每个请求的处理流分解为一组相互独立的组件(或筛选器),分别执行各自任务。通过对每个组件接收和发送的数据格式标准化,这些过滤器组合起来就会成为管道。这有助于避免代码重复,并在需求更改时可以方便的删除、替换或集成其他组件。下图显示了使用了管道过滤器的解决方案。



处理单个请求所需的时间取决于管线中最慢过滤器的速度。其中一个或多个过滤器可能是一个瓶颈,特别是当来自特定数据源的流涌入大量请求时。管线结构的一个主要优点是它提供了在一些过滤器实例运行过慢的情况下,使系统能够分散负载从而提高吞吐量。组成管线的过滤器可以在不同的机器上运行,使它们能够独立缩放,并充分利用云环境中的弹性。一个计算密集型的过滤器可以在高性能硬件上运行,而其他要求较低的过滤器可以在较便宜的硬件上进行托管。过滤器不必位于相同的数据中心也不受限于地理位置,这使得管线中的每个元素都可以在就近的资源的环境中运行。下图显示了对来自数据源1中的数据使用管道过滤的示例。



如果筛选器的输入和输出结构为流,则可以将筛选器的处理逻辑并行执行。管道中的第一个筛选器可以启动其工作并输出其结果,这在第一个筛选器完成其工作之前直接传递到序列中的下一个筛选器。另一个好处是该模型可以增加系统弹性。如果筛选器实例出现故障或它正在运行的计算机不再可用,则管线可以将任务分发给该组件的另一个实例。单个筛选器的失败不一定会导致整个管线失败。


将管道和过滤器模式与补偿事务模式结合使用是实现分布式事务的另一种方法。分布式事务可以分解为单独的、可赔偿的任务,每个都可以同时实现补偿事务模式和筛选器。管道中的筛选器可以托管在数据中心就近的位置。


问题和注意事项
在决定如何实现此模式时, 应考虑以下几点:
复杂性.这种模式会带来灵活性的同时也带来了复杂性,特别是当管道中的筛选器分布在不同的服务器上时。
可靠性.基础架构需要能够确保管道中筛选器数据不会丢失。
幂等.如果管道中的筛选器在接收到消息后失败,并且任务被安排到筛选器的另一个实例,而此时部分工作可能已经完成。如果这项筛选工作更新了部分状态(如存储在数据库中的信息),需要能够保证没有副作用的重复执行。在过滤任务全部完成前,如果筛选器在将结果传递到管线中的下一个筛选器时失败,可能会发生类似的问题。在这些情况下,同样的工作可以由筛选器的另一个实例重复执行来完成,这会导致相同的结果被处理两次。从而导致后续的筛选器在管线中处理同一数据两次。因此,管道中的过滤器应设计为幂等操作。有关详细信息, 请参阅乔纳森·奥利弗的博客上的幂等模式。(http://blog.jonathanoliver.com/idempotency-patterns/)


重复消息。如果管道中的筛选器在将消息传递到管线的下一阶段时失败,则可能会调度该筛选器的另一个实例,并将同一消息的副本进行处理。这可能会导致将同一消息的两个实例传递给下一个筛选器。为了避免这一点,管道应该能够检测重复并去重。如果使用消息队列(如微软Azure服务总线队列)实现管线模式,则消息队列的基础架构会提供消息自动去重机制。


上下文状态。在管道中,每个筛选器实质上都是隔离运行的,不应该对它的调用方式有任何假设。这意味着应为每个筛选器提供足够的上下文来执行。上下文可以包含足够的状态信息。


何时使用此模式
在以下情况下考虑使用此模式:
应用程序所需的处理可以很容易地分解成一组独立的步骤。
应用程序执行的步骤有不同的伸缩性要求。
应将能够在同一进程中扩展的筛选器分在一组。关于详细信息,请参阅资源整合模式。
对任务处理步骤的灵活性有要求,可将应用程序执行的处理步骤进行重新排序,或添加和删除其中步骤。
将处理步骤分发到不同的服务器上,系统可以真正从中受益。
需要一个可靠的解决方案,在数据处理的过程中将每个步骤的失败损失降为最低。


此模式在以下情况下可能不会有用:
应用程序执行的处理步骤不是独立的,或者必须作为同一事务的一部分一起执行。
执行步骤所需的上下文或状态信息过多使这种方法效率低下。可考虑将状态信息保存到数据库中,但如果数据库上的额外负载导致过多资源争用,则不要使用这种策略。


例子
可以使用消息队列来实现管线的基础架构。消息队列用于接收未经处理的消息。第一个筛选器任务对队列中的消息进行监听,执行其任务逻辑,然后将处理后的消息传递到下一个队列。另一个筛选器任务侦听此队列上的消息并处理它们,将结果发到另一个队列,等等,直到数据全部出现在最后的队列中为止。下图说明了如何使用消息队列来实现管道。



如果要在Azure上构建解决方案,可使用服务总线队列中提供的可靠且可伸缩的排队机制。下面在c#中显示的ServiceBusPipeFilter类演示了如何实现从队列接收消息、处理消息并将结果发布到另一个队列的筛选器。ServiceBusPipeFilter类在PipesAndFilters的项目中定义与实现,可从 GitHub获得。(https://docs.microsoft.com/en-us/azure/architecture/patterns/pipes-and-filters)


public class ServiceBusPipeFilter
{
  ...
  private readonly string inQueuePath;
  private readonly string outQueuePath;
  ...
  private QueueClient inQueue;
  private QueueClient outQueue;
  ...


  public ServiceBusPipeFilter(..., string inQueuePath, string outQueuePath = null)
  {
     ...
     this.inQueuePath = inQueuePath;
     this.outQueuePath = outQueuePath;
  }


  public void Start()
  {
    ...
    // Create the outbound filter queue if it doesn't exist.
    ...
    this.outQueue = QueueClient.CreateFromConnectionString(...);


    ...
    // Create the inbound and outbound queue clients.
    this.inQueue = QueueClient.CreateFromConnectionString(...);
  }


  public void OnPipeFilterMessageAsync(
    Func<BrokeredMessage, Task<BrokeredMessage>> asyncFilterTask, ...)
  {
    ...


    this.inQueue.OnMessageAsync(
      async (msg) =>
    {
      ...
      // Process the filter and send the output to the
      // next queue in the pipeline.
      var outMessage = await asyncFilterTask(msg);


      // Send the message from the filter processor
      // to the next queue in the pipeline.
      if (outQueue != null)
      {
        await outQueue.SendAsync(outMessage);
      }


      // Note: There's a chance that the same message could be sent twice
      // or that a message gets processed by an upstream or downstream
      // filter at the same time.
      // This would happen in a situation where processing of a message was
      // completed, it was sent to the next pipe/queue, and then failed
      // to complete when using the PeekLock method.
      // Idempotent message processing and concurrency should be considered
      // in a real-world implementation.
    },
    options);
  }


  public async Task Close(TimeSpan timespan)
  {
    // Pause the processing threads.
    this.pauseProcessingEvent.Reset();


    // There's no clean approach for waiting for the threads to complete
    // the processing. This example simply stops any new processing, waits
    // for the existing thread to complete, then closes the message pump
    // and finally returns.
    Thread.Sleep(timespan);


    this.inQueue.Close();
    ...
  }


  ...
}

ServiceBusPipeFilter类中的Start方法用于连接一对输入和输出队列,Close方法用于从输入队列断开连接。OnPipeFilterMessageAsync方法执行消息的实际处理,这个方法的asyncFilterTask参数指定要执行的处理逻辑。OnPipeFilterMessageAsync方法等待输入队列中的传入消息,在消息到达时运行asyncFilterTask,并将结果传递到输出队列。队列本身由构造函数来指定。

解决方案演示了如何在一组工作角色中实现筛选器。每个工作角色都可以独立伸缩,具体取决于业务复杂性或处理所需的资源。此外,可以并行运行每个工作角色的多个实例,以提高吞吐量。


下面的代码显示了一个名为PipeFilterARoleEntr 的Azure工作角色,在PipeFilterA项目中定义。


public class PipeFilterARoleEntry : RoleEntryPoint
{
  ...
  private ServiceBusPipeFilter pipeFilterA;


  public override bool OnStart()
  {
    ...
    this.pipeFilterA = new ServiceBusPipeFilter(
      ...,
      Constants.QueueAPath,
      Constants.QueueBPath);


    this.pipeFilterA.Start();
    ...
  }


  public override void Run()
  {
    this.pipeFilterA.OnPipeFilterMessageAsync(async (msg) =>
    {
      // Clone the message and update it.
      // Properties set by the broker (Deliver count, enqueue time, ...)
      // aren't cloned and must be copied over if required.
      var newMsg = msg.Clone();


      await Task.Delay(500); // DOING WORK


      Trace.TraceInformation("Filter A processed message:{0} at {1}",
        msg.MessageId, DateTime.UtcNow);


      newMsg.Properties.Add(Constants.FilterAMessageKey, "Complete");


      return newMsg;
    });


    ...
  }


  ...
}

这个角色包含了一个ServiceBusPipeFilter对象。角色中的OnStart方法连接到队列以接收输入消息并输出消息(队列的名称在常量类中定义)。Run方法调用OnPipeFilterMessagesAsync方法对收到的每个消息执行一些处理(在本例中,通过等待很短的时间来模拟处理过程)。完成处理后,构造一个包含结果的新消息对象(在本例中,输入消息包含了一个附加的自定义属性),并将此消息发送到输出队列。


PipeFilterB项目中名为PipeFilterBRoleEntry的另一个辅助角色与PipeFilterARoleEntry类似,只是在 Run方法中执行不同的处理。在示例解决方案中,这两个角色结合起来构建一个管道,PipeFilterARoleEntry 角色的输出队列是PipeFilterBRoleEntry角色的输入队列。


示例解决方案还提供名为InitialSenderRoleEntry(在InitialSender项目中)和FinalReceiverRoleEntry(在FinalReceiver项目中)的两个附加角色。InitialSenderRoleEntry角色提供了管道中的初始消息。OnStart方法连接到一个队列,Run方法用于将方法发送到此队列。此队列是PipeFilterARoleEntry角色所使用的输入队列,将消息发送到它之后,消息会被PipeFilterARoleEntry角色接收和处理。然后经过处理的消息将通过PipeFilterBRoleEntry角色。


FinalReceiveRoleEntry角色的输入队列是PipeFilterBRoleEntry角色的输出队列。FinalReceiveRoleEntry角色中的Run方法(如下所示)接收消息并执行最终处理。然后,它将管道中的筛选器附加的自定义属性值输出到追踪日志。

public class FinalReceiverRoleEntry : RoleEntryPoint
{
  ...
  // Final queue/pipe in the pipeline to process data from.
  private ServiceBusPipeFilter queueFinal;


  public override bool OnStart()
  {
    ...
    // Set up the queue.
    this.queueFinal = new ServiceBusPipeFilter(...,Constants.QueueFinalPath);
    this.queueFinal.Start();
    ...
  }


  public override void Run()
  {
    this.queueFinal.OnPipeFilterMessageAsync(
      async (msg) =>
      {
        await Task.Delay(500); // DOING WORK


        // The pipeline message was received.
        Trace.TraceInformation(
          "Pipeline Message Complete - FilterA:{0} FilterB:{1}",
          msg.Properties[Constants.FilterAMessageKey],
          msg.Properties[Constants.FilterBMessageKey]);


        return null;
      });
    ...
  }


  ...
}

相关阅读
在实现此模式时,以下模式和指导也可能是相关的:
GitHub上提供了上例的完整代码。(https://github.com/mspnp/cloud-design-patterns/tree/master/pipes-and-filters)
竞争环境的消费者模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/competing-consumers)。管道可以包含一个或多个筛选器的多个实例。当某筛选器过慢,并行运行它的实例非常有用,使系统能够分散负载并提高吞吐量。筛选器的每个实例都会与其他实例争夺输入,筛选器的两个实例不应该能够处理相同的数据。这里有详细的说明。
资源整合模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/compute-resource-consolidation)。应将能够在同一进程中扩展的筛选器分在一组。这里提供有关此策略的详细信息。
补偿事务模式(https://docs.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction)。可以将筛选器实现为可撤销的操作,或者提供补偿操作,在发生故障时将状态恢复为以前的版本。这里解释了如何实现该模式以维护或实现最终一致性。
乔纳森奥利弗的博客中的幂等模式。(http://blog.jonathanoliver.com/idempotency-patterns/)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值