升訊威在線客服系統的併發高性能數據處理技術:PLINQ並行查詢技術

我在業餘時間開發維護了一款免費開源的升訊威在線客服系統,也收穫了許多用戶。對我來說,只要能獲得用戶的認可,就是我最大的動力。

最近客服系統成功經受住了客戶現場組織的壓力測試,獲得了客戶的認可。
客戶組織多名客服上線後,所有員工同一時間打開訪客頁面瘋狂不停的給在線客服發消息,系統穩定無異常無掉線,客服回覆消息正常。消息實時到達無任何延遲。

https://kf.shengxunwei.com/


我會通過一系列的文章詳細分析升訊威在線客服系統的併發高性能技術是如何實現的,使用了哪些方案以及具體的做法。

本篇介紹 PLINQ 並行查詢技術。

並行 LINQ (PLINQ) 是語言集成查詢 (LINQ) 模式的並行實現。 PLINQ 將整套 LINQ 標準查詢運算符實現爲 System.Linq 命名空間的擴展方法,並提供適用於並行操作的其他運算符。 PLINQ 將 LINQ 語法的簡潔和可靠性與並行編程的強大功能結合在一起。

什麼是並行查詢

一個 PLINQ 查詢的許多方面都類似於非並行的 LINQ to Objects 查詢。 與順序 LINQ 查詢一樣,PLINQ 查詢對任何內存中 IEnumerable 或 IEnumerable 數據源執行操作,並且推遲了執行,即在枚舉查詢前不會開始執行。 主要區別在於,PLINQ 會嘗試充分利用系統上的所有處理器。 方法是將數據源分區成片段,然後在多個處理器上針對單獨工作線程上的每個片段執行並行查詢。 在許多情況下,並行執行意味着查詢運行速度顯著提高。

通過並行執行,通常只需向數據源添加 AsParallel 查詢操作,PLINQ 即可顯著提升性能(與某些類型查詢的舊代碼相比)。 但是,並行可能會引入其自身的複雜性,因此並非所有的查詢操作的運行速度在 PLINQ 中都更快。 事實上,並行實際上會降低某些查詢的速度。 因此,應瞭解排序等問題將如何對並行查詢產生影響。

PLINQ 查詢性能的影響因素

下面各部分列出了並行查詢性能的一些最重要的影響因素。 這些都是一般性說明,本身並不足以用於在所有情況下預測查詢性能。

  1. 整體工作的計算成本。
    爲了實現加速,PLINQ 查詢必須有足夠多的適合並行操作來抵消開銷。 工作可以表示爲每個委託的計算成本乘以源集合中的元素數量。 假設操作可以並行執行,它的計算成本越高,加速的機會就越大。 例如,如果函數的執行時間爲 1 毫秒,那麼超過 1000 個元素的順序查詢需要 1 秒的時間才能執行此操作,而在四核計算機上,並行查詢可能只需要 250 毫秒就能完成。 這就產生 750 毫秒的加速。 如果函數執行每個元素需要 1 秒,就會產生 750 秒的加速。 如果委託成本很高,PLINQ 可能會讓速度顯著提升,前提是源集合中只有幾項。 相反,包含最簡單的委託的小型源集合通常不適合執行 PLINQ。
    在下面的示例中,queryA 可能很適合執行 PLINQ,前提是它的 Select 函數涉及很多工作。 queryB 可能不適合執行 PLINQ,因爲 Select 語句中沒有足夠多的工作,並行開銷會抵消大部分或全部加速。
var queryA = from num in numberList.AsParallel()  
             select ExpensiveFunction(num); //good for PLINQ  

