リリカルメール配信機能のアーキテクチャ

はじめに

RubyのMongoDBドライバには不具合があります。
ある条件でfind and modifyを使用すると失敗するというものです。
リリカルメールの配信機能を実装していた時にこれに気付いてチケットを作ったのですが、次のver 1.9.0で解消されるとのことでした。
実装していたのは去年の9月で大分前の話ですが、この知らせを受けて思い出したので、配信のアーキテクチャについて書こうと思い立ちました。

配信機能

ユーザーが指定した条件に一致する投稿をリアルタイムでメール送信するのが配信機能です。
例えば同年代の人の投稿のみ受け取りたければ20代と指定しておくと、20歳から29歳までの人の投稿が自動的に届くようになります。
便利な機能ですがリリカルメールの中で最も負荷が高い機能です。
例えば秒間10投稿あるとして、配信に登録しているユーザーが1万人いるとすると、条件に一致するかの検索を秒間10万回行う必要があります。

リリカルメールの検索はタグがツリー構造を取っているために、完全一致の検索よりは重い処理になります。具体的には、タグのツリー構造をマテリアライズドパスで表現していて、正規表現の前方一致による検索を行っています。

そのためスケール可能な構成にする必要があり、リリカルメールではキュー・ワーカーモデルの構成をとっています。
投稿があるとキューに貯めて、ワーカーがキューに対して検索を行うというものです。ワーカーは2種類いて、検索ワーカーと、メール送信ワーカーです。
検索ワーカーが条件にヒットした投稿を別のキューに貯めて、そのキューからメール送信ワーカーがメールをユーザーへ送信します。
キューというとRabbitMQやZeroMQを思い浮かべる方が多いと思いますが、リリカルメールはMongoDBを使っています。
MongoDBはRDBMSの代替として使っていて、ミドルウェアを増やして複雑にしたくなかったので、キューについても利用しようという考えです。
ただ、MongoDBにはトランザクションがありません。それでどうやってキューを実現するかというと、先ほどのfind and modifyというMongoDBのコマンドです。
このコマンドは参照と更新を一貫性をもって実行することができます。MongoDBでトランザクションっぽい操作ができるコマンドです。
これを各ワーカーがMongoDBに対して実行し、各ユーザーの検索条件を取得して検索済みの時刻を設定するということをやっています。

なお、MongoDBはレプリカセットを使って負荷分散しています。つまりセカンダリ優先の読み込み設定としているのですが、書き込みはマスターにしか行えないのにも関わらず、ドライバはセカンダリに書きにいこうとして失敗するというのが最初に書いた内容です。

実のところ、スケールアウトする必要性は今のところないのですが、一人でやっている以上は必要に迫られてから実装するのでは遅いので、サーバーを追加すればスループットが上がるというのを意識して開発をしています。

FoodcriticとJenkinsでcookbookの静的解析を行う

Jenkins logo

はじめに

Foodcriticはcookbookのlintツールです。
インストールにはRuby 1.9.2+が必要です。
gemをインストールします。

gem install foodcritic

インストール後、foodcritic [cookbook_path]で実行でき、解析結果が表示されますが、今回はこれをJenkinsから実行するようにします。


Jenkinsの設定

Git pluginのインストール

今回cookbookはGithubから取ってこようと思いますので、Git pluginを使います。

Jenkinsの管理 > プラグインの管理 > 利用可能タブ > Jenkins GIT plugin

からインストールできます。

ジョブを作成

新規ジョブ作成を選択し、ジョブ名に適当な名前を入力し、フリースタイル・プロジェクトのビルドを選択してOKを押します。

ソースコード管理システムにGitを選択し、Repository URLにGithubの読み取り専用のURLを入力します。


ビルド手順の追加でシェルの実行を選択します。
foodcriticを実行するようにします。-f anyを指定すると、構文解析の警告時にビルドが失敗するようになります。

/usr/local/bin/foodcritic -f any .


Warnings pluginの追加

このままだとコンソール出力で結果が確認できるだけですので、警告を追跡できるようにします。
Warnings pluginを使います。

プラグインをインストールしたら、
Jenkinsの管理 > システムの設定 > コンパイラの警告
で、追加ボタンを押して次のように入力します。

名前、リンク名、推移レポート名

Foodcritic

正規表現

^(FC[0-9]+): (.*): ([^:]+):([0-9]+)$

マッピングスクリプト

import hudson.plugins.warnings.parser.Warning

String fileName = matcher.group(3)
String lineNumber = matcher.group(4)
String category = matcher.group(1)
String message = matcher.group(2)

return new Warning(fileName, Integer.parseInt(lineNumber), "Chef Lint Warning", category, message);

これらはつまり、Foodcriticの警告文字列をWarnings pluginのどの項目に割り当てるかの設定です。


