Pipe和Filter模式將執行複雜處理的任務分解為可重複使用的一系列離散元素。這種模式可以提高效能,可擴充性和可重用性,允許執行部署和縮放獨立處理的任務元素。 問題
一個應用程式可能根據其處理的不同的資訊需要執行各種複雜的任務。一個簡單的,但不靈活的方法就是可以將應用的處理作為一個單獨的模組。但是,如果部分相同的處理需要在應用程式的其他地方,這種方法可能會減少代碼重構,重用,最佳化的機會。
下圖1說明了單獨模組方法處理資料的流程。應用程式接收和處理來自兩個源的資料。每個源的資料由一個單獨的模組處理,它們執行一系列任務來轉換這些資料,然後將結果傳遞給應用程式的商務邏輯。
圖1.
實現獨立模組的解決方案
很多獨立模組執行的一些任務在功能上非常相似,但是因為這些模組是分開設計的,實現任務的代碼都是緊密耦合在一個模組中,其中的重複部分無法得到重用來提高擴充性和重用性。
然而,每個模組執行的處理任務,或每個任務的部署要求,可能都會隨著業務需求的修改而改變。有些任務可能是計算密集型的,可能會受益於運行在強大的硬體,而其他任務可能不需要這樣昂貴的資源。此外,在未來可能需要執行一些額外的處理,或命令執行的任務可能會改變。所以需要一個技能解決這些問題,同時也能增加代碼重用的解決方案。 解決方案
將每個資料流所需的處理分解成一組離散的組件(或過濾器),然後由每個組件來執行一個任務是一種不錯的解決方案。通過標準化每個組件接收和發出的資料的格式,這些組件(過濾器)可以組合成一個管道。這種解決方案有助於避免重複代碼,並且在需求發生變化的時候,可以很容易地刪除,替換或整合額外的組件來實現功能。圖2顯示了這種結構的一個例子。
圖2
通過管道和過濾器的解決方案
處理單個請求的時間取決於管道中最慢的過濾器的速度。尤其在大量請求發送到組件的時候,某個或者某些組件就可能成為系統的效能瓶頸。管道結構的一個主要優點就在於,它為運行緩慢的過濾器提供了使用並行執行個體的機會,這樣使系統能夠均衡負載,以提高輸送量。
組成管道的過濾器完全可以運行在不同的機器上,並且它們可以利用許多雲環境提供的彈效能夠來實現獨立擴充。一個計算密集型的過濾器可以運行在高效能硬體上,而其他要求較低的過濾器可以運行在稍差的硬體上。過濾器甚至不必在相同的資料中心或不在同一個地點,管道解決方案允許管道中的每個元素可以在接近其所需資源的環境中運行。
圖3展示了一個應用了管道處理資料流的例子:
圖3
管道中組件的負載分擔
如果一個過濾器的輸入和輸出被結構化為一個流,它就有可能對多個過濾器進行平行處理。管道中的第一個過濾器可以開始它的工作,並開始分發它的處理結果,這是直接傳遞到下一個過濾器的序列之前,第一個過濾器已完成其工作。
另一個好處是管道過濾器模式可以提供很好的跳轉。如果過濾器失敗或正在啟動並執行機器不再可用,則管道可以重新安排過濾器正在執行的工作,並將此工作直接指向組件的另一執行個體。一個過濾器的故障並不會導致整個管道的故障。
使用管道和過濾器模式與事務補償模式相結合,可以提供一種替代的方法來實現分散式交易。分散式交易可以分解成獨立的有償任務,每一個都可以通過使用一個過濾器,還實現了事務補償模式。管道中的過濾器可以作為獨立的託管任務來執行,這些任務還可以在物理位置上接近他們所維護的資料,以降低網路代價。 實現管道過濾器模式需要考慮的問題
開發人員在考慮實現Pipe和Filter模式的時候,需要考慮以下一些方面: 複雜性。該模式在增加靈活性的同時,也會在複雜性上面帶來對應的代價。尤其是當管道中的過濾器分布在不同的伺服器的時候。 可靠性。最好使用一個架構技術以確保管道中過濾器之間的資料流不會丟失。 等冪性。如果管道中的某個過濾器在接收訊息處理的時候過程中失敗了,並且重新導向到另一個新的過濾的時候,其中的部分工作可能已經完成了。如果這部分工作包含了更新某些全域狀態(比如資料庫中儲存的一些資訊),同樣的更新操作可能會重複執行,這樣會帶來不一致問題。同樣的,當過濾器將其中的一個處理結果發送到下一個過濾器中的過程中發生了類似的錯誤的話,但是失敗的過濾器對於請求的處理可能已經完成了。在這些情況下,所有的工作必須保證能夠在重複執行的情況下,也能夠保證其結果的一致。當然,這樣也可能會令其中的某些過濾器針對同一請求執行多次。因此,在對管道進行設計的時候,必須要保證其等冪性。想瞭解更多的內容,可以參考Jonathan Oliver的部落格等冪模式.
重複的訊息。如果管道中的一個過濾器在將處理之後的訊息發送到下一個過濾器失敗的情況下,另一個過濾器會啟動(如上面對於等冪性的方面的考慮),那麼該過濾器會將發送額外的一份訊息的副本給管道。這可能會造成一個訊息的兩個相同執行個體發送給了下一個過濾器。為了避免這一情況,管道應該檢測以消除重複的訊息。
如果開發人員在通過訊息佇列來實現管道技術(比如Windows Azure服務匯流排隊列),訊息佇列架構是提供了自動探索消除重複訊息的功能的。
上下文和狀態。在管道中,每一個過濾器都處於一個獨立的運行狀態,並且不對調用的方式作出任何假設的。這也意味著,每個過濾器必須知道一個訊息的足夠的上下文資訊,來滿足其處理的最低需求。這個上下文資訊是必須包含了相當多的狀態才足夠的。 何時使用該模式
在以下一些情境的時候,可以考慮使用管道過濾器模式: 整個應用的處理可以被分解成一系列離散的,獨立的步驟的情況下,可以考慮使用管道過濾器模式。
當應用處理的不同的步驟,有不同的擴充性需求的情況下,可以考慮實現管道過濾器模式。
當然,將不同的過濾器組合在一起,來統一進行擴充也是可以的。想瞭解更多這方面的資訊,可以參考Compute Resource Consolidation模式
如果需要較強的靈活性的時候,可以考慮使用管道過濾器模式。管道過濾器模式令應用可以靈活的配置不同的執行步驟和執行序列,而且應用還可以按需來增加或者減少對應的處理單元。 當需要最大化伺服器利用率的時候,可以選擇管道過濾器模式。 如果解決方案需要保證的可靠,需要盡量降低每一個單獨的處理單元中執行的失敗的可能性的時候,可以考慮使用管道過濾器模式。
d管道過濾器模式在以下一些情境可能不是十分適用: 在應用中處理的一些步驟如果不是等冪的,或者這些步驟必須作為事務的一部分來執行的時候。 一個過濾器執行單元在進行處理的時候,需要的內容相關的數量或者狀態資訊已經令其效率低下的時候,不適合使用管道過濾器模式。 使用舉例
開發人員可以選擇使用訊息佇列架構來實現管道過濾器模式。訊息佇列不斷的接收那些沒有處理的訊息。然後令所有的過濾器組件監聽訊息佇列的訊息。當有新的訊息來得時候,執行其工作,然後不斷的消費訊息,並且將處理後的訊息添加到下一個隊列之中。然後另一個類似的任務同樣監聽訊息佇列的訊息進行處理,直到整個管道中的過濾器都完成了處理為止。
圖4
通過訊息佇列實現管道過濾器模式
如果開發人員是基於Windows Azure來開發的話,可以使用Windows Azure服務匯流排隊列來為解決方案提供一個可靠的可擴充的隊列機制。下面的ServiceBusPipeFilter類就是這樣的一個例子。下面的代碼展示了一個過濾器如何從隊列中接收訊息,處理訊息,並將結果發送到另一個隊列之中。
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 does not 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 is a chance that the same message could be sent twice // or that a message may be 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 is 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參數來對接收到的訊息進行處理,然後將結果發布到輸出隊列。隊列本身都是通過建構函式來指定的。
一般的解決方案都是將過濾器當做一組工作單元的方式來實現的。每一個工作單元都可以獨立的進行伸縮,這都取決於業務的複雜性和其所需要執行處理需要的資源的多少。而且,可以通過對過濾器的並存執行來提高吞吐。下面的代碼展示了一個名為PipeFilterARoleEntry的Windows Azure的工作單元。
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, ...) // are not 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方法會串連接收訊息的輸入隊列和接收處理完成訊息的輸出隊列(定義在Constats類中)。而Run方法會調用OnPipeFilterMessagesAsync來對每一個接收到的訊息進行一些處理(在該樣本中,只是簡單的等待一段時間)。當處理完成的時候,會產生一個新的訊息(在該樣本中,只是在屬性中增加了部分資訊),然後發送到輸出隊列。
開發人員也可以根據範例程式碼定義新的RoleEntryPoint實現。在實現上,基本不會有太多的差別,只是在其中的Run方法中處理略有不同。然後將不同的工作單元串連起來就是一個管道過濾器模式的實現了。參考如下代碼:
public class FinalReceiverRoleEntry : RoleEntryPoint{ ... // Final queue/pipe in the pipeline from which to process data. 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; } ); ... } ...}
相關的其他模式
在實現管道過濾器模式的時候,可以參考如下一些其他的模式: Competing Consumers模式.每個管道可能會包含一個或者更多的過濾器組件。該方法在考慮將過濾器組件並存執行的時候是不錯的參考。競爭消費者模式可以令多個組件很好的分擔負載提高輸送量。每個過濾器的案例都可以參與競爭,同時過濾器的多個執行個體還需要保證不會處理到同一個訊息。競爭消費者模式對一些細節進行了詳細的描述。 Compute Resource Consolidation模式.有的時候,可以考慮將多個過濾器組合到一起來一起進行擴充,統一進行伸縮處理。而競爭資源合并模式對該情況在更多細節上提出了一些優劣方面的考慮和權衡。 Compensating-Transaction模式.一個過濾器可以作為一個處理單元來處理,也可能在有些時候需要執行復原,或者有對應的一些補償的操作來恢複之前訊息的狀態。補償事務模式描述了如何將組件配置實現以保證最終一致性。