共用方式為


無關的擷取反模式

反模式是常見的設計缺陷,可在壓力情況下破壞軟體或應用程式,不應忽略。 在無關的擷取反模式中,會針對商務作業擷取所需的數據,通常會導致不必要的 I/O 額外負荷並降低回應性。

外部擷取反模式的範例

如果應用程式嘗試藉由擷取可能需要 的所有數據,將 I/O 要求降到最低,就 可能發生這種反模式。 這通常是對 Chatty I/O 反模式進行過度補償的結果。 例如,應用程式可能會擷取資料庫中每個產品的詳細數據。 但使用者可能只需要一部分的詳細數據(有些可能與客戶無關),而且可能不需要一次查看 所有 產品。 即使使用者正在瀏覽整個目錄,分頁結果還是有道理的,例如一次顯示 20 個。

此問題的另一個來源是遵循不良的程式設計或設計做法。 例如,下列程式代碼會使用 Entity Framework 來擷取每個產品的完整詳細數據。 然後,它會篩選結果,只傳回字段的子集,捨棄其餘字段。 您可以在這裡找到完整的範例

public async Task<IHttpActionResult> GetAllFieldsAsync()
{
    using (var context = new AdventureWorksContext())
    {
        // Execute the query. This happens at the database.
        var products = await context.Products.ToListAsync();

        // Project fields from the query results. This happens in application memory.
        var result = products.Select(p => new ProductInfo { Id = p.ProductId, Name = p.Name });
        return Ok(result);
    }
}

在下一個範例中,應用程式會擷取數據來執行資料庫可以完成的匯總。 應用程式會藉由取得所有已售出訂單的每個記錄來計算總銷售額,然後計算這些記錄的總和。 您可以在這裡找到完整的範例

public async Task<IHttpActionResult> AggregateOnClientAsync()
{
    using (var context = new AdventureWorksContext())
    {
        // Fetch all order totals from the database.
        var orderAmounts = await context.SalesOrderHeaders.Select(soh => soh.TotalDue).ToListAsync();

        // Sum the order totals in memory.
        var total = orderAmounts.Sum();
        return Ok(total);
    }
}

下一個範例顯示 Entity Framework 使用 LINQ to Entities 的方式所造成微妙的問題。

var query = from p in context.Products.AsEnumerable()
            where p.SellStartDate < DateTime.Now.AddDays(-7) // AddDays cannot be mapped by LINQ to Entities
            select ...;

List<Product> products = query.ToList();

應用程式正嘗試尋找一 SellStartDate 個多星期的產品。 在大部分情況下,LINQ to Entities 會將 子句轉譯 where 為資料庫所執行的 SQL 語句。 不過,在此情況下,LINQ to Entities 無法將 AddDays 方法對應至 SQL。 相反地,會傳回數據表中的每個數據列 Product ,並將結果篩選在記憶體中。

的呼叫 AsEnumerable 是有問題的提示。 這個方法會將結果 IEnumerable 轉換成介面。 雖然IEnumerable支持篩選,但篩選是在用戶端完成,而不是資料庫。 根據預設,LINQ to Entities 會使用 IQueryable,這會將篩選的責任傳遞給數據源。

如何修正多餘的擷取反模式

避免擷取大量數據,這些數據可能很快就會過時或可能捨棄,而且只會擷取執行作業所需的數據。

不要從數據表取得每個數據行,然後篩選它們,而是從資料庫選取您需要的數據行。

public async Task<IHttpActionResult> GetRequiredFieldsAsync()
{
    using (var context = new AdventureWorksContext())
    {
        // Project fields as part of the query itself
        var result = await context.Products
            .Select(p => new ProductInfo {Id = p.ProductId, Name = p.Name})
            .ToListAsync();
        return Ok(result);
    }
}

同樣地,請在資料庫中執行匯總,而不是在應用程式記憶體中執行匯總。

public async Task<IHttpActionResult> AggregateOnDatabaseAsync()
{
    using (var context = new AdventureWorksContext())
    {
        // Sum the order totals as part of the database query.
        var total = await context.SalesOrderHeaders.SumAsync(soh => soh.TotalDue);
        return Ok(total);
    }
}

使用 Entity Framework 時,請確定使用 介面解析 IQueryable LINQ 查詢,而不是 IEnumerable。 您可能需要調整查詢,只使用可對應至數據源的函式。 您可以重構先前的範例,以從查詢中移除 AddDays 方法,以便讓資料庫完成篩選。

