フッターのキャッシュとかフラグメントキャッシュはできたので、サイトのなかで一番重いアーカイブページのキャッシュを考えてみることにした。

当初はアーカイブページも、一番重い記事一覧表示部分をフラグメントキャッシュしてみていた。しかしあまり効果がなかった。Sinatra は仕組み上、コントローラーにいろいろ書いてしまいがちになり、アーカイブページのコントローラーが Fat になっていた。そのためフラグメントキャッシュをしたところでコントローラーの重い処理はビューがレンダリングされる前に走ってしまい、キャッシュの意味があまりない状態だった。

Rails だったらアクションキャッシュとかあるけど、先日から Folk して改造を進めている sinatra-cache でできるのはページキャッシュとフラグメントキャッシュだけなため、ページキャッシュをしてみることにした。

ページキャッシュの残念な点は Nginx 側の設定も必要なことだ。せっかく Lokka は Heroku や Sqale など Rack アプリケーション置ける PaaS にならどこにでも置けるのに、Nginx の設定変更を前提とした変更を行うと CMS for Cloud ではなくなってしまう。しかしこのブログは自分の勉強の場でもあるのでえいやっとやてみた。

sinatra-cache は Lokka + Nginx + Unicorn という環境であれば、LOKKA_ROOT/lib/lokka/app.rb を開いて以下のようにしてやれば使えるようになります。

require 'lokka'
require 'sinatra/cache'        # <= 追加

module Lokka
  class App < Sinatra::Base
    configure do
      # ...
      register Sinatra::Cache  # <= 追加
      set :cache_enabled, true # <= 追加
      # ...
    end
    # ...
  end
end

とかやってやれば、勝手に LOKKA_ROOT/public にキャッシュファイルを作るようになります。Nginx 側でキャッシュファイルがあれば Unicorn に proxy せずキャッシュファイルを返すようにすればページキャッシングで爆速になる。sinatra-cache は {$request_filename}.html という名前でキャッシュファイルを作るので Nginx の設定は以下のような感じになる。

server {
    location {
        root /var/wwww/portalshit/public;
        # ...
        if (!-f $request_filename.html) {
            add_header Cache-Control public;
            rewrite (.*) $1.html;
            break;
        }
        # ...
    }
}

ポータルシットはトップページも重いのでトップページもページキャッシュしようかなと思ったけど、ページングとかあるのでいろいろ面倒くさいことになることに気がついた(キャッシュファイルがない状態で Google のクローラーが 35 ページ目とかをクローリングしてたら 35 ページ目の html が index.html としてキャッシュされてトップページに来た人が全員 35 ページ目を見ることになってしまう)のでやめた。

キャッシュ、レスポンスを速くしてくれるけど何でもキャッシュすれば良いわけではないし奥が深い。

Lokka でキャッシュしたいと書いてたけどキャッシュできるようになった。

Lokka でキャッシュしたい - portal shit!

結局 sinatra-cache を使った。sinatra-cache、2 年くらいメンテされてなくて全然だめかなと思ってたけどドキュメントが執拗なほど詳しく書かれてて使い方がわかりやすかったので使ってみることにした。フラグメントキャッシュがページごとに共有されなくて非効率的だったところは適当に改造した(morygonzalez/sinatra-cache · GitHub)。

footer 部分だけキャッシュするようにしてるのでトップページのレンダリングはあまり変わってないように感じる。個別記事ページは HTML が返ってくるまで 2, 3 秒かかってたのが 500ms から 600ms くらいになった。割とよいと思う。

いまのところ cache の有効期間みたいのを設定できないのでこれを任意の時間で設定できるようにしたい。しばらく使ってみて問題なさそうだったら Lokka 本体に Pull Request してもよいかも。

Lokka がすごく遅い

EC2 の micro で Lokka を運用してたけど、Unicorn が CPU 100% 近く使う状態が続き、リクエスト送ってレスポンスが返ってくるまでに 30 秒近くかかる状態になってて、AWS での運用を諦めざるを得なかった。さくら VPS に戻したところ、CPU 使用率もロードアベレージも落ち着いた。しかしレスポンスは遅くて、HTML が返ってくるまでに 5 秒くらいかかってる。

原因調べた

Newrelic を入れて調べてみた。Application が一番遅い。MySQL も遅いけど気になるレベルではないみたいだった。

  • EC2 に Application
  • さくら VPS に MySQL

