Momentum, a SPDY Server
Momentum is a Rack handler for SPDY clients. That means, it receives connections from SPDY clients and runs Rack apps. It's that simple.
But that doesn't mean you can only run your Rack app on it. Adapters enable Momentum to act as a proxy to your existing HTTP backend, and should you want to, you can also run your Rack app in seperate threads.
Installation
Installation is quite complicated at the moment, as there is no gem release yet. First, clone this repo and dependencies:
$ git clone git://github.com/jonasschneider/momentum.git
$ cd momentum
$ git submodule update --init
$ bundle install
Then go ahead and run the proxy example:
$ bundle exec ruby examples/proxy.rb 80
This will start Momentum on 0.0.0.0:5555
with the Proxy
adapter (see below), forwarding all requests to
an HTTP server running on port 80.
If you have a recent version of Chrome/Chromium, use the command line flag --use-spdy=no-ssl
to force
it to use SPDY. Point it at your_server:5555
, and bam! You're running SPDY.
For more usage examples, see the examples/
directory.
Backend
As Momentum is Rack-based, the server will deliver all requests to a Rack app.
This Rack app is the argument to Momentum.start
.
The simplest possible solution is to just use your regular Rack app with the Momentum backend.
SPDY requests to your app will cause your application code to be executed in the SPDY server
itself. But you probably don't want this, for several reasons.
First, as your app is most likely not asynchronous, the event loop of the SPDY server will be blocked when running your app's code. This can cause timeouts and will effectively make your SPDY sever synchronous.
Second, SPDY is meant as a front-end protocol. Running your app on a front-end server will break when you need to scale.
The recommended solution is simple: keep your existing HTTP infrastructure and backend servers, and use Momentum as a proxy to your backend servers, as described in the usage example above.
If you don't care about scaling
Add the following to your app's Gemfile
:
gem 'momentum', :git => 'git://github.com/jonasschneider/momentum.git', :submodules => true
Then download the code and start the server by running:
$ bundle install
$ bundle exec momentum
This will start momentum running your config.ru
with the Defer adapter.
This adapter (see below) will run your application code in a separate thread per request,
so make sure it's threadsafe.
If you're curious, you can try out what happens without an adapter (running your app directly
in the event loop, no threads) by running momentum --plain
in your app's directory.
Adapters
Adapters are Rack apps. They can be thought of as middleware. If you do not want your app
to be executed within the SPDY server event loop (i.e. because it blocks), you should use an
adapter. The design of adapters is to return an :async
response immediately, so the event loop
of the SPDY server is not blocked. Of course, the adapter has to get the real response from somewhere.
The adapters use mechanisms that are provided by EventMachine to generate or fetch the
response asynchronously.
Momentum::Adapters::Proxy
The Proxy
adapter will cause all requests to be forwarded to a given HTTP server. The SPDY server
will act as an HTTP proxy.
Internally, EM::HttpRequest
is used to asynchronously fetch the resource from the backend.
Advanced features of the SPDY protocol, such as Server Push, currently cannot be used, as they would require communication betweeen the backend and the SPDY server before the response is sent.
Trivial performance tests showed that using the Proxy adapter in combination with a Thin/nginx setup yields the best results for serving clients over the internet for sites with many assets (see below.)
Note that is is not a SPDY/HTTPS proxy for proxying connections to arbitrary servers through a SPDY tunnel as described in http://dev.chromium.org/spdy/spdy-proxy-examples. It is merely a way of providing the SPDY protocol to clients without any backend configuration changes.
Momentum::Adapters::Defer
Defer
will use the EventMachine thread pool (configured with a size of 100) to run your application code.
This requires your code to be threadsafe. This adapter is used when running the momentum
command.
Server Push (see below) is possible with this adapter.
Backwards compatibility is important! HTTP clients should be handled by a slave HTTP server that forwards requests to the master SPDY server. HTTP support for the SPDY server is a work in progress.
Taking advantage of SPDY Server Push
A Momentum::AppDelegate
object is available in env['spdy']
when running on Momentum.
The public API for this object currently consists of just one method, push(url)
.
It should be called when the app can safely determine that the resource
at path
is going to be required to render the page. However, you should not rely on env['spdy']
being
available, in order to stay compatible to regular Rack servers.
Using it requires the Defer
adapter. If you are not running on it, calling push
will result
in a no-op. If you are, calling push(url)
will initiate a SPDY Server Push to the client.
The SPDY server will be informed of the app's push request, and will start processing the request immediately as if was sent as a separate request by the client.
Per the SPDY spec, the virtual request will look to the application completely identical to
the original request, except for the host
, scheme
and path
headers (and also url
to stay compatible
to the current Chromium implementation), which contain the location of the requested resource.
Performance
This project is somewhat in development.
Since one of the main goals of SPDY is to improve loading times, performance is considered vital for Momentum.
I performed some totally unscientific performance tests. The app in question is located in examples/lots_of_images.ru
.
It displays a bare-bones HTML page, which in turn loads 100 thumbnail-sized JPEG images from the server.
Loading times were measured using the Chrome DOM inspector, reading the time of the DOMContentLoaded event,
which indicates the arrival of the main HTML document, and the onLoad event, which indicates that all of the
images have been loaded.
Traditional benchmark approaches using tools like ab
are deemed inappropriate because SPDY is not optimized
for raw request benchmarking, but instead focuses on the results given by real browsers.
Besides that, there is no ab
equivalent for SPDY. High-concurrency benchmarks are therefore still to be done.
To test SPDY, Chrome was started with the --use-spdy=no-ssl
flag, which forces Chrome to talk SPDY everywhere.
This means that TLS SPDY negotiation is not included in the benchmark. For testing the HTTP servers,
Chrome was started without command-line flags.
The first, smaller group of tests was performed over a local network. The server was running on a media center-style box under Debian. For the second group of tests, a small Amazon EC2 instance was fired up. The results of the two test groups should not be cross-compared, as the system specs differ vastly.
<tr>
<td>WEBrick (for comparison)</td>
<td>0,2s</td>
<td>1,5s</td>
</tr>
<tr>
<td>Thin</td>
<td>0,2s</td>
<td><b>0,8s</b></td>
</tr>
<tr>
<td>Unicorn, 4 workers</td>
<td>0,3s</td>
<td>1,0s</td>
</tr>
<tr>
<td>Momentum/Defer</td>
<td>0,3s</td>
<td>2,5s</td>
</tr>
<tr>
<td>Momentum/Proxy in front of Unicorn</td>
<td>0,3s</td>
<td>3,4s</td>
</tr>
<tr>
<td>Momentum/Proxy in front of Thin</td>
<td>0,2s</td>
<td>2,3s</td>
</tr>
<tr>
<td colspan=3><b>Internet connection</b></td>
</tr>
<tr>
<td>WEBrick (for comparison)</td>
<td>0,5s</td>
<td>4,5s</td>
</tr>
<tr>
<td>Unicorn, 1 worker</td>
<td>0,6s</td>
<td>6,5s</td>
</tr>
<tr>
<td>Unicorn, 4 workers</td>
<td>0,6s</td>
<td>5,9s</td>
</tr>
<tr>
<td>Unicorn, 4 workers behind nginx</td>
<td>0,5s</td>
<td><b>3,0s</b></td>
</tr>
<tr>
<td>Thin</td>
<td>0,5s</td>
<td>4,5s</td>
</tr>
<tr>
<td>Thin behind nginx</td>
<td>0,4s</td>
<td><b>2,6s</b></td>
</tr>
<tr>
<td>Momentum/Defer</td>
<td>0,4s</td>
<td>2,3s</td>
</tr>
<tr>
<td>Momentum/Defer adapter (subsequent)</td>
<td>0,3s</td>
<td><b>2,2s</b></td>
</tr>
<tr>
<td>Momentum/Proxy in front of Thin</td>
<td>0,5s</td>
<td>2,7s</td>
</tr>
<tr>
<td>Momentum/Proxy in front of Thin (subsequent)</td>
<td>0,4s</td>
<td>2,5s</td>
</tr>
<tr>
<td>Momentum/Proxy in front of nginx and Thin</td>
<td>0,5s</td>
<td>2,4s</td>
</tr>
<tr>
<td>Momentum/Proxy in front of nginx and Thin (subsequent)</td>
<td>0,3s</td>
<td><b>2,3s</b></td>
</tr>
Β | DOMContentLoaded | onLoad |
---|---|---|
LAN connection |
Over a local connection, the advantage of the single connection is negated by the protocol overhead, making the multi-connection approach much faster. Additional tests were therefore ommited.
But over a remote connection, the results look drastically different. As can be seen, the high-performant servers Thin and Unicorn are meant to be run behind a reverse proxy. There, they achieve great loading times. Measuring subsequent requests showed that no speedup was gained. Those results are ommited.
The Momentum tests were performed on both initial and subsequent tests to show the improvement caused by the held SPDY connection. The simplicity of the Defer makes it faster than the Proxy adapter in front of Thin. Sadly, testing the Proxy adapter with Unicorn behind nginx was forgotten.
Therefore, the best results were achieved with the Defer adapter and the Proxy adapter proxying to nginx, which in turn proxies to Thin. This is surprising given the overhead of the multiple proxies, but shows the power that lies in the simple addition of a SPDY server to an existing nginx/Thin configuration (and probably also Unicorn/nginx.)
The test may seem biased, as the amount of assets to be loaded is quite high, which favors SPDY. But, looking at sites such as http://www.nytimes.com/, asset counts in this dimension are quite normal. For future investigation, different asset file sizes should be considered, especially larger stylesheet and JavaScript files.
Compliance
Momentum is meant to be compliant with version 2 of the SPDY spec: http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft2
License
Released under the MIT License - Copyright (c) 2011-2012 Jonas Schneider
Credits
Thanks to Ilya Grigorik for the great SPDY parser gem. Inspired by Roman Shterenzon's SPDY server. And thanks to https://github.com/inkel/spdy-examples for the sample images ;)