基于ABP、SignalR与EF Core的游戏服务器高内存占用求助
问题
我用Aspnetboilerplate(ABP)、EF Core与SignalR搭建游戏服务器,启动时内存占用约200MB;IIS中每次请求会使私有字节增加约2MB,用户断开连接后内存无法释放。仅20位用户连接1-2分钟后,私有字节就飙升至5GB,目前只能靠设置应用池回收阈值重置服务。我了解到EF Core可能因导航属性在启动或查询阶段占用大量资源,想咨询:SignalR是否不适配该技术栈?或是有办法在每次请求处理完成或用户断开连接时手动释放内存?
相关GreenGameHub代码
[UnitOfWork] public class GreenGameHub : AbpHubBase, ITransientDependency { private readonly GreenUserManager _gum; private readonly IConfiguration _config; private readonly GameUserManager _gm; private readonly GameItemManager _gi; private readonly GreenOrderManager _greenOrderManager; private readonly SettingManager _settingManager; private readonly GameUserCheckInManager _gameUserCheckInManager; private readonly GameFavouriteFriendManager _gameFavouriteFriendManager; private readonly GameMissionManager _gameMissionManager; private readonly IUnitOfWorkManager _unitOfWorkManager; public GreenGameHub( GreenUserManager gum, GameUserManager gm, GameItemManager gi, SettingManager sm, IConfiguration config, GameUserCheckInManager guci, GreenOrderManager gom, GameFavouriteFriendManager gffm, GameMissionManager gmm, IUnitOfWorkManager unitOfWorkManager ) { _gm = gm; _gi = gi; _gum = gum; _settingManager = sm; _config = config; _gameUserCheckInManager = guci; _greenOrderManager = gom; _gameFavouriteFriendManager = gffm; _gameMissionManager = gmm; _unitOfWorkManager = unitOfWorkManager; } public override async Task OnConnectedAsync() { Console.WriteLine("User Connected: " + Context.ConnectionId + " / " + Context.GetUserIdOrNull()); //await Clients.All.SendAsync("OnUserConnected", "A user has connected: " + Context.GetUserIdOrNull()); await _gm.AddConnectionByGameUserAsync(Context.ConnectionId, Context.GetHttpContext().Request.Headers["User-Agent"], null); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception ex) { Console.WriteLine("User Disconnected: "+Context.ConnectionId + " / " + Context.GetUserIdOrNull()); // unregister user from online list await _gm.RemoveConnectionAsync(Context.ConnectionId, Context.GetHttpContext().Request.Headers["User-Agent"], null); await base.OnDisconnectedAsync(ex); } public async Task<int> GetGameSetting() { //await Clients.All.SendAsync("getSetting", "User GetGameSetting"); return await _settingManager.GetSettingValueForApplicationAsync<int>(AppSettingNames.GA_Maintenance); } private async Task<GameUser> createNewGameUser(GreenUser greenUser) { var gu = await _gm.CreateGameUserAsync(greenUser); List<GameItem> gameItems = new List<GameItem>(); var itemIdsSettings = await SettingManager.GetSettingValueForApplicationAsync(AppSettingNames.GA_WelcomeGift); var itemIds = itemIdsSettings.Split(","); try { for (int i = 0; i < itemIds.Length; i++) { var itemId = Int32.Parse(itemIds.ElementAt(i)); gameItems.Add(await _gi.CheckAndGetGameItemAsync(itemId)); } var userItemList = await _gm.AddGameItemListToUser(gameItems, gu); if (userItemList != null) { for (int i = 0; i < userItemList.Count; i++) { var userItem = userItemList[i]; await TryConsumeOnReceive(gu.Id, userItem); } } } catch (Exception ex) { Console.WriteLine("Create new item failed: " + ex.Message); } return gu; } // LOGIN method public virtual async Task<GameUserDto> VerifyGameUserAsync(String shopperId) { GreenUser greenUser = await _gum.GetUserByShopperIdAsync(shopperId); if(greenUser == null) { throw new UserFriendlyException("User not found: "+ shopperId); } GameUser gu = await _gm.CheckAndGetGameUserByGreenUserIdAsync(greenUser.Id, false); if(gu == null) { gu = await createNewGameUser(greenUser); } GameUserDto gudto = ObjectMapper.Map<GameUserDto>(gu); gudto.GameUserId = gu.Id; //TO-DO summarise on add/minus gudto.scoreValue = await this._greenOrderManager.GetSomethingAsync(greenUser); await _gm.AddConnectionByGameUserAsync(Context.ConnectionId, Context.GetHttpContext().Request.Headers["User-Agent"], gu); return gudto; } ...
SignalR启动配置代码
services.AddSignalR(options => { options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); options.EnableDetailedErrors = false; //options.KeepAliveInterval = TimeSpan.FromSeconds(10); options.HandshakeTimeout = TimeSpan.FromSeconds(10); options.KeepAliveInterval = TimeSpan.FromSeconds(15); options.AddFilter(typeof(GameHubFilter)); }).AddMessagePackProtocol().AddJsonProtocol(o => { o.PayloadSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; }); services.AddSingleton<GameHubFilter>(); .... app.UseEndpoints(endpoints => { endpoints.MapHub<AbpCommonHub>("/signalr"); endpoints.MapHub<GreenGameHub>("/signalr-greengame", options => { options.Transports = HttpTransportType.WebSockets; options.TransportSendTimeout = TimeSpan.FromSeconds(10); }); }
解决方案
1. SignalR与ABP+EF Core完全适配,内存泄漏并非技术栈兼容性问题
SignalR可以和ABP、EF Core稳定配合,你遇到的内存飙升大概率是代码层面的资源未正确释放、EF Core查询不当或ABP的UnitOfWork管理问题导致的,不是技术栈本身不兼容。
2. 针对性内存优化方案
(1)EF Core层面优化
- 避免不必要的导航属性加载:检查
GameUser、GreenUser等实体的导航属性,确保查询时仅加载需要的数据,使用AsNoTracking()(只读查询场景)或Include/ThenInclude精准控制加载范围。比如CheckAndGetGameUserByGreenUserIdAsync方法如果默认加载所有导航属性,会导致大量冗余数据占用内存。 - 清理DbContext缓存:ABP的DbContext默认是请求生命周期,若Manager类长期持有DbContext,会导致实体被缓存无法释放。必要时可在查询完成后手动调用
_dbContext.ChangeTracker.Clear()(需注入DbContext)。 - 减少实体对象创建量:比如
createNewGameUser中的循环创建GameItem逻辑,可尝试批量处理或复用对象,降低内存开销。
(2)ABP UnitOfWork管理
- 移除Hub类上的
[UnitOfWork]特性:SignalR是长连接场景,全局[UnitOfWork]会导致资源长期被持有,无法随请求结束释放。应在Hub的具体方法内部手动管理UnitOfWork:using (var uow = _unitOfWorkManager.Begin()) { // 执行数据库操作 await uow.CompleteAsync(); } - 确保UnitOfWork及时完成:所有涉及数据库操作的方法,必须保证UnitOfWork被正确提交或回滚,避免资源泄漏。
(3)SignalR Hub生命周期与资源释放
- 检查依赖注入生命周期:
GreenGameHub实现了ITransientDependency,每个连接/请求都会创建新实例,但需确认注入的Manager类(如GameUserManager)是否为Singleton/Scoped,且内部未持有长期无效引用。 - 断开连接时清理资源:在
OnDisconnectedAsync中,除了移除连接,还要清理绑定到当前ConnectionId的事件订阅、缓存用户数据等,避免内存残留无效引用。 - 避免静态集合堆积:检查
GameUserManager等类是否用静态集合存储用户连接或实体数据,若未及时移除过期数据,会导致内存持续增长。
(4)手动内存释放辅助
- 谨慎使用强制GC:可在
OnDisconnectedAsync末尾尝试调用GC.Collect(),但这只是临时方案,不能替代根源优化,频繁调用会影响性能。 - 定位泄漏点:使用Visual Studio内存诊断工具或dotMemory,捕捉内存快照分析未释放的对象(如DbContext、实体缓存、静态集合等)。
3. 额外配置优化
- SignalR连接管理:确保
AddConnectionByGameUserAsync和RemoveConnectionAsync正确维护连接列表,无重复添加或未移除的情况,避免连接对象堆积。 - Json序列化调整:当前
ReferenceHandler.IgnoreCycles会保留循环引用,可能导致对象无法被GC回收,可尝试改为ReferenceHandler.Preserve并配合MaxDepth限制,或确保DTO无循环引用。
内容的提问来源于stack exchange,提问作者Orange
相关产品推荐
相关产品推荐

