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

ASP.NET Framework 4.8中SignalR使用背板时性能低下问题排查

问题描述

我有一个基于ASP.NET Framework 4.8的ERP应用,使用AspNet.SignalR库。应用场景如下:客户端使用刷卡终端、打印机等设备,这些设备仅与客户端本地机器通信,因此我们通过SignalR Hub与客户端本地EXE交互。

此前系统运行正常,但近期调整客户端服务器的IIS进程池进程数后,系统完全无法工作。经排查得知需要实现SignalR背板,于是分别部署了基于SQL Server和Redis的背板,但性能远低于未使用背板时的水平。

相关代码片段

每次处理刷卡终端交易时调用的核心函数:

public async Task<DadosComunicacao> Conecta()
{
    this.HubProxy = await RetornaConexao();
    this._tokenTransacao = string.Empty;

    try
    {
        await this._conexao.Start();
        await this.HubProxy.Invoke("entrarGrupo", this._nomeGrupo);

        if (this._dados.TipoComunicacao == TipoComunicacao.TEF)
        {
            this._tokenTransacao = TokenHelper.GerarToken();
            this._dados.TokenTransacao = this._tokenTransacao;
        }

        string envioJson = JsonHelper.SerializeObject(this._dados);

        if (!this._dados.TipoComunicacao.Equals(TipoComunicacao.POSStone))
            await this.HubProxy.Invoke("enviarJson", envioJson, this._nomeGrupo);
    }
    catch (Exception ex)
    {
        throw new Exception(ex.Message, ex.InnerException);
    }

    this._waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset);

    this.HubProxy.On("receberJson", (mensagem) =>
    {
        this._dados = JsonHelper.DeserializeObject<DadosComunicacao>(mensagem);

        if (this._dados.TipoComunicacao == TipoComunicacao.TEF)
        {
            if (this._dados.TokenTransacao == this._tokenTransacao)
                this._waitHandle.Set();
        }
        else
            this._waitHandle.Set();
    });

    System.Timers.Timer timer = new System.Timers.Timer(30000); // 默认等待30秒

    if (this._dados.TipoComunicacao == TipoComunicacao.TEF)
        timer.Interval = 1200000; // 等待20分钟
    else if (this._dados.TipoComunicacao.Equals(TipoComunicacao.POSStone))
        timer.Interval = 1200000;

    timer.Enabled = true;
    timer.Elapsed += OnTimedEvent;

    await Task.Run(() => this._waitHandle.WaitOne());

    timer.Dispose();
    this._conexao.Dispose();

    return this._dados;
}

返回Hub代理的方法:

public async Task<IHubProxy> RetornaConexao(bool entrarGrupo = false)
{
    string url = string.Empty;

    if (string.IsNullOrWhiteSpace(this._urlWebHub))
    {
        url = string.Format("{0}://{1}:{2}/{3}", this._currentContext.Request.Url.Scheme,
                                                   this._currentContext.Request.Url.Host,
                                                   this._currentContext.Request.Url.Port,
                                                    UrlHelper.ResolveUrl("~/", this._currentContext));
    }
    else
        url = this._urlWebHub;

    this._conexao = new HubConnection(url);

    var proxy = this._conexao.CreateHubProxy("webhub"); // 通信方法所在的Hub类

    if (entrarGrupo)
    {
        await this._conexao.Start();
        await proxy.Invoke("entrarGrupo", this._nomeGrupo);
    }

    return proxy;
}

测试情况

  • 测试了两种背板:作为Windows服务的Redis(无法使用Linux子系统)、SQL Server背板(已启用Service Broker,调整过tablecount、maxqueue等参数)。
  • 使用SQL Server背板时,通过SQL Profiler发现每次交易都会产生大量查询,性能下降明显。

请问是否有未关注的优化指标或可探索的替代方案?


优化建议与替代方案