という構成で運用していたのが遅い原因だったかも。App も DB も同じサーバーに置いたら Newrelic 上で Database が遅いと表示されなくなった。つまり Application をどうにかするしかない。

やろうとしてること

Lokka で動いててもそんなに遅くないページもある。komagata さんのブログは遅くない(heroku に置いてあるっぽいので heroku 側でキャッシュとかいろいろやってあるのもあると思う)。自分のブログに関してはテンプレートで最近の過去数ヶ月の月ごとの記事数表示したりタグクラウド出したりしてるところが遅そう。なので重い処理のところをフラグメントキャッシュしたい。

試したこと

sinatra-cache

導入できて動いた。勝手にページキャッシュする。フラグメントキャッシュできるけど、キャッシュのキーが URL のディレクトリベースのため、効率が悪い。

padrino-cache

導入できなかった。Padrino::Routing に依存してるっぽくて素の Sinatra で使いづらい。

落っこちてた gist (Simple fragment caching in sinatra)

これも Sinatra が前提。view から使うフラグメントキャッシュ専用ヘルパーメソッド。なんか Sinatra が内部的に使ってるインスタンス変数を上書きするというやり方みたい そのままでは Lokka で使えず <- イマココ。

最後のやつが一番導入に近いところまで来てるっぽいけど、断片の部分だけ haml のコードを実行させて結果を取得させる、というところがなかなか難しい。Rails の ActionController::Caching のコードを見て参考にしようとしてみたけどちょっとよく分からなかった。

実は最近、仕事で Ruby 書かないおじさんになってしまったので週末に Ruby のコード見てもすんなり頭に入ってこない。ダメだなぁ。

Lokka で Syntax Highlight するプラグイン(morygonzalez/lokka-pygmentize · GitHub)、これまで Ajax で Syntax Highlight させてたんだけど、HTTP リクエストが増えていけてないと感じたのでサーバーサイドでハイライトが完結するように変更した。Railscasts の #272 Markdown with Redcarpet - RailsCasts を参考にして、プラグインの中で Entry クラスを再オープンした。こんな感じ。

class Entry
  def body
    doc  = Nokogiri::HTML(self.long_body)
    doc.search("//pre").each do |pre|
      code = pre.css("code")[0]
      pre.replace Pygments.highlight(
        code.text.rstrip,
        :lexer   => code[:class],
        :options => { :encoding => 'utf-8' }
      )
    end
    doc.to_s
  end
end

前の実装はだいぶいけてなかったと思うので随分マシになったと思う。あと この Pull Request でレンダリングエンジンに Redcarpet が追加されたので、GitHub と同じように

```ruby

```

みたいな感じの書き方でコードがハイライトされるようになった。便利。

追記1

ちゃんと動いてなかった…。夜直します…

追記2 2013/02/04 0:45

最終的にオープンした Entry クラスのコードは以下のようになった。

class Entry
  alias_method :original_long_body, :body
  def highlighted_long_body
    syntax_highlight(self.original_long_body)
  end
  alias_method :body, :highlighted_long_body

  alias_method :original_short_body, :short_body
  def highlighted_short_body
    syntax_highlight(self.original_short_body)
  end
  alias_method :short_body, :highlighted_short_body

  def syntax_highlight(body)
    doc = Nokogiri::HTML(body)
    doc.search("//pre").each do |pre|
      code  = pre.css("code")[0]
      lexer = if pre[:class].present?
                pre[:class]
              elsif code.present? && code[:class].present?
                code[:class]
              else
                nil
              end
      begin
        pre.replace Pygments.highlight(
          code.text.rstrip,
          :lexer   => lexer,
          :options => { :encoding => 'utf-8' }
        ) if code
      rescue MentosError
        next
      end
    end
    doc.to_s
  end
end

Lokka、結構メタプログラミングが多くて、Entry クラスのインスタンスの body メソッドは単なるゲッターではなく、 index アクションのときと show アクションのときで別々にエイリアスが設定されていて、index アクションのときは Entry#short_body 、show アクションのときは Entry#long_body が呼ばれるようになっていた。アラウンドエイリアス使って力業で解決したけど他のプラグインが同じように振る舞ったら破滅を招きそうな気がする…。それにしても『メタプログラミング Ruby』読んでなかったらどうすればいいか皆目検討付かなかっただろうなー。

