Dapr 状态管理构建基块

提示

此内容摘自电子书《面向 .NET 开发人员的 Dapr》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

《面向 .NET 开发人员的 Dapr》电子书封面缩略图。

分布式应用程序由独立服务组成。 虽然每个服务都应是无状态的,但某些服务必须跟踪状态才能完成业务操作。 请考虑电子商务网站的购物篮服务。 如果服务无法跟踪状态,则客户可能因为离开网站丢失购物篮内容,从而导致销售损失和不愉快的客户体验。 对于这些情况,需要将状态持久保存在分布式状态存储中。 Dapr 状态管理构建基块简化了状态跟踪,并跨各种数据存储提供高级功能。

若要试用状态管理构建基块,请参阅第 3 章中的计数器应用程序示例

它的用途

跟踪分布式应用程序中的状态可能很有挑战性。 例如:

  • 应用程序可能需要不同类型的数据存储。
  • 访问和更新数据可能需要不同的一致性级别。
  • 多个用户可以同时更新数据,这需要解决冲突。
  • 服务必须重试与数据存储交互时发生的任何短期暂时性错误

Dapr 状态管理构建基块解决了这些难题。 它简化了跟踪状态,没有依赖关系或第三方存储 SDK 学习曲线。

重要

Dapr 状态管理提供密钥/值 API。 该功能不支持关系数据存储或图形数据存储。

工作原理

应用程序与 Dapr sidecar 进行交互,以存储和检索键/值数据。 在后台,sidecar API 使用可配置的状态存储组件来持久保存数据。 开发人员可以从不断增长的受支持状态存储集合中选择,这些存储包括 Azure Cosmos DB、SQL Server 和 Cassandra。

可使用 HTTP 或 gRPC 调用 API。 使用以下 URL 调用 HTTP API:

http://localhost:<dapr-port>/v1.0/state/<store-name>/
  • <dapr-port>:Dapr 侦听的 HTTP 端口。
  • <store-name>:要使用的状态存储组件的名称。

图 5-1 显示已启用 Dapr 的购物篮服务如何使用名为 statestore 的 Dapr 状态存储组件存储键/值对。

在 Dapr 状态存储中存储键/值对示意图。

图 5-1。 在 Dapr 状态存储中存储键/值对。

请注意上图中的步骤:

  1. 购物篮服务在 Dapr sidecar 上调用状态管理 API。 请求正文包含一个 JSON 数组,该数组可以包含多个键/值对。
  2. Dapr sidecar 根据组件配置文件确定状态存储。 在本例中,它是 Redis 缓存状态存储。
  3. sidecar 将数据持久保存到 Redis 缓存。

检索存储的数据是类似的 API 调用。 在以下示例中,curl 命令通过调用 Dapr sidecar API 检索数据:

curl http://localhost:3500/v1.0/state/statestore/basket1

该命令返回响应正文中的存储状态:

{
  "items": [
    {
      "itemId": "DaprHoodie",
      "quantity": 1
    }
  ],
  "customerId": 1
}

以下部分介绍如何使用状态管理构建基块的更高级功能。

一致性

CAP 定理是一组适用于存储状态的分布式系统的原则。 图 5-2 显示了 CAP 定理的三个属性。

CAP 定理。

图 5-2。 CAP 定理。

该定理指出,分布式数据系统将在一致性、可用性和分区容错之间做出权衡。 而且,任何数据存储只能保证三个属性中的两个:

  • 一致性 (C)。 群集中的每个节点都会使用最新数据进行响应(即使系统必须阻止请求),直到所有副本都更新。 如果你向“一致性系统”查询当前正在更新的项,直到所有副本都成功更新,才会获得响应。 不过,你将始终收到最新的数据。

  • 可用性 (A)。 每个节点都会返回即时响应,即使该响应不是最新数据。 如果你向“可用系统”查询正在更新的项,将获得服务此时可以提供的最佳答案。

  • 分区容错 (P)。 保证系统继续运行,即使复制的数据节点发生故障或者与其他复制的数据节点断开连接。

分布式应用程序必须处理 P 属性。 当服务与网络调用相互通信时,会发生网络中断 (P)。 因此,分布式应用程序必须是 AP 或 CP。

AP 应用程序选择可用性,而不选择一致性。 Dapr 通过最终一致性策略支持此选择。 请考虑基础数据存储(例如 Azure CosmosDB),它在多个副本上存储冗余数据。 借助最终一致性,状态存储会将更新写入副本,并完成客户端的写入请求。 之后,存储将异步更新其副本。 读取请求可以从任何副本返回数据(包括尚未收到最新更新的副本)。

