ASP.NET Core SignalR 托管和缩放

作者:Andrew Stanton-NurseBrady GasterTom Dykstra

本文说明使用 ASP.NET Core SignalR 的高流量应用的托管和缩放注意事项。

粘滞会话

SignalR 要求由同一服务器进程处理特定连接的所有 HTTP 请求。 当 SignalR 在服务器场(多个服务器)上运行时,必须使用“粘滞会话”。 “粘滞会话”也称为会话相关性。 Azure 应用服务使用应用程序请求路由 (ARR) 对请求进行路由。 在 Azure 应用服务中启用“会话相关性”(ARR 相关性)设置会启用“粘滞会话”。应用不需要粘滞会话的唯一情况是:

  1. 在单个服务器的单个进程中托管时。
  2. 使用 Azure SignalR 服务时(为服务而不是应用启用了粘滞会话)。
  3. 所有客户端都配置为仅使用 WebSocket,并且在客户端配置中已启用 SkipNegotiation 设置时。

在所有其他情况下(包括使用 Redis 底板时),必须为粘滞会话配置服务器环境。

有关为 SignalR 配置 Azure 应用服务的指导,请参阅将 ASP.NET Core SignalR 应用发布到 Azure 应用服务。 有关为使用 Azure SignalR 服务的 Blazor 应用配置粘滞会话的指导,请参阅托管和部署 ASP.NET Core 服务器端 Blazor 应用

TCP 连接资源

Web 服务器可以支持的并发 TCP 连接数受到限制。 标准 HTTP 客户端使用临时连接。 这些连接可以在客户端进入空闲状态时关闭,并在以后重新打开。 另一方面,SignalR 连接是持久性的。 SignalR 连接即使在客户端进入空闲状态时也保持打开状态。 在为许多客户端提供服务的高流量应用中,这些持久性连接可能会导致服务器达到其最大连接数。

持久性连接还会占用一些额外内存来跟踪每个连接。

SignalR 大量使用连接相关资源可能会影响在同一服务器上托管的其他 Web 应用。 SignalR 打开并保持最后一个可用 TCP 连接时,同一服务器上其他 Web 应用也不再有可用连接。

如果服务器的连接用完,则你会看到随机套接字错误和连接重置错误。 例如:

An attempt was made to access a socket in a way forbidden by its access permissions...

若要防止 SignalR 资源使用在其他 Web 应用中导致错误,请在与其他 Web 应用不同的服务器上运行 SignalR。

若要防止 SignalR 资源使用在 SignalR 应用中导致错误,请横向扩展以限制服务器必须处理的连接数。

向外扩展

使用 SignalR 的应用需要跟踪其所有连接,这会给服务器场造成问题。 添加服务器,它会获取其他服务器不了解的新连接。 例如,下图中每个服务器上的 SignalR 都不了解其他服务器上的连接。 当其中一个服务器上的 SignalR 要向所有客户端发送消息时,消息只会发送给连接到该服务器的客户端。

无底板缩放 SignalR

解决此问题的选项包括 Azure SignalR 服务Redis 底板

Azure SignalR 服务

Azure SignalR 服务用作实时流量的代理,会在应用跨多个服务器横向扩展时作为底板进行加倍。 每当客户端启动与服务器的连接时,客户端都会进行重定向以连接到服务。 下图说明了该过程:

建立与 Azure SignalR 服务的连接

结果是该服务会管理所有客户端连接,而每个服务器只需与该服务建立少量的恒定数量连接,如下图所示:

客户端连接到服务,服务器连接到服务

与 Redis 底板替代方法相比,这种横向扩展方法具有几个优点:

  • 粘滞会话(也称为客户端相关性)不是必需的,因为客户端在连接时会立即重定向到 Azure SignalR 服务。
  • SignalR 应用可以基于发送的消息数进行横向扩展,而 Azure SignalR 服务可缩放以处理任意数量的连接。 例如,可能有数千个客户端,但如果每秒只发送少量消息,则 SignalR 应用无需横向扩展到多个服务器,即可自行处理连接。
  • 与不使用 SignalR 的 Web 应用相比,SignalR 应用使用的连接资源不会显著更多。

出于这些原因,建议将 Azure SignalR 服务用于 Azure 上托管的所有 ASP.NET Core SignalR 应用(包括应用服务、VM 和容器)。