var queryB = from num in numberList.AsParallel()  
             where num % 2 > 0  
             select num; //not as good for PLINQ  
  1. 系統上的邏輯內核數量(並行度)。
    這一點是上一部分的必然結果,在具有更多內核的計算機上,適合並行查詢運行得更快,這是因爲可以在更多併發線程之間劃分工作。 加速總量取決於查詢整體工作的並行度百分比。 不過,不要認爲所有查詢在八覈計算機上的運行速度都是在四核計算機上的兩倍。 優化查詢以實現最佳性能時,請務必在具有不同數量內核的計算機上度量實際結果。 這一點與第 1 點相關:需要更大的數據集,才能利用更多的計算資源。

  2. 操作的數量和種類。
    如果有必要維護源序列中的元素順序,PLINQ 提供 AsOrdered 運算符。 雖然排序有相關成本,但此成本通常還算低。 GroupBy 和 Join 操作同樣也會產生開銷。 如果允許按任意順序處理源集合中的元素,並在準備就緒後立即將它們傳遞給下一個運算符,PLINQ 的性能最佳。

  3. 查詢執行形式。
    若要通過調用 ToArray 或 ToList 存儲查詢結果,所有並行線程的結果都必須合併到一個數據結構中。 這就涉及不可避免的計算成本。 同樣,如果使用 foreach(Visual Basic 中的 For Each)循環來循環訪問結果,工作線程的結果必須串行化到枚舉器線程。 不過,如果只想根據每個線程的結果執行某操作,可以使用 ForAll 方法對多個線程執行此操作。

  4. 合併選項類型。
    PLINQ 可以配置爲緩衝輸出並在生成整個結果集後分塊區生成或一次性全部生成,也可以配置爲在各個結果生成時流式傳輸它們。 前一個導致總體執行時間減少,後一個導致所生成元素之間的延遲減少。 儘管合併選項不一定會對總體查詢性能造成重大影響,但它們可能會影響感知性能,因爲它們控制用戶在看到結果前必須等待的時間。

選擇使用模型

var source = Enumerable.Range(1, 10000);

// Opt in to PLINQ with AsParallel.
var evenNums = from num in source.AsParallel()
               where num % 2 == 0
               select num;
Console.WriteLine("{0} even numbers out of {1} total",
                  evenNums.Count(), source.Count());
// The example displays the following output:
//       5000 even numbers out of 10000 total

AsParallel 擴展方法將後續查詢運算符(在此示例中爲 where 和 select)綁定到 System.Linq.ParallelEnumerable 實現。

執行模式

默認情況下,PLINQ 是保守的。 在運行時,PLINQ 基礎結構將分析查詢的總體結構。 如果通過並行可能會提高查詢速度,PLINQ 則將源序列分區爲可以同時運行的任務。 如果並行化查詢不安全,PLINQ 則只會按順序運行查詢。 如果 PLINQ 可以在可能會較昂貴的並行算法或成本較低的順序算法之間進行選擇,它會默認選擇順序算法。 可以使用 WithExecutionMode 方法和 System.Linq.ParallelExecutionMode 枚舉指示 PLINQ 選擇並行算法。 如果你通過測試和測量知道特定查詢以並行方式執行得更快時,此做法非常有用。

並行度

默認情況下,PLINQ 使用主機計算機上的所有處理器。 可以使用 WithDegreeOfParallelism 方法指示 PLINQ 使用不超過指定數量的處理器。 當你要確保計算機上運行的其他進程收到一定的 CPU 時間量時,此做法將非常有用。 下面的片段將查詢限制爲最多使用兩個處理器。

var query = from item in source.AsParallel().WithDegreeOfParallelism(2)
            where Compute(item) > 42
            select item;

在查詢要執行大量非受計算限制的工作(如文件 I/O)的情況下,最好指定比計算機上的內核數要大的並行度。

已排序和未排序的並行查詢

在某些查詢中,一個查詢運算符必須產生保留源序列排序的結果。 爲此,PLINQ 提供了 AsOrdered 運算符。 AsOrdered 不同於 AsSequential。 儘管仍並行處理 AsOrdered 序列,但會緩衝和排序它的結果。 由於順序暫留通常涉及額外的工作,因此處理 AsOrdered 序列可能比處理默認 AsUnordered 序列更慢。 特定的已排序並行操作是否比操作的順序版本更快取決於許多因素。

下面的代碼示例演示瞭如何選擇使用順序保留。

var evenNums =
    from num in numbers.AsParallel().AsOrdered()
    where num % 2 == 0
    select num;

並行和順序查詢

