MySQL-MHAを試してみたよ。

新しく購入したDELLのサーバがネットワークエラーで不通になっちゃう事象が半年に1回くらい起きてて、それが運悪くマスタDBだったので、そろそろ本気で対策をしないといけないかしらんと思ってMySQL-MHAについて調べてみました。

MySQL-MHAについてはpublic keyの記事を参考に。

作業に入る前に本家のプロジェクトと、試した方々のブログを拝見しました。
・本家
http://code.google.com/p/mysql-master-ha/
・試した方々のブログ
http://myhome.munetika.mydns.jp/ossdbwiki/index.php/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8
http://6pongi.wordpress.com/2011/11/14/mysql-mha/
http://d.hatena.ne.jp/ke-16/20110912/1315824419
http://d.hatena.ne.jp/kaze-kaoru/20110830/1314677120

先ず最初に疑問に思ったのが、通常WEBアプリケーションというのはアプリケーションサーバとDBサーバが物理的に分かれていると思うんですが、スレーブがマスターに昇格した場合、アプリケーション側からどう見えるのか?ということでしたが、それについては本家にも書いてありますが、こちらがわかりやすくQAにまとめてくれていました。
簡単に言うと、MHAはマスタ昇格のタイミングで任意のスクリプトを走らせることができるので、IP付け替え(仮想NICだったりコールドスタンバイのNIC起動だったり)はそのタイミングでできるそうです。

次に疑問だったのが、フェイルオーバー後に旧マスタを復旧させた場合、旧マスタの扱いはどうなるのかということでした。

これについてあまり言及してるブログがなかったので、せっかくの機会なので自分で試してみることにしました。
ローカルのPC2台にCentOSを入れ、1台がマスタノード兼マネージャ、2台めをスレーブとしました。
MySQLのバージョンはマスタが若干古くて5.5.11、スレーブが5.5.25aでした。MySQLは1つ上のメジャーバージョンまでレプリケーションの互換性をみてくれるので問題無いと判断しました。

インストール方法などはこちらを参考に。特につまづくところもなくセットアップまで完了しました。

Checking slave recovery environment settings..
Opening /var/lib/mysql/relay-log.info ... ok.
Relay log found at /var/lib/mysql, up to localhost-relay-bin.000002
Temporary relay log file is /var/lib/mysql/localhost-relay-bin.000002
Testing mysql connection and privileges.. done.
Testing mysqlbinlog output.. done.
Cleaning up test file(s).. done.
Thu Nov 29 17:05:22 2012 - [info] Slaves settings check done.
Thu Nov 29 17:05:22 2012 - [info]

マスタ側のMySQLを落とす。

# /etc/rc.d/init.d/mysql stop

マネージャーがマスタの異常を検出

Thu Nov 29 17:07:47 2012 - [info] HealthCheck: SSH to 192.168.0.187 is reachable.
Thu Nov 29 17:07:49 2012 - [warning] Got error on MySQL connect: 2003 (Can't connect to MySQL server on '192.168.0.187' (111))
Thu Nov 29 17:07:49 2012 - [warning] Connection failed 1 time(s)..
Thu Nov 29 17:07:52 2012 - [warning] Got error on MySQL connect: 2003 (Can't connect to MySQL server on '192.168.0.187' (111))
Thu Nov 29 17:07:52 2012 - [warning] Connection failed 2 time(s)..
Thu Nov 29 17:07:55 2012 - [warning] Got error on MySQL connect: 2003 (Can't connect to MySQL server on '192.168.0.187' (111))
Thu Nov 29 17:07:55 2012 - [warning] Connection failed 3 time(s)..
Thu Nov 29 17:07:55 2012 - [warning] Master is not reachable from health checker!

落ちたかどうかの判定は設定ファイルで変更できるようです。

ヘルスチェックを行った後、設定ファイルにしたがってフェイルオーバーするよ!というログが出てフェイルオーバーが始まります。

Thu Nov 29 17:07:55 2012 - [warning] Master is not reachable from health checker!
Thu Nov 29 17:07:55 2012 - [warning] Master 192.168.0.187(192.168.0.187:3306) is not reachable!
Thu Nov 29 17:07:55 2012 - [warning] SSH is reachable.
Thu Nov 29 17:07:55 2012 - [info] Connecting to a master server failed. Reading configuration file /etc/masterha_default.cnf and /etc/app1.cnf again, and trying to connect to all servers to check server status..
Thu Nov 29 17:07:55 2012 - [warning] Global configuration file /etc/masterha_default.cnf not found. Skipping.
Thu Nov 29 17:07:55 2012 - [info] Reading application default configurations from /etc/app1.cnf..
Thu Nov 29 17:07:55 2012 - [info] Reading server configurations from /etc/app1.cnf..
Thu Nov 29 17:07:56 2012 - [info] Dead Servers:
Thu Nov 29 17:07:56 2012 - [info] 192.168.0.187(192.168.0.187:3306)
Thu Nov 29 17:07:56 2012 - [info] Alive Servers:
Thu Nov 29 17:07:56 2012 - [info] 192.168.0.232(192.168.0.232:3306)
Thu Nov 29 17:07:56 2012 - [info] Alive Slaves:
Thu Nov 29 17:07:56 2012 - [info] 192.168.0.232(192.168.0.232:3306) Version=5.5.25a-log (oldest major version between slaves) log-bin:enabled
Thu Nov 29 17:07:56 2012 - [info] Replicating from 192.168.0.187(192.168.0.187:3306)
Thu Nov 29 17:07:56 2012 - [info] Checking slave configurations..
Thu Nov 29 17:07:56 2012 - [info] read_only=1 is not set on slave 192.168.0.232(192.168.0.232:3306).
Thu Nov 29 17:07:56 2012 - [warning] relay_log_purge=0 is not set on slave 192.168.0.232(192.168.0.232:3306).
Thu Nov 29 17:07:56 2012 - [info] Checking replication filtering settings..
Thu Nov 29 17:07:56 2012 - [info] Replication filtering check ok.
Thu Nov 29 17:07:56 2012 - [info] Master is down!
Thu Nov 29 17:07:56 2012 - [info] Terminating monitoring script.
Thu Nov 29 17:07:56 2012 - [info] Got exit code 20 (Master dead).
Thu Nov 29 17:07:56 2012 - [warning] Global configuration file /etc/masterha_default.cnf not found. Skipping.
Thu Nov 29 17:07:56 2012 - [info] Reading application default configurations from /etc/app1.cnf..
Thu Nov 29 17:07:56 2012 - [info] Reading server configurations from /etc/app1.cnf..
Thu Nov 29 17:07:56 2012 - [info] MHA::MasterFailover version 0.53.
Thu Nov 29 17:07:56 2012 - [info] Starting master failover.

ぐりぐりと進んでいきます。

Thu Nov 29 17:07:56 2012 - [info] * Phase 1: Configuration Check Phase..
Thu Nov 29 17:07:56 2012 - [info]
[中略]
Thu Nov 29 17:07:56 2012 - [info] ** Phase 1: Configuration Check Phase completed.
Thu Nov 29 17:07:56 2012 - [info]
[中略]
Thu Nov 29 17:07:56 2012 - [info] * Phase 2: Dead Master Shutdown Phase..
Thu Nov 29 17:07:56 2012 - [info]
[中略]
Thu Nov 29 17:07:56 2012 - [info] * Phase 2: Dead Master Shutdown Phase completed.
Thu Nov 29 17:07:56 2012 - [info]
[中略]
Thu Nov 29 17:07:56 2012 - [info] * Phase 3: Master Recovery Phase..
Thu Nov 29 17:07:56 2012 - [info]
[中略]
Thu Nov 29 17:07:56 2012 - [info] * Phase 3.1: Getting Latest Slaves Phase..
Thu Nov 29 17:07:56 2012 - [info]
[中略]
Thu Nov 29 17:07:56 2012 - [info] * Phase 3.2: Saving Dead Master's Binlog Phase..
Thu Nov 29 17:07:56 2012 - [info]
[後略]

終わった!

----- Failover Report -----

app1: MySQL Master failover 192.168.0.187 to 192.168.0.232 succeeded

Master 192.168.0.187 is down!

Check MHA Manager logs at hogehoge for details.

Started automated(non-interactive) failover.
The latest slave 192.168.0.232(192.168.0.232:3306) has all relay logs for recovery.
Selected 192.168.0.232 as a new master.
192.168.0.232: OK: Applying all logs succeeded.
Generating relay diff files from the latest slave succeeded.
192.168.0.232: Resetting slave info succeeded.

全ての処理をやり終えるとマネージャーは停止します。。

スレーブが昇格したのを確認して、マスタのMySQLを再度起動します。

/etc/rc.d/init.d/mysql start

起動しましたが自動的にスレーブになったりするわけではありませんでした。
やってみて思ったんですが、異常で死んだスレーブが勝手にフェイルオーバーしてマスタに戻るとデータの欠損などのトラブルの元になりますからね。。。

とはいえ、調べてみて特に異常ないことがわかった旧マスタをまたスレーブとして登録するためにはデータのリストアが必要で、データ量が多ければ多いほどこの作業は時間がかかってしまいます。
そういうところも見越してか、マネージャーの実行結果には昇格時のバイナリログのポジション情報が出力されています。

Thu Nov 29 17:07:58 2012 - [info] All relay logs were successfully applied.
Thu Nov 29 17:07:58 2012 - [info] Getting new master's binlog name and position..
Thu Nov 29 17:07:58 2012 - [info] localhost-bin.000003:787
Thu Nov 29 17:07:58 2012 - [info] All other slaves should start replication from here. Statement should be: CHANGE MASTER TO MASTER_HOST='192.168.0.232', MASTER_PORT=3306, MASTER_LOG_FILE='localhost-bin.000003', MASTER_LOG_POS=787, MASTER_USER='repl', MASTER_PASSWORD='xxx';
Thu Nov 29 17:07:58 2012 - [warning] master_ip_failover_script is not set. Skipping taking over new master ip address.

データを確認して問題無いと判断したら、旧マスタにchange masterしてスレーブとして加えることもできます。すごいなあ。

ちなみに起動中にマネージャーを強制停止し、再実行しても問題ありませんでした。どうやらマネージャーは起動した時点からのログの整合性を取っているようです。
なので仮にマネージャーが落ちても他のサーバを代用できそうです(あくまで推測なので実際に作業される方は十分にテストしてくだい)。

ざっと使ってみた感じですが、MySQLサーバ側にほとんど影響を与えないよう作られているように感じました。
マネージャーは、どのタイミングで登録してもサーバに影響しないので、稼働中のサービスにも導入できそうでした。
HA製品というと導入や運用が難しく、専用のサポートが必要なイメージがありますが、MySQL-MHAはシンプルに極力ダウンタイムを発生させずフェイルオーバーを実現させることに重点を置いている感じがし、とても魅力的なOSSですので、本番環境にも導入を進めたいと思います。

RailsでAPNSするときの注意点

ライブラリはこちらを使わせてもらう

jpoz / APNS

railsからapnsを使うためにgemfileに追記する


gem "apns"

bundle install

Apple Developer Centerからダウンロードした開発用.p12証明書を鍵とセットの証明書に変換

openssl pkcs12 -in 証明書.p12 -out apns.pem -nodes

証明書をRailsのプロジェクトディレクトリににアップロード

あとはアップロードしたファイルを指定して、指定のデバイスIDに送ればOK!

APNS.pem = Rails.root.join("cert","apns.pem")
APNS.send_notification(アプリから登録したデバイスID, "Hello iPhone!")


なはずだったのだが・・・・

なぜか英語の通知はできるのに日本語だと通知が行かない。



とりあえずtcpdump port 2195すると、正常に送れる時と送れない時で微妙に応答がちがう。。。

半日ぐらい悩んだ挙句、こちらにたどり着いた

APNS.pem = Rails.root.join("cert","apns.pem")
alert = String.new("はろーあいふぉん")
alert.force_encoding("ascii-8bit") if RUBY_VERSION.to_f >= 1.9
APNS.send_notification(アプリから登録したデバイスID, alert)

これで日本語もちゃんとpushできました。


参考にさせてもらったページ
https://github.com/jpoz/APNS
http://d.hatena.ne.jp/arcright/20120620/1340428736
http://atotok.net/note/rails/?incompatible+character+encodings%3A+ASCII-8BIT+and+U

テーブルの内容をテキストに書き出す方法

MySQLでテーブルのデータをテキストにエクスポートする場合、ターミナルからコマンドラインでやる方法とCUIから出力するのと2種類方法があります。

ターミナルからやる

# echo "select * from fuga" | mysql hogehoge > abc.csv

MySQL CUIからやる

select * from `fuga` into outfile "/tmp/abc.csv" fields terminated by ',';

ターミナルからやるとTSV形式になるけどカラムヘッダが出力されるのでデータを渡したりエクセルとかに取り込んで何か加工する時に何かと都合がいいけど、テーブルサイズによっては落ちちゃう。
CUIだと区切り文字が選べるけどヘッダが吐き出せないので、後でどのカラムが何のデータだったかわかんなくなる可能性がある、でもサイズが大きくてもちゃんと書きだしてくれる。

どっちも一長一短があるけど、とりあえずCUIでヘッダを吐き出す方法を考えた。

select id1,id2,id3,... from (select "id1","id2","id3",... union select id1,id2,id3 from fuga) as a into outfile "/tmp/abc.csv" fields terminated by ',';

何か他にいい方法ないですかねぇ。

追記
最後のやつだと一回一時領域にデータをコピーするのですごく遅い

Rails3.2+Backbone.jsでモデルの永続化ができない場合

Rails3.2+Backbone.jsの組み合わせで

this.model.save({hogehoge:...})

とやっても何故か永続化できない。

webrickのログを見ると

Started OPTIONS "/test" for 192.168.26.120 at 2012-10-11 18:24:35 +0900

とかなってて、正しくHTTPメソッドがセットされてない。

色々調べた結果、自分の環境ではAJAX通信時に

contentType="application/json"

をセットしてもRailsJSONリクエストとして処理してくれないらしい。

とりあえず

Backbone.emulateJSON = true;

にして、test.json宛てのリクエストに変更して逃げたけど、そもそも何故jsonヘッダを見てくれないんだぜ。

Webviewのlocalstorage

PhoneGap(cordva)のお話です。

よく釣り記事でPhoneGap+Javascriptでアプリを作ろう!なんて話で、不発揮データはlocalstorageに保存しましょうなどと簡単に書かれてますが、AndroidのWebviewだとlocalstorageのデータはRAMに保存されるので、RAMフラッシュのタイミングで消去されます。
https://groups.google.com/forum/?fromgroups=#!topic/phonegap/joymOyDsMlE

If I recall correctly, Android (at least in 2.X) localStorage was stored in available RAM. If this is the case, it might make sense that it would be wiped out on restart of device.

Android4.xでも試したけどリセットで消えちゃったので、ここらへんの挙動は変わってないっぽい。デフォルトブラウザなら保存されるんだけどなあ。
iOSはちゃんと保存されてた。

jQuery Mobileの初期化イベントが微妙

jQuery Mobileではpage="ID"の要素がページとして認識されます。

ページなのでjQuery Mobile APIが使えます。

jQuery Mobile APIとは初期化メソッドとかchangePageとかのアレです。

この初期化イベント、使ってみてわかったんですが結構厄介でした。

・たとえば一番最初に読み込まれるページ(index.htmlの一番上のpage="ID"のセクション)だとそもそも初期化イベントが呼ばれない。
起点になるページが#topだとすると、$("top").on("showpage",f(){})や$("top").on("createpage",f(){})が発火しない(1.1で確認)。
※"showpage"が評価されるのは再度#topを表示した時から。

・"pagebeforeshow"イベントでAJAX通信して準備して、"pageshow"で表示イベントを設定したとします。
$.mobile.changePage(#hoge)すると"pageshow"は評価されるけど"pagebeforeshow"は評価されないので、動的コンテンツが反映されない、もしくは最悪の場合前回表示された結果が再度表示される。

・"pageshow"とか一連のページ系初期化処理、dialogの表示非表示でも評価される。
 ダイアログを閉じるとそのページの"pageshow"が再度実行されてアッーとなる。

とまあ、ちょっといじっただけでもこれだけのトラップが待ってます。
jQuery Mobileだとテンプレート用意しなくていいのは魅力だけど、ちょっと盛りすぎてて使いどころを考えないと結構苦労する。
あとpage()の挙動が微妙。

ちなみにjQuery Mobile + Phonegapだとクリックの反応が鈍くなります。iOSだと大丈夫なのにな。
しかもjQuery Mobileの特徴の一つであるtransitionのほとんどがアンドロイドブラウザで動かない。
よく聞くjQuery MObile+Phonegapでアプリ作ろう!っていうのはちょっとちゃんと考えないと後々大変なことになるよ(自戒)