ぴよログ

↓に移転したのでこっちは更新されません、多分。

mysqlslapコマンドの使いかた事例

1年ほど前にRSSリーダーの開発をしていたとき、環境ごとのMySQLのパフォーマンスを調べるのにmysqlslapというコマンドを使っていたが、割と適当に使っていたので今こそそれを振り返ってみようと思う。

そもそもmysqlslapが何かと言うと、MySQLの公式ドキュメントにはこのように書かれている。

MySQL :: MySQL 5.1 リファレンスマニュアル :: 7.15 mysqlslap — クライアント負荷エミュレーション

mysqlslapはMySQLサーバのクライアント負荷をエミュレートし、各ステージのタイミングを報告する診断プログラムです。サーバにたいして複数のクライアントがアクセスしているかのように作動します。mysqlslapはMySQL 5.1.4.から提供されています。

なるほどなるほど、複数クライアントから接続されている状況をエミューレートできるんだね。

で、僕が検証に使っていたコマンドの一部がこんなのだった↓。ネットワークが影響しない状況で確認したかったからSSHで入ってlocalhostMySQLに接続してテストした。

$ mysqlslap \
--no-defaults \
--concurrency=10 \
--iterations=1 \
--number-int-cols=2 \
--number-char-cols=3 \
--auto-generate-sql \
--engine=innodb \
--auto-generate-sql-add-autoincrement \
--auto-generate-sql-load-type=key \
--auto-generate-sql-write-number=2000 \
--number-of-queries=30000 \
--host=localhost \
--port=3306 \
--user=root

# 結果はこんなふうに出る
Benchmark
        Running for engine innodb
        Average number of seconds to run all queries: 3.274 seconds
        Minimum number of seconds to run all queries: 3.274 seconds
        Maximum number of seconds to run all queries: 3.274 seconds
        Number of clients running queries: 10
        Average number of queries per client: 3000

それぞれのオプションがどんな意味なのかを見ていってみようと思う。

--no-defaults

設定ファイルに書かれているデフォルト値を無視するためのオプション。色々な環境で試すときにはこれをやっておくと環境ごとの差異をなくせる。

--concurrency=10

シミュレートする同時接続クライアントの数で、↑の場合は10。

--iterations=1

実行する回数。

--number-int-cols=2

使うINTカラムの数。

--number-char-cols=3

使うVARCHARカラムの数。

--auto-generate-sql

テストに使うSQLを自動生成する。ファイルやオプションでSQLが与えられていない場合。

--engine=innodb

エンジンはInnoDBだよね〜。

--auto-generate-sql-add-autoincrement

自動生成されたテーブルにAUTO_INCREMENTカラムを追加する。

--auto-generate-sql-load-type=key

テストのタイプを指定する。ここではkey(主キーの読み取り)を使ったけど、他にも色々ある。

  • read(テーブルのスキャン)
  • write(テーブルの読み取り)
  • update(主キーの更新)
  • mixed(挿入とスキャンを半分ずつ)

--auto-generate-sql-write-number=2000

各スレッドで実行する挿入クエリの数。…ん?keyのときは意味ないのでは…

--number-of-queries=30000

クライアントで実行される合計のクエリ数。

--host=localhost

接続するホスト。

--port=3306

接続するポート番号。

--user=root

ユーザー名

こうやって見るとオプションが多すぎてきちんと把握していなかったな。よく見ると意味がないかもしれないオプションとかもある。

Railsでマイグレーションを介さずにサーバーでDBを変更してしまったとき

本番環境で動いているRailsアプリケーションのDBにおいて、問題に緊急対応するために直接ALTER_TABLEしてしまったようなとき、Railsアプリケーション側ではどんな対応をすればいいか、というお話。

レアケースかもしれないし意外とあるかもしれない。

僕は実際に1年ほど前に開発していたRSSリーダーのときに経験したことがある。ある日の夜中、サービスにアクセスが集中してデータベースのレスポンスがめちゃくちゃ悪くなったとき、僕は普通に寝ていた。いや、それまでの対応で力尽きていたと言っていい。

サーバーがうんともすんとも言わないそんな状況の中、ヘルプで入ってくれている人がスキーマを調べてインデックスが足りない(!)ということに気がついたらしい。翌朝起きてみると「とりあえず直接インデックス追加してなんとかしました」という連絡が来ていた。

インデックス足りないとか!しょぼすぎるミスなのはさておいてひとまず問題は解決した。あれ?でも直接DBいじっちゃったらRails側のマイグレーションと整合性とれないぞ?

そういうときの対処法です。

そもそもマイグレーションとは?