一、代码层面优化(优先处理,直接降低背板负载)

  1. 复用SignalR连接,避免频繁创建销毁
    当前代码每次交易都会新建HubConnection,用完立即Dispose,加上背板同步逻辑,会产生大量的连接建立/销毁、组加入/退出的背板操作(SQL Server会生成大量组同步查询)。
    优化方案:为每个客户端实例复用同一个HubConnection,仅在连接断开时重新创建,避免重复调用entrarGrupo。

  2. 替换EventWaitHandle为TaskCompletionSource
    当前用EventWaitHandle配合Task.Run等待回调的方式,会额外占用线程资源,且异步逻辑不够简洁。改用TaskCompletionSource可以更高效地实现异步等待:

    var tcs = new TaskCompletionSource<DadosComunicacao>();
    this.HubProxy.On("receberJson", (mensagem) =>
    {
        var dados = JsonHelper.DeserializeObject<DadosComunicacao>(mensagem);
        if (dados.TipoComunicacao == TipoComunicacao.TEF && dados.TokenTransacao != this._tokenTransacao)
            return;
        tcs.SetResult(dados);
    });
    // 配合定时器取消任务
    using var timer = new System.Timers.Timer(timerInterval);
    timer.Elapsed += (s,e) => tcs.TrySetException(new TimeoutException("请求超时"));
    timer.Enabled = true;
    var result = await tcs.Task;
    
  3. 避免重复加入组
    若同一个客户端多次调用Conecta,会重复执行entrarGrupo,导致背板重复同步组信息。可以在客户端实例中记录是否已加入目标组,仅在未加入时调用该方法。

二、SQL Server背板优化

  1. 优化数据库连接池
    在SignalR的SQL连接字符串中设置Max Pool Size=200(根据服务器配置调整),避免频繁创建数据库连接,减少查询开销。

  2. 调整背板参数

    • 增大tableCount参数:默认值为1,若交易量大,可设置为服务器CPU核心数(如4或8),分摊消息存储的负载。
    • 调高Service Broker的MAX_QUEUE_READERS:默认值为1,可设置为4-8,提升消息消费的并发能力。
    • 确保SignalR的背板表(如SignalR.Messages_0、SignalR.Connections)存在合适的索引,避免全表扫描。
  3. 监控数据库性能指标
    关注SQL Server的以下指标:

    • 等待类型:查看是否存在LOCK_ESCALATION、PAGEIOLATCH_SH等等待,排查是否有锁竞争或IO瓶颈。
    • 事务日志:若日志增长过快,调整备份策略或启用简单恢复模式(根据业务需求)。

三、Redis背板优化

  1. 调整Redis服务配置

    • 关闭不必要的持久化(如RDB/AOF),或延长持久化间隔,减少IO开销。
    • 增大Redis的内存分配,避免内存不足导致swap使用,影响性能。
    • 确保Redis服务器的CPU使用率不超过70%,若单线程满载,可考虑使用Redis哨兵模式提升可用性。
  2. 优化SignalR Redis配置
    在连接字符串中设置合理的超时参数:syncTimeout=5000、connectTimeout=3000,避免频繁重试。同时确保使用的是最新版本的Microsoft.AspNet.SignalR.Redis包,修复已知的性能问题。

四、替代方案

  1. 基于RabbitMQ实现自定义背板
    SignalR官方背板中,RabbitMQ的消息传递性能优于SQL Server,且比Redis更适合复杂的组消息场景。可以通过实现IMessageBus接口,基于RabbitMQ构建自定义背板,减少不必要的同步开销。

  2. 调整IIS进程池配置
    若业务场景允许,可尝试减少进程池的进程数(如改为1个进程),避免背板的跨进程同步开销,但会失去多进程的扩展性,需要在性能和可用性之间权衡。

  3. 使用Azure Service Bus背板(云环境)
    若应用部署在Azure上,Azure Service Bus背板的性能和可靠性都优于SQL Server,且支持自动扩展,适合高并发场景。


内容的提问来源于stack exchange,提问作者Gregori Schuster

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 17:55:56