monolithic kernel

SinatraとVarnishとnginxなWebアプリを高速化

うちのWebアプリを高速化したときのメモともいいます。フロントがnginxで、Varnishを挟んで後ろにいるSinatra(というよりRack)アプリケーションへアクセスするような感じの環境を想定しています。

メモなので脈絡がないですが、以下のようなことをしています。

  • nginxでのキャッシュ制御
  • JavaScriptの軽量化
  • Rackミドルウェアの導入
  • Varnishでのキャッシュ制御

nginxでのキャッシュ制御

静的なファイルはアプリケーションにリクエストを渡さなくてもnginxだけで処理できます。このとき、expiresディレクティブを利用すると、クライアントにキャッシュさせる期間を指定することができます。静的なファイルの中身が書き換わることはまれなので、長めにキャッシュさせても問題ないと思います。

location /static/ {
  root /path/to/static;
  expires 30d;
}

JavaScriptの軽量化

JSMinというライブラリを利用してJavaScriptを軽量化しました。次の2つのコードは上が軽量化前、下が軽量化後の例です。無駄な改行やスペースが削除され、軽量化前の79バイトに対して軽量化後は59バイトと僅かではありますが容量が小さくなっています。

function hello() {
  for(var i = 0; i <> 3; ++i) {
    alert(i);
  }
}

hello();
function hello(){for(var i=0;i&lt;3;++i){alert(i);}}hello();

JavaScriptは動的に中身が書き換わるわけではないので、あらかじめ軽量化したファイルを用意しておいてもよいのですが、テンプレートエンジンとして使えるようにしてみました。テンプレートではないので少し違和感はありますが、簡単な記述で利用できて便利なのでこのようにしています。

require 'tilt'

class JSTemplate < Tilt::Template
  def initialize_engine
    return if defined? ::JSMin
    require_template_library 'jsmin'
  end

  def prepare
    @options = DEFAULT_OPTIONS.merge(options)
  end

  def evaluate(scope, locals, &block)
    options[:minify] ? JSMin.minify(data) : data
  end

  private

  DEFAULT_OPTIONS = {
    minify: true,
  }
end

Tilt.register 'js', JSTemplate

helpers do
  def js(template, options = {})
    render :js, template, options, {}
  end
end

これを利用すると、他のテンプレートエンジンと同様に「js :テンプレート名」という非常にシンプルな記述で利用可能です。

set :js, :minify => production?
get '/scripts/foo.js' do
  js :'scripts/foo'
end

なお、この例ではJavaScriptを外部ファイルにしていますが、小さなスクリプトであればHTMLに埋め込んだほうが高速化に繋がる場合もあります。HTML内部に埋め込む場合であっても、今回のようにテンプレートエンジンの一種として扱えるようにしておくことで、別ファイルに分離して記述しておいてレンダリング時に埋め込むといった対応が簡単にできます。

Rackミドルウェアの導入

Rack::ETagとRack::Deflaterという2つのミドルウェアを導入してみました。Rack::ETagはキャッシュのためのエンティティタグを生成するミドルウェアで、Rack::Deflaterはレスポンスをdeflateで圧縮するミドルウェアです。

use Rack::ETag
use Rack::Deflater

Varnishでのキャッシュ制御

Varnishは特に設定しなくてもいい感じに動いてくれるのですが、Accept-Encodingヘッダの正規化とキャッシュ期間の設定を加えてみました。

Accept-Encodingの正規化部分は公式Wikiから拾ってきただけなので特に言うことはありません。

キャッシュ期間の設定はCache-Controlヘッダを追加しているだけです。

あまり説明することがないですが、それだけVarnishがよくできているということでしょうね。

backend app {
    .host = "127.0.0.1";
    .port = "9101";
    .connect_timeout = 30s;
}

# http://www.varnish-cache.org/trac/wiki/VCLExampleNormalizeAcceptEncoding
sub normalize_accept_encoding {
    if (req.http.Accept-Encoding) {
        if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") {
            remove req.http.Accept-Encoding;
        }
        elsif (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        }
        elsif (req.http.Accept-Encoding ~ "deflate" && req.http.User-Agent !~ "Internet Explorer") {
            set req.http.Accept-Encoding = "deflate";
        }
        else {
            remove req.http.Accept-Encoding;
        }
    }
}

sub vcl_recv {
    call normalize_accept_encoding;
    set req.backend = app;
    return (lookup);
}

sub vcl_fetch {
    if (req.url ~ "\.css$") {
        unset beresp.http.Expires;
        set beresp.http.Cache-Control = "max-age=86400";
        set beresp.ttl = 24h;
    }
    else {
        set beresp.ttl = 1h;
    }
}

参考