そもそもrake db:migrateは何をやっているのかというと、db/migrate以下にあるファイルに書いてあるDBに対する変更処理をファイルの日付順に実施していくというものだ。このとき、DBのschema_migrationsというテーブルにこれまでに適用したマイグレーションの番号(日付の文字列)を持っている。

rake db:migrateではこのテーブルを見に行って未適用のマイグレーションを実行するような感じになっている。その都度schema_migrationsは更新される。

具体的な対応方法

Railsアプリケーション側

% rails generate migration AddIndexToHoge みたいなコマンドで新しいマイグレーションを生成する。そして、手作業で変更してしまった処理に相当する処理をマイグレーションファイルには書いておく。

例えば僕の経験したケースでは、プロダクションのデータベースでインデックスの追加を行った。そういうときはこのような感じのマイグレーションを書く。

class AddIndexToHoge < ActiveRecord::Migration
  def change
    add_index :hoges, :fuga, name:'idx_hoge_name'
  end
end

nameを敢えて指定しているのはプロダクションDBで行った変更で追加したインデックスの名前がRailsのデフォルトとは異なっていたから。

さて、これでRailsアプリケーション側での準備は整った。

プロダクションDB側

先ほど作ったマイグレーションの内容はすでに手動で適用済みであるため、もう一度マイグレーションが走るとエラーになってしまう。そこで先程作ったマイグレーションの番号をschema_migrationsに手動で追加してやる。

> INSERT INTO schema_migrations (version) VALUES(20131031113857);

これであたかもrake db:migrateマイグレーションを適用したような結果にすることができる。

一応これらの対策で、プロダクションDBに対して手動で変更を行ってしまったとしてもRailsアプリケーションの流れに戻せるということになる。

Rebuild.fmを勝手に振り返る

Rebuild.fmめちゃくちゃ面白いので勝手に振り返ります。もう1年以上になるので僕の人生の30分の1をカバーしているわけです。結構すごい。

話題別に行こう

RSSリーダーネタ

これのすぐあとにRSSリーダーを作ることになったので何回か聴きました。pubsubhubbubという仕組みやRSSリーダーのユーザーエージェントなど何も知らなかったな。懐かしい。

結局データ量が多くてコストがかさんだこととユーザーがあまりつかなかったこともあってシャットダウンしてしまいましたが。。。RSSリーダーのデータはどのように扱うのがいいのか詳しい方に考えを聞いてみたいです。

Emacsネタ

@miyagawaさんと@naoya_itoさんがEmacsユーザーだということで度々登場するEmacsネタ、僕もEmacsユーザーだったのですげーおもしろいです。「いやそれEmacsでやったほうが」→「201X年になってEmacsの話するのはやめましょう」っていう流れが定番。

最近では@naanさんが出た回でGitHubAtomエディタの話に絡めて、ちょっとずつ乗り換えたってのを聞いて僕も気合を入れてSublimeText3に乗り換えました。できるだけこれまで使っていたEmacsと同じようにキーアサインして何とか使っています。

正直Emacsに戻りたくなることのほうが多いのですが、プラグイン周りの拡張機能の作りやすさを考えるとelispから離れたほうがいいだろうなというのはあります。AtomCoffeeScriptで拡張できるそうなので本当はそちらがいいと思ったんですが、パフォーマンスを理由に今はSublimeでなんとかしています。

ガジェットネタ

このあたり。

主に@hakさんが出ている回。iOSMac、Oculusなどのコア〜〜な話を聞けて面白い。Rebuild聞いてなかったらm7のことなんて全然知らなかったかもしれません。チップの話なんて考えたことなかった。名言「チップマニアなら買い」。

Infra as Code, Immutable Infra

ここ1年ぐらいのHottestなトピックですかね。Webは全然やっていなかったのですがこの話題のおかげで少しは興味を持つようになったし、前述のRSSリーダーをホストするときにChef Soloを使うきっかけになりました。てか、それまではサーバーの設定を自動化するなんて考えたこともなかったので。。。今使っているさくらVPSのサーバーもChefで書き直したいぐらいです。

先日の大江戸Ruby会議でもInfratasterのトークのときにここらへんのネタが登場しましたが、Rebuild聞いていたおかげでわかりました。まさに進研ゼミ。

RubyのOPMLパーサー

結論

これ使えば一発です。

feedbin/opml_saw

あとは雑談

2013年6月末のGoogleリーダーのシャットダウンの際、OPMLというフォーマットを初めて知りました。なんのことはないただのXMLなんですが、一応アウトラインプロセッサーのためのMLということでOPMLという名前になっているらしいです。

