無關的擷取反模式

反模式是常見的設計缺陷,可在壓力情況下破壞軟體或應用程式,不應忽略。 在無關的擷取反模式 中,會針對商務作業擷取所需的資料,通常會導致不必要的 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 並行使用者。 當負載增加時,輸送量會變慢。 工作負載增加時,平均回應時間會上升。

Load test results for the GetAllFieldsAsync method

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

Load test results for the AggregateOnClientAsync method

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

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

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

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

識別工作負載緩慢中的資料來源

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

針對每個資料來源,檢測系統以擷取下列專案:

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

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

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

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

Telemetry for the GetAllFieldsAsync method

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

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

Telemetry for the AggregateOnClientAsync method

識別和分析慢速查詢

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

The Query Details pane in the Windows Azure SQL Database management portal

實作解決方案並驗證結果

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

Load test results for the GetRequiredFieldsAsync method

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

Telemetry for the GetRequiredFieldsAsync method

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

Load test results for the AggregateOnDatabaseAsync method

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

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

Telemetry for the AggregateOnDatabaseAsync method