冠军

导航

Dapr-6: Dapr 状态管理构建块

第 6 章 Dapr 状态管理构建块

The Dapr state management building block | Microsoft Docs

分布式应用程序由一组独立的服务构成。尽管每个服务应当是无状态的,有些服务必须却必须跟踪状态的变化以生成业务操作。例如对于电子商务系统中的购物车服务,如果该服务不能跟踪状态,那么在离开站点的时候,顾客就会丢失购物车的内容,导致销售机会的丧失和顾客体验的下降。对于这些场景,状态需要被持久化到分布式的状态存储中。Dapr 状态管理构建块简化了状态跟踪处理,并提供跨多种数据存储的高级功能。

尝试状态管理构建块的话,可以看一下第 3 章中的计数器示例应用程序。

处理何种问题

在分布式应用程序中跟踪状态是一种挑战,例如:

  • 应用程序可能需要不同类型的数据存储
  • 对访问数据和更新数据的不同一致性级别
  • 在同一时间可能由多个用户更新数据,需要冲突处理方案
  • 在与数据存储交互的时候,出现暂时的瞬时错误必须能够进行重试

Dapr 状态管理构建块定位于处理这些挑战,在不需要依赖,或者学习第 3 方存储 SDK 学习曲线的情况下,Dapr 平滑地处理了状态跟踪问题。

重要:

Dapr 状态管理提供 key/value API。该功能不支持关系存储,以及图数据存储。

如何工作

应用程序与 Dapr 的 sidebar 交互来存储并提供 key/value 数据。在该机制的背后,sidebar API 基于可配置的状态存储组件来持久化数据。开发人员可以从众多支持的状态存储中选择,包括 Azure Cosmos DB,SQL Server,以及 Cassandra。

该 API 即可以使用 HTTP 也可以使用 gRPC 进行调用,使用如下所示的 URL 调用 HTTP API

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

图 5-1 展示了使用 Dapr 的购物车服务,使用名为 statestore 的 Dapr 存储组件存储键值对。

图 5-1 在 Dapr 状态存储中存储 key/value

注意前图中的步骤:

  1. 购物车服务调用 Dapr sidebar 的状态管理 API。请求的 body 部分封装了多个 key/value 对。
  2. Dapr sidebar 基于组件配置文件检查状态存储,在本例中,使用 Redis 缓存状态存储。
  3. sidecar 持久化数据到 Redis cache 中

获取存储的数据通过简单的 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 理论的 3 个属性。

图 5-2 CAP 理论

CAP 理论指出分布式系统在一致性、可用性和分区容差之间的权衡。进而,任何数据分布式数据存储只能保证三种属性中的两种。

  • 一致性(C)。群集中的每个节点都会使用最新的数据进行响应,即使系统必须在所有副本更新之前阻止请求。如果查询当前正在更新的项目的“一致系统”,则在所有副本成功更新之前,不会得到响应。但是,您将始终收到最新的数据。
  • 可用性(A)。每个节点都会立即返回响应,即使该响应不是最新的数据。如果您在“可用系统”中查询正在更新的项目,您将得到该服务在此时可能提供的最佳答案。
  • 分区公差(P)。保证即使复制数据节点出现故障或与其他复制数据节点失去连接,系统仍能继续运行。

分布式应用程序必须处理 P 属性。当服务通过网络呼叫相互通信时,将发生网络中断(P)。考虑到这一点,分布式应用程序必须是 AP 或 CP。

AP:应用程序选择可用性而不是一致性。Dapr 通过其 eventual consistency 最终一致性策略支持这一选择。考虑底层数据存储,例如Azure CosmosDB,它在多个副本上存储冗余数据。为了最终的一致性,状态存储将更新写入一个副本,并与客户端一起完成写入请求。在此之后,存储将异步更新其副本。读取请求可以从任何副本返回数据,包括尚未收到最新更新的副本。
CP:应用程序选择一致性而不是可用性。Dapr 以其 strong consistency 强一致性策略支持这一选择。在这种情况下,状态存储将在完成写入请求之前同步更新所有(或在某些情况下,一组)所需的副本。读取操作将在多个副本中一致地返回最新数据。
状态操作的一致性级别是通过向操作附加 一致性提示 来指定的。以下 curl 命令使用强一致性提示将 Hello=World键/值对写入状态存储:

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 存储组件完成该操作的一致性提示。不是所有的数据存储都支持所有这两种一致性级别。如果没有设置一致性提示,默认的行为为 eventual

并发性

