如何避免Event Store DB客户端持续内存占用增长?
Event Store .NET客户端内存泄漏问题及解决方案
问题描述
我在使用Event Store .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);
复现步骤
- 按照文档推荐,将EventStoreClient注册为单例。
- 长期订阅大量流。
- 取消传入流订阅的CancellationToken并等待其被垃圾回收。
- 观察服务内存占用持续增长。
流创建与订阅代码
我创建和订阅流的方式如下:
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);
已尝试的方案
- 注册为Transient或Scoped:将客户端注册为Transient或Scoped时,.NET DI内部会抛出大量异常,引发诸多问题。
- 手动管理客户端生命周期:通过单例服务定期销毁并创建新客户端,确保同一时间仅存在一个实例,但结果与注册为Transient或Scoped时相同。
内存增长原因分析
通过内存dump分析,发现内存增长源于gRPC客户端中的*ActiveCalls* HashSet,订阅取消后相关资源未被正确释放。
解决方案
1. 显式释放订阅资源
使用SubscribeToStreamAsync返回的EventStoreSubscription对象,在订阅取消后主动调用DisposeAsync(),确保释放gRPC调用资源:
var streamCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(30)); var subscription = await _client.SubscribeToStreamAsync(streamName, FromStream.Start, async (sub, evnt, token) => { if (evnt.Event.EventType == "PongEvent") { _previousStreamIsDone = true; streamCancellationTokenSource.Cancel(); } }, cancellationToken: streamCancellationTokenSource.Token); // 等待订阅完成或取消后释放资源 try { await subscription.WaitForCompletionAsync(streamCancellationTokenSource.Token); } catch (OperationCanceledException) { // 预期的取消操作,无需处理 } finally { await subscription.DisposeAsync(); }
2. 优化gRPC通道配置
在创建EventStoreClientSettings时,配置gRPC通道的空闲超时,避免闲置连接占用资源:
var esdbConnectionString = configuration.GetValue("ESDB_CONNECTION_STRING", "esdb://admin:changeit@localhost:2113?tls=false"); var eventStoreClientSettings = EventStoreClientSettings.Create(esdbConnectionString); // 配置通道空闲超时与保活参数 eventStoreClientSettings.ChannelOptionsActions.Add(options => { options.HttpHandler = new SocketsHttpHandler { PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5), KeepAlivePingDelay = TimeSpan.FromMinutes(1), KeepAlivePingTimeout = TimeSpan.FromSeconds(30) }; }); var eventStoreClient = new EventStoreClient(eventStoreClientSettings); services.AddSingleton(eventStoreClient);
3. 升级客户端版本
检查Event Store .NET客户端的最新版本,后续版本可能已修复gRPC连接泄漏相关问题(当前使用版本为22.0.0)。
内容的提问来源于stack exchange,提问作者Espen
相关产品推荐
相关产品推荐

