沒有快取反模式
反模式是常見的設計缺陷,可在壓力情況下破壞軟體或應用程式,不應忽略。 當處理許多並行要求的雲端應用程式重複擷取相同的數據時,不會發生任何快取反模式。 這可以降低效能和延展性。
未快取數據時,可能會導致許多不想要的行為,包括:
- 在 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(服務無法使用)錯誤。 這些表示您應該相應放大前端。
如何偵測沒有快取反模式
您可以執行下列步驟,以協助識別缺乏快取是否會導致效能問題:
檢閱應用程式設計。 清查應用程式所使用的所有資料存放區。 針對每個,判斷應用程式是否使用快取。 可能的話,請判斷數據變更的頻率。 快取的良好初始候選專案包括變更緩慢的數據,以及經常讀取的靜態參考數據。
檢測應用程式並監視即時系統,以瞭解應用程式擷取數據或計算信息的頻率。
在測試環境中分析應用程式,以擷取與數據存取作業或其他經常執行的計算相關聯的額外負荷低階計量。
在測試環境中執行負載測試,以識別系統在一般工作負載和負載過重下如何回應。 負載測試應模擬使用實際工作負載在生產環境中觀察到的數據存取模式。
檢查基礎數據存放區的數據存取統計數據,並檢閱重複相同數據要求的頻率。
診斷範例
下列各節會將這些步驟套用至先前所述的範例應用程式。
檢測應用程式並監視即時系統
檢測應用程式並加以監視,以取得使用者在應用程式處於生產環境中時所提出之特定要求的相關信息。
下圖顯示 New Relic 在負載測試期間擷取的監視數據。 在這裡情況下,唯一執行的 HTTP GET 作業是 Person/GetAsync
。 但在實時生產環境中,瞭解每個要求執行的相對頻率,可讓您深入瞭解應該快取哪些資源。
如果您需要更深入的分析,您可以使用分析工具在測試環境中擷取低階效能數據(而非生產系統)。 查看 I/O 要求速率、記憶體使用量和 CPU 使用率等計量。 這些計量可能會顯示對數據存放區或服務的大量要求,或重複執行相同計算的處理。
負載測試應用程式
下圖顯示範例應用程式負載測試的結果。 負載測試會模擬最多800位用戶執行一系列一般作業的步驟負載。
每秒執行的成功測試數目達到高原,因此其他要求會變慢。 隨著工作負載,平均測試時間會穩步增加。 一旦使用者載入尖峰,回應時間就會關閉。
檢查數據存取統計數據
數據存放區所提供的數據存取統計數據和其他資訊可以提供有用的資訊,例如最常重複的查詢。 例如,在 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 次,明顯高於任何其他查詢。
以下是造成這麼多資料庫要求的 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
產生的查詢。
實作快取策略解決方案並確認結果
合併快取之後,請重複負載測試,並將結果與先前的負載測試進行比較,而不需要快取。 以下是將快取新增至範例應用程式之後的負載測試結果。
成功的測試數量仍然達到高原,但在較高的用戶負載。 此負載的要求速率明顯高於先前。 平均測試時間仍隨著負載而增加,但響應時間上限為0.05毫秒,而稍早的1毫秒則為20×改善。