不正確的具現化反模式

有時候,當類別的新實例要建立一次再共用時,就會持續建立。 此行為可能會損害效能,並稱為不 適當的具現化反模式 。 反模式是週期性問題的常見回應,通常無效,甚至可能適得其反。

問題說明

許多程式庫提供外部資源的抽象概念。 在內部,這些類別通常會管理自己與資源的連線,做為用戶端可用來存取資源的訊息代理程式。 以下是與 Azure 應用程式相關的一些訊息代理程式類別範例:

  • System.Net.Http.HttpClient. 使用 HTTP 與 Web 服務通訊。
  • Microsoft.ServiceBus.Messaging.QueueClient. 將訊息張貼並接收至服務匯流排佇列。
  • Microsoft.Azure.Documents.Client.DocumentClient. 連線至 Azure Cosmos DB 實例。
  • StackExchange.Redis.ConnectionMultiplexer. 連線至 Redis,包括 Azure Cache for Redis。

這些類別旨在具現化一次,並在應用程式的整個存留期內重複使用。 不過,只有在必要時才應該取得這些類別並快速發行,這是常見的誤解。 (這裡列出的程式庫恰好是 .NET 程式庫,但此模式對 .NET 並不是唯一的。下列 ASP.NET 範例會建立 的實例 HttpClient ,以便與遠端服務通訊。 您可以在這裡 找到完整的範例

public class NewHttpClientInstancePerRequestController : ApiController
{
    // This method creates a new instance of HttpClient and disposes it for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        using (var httpClient = new HttpClient())
        {
            var hostName = HttpContext.Current.Request.Url.Host;
            var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
            return new Product { Name = result };
        }
    }
}

在 Web 應用程式中,這項技術無法調整。 系統會針對每個使用者要求建立新的 HttpClient 物件。 在負載過重的情況下,網頁伺服器可能會耗盡可用的通訊端數目,而導致 SocketException 錯誤。

這個問題不限於 類別 HttpClient 。 包裝資源或建立成本高昂的其他類別可能會導致類似的問題。 下列範例會建立 類別的 ExpensiveToCreateService 實例。 在這裡,問題不一定是通訊端耗盡,而是建立每個實例所需的時間。 持續建立和終結此類別的實例可能會對系統的延展性造成負面影響。

public class NewServiceInstancePerRequestController : ApiController
{
    public async Task<Product> GetProductAsync(string id)
    {
        var expensiveToCreateService = new ExpensiveToCreateService();
        return await expensiveToCreateService.GetProductByIdAsync(id);
    }
}

public class ExpensiveToCreateService
{
    public ExpensiveToCreateService()
    {
        // Simulate delay due to setup and configuration of ExpensiveToCreateService
        Thread.SpinWait(Int32.MaxValue / 100);
    }
    ...
}

如何修正不正確的具現化反模式

如果包裝外部資源的類別是可共用且安全線程的,請建立共用的單一實例或 類別可重複使用實例的集區。

下列範例會使用靜態 HttpClient 實例,因此會跨所有要求共用連線。

public class SingleHttpClientInstanceController : ApiController
{
    private static readonly HttpClient httpClient;

    static SingleHttpClientInstanceController()
    {
        httpClient = new HttpClient();
    }

    // This method uses the shared instance of HttpClient for every call to GetProductAsync.
    public async Task<Product> GetProductAsync(string id)
    {
        var hostName = HttpContext.Current.Request.Url.Host;
        var result = await httpClient.GetStringAsync(string.Format("http://{0}:8080/api/...", hostName));
        return new Product { Name = result };
    }
}

考量

  • 這個反模式的關鍵元素會重複建立和終結可 共用物件的實例 。 如果類別不可共用(不是安全線程),則不適用此反模式。

  • 共用資源的類型可能會決定您應該使用單一資源或建立集區。 類別 HttpClient 的設計目的是要共用,而不是共用。 其他物件可能支援共用,讓系統能夠將工作負載分散到多個實例。

  • 您在多個要求 之間共用的物件必須是 安全線程。 類別 HttpClient 的設計目的是要以這種方式使用,但其他類別可能不支援並行要求,因此請檢查可用的檔。

  • 請小心在共用物件上設定屬性,因為這可能會造成競爭條件。 例如,在每個要求可以建立競爭條件之前,先對 HttpClient 類別進行設定 DefaultRequestHeaders 。 設定這類屬性一次(例如,在啟動期間),如果您需要設定不同的設定,請建立個別的實例。

  • 某些資源類型很少,不應保留。 資料庫連接是範例。 持有不需要的開啟資料庫連線,可能會防止其他並行使用者取得資料庫的存取權。

  • 在 .NET Framework 中,許多建立與外部資源連線的物件都是使用管理這些連線之其他類別的靜態處理站方法所建立。 這些物件是要儲存和重複使用,而不是處置和重新建立。 例如,在Azure 服務匯流排中, QueueClient 物件是透過 MessagingFactory 物件建立的。 在內部,會 MessagingFactory 管理連線。 如需詳細資訊,請參閱 使用服務匯流排傳訊 改善效能的最佳做法。