CP 应用程序选择一致性,而不选择可用性。 Dapr 通过其强一致性策略支持此选择。 在此方案中,状态存储将在完成写入请求前同步更新所有必需副本(或者,在某些情况下,达到仲裁)。 读取操作将跨副本一致地返回最新数据。

通过向操作附加一致性提示来指定状态操作的一致性级别。 以下 curl 命令使用强一致性提示将 键/值对写入状态存储:

curl -X POST http://localhost:3500/v1.0/state/<store-name> \
  -H "Content-Type: application/json" \
  -d '[
        {
          "key": "Hello",
          "value": "World",
          "options": {
            "consistency": "strong"
          }
        }
      ]'

重要

由 Dapr 状态存储组件完成附加到该操作的一致性提示。 并非所有数据存储都支持这两种一致性级别。 如果未设置一致性提示,则默认行为是最终一致性。

并发

在多用户应用程序中,多个用户可能(同时)更新同一数据。 Dapr 支持使用乐观并发控制 (OCC) 来管理冲突。 OCC 基于以下假设:更新冲突不常见,因为用户处理数据的不同部分。 更有效的做法是假设更新会成功,如果失败则重试。 实现悲观锁定的替代方法可能会影响性能,长时间运行的锁定会导致数据争用。

Dapr 支持使用 ETag 进行乐观并发控制 (OCC)。 ETag 是一个与存储的键/值对的特定版本关联的值。 每次键/值对更新时,ETag 值也会更新。 当客户端检索键/值对时,响应包括当前的 ETag 值。 客户端更新或删除键/值对时,必须将该 ETag 值发送回请求正文中。 如果另一个客户端同时更新了数据,则 ETag 不匹配,请求将失败。 此时,客户端必须检索已更新的数据,再次进行更改,然后重新提交更新。 此策略称为“先写入优先”。

Dapr 还支持“后写入优先”策略。 通过此方法,客户端不会将 ETag 附加到写入请求。 状态存储组件将始终允许更新(即使基础值在会话期间已更改)。 后写入优先非常适用于低数据争用的高吞吐量写入方案。 也可以容忍覆盖偶尔的用户更新。

事务

Dapr 可以将多项更改写入数据存储,作为事务实现的单个操作。 此功能仅适用于支持 ACID 事务的数据存储。 撰写本文时,这些存储包括 Redis、MongoDB、PostgreSQL、SQL Server 和 Azure CosmosDB。

在以下示例中,多项操作将发送到单个事务中的状态存储。 所有操作都必须成功,事务才能提交。 如果一个或多个操作失败,则回退整个事务。

curl -X POST http://localhost:3500/v1.0/state/<store-name>/transaction \
  -H "Content-Type: application/json" \
  -d '{
        "operations": [
          {
            "operation": "upsert",
            "request": { "key": "Key1", "value": "Value1"
            }
          },
          {
            "operation": "delete",
            "request": { "key": "Key2" }
          }
        ]
      }'

对于不支持事务的数据存储,仍可将多个密钥作为单个请求发送。 以下示例演示批量写入操作:

curl -X POST http://localhost:3500/v1.0/state/<store-name> \
  -H "Content-Type: application/json" \
  -d '[
        { "key": "Key1", "value": "Value1" },
        { "key": "Key2", "value": "Value2" }
      ]'

对于批量操作,Dapr 将每个键/值对更新作为单独的请求提交到数据存储。

使用 Dapr .NET SDK

Dapr .NET SDK 为 .NET 平台提供特定于语言的支持。 开发人员可以使用DaprClient中引入的 DaprClient 类来读取和写入数据。 以下示例演示了如何使用 DaprClient.GetStateAsync<TValue> 方法从缓存中检索数据。 方法需要存储名称 statestore 和密钥 AMS 作为参数:

var weatherForecast = await daprClient.GetStateAsync<WeatherForecast>("statestore", "AMS");

如果状态存储不包含密钥 AMS 的数据,则结果为 default(WeatherForecast)

若要将数据写入数据存储,请使用 DaprClient.SaveStateAsync<TValue> 方法:

daprClient.SaveStateAsync("statestore", "AMS", weatherForecast);

该示例使用“后写入优先”策略,因为 ETag 值不会传递给状态存储组件。 若要将乐观并发控制 (OCC) 与“先写入优先”策略一起使用,请首先使用 方法检索当前的 ETag。 然后,写入更新的值,并使用 DaprClient.TrySaveStateAsync 方法传递检索到的 ETag。

var (weatherForecast, etag) = await daprClient.GetStateAndETagAsync<WeatherForecast>("statestore", city);

// ... make some changes to the retrieved weather forecast

var result = await daprClient.TrySaveStateAsync("statestore", city, weatherForecast, etag);