RSSフィードの購読データのエクスポートやインポートに使われているのしか見たことがありません。GoogleリーダーFeedlyでも購読しているフィードをOPMLファイルで出力することができるようになっています。

昨年僕が開発していたRSSリーダーでもOPMLのインポート機能を実装していました。これを実装するにあたりやるべきステップは3つありました。

  1. OPMLをパースする
  2. フィードのフォルダを作る
  3. フィードを購読する

このうち2と3はOPML対応前から持っていた機能だったのでOPMLインポート機能のためにはパース処理のみを書けばよかったわけです。

XMLなのでNokogiriで簡単にパースできるのでしょうが、RSSリーダーのFeedbinがちょうどぴったりなライブラリ、feedbin/opml_sawというものを公開していたのでこちらを使うことにしました。

使い方は至ってシンプルで、OPMLファイルの中身を渡すとフィードのArrayが返ってくるというものです。

READMEから拝借しますが、これだけです。Nokogiriで頑張るよりも更に楽なので良いですね。

file = File.open('subscriptions.xml', 'r')
contents = file.read
opml = OpmlSaw::Parser.new(contents)
opml.parse
pp opml.feeds☄

opmlsawのおかげでほとんど実装要らずでOPMLインポートの機能を実装することができたという話でした。

ちなみに、OPMLインポート自体はとても重い処理なのでバックグラウンドに回しました。実装は簡単だったものの、バックグラウンドジョブでエラーになったりしてそっちの解消が大変だったりしました。例えばOPMLファイルがでかすぎてインポートが全然終わらないとか…。懐かしい。

RSSリーダーの記事更新のバックグラウンドジョブ概要

RSSリーダーを作ったときのことをまとめておくシリーズ。

これまでの記事はこちら。

RSSリーダー - PILOG

僕が知る限り全てのRSSリーダーは登録フィードをバックグラウンドで更新し記事を追加します。RSSリーダーのユーザー側、システム側から見てみます。

ユーザー視点

  1. RSSリーダーにブログやニュースサイトのフィードを購読
  2. これまでの記事を閲覧できるようになる
  3. サイトが更新されたら新しい記事を閲覧可能になる

RSSリーダー視点

  1. ユーザーがRSSフィードを購読する
  2. すでに誰かが購読済みのフィードかどうかを調べる
    • 誰かが購読済み→フィードの購読者にこのユーザーを加える
    • 最初の購読者→フィードデータを作成し記事をクローリングする、また購読者にこのユーザーを加える
  3. 取得済みの記事をユーザーに見せる
  4. フィード配信元に更新があるかどうか定期的にクローリング
  5. 更新があれば新たな記事を追加する
  6. (更新後に見たユーザーは新しい記事を読めるようになっている)

定期的なクローリング

両者を比べてみるたときに一番違うのは、強調表示した「フィード配信元に更新があるかどうか定期的にクローリング」というところです。もっと言い換えれば、更新があるかどうか見に行くという行為をユーザーの代わりに行ってくれるのがRSSリーダーの価値であると言えます。

そして、システムからすればもっとも負荷の高い部分のうちの一つとも言えます(もう一つはクロールした記事のデータ量。日に日に増えていくので、、、)。

ではこのクローリングをどのように実施したかを紹介していきます。

Rails & Resque

クローリングはユーザーの操作がないときでも定期的に動かす必要があります。したがってアプリケーションサーバーやWebサーバーは関係ありません。もっとも簡単に実行するには、定期的にスクリプトが動いてデータベースを更新するようなものでもできてしまいます。

とはいえ、単なるジョブ用スクリプトを使うとアプリケーションと同じモデルを利用するのが困難でスクリプト自体が大変複雑になることは間違いないので、もうちょっとマシな方法を使います。

RSSリーダーRuby on Railsで作り、バックグラウンドジョブにはResqueを使用しました。

Resqueに関してはiQONを運営するVASILYさんの記事が詳しかったのでこちらを参考にいろいろ実装しました。超助かりました。

iQONのバックエンドの非同期処理について(具体的な実装編) | VASILY  DEVELOPERS BLOGiQONのバックエンドの非同期処理について(具体的な実装編) | VASILY DEVELOPERS BLOG

Resqueのバックグラウンドジョブ用のワーカーは定期的にジョブキューを監視しつづけてくれます。暇なワーカーがいる限り、ジョブキューにジョブを放り投げるとすぐにジョブを実行してくれるようになっています。つまりワーカーを多数用意しておけば、がんがんジョブを回すことができますね。

RSSフィードの定期チェックのためには「あるRSSフィードを見にいって、更新があれば記事を追加する」というジョブを全ての記事分ジョブキューに登録すればよい、ということになります。