DateTime dateSince = DateTime.Now.AddDays(-7); // AddDays has been factored out.
var query = from p in context.Products
            where p.SellStartDate < dateSince // This criterion can be passed to the database by LINQ to Entities
            select ...;

List<Product> products = query.ToList();

考量

  • 在某些情況下,您可以水準分割數據來改善效能。 如果不同的作業存取數據的不同屬性,水平數據分割可能會減少爭用。 通常,大部分的作業都會針對少量的數據子集執行,因此分散此負載可能會改善效能。 請參閱 數據分割

  • 對於必須支援未系結查詢的作業,請實作分頁,一次只擷取有限的實體數目。 例如,如果客戶正在瀏覽產品目錄,您可以一次顯示一頁的結果。

  • 可能的話,請利用數據存放區內建的功能。 例如,SQL 資料庫通常會提供聚合函數。

  • 如果您使用不支援特定函式的數據存放區,例如匯總,您可以將匯出結果儲存到別處,將值更新為新增或更新記錄,因此應用程式不需要在每次需要時重新計算值。

  • 如果您看到要求正在擷取大量的欄位,請檢查原始程式碼,以判斷這些欄位是否必要。 有時候這些要求是設計 SELECT * 不佳查詢的結果。

  • 同樣地,擷取大量實體的要求可能會表示應用程式未正確篩選數據。 確認需要所有這些實體。 例如,使用 SQL 中的 子句,盡可能使用 WHERE 資料庫端篩選。

  • 將處理卸除至資料庫不一定是最佳選項。 只有在資料庫設計或優化時,才使用此策略。 大部分的資料庫系統都針對某些函式進行高度優化,但並非設計為做為一般用途的應用程式引擎。 如需詳細資訊,請參閱 忙碌資料庫反模式

如何偵測無關的擷取反模式

多餘的擷取徵兆包括高延遲和低輸送量。 如果從數據存放區擷取數據,可能也會增加爭用。 終端使用者可能會報告服務逾時所造成的延長回應時間或失敗。這些失敗可能會傳回 HTTP 500(內部伺服器)錯誤或 HTTP 503(服務無法使用)錯誤。 檢查網頁伺服器的事件記錄檔,其中可能包含有關錯誤原因和情況的詳細資訊。

這個反模式和取得的一些遙測徵兆可能非常類似整合式持續性反模式遙測。

您可以執行下列步驟來協助識別原因:

  1. 藉由執行負載測試、行程監視或其他擷取檢測數據的方法,找出緩慢的工作負載或交易。
  2. 觀察系統所展示的任何行為模式。 每秒交易或用戶數量是否有特定限制?
  3. 將慢速工作負載的實例與行為模式相互關聯。
  4. 識別正在使用的數據存放區。 針對每個數據源,執行較低層級的遙測來觀察作業的行為。
  5. 識別參考這些數據源的任何緩慢執行查詢。
  6. 執行慢速執行查詢的資源特定分析,並確認數據的使用和取用方式。

尋找下列任何徵兆:

  • 對相同資源或數據存放區提出的大型 I/O 要求頻繁。
  • 共用資源或數據存放區中的爭用。
  • 經常透過網路接收大量數據的作業。
  • 應用程式和服務花費大量時間等待 I/O 完成。

診斷範例

下列各節會將這些步驟套用至先前的範例。

識別緩慢的工作負載

此圖表顯示負載測試的效能結果,該測試會模擬最多 400 位執行稍早所示方法的 GetAllFieldsAsync 並行使用者。 當負載增加時,輸送量會變慢。 工作負載增加時,平均響應時間會上升。

GetAllFieldsAsync 方法的負載測試結果

作業的 AggregateOnClientAsync 負載測試會顯示類似的模式。 要求的數量相當穩定。 平均響應時間隨著工作負載而增加,但速度比上一個圖表慢。

AggregateOnClientAsync 方法的負載測試結果

將慢速工作負載與行為模式相互關聯

高使用量和效能緩慢的一般期間之間的任何相互關聯都可能表示關注的領域。 仔細檢查懷疑執行速度緩慢的功能效能配置檔,以判斷它是否符合稍早執行的負載測試。

