一、为什么SignalR服务端会内存泄漏?
SignalR是一个强大的实时通信框架,但处理大量长连接时,稍不注意就会让服务器内存"撑爆"。想象一下:每个连接就像一条水管,如果不用了就关掉阀门,但忘记关的水管越多,水池迟早会溢出来。
常见的内存泄漏场景包括:
- 客户端异常断开时,服务端没有及时清理连接对象
- 消息队列积压导致消息对象无法释放
- 全局集合中保存了不再使用的连接引用
举个典型例子(技术栈:.NET Core + SignalR):
// 错误示例:全局字典保存所有连接
public static Dictionary<string, HubCallerContext> ActiveConnections = new();
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
// 连接建立时存入字典
ActiveConnections[Context.ConnectionId] = Context;
await base.OnConnectedAsync();
}
// 缺少OnDisconnectedAsync实现!
}
这段代码的问题很明显:连接建立时被存入全局字典,但断开时没有移除。随着时间推移,字典会越来越大,即使客户端早已断开。
二、连接生命周期管理技巧
正确处理连接生命周期是解决内存问题的关键。SignalR提供了三个核心事件:
- OnConnectedAsync - 连接建立时触发
- OnDisconnectedAsync - 连接断开时触发
- 超时机制 - 默认30秒无心跳自动断开
改进后的正确写法:
public class ChatHub : Hub
{
private static readonly ConcurrentDictionary<string, HubCallerContext> _connections = new();
public override async Task OnConnectedAsync()
{
// 使用线程安全的ConcurrentDictionary
_connections.TryAdd(Context.ConnectionId, Context);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
// 确保连接断开时移除
_connections.TryRemove(Context.ConnectionId, out _);
await base.OnDisconnectedAsync(exception);
}
}
这里做了三处改进:
- 改用线程安全的ConcurrentDictionary
- 实现OnDisconnectedAsync清理逻辑
- 使用TryAdd/TryRemove避免异常
三、资源释放的进阶技巧
除了基本清理,还有更精细的内存控制方法:
3.1 消息缓冲区控制
services.AddSignalR(options => {
// 限制单个连接的最大缓冲区大小(单位:KB)
options.MaximumReceiveMessageSize = 32;
// 客户端超时设置(单位:秒)
options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
// 心跳间隔(单位:秒)
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
});
3.2 使用WeakReference弱引用
对于需要跟踪但不应该阻止GC回收的对象:
private static List<WeakReference<HubCallerContext>> _weakConnections = new();
void TrackConnection(HubCallerContext context)
{
_weakConnections.RemoveAll(wr => !wr.TryGetTarget(out _));
_weakConnections.Add(new WeakReference<HubCallerContext>(context));
}
3.3 定期清理机制
// 定时清理无效连接
_ = Task.Run(async () =>
{
while (true)
{
await Task.Delay(TimeSpan.FromMinutes(5));
var deadConnections = _connections
.Where(c => !c.Value.ConnectionAborted.IsCancellationRequested)
.ToList();
foreach (var conn in deadConnections)
{
_connections.TryRemove(conn.Key, out _);
}
}
});
四、实战中的注意事项
- 连接状态检测:
// 检测连接是否活跃
public async Task<bool> IsConnectionActive(string connectionId)
{
if (_connections.TryGetValue(connectionId, out var context))
{
return !context.ConnectionAborted.IsCancellationRequested;
}
return false;
}
- 异常处理:
public override async Task OnDisconnectedAsync(Exception exception)
{
try
{
// 清理逻辑...
}
catch (Exception ex)
{
_logger.LogError(ex, "清理连接时出错");
}
finally
{
await base.OnDisconnectedAsync(exception);
}
}
- 性能监控: 建议在项目中添加内存监控:
// 在Startup.cs中
app.Use(async (context, next) =>
{
var process = Process.GetCurrentProcess();
_logger.LogInformation($"当前内存使用:{process.WorkingSet64 / 1024 / 1024}MB");
await next();
});
五、不同场景下的优化策略
5.1 高并发短连接场景
services.AddSignalR()
.AddMessagePackProtocol() // 使用更小的消息格式
.AddHubOptions<ChatHub>(options => {
options.EnableDetailedErrors = false; // 生产环境关闭详细错误
options.HandshakeTimeout = TimeSpan.FromSeconds(5); // 缩短握手超时
});
5.2 长连接+大数据量场景
// 在Hub方法中分块传输大数据
public async Task UploadLargeData(byte[] chunk, int totalChunks)
{
// 使用临时文件而不是内存保存
var tempPath = Path.GetTempFileName();
await File.AppendAllBytesAsync(tempPath, chunk);
if (chunk.Length == totalChunks)
{
// 处理完整数据
ProcessCompleteData(tempPath);
}
}
六、总结与最佳实践
经过以上分析,我们得出以下优化经验:
- 必做事项:
- 始终实现OnDisconnectedAsync
- 使用线程安全的集合类型
- 设置合理的超时和缓冲区大小
- 推荐事项:
- 添加定期清理机制
- 实现内存监控
- 根据场景选择合适的配置
- 高级技巧:
- 大数据使用流式处理
- 考虑使用WeakReference
- 采用更高效的协议如MessagePack
记住,内存优化不是一劳永逸的,需要根据实际运行情况持续调整。建议在开发阶段就建立内存监控机制,这样才能在问题变得严重之前及时发现。
最后分享一个完整的Hub实现示例:
public class OptimizedChatHub : Hub
{
private static readonly ConcurrentDictionary<string, (HubCallerContext ctx, DateTime lastActive)> _connections = new();
private readonly ILogger<OptimizedChatHub> _logger;
public OptimizedChatHub(ILogger<OptimizedChatHub> logger)
{
_logger = logger;
StartCleanupTask();
}
private void StartCleanupTask()
{
_ = Task.Run(async () =>
{
while (true)
{
await Task.Delay(TimeSpan.FromMinutes(1));
CleanInactiveConnections();
}
});
}
private void CleanInactiveConnections()
{
var cutoff = DateTime.UtcNow.AddMinutes(-2);
var deadConnections = _connections
.Where(c => c.Value.lastActive < cutoff ||
c.Value.ctx.ConnectionAborted.IsCancellationRequested)
.ToList();
foreach (var conn in deadConnections)
{
_connections.TryRemove(conn.Key, out _);
_logger.LogInformation($"清理闲置连接:{conn.Key}");
}
}
public override async Task OnConnectedAsync()
{
_connections[Context.ConnectionId] = (Context, DateTime.UtcNow);
_logger.LogInformation($"新连接:{Context.ConnectionId},当前连接数:{_connections.Count}");
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
_connections.TryRemove(Context.ConnectionId, out _);
_logger.LogInformation(exception == null
? $"连接正常关闭:{Context.ConnectionId}"
: $"连接异常断开:{Context.ConnectionId},错误:{exception.Message}");
await base.OnDisconnectedAsync(exception);
}
public async Task SendMessage(string user, string message)
{
// 更新活动时间
if (_connections.TryGetValue(Context.ConnectionId, out var entry))
{
_connections[Context.ConnectionId] = (entry.ctx, DateTime.UtcNow);
}
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
这个实现包含了我们讨论的所有优化点:
- 使用线程安全集合
- 连接状态跟踪
- 定期清理
- 活动时间记录
- 完善的日志记录
- 异常处理
希望这些实践经验能帮助你构建更稳定的SignalR服务!
评论