ジョブの設定で、ビルド後の処理にコンパイラの警告の集計を追加します。
コンソールログをスキャンするのパーサーに、先ほど作成したFoodcriticを選択します。

さらに、高度な設定をクリックして、常に実行をチェックします。

これでビルドを実行すると、次のように警告が表示されます。

result


あとはビルド失敗時に通知するようにすればよいと思います。


おわりに

今回なぜFoodcriticを使おうと思ったかというと、cookbookは本来、環境の差異を抽象化して吸収できるように作るべきかと思いますが、今自分が作っているものはあまりにも個人的な作りなので、そういった点を指摘してくれるかと考えたからです。

追加のルールがいくつか公開されているので、それらも使っていこうと思っています。

(参考 http://acrmp.github.com/foodcritic/

BerkshelfとVagrantで環境構築を自動化する


Vagrant logo


概要

目標は、仮想マシンの新規作成からWebアプリケーションのデプロイまでを自動化することです。
条件は以下です。

・インフラ系のcookbookはGithubの読み取り専用のURLから取ってくる。
・Webアプリケーションをデプロイするcookbookはプライベートリポジトリから取ってくる。
 ここへのアクセスはパスワード付きの秘密鍵を使用する。

Berkshelfはcookbookを管理します。
cookbookにおけるBundlerと考えると分かりやすいと思います。
BundlerではGemfileに必要なgemを書くように、BerkshelfはBerksfileに必要なcookbookを記述します。また、BerkshelfはVagrantのpluginを提供しています。
これを使うと、仮想マシン作成時などにBerksfileに記述したcookbookを使用してプロビジョニングを行うことができます。

環境

ホスト:OS X Mountain Lion 10.8.2
ゲスト:CentOS 6.3

Vagrantをインストールする

下のURLに書いてある通りですが、要点のみ書きます。

http://docs.vagrantup.com/v1/docs/getting-started/index.html

VirtualBoxをホストにインストールします。下のURLからパッケージをダウンロードします。

https://www.virtualbox.org/wiki/Downloads

Vagrantをインストールします。

gem install vagrant

Berkshelfをインストールする

下のURLにインストール方法から使い方まで書いてありますが、

http://berkshelf.com/

下記コマンドでインストールします。

gem install berkshelf

設定

Berkshelfのコマンドから新しいcookbookを作ります。

berks cookbook LyricalMail

これでLyricalMailという名前のcookbookが作成されます。chefのknifeを使ってcookbookを作った時と異なるのは、BerksfileとVagrantfileのテンプレートが作成されます。

このcookbookを他の全てのcookbookの親にしようと思いますので、このcookbook自体のrecipeは何も書きません。(テンプレートが欲しかっただけです。)

Berksfileを編集します。

以下のように、gitのリポジトリを指定します。上の方は読み取り専用の公開リポジトリで、下の方はプライベートリポジトリです。

cookbook 'ruby', git: 'git://github.com/yudozen/chef-ruby.git'
cookbook 'lyricalmail', git: 'git@bitbucket.org:yudozen/chef-lyricalmail.git'

Vagrantfileを編集し、cookbookを指定します。

chef.run_list = [
    "recipe[ruby]",
    "recipe[lyricalmail]"
]

ここまで設定してvagrant upとコマンドを叩けば仮想マシンが作成され、cookbookに書いた通りに環境が構築されると思いたいですが問題があります。

Permission denied (publickey)

のようなエラーが表示されるはずです。

なぜこのエラーとなるかというと、プライベートリポジトリからcloneするために秘密鍵を使用しており、鍵にはパスワードがかかっています。
Vagrantを通してchefを実行している時は、パスワードを入力する必要があってもホストのターミナルで入力待ちにならないのでパスワードが入力できずにエラーになってしまいます。

なのでssh-agentを使います。

ホストでssh-agentを起動し秘密鍵を登録するだけでは当然ですがうまくいきません。
仮想マシン側にssh-agentをフォーワドする設定がVagrantにありますので、これを使用します。
Vagrantfileに以下を追記します。

config.ssh.forward_agent = true

これで仮想マシンにssh-agentの環境変数が引き継がれるようになりますが、これでもまだ失敗します。

なぜならchefがsudo実行されるからです。

/etc/sudoersDefaults env_keep = “SSH_AUTH_SOCK”の設定があれば問題ないかと思いますが、今回は仮想マシンを新規に作成しているので、この設定は入っていません。
(もちろん、設定が入っているBoxを作っておいてもよいと思います。)

調べると、親プロセス(sudoの実行元)のssh-agentのソケットを探して環境変数にセットするというやり方をしている人がいました。

http://stackoverflow.com/questions/7211287/use-ssh-keys-with-passphrase-on-a-vagrantchef-setup

上記URLの通りですが、以下のコードをrecipeに書きます。

ruby_block "Give root access to the forwarded ssh agent" do
  block do
    # find a parent process' ssh agent socket
    agents = {}
    ppid = Process.ppid
    Dir.glob('/tmp/ssh*/agent*').each do |fn|
      agents[fn.match(/agent\.(\d+)$/)[1]] = fn
    end
    while ppid != '1'
      if (agent = agents[ppid])
        ENV['SSH_AUTH_SOCK'] = agent
        break
      end
      File.open("/proc/#{ppid}/status", "r") do |file|
        ppid = file.read().match(/PPid:\s+(\d+)/)[1]
      end
    end
    # Uncomment to require that an ssh-agent be available
    # fail "Could not find running ssh agent - Is config.ssh.forward_agent enabled in Vagrantfile?" unless ENV['SSH_AUTH_SOCK']
  end
  action :create
end

これでvagrant upvagrant provisionなどで成功します。

少し複雑になってしまいますが、こういうのをRubyで書けるのがChefのいいところだと思いました。
あと、Vagrantfileにchef.log_level = :debugを記述するとログレベルを変更できるので、役に立つことがあると思います。

これで定期的に環境構築〜インテグレーションテストを実行するようにしておくことで、インフラを含めたシステム全体のテストを自動化することができます。

まだそこまではできていないので、確立できたらまた書こうと思います。

lokkaをApache+Passengerでproduction環境で動かす

Lokka logo


概要

Ruby製CMSのlokkaをPassengerで動かした時の手順です。

Wordpressじゃなくlokkaを使う理由は、軽量フレームワークのsinatraを採用しているので全体が把握しやすく、カスタマイズもしやすいのではないかと思ったからです。

あとは個人的にPHPよりはRubyの方が慣れているというのもあります。

Rubyのサーバーに関してはUnicornかPassengerかと思いましたが、PassengerはApacheのモジュールとして動作するので、設定はhttpd.confを書けばよく、手軽に導入できるのでこちらにしました。

公式サイトにWEBrickで動かす手順が書いてあります。まずはこれで試してみるとよいと思います。
http://lokka.org/getting-started


環境

CentOS release 6.3 (Final)

ruby 1.9.3p327


手順

bundlerをインストールします。lokkaで使用するgemの管理に使います。

gem install bundler

lokkaのソースを取ってきます。

git clone git://github.com/komagata/lokka.git

データストアにはsqliteを使おうと思いますので、必要なパッケージをインストールします。

yum install "sqlite-devel"

production環境でsqliteを使うように設定を編集します。
本来環境変数を使うべきなんでしょうが、ハードコードしました。

lokka/database.default.yml

production:
  #dsn: <%= env['database_url']="" %="">
  dsn: sqlite3://<%= root="" %="">/db/production.sqlite3

gemをシステムではなく、lokka以下にインストールすることにします。pathにvendor/bundleを指定します。

bundle install --without=production:postgresql:mysql --path vendor/bundle

DBマイグレーションを行います。pruduction環境を指定します。

bundle exec rake db:setup RACK_ENV=production

Passengerで必要なパッケージをインストールします。
私の場合は以下の通りインストールしました。
※どのパッケージが必要かは、後でPassengerのインストールをする時に分かります。

yum install httpd curl-devel httpd-devel apr-devel apr-util-devel

Passengerをインストールします。

gem install passenger

PassengerのApache用モジュールをインストールします。

passenger-install-apache2-module

ここで、足りてないパッケージがあれば警告してくれるので、標準出力の指示に従ってインストールをしてください。
Apache用モジュールのインストールに成功すると、Apacheの設定ファイルに記述する内容が表示されるので、その通り設定します。
私の場合はpassenger.confというファイルを作成し、/etc/httpd/conf.d/以下に配置しました。

passenger.conf

LoadModule passenger_module /usr/local/rbenv/versions/1.9.3-p327/lib/ruby/gems/1.9.1/gems/passenger-3.0.18/ext/apache2/mod_passenger.so
PassengerRoot /usr/local/rbenv/versions/1.9.3-p327/lib/ruby/gems/1.9.1/gems/passenger-3.0.18
PassengerRuby /usr/local/rbenv/versions/1.9.3-p327/bin/ruby

<VirtualHost douzen.net:80>
    ServerName douzen.net
    DocumentRoot /home/yudozen/blog/lokka/public
    <Directory /home/yudozen/blog/lokka/public>
        # This relaxes Apache security settings.
        AllowOverride all
        # MultiViews must be turned off.
        Options -MultiViews
    </Directory>
</VirtualHost>

Apacheを再起動して完了です。

service httpd restart


今回作ったchefのcookbookです。汎用的に作っていないので、参考程度にしてください。

yudozen / chef-lokka