沒有快取反模式

反模式是常見的設計缺陷,可在壓力情況下破壞軟體或應用程式,不應忽略。 當處理許多並行要求的雲端應用程式重複擷取相同的資料時,不會發生任何快取反模式 。 這可以降低效能和延展性。

未快取資料時,可能會導致許多不想要的行為,包括:

  • 在 I/O 額外負荷或延遲方面,重複從資源擷取相同資訊,而資源成本高昂。
  • 針對多個要求重複建構相同的物件或資料結構。
  • 對具有服務配額的遠端服務進行過多呼叫,並將用戶端節流超過特定限制。

反過來,這些問題可能會導致回應時間不佳、資料存放區中的爭用增加,以及延展性不佳。

沒有快取反模式的範例

下列範例會使用 Entity Framework 連線到資料庫。 即使有多個要求擷取完全相同的資料,每個用戶端要求都會產生對資料庫的呼叫。 重複要求的成本,就 I/O 額外負荷和資料存取費用而言,可能會快速累積。

public class PersonRepository : IPersonRepository
{
    public async Task<Person> GetAsync(int id)
    {
        using (var context = new AdventureWorksContext())
        {
            return await context.People
                .Where(p => p.Id == id)
                .FirstOrDefaultAsync()
                .ConfigureAwait(false);
        }
    }
}

您可以在這裡 找到完整的範例

此反模式通常會發生,因為:

  • 不使用快取會比較容易實作,而且可在低負載下正常運作。 快取可讓程式碼變得更複雜。
  • 目前還不清楚使用快取的優點和缺點。
  • 對於維護快取資料精確度和新鮮度的額外負荷感到擔憂。
  • 應用程式已從內部部署系統移轉,其中網路延遲不是問題,而且系統在昂貴的高效能硬體上執行,因此在原始設計中不會考慮快取。
  • 開發人員不知道快取在指定的案例中是可能的。 例如,開發人員在實作 Web API 時可能不會考慮使用 ETag。

如何修正無快取反模式

最受歡迎的快取策略是 隨選 另行 快取策略。

  • 在讀取時,應用程式會嘗試從快取讀取資料。 如果資料不在快取中,應用程式會從資料來源擷取資料,並將它新增至快取。
  • 在寫入時,應用程式會將變更直接寫入資料來源,並從快取中移除舊的值。 它會在下次需要時擷取並新增至快取。

此方法適用于經常變更的資料。 以下是先前更新為使用 Cache-Aside 模式的 範例。

public class CachedPersonRepository : IPersonRepository
{
    private readonly PersonRepository _innerRepository;

    public CachedPersonRepository(PersonRepository innerRepository)
    {
        _innerRepository = innerRepository;
    }

    public async Task<Person> GetAsync(int id)
    {
        return await CacheService.GetAsync<Person>("p:" + id, () => _innerRepository.GetAsync(id)).ConfigureAwait(false);
    }
}

public class CacheService
{
    private static ConnectionMultiplexer _connection;

    public static async Task<T> GetAsync<T>(string key, Func<Task<T>> loadCache, double expirationTimeInMinutes)
    {
        IDatabase cache = Connection.GetDatabase();
        T value = await GetAsync<T>(cache, key).ConfigureAwait(false);
        if (value == null)
        {
            // Value was not found in the cache. Call the lambda to get the value from the database.
            value = await loadCache().ConfigureAwait(false);
            if (value != null)
            {
                // Add the value to the cache.
                await SetAsync(cache, key, value, expirationTimeInMinutes).ConfigureAwait(false);
            }
        }
        return value;
    }
}

請注意, GetAsync 方法現在會呼叫 CacheService 類別,而不是直接呼叫資料庫。 類別 CacheService 會先嘗試從 Azure Cache for Redis 取得專案。 如果在快取中找不到值,呼叫 CacheService 端會叫用傳遞給它的 Lambda 函式。 Lambda 函式負責從資料庫擷取資料。 此實作會將存放庫與特定快取解決方案分離,並將 與 資料庫分離 CacheService

