Orleans 2.0官方文件(閆輝的個人翻譯)——4.8.1 grain持久化的目標
grain持久化的目標
- 允許不同的grain型別,使用不同型別的儲存提供程式(例如,一個使用Azure表,一個使用ADO.NET),或相同型別的儲存提供程式但具有不同的配置(例如,兩者都使用Azure表,但一個使用儲存帳戶#1和一個使用儲存帳戶#2)
- 允許只更改配置檔案而不需要更改程式碼,就能交換儲存提供程式例項(例如,Dev-Test-Prod)。
- 提供一個框架,以便以後可以由Orleans團隊或其他人,編寫其他的儲存提供程式。
- 提供最少一組的生產級儲存提供程式
- 儲存提供程式可以完全控制如何在持久化後端儲存中,儲存grain的狀態資料。結果就是,Orleans沒有提供全面的ORM儲存解決方案,但允許自定義儲存提供程式在需要時支援特定的ORM要求。
grain 持久化 API
可以通過以下兩種方式之一宣告grain型別:
- 繼承Grain:如果它們沒有任何持久化狀態,或者它們自己處理所有的持久化狀態。
繼承Grain<T>
:如果它們有一些持久化狀態,這些狀態希望由Orleans執行時來處理。換句話說,通過繼承Grain<T>
,grain型別自動選擇加入Orleans系統管理的永續性框架。
對於本節的其餘部分,我們將僅考慮第2種宣告方式 (擴充套件Grain<T>
) ,因為第1種宣告方式,grain將繼續像現在一樣執行,而不會有任何行為更改。
grain 狀態儲存
從Grain<T>
繼承的grain類,(其中T
是需要被持久化的、特定於應用程式的狀態資料型別)將從指定的儲存中自動載入其狀態。
grain將標記一個[StorageProvider]
屬性,該屬性指定儲存提供程式的命名例項,用於讀取/寫入此grain的狀態資料。
[StorageProvider(ProviderName="store1")]
public class MyGrain<MyGrainState> ...
{
...
}
Orleans框架提供了一種機制,來指定和註冊不同的儲存提供程式,並使用ISiloHostBuilder
來配置它們。
var silo = new SiloHostBuilder()
.AddMemoryGrainStorage("DevStore")
.AddAzureTableGrainStorage("store1", options => options.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=data1;AccountKey=SOMETHING1")
.AddAzureBlobGrainStorage("store2", options => options.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=data2;AccountKey=SOMETHING2")
.Build();
配置IGrainStorage提供程式
Orleans本身支援一系列的IGrainStorage實現,您可以將這些實現用於您的應用程式,來儲存grain的狀態。在本節中,我們將介紹如何在一個silo中,配置AzureTableGrainStorage
,AzureBlobGrainStorage
,DynamoDBGrainStorage
,MemoryGrainStorage
,和AdoNetGrainStorage
在。其他IGrainStorage
提供程式的配置類似。
AzureTableGrainStorage提供程式
var silo = new SiloHostBuilder()
.AddAzureTableGrainStorage("TableStore", options => options.ConnectionString = "UseDevelopmentStorage=true")
...
.Build();
通過AzureTableGrainStorageOptions
,以下設定可用於配置AzureTableGrainStorage
提供程式:
/// <summary>
/// Configuration for AzureTableGrainStorage
/// </summary>
public class AzureTableStorageOptions
{
/// <summary>
/// Azure table connection string
/// </summary>
[RedactConnectionString]
public string ConnectionString { get; set; }
/// <summary>
/// Table name where grain stage is stored
/// </summary>
public string TableName { get; set; } = DEFAULT_TABLE_NAME;
public const string DEFAULT_TABLE_NAME = "OrleansGrainState";
/// <summary>
/// Indicates if grain data should be deleted or reset to defaults when a grain clears it's state.
/// </summary>
public bool DeleteStateOnClear { get; set; } = false;
/// <summary>
/// Stage of silo lifecycle where storage should be initialized. Storage must be initialzed prior to use.
/// </summary>
public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;
#region json serialization
public bool UseJson { get; set; }
public bool UseFullAssemblyNames { get; set; }
public bool IndentJson { get; set; }
public TypeNameHandling? TypeNameHandling { get; set; }
#endregion json serialization
}
注意:狀態大小不應超過64KB,這是Azure Table Storage強加的限制。
AzureBlobGrainStorage提供程式
var silo = new SiloHostBuilder()
.AddAzureBlobGrainStorage("BlobStore", options => options.ConnectionString = "UseDevelopmentStorage=true")
...
.Build();
通過AzureBlobStorageOptions
,以下設定可用於配置AzureBlobGrainStorage
提供程式:
public class AzureBlobStorageOptions
{
/// <summary>
/// Azure connection string
/// </summary>
[RedactConnectionString]
public string ConnectionString { get; set; }
/// <summary>
/// Container name where grain stage is stored
/// </summary>
public string ContainerName { get; set; } = DEFAULT_CONTAINER_NAME;
public const string DEFAULT_CONTAINER_NAME = "grainstate";
/// <summary>
/// Stage of silo lifecycle where storage should be initialized. Storage must be initialzed prior to use.
/// </summary>
public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;
#region json serialization
public bool UseJson { get; set; }
public bool UseFullAssemblyNames { get; set; }
public bool IndentJson { get; set; }
public TypeNameHandling? TypeNameHandling { get; set; }
#endregion json serialization
}
DynamoDBGrainStorage提供程式
var silo = new SiloHostBuilder()
.AddDynamoDBGrainStorage("DDBStore", options =>
{
options.AccessKey = "MY_ACCESS_KEY";
options.SecretKey = "MY_SECRET_KEY";
options.Service = "us-wes-1";
})
...
.Build();
通過DynamoDBStorageOptions
,以下設定可用於配置DynamoDBGrainStorage
提供程式:
public class DynamoDBStorageOptions
{
/// <summary>
/// Gets or sets a unique identifier for this service, which should survive deployment and redeployment.
/// </summary>
public string ServiceId { get; set; } = string.Empty;
/// <summary>
/// AccessKey string for DynamoDB Storage
/// </summary>
[Redact]
public string AccessKey { get; set; }
/// <summary>
/// Secret key for DynamoDB storage
/// </summary>
[Redact]
public string SecretKey { get; set; }
/// <summary>
/// DynamoDB Service name
/// </summary>
public string Service { get; set; }
/// <summary>
/// Read capacity unit for DynamoDB storage
/// </summary>
public int ReadCapacityUnits { get; set; } = DynamoDBStorage.DefaultReadCapacityUnits;
/// <summary>
/// Write capacity unit for DynamoDB storage
/// </summary>
public int WriteCapacityUnits { get; set; } = DynamoDBStorage.DefaultWriteCapacityUnits;
/// <summary>
/// DynamoDB table name.
/// Defaults to 'OrleansGrainState'.
/// </summary>
public string TableName { get; set; } = "OrleansGrainState";
/// <summary>
/// Indicates if grain data should be deleted or reset to defaults when a grain clears it's state.
/// </summary>
public bool DeleteStateOnClear { get; set; } = false;
/// <summary>
/// Stage of silo lifecycle where storage should be initialized. Storage must be initialzed prior to use.
/// </summary>
public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;
#region JSON Serialization
public bool UseJson { get; set; }
public bool UseFullAssemblyNames { get; set; }
public bool IndentJson { get; set; }
public TypeNameHandling? TypeNameHandling { get; set; }
#endregion
}
ADO.NET grain儲存提供程式
ADO .NET grain 儲存提供程式,允許您在關係資料庫中儲存grain狀態。目前支援以下資料庫:
- SQL Server
- MySQL/ MariaDB
- PostgreSQL
- Oracle
首先,安裝基礎包:
Install-Package Microsoft.Orleans.Persistence.AdoNet
還原專案的nuget包後,您就會找到受支援的資料庫供應商的不同SQL指令碼,這些指令碼被複制到專案目錄\OrleansAdoNetContent中,其中每個受支援的ADO.NET擴充套件都有自己的目錄。您也可以從Orleans.Persistence.AdoNet庫中獲取它們。建立資料庫,然後執行相應的指令碼來建立表。
接下來的步驟是,安裝第二個特定於所需資料庫供應商的NuGet包(參見下表),並以程式設計方式或通過XML,配置儲存提供程式。
資料庫 | 指令碼 | NuGet包 | AdoInvariant | 備註 |
---|---|---|---|---|
SQL Server | SQLServer的-Persistence.sql | System.Data.SqlClient | System.Data.SqlClient | |
MySQL / MariaDB | MySQL-Persistence.sql | MySql.Data | MySql.Data.MySqlClient | |
PostgreSQL | PostgreSQL-Persistence.sql | Npgsql | Npgsql | |
Oralce | Oracle-Persistence.sql | ODP.net | Oracle.DataAccess.Client | 不支援.net core |
以下是如何通過ISiloHostBuilder
配置ADO.NET儲存提供程式的示例:
var siloHostBuilder = new SiloHostBuilder()
.AddAdoNetGrainStorage("OrleansStorage", options=>
{
options.Invariant = "<Invariant>";
options.ConnectionString = "<ConnectionString>";
options.UseJsonFormat = true;
});
實質上,您只需要設定特定於資料庫供應商的連線字串和標識供應商的 Invariant
(參見上表)。您還可以選擇儲存資料的格式,可以是二進位制(預設),JSON或XML。雖然二進位制是最緊湊的選項,但它是不透明的,您將無法讀取或處理資料。建議使用JSON。
您可以通過AdoNetGrainStorageOptions
,設定以下屬性:
/// <summary>
/// Options for AdonetGrainStorage
/// </summary>
public class AdoNetGrainStorageOptions
{
/// <summary>
/// Connection string for AdoNet storage.
/// </summary>
[Redact]
public string ConnectionString { get; set; }
/// <summary>
/// Stage of silo lifecycle where storage should be initialized. Storage must be initialzed prior to use.
/// </summary>
public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
/// <summary>
/// Default init stage in silo lifecycle.
/// </summary>
public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;
/// <summary>
/// The default ADO.NET invariant used for storage if none is given.
/// </summary>
public const string DEFAULT_ADONET_INVARIANT = AdoNetInvariants.InvariantNameSqlServer;
/// <summary>
/// The invariant name for storage.
/// </summary>
public string Invariant { get; set; } = DEFAULT_ADONET_INVARIANT;
#region json serialization related settings
/// <summary>
/// Whether storage string payload should be formatted in JSON.
/// <remarks>If neither <see cref="UseJsonFormat"/> nor <see cref="UseXmlFormat"/> is set to true, then BinaryFormatSerializer will be configured to format storage string payload.</remarks>
/// </summary>
public bool UseJsonFormat { get; set; }
public bool UseFullAssemblyNames { get; set; }
public bool IndentJson { get; set; }
public TypeNameHandling? TypeNameHandling { get; set; }
#endregion
/// <summary>
/// Whether storage string payload should be formatted in Xml.
/// <remarks>If neither <see cref="UseJsonFormat"/> nor <see cref="UseXmlFormat"/> is set to true, then BinaryFormatSerializer will be configured to format storage string payload.</remarks>
/// </summary>
public bool UseXmlFormat { get; set; }
}
ADO.NET持久化具有:版本資料,以及使用任意應用程式規則和流定義任意(反)序列化程式的功能,但目前沒有方法將它們暴露給應用程式程式碼。參見ADO.NET Persistence Rationale中的更多資訊。
MemoryGrainStorage
MemoryGrainStorage
是一個簡單的grain儲存實現,它背後實際上並沒有使用持久化資料儲存。便於快速學習使用grain 儲存,但不要打算用於生產場景。
注意:此提供程式將狀態持久化為易失性記憶體,該記憶體將在silo關閉時被刪除。僅用於測試。
以下通過ISiloHostBuilder
,如何設定記憶體儲存提供程式 。
var siloHostBuilder = new SiloHostBuilder()
.AddMemoryGrainStorage("OrleansStorage", options=>options.NumStorageGrains = 10);
您可以通過MemoryGrainStorageOptions
,設定以下屬性:
/// <summary>
/// Options for MemoryGrainStorage
/// </summary>
public class MemoryGrainStorageOptions
{
/// <summary>
/// Default number of queue storage grains.
/// </summary>
public const int NumStorageGrainsDefaultValue = 10;
/// <summary>
/// Number of store grains to use.
/// </summary>
public int NumStorageGrains { get; set; } = NumStorageGrainsDefaultValue;
/// <summary>
/// Stage of silo lifecycle where storage should be initialized. Storage must be initialzed prior to use.
/// </summary>
public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
/// <summary>
/// Default init stage
/// </summary>
public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;
}
儲存提供程式的注意事項
如果沒有為一個Grain<T>
的grain類指定[StorageProvider]
屬性,則將搜尋名為Default
的提供程式。如果未找到,則將其視為缺少儲存提供程式。
如果在配置時,未將grain類引用的儲存提供程式新增到silo中,則該型別的grain將無法在執行時啟用,並且對它們的呼叫將失敗,異常資訊Orleans.Storage.BadProviderConfigException
指出此grain型別未被載入。但其餘的grain型別不會受到影響。
不同的grain型別,可以使用不同的已配置的儲存提供程式,即使兩者都是相同的型別:例如,兩個不同的Azure表儲存提供程式例項,它們連線到不同的Azure儲存帳戶(請參閱上面的配置檔案示例)。
儲存提供程式的所有詳細配置的資訊都是通過ISiloHostBuilder定義的。目前沒有提供動態更新或更改一個silo所使用的儲存提供程式列表的機制。但是,這是一個優先順序/工作負載約束,而不是一個基本設計約束。
狀態持久化 API
狀態持久化API有兩個部分:grain狀態API和儲存提供程式API。
grain狀態API
Orleans 執行時中的grain狀態儲存功能,將提供讀寫操作,以自動填充/儲存該grain的資料物件GrainState
。在幕後,這些功能將連線(在Orleans client-gen工具生成的程式碼中)到為該grain配置的適當的持久化提供程式。
grain狀態的讀/寫功能
當一個grain被啟用時,grain狀態會被自動讀取,不過,grain負責在必要時顯式地觸發任何改變的grain狀態的寫入。有關錯誤處理機制的詳細資訊,請參閱下面的“ 故障模式”部分。
在為該啟用呼叫OnActivateAsync()
方法之前,GrainState
被自動讀取(等同於使用base.ReadStateAsync()
)。 在任何方法呼叫一個grain之前,除非該grain被這個呼叫啟用,否則GrainState
不會被重新整理。
在任何grain方法呼叫期間,grain都可以通過呼叫base.WriteStateAsync()
,請求Orleans執行時將該啟用體的當前grain狀態資料,寫入指定的儲存提供程式。當對其狀態資料進行重大更新時,grain負責顯式地執行寫操作。最常見的做法是,grain方法把base.WriteStateAsync()
的Task
,作為從該grain方法返回的最終結果Task
,但不是必須要遵循此模式。在任何grain方法呼叫之後,執行時不會自動更新儲存的grain狀態。
在grain中的任何grain方法或定時器回撥處理方法中,grain通過呼叫base.ReadStateAsync()
,請求Orleans執行時從指定的儲存提供程式中,重新讀取當前grain狀態資料,以進行該啟用操作。這將使用從持久化儲存中讀取的最新狀態資料,完全覆蓋儲存在grain狀態物件中的當前的狀態資料。
一個隱性的特定於提供程式的Etag
值(string
),當狀態被讀取時,可以由儲存提供程式設定為佔據grain狀態的元資料的一部分。某些提供程式如果不使用Etag
的話,可以選擇將其保留為null
。
從概念上講,Orleans 執行時會持有grain狀態資料物件的深拷貝,以供自己在任何寫操作時使用。在幕後,執行時可以使用優化規則和啟發式方法,來避免在某些情況下執行部分或全部的深拷貝,前提是預留了預期的邏輯隔離語義。
grain狀態讀/寫操作的示例程式碼
grain必須繼承Grain<T>
類,才能參與Orleans的grain狀態持久化機制。上述定義中的T
,將被替換為該grain的特定於應用程式的grain狀態類;見下面的例子。
grain類還應該使用一個[StorageProvider]
屬性進行註解,該屬性告訴執行時,哪個儲存提供程式(例項)與此型別的grain一起使用。
public class MyGrainState
{
public int Field1 { get; set; }
public string Field2 { get; set; }
}
[StorageProvider(ProviderName="store1")]
public class MyPersistenceGrain : Grain<MyGrainState>, IMyPersistenceGrain
{
...
}
grain狀態讀取
在呼叫grain的OnActivateAsync()
方法之前,Orleans執行時將自動進行grain狀態的初始讀取;不需要任何應用程式程式碼即可實現此操作。從這一刻起,grain的狀態可以通過grain類內的屬性Grain<T>.State
來獲得。
grain狀態寫入
在對grain的記憶體狀態進行任何適當的更改之後,grain應該呼叫base.WriteStateAsync()
方法,通過為此grain型別定義的儲存提供程式,將更改寫入持久化儲存。此方法是非同步的,並返回一個Task
,此Task
通常被grain方法作為它自己的完成Task
返回。
public Task DoWrite(int val)
{
State.Field1 = val;
return base.WriteStateAsync();
}
grain狀態重新整理
如果grain希望從後端儲存顯式地重讀此grain的最新狀態,那麼grain應該呼叫base.ReadStateAsync()
方法。這將通過為該grain型別定義的儲存提供程式,從持久化儲存中重新載入grain狀態,當ReadStateAsync()
Task
完成時,將覆蓋並替換在先前的記憶體中的grain狀態副本。
public async Task<int> DoRead()
{
await base.ReadStateAsync();
return State.Field1;
}
grain狀態持久化操作失敗的模式
grain狀態讀取操作失敗的模式
在初始讀取特定grain的狀態資料期間,儲存提供程式返回的失敗,將導致該grain的啟用操作失敗; 在這種情況下,不會呼叫該grain的生命週期回撥方法OnActivateAsync()
。對引發該grain啟用的原始請求,將以與grain啟用期間的任何其他失敗相同的方式,以失敗的形式被返回給呼叫者。儲存提供程式在讀取特定grain的狀態資料時,所遭遇的失敗將導致ReadStateAsync()
Task
失敗。grain可以選擇處理或忽略失敗的Task
,就像Orleans的其他任何Task
一樣。
在silo啟動時,某個grain由於儲存提供程式配置的缺少/錯誤,而未能成功載入,此時嘗試將訊息傳送到該grain,將返回永久性錯誤Orleans.BadProviderConfigException
。
grain狀態寫入操作失敗的模式
儲存提供程式寫入特定grain的狀態資料時遇到的失敗,將導致WriteStateAsync()
Task
失敗。通常,這意味著,grain呼叫將以失敗的形式,被返回給客戶端呼叫程式,前提是WriteStateAsync()
Task
被正確地鏈入到此grain方法的最終返回的Task
。但是,某些高階場景下,可能會編寫grain程式碼來專門處理此類寫入錯誤,就像它們可以處理任何其他失敗的Task
一樣。
執行錯誤處理/恢復程式碼的grain,必須捕獲異常/失敗的WriteStateAsync()
Task
,而不是重新丟擲,以表示它們已成功處理寫入錯誤。
儲存提供程式API
有一個服務提供程式API,用於編寫額外的持久化提供程式 —— IGrainStorage
。
Persistence Provider API涵蓋了GrainState資料的讀寫操作。
/// <summary>
/// Interface to be implemented for a storage able to read and write Orleans grain state data.
/// </summary>
public interface IGrainStorage
{
/// <summary>Read data function for this storage instance.</summary>
/// <param name="grainType">Type of this grain [fully qualified class name]</param>
/// <param name="grainReference">Grain reference object for this grain.</param>
/// <param name="grainState">State data object to be populated for this grain.</param>
/// <returns>Completion promise for the Read operation on the specified grain.</returns>
Task ReadStateAsync(string grainType, GrainReference grainReference, IGrainState grainState);
/// <summary>Write data function for this storage instance.</summary>
/// <param name="grainType">Type of this grain [fully qualified class name]</param>
/// <param name="grainReference">Grain reference object for this grain.</param>
/// <param name="grainState">State data object to be written for this grain.</param>
/// <returns>Completion promise for the Write operation on the specified grain.</returns>
Task WriteStateAsync(string grainType, GrainReference grainReference, IGrainState grainState);
/// <summary>Delete / Clear data function for this storage instance.</summary>
/// <param name="grainType">Type of this grain [fully qualified class name]</param>
/// <param name="grainReference">Grain reference object for this grain.</param>
/// <param name="grainState">Copy of last-known state data object for this grain.</param>
/// <returns>Completion promise for the Delete operation on the specified grain.</returns>
Task ClearStateAsync(string grainType, GrainReference grainReference, IGrainState grainState);
}
儲存提供程式的語義
當儲存提供程式檢測到Etag
約束衝突時,任何執行寫入操作的嘗試,都應該導致寫入Task
失敗,並出現瞬時錯誤Orleans.InconsistentStateException
,幷包裝底層的儲存異常。
public class InconsistentStateException : AggregateException
{
/// <summary>The Etag value currently held in persistent storage.</summary>
public string StoredEtag { get; private set; }
/// <summary>The Etag value currently held in memory, and attempting to be updated.</summary>
public string CurrentEtag { get; private set; }
public InconsistentStateException(
string errorMsg,
string storedEtag,
string currentEtag,
Exception storageException
) : base(errorMsg, storageException)
{
this.StoredEtag = storedEtag;
this.CurrentEtag = currentEtag;
}
public InconsistentStateException(string storedEtag, string currentEtag, Exception storageException)
: this(storageException.Message, storedEtag, currentEtag, storageException)
{ }
}
寫入操作的任何其他失敗,都應該導致寫入Task
中斷,並帶有一個異常資訊,此異常資訊包含底層的儲存異常。
資料對映
各個儲存提供程式應該決定如何最好地儲存grain狀態 ——blob(各種格式/序列化形式)或column-per-field是顯而易見的選擇。
Azure Table的基本儲存提供程式,使用Orleans二進位制序列化,將狀態資料欄位編碼為單個表列。
ADO.NET持久化基礎理論
ADO.NET支援的持久化儲存的原則是:
- 在資料,資料格式和程式碼的演變過程中,保護關鍵業務資料的安全。
- 利用供應商和特定於儲存的功能。
實際上,這意味著要遵循ADO.NET的實現目標,並在特定於ADO.NET的儲存提供程式中,新增一些實現邏輯,以使儲存中的資料形態不斷演化。
除了通常的儲存提供程式功能外,ADO.NET提供程式還具有內建功能:
- 在往返狀態時,將儲存資料格式從一種格式更改為另一種格式(例如,從JSON到二進位制)。
- 以任意的方式,塑造要儲存或從儲存中讀取的型別。這有助於改進版本狀態。
- 以流資料的形式,從資料庫中獲取資料。
1.和
2.
可以被應用任意的決策引數,例如grain ID,grain型別,有效載荷資料。
這樣做是為了選擇一種格式,例如簡單二進位制編碼(SBE),並實現了 IStorageDeserializer和IStorageSerializer。內建的(反)序列化器是使用此方法構建的。該OrleansStorageDefault(反)序列化 可以用作如何實現其他格式的示例。
當(de)序列化器被實現後,它們需要新增到AdoNetGrainStorage中的StorageSerializationPicker
屬性中。這是IStorageSerializationPicker的一個實現。預設情況下, 將使用StorageSerializationPicker。在RelationalStorageTests中,可以看到更改資料儲存格式或使用(反)序列化器的示例。
目前沒有方法將此公開給Orleans應用程式消費,因為沒有方法可以訪問框架建立的AdoNetGrainStorage。