題記:這篇介紹狀態管理構件塊,這個概念相對於微服務框架而言是比較特殊的。
注:本文僅針對非Actor狀態存儲的情況進行說明,對於Actor狀態存儲會在講述Actor的時候一並說明。
原理
要用好這個構件塊,首先需要正確理解狀態管理的概念。
大部分微服務開發框架或者說指導,都提倡微服務以無狀態類型的方式來運行,這種無狀態微服務當然更容易進行伸縮,但是在遇到需要處理一些類似Session這樣的數據的時候,為了應對分布式的環境往往要借助於外部存儲(一般是數據庫或者緩存中間件)。但是這樣做不可避免引入了對外部服務以及特定協議的依賴。
如果對Service Fabric熟悉的同學,可能對有狀態服務這個概念有所了解,這種SF中特有的微服務類型,直接通過運行時和SDK給微服務提供了一種開箱即用的狀態管理機制——即可靠集合(Reliable Collections)。這種機制讓你可以通過Key/Value的方式來存取相關狀態值(業務數據),在某些情況下甚至可以當作一個NoSQL來使用。有狀態服務的優勢是把數據和業務邏輯作為一個整體來處理,特別適合領域驅動設計為指導的每個微服務對應獨立的數據源的原則。
由於Dapr和Service Fabric有一些淵源,所以“有狀態”的這個概念也被引入到了Dapr當中,但是這個時候的微服務類型其實還是移舊保持着無狀態。因此狀態管理構件塊本質上是給開發人員提供了一種狀態值存取的機制和API,並把狀態存儲的存儲源(也即狀態存儲組件)和訪問協議進行了抽象和屏蔽。原理圖如下(其中使用了Redis作為狀態存儲組件,這也是開發環境默認的組件):
從上圖所知,微服務只需要對自己的Dapr邊車進行訪問,即可完成狀態的保存和獲取(可批量)。而存取的方式遵循了標准了HTTP規范的謂詞。
因而,有了狀態管理構件塊,微服務輕可以利用其完成臨時狀態的持久化和微服務間共享,重可以利用其實現有狀態服務來保存業務模型。
能力
Dapr的狀態管理構件塊並非是簡簡單單為大家提供了一種狀態存取的機制(可以從原理圖直觀的看出),更為重要的是提供了如下額外能力:
在運行微服務的時候配置存儲組件:開發的時候只需要關心Dapr的規范接口,並使用某些簡單易得的存儲組件來進行調試,比如默認的Redis存儲組件;運行的時候可以引入(替換)為其他存儲組件,比如Azure CosmosDB,而無需改變業務代碼。
內置重試機制:和服務調用構件塊一樣,狀態管理的API提供了內置的重試能力,並可以用同樣的語義配置重試策略。這樣的重試能力也為並發和一致性等能力提供了基礎。
內置並發控制機制:Dapr依賴ETag這一特殊狀態屬性來保證樂觀並發控制的處理。也即在更新或者刪除狀態的時候,會檢查ETag是否匹配,從而決定是否完成數據操作。眾所周知,樂觀並發這種模式是比較適合數據沖突很少的情況,也即數據的更新主要由不同的業務數據操作而導致。注意:某些狀態存儲中間件是不支持ETag的,所以Dapr進行了額外的處理模擬了這一機制。
內置一致性處理機制:Dapr支持兩種一致性處理——強一致性和最終一致性。對於強一致性,Dapr會等待所有底層請求返回確認信息才最終完成操作;最終一致性不會等待底層請求確認。
批量處理:Dapr的狀態管理提供了兩種模式的批量處理。Bulk模式用於把一種同類型的請求合並,這種時候不會保證事務性;Multi模式可以把不同類型的請求一起發送,可以保證事務性。
需要注意的是由於Dapr需要支持盡量多的狀態存儲源,所以必然有一些存儲源是無法支持以上所有的能力的(主要是事務能力),可以通過瀏覽這個列表來確認存儲源的支持情況,還算我們常用的Redis、SQL Server、MySQL、PostgreSQL和CosmosDB是可以完整支持。
規范
Dapr狀態管理構件塊由於提供了這種特定的能力給你的微服務使用,所以給使用的方式制訂了如下規范:
- 由於狀態管理依賴於狀態組件,所以首先規定了應用狀態組件的聲明格式
- 從概念所知,狀態的存儲需要依賴狀態鍵,所以接着規定了鍵的構成方式
- 最為重要的規范是規定了狀態存取的HTTP/gRPC的地址格式
狀態組件聲明
通過如下yaml文件來聲明對狀態組件的引用(在本地開發環境可以不聲明,使用默認的狀態存儲源):
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: <NAME>
namespace: <NAMESPACE>
spec:
type: state.<TYPE>
metadata:
- name:<KEY>
value:<VALUE>
- name: <KEY>
value: <VALUE>
由於整個聲明的解釋,會在后續單獨的組件文章中詳細展開。我們只要記住其中的 metadata.name
代表了存儲源的名稱,對於應用程序而言需要匹配的就是這個名稱。另外 spec.type
代表了存儲源所使用的存儲類型,這個對於應用開發者而言可以了解到存儲源是否具備完整的狀態存儲能力。
鍵的組成模式
從上所知,狀態管理構件塊是以鍵值的方式保存數據的,為了保證和存儲源的兼容,那么就需要按照一定的模式來定義鍵的組成。
普通的(非Actor)狀態的鍵為:<App ID>||<state key>
對於應用開發者而言,其實只需要關心 <state key>
即可。
請求地址
要對狀態進行操作,需要對如下地址進行HTTP/gRPC請求:http://localhost:<daprPort>/v1.0/state/<storename>
其中daprPort代表了Dapr邊車的特定協議端口,HTTP默認50001或者gRPC默認3500;storename即是在組件聲明中的 metadata.name
。
保存狀態
對上述地址進行POST請求,並傳遞一個鍵值對(外加可選的etag)的數組作為請求體,比如:
[
{
"key": "weapon",
"value": "DeathStar",
"etag": "1234"
},
{
"key": "planet",
"value": {
"name": "Tatooine"
}
}
]
獲取狀態
對上述地址進行GET請求,並傳遞狀態鍵作為路由參數:
GET http://localhost:<daprPort>/v1.0/state/<storename>/<key>
返回結果是一個json的對象,具體格式是由你確定(即你保存狀態的時候傳入什么格式);另外etag會附加在響應頭 ETag
當中。
以bulk的方式獲取狀態
對上述地址進行POST/PUT請求,並傳遞 bulk
作為路由參數:
POST/PUT http://localhost:<daprPort>/v1.0/state/<storename>/bulk
同時再構建一個如下格式的請求體,把需要獲取的狀態鍵放到 keys
數組當中,同時設定 parallelism
的值來確定在存儲源中執行查找操作的並行度(如果狀態是以分區的方式保存在存儲源中的話):
{
"keys": [ "key1", "key2" ],
"parallelism": 10
}
請求后,響應體是一個包含了鍵值對數組的json對象:
[
{
"key": "key1",
"data": "value1",
"etag": "1"
},
{
"key": "key2",
"data": "value2",
"etag": "1"
}
]
刪除狀態
對上述地址進行GET請求,並傳遞狀態鍵作為路由參數:
DELETE http://localhost:<daprPort>/v1.0/state/<storename>/<key>
刪除狀態請求沒有響應體,通過響應狀態碼204來確認刪除成功。
事務操作
如果你希望一次請求執行多步操作的話,可以使用這種請求方式。這種請求由於是支持事務的,所以並非所有存儲源都支持。
對上述地址進行POST/PUT請求,並傳遞 transaction
作為路由參數:
POST/PUT http://localhost:<daprPort>/v1.0/state/<storename>/transaction
請求體是一個操作的數組,標明了各個操作要完成的操作類型和狀態內容,如:
{
"operations": [
{
"operation": "upsert",
"request": {
"key": "key1",
"value": "myData"
}
},
{
"operation": "delete",
"request": {
"key": "key2"
}
}
],
"metadata": {
"partitionKey": "planet"
}
}
其中 operation
有兩種類型:upsert
(更新或插入)和 delete
(刪除)。
目前支持事務操作的存儲源有:
- Redis
- MongoDB
- MySQL
- RethinkDB
- PostgreSQL
- SQL Server
- Azure CosmosDB
DOTNET SDK
由於狀態管理構件塊為你的應用程序提供了一些和狀態相關的操作接口,SDK除了提供 DaprClient
這個客戶端封裝類方便你使用.NET函數庫來操作狀態以外,也為ASP.NET Core提供了更加便捷的模型綁定屬性標記類 FromStateAttribute
方便你在Controller中通過屬性綁定的方式來獲取狀態。
DaprClient中和狀態相關的方法有:
- GetStateAsync:基於storeName和key獲取狀態值
- GetBulkStateAsync:基於storeName和keys列表獲取多個狀態值
- GetStateAndETagAsync:基於storeName和key獲取狀態值和etag
- GetStateEntryAsync:基於storeName和key獲取StateEntry封裝類,此類包含了狀態的更詳細信息
- SaveStateAsync:基於storeName和key保存狀態值
- ExecuteStateTransactionAsync:執行狀態事務操作
- DeleteStateAsync:基於storeName和key刪除狀態值
FromStateAttribute可以在Controller的Action中直接獲取StateEntry,如:
[HttpGet("{account}")]
public ActionResult<Account> Get([FromState(StoreName)] StateEntry<Account> account)
{
if (account.Value is null)
{
return this.NotFound();
}
return account.Value;
}
用法與例子
其實通過上面對規范的講解,對狀態管理的基本用法應該有一定的理解了。官方文檔給出了如下文章來分別講述了狀態管理構件塊的3種使用場景:
- 如何保存和獲取狀態:基本的HTTP使用方式,如果希望看到DOTNET SDK的使用方式,需要參考(https://github.com/dapr/dotnet-sdk)中的例子,其中包含了Client的使用和ASP.NET Core中的使用
- 如何構建有狀態服務,其依賴了狀態管理構件塊提供的並發和一致性特性
- 如何在服務之間共享狀態,通過給狀態存儲源設置不同的keyPrefix策略讓不同的服務之間可以以特定的鍵組成格式來讀取同一個存儲源
另外,我的dapr-dotnet-quickstarts開源項目(https://github.com/heavenwing/dapr-dotnet-quickstarts)也包含了狀態管理構件塊的基本用法的例子:
- StateManagement:使用原生的HTTP請求來保存、獲取和刪除狀態
- StateManagementWithSdk:使用SDK的DaprClient來保存、獲取和刪除狀態