負載會使用以步驟為基礎的使用者載入來測試相同的功能,以找出效能大幅下降或完全失敗的點。 如果該點落在您預期的真實世界使用範圍內,請檢查功能實作方式。

慢速作業不一定是問題,如果在系統處於壓力時未執行,不是時間關鍵,也不會對其他重要作業的效能造成負面影響。 例如,產生每月作業統計數據可能是長時間執行的作業,但可能會以批處理方式執行,並以低優先順序作業執行。 另一方面,查詢產品目錄的客戶是重要的商務作業。 將焦點放在這些重要作業所產生的遙測,以瞭解效能在高使用量期間的變化。

識別工作負載緩慢中的數據源

如果您懷疑服務因為擷取數據的方式而效能不佳,請調查應用程式與使用存放庫的互動方式。 監視即時系統,以查看在效能不佳期間存取哪些來源。

針對每個數據源,檢測系統以擷取下列專案:

  • 存取每個數據存放區的頻率。
  • 進入和結束數據存放區的數據量。
  • 這些作業的時間,特別是要求的延遲。
  • 在一般負載下存取每個數據存放區時發生之任何錯誤的本質和速率。

比較這項資訊與應用程式所傳回的數據量與用戶端。 根據傳回給客戶端的數據量,追蹤數據存放區所傳回之數據量的比例。 如果有任何很大的差異,請調查以判斷應用程式是否正在擷取不需要的數據。

您可以藉由觀察即時系統並追蹤每個使用者要求的生命週期來擷取此數據,也可以建立一系列綜合工作負載的模型,並針對測試系統執行這些數據。

下圖顯示方法負載測試期間使用 New Relic APM 擷取的 GetAllFieldsAsync 遙測。 請注意從資料庫收到的數據量與對應的 HTTP 回應之間的差異。

GetAllFieldsAsync 方法的遙測

對於每個要求,資料庫會傳回80,503個字節,但對客戶端的回應只包含19,855個字節,大約是資料庫回應大小的25%。 傳回給客戶端的數據大小可能會根據格式而有所不同。 針對此負載測試,用戶端要求 JSON 數據。 使用 XML 進行個別測試(未顯示)的回應大小為 35,655 個字節,或資料庫回應大小的 44%。

方法的 AggregateOnClientAsync 負載測試會顯示更極端的結果。 在此情況下,每個測試都會執行從資料庫擷取超過 280 KB 數據的查詢,但 JSON 回應只是 14 個字節。 寬差異是因為方法會從大量數據計算匯總結果。

AggregateOnClientAsync 方法的遙測

識別和分析慢速查詢

尋找耗用最多資源的資料庫查詢,並花最多時間執行。 您可以新增檢測來尋找許多資料庫作業的開始和完成時間。 許多數據存放區也提供有關查詢執行和優化方式的深入資訊。 例如,Azure SQL 資料庫 管理入口網站中的 [查詢效能] 窗格可讓您選取查詢並檢視詳細的運行時間效能資訊。 以下是作業所產生的 GetAllFieldsAsync 查詢:

Windows Azure SQL 資料庫 管理入口網站中的 [查詢詳細數據] 窗格

實作解決方案並驗證結果

將方法變更 GetRequiredFieldsAsync 為在資料庫端使用SELECT語句之後,負載測試會顯示下列結果。

GetRequiredFieldsAsync 方法的負載測試結果

此負載測試使用相同的部署,以及與之前相同的 400 個並行用戶模擬工作負載。 此圖表顯示低得多的延遲。 回應時間會隨著負載增加約 1.3 秒,而前一個案例則為 4 秒。 相較於先前的100,輸送量每秒350個要求也較高。 從資料庫擷取的數據量現在與 HTTP 回應訊息的大小密切相關。

GetRequiredFieldsAsync 方法的遙測

使用 方法的 AggregateOnDatabaseAsync 負載測試會產生下列結果:

AggregateOnDatabaseAsync 方法的負載測試結果

平均回應時間現在最少。 這是效能的大幅改善順序,主要是因為資料庫 I/O 的大幅減少所造成。

以下是 方法的 AggregateOnDatabaseAsync 對應遙測。 從資料庫擷取的數據量大幅減少,從每筆交易超過 280 KB 到 53 個字節。 因此,每分鐘的最大持續要求數目從大約 2,000 個提高到超過 25,000 個。

AggregateOnDatabaseAsync 方法的遙測