如何偵測不正確的具現化反模式

此問題的徵兆包括輸送量下降或錯誤率增加,以及下列一或多個:

  • 例外狀況增加,指出通訊端、資料庫連線、檔案控制代碼等資源耗盡。
  • 增加記憶體使用量和垃圾收集。
  • 網路、磁片或資料庫活動增加。

您可以執行下列步驟來協助識別此問題:

  1. 執行生產系統的進程監視,以識別回應時間變慢或系統因資源不足而失敗的點。
  2. 檢查在這些點擷取的遙測資料,以判斷哪些作業可能會建立及終結耗用資源的物件。
  3. 在受控制的測試環境中,負載測試每個可疑的作業,而不是生產系統。
  4. 檢閱原始程式碼,並檢查訊息代理程式物件的管理方式。

查看堆疊追蹤,以尋找執行緩慢或當系統負載不足時產生例外狀況的作業。 這項資訊有助於識別這些作業如何使用資源。 例外狀況有助於判斷是否由共用資源耗盡所造成的錯誤。

診斷範例

下列各節會將這些步驟套用至稍早所述的範例應用程式。

識別速度變慢或失敗點

下圖顯示使用 New Relic APM 所產生的結果,其中顯示回應時間不佳的作業。 在此情況下, GetProductAsync 控制器中的 NewHttpClientInstancePerRequest 方法值得進一步調查。 請注意,當這些作業執行時,錯誤率也會增加。

The New Relic monitor dashboard showing the sample application creating a new instance of an HttpClient object for each request

檢查遙測資料並尋找相互關聯

下一個影像顯示使用執行緒分析擷取的資料,與上一個影像對應的相同期間內。 系統花費大量時間開啟通訊端連線,甚至有更多時間關閉它們並處理通訊端例外狀況。

The New Relic thread profiler showing the sample application creating a new instance of an HttpClient object for each request

執行負載測試

使用負載測試來模擬使用者可能執行的一般作業。 這有助於識別系統哪些部分在各種負載下遭受資源耗盡。 在受控制的環境中執行這些測試,而不是生產系統。 下圖顯示控制器處理 NewHttpClientInstancePerRequest 的要求輸送量,因為使用者負載增加至 100 個並行使用者。

Throughput of the sample application creating a new instance of an HttpClient object for each request

一開始,每秒處理的要求量會隨著工作負載增加而增加。 不過,在大約 30 位使用者中,成功的要求量達到限制,而且系統會開始產生例外狀況。 從此為止,例外狀況的數量會隨著使用者負載逐漸增加。

負載測試會回報這些失敗,因為 HTTP 500 (內部伺服器) 錯誤。 檢閱遙測顯示,這些錯誤是由系統用盡通訊端資源所造成,因為已建立越來越多的 HttpClient 物件。

下一個圖表顯示建立自訂 ExpensiveToCreateService 物件的控制器類似的測試。

Throughput of the sample application creating a new instance of the ExpensiveToCreateService for each request

這一次,控制器不會產生任何例外狀況,但輸送量仍然達到高原,而平均回應時間則增加 20。 (圖表會針對回應時間和輸送量使用對數刻度。遙測顯示,建立 的新實例 ExpensiveToCreateService 是問題的主要原因。

實作解決方案並驗證結果

切換 GetProductAsync 方法以共用單 HttpClient 一實例之後,第二個負載測試會顯示改善的效能。 未報告任何錯誤,而且系統能夠處理每秒最多 500 個要求的增加負載。 相較于先前的測試,平均回應時間減少了一半。

Throughput of the sample application reusing the same instance of an HttpClient object for each request

為了進行比較,下圖顯示堆疊追蹤遙測。 這一次,系統大部分時間執行實際工作,而不是開啟和關閉通訊端。

The New Relic thread profiler showing the sample application creating single instance of an HttpClient object for all requests

下一個圖表顯示使用 物件共用實例的 ExpensiveToCreateService 類似負載測試。 同樣地,處理的要求數量會隨著使用者負載而增加,而平均回應時間仍然很低。

Graph showing a similar load test using a shared instance of the ExpensiveToCreateService object.