Inside the LinkRiver Favicon Server – Ruby + Nginx + Thin + Rack

Favicons on LinkRiver

LinkRiver displays favicons next to most links to help users recognize link targets. Those favicons are served separately from the main LinkRiver server. This post describes some of the design decisions and approaches I took when building the FI server.

My most important requirement for the favicon server (FI) was that it be loosely coupled to the LinkRiver (LR) server and reusable for other applications. The LR server could link to a favicon for *any* page without worrying about whether the icon exists on the FI server. If the FI server already had the icon, great – it would serve it up. If not, it would send back a default icon. This requirement ruled out Amazon’s S3 service because it won’t allow you to return a default image/page in response to “404 Not Found” errors.

When LR wants to display the favicon for a site like Twitter, it generates a URL like this:

http://favicons.linkriver.com/f1/25/twitter.com.ico

LR knows how to “map” host names to the directory structure (f1/25 in the example above). Keeping icons in a two-tiered directory system likes this makes it easier to manage the large number of cached files (its bad to have zillions of files in one directory). It also serves as a minor obstacle to others hotlinking to these favicons.

Behind the scenes it would work like this. A fast/lightweight web server like lighttpd or Nginx would sit in front of all requests to serve already-cached static files. When an uncached icon is requested, the FI server queues it up for later download. I have a lightweight non-persistent message queuing class built on memcached and Ruby that would be perfect for this. All the FI server has to do push the request values onto memcached and then tell the web server to send back the default icon.

First Attempt — PHP via Lighttpd and FastCGI

LinkRiver is written in Ruby on Rails using Nginx as a load balancer and static page server with mongrel as the Ruby app server. I love working in Ruby, but for this app, rails would have been overkill. I wasn’t familiar with ways to run Ruby using a faster/lighter server so I dusted off my trusty/rusty PHP skills. Remember – the only thing PHP had to do was push request values to memcached and tell the web server to return the default icon. Something like this:


X-LIGHTTPD-Send-File header tells lighttpd to return a static file to the browser — this is much faster than having PHP do it. I banged this out in about an hour and it worked great.

Second Attempt — Ruby Via Nginx and Thin/Rack

My PHP+Lighttpd version of the FI server worked just fine but I didn’t like supporting both Nginx and lighttpd. I also prefer coding in Ruby whenever possible. Was there a lightweight way to run Ruby on a web server? That’s where Thin and Rack come in.

Thin is a wicked-fast Ruby web server that’s perfect for what I was trying to do — run a fairly simple Ruby script on a web server. Thin is the web server itself – Rack is an interface that defines how Ruby interacts with the server.

Thin runs a Rack config file that looks something like this:

require 'favicon'
require 'mcqueue'
q = MCQueue.new(QUEUE_SERVER, QUEUE_NAMESPACE)
map '/' do
  run FaviconAdapter.new(q)
end

For all requests that make it to Thin (remember – all cached icons are served by Nginx directly and never reach Thin), Thin creates an instance of my FaviconAdapter class and “runs” it, which means it will call the FaviconAdapter’s “call” method and pass in information about the request. Our call method parses out some request information (the hostname for the favicon), pushes it to memcached, and returns an HTTP status code, headers and body, just like the PHP version.

require 'rubygems'
require 'thin'

DEFAULT_HEADERS = {
  'Content-Type' => 'image/x-icon',
  'X-Accel-Redirect' => '/protected/default.png'
}

class FaviconAdapter
  def initialize(queue)
    @queue = queue
  end

  def call(env)
    req = Rack::Request.new(env)
    //
    // A couple of lines removed to parse the request and
    // push it to memcached...
    //
    [200, DEFAULT_HEADERS, ['']]
  end
end

The X-Accel-Redirect does the same thing for Nginx that the X-LIGHTTPD-Send-File header does for lighttpd: it tells the web server to return the file directly instead of streaming it through our Ruby or PHP code.

The new Ruby FI version has been solid and stable like the PHP version before it. The new version should scale better too — in my tests, Nginx handles high load better and serves static files at the same high speed at lighttpd. My Ruby code is outperforming the PHP code by about 30%, but that’s not quite a fair comparison. The Ruby version caches its connection to memcached while the PHP version must reconnect for each request.

That’s all for now.

Advertisements

2 thoughts on “Inside the LinkRiver Favicon Server – Ruby + Nginx + Thin + Rack

  1. Adam, I ran across your blog because my community data aggregation site http://www.hightechcville.com/ could do with some gravatar/avatar love for organizations, which led me to using favicons for an organization. I did the straight linking to each organizations http://url/favicon.ico, but many who have one seem to block it. So I thought about downloading them, and doing somewhat what you’ve got.

    However, it makes me wonder, does anyone have a repository of all favicons? Sort of like the gravatar idea? Or does each site have to roll their own, similar to yours?

    Thanks for an informative post. I’m looking at basing something off of what you’ve written as well as http://pastie.caboo.se/31473

    Eric

    Thanks,
    Eric

  2. Pingback: links for 2008-03-19 « Bloggitation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s