共用方式為


不正確的具現化反模式

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

問題說明

許多連結庫提供外部資源的抽象概念。 在內部,這些類別通常會管理自己與資源的連線,做為用戶端可用來存取資源的訊息代理程式。 以下是與 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 方法值得進一步調查。 請注意,當這些作業執行時,錯誤率也會增加。

New Relic 監視器儀錶板,顯示針對每個要求建立 HttpClient 物件新實例的範例應用程式

檢查遙測數據並尋找相互關聯

下一個影像顯示使用線程分析擷取的數據,與上一個影像對應的相同期間內。 系統花費大量時間開啟套接字連線,甚至有更多時間關閉它們並處理套接字例外狀況。

New Relic 線程分析工具,顯示針對每個要求建立 HttpClient 物件新實例的範例應用程式

執行負載測試

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

針對每個要求建立 HttpClient 物件新實例的範例應用程式輸送量

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

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

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

針對每個要求建立 ExpensiveToCreateService 新實例的範例應用程式輸送量

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

實作解決方案並驗證結果

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

針對每個要求重複使用相同 HttpClient 物件實例的範例應用程式輸送量

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

New Relic 線程分析工具,顯示針對所有要求建立 HttpClient 物件單一實例的範例應用程式

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

圖表顯示使用 ExpensiveToCreateService 對象的共用實例進行類似的負載測試。