某些操作要求按順序提供源數據。 必要時,ParallelEnumerable 查詢運算符自動還原爲順序模式。 對於要求順序執行的用戶定義的查詢運算符和用戶委託,PLINQ 提供了 AsSequential 方法。 使用 AsSequential 時,查詢中的所有後續運算符都會順序執行,直到再次調用 AsParallel。

異常

當一個 PLINQ 查詢執行時,可能會同時從不同的線程引發多個異常。 此外,處理異常的代碼可能與引發異常的代碼處於不同的線程上。 PLINQ 使用 AggregateException 類型封裝查詢拋出的所有異常,並將這些異常封送回調用線程。 在調用線程上,只需要一個 try-catch 塊。 不過,可以循環訪問在 AggregateException 中封裝的所有異常,並捕獲任何可以安全恢復的異常。 在極少數情況下,可能會拋出一些未在 AggregateException 中包裝、ThreadAbortException 也沒有進行包裝的異常。

如果允許異常向上冒泡回到聯接線程,則查詢也許可以在引發異常後繼續處理一些項。

自定義分區程序

在某些情況下,可以通過編寫利用源數據的某些特徵的自定義分區程序來提高查詢性能。 在查詢中,自定義分區程序本身是被查詢的可枚舉對象。

int[] arr = new int[9999];
Partitioner<int> partitioner = new MyArrayPartitioner<int>(arr);
var query = partitioner.AsParallel().Select(SomeFunction);

PLINQ 支持固定數量的分區(儘管在運行時期間爲了負載均衡可能會將數據重新動態分配到這些分區)。 For 和 ForEach 僅支持動態分區。也就是說,分區數在運行時發生變化。

ForAll 運算符

在順序 LINQ 查詢中,執行一直延遲到在 foreach(Visual Basic 中爲 For Each)循環中或通過調用 ToList、ToArray 或 ToDictionary 等方法枚舉查詢。 在 PLINQ 中,還可以使用 foreach 執行查詢以及循環訪問結果。 但是,foreach 本身不會並行運行,因此,它要求將所有並行任務的輸出合併回該循環正在上面運行的線程中。 在 PLINQ 中,在必須保留查詢結果的最終排序,以及以按串行方式處理結果時,例如當爲每個元素調用 Console.WriteLine 時,則可以使用 foreach。 爲了在無需順序暫留以及可自行並行處理結果時更快地執行查詢,請使用 ForAll 方法執行 PLINQ 查詢。 ForAll 不執行最終的這一合併步驟。 下面的代碼示例說明如何使用 ForAll 方法。 此處使用 System.Collections.Concurrent.ConcurrentBag 是因爲它已優化,可以同時添加多個線程,而無需嘗試移除任何項。

var nums = Enumerable.Range(10, 10000);
var query =
    from num in nums.AsParallel()
    where num % 10 == 0
    select num;

// Process the results as each thread completes
// and add them to a System.Collections.Concurrent.ConcurrentBag(Of Int)
// which can safely accept concurrent add operations
query.ForAll(e => concurrentBag.Add(Compute(e)));

衡量 PLINQ 性能

在很多情況下,可以並行化查詢,但是設置並行查詢的開銷可能會超出獲得的性能收益。 如果查詢不執行大量的計算,或者如果數據源較小,則 PLINQ 查詢的速度可能比順序 LINQ to Objects 查詢的速度慢。 可以在 Visual Studio Team Server 中使用並行性能分析器比較各種查詢的性能,查找處理瓶頸,以及確定查詢是並行運行還是按順序運行。


簡介

升訊威在線客服與營銷系統是一款客服軟件,但更重要的是一款營銷利器。

https://kf.shengxunwei.com/

  • 可以追蹤正在訪問網站或使用 APP 的所有訪客,收集他們的瀏覽情況,使客服能夠主動出擊,施展話術,促進成單。
    訪* 客端在 PC 支持所有新老瀏覽器。包括不支持 WebSocket 的 IE8 也能正常使用。
  • 移動端支持所有手機瀏覽器、APP、各大平臺的公衆號對接。
  • 支持訪客信息互通,可傳輸訪客標識、名稱和其它任意信息到客服系統。
  • 具備一線專業技術水平,網絡中斷,拔掉網線,手機飛行模式,不丟消息。同類軟件可以按視頻方式對比測試。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章