使用.NET 6和Dapr进行分布式服务开发 – 第四部分:状态管理(State Management)
通过状态管理保持微服务的状态
我們一直以來都是按照以下的流程進行工作。這次我們想要提及利用Dapr中提供的狀態管理功能來進行狀態維護。
关于状态管理(State Management)
状态管理(State Management) 是指在每个服务中使用的状态维持存储库。就像会话(session)和电子商务(EC)中的购物车状态一样,它也可以用作此类状态存储库。在Dapr中,它作为组件功能进行提供,通过在组件中进行以下服务配置来进行抽象化,可以方便地用作会话存储(session store),从而使访问变得容易。
由于我们目前只在本地执行,您可能会问这样做有什么好处。但是当将来部署到Kubernetes并扩展到两个Pod时,状态管理将非常方便。换句话说,即使在负载均衡器将服务的Pod分配给不同的节点时,服务的Pod也可以访问相同的状态管理。
-
- Azure CosmosDB
-
- Azure SQL Server
-
- MongoDB
-
- PostgreSQL
-
- Redis
-
- Aerospike
-
- Azure Blob Storage
-
- Azure Table Storage
-
- Cassandra
-
- Cloudstate
-
- Couchbase
-
- etcd
-
- Google Cloud Firestore
-
- Hashicorp Consul
-
- Hazelcast
-
- Memcached
- Zookeeper
既然这也是个更快的选择,我会试试看。
设定组件
首先,我们将编辑上次项目中的 tye.yaml 文件。取消以下注释,添加注释:将 components-path: “./components/” 这一行注释掉。同时,还需要创建一个 components 文件夹。
# tye application configuration file
# read all about it at https://github.com/dotnet/tye
#
# when you've given us a try, we'd love to know what you think:
# https://aka.ms/AA7q20u
#
name: dapr
extensions:
- name: dapr
# log-level configures the log level of the dapr sidecar
log-level: debug
# config allows you to pass additional configuration into the dapr sidecar
# config will be interpreted as a named k8s resource when deployed, and will be interpreted as
# a file on disk when running locally at `./components/myconfig.yaml`
#
# config: myconfig
# components-path configures the components path of the dapr sidecar
# 以下のコメントを外す
components-path: "./components/"
# If not using the default Dapr placement service or otherwise using a placement service on a nonstandard port,
# you can configure the Dapr sidecar to use an explicit port.
# placement-port: 6050
services:
- name: service-a
project: ServiceA/ServiceA.csproj
- name: service-b
project: ServiceB/ServiceB.csproj
- name: app
project: App/App.csproj
# This may conflict with the redis instance that dapr manages.
#
# Doing a `docker ps` can show if its already running. If that's the case
# then comment out out when running locally.
# - name: redis
# image: redis
# bindings:
# - port: 6379
设置状态管理
我们会在之前添加的components文件夹中新增statestore.yaml文件。下面是一个使用Redis进行状态存储的示例。你可能会想知道这个访问的Redis在哪里,不用担心,在执行dapr init时,会预先设置好这个Redis。
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"
当我运行“docker ps”命令时,发现Redis在本地的6379端口上正在运行。
C:\Users\user>docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba8a9bdb62ab daprio/dapr:1.6.0 "./placement" 18 hours ago Up 18 hours 0.0.0.0:6050->50005/tcp dapr_placement
808b6358b4e7 openzipkin/zipkin "start-zipkin" 5 days ago Up 27 hours (healthy) 9410/tcp, 0.0.0.0:9411->9411/tcp dapr_zipkin
bed085e54a77 redis "docker-entrypoint.s…" 5 days ago Up 27 hours 0.0.0.0:6379->6379/tcp dapr_redis
先启动并确认一下。
如果能够做到这一点,让我们使用tye进行一次启动确认吧。
C:\Users\user\DaprQiita>tye run
Loading Application Details...
Launching Tye Host...
[13:52:20 INF] Executing application from C:\Users\user\DaprQiita\tye.yaml
[13:52:20 INF] Dashboard running on http://127.0.0.1:8000
[13:52:20 INF] Build Watcher: Watching for builds...
[13:52:20 INF] Building projects
[13:52:22 INF] Application dapr started successfully with Pid: 11604
[13:52:22 INF] Launching service service-a-dapr_9f730849-f: C:/dapr/dapr.exe run --app-id service-a --dapr-grpc-port 51097 --dapr-http-port 51098 --metrics-port 51099 --app-port 51091 --components-path ./components/ --log-level debug
[13:52:22 INF] Launching service service-b-dapr_4249ed66-1: C:/dapr/dapr.exe run --app-id service-b --dapr-grpc-port 51100 --dapr-http-port 51101 --metrics-port 51102 --app-port 51093 --components-path ./components/ --log-level debug
[13:52:22 INF] Launching service app-dapr_ba083775-8: C:/dapr/dapr.exe run --app-id app --dapr-grpc-port 51103 --dapr-http-port 51104 --metrics-port 51105 --app-port 51095 --components-path ./components/ --log-level debug
在Sidecar的Dapr过程中,已经添加了–components-path ./components/选项。
[13:52:22 INF] Launching service service-a-dapr_9f730849-f: C:/dapr/dapr.exe run --app-id service-a --dapr-grpc-port 51097 --dapr-http-port 51098 --metrics-port 51099 --app-port 51091 --components-path ./components/ --log-level debug
保存和加载状态
这次我们为了保存状态,尝试创建一个新的服务。
在项目的根目录下执行以下命令。
dotnet new webapi -o StateService
dotnet sln add StateService
我认为完成之后会是这个样子。
不要忘记在tye.yaml中添加项目。
# tye application configuration file
# read all about it at https://github.com/dotnet/tye
#
# when you've given us a try, we'd love to know what you think:
# https://aka.ms/AA7q20u
#
name: dapr
extensions:
- name: dapr
# log-level configures the log level of the dapr sidecar
log-level: debug
# config allows you to pass additional configuration into the dapr sidecar
# config will be interpreted as a named k8s resource when deployed, and will be interpreted as
# a file on disk when running locally at `./components/myconfig.yaml`
#
# config: myconfig
# components-path configures the components path of the dapr sidecar
components-path: "./components/"
# If not using the default Dapr placement service or otherwise using a placement service on a nonstandard port,
# you can configure the Dapr sidecar to use an explicit port.
# placement-port: 6050
services:
- name: service-a
project: ServiceA/ServiceA.csproj
- name: service-b
project: ServiceB/ServiceB.csproj
- name: app
project: App/App.csproj
- name: stateservice
project: StateService/StateService.csproj
# This may conflict with the redis instance that dapr manages.
#
# Doing a `docker ps` can show if its already running. If that's the case
# then comment out out when running locally.
# - name: redis
# image: redis
# bindings:
# - port: 6379
编辑项目
首先,让我们像上次一样,在项目中添加Dapr的客户端库。这次我们使用了Visual Studio 2022的图形界面来进行添加。
如果可以追加的话,请参考以下内容来重新书写项目的Program.cs文件。
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDaprClient(); // この行追加
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 以下のリダイレクションはコメントアウト(backgroundでは、httpのみで通信)
//app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
接下来,我们来编辑WeatherForecastController.cs。
我写得有点随意,所以没有写异常处理之类的。在使用的时候,请务必写好。
内容很简单,当以GET方式调用时,会从指定的键和状态存储区域中读取出常量指定的值。
而对于POST请求,则会使用同样的键将数据保存到状态存储区域中。
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
namespace StateService.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly DaprClient _daprClient;
const string storeName = "statestore";
const string key = "LastWeatherForecast";
public WeatherForecastController(ILogger<WeatherForecastController> logger, DaprClient daprClient)
{
_logger = logger;
_daprClient = daprClient;
}
[HttpGet(Name = "GetWeatherForecast")]
public async Task<WeatherForecast> GetAsync()
{
return await _daprClient.GetStateAsync<WeatherForecast>(storeName, key);
}
[HttpPost(Name = "PostWeatherForecast")]
public async Task PostAsync(WeatherForecast weatherForecast)
{
await _daprClient.SaveStateAsync(storeName, key, weatherForecast);
return;
}
}
确认
到了这个地步,尝试用 tye run 命令启动,并确认只有这个服务在运行。
关于调试,请参考上一篇文章,尝试附加进程进行调试。
以下是服务已启动的情况。在这次的tye中,stateservice的进程被分配到了 http://localhost:64377。
这次我被分配到了 http://localhost:64377/swagger,在Swagger上尝试访问一下。
首先,进行POST并进行确认。我将温度适当设定为100度。
由于响应代码为200,表示运行正常。
服务调用和状态存储的控制
现在,我们关心的是每个服务的状态是否通过Dapr共享?或者,如果服务不同,会被视为不同的状态吗?因此,接下来,我想尝试以下情况。
-
- 在前端的App中,使用App端的Dapr直接将数据保存到Statestore中。
-
- 在前端的App中,使用App端的Dapr直接从Statestore读取数据。(应该与第一句保存的内容相同)
- 然后,使用Service invocation尝试从Statestore服务中以GET方式读取。在前面提到的步骤中,应该返回App中写入的值。
首先,我尝试修改了App中的WeatherForecastController.cs文件如下。
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
namespace App.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly DaprClient _daprClient;
const string storeName = "statestore";
const string key = "LastWeatherForecast";
public WeatherForecastController(ILogger<WeatherForecastController> logger, DaprClient daprClient)
{
_logger = logger;
_daprClient = daprClient;
}
[HttpGet(Name = "GetWeatherForecast")]
public async Task<IEnumerable<WeatherForecast>> GetAsync()
{
WeatherForecast forcast = new WeatherForecast
{
TemperatureC = 1000,
Summary = "from App",
Date = DateTime.Now
};
// Appからステートを保存
await _daprClient.SaveStateAsync(storeName, key, forcast);
// 保存したステートを読み出し
WeatherForecast forcastFromApp = await _daprClient.GetStateAsync<WeatherForecast>(storeName, key);
// サービス間起動でバックエンドに存在するStateserviceからも読み出し
WeatherForecast forcastFromService = await _daprClient.InvokeMethodAsync<WeatherForecast>(HttpMethod.Get, "stateservice", "weatherforecast");
// それぞれの結果をListに格納して返却
List<WeatherForecast> weatherForecasts = new List<WeatherForecast>();
weatherForecasts.Add(forcastFromApp);
weatherForecasts.Add(forcastFromService);
return weatherForecasts;
}
}
当您使用tye启动后,首先可以尝试从Stateservice的Swagger中通过GET方法读取状态值。
{
"date": "2022-03-03T08:54:48.258Z",
"temperatureC": 100,
"temperatureF": 211,
"summary": "string"
}
接下来,我将从App端的Swagger尝试使用GET方式。
在App端的代码中,我有意将”temperatureC”设置为1000。
以下是返回结果。
第一个是在应用程序中记录和检索的结果。第二个是通过调用服务呼叫的结果。从这些结果可以看出,记录的状态是由每个服务单独管理的。
[
{
"date": "2022-03-03T18:26:21.0188215+09:00",
"temperatureC": 1000,
"temperatureF": 1831,
"summary": "from App"
},
{
"date": "2022-03-03T08:54:48.258Z",
"temperatureC": 100,
"temperatureF": 211,
"summary": "string"
}
]
我希望能够在各个服务之间共享状态。
在之前的实验中,我们发现每个服务保存的状态都是单独存储在各个应用程序中的。这是为什么呢?我们将实际查看Redis的内部情况。(我在VSCode中使用了一个名为Database Client的扩展程序。)
我明白了,一看到键就一目了然了。它以“服务名称||键名”的形式保存。
其实,这是因为组件的设置仍然保持默认状态,初始状态将在每个服务单元中进行保持。具体细节请参考以下,但在进行状态存储的设置时,可以通过更改键管理策略来进行相应调整。
请将以下内容添加到statestore.yaml文件中。
- name: keyPrefix
value: "name"
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"
- name: keyPrefix
value: "name"
让我们进行确认,尝试使用tye run进行实施。
我将尝试从应用程序的Swagger页面上进行GET请求,就像之前一样。
在应用程序的代码中,我故意将”temperatureC”设为1000。
以下是被返回的:
[
{
"date": "2022-03-07T16:56:17.3166189+09:00",
"temperatureC": 1000,
"temperatureF": 1831,
"summary": "from App"
},
{
"date": "2022-03-07T16:56:17.3166189+09:00",
"temperatureC": 1000,
"temperatureF": 1831,
"summary": "from App"
}
]
在App中保存了状态,该状态被共享给了stateservice,因此返回的数据是相同的。这意味着两个服务之间可以访问相同的状态。
当键管理策略被命名为Name之后,可以看出服务名||键名变为类别名||键名的元数据。
让我们灵活运用关键策略
在服务内共享的密钥和可在服务之间共享的密钥两者都可以使用。
我会尝试设置两种。在components文件夹下添加一个新的sharestore.yaml,并微调statestore.yaml。
都可以设置
首先,有statestore.yaml文件。这个文件是用来为每个服务单独保存状态的。
也就是说,影响范围仅限于同一服务内部共享。比如,在POD中执行批处理进度管理,或者是在服务内部共享的数据等,看起来很方便。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
接下来是sharestore.yaml。我们将其用于在服务之间共享状态。我们刚才添加了keyPrefix,这意味着它仅在同一服务内共享。例如,在批处理执行期间的进度管理或仅在服务内部共享的数据等情况下,这将非常方便。
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: sharestore
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: localhost:6379
- name: redisPassword
value: ""
- name: actorStateStore
value: "true"
- name: keyPrefix
value: "name"
请注意,actorStateStore只能设置为其中一个选项。关于为什么只能如此,我希望可以在稍后讨论Actor服务模型的时候涉及到这个话题。现在,请将其设置为原样。
代码的修改 de
我试着将App项目的代码修改如下。
using Dapr.Client;
using Microsoft.AspNetCore.Mvc;
namespace App.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly DaprClient _daprClient;
const string shareStore = "sharestore";
const string storeName = "statestore";
const string key = "LastWeatherForecast";
public WeatherForecastController(ILogger<WeatherForecastController> logger, DaprClient daprClient)
{
_logger = logger;
_daprClient = daprClient;
}
[HttpGet(Name = "GetWeatherForecast")]
public async Task<IEnumerable<WeatherForecast>> GetAsync()
{
WeatherForecast forcast = new WeatherForecast
{
TemperatureC = 1000,
Summary = "from App",
Date = DateTime.Now
};
// Sharestoreに保存(これはサービスをまたいで保持参照可能)
// ここでは、仮に全体で識別できるユーザーが以下として、データを保持
var username = "410e0136-f7d5-437f-a844-e40c0ce40e00";
await _daprClient.SaveStateAsync(shareStore, username, "from_users_data");
// Appからステートを保存(これは、Appサービス範囲内でのみ保持)
await _daprClient.SaveStateAsync(storeName, key, forcast);
// Appからステートを読み取り(これは、Appサービス範囲内でのみ保持)
WeatherForecast forcastFromApp = await _daprClient.GetStateAsync<WeatherForecast>(storeName, key);
// サービス間起動でバックエンドに存在するStateserviceからも読み出し
// つまり、Stateservice側でのみ、保持されている値をService Invocation経由で読み出している
WeatherForecast forcastFromService = await _daprClient.InvokeMethodAsync<WeatherForecast>(HttpMethod.Get, "stateservice", "weatherforecast");
// それぞれの結果をListに格納して返却
List<WeatherForecast> weatherForecasts = new List<WeatherForecast>();
weatherForecasts.Add(forcastFromApp);
weatherForecasts.Add(forcastFromService);
return weatherForecasts;
}
}
试一试
让我们确认是否按照以下的设想运行。我觉得你已经逐渐熟悉了,现在让我们使用”tye run”来执行。
首先,从Statestore的Swagger上尝试通过POST进行以下操作。在这里记录的值应该只能通过Statestore进行获取。
{
"date": "2022-03-07T08:34:29.650Z",
"temperatureC": 2000,
"summary": "from state store"
}
即使GET,也可以读取到本次记录的值。
我可以通过Redis中的stateservice||键名来确认已注册。
接下来是App端。我们将从App端的Swagger进行GET尝试。
在App端,我们期望得到以下返回结果。
- 在前端应用中,使用App的Dapr来直接将状态存储到Statestore中。为了易于理解,将温度设置为1000。
{
"date": "2022-03-07T08:34:29.650Z",
"temperatureC": 1000,
"summary": "from app"
}
- 在前台的App中,我们使用App侧的Dapr来直接从Statestore中读取。应该和保存在1中的内容相同。
3. 我用一个临时用户ID将其保存到sharestore。它应该以sharestore || 键名保存在Redis的一侧。
- 下一步,使用Service invocation从Statestore服务进行GET请求来读取。这应该会返回先前在Stateservice端记录的以下值。
{
"date": "2022-03-07T08:34:29.650Z",
"temperatureC": 2000,
"summary": "from state store"
}
[
{
"date": "2022-03-07T17:45:16.8532645+09:00",
"temperatureC": 1000,
"temperatureF": 1831,
"summary": "from App"
},
{
"date": "2022-03-07T08:34:29.65Z",
"temperatureC": 2000,
"temperatureF": 3631,
"summary": "from state store"
}
]
我会查看Redis的内容。它似乎同时运行着用于服务间共享的状态存储和仅限于单个服务使用的状态存储。
这真是非常方便啊,有了这个,我们可能会觉得在服务之间保持数据变得更轻松了,不再需要通过Service Invoke来进行POST等操作。当然,这种情况下的设计会根据不同情况而有所不同,考虑到目标和服务领域的分离以及思想,我认为可能会有一天我会写下来的。