快取策略的考慮

  • 如果快取無法使用,可能是因為暫時性失敗,請勿將錯誤傳回用戶端。 相反地,從原始資料來源擷取資料。 不過,請注意,在復原快取時,原始資料存放區可能會被要求淹沒,導致逾時和失敗的連線。 (畢竟,這是一開始使用快取的動機之一。使用斷路器模式 之類的 技術,以避免造成資料來源壓倒性。

  • 快取動態資料的應用程式應該設計成支援最終一致性。

  • 針對 Web API,您可以在要求和回應訊息中包含 Cache-Control 標頭,以及使用 ETag 來識別物件的版本,以支援用戶端快取。 如需詳細資訊,請參閱 API 實作

  • 您不需要快取整個實體。 如果大部分實體是靜態的,但只有一小段經常變更,請快取靜態元素,並從資料來源擷取動態元素。 這種方法有助於減少針對資料來源執行的 I/O 數量。

  • 在某些情況下,如果動態資料短期存在,則快取資料可能會很有用。 例如,請考慮持續傳送狀態更新的裝置。 在資訊送達時快取此資訊並完全不寫入永續性存放區可能很合理。

  • 為了防止資料變得過時,許多快取解決方案都支援可設定的到期期限,以便在指定的間隔之後自動從快取中移除資料。 您可能需要調整案例的到期時間。 高度靜態的資料在快取中停留的時間比可能很快過時的揮發性資料更長。

  • 如果快取解決方案未提供內建到期日,您可能需要實作偶爾會清除快取的背景進程,以防止其成長而不受限制。

  • 除了從外部資料源快取資料之外,您還可以使用快取來儲存複雜計算的結果。 不過,在您這樣做之前,請先檢測應用程式,以判斷應用程式是否真的是 CPU 系結。

  • 在應用程式啟動時,對快取進行質素可能很有用。 使用最有可能使用的資料填入快取。

  • 一律包含偵測快取叫用和快取遺漏的檢測。 使用這項資訊來微調快取原則,例如要快取的資料,以及在快取到期前保留資料的時間長度。

  • 如果缺少快取是瓶頸,則新增快取可能會增加要求數量,讓 Web 前端變得多載。 用戶端可能會開始收到 HTTP 503(服務無法使用)錯誤。 這些表示您應該相應放大前端。

如何偵測沒有快取反模式

您可以執行下列步驟,以協助識別缺乏快取是否會導致效能問題:

  1. 檢閱應用程式設計。 清查應用程式所使用的所有資料存放區。 針對每個,判斷應用程式是否使用快取。 可能的話,請判斷資料變更的頻率。 快取的良好初始候選項目包括變更緩慢的資料,以及經常讀取的靜態參考資料。

  2. 檢測應用程式並監視即時系統,以瞭解應用程式擷取資料或計算資訊的頻率。

  3. 在測試環境中分析應用程式,以擷取與資料存取作業或其他經常執行的計算相關聯的額外負荷低階計量。

  4. 在測試環境中執行負載測試,以識別系統在一般工作負載和負載過重下如何回應。 負載測試應模擬使用實際工作負載在生產環境中觀察到的資料存取模式。

  5. 檢查基礎資料存放區的資料存取統計資料,並檢閱重複相同資料要求的頻率。

診斷範例

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

檢測應用程式並監視即時系統

檢測應用程式並加以監視,以取得使用者在應用程式處於生產環境中時所提出之特定要求的相關資訊。

下圖顯示 New Relic 在負載測試期間擷取的 監視資料。 在此情況下,唯一執行的 HTTP GET 作業是 Person/GetAsync 。 但在即時生產環境中,瞭解每個要求執行的相對頻率,可讓您深入瞭解應該快取哪些資源。

New Relic showing server requests for the CachingDemo application

如果您需要更深入的分析,您可以流量分析工具在測試環境中擷取低階效能資料(而非生產系統)。 查看 I/O 要求速率、記憶體使用量和 CPU 使用率等計量。 這些計量可能會顯示對資料存放區或服務的大量要求,或重複執行相同計算的處理。

負載測試應用程式

下圖顯示範例應用程式負載測試的結果。 負載測試會模擬最多 800 位使用者執行一系列一般作業的步驟負載。

Performance load test results for the uncached scenario

每秒執行的成功測試數目達到高原,因此其他要求會變慢。 隨著工作負載,平均測試時間會穩步增加。 一旦使用者載入尖峰,回應時間就會關閉。

檢查資料存取統計資料

資料存放區所提供的資料存取統計資料和其他資訊可以提供有用的資訊,例如最常重複的查詢。 例如,在 Microsoft SQL Server 中 sys.dm_exec_query_stats ,管理檢視具有最近執行的查詢的統計資料。 檢視中提供每個查詢的 sys.dm_exec-query_plan 文字。 您可以使用 SQL Server Management Studio 之類的工具來執行下列 SQL 查詢,並判斷查詢的執行頻率。

SELECT UseCounts, Text, Query_Plan
FROM sys.dm_exec_cached_plans
CROSS APPLY sys.dm_exec_sql_text(plan_handle)
CROSS APPLY sys.dm_exec_query_plan(plan_handle)

UseCount結果中的資料行會指出每個查詢的執行頻率。 下圖顯示第三個查詢的執行次數超過 250,000 次,明顯高於任何其他查詢。

Results of querying the dynamic management views in SQL Server Management Server

以下是造成這麼多資料庫要求的 SQL 查詢:

(@p__linq__0 int)SELECT TOP (2)
[Extent1].[BusinessEntityId] AS [BusinessEntityId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName]
FROM [Person].[Person] AS [Extent1]
WHERE [Extent1].[BusinessEntityId] = @p__linq__0

這是 Entity Framework 在稍早所顯示方法中 GetByIdAsync 產生的查詢。

實作快取策略解決方案並確認結果

合併快取之後,請重複負載測試,並將結果與先前的負載測試進行比較,而不需要快取。 以下是將快取新增至範例應用程式之後的負載測試結果。

Performance load test results for the cached scenario

成功的測試數量仍然達到高原,但在較高的使用者負載。 此負載的要求速率明顯高於先前。 平均測試時間仍隨著負載而增加,但回應時間上限為 0.05 毫秒,而稍早的 1 毫秒則為 20×改善。