c# SignalR 高并发连接数优化 SignalR 性能瓶颈

10次阅读

SignalR 默认连接数上不去是因服务器配置和底层限制所致,需调优Kestrel连接数、http/2、TLS及Hub广播机制,并优化客户端重连策略。

c# SignalR 高并发连接数优化 SignalR 性能瓶颈

SignalR 默认连接数为什么上不去?

SignalR 在 iis 或 Kestrel 上默认受限于服务器配置和 ASP.NET Core 的底层限制,不是代码写得“高级”就能突破的。常见现象是:压测时连接数卡在几百就报 ConnectionRefusedTimeoutException 或客户端反复重连失败——这往往不是 SignalR 自身问题,而是 windows 句柄、线程池、HTTP/2 流控或 TLS 握手积导致的。

  • IIS 默认 maxConcurrentRequestsPerCPU 是 5000,但实际受 applicationPool → queueLength(默认1000)和系统 MaxUserPort(默认5000)拖累
  • Kestrel 默认不启用 HTTP/2,而大量短连接 + 频繁重连会快速耗尽端口和 TLS session cache
  • HubLifetimeManager 默认用内存实现,高并发下广播操作易成瓶颈,尤其 Clients.All.SendAsync 会同步阻塞整个 Hub 线程

必须改的 Kestrel 和 ASP.NET Core 配置项

别只盯着 Hub 类写法,真正起效的是宿主层调优。以下配置需在 Program.cs 中显式设置:

var builder = WebApplication.CreateBuilder(args);  // 关键:Kestrel 连接与缓冲调优 builder.WebHost.ConfigureKestrel(serverOptions => {     serverOptions.Limits.MaxConcurrentConnections = 100_000; // 显式放开     serverOptions.Limits.MaxConcurrentUpgradedConnections = 100_000;     serverOptions.Limits.MaxRequestBodySize = NULL; // 若不用上传     serverOptions.Limits.MinRequestBodyDataRate = null; // 防止慢速攻击误杀     serverOptions.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(5); });  // 关键:ASP.NET Core 主机级并发控制 builder.Services.Configure(options => {     options.AutomaticAuthentication = false;     options.AuthenticationDisplayName = "windows Authentication"; }); builder.Services.Configure(options => {     options.MaxConnections = 100_000;     options.MaxRequestBodySize = null; });  var app = builder.Build();

注意:MaxConcurrentConnections 必须设为 null 或足够大数值,否则 Kestrel 内部连接队列会直接拒绝新连接;MaxConcurrentUpgradedConnectionswebsocket 升级专用阈值,SignalR 默认走 WebSocket,此项常被忽略。

Hub 层避免广播阻塞的实操方式

高频消息场景下,Clients.All.SendAsync 是性能杀手——它会把所有连接序列化一次、再逐个 await 发送,中间任意一个慢连接都会拖垮整批。替代方案不是“优化序列化”,而是绕过同步广播模型:

  • Clients.AllExcept(...).SendAsync 减少目标集,但本质未解耦
  • 改用 ITransportHeartbeat + 自定义 IHubLifetimeManager 实现异步批量投递(推荐)
  • 更轻量做法:在 Hub 外启动独立后台服务(BackgroundService),将消息入队(如 channel),由消费者线程池分片推送
  • 禁用自动 jsON 序列化缓存:AddJsonProtocol(opt => opt.PayloadSerializerOptions.WriteIndented = false),减小 GC 压力

示例:用 Channel 实现非阻塞广播

public class BroadcastService : BackgroundService {     private readonly ChannelReader _reader;     private readonly IHubContext _hubContext;      public BroadcastService(Channel channel, IHubContext hubContext)     {         _reader = channel.Reader;         _hubContext = hubContext;     }      protected override async Task ExecuteAsync(CancellationToken stoppingToken)     {         await foreach (var msg in _reader.ReadAllAsync(stoppingToken))         {             // 不 await SendAsync,用 Fire-and-forget 避免阻塞             _ = _hubContext.Clients.All.SendAsync("ReceiveMessage", msg, CancellationToken.None);         }     } }

客户端连接复用与重连策略关键点

服务端再强,客户端乱建连也会拖垮整体。SignalR JS 客户端默认重试逻辑极易引发雪崩:

  • 禁用默认自动重连:new HubConnectionBuilder().withUrl(...).configureLogging(LogLevel.None).build(),手动控制重连间隔
  • 连接前检查 navigator.onLine,避免离线时疯狂重试
  • 服务端返回 429 时,客户端应退避重连(如指数退避),而非立即重试
  • Web Worker 中运行 SignalR 连接,防止 UI 线程阻塞影响心跳上报
  • .NET 客户端务必设置 HubConnectionBuilder.WithAutomaticReconnect() 并传入自定义策略,避免 RetryExponential 默认最大间隔仅 30 秒

最容易被忽略的是:WebSocket 连接本身不感知网络闪断,依赖心跳超时(默认 30 秒)才发现断连。生产环境建议将 KeepAliveInterval 设为 10–15 秒,ClientTimeoutInterval 设为 30–45 秒,并在 OnCloseAsync 中清理资源。

Copyright ©  SEO

 Theme by Puock