定期的にジョブを発行する

ジョブを処理する側はResqueを使って実装することができました。ではジョブはどのように発行すればいいでしょうか?

これにはcronのようにある時間毎に起動するプログラムを用いることにしました。

tomykaira/clockwork https://github.com/tomykaira/clockwork

このclockworkというgemを使うと、「◯◯分毎に△△する」というようなデーモンをRubyで書くことができます。実際には次のような手段を取りました。

  • clockworkを使い1分ごとにRSSリーダー特定のURL(例:/feed/update)にアクセス
  • Rails側で上のURLに対応したコントローラのアクションを作っておく
  • 対応アクションで更新ジョブをジョブキューに登録する

これで定期的にジョブを発行するという流れができました。

この方法だとジョブ発行アクションが1時間に60回呼ばれることになります。1回のアクション毎に1個のフィード更新ジョブを走らせていたのでは、ユーザーが期待するフィード更新頻度を保てません。

ユーザー視点で考えると元サイトが更新されたらすぐにRSSリーダーに反映されて欲しいはずなので、せいぜい1時間に1回はすべてのフィードを更新するようにしておくべきです。

これは単純計算すると、RSSリーダーのシステム内に存在するフィード数の60分の1を1分で更新しなければいけません。1分毎に呼ばれる更新アクションでは、フィードの更新時刻が古いものから60分の1個を取り出して、それを更新するジョブを作りジョブキューに投げるということを行います。

Resqueワーカーの数が十分で、データベースがボトルネックにならないのであれば、これでユーザーが必要としているRSSリーダーの価値の部分を実装できたことになります。

まとめ

  • RSSリーダーはバックグラウンドのフィード更新処理が肝
  • ジョブキューを監視するワーカーをたくさん動かしておく
  • ユーザーが期待する頻度で更新ジョブをキューに登録する

でも、さっきしれっと書いた十分な数のワーカーを動かしたりボトルネックにならないデータベースを用意するほうが大変だったりする。

RubyでRSSフィードをパースするgem、Feedzirra

RailsRSSリーダーを作ったときに得たノウハウ第2弾、RSSフィードのパースのところを書いてみようと思います。

第1弾としてこんなん書きました。

個別ページのURLからRSSフィードURLを取得するfeedbag - PILOG

サイトURLを渡すとHTMLを解析してRSSフィードのURL候補を返してくれるというgemの紹介でした。今回は得られたRSSフィードのURLに情報を取りに行ってパースしてくれるgemを紹介してみようかと思います。

Feedzirra

これを使います。

pauldix/feedzirra

いくつか候補がありました(全部忘れました)が、最も新しくてその上現在進行形で開発が進んでいる風だったこともあってこちらを使用していました。

初回のフィード取得

GitHubのREADMEにある通りですんなりいけます。折角なのでfeedbagを絡めてみます。

require "feedbag"
require "feedzirra"

feed_urls = Feedbag.find "http://xoyip.hatenablog.com/" # このブログ
p feed_urls.first # => "http://xoyip.hatenablog.com/feed"

feed = Feedzirra::Feed.fetch_and_parse(feed_urls.first)

p feed.title          # => "PILOG"
p feed.url            # => "http://xoyip.hatenablog.com/"
p feed.feed_url       # => "http://xoyip.hatenablog.com/feed"
p feed.etag           # => "f6ce826cbbd07e222b6cee445fc981f730ad0693"
p feed.last_modified  # => 2014-01-15 11:47:34 UTC
p feed.entries.count  # => 7

こんな風に結構簡単にRSSフィードの解析が終わりました。このブログを対象としてみましたが、7記事分の情報が取れるようですね。それ以前の記事はフィードされていないということになります。

更新の確認

RSSリーダーというものは、RSSの仕組みのせいでリーダー側からRSS配信元を見に行かなくてはならない、いわゆるプル型のサービスです。そのため、更新されているかどうかもわからないのにRSSフィードを見に行かなくてはなりません。そして更新があれば新しい記事の分だけをサービスに保存する、というようなことを行うことになります。

この辺りの設計について話しだすと長くなるので、後日書くかもしれません。

話をFeedzirraに戻します。

先ほどの例でもわかるように、はてなブログRSSフィードでは7記事分配信されるようです。ここではあるはてなブログを例に解説を進めます。

例えば初回取得時と2回目の取得時(つまり更新時)の間に対象のはてなブログに新たに2記事追加されたとします。

図でいうとこんな感じ。

先ほど使ったメソッド、fetch_and_parseを使うと現在のRSSフィードの内容をパースしてそこに載っている記事を全て取得してくることになるので、そのままデータベース等に保存しようとすると記事3〜記事7はダブってしまうことになります。