在多用户应用程序中,多个用户有可能同时更新相同的数据。Dapr 支持乐观并发控制(OCC)来管理冲突。OCC基于这样一种假设:由于用户处理数据的不同部分,所以更新冲突不常见。更有效的方法是假设更新会成功,如果不成功则重试。另一种方法是实现悲观锁定,这会影响性能,因为长时间运行的锁定会导致数据争用。
Dapr 支持使用 ETag 的乐观并发控制(OCC)。ETag 是与存储的密钥/值对的特定版本关联的值。每次密钥/值对更新时,ETag 值也会更新。当客户端检索密钥/值对时,响应包括当前 ETag 值。当客户端更新或删除密钥/值对时,它必须将该 ETag 值发送回请求正文。如果另一个客户端同时更新了数据,ETag 将不匹配,请求将失败。此时,客户端必须检索更新的数据,再次进行更改,然后重新提交更新。这种策略称为 first-write-wins
Dapr 还支持 last-write-wins 策略。使用这种方法,客户机不会将 ETag 附加到写请求。状态存储组件将始终允许更新,即使在会话期间基础值已更改。last-write-wins 对于低数据争用的高吞吐量写入场景非常有用。此外,可以容忍覆盖偶尔的用户更新。

事务

Dapr可以将多项更改作为一个事务实现的单个操作写入数据存储。此功能仅适用于支持 [ACID 事务](https://en.wikipedia.org/wiki/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" }
          }
        ]
      }'

对于不支持事务的数据存储,多组键值仍然可以作为单个请求发送。下面的示例展示了 bulk 写操作。

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 平台提供特定语言的支持。开发者可以使用在第 3 章中介绍的 DaprClient 类来读取和写入数据。下面的示例展示了如何使用 DaprClient.GetStateAsync<TVale> 方法从数据存储中读取数据。该方法需要使用存储的名称,statestore 和键值来作为参数:

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

如果存储中没有关于键值 AWS 的数据,返回结果为 default(WeatherForecast)

写入数据到数据存储中,使用 DaprClient.SaveStateAsync<TValue> 方法。

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

示例使用 last-write-wins 策略,因为没有提供 ETag 到存储组件中。为了使用乐观并发控制 (OCC) 的 first-write-wins 策略,首先使用 DaprClient。GetStateAndEtagAsync 方法获取当前的 ETag 值,然后带有获取的 Etag 来写入更新之后的值,使用 DaprClient.TrySaveStateAsync 方法。

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 方法将会失败。该方法返回一个 boolean 值来指示该调用是否成功。处理失败的一种策略是简单地从存储中重新加载数据,重新变更,最后重新提交更新。

如果你总是期望写入成功,而不管对数据的其它变更,使用 last-write-wins 策略。

该 SDK 提供了其它方法来批量获取数据,删除数据,以及执行事务。更多信息,请参考:Dapr .NET SDK repository

ASP.NET Core 集成

Dapr 也支持 ASP.NET Core,一种跨平台的构建现代云基础的 Web 应用程序的框架。Dapr SDK 提供了直接集成状态管理能力到 ASP.NET Core 模型绑定 的能力。通过在 IMVCBuilder.AddDapr 后面追加 .AddDapr 扩展方法到你的 Startup.cs 类中进行简单的配置。如下所示:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddDapr();
}

一旦配置完成,Dapr 可以通过 ASP.NET Core 的 FromState 特性,直接注入 key/value 对到控制器的 Action 方法中。不再需要引用 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,它是路由模板变量的名称,用来获取状态的键值。如果你忽略掉第二个参数,绑定方法的参数 forecast 将被用于查找路由模板变量。

StateEntry 包含提取的键值对的所有信息的属性:StoreNmaeKeyValue 以及 Etag。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
  • MongoDB
  • PostgreSQL
  • Redis

Dapr 还包括对支持 CRUD,但是没有事务支持的状态存储:

  • Aerospike
  • Azure Blob Storage
  • Azure Table Storage
  • Cassandra
  • Cloudstate
  • Couchbase
  • etcd
  • Google Cloud Firestore
  • Hashicorp Consul
  • Hazelcast
  • Memcached
  • 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,指示状态存储如何被 actors 构建块所消费。

键前缀策略

在底层存储中,状态管理组件使用不同的策略来管理存储的 key/value 对。回顾签名的示例

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 用于数据的 Key 是 basketservice||basket1。默认情况下,Dapr 使用 Dapr 实例的应用程序标识 (application_id) 作为键的前缀。这种命名约定使得多个 Dapr 实例可以共享同一个数据存储而不会出现键名称冲突。对于开发者来说,关键是在运行使用 Dapr 的应用程序时,总是指定相同的 application_id。如果忽略了,Dapr 将生成一个唯一的 application ID。而如果 application id 变化了,应用程序就不能访问使用以前的键保存的状态了。

也就是说,在

示例应用:Dapr 流量管控

总结

posted on 2022-03-18 15:22  冠军  阅读(274)  评论(0编辑  收藏  举报