P_BLOG 使ってた頃から過去記事タイトルを一覧表示する機能は自分で自分の記事を読み返すときに重宝していて、この機能が欲しかったので作った。過去記事を全部一覧表示すると重いので一年分ずつ表示するようにした。便利。

portal shit!

portal shit!

博多市在住のサイト片手間ビリティエンジニアのウェブログです。

portalshit.net

ソースコードは以下。

morygonzalez/lokka-archives · GitHub

ちなみにテストはありません。悪しからず。

Lokka 、データベースはずっと SQLite で使ってたけど仕事で MongoDB を使っているため SQL 力の弱まりを感じてきたので MySQL に変えてみた。SQLite3 から MySQL への移行は意外と面倒くさくて、以下の Redmine の手順を参考にやってみた。

  1. Strip out PRAGMA lines
  2. Strip out BEGIN TRANSACTION; lines
  3. Strip out COMMIT; lines
  4. Strip out DELETE FROM and INSERT INTO the sqlite_sequence table
  5. Replace AUTOINCREMENT with AUTO_INCREMENT
  6. Replace DEFAULT ‘t’ and DEFAULT ‘f’ with DEFAULT ‘1’ and DEFAULT ‘0’
  7. Replace ,’t’ and ,’f’ with ,’1’ and ,’0’
  8. Replace “ with ` except in string values (otherwise it replaces all quotes in your text)

Migrating from sqlite3 to mysql - Redmine

↑の通りにやっておおむねうまくいったんだけど、なんか過去の記事を編集して更新すると、updated_at カラムだけじゃなく created_at まで更新されてしまうっぽい。SQLite で使ってるときにはそんなことなかったんだけどなぁ。これは問題な気がする。DataMapper のバグかな。土日で余裕があったら調べてみる。

ここにはっつけるコードのシンタックスハイライトには Google Code Prettify をこれまで使ってたんですけど、どうもいまいちでした。有名な SyntaxHighlighter も好きになれなかった。はてなブログのように綺麗なシンタックスハイライトさせたい! ということで作った。

morygonzalez/lokka-pygmentize

かなりシャレオツな感じにシンタックスハイライトできるようになったと思います。拾ってきた Monokai スタイルの CSS を当てています。

動作環境

裏側で使っているのは Python の Pygments。JavaScript オンリーで色付けするやつよりもこいつの方が圧倒的に綺麗でした。なのでこのプラグインを使うには Python と Pygments が必要です。Heroku では動くんでしょうか。動作未確認です。

使い方

HTML の pre タグのクラス名を lexer として Pygments に渡します。Ruby のコードを書くのであれば以下のようにします。

<pre class="ruby">
  <code>
  class Book
    def off
      "all your book is 10 yen"
    end
  end
  </code>
</pre>

やってること

JavaScript で pre タグを探してサーバーにコードの中身と pre タグのクラス名を投げると、Pygmentize された HTML が返ってくるようになっております。そいつを JavaScript で拾って pre タグの中身を入れ替え。

当初は

```ruby
class Book
  def off
    "all your book is 10 yen"
  end
end
```

みたいな感じで GitHub 風にしたいと思っていて、以下のような JavaScript を書いていたんですが JavaScript 力が低すぎて断念。

$('.body').each(function() {

  var Pygmentize = function(lexar, snippet) {
    var result;

    $.ajax({
      type: 'POST',
      url: '/pygmentize',
      async: false,
      data: {
        lexar: lexar,
        snippet: snippet
      },
    }).done(function(data) {
      result = data;
    });

    return result;
  }

  var entryBody = $(this).text();

  entryBody = entryBody.replace(/```(.+?) ([sS]*?)```/g,
    function(whole, lexar, snippet) {
      return Pygmentize(lexar, snippet);
    }
  );

  $(this).html(entryBody);
})

※Ajax なのに async: false とかしててイマイチ感ありますね。

とはいえ、汎用性の高い pre タグのクラス名を拾ってくる、という形での実装にしたので、 Markdown でなくても HTML でも Textile でもハイライトできるので結果オーライとします。

ちなみに Kramdown でクラス名を指定したいときは以下のようにするみたいです。

> hogehoge
{: .hoge }