使用可靠的集合
Service Fabric 透過可靠的集合向 .NET 開發人員提供具狀態的程式設計模型。 具體來說,Service Fabric 提供了可靠的字典和可靠的佇列類別。 當您使用這些類別時,您的狀態是分割的 (延展性)、複寫的 (可用性),且在分割區內交易 (ACID 語意)。 讓我們看看可靠字典物件的一般用法,並查看它究竟做了些什麼。
try
{
// Create a new Transaction object for this partition
using (ITransaction tx = base.StateManager.CreateTransaction())
{
// AddAsync takes key's write lock; if >4 secs, TimeoutException
// Key & value put in temp dictionary (read your own writes),
// serialized, redo/undo record is logged & sent to secondary replicas
await m_dic.AddAsync(tx, key, value, cancellationToken);
// CommitAsync sends Commit record to log & secondary replicas
// After quorum responds, all locks released
await tx.CommitAsync();
}
// If CommitAsync isn't called, Dispose sends Abort
// record to log & all locks released
}
catch (TimeoutException)
{
// choose how to handle the situation where you couldn't get a lock on the file because it was
// already in use. You might delay and retry the operation
await Task.Delay(100);
}
可靠字典物件上的所有作業 (除了無法復原的 ClearAsync) 都需要一個 ITransaction 物件。 您在單一分割區內對任何可靠的字典和/或可靠的佇列物件嘗試進行的任何及所有變更,都會與這個物件產生關聯性。 您可透過呼叫分割區的 StateManager 的 CreateTransaction 方法,取得 ITransaction 物件。
在上面的程式碼中,ITransaction 物件會傳遞至可靠字典的 AddAsync 方法。 就內部而言,接受索引鍵的字典方法會採用與該索引鍵相關聯的讀取器/寫入器鎖定。 如果此方法會修改索引鍵的值,則這個方法就會在索引鍵上使用寫入鎖定;如果方法只讀取索引鍵的值,則會在索引鍵上使用讀取鎖定。 因為 AddAsync 會將索引鍵值修改成新的傳入值,所以會使用該索引鍵的寫入鎖定。 因此,如果有 2 個 (或多個) 執行緒同時嘗試新增具相同索引鍵的值,則其中一個執行緒將會取得寫入鎖定,而另一個執行緒將會封鎖。 方法預設最多封鎖 4 秒以取得鎖定,4 秒後方法就會擲回 TimeoutException。 如果喜歡的話,方法多載的存在可讓您傳遞明確的逾時值。
通常,您撰寫程式碼回應 TimeoutException 的方式是攔截它,然後重試整個作業 (如上面的程式碼所示)。 在這個簡單程式碼中,我們只呼叫 Task.Delay 每次傳遞 100 毫秒。 但在實際狀況裡,最好還是改用某種指數型的撤退延遲。
一旦取得鎖定,AddAsync 就會在與 ITransaction 物件相關聯的內部暫存字典中加入索引鍵和值物件參考。 這就完成了讀取自己撰寫的語意。 也就是說,在您呼叫 AddAsync 之後,稍後使用相同的 ITransaction 物件對 TryGetValueAsync 發出的呼叫會傳回值,即使您尚未認可交易。
注意
以新的交易呼叫 TryGetValueAsync 將會傳回前一個認可值的參考。 請勿直接修改該參考,因為這會略過保存和複寫變更的機制。 建議您將值設為唯讀,這樣一來,變更索引鍵值的唯一方法就只有透過可靠的字典 API。
接下來,AddAsync 會將索引鍵和值物件序列化為位元組陣列,並將這些位元組陣列附加至本機節點的記錄檔。 最後,AddAsync 會將位元組陣列傳送給所有次要複本,讓它們有相同的索引鍵/值資訊。 即使索引鍵/值資訊已寫入記錄檔,在相關交易獲認可之前,資訊都不會被視為字典的一部分。
在上述程式碼中,呼叫 CommitAsync 即認可所有交易作業。 具體來說,它會將認可資訊附加到本機節點的記錄檔,也會將認可記錄傳送給所有的次要複本。 一旦回覆了複本的仲裁 (多數),則所有資料變更就會視為永久性,並且會釋出透過 ITransaction 物件所操作的任何索引鍵相關聯鎖定,讓其他執行緒/交易可以操作相同的索引鍵及其值。
如未呼叫 CommitAsync (通常是因為擲回例外狀況),則會處置 ITransaction 物件。 在處置未認可的 ITransaction 物件時,Service Fabric 會將中止資訊附加到本機節點的記錄檔,且不需要傳送任何資訊至任何次要複本。 然後會釋出透過交易操作的任何與索引鍵相關聯的鎖定。
動態可靠的集合
例如,在某些工作負載中(例如複寫的快取),可以容許偶爾的資料遺失。 在寫入可靠的字典時,避免在磁碟建立資料持續性可以獲得更好的延遲和輸送量。 缺乏持續性的缺點是,如果發生仲裁遺失,將會造成完整資料遺失。 由於仲裁遺失是很罕見的情況,因此在權衡難得發生的工作負載資料遺失可能性後,提升效能可能仍是值得一試的方式。
目前,只提供可靠的字典和可靠的佇列使用這個動態支援,但不支援 ReliableConcurrentQueues 使用。 請參閱注意事項清單,判斷是否要使用動態集合。
若要在服務中啟用動態支援,請將服務類型宣告中的 HasPersistedState
旗標設定為 false
,如下所示:
<StatefulServiceType ServiceTypeName="MyServiceType" HasPersistedState="false" />
注意
現有的持續性服務無法變成動態,反之亦然。 如果您想要這樣做,則必須刪除現有的服務,然後使用更新的旗標來部署服務。 這表示如果您想要變更 HasPersistedState
旗標,則必須冒著遺失完整資料的風險。
常見陷阱以及如何避免
現在您已了解可靠的集合在內部的運作方式,讓我們看看其中一些常見的誤用。 參閱下列程式碼:
using (ITransaction tx = StateManager.CreateTransaction())
{
// AddAsync serializes the name/user, logs the bytes,
// & sends the bytes to the secondary replicas.
await m_dic.AddAsync(tx, name, user);
// The line below updates the property's value in memory only; the
// new value is NOT serialized, logged, & sent to secondary replicas.
user.LastLogin = DateTime.UtcNow; // Corruption!
await tx.CommitAsync();
}
使用一般的 .NET 字典時,您可以在字典中加入索引鍵/值,然後變更屬性的值 (例如 LastLogin)。 不過,這段程式碼和可靠的字典不會合作無間。 我們稍早討論過:呼叫 AddAsync 會將索引鍵/值物件序列化成位元組陣列,然後將陣列儲存到本機檔案,並將它們傳送到次要複本。 稍後如果變更屬性,只會變更記憶體中的屬性值,不會影響本機檔案或傳送到複本的資料。 如果處理序損毀,記憶體中內容將全部遺失。 啟動新的處理序,或另一個複本變成主要複本時,使用的就是舊的屬性值。
我一再強調上面這種錯誤是很容易發生的。 而且您只有在處理序當機時才會發現錯誤。 撰寫程式碼的正確方式其實只要反轉兩行即可︰
using (ITransaction tx = StateManager.CreateTransaction())
{
user.LastLogin = DateTime.UtcNow; // Do this BEFORE calling AddAsync
await m_dic.AddAsync(tx, name, user);
await tx.CommitAsync();
}
這是另一個常見的錯誤︰
using (ITransaction tx = StateManager.CreateTransaction())
{
// Use the user's name to look up their data
ConditionalValue<User> user = await m_dic.TryGetValueAsync(tx, name);
// The user exists in the dictionary, update one of their properties.
if (user.HasValue)
{
// The line below updates the property's value in memory only; the
// new value is NOT serialized, logged, & sent to secondary replicas.
user.Value.LastLogin = DateTime.UtcNow; // Corruption!
await tx.CommitAsync();
}
}
同樣地,使用一般的 .NET 字典時,上面的程式碼以常見的模式正常運作:開發人員使用索引鍵查詢值。 如果值存在,開發人員會變更屬性的值。 不過,使用可靠的集合,這段程式碼會出現前面討論過的同樣問題:物件一旦給了可靠的集合,就「無法」修改。
在可靠的集合中更新值的正確方式,是取得現有值的參考,並考慮這個參考所參考的物件為不可變。 接著,建立新的物件,此物件是與原始物件完全相同的複本。 現在,您可以修改這個新物件的狀態,將新物件寫入集合,讓它序列化為位元組陣列,附加至本機檔案並傳送到複本。 認可變更之後,記憶體中的物件、本機檔案及所有複本都會有完全一致的狀態。 大功告成!
下列程式碼會示範在可靠的集合中更新值的正確方式︰
using (ITransaction tx = StateManager.CreateTransaction())
{
// Use the user's name to look up their data
ConditionalValue<User> currentUser = await m_dic.TryGetValueAsync(tx, name);
// The user exists in the dictionary, update one of their properties.
if (currentUser.HasValue)
{
// Create new user object with the same state as the current user object.
// NOTE: This must be a deep copy; not a shallow copy. Specifically, only
// immutable state can be shared by currentUser & updatedUser object graphs.
User updatedUser = new User(currentUser);
// In the new object, modify any properties you desire
updatedUser.LastLogin = DateTime.UtcNow;
// Update the key's value to the updateUser info
await m_dic.SetValue(tx, name, updatedUser);
await tx.CommitAsync();
}
}
定義不可變的資料類型,以防止程式設計人員犯錯。
在理想情況下,當您不小心產生的程式碼變更了您認為是不可變的物件狀態時,我們希望編譯器可以報告相關錯誤。 但是 C# 編譯器做不到這一點。 所以,為避免潛在的程式設計人員錯誤,我們強烈建議您將可靠集合所使用的類型定義為不可變的類型。 具體來說,這表示您要堅持核心值類型 (例如數字 [Int32、UInt64 等等]、DateTime、Guid、TimeSpan 等等)。 您也可以使用 String。 最好避免使用集合屬性,因為將其序列化和還原序列化經常會降低效能。 不過,如果您想要使用集合屬性,強烈建議您使用 .NET 的不可變集合程式庫 (System.Collections.Immutable)。 您可以從 https://nuget.org 下載這個程式庫。另外,也建議您盡可能密封類別,並將欄位變成唯讀。
以下的 UserInfo 類型會示範如何利用上述建議定義不可變的類型。
[DataContract]
// If you don't seal, you must ensure that any derived classes are also immutable
public sealed class UserInfo
{
private static readonly IEnumerable<ItemId> NoBids = ImmutableList<ItemId>.Empty;
public UserInfo(String email, IEnumerable<ItemId> itemsBidding = null)
{
Email = email;
ItemsBidding = (itemsBidding == null) ? NoBids : itemsBidding.ToImmutableList();
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
// Convert the deserialized collection to an immutable collection
ItemsBidding = ItemsBidding.ToImmutableList();
}
[DataMember]
public readonly String Email;
// Ideally, this would be a readonly field but it can't be because OnDeserialized
// has to set it. So instead, the getter is public and the setter is private.
[DataMember]
public IEnumerable<ItemId> ItemsBidding { get; private set; }
// Since each UserInfo object is immutable, we add a new ItemId to the ItemsBidding
// collection by creating a new immutable UserInfo object with the added ItemId.
public UserInfo AddItemBidding(ItemId itemId)
{
return new UserInfo(Email, ((ImmutableList<ItemId>)ItemsBidding).Add(itemId));
}
}
ItemId 類型也是不可變的類型,如下所示︰
[DataContract]
public struct ItemId
{
[DataMember] public readonly String Seller;
[DataMember] public readonly String ItemName;
public ItemId(String seller, String itemName)
{
Seller = seller;
ItemName = itemName;
}
}
結構描述版本控制 (升級)
就內部而言,可靠的集合會使用 .NET 的 DataContractSerializer 序列化物件。 序列化的物件會保存在主要複本的本機磁碟中,並傳送至次要複本。 隨著您的服務日趨成熟,您可能會想要變更服務需要的資料種類 (結構描述)。 請謹慎處理資料的版本設定。 首先也是最重要的,您必須永遠有能力還原序列化舊的資料。 具體來說,這表示您的還原序列化程式碼必須具有無限回溯相容性︰服務程式碼的版本 333 必須能夠操作 5 年前放在可靠的集合中,第 1 版的服務程式碼資料。
而且,服務程式碼一次只能升級一個網域。 所以,在升級期間,您會同時執行兩個不同版本的服務程式碼。 您必須避免新版本的服務程式碼使用新的結構描述,因為舊版的服務程式碼可能無法處理新的結構描述。 您應該盡可能將服務的每個版本都設計為正向相容一個版本。 具體來說,這表示服務程式碼的 V1 應該能夠略過它未明確處理的任何結構描述元素。 不過,它必須能夠儲存所有未明確了解的資料,並且在更新字典索引鍵或值時寫回。
警告
雖然您可以修改索引鍵的結構描述,但您必須確保索引鍵的等式和比較演算法是穩定的。 上述任一演算法變更之後的可靠集合行為是未定義的,而且可能會導致資料損毀、遺失和服務損毀。 .NET 字串可以用作索引鍵,但使用此字串本身作為索引鍵;請勿使用 String.GetHashCode 的結果作為索引鍵。
或者,您可以執行多階段升級。
- 將服務升級至
- 服務程式碼封裝同時包含原始 V1 和新 V2 版本資料合約的新版本;
- 視需要註冊自訂 V2 狀態序列化程式;
- 使用 V1 資料合約來對原始 V1 集合執行所有作業。
- 將服務升級至
- 會建立新 V2 集合的新版本;
- 在單一交易中對第一個 V1 集合執行個別新增、更新和刪除作業,然後對 V2 集合執行相同作業;
- 只對 V1 集合執行讀取作業。
- 將 V1 集合中的所有資料複製到 V2 集合。
- 這可以透過在步驟 2 中部署的服務版本而在背景處理序中完成。
- 從 V1 集合重新擷取所有索引鍵。 列舉會預設使用 IsolationLevel.Snapshot 來執行,以避免在作業期間鎖定集合。
- 針對每個索引鍵,請使用個別交易來
- 從 V1 集合 TryGetValueAsync。
- 如果該值自複製處理序啟動後已從 V1 集合中移除,則應跳過索引鍵且不在 V2 集合中使其恢復。
- 將該值 TryAddAsync 值至 V2 集合。
- 如果該值自複製處理序啟動後已新增至 V2 集合中移除,則應跳過索引鍵。
- 只有在
TryAddAsync
傳回true
時,才應認可交易。 - 值存取 API 預設會使用 IsolationLevel.ReadRepeatable,並依賴鎖定來保證在認可或中止交易之前,其他呼叫者不會修改這些值。
- 將服務升級至
- 只對 V2 集合執行讀取作業的新版本;
- 仍然對第一個 V1 集合執行個別新增、更新和刪除作業,然後對 V2 集合執行相同作業,以保留復原到 V1 的選項。
- 全面測試服務,並確認服務如預期般運作。
- 如果您遺漏了任何未更新為適用於 V1 和 V2 集合的值存取作業,則可能會注意到遺失的資料。
- 如果有遺漏任何資料,請復原至步驟 1、移除 V2 集合並重複此處理序。
- 將服務升級至
- 只對 V2 集合執行所有作業的新版本;
- 透過服務復原回到 V1 已經不可能,而是需要使用反向步驟 2-4 來向前復原。
- 將服務升級至
- 會移除 V1 集合的新版本。
- 等候記錄截斷。
- 根據預設,每次對可靠集合 50MB 的寫入 (新增、更新和刪除),就會發生這種情況。
- 將服務升級至
- 服務程式碼封裝中不再包含 V1 資料合約的新版本。
下一步
若要了解如何建立正向相容的資料合約,請參閱正向相容的資料合約 \(機器翻譯\)。
若要了解資料合約版本設定的最佳做法,請參閱資料合約版本設定 \(機器翻譯\)。
若要了解如何實作版本容錯的資料合約,請參閱版本容錯序列化回呼 \(機器翻譯\)。
若要了解如何提供可跨多個版本相互操作的資料結構,請參閱 IExtensibleDataObject \(機器翻譯\)。
若要了解如何設定可靠的集合,請參閱複寫器設定