有关详细信息,请参阅 Azure SignalR 服务文档

Redis 底板

Redis 是一种内存中键-值存储,支持具有发布/订阅模型的消息系统。 SignalR Redis 底板使用发布/订阅功能将消息转发到其他服务器。 当客户端建立连接时,连接信息会传递到底板。 当服务器要向所有客户端发送消息时,它会发送给底板。 底板了解所有连接的客户端以及它们所处的服务器。 它通过各自的服务器将消息发送给所有客户端。 下图对此过程进行了阐释:

Redis 底板,消息从一台服务器发送到所有客户端

对于在你自己的基础结构上托管的应用,Redis 底板是推荐横向扩展方法。 如果你的数据中心与 Azure 数据中心之间的连接延迟很高,则对于延迟较低或吞吐量要求较高的本地应用而言,Azure SignalR 服务可能不是可行选项。

前面提到的 Azure SignalR 服务优点便是 Redis 底板的缺点:

  • 粘滞会话(也称为客户端相关性)是必需的,不过在满足以下两种条件时除外
    • 所有客户端都配置为仅使用 WebSocket。
    • 在客户端配置中启用了 SkipNegotiation 设置。 在服务器上启动连接后,连接必须在该服务器上保持。
  • 即使发送的消息很少,SignalR 应用也必须基于客户端数进行横向扩展。
  • 与不使用 SignalR 的 Web 应用相比,SignalR 应用使用的连接资源显著更多。

Windows 客户端操作系统上的 IIS 限制

Windows 10 和 Windows 8.x 是客户端操作系统。 客户端操作系统上的 IIS 限制为 10 个并发连接。 SignalR 的连接具有以下特征:

  • 是暂时性的且经常重新建立。
  • 在不再使用时不会立即释放。

以上条件可能会导致在客户端操作系统上达到 10 个连接限制。 当客户端操作系统用于开发时,建议:

  • 避免使用 IIS。
  • 使用 Kestrel 或 IIS Express 作为部署目标。

Linux 与 Nginx

以下内容包含为 SignalR 启用 WebSocket、ServerSentEvents 和 LongPolling 所需的最低设置:

http {
  map $http_connection $connection_upgrade {
    "~*Upgrade" $http_connection;
    default keep-alive;
  }

  server {
    listen 80;
    server_name example.com *.example.com;

    # Configure the SignalR Endpoint
    location /hubroute {
      # App server url
      proxy_pass http://localhost:5000;

      # Configuration for WebSockets
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection $connection_upgrade;
      proxy_cache off;
      # WebSockets were implemented after http/1.0
      proxy_http_version 1.1;

      # Configuration for ServerSentEvents
      proxy_buffering off;

      # Configuration for LongPolling or if your KeepAliveInterval is longer than 60 seconds
      proxy_read_timeout 100s;

      proxy_set_header Host $host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
    }
  }
}

使用多个后端服务器时,必须添加粘滞会话,以防止 SignalR 连接在连接时切换服务器。 可通过多种方法在 Nginx 中添加粘滞会话。 下面显示了两种方法,具体取决于提供的功能。

除了前面的配置外,还添加了以下内容。 在下面的示例中,backend 是服务器组的名称。

对于 Nginx 开放源代码,使用 ip_hash 基于客户端的 IP 地址将连接路由到服务器:

http {
  upstream backend {
    # App server 1
    server localhost:5000;
    # App server 2
    server localhost:5002;

    ip_hash;
  }
}

对于 Nginx Plus,使用 sticky 将 cookie 添加到请求,并将用户的请求固定到服务器:

http {
  upstream backend {
    # App server 1
    server localhost:5000;
    # App server 2
    server localhost:5002;

    sticky cookie srv_id expires=max domain=.example.com path=/ httponly;
  }
}

最后,将 server 部分中的 proxy_pass http://localhost:5000 更改为 proxy_pass http://backend

有关 Nginx 上的 WebSocket 的详细信息,请参阅 NGINX 作为 WebSocket 代理

有关负载均衡和粘滞会话的详细信息,请参阅 NGINX 负载均衡

有关将 ASP.NET Core 与 Nginx 结合使用的详细信息,请参阅以下文章:

第三方 SignalR 底板提供程序

后续步骤

有关详细信息,请参阅以下资源: