mysqlslapコマンドの使いかた事例
1年ほど前にRSSリーダーの開発をしていたとき、環境ごとのMySQLのパフォーマンスを調べるのにmysqlslap
というコマンドを使っていたが、割と適当に使っていたので今こそそれを振り返ってみようと思う。
そもそもmysqlslapが何かと言うと、MySQLの公式ドキュメントにはこのように書かれている。
MySQL :: MySQL 5.1 リファレンスマニュアル :: 7.15 mysqlslap — クライアント負荷エミュレーション
mysqlslapはMySQLサーバのクライアント負荷をエミュレートし、各ステージのタイミングを報告する診断プログラムです。サーバにたいして複数のクライアントがアクセスしているかのように作動します。mysqlslapはMySQL 5.1.4.から提供されています。
なるほどなるほど、複数クライアントから接続されている状況をエミューレートできるんだね。
で、僕が検証に使っていたコマンドの一部がこんなのだった↓。ネットワークが影響しない状況で確認したかったからSSHで入ってlocalhostのMySQLに接続してテストした。
$ 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さんが出た回でGitHubのAtomエディタの話に絡めて、ちょっとずつ乗り換えたってのを聞いて僕も気合を入れてSublimeText3に乗り換えました。できるだけこれまで使っていたEmacsと同じようにキーアサインして何とか使っています。
正直Emacsに戻りたくなることのほうが多いのですが、プラグイン周りの拡張機能の作りやすさを考えるとelispから離れたほうがいいだろうなというのはあります。AtomはCoffeeScriptで拡張できるそうなので本当はそちらがいいと思ったんですが、パフォーマンスを理由に今はSublimeでなんとかしています。
ガジェットネタ
このあたり。
- Rebuild: 20: iPhone 5s (Kenn Ejima, Hakuro Matsuda)
- Rebuild: 23: iPad, Mavericks and Macbook Pro (Kenn Ejima, Hakuro Matsuda)
- Rebuild: 38: As Social As Second Life (hak, honmax)
主に@hakさんが出ている回。iOSやMac、Oculusなどのコア〜〜な話を聞けて面白い。Rebuild聞いてなかったらm7のことなんて全然知らなかったかもしれません。チップの話なんて考えたことなかった。名言「チップマニアなら買い」。
Infra as Code, Immutable Infra
- Rebuild: 14: DevOps with Docker, chef and serverspec (naoya, mizzy)
- Rebuild: 25: Immutable Infrastructure (Naoya Ito, Gosuke Miyashita)
ここ1年ぐらいのHottestなトピックですかね。Webは全然やっていなかったのですがこの話題のおかげで少しは興味を持つようになったし、前述のRSSリーダーをホストするときにChef Soloを使うきっかけになりました。てか、それまではサーバーの設定を自動化するなんて考えたこともなかったので。。。今使っているさくらVPSのサーバーもChefで書き直したいぐらいです。
先日の大江戸Ruby会議でもInfratasterのトークのときにここらへんのネタが登場しましたが、Rebuild聞いていたおかげでわかりました。まさに進研ゼミ。
仕事ではほとんど使わないInfra as Codeとか出てくると #rebuildfm できいたやつだ!って感じになる。 #oedo04
— 大野浩誠 (@xoyip) April 19, 2014
RubyのOPMLパーサー
結論
これ使えば一発です。
あとは雑談
2013年6月末のGoogleリーダーのシャットダウンの際、OPMLというフォーマットを初めて知りました。なんのことはないただのXMLなんですが、一応アウトラインプロセッサーのためのMLということでOPMLという名前になっているらしいです。
RSSフィードの購読データのエクスポートやインポートに使われているのしか見たことがありません。GoogleリーダーやFeedlyでも購読しているフィードをOPMLファイルで出力することができるようになっています。
昨年僕が開発していたRSSリーダーでもOPMLのインポート機能を実装していました。これを実装するにあたりやるべきステップは3つありました。
- OPMLをパースする
- フィードのフォルダを作る
- フィードを購読する
このうち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リーダーは登録フィードをバックグラウンドで更新し記事を追加します。RSSリーダーのユーザー側、システム側から見てみます。
ユーザー視点
- RSSリーダーにブログやニュースサイトのフィードを購読
- これまでの記事を閲覧できるようになる
- サイトが更新されたら新しい記事を閲覧可能になる
RSSリーダー視点
- ユーザーがRSSフィードを購読する
- すでに誰かが購読済みのフィードかどうかを調べる
- 誰かが購読済み→フィードの購読者にこのユーザーを加える
- 最初の購読者→フィードデータを作成し記事をクローリングする、また購読者にこのユーザーを加える
- 取得済みの記事をユーザーに見せる
- フィード配信元に更新があるかどうか定期的にクローリング
- 更新があれば新たな記事を追加する
- (更新後に見たユーザーは新しい記事を読めるようになっている)
定期的なクローリング
両者を比べてみるたときに一番違うのは、強調表示した「フィード配信元に更新があるかどうか定期的にクローリング」というところです。もっと言い換えれば、更新があるかどうか見に行くという行為をユーザーの代わりに行ってくれるのがRSSリーダーの価値であると言えます。
そして、システムからすればもっとも負荷の高い部分のうちの一つとも言えます(もう一つはクロールした記事のデータ量。日に日に増えていくので、、、)。
ではこのクローリングをどのように実施したかを紹介していきます。
Rails & Resque
クローリングはユーザーの操作がないときでも定期的に動かす必要があります。したがってアプリケーションサーバーやWebサーバーは関係ありません。もっとも簡単に実行するには、定期的にスクリプトが動いてデータベースを更新するようなものでもできてしまいます。
とはいえ、単なるジョブ用スクリプトを使うとアプリケーションと同じモデルを利用するのが困難でスクリプト自体が大変複雑になることは間違いないので、もうちょっとマシな方法を使います。
RSSリーダーはRuby on Railsで作り、バックグラウンドジョブにはResqueを使用しました。
- Ruby on Rails
- Resque
Resqueに関してはiQONを運営するVASILYさんの記事が詳しかったのでこちらを参考にいろいろ実装しました。超助かりました。
Resqueのバックグラウンドジョブ用のワーカーは定期的にジョブキューを監視しつづけてくれます。暇なワーカーがいる限り、ジョブキューにジョブを放り投げるとすぐにジョブを実行してくれるようになっています。つまりワーカーを多数用意しておけば、がんがんジョブを回すことができますね。
RSSフィードの定期チェックのためには「あるRSSフィードを見にいって、更新があれば記事を追加する」というジョブを全ての記事分ジョブキューに登録すればよい、ということになります。
定期的にジョブを発行する
ジョブを処理する側はResqueを使って実装することができました。ではジョブはどのように発行すればいいでしょうか?
これにはcron
のようにある時間毎に起動するプログラムを用いることにしました。
この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
RailsでRSSリーダーを作ったときに得たノウハウ第2弾、RSSフィードのパースのところを書いてみようと思います。
第1弾としてこんなん書きました。
個別ページのURLからRSSフィードURLを取得するfeedbag - PILOG
サイトURLを渡すとHTMLを解析してRSSフィードのURL候補を返してくれるというgemの紹介でした。今回は得られたRSSフィードのURLに情報を取りに行ってパースしてくれるgemを紹介してみようかと思います。
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を紹介します。
この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タグを解析していろいろ頑張ってくれているみたい。
体感的にはフィードを持つ全てのサイトで動く感じがしたので、このライブラリに任せれば良いと思います。