You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 00:37:57