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发现每次交易都会产生大量查询,性能下降明显。
请问是否有未关注的优化指标或可探索的替代方案?
一、代码层面优化(优先处理,直接降低背板负载)
复用SignalR连接,避免频繁创建销毁
当前代码每次交易都会新建HubConnection,用完立即Dispose,加上背板同步逻辑,会产生大量的连接建立/销毁、组加入/退出的背板操作(SQL Server会生成大量组同步查询)。
优化方案:为每个客户端实例复用同一个HubConnection,仅在连接断开时重新创建,避免重复调用entrarGrupo。替换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;避免重复加入组
若同一个客户端多次调用Conecta,会重复执行entrarGrupo,导致背板重复同步组信息。可以在客户端实例中记录是否已加入目标组,仅在未加入时调用该方法。
二、SQL Server背板优化
优化数据库连接池
在SignalR的SQL连接字符串中设置Max Pool Size=200(根据服务器配置调整),避免频繁创建数据库连接,减少查询开销。调整背板参数
- 增大
tableCount参数:默认值为1,若交易量大,可设置为服务器CPU核心数(如4或8),分摊消息存储的负载。 - 调高Service Broker的
MAX_QUEUE_READERS:默认值为1,可设置为4-8,提升消息消费的并发能力。 - 确保SignalR的背板表(如
SignalR.Messages_0、SignalR.Connections)存在合适的索引,避免全表扫描。
- 增大
监控数据库性能指标
关注SQL Server的以下指标:- 等待类型:查看是否存在
LOCK_ESCALATION、PAGEIOLATCH_SH等等待,排查是否有锁竞争或IO瓶颈。 - 事务日志:若日志增长过快,调整备份策略或启用简单恢复模式(根据业务需求)。
- 等待类型:查看是否存在
三、Redis背板优化
调整Redis服务配置
- 关闭不必要的持久化(如RDB/AOF),或延长持久化间隔,减少IO开销。
- 增大Redis的内存分配,避免内存不足导致swap使用,影响性能。
- 确保Redis服务器的CPU使用率不超过70%,若单线程满载,可考虑使用Redis哨兵模式提升可用性。
优化SignalR Redis配置
在连接字符串中设置合理的超时参数:syncTimeout=5000、connectTimeout=3000,避免频繁重试。同时确保使用的是最新版本的Microsoft.AspNet.SignalR.Redis包,修复已知的性能问题。
四、替代方案
基于RabbitMQ实现自定义背板
SignalR官方背板中,RabbitMQ的消息传递性能优于SQL Server,且比Redis更适合复杂的组消息场景。可以通过实现IMessageBus接口,基于RabbitMQ构建自定义背板,减少不必要的同步开销。调整IIS进程池配置
若业务场景允许,可尝试减少进程池的进程数(如改为1个进程),避免背板的跨进程同步开销,但会失去多进程的扩展性,需要在性能和可用性之间权衡。使用Azure Service Bus背板(云环境)
若应用部署在Azure上,Azure Service Bus背板的性能和可靠性都优于SQL Server,且支持自动扩展,适合高并发场景。
内容的提问来源于stack exchange,提问作者Gregori Schuster