普通はそんなの困るのですでに取得済みの記事は除外して新しいものだけを新たに保存したいですよね。自前でやる場合はタイムスタンプ等で判別しなければいけません。

当然Feedzirraにはそのような方法が用意されています。

require "feedbag"
require "feedzirra"

# 最初は一緒

feed_urls = Feedbag.find "http://xoyip.hatenablog.com/" # このブログ
p feed_urls.first # => "http://xoyip.hatenablog.com/feed"

feed = Feedzirra::Feed.fetch_and_parse(feed_urls.first) 
p feed.entries.count # => 7

# この瞬間に2記事分更新されたとする(そんなことあり得ないけど)

updated_feed = Feedzirra::Feed.update(feed)
updated_feed.updated? # => true
updated_feed.new_entries.count # => 2

これで更新のときに必要な記事だけ取り出せますね、、と言いたいところですがそうはいきません。

何がだめ?

先ほどの更新の例では、最初にfetch_and_parseしたときに帰ってきたfeedというオブジェクトが生き残っていて、そのオブジェクトに対してupdateメソッドを呼ぶことで更新を確認しています。

普通に考えて初回取得時に存在していたオブジェクトが、それ以降も存在しているわけはありません。初回のRSSフィード取得が終わったしばらくあと(数時間後とか)にスケジュールされたジョブとして2回目のフィード取得が走るのが普通だからです。

つまり、feedに相当するオブジェクトを自分で生成した上でupdateを呼んであげる必要があるわけです。取得済みの記事だとかフィードの最終更新などの情報はデータベースなどに入れてあるはずなので、それらの情報からfeedを再作成します。こんな感じで。

feed = Feedzirra::Parser::RSS.new
feed.feed_url = "http://xoyip.hatenablog.com/"
feed.etag = "f6ce826cbbd07e222b6cee445fc981f730ad0693" # いらないかも
feed.last_modified = DateTime.parse("2014-01-15 11:47:34 UTC")
last_entry = Feedzirra::Parser::RSSEntry.new
last_entry.url = "取得済みの記事の中で最も新しいのURLを入れておく"
feed.entries = [last_entry]

Feedzirra::Feed.update(feed) 

そうするとupdateメソッドが使えるようになります。このような流れで更新された記事だけを取り出すことができるようになりました。

なお、Feedzirra::Parser::RSS以外にもFeedzirra::Parser::Atomなどのパーサーがあるのですが、違うフォーマットでもいい感じにやってくれるから大丈夫!という旨がこちらのコメントで確認できます。

rss - Ruby - Feedzirra and updates - Stack Overflow

ソース読めばわかるんでしょうがまだ読んでいないという。今度読みます。

個別ページのURLからRSSフィードURLを取得するfeedbag

自分用のメモも兼ねて今は亡きRSSリーダーを作ったときのノウハウを少しずつときどき書いていこうと思います。

RSSリーダーによくある機能として現在開いているページのRSSフィードを購読するというフローがあるんですが、それと同じ機能を実現しようとしたときに使用したruby gemを紹介します。

damog/feedbag

このfeedbagは渡されたURLからフィードのURLをいろいろ頑張って抽出し、URLのArrayを返すというシンプルなライブラリです。

$ gem install feedbag

でインストールでき、

Feedbag.find("URL")

とすれば使用できます。

ためしにこのブログのトップページのURLを渡してみます。

$ pry
[1] pry(main)> require 'feedbag'
=> true
[2] pry(main)>
[2] pry(main)> Feedbag.find "http://xoyip.hatenablog.com/"
=> ["http://xoyip.hatenablog.com/feed",
 "http://xoyip.hatenablog.com/rss",
 "http://developer.hatena.ne.jp/ja/documents/bookmark/apis/atom"]

このように3つのURLを得ることができました。1つ目と2つ目が使えそうです。3つ目は関係ないURLですが、フィードだと判定されてしまったようです。

次にこのブログの個別ページのURLを渡してみます。

[3] pry(main)> Feedbag.find "http://xoyip.hatenablog.com/entry/2014/01/02/204306"
=> ["http://xoyip.hatenablog.com/feed", "http://xoyip.hatenablog.com/rss"]

今度は2つになりましたが、トップページを渡したときに得られたうちの2つと一致しています。どちらも使えそう。

このライブラリの実装を見てみるとHTMLタグを解析していろいろ頑張ってくれているみたい。

体感的にはフィードを持つ全てのサイトで動く感じがしたので、このライブラリに任せれば良いと思います。