如何在内存使用量不持续增长的情况下使用 Event Store DB 客户端?

Esp*_*pen 5 dependency-injection grpc eventstoredb grpc-c# .net-6.0

我正在使用.Net 的事件存储客户端,并且正在努力寻找使用该客户端的正确方法。当我在 .Net 依赖注入中将客户端注册为单例并在较长时间内运行我的应用程序时,内存使用量会随着每次订阅而不断增长。

我通过以下方式创建并注册客户端。可以在此处找到遇到该问题的完整最小应用程序

var esdbConnectionString = configuration.GetValue("ESDB_CONNECTION_STRING", "esdb://admin:changeit@localhost:2113?tls=false");
var eventStoreClientSettings = EventStoreClientSettings.Create(esdbConnectionString);
var eventStoreClient = new EventStoreClient(eventStoreClientSettings);
services.AddSingleton(eventStoreClient);
Run Code Online (Sandbox Code Playgroud)

我的应用程序在很长一段时间内有大量短流

重现

重现该行为的步骤:

  1. 按照文档中的建议将 EventStoreClient 注册为单例。
  2. 在较长时间内订阅大量流。
  3. 取消发送到流订阅中的 CancellationToken 并让它被垃圾收集。
  4. 观察服务的内存使用量增长。

我如何创建和订阅流:

var streamName = CreateStreamName();
var payload = new PingEvent { StreamNr = _currentStreamNumber };
var eventData = new EventData(Uuid.NewUuid(), typeof(PingEvent).Name, EventSerialization.SerializeEventData(payload));
await _client.AppendToStreamAsync(streamName, StreamState.Any, new[] { eventData });

var streamCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(30));

await _client.SubscribeToStreamAsync(streamName, FromStream.Start, async (sub, evnt, token) =>
{
    if (evnt.Event.EventType == "PongEvent")
    {
        _previousStreamIsDone = true;
        streamCancellationTokenSource.Cancel();
    }
},
cancellationToken: streamCancellationTokenSource.Token);

Run Code Online (Sandbox Code Playgroud)

尝试的方法

注册为 Transient 或 Scoped 如果我在 .Net DI 中将客户端注册为 Transient 或 Scoped,它会在内部引发数千个异常并导致多个问题。

手动处理客户端的生命周期 通过使用处理客户端生命周期的单例服务,我尝试每隔一段时间处理客户端并创建一个新客户端,确保同时只存在一个客户端实例。这会导致与将服务注册为 Transient 或 Scoped 相同的问题。

我在 .Net 6 中针对事件存储数据库 21.10.0 使用版本 22.0.0 的事件存储客户端。在 Windows 和标准 aspnet:6.0 linux docker 容器上运行时都会出现该问题。

通过检查这些 dotnet-dump的结果,内存增长似乎发生在gRPC 客户端的 ActiveCalls 哈希集中。

我希望找到一种不会导致内存增长的客户端使用方法。

Tim*_*imC 7

在您的复制中,泄漏的调用来自您在处理订阅上收到的事件时发出的额外读取。

目前有一个未解决的问题(https://github.com/EventStore/EventStore-Client-Dotnet/issues/219)可以更好地处理这个问题,但目前如果您发出读取但不消耗所有事件并且不要取消读取,然后呼叫保持打开状态。在您的情况下,如果从属设备在主设备发出因在订阅中Pong接收自己的读取而产生的读取之前成功回复,就会发生这种情况。Ping然后,该读取将包含 Ping 和 Pong,仅读取 Ping,并且呼叫保持打开状态。

目前,如果您通过将要取消的取消标记传递到 ReadFromStartOfStreamToEnd 中的 ReadStreamAsync 调用来取消这些读取,则应该可以解决您的问题。

如果它对你有帮助,你可以看到Current Calls存活的数量,而不是等待很长时间才能看到对内存的影响:

dotnet-counters monitor --counters "Grpc.Net.Client" -p <processid>
Run Code Online (Sandbox Code Playgroud)