In general, there are two ways to scale a web application: scale up and scale out.
- Scale up means using a larger server (or a larger VM) with more RAM, CPUs, etc.
- Scale out means adding more servers to handle the load.
The problem with scaling up is that you quickly hit a limit on the size of the machine. Beyond that, you need to scale out. However, when you scale out, clients can get routed to different servers. A client that is connected to one server will not receive messages sent from another server.
One solution is to forward messages between servers, using a component called a backplane. With a backplane enabled, each application instance sends messages to the backplane, and the backplane forwards them to the other application instances. (In electronics, a backplane is a group of parallel connectors. By analogy, a SignalR backplane connects multiple servers.)
SignalR currently provides three backplanes:
- Windows Azure Service Bus. Service Bus is a messaging infrastructure that allows components to send messages in a loosely coupled way.
- Redis. Redis is an in-memory key-value store. Redis supports a publish/subscribe (“pub/sub”) pattern for sending messages.
- SQL Server. The SQL Server backplane writes messages to SQL tables. The backplane uses Service Broker for efficient messaging. However, it also works if Service Broker is not enabled.
If you deploy your application on Windows Azure, consider using the Azure Service Bus backplane. If you are deploying to your own server farm, consider the SQL Server or Redis backplanes.
The following topics contain step-by-step tutorials for each backplane:
- SignalR Scaleout with Azure Service Bus
- SignalR Scaleout with Redis
- SignalR Scaleout with SQL Server
In SignalR, every message is sent through a message bus. A message bus implements the IMessageBus interface, which provides a publish/subscribe abstraction. The backplanes work by replacing the default IMessageBus with a bus designed for that backplane. For example, the message bus for Redis is RedisMessageBus, and it uses the Redispub/sub mechanism to send and receive messages.
Each server instance connects to the backplane through the bus. When a message is sent, it goes to the backplane, and the backplane sends it to every server. When a server gets a message from the backplane, it puts the message in its local cache. The server then delivers messages to clients from its local cache.
For each client connection, the client’s progress in reading the message stream is tracked using a cursor. (A cursor represents a position in the message stream.) If a client disconnects and then reconnects, it asks the bus for any messages that arrived after the client’s cursor value. The same thing happens when a connection uses long polling. After a long poll request completes, the client opens a new connection and asks for messages that arrived after the cursor.
The cursor mechanism works even if a client is routed to a different server on reconnect. The backplane is aware of all the servers, and it doesn’t matter which server a client connects to.
Using a backplane, the maximum message throughput is lower than it is when clients talk directly to a single server node. That’s because the backplane forwards every message to every node, so the backplane can become a bottleneck. Whether this limitation is a problem depends on the application. For example, here are some typical SignalR scenarios:
- Server broadcast (e.g., stock ticker): Backplanes work well for this scenario, because the server controls the rate at which messages are sent.
- Client-to-client (e.g., chat): In this scenario, the backplane might be a bottleneck if the number of messages scales with the number of clients; that is, if the rate of messages grows proportionally as more clients join.
- High-frequency realtime (e.g., real-time games): A backplane is not recommended for this scenario.
Enabling Tracing For SignalR Scaleout
To enable tracing for the backplanes, add the following sections to the web.config file, under the root configurationelement:
<configuration> <system.diagnostics> <sources> <source name="SignalR.SqlMessageBus"> <listeners> <add name="SignalR-Bus" /> </listeners> </source> <source name="SignalR.ServiceBusMessageBus"> <listeners> <add name="SignalR-Bus" /> </listeners> </source> <source name="SignalR.ScaleoutMessageBus"> <listeners> <add name="SignalR-Bus" /> </listeners> </source> </sources> <switches> <add name="SignalRSwitch" value="Verbose" /> <!-- Off, Critical, Error, Warning, Information, Verbose --> </switches> <sharedListeners> <add name="SignalR-Bus" type="System.Diagnostics.TextWriterTraceListener" initializeData="bus.log.txt" /> </sharedListeners> <trace autoflush="true" /> </system.diagnostics> . . . </configuration>