Dapr微服务应用开发系列4:状态管理构件块
原理
要用好这个构件块,首先需要正确理解状态管理的概念。
大部分微服务开发框架或者说指导,都提倡微服务以无状态类型的方式来运行,这种无状态微服务当然更容易进行伸缩,但是在遇到需要处理一些类似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来保存、获取和删除状态