检索数据后,数据(以及关联的 ETag)在状态存储中被更改时,DaprClient.TrySaveStateAsync 方法将失败。 该方法返回一个布尔值,以指示调用是否成功。 处理失败的一种策略是,仅需从状态存储重新加载更新后的数据,再次进行更改,然后重新提交更新。

如果始终希望写入成功,而不考虑对数据的其他更改,请使用“后写入优先”策略。

SDK 提供其他方法用于批量检索数据、删除数据和执行事务。 有关详细信息,请参阅 Dapr .NET SDK 存储库

ASP.NET Core 集成

Dapr 还支持 ASP.NET Core,它是构建基于云的新式 Web 应用程序的跨平台框架。 Dapr SDK 将状态管理功能直接集成到 ASP.NET Core 模型绑定功能中。 配置很简单。 在 Program.cs 文件中,在 WebApplication 生成器上调用以下扩展方法:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers().AddDapr();

配置后,Dapr 可以将键/值对直接注入使用 ASP.NET Core FromState 属性的控制器操作。 不再需要引用 DaprClient 对象。 下一个示例演示返回给定城市天气预报的 Web API:

[HttpGet("{city}")]
public ActionResult<WeatherForecast> Get([FromState("statestore", "city")] StateEntry<WeatherForecast> forecast)
{
    if (forecast.Value == null)
    {
      return NotFound();
    }

    return forecast.Value;
}

在示例中,控制器使用 FromState 属性加载天气预报。 第一个属性参数是状态存储 statestore。 第二个属性参数 city 是获取状态密钥的city变量的名称。 如果省略第二个参数,则将使用绑定方法参数 (forecast) 的名称来查找路由模板变量。

StateEntry 类包含为单个键/值对检索到的各种属性的所有信息:StoreNameKeyValueETag。 ETag 可用于实现乐观并发控制 (OCC) 策略。 该类还提供无需 DaprClient 实例即可删除或更新检索到的键/值数据的方法。 在下一个示例中,TrySaveAsync 方法用于使用 OCC 更新检索到的天气预报。

[HttpPut("{city}")]
public async Task Put(WeatherForecast updatedForecast, [FromState("statestore", "city")] StateEntry<WeatherForecast> currentForecast)
{
    // update cached current forecast with updated forecast passed into service endpoint
    currentForecast.Value = updatedForecast;

    // update state store
    var success = await currentForecast.TrySaveAsync();

    // ... check result
}

状态存储组件

在撰写本文时,Dapr 支持以下事务状态存储:

  • Azure CosmosDB
  • Azure SQL Server
  • CockroachDB
  • 内存中
  • MongoDB
  • MySQL
  • Oracle 数据库
  • PostgreSQL
  • Redis
  • RethinkDB

Dapr 还包括对支持 CRUD 操作但不支持事务功能的状态存储的支持:

  • Aerospike
  • Apache Cassandra
  • AWS DynamoDB
  • Azure Blob 存储
  • Azure 表存储
  • Couchbase
  • GCP Firestore
  • Hashicorp Consul
  • Hazelcast
  • JetStream KV
  • Memcached
  • Oracle 对象存储
  • Zookeeper

配置

初始化本地自承载开发时,Dapr 将 Redis 注册为默认状态存储。 以下是默认状态存储配置的示例。 记住默认名称 statestore

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"

注意

许多状态存储可以注册到单个应用程序,每个应用程序具有不同的名称。

Redis 状态存储需要 redisHostredisPassword 元数据来连接到 Redis 实例。 在以上示例中,Redis 密码(默认情况下为空字符串)存储为纯字符串形式。 最佳做法是避免使用纯文本字符串,并始终使用机密引用。 若要详细了解机密管理,请参阅第 10 章

另一个元数据字段 actorStateStore 指示执行组件构建基块是否可以使用状态存储。

密钥前缀策略

状态存储组件支持不同的策略在基础存储中存储键/值对。 回想一下前面有关购物篮服务的示例,该服务存储客户想要购买的项目:

curl -X POST http://localhost:3500/v1.0/state/statestore \
  -H "Content-Type: application/json" \
  -d '[{
        "key": "basket1",
        "value": {
          "customerId": 1,
          "items": [
            { "itemId": "DaprHoodie", "quantity": 1 }
          ]
        }
     }]'

使用 Redis 控制台工具,在 Redis 缓存中查看 Redis 状态存储组件如何持久保存数据:

127.0.0.1:6379> KEYS *
1) "basketservice||basket1"

127.0.0.1:6379> HGETALL basketservice||basket1
1) "data"
2) "{\"items\":[{\"itemId\":\"DaprHoodie\",\"quantity\":1}],\"customerId\":1}"
3) "version"
4) "1"

输出将数据的完整 Redis 密钥显示为 。 默认情况下,Dapr 使用 Dapr 实例 (basketservice) 的 application id 作为密钥的前缀。 此命名约定使多个 Dapr 实例可以共享相同的数据存储,而不会发生密钥名称冲突。 对于开发人员,在使用 Dapr 运行应用程序时,必须始终指定相同的 application id。 如果省略,Dapr 将生成唯一的应用程序 ID。 如果更改 application id,应用程序将无法再访问使用上一个密钥前缀存储的状态。

也就是说,在状态存储组件文件的 元数据字段中,可以配置密钥前缀的常量值。 请考虑以下示例:

spec:
  metadata:
  - name: keyPrefix
  - value: MyPrefix

常量密钥前缀允许跨多个 Dapr 应用程序访问状态存储。 此外,将 keyPrefix 设置为 none 完全省略前缀。

示例应用程序:Dapr 流量控制

在 Dapr 流量控制示例应用中,TrafficControl 服务使用 Dapr 状态管理构建基块来保存每辆过往车辆的进出时间戳。 图 5-3 显示了 Dapr 流量控制示例应用程序的概念体系结构。 Dapr 状态管理构建基块在图中标记有数字 3 的流中使用:

Dapr 流量控制示例应用程序的概念体系结构。

图 5-3。 Dapr 流量控制示例应用程序的概念体系结构。

入口和出口事件逻辑由 TrafficController 类(一个普通的 ASP.NET 控制器)处理。 TrafficController.VehicleEntry 方法接受传入 VehicleRegistered 消息并保存封闭车辆状态:

// store vehicle state
var vehicleState = new VehicleState
{
    LicenseNumber = msg.LicenseNumber,
    EntryTimestamp = msg.Timestamp
};
await _vehicleStateRepository.SaveVehicleStateAsync(vehicleState);

在以上代码片段中,抽象 _vehicleStateRepository 负责将状态保存到数据存储。 具体实现 DaprVehicleStateRepository 如下所示:

public class DaprVehicleStateRepository : IVehicleStateRepository
{
    private const string DAPR_STORE_NAME = "statestore";
    private readonly DaprClient _daprClient;

    public DaprVehicleStateRepository(DaprClient daprClient)
    {
        _daprClient = daprClient;
    }

    public async Task SaveVehicleStateAsync(VehicleState vehicleState)
    {
        await _daprClient.SaveStateAsync<VehicleState>(
            DAPR_STORE_NAME, vehicleState.LicenseNumber, vehicleState);
    }

    public async Task<VehicleState> GetVehicleStateAsync(string licenseNumber)
    {
        return await _daprClient.GetStateAsync<VehicleState>(
            DAPR_STORE_NAME, licenseNumber);
    }
}

如前面的代码片段所示,实现 DaprVehicleStateRepository 类非常简单。 SaveVehicleStateAsync 方法使用注入的 DaprClient 对象将状态保存到配置的 Dapr 状态存储。 它使用车辆许可证号作为密钥。 应用程序可以通过调用 GetVehicleStateAsync 方法检索保存的状态。

TrafficControl 服务使用 Redis 作为其基础数据存储。 只查看代码,你永远不会了解它。 使用 Dapr 状态管理构建基块的服务不会直接引用任何状态组件。 而 Dapr 组件配置文件指定存储:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
  namespace: dapr-trafficcontrol
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    secretKeyRef:
      name: state.redisPassword
      key: state.redisPassword
scopes:
  - trafficcontrolservice

注意

组件配置文件包含元素 secretKeyRef。 应用程序使用它从 Dapr 机密构建块引用 Redis 密码值。 请参阅第 10 章,详细了解如何通过 Dapr 管理机密。

配置中的 type 元素 state.redis 指示构建基块使用 Dapr Redis 组件管理状态。

配置中的 scopes 元素限制应用程序对状态存储组件的访问。 只有 TrafficControl 服务可以访问状态存储。

总结

Dapr 状态管理构建块提供了一个 API,用于在各种数据存储区中存储键/值数据。 API 支持以下内容:

  • 批量操作
  • 强一致性和最终一致性
  • 乐观并发控制
  • 多项事务

.NET SDK 为 .NET 和 ASP.NET Core 提供特定于语言的支持。 模型绑定集成简化了从 ASP.NET Core 控制器操作方法访问和更新状态。

在 Dapr 流量控制示例应用程序中,使用 Dapr 状态管理的好处显而易见:

  1. 它还消除了使用第三方 SDK(例如 StackExchange.Redis)的复杂性。
  2. 将基础 Redis 缓存替换为不同类型的数据存储只需要更改组件配置文件。

参考