主夫ときどきプログラマ

データベース、Webエンジニアリング、コミュニティ、etc

YUITest を使ってJavascriptの単体テストを自動化するまで (後編)

前編 YUITest を使ってJavascriptの単体テストを自動化するまで (前編) - masayuki14’s diary


Git Repository への登録

これまでに使ったファイルをリポジトリに登録しよう。今回はGitをバージョン管理に利用する。実際のプロジェクトでテストを自動化して継続させるのにバージョン管理は必須要素だ。

$ git init
$ git add .
$ git commit -m ‘First commit’

これでGit Repository への登録は完了。

JenkinsのInstall

テストを自動実行するには何らかのツールを使う必要がある。そこで登場するのがJenkinsだ。とにかくインストールして動かしてみよう。大事なのは「射撃しつつ前進」なのだから。

$ brew install jenkins

インストールが終わったらJenkinsを起動してブラウザでアクセスしよう。Jenkinsはデフォルトでポート8080で起動する。

$ java -jar  -Dfile.encoding=utf-8 /usr/local/opt/jenkins/libexec/jenkins.war
# 文字コードをUTF8にするオプションを指定している

ブラウザを開いて http://localhost:8080/ にアクセスするとJenkinsが表示されるはずだ。
task.shを実行するようなジョブを登録すれば、自動的にJenkinsが継続して実行してくれる。
ジョブを登録していこう。

ジョブの登録

メニューから「新規ジョブ作成」を選んで必要な情報を入力しよう。
ジョブ名は好きに登録してOK。今回は「Run YUI Test」とし「フリースタイル・プロジェクトのビルド」を選んで「OK」だ。
次に各種設定を行っていく。

ソースコード管理

「Git」選び Repository URL を入力する。
今回はローカルのファイルシステムリポジトリを用意したので、Repository URL にリポジトリへのパスを入力する。これでGitの設定は完了。

ビルド・トリガ

「SCMをポーリング」にチェックをいれてスケジュールを設定する。定期的にリポジトリを確認し、変更があればビルドをする、という意味だ。スケジュールの書式はcronとほとんど同じ。
15分毎にポーリングさせるため「H/15 * * * * 」と入力する。

ビルド

ビルドの方法を設定する。task.sh の実行がビルドだ。
ビルド手順の追加から「シェルの実行」を選んでシェルスクリプトを記述する。
$ sh task.sh
これで設定は完了。「保存」ボタンで終了しよう。
リポジトリのルートディレクトリでシェルが実行される

これでJenkinsが自動でビルドを実行してくれるようになった。
リポジトリを15分毎に監視して、差分が発生していればビルドを実行してくれる。


テスト結果の収集

これまでの設定だけではJenkinsはビルドの成功・失敗しか結果を教えてくれない。しかしgroverからはテストの結果が出力されている。それをJenkinsに教えてあげれば、Jenkinsで集計をとり表示してくれるようになる。
その方法を見ていこう

テスト結果の出力

テスト結果をJenkinsに取り込ませるにはjUnit形式のXMLファイルに出力しなければならない。
幸いにも grover はこの出力に対応している

$ grover testRunner.html —junit -o result.junit.xml

これで result.junit.xml にテスト結果が出力される。
これをJenkinsに読み込むように設定する。

ビルドの設定

プロジェクトの設定を開き「ビルド後の処理」に設定を追加する。
ビルド後の処理の追加から「JUnitテスト結果の集計」を選び、テスト結果XMLに「result.junit.xml」を入力して保存。これで設定完了だ。


task.sh の grover コマンドを上記のように編集してgit repositoryにコミットしよう

# task.sh を編集
$ vim task.sh
     #!/bin/sh
     java -jar yuitest-coverage.jar -o target-coveraged.js target.js
     grover --coverage testRunner.html --junit -o result.junit.xml

# 変更内容をコミット
$ git add task.sh
$ git commit -m ‘テスト結果を result.juni.xml に出力’

これでJenkinsのビルド実行を待つ。ビルドに成功すればJenkinsがテスト結果を取り込み表示される。


カバレッジ結果の収集

Pluginのインストール

テスト結果と同様にカバレッジをJenkinsに取り込むにはCobertura Pluginプラグインをインストールする。
「Jenkinsの管理」> 「プラグインの管理」と進み「利用可能」タブを選択。フィルターに「Cobertura Plugin」を入力しすれば簡単に見つけることが出来る。
「インストール」にチェックを入れてインストールしよう。

カバレッジの出力

grover はテスト結果と同様にカバレッジ結果をLcov形式のファイルに出力することが出来る。

$ grover --coverage testRunner.html -co coverage.lcov

これでcoverage.lcov にカバレッジが出力される。

カバレッジ結果の変換

coverage.lcov をそのままJenkinsに取り込めると良いが、残念ながらJenkinsはLCOV形式には対応していない。
Coberturaで対応しているXML形式にcoverage.lcovを変換する必要がある。
変換には https://github.com/eriwen/lcov-to-cobertura-xml で公開されているPythonスクリプトを使う。ここから lcov_cobertura.py をダウンロードしよう。

$ python lcov_cobertura.py coverage.lcov -o coverage.xml

これで変換が完了

ビルドの設定

プロジェクトの設定を開き「ビルド後の処理」に設定を追加する。
ビルド後の処理の追加から「Coberturaカバレッジ・レポートの集計」を選び、Cobertura XMLレポート パターンに「result.junit.xml」を入力して保存する。
これで準備は整った。


task.sh を編集してコミットすれば全ての設定が完了。

$ vim task.sh

     #!/bin/sh
     java -jar yuitest-coverage.jar -o target-coveraged.js target.js
     grover --coverage testRunner.html --junit -o result.junit.xml -co coverage.lcov
     python lcov_cobertura.py coverage.lcov -o coverage.xml

$ git add task.sh lcov_cobertura.py
$ git commit -m ‘Coverageをcoverage.xmlに出力'

CIを回す

あとはJenkinsが自動でテストを実行し結果を集計してくれる。CIを回していこう!
おしまい。

YUITest を使ってJavascriptの単体テストを自動化するまで (前編)

Javascriptのテストフレームワークにはいくつも種類があるが、そのなかでYUIのTestライブラリの使い方を紹介する。YUITestでテストを書くことで、それの実行から自動化までを用意に実現することができる。その一連の方法を紹介する。

テストを書く

まず大事なのはテストを書くこと。今回はサンプルとしてとてもシンプルな関数を用意する。シンプルすぎる。


target.js

/**
 * 引数を足しあわせて結果を返す
 *
 * @param {Number} operand1
 * @param {Number} operand2
 * @return {Number} sum of args
 */
var sum = function(op1, op2) {
     return op1 + op2;
}


この関数をテストしていこう。テストファーストという言葉もあるが、それは空の彼方へ置いておけばいい。いまのところは。
テストはこんなかんじになるだろう。


test.js

YUI({ loadInclude: {TestRunner: true}}).use('test', function (Y) {

     var testCase = new Y.Test.Case({

          name : 'sum() のテスト',

          setUp : function() {
          },

          tearDown : function() {
          },

          'test normal' : function() {
               Y.Assert.areSame(10, sum(2, 8), '合計は10');
          },

          testNull : function() {
               Y.Assert.isNull(sum(null, 1), '不正な引数の場合はNullが返る');
          }
     });

     // load TestCase
     Y.Test.Runner.add(testCase);
});

いきなりよくわからないキーワードが出てきてしまった。YUI? なんだそれは。まだそれはわからなくていい。大事なのは

  • testXXX という名前のメソッドを用意すれば、それがテストとして実行されること
  • ’空白を含む 名前' というメソッドもテストとして実行されること
  • Y.Assert.XXXX というAssertionsがあること
  • setUp, tearDown はよくあるはなし

だろう。

テストを実行する

さぁ、さっそくテストを実行しよう。実行するためのHTMLを用意してブラウザで読みこめばテストが実行される。

testRunner.html

<html>
    <head>
         <meta http-equiv="content-type" content="text/html; charset=utf-8">
    </head>
    <body>
        <div id='log' class='yui3-skin-sam'></div>
        <script src="http://yui.yahooapis.com/3.16.0/build/yui/yui-min.js"></script>
        <script type='text/javascript'>

            // コンソール表示の設定を先にロードする
            YUI({ loadInclude: {TestRunner: true}}).use('test', 'test-console', function (Y) {
                // out puts Console
                (new Y.Test.Console({
                    newestOnTop : false,
                    filters : { pass : true, fail : true, info : true, status : true }
                })).render('#log');

                Y.Test.Runner.setName('単体テスト');
            });
        </script>

        <!-- テスト対象をロード -->
        <script src="target.js"></script>

        <!-- テストをロード -->
        <script src="test.js"></script>

        <script type='text/javascript'>
            YUI({ loadInclude: {TestRunner: true}}).use('test', 'test-console', function (Y) {
                // テスト実行
                Y.Test.Runner.run();
            });
        </script>
    </body>
</html>

YUIのライブラリをサイトから直接ロードして実行する。テスト結果を表示するコンソールは<div id=‘log’></div>に出力されるので必ず用意しよう。コンソール、テスト対象、テスト、の順にロードして最後にテストを実行だ。これをブラウザでロードしよう。


Very Good!!

見事に結果が表示されている。たった3行のコードにだってエラーがあるんだから、君が書いているコードにだってかならずエラーが潜んでいる。テストをどんどん書いてバグをどんどん潰していくんだ!まず手始めに testNull のエラーを取り除いてみよう。


より詳しい使い方は下記を参照しよう。
YUITest
http://yuilibrary.com/yui/docs/test/
コンソール
http://yuilibrary.com/yui/docs/test-console/
Assertion
http://yuilibrary.com/yui/docs/api/ Assertを検索

コマンドラインからのテスト実行

ブラウザからテストを実行することはできたが、これは少々面倒な作業だ。なにより自動化することがとても困難だ。テストを迅速に実行しそれを自動化するにはコマンドラインからテストを実行する必要があるのだ。

PhantomJS + grover


PhantomJSはコマンドから利用できるブラウザで、これを使うことでテストを実行することができる。合わせて grover というYUITest専用のPhantomJSラッパーツールを組み合わせることで簡単にコマンドラインからテストを実行出来るようなる。
それぞれは簡単にインストールすることができる

$ brew install phantomjs
$ npm install -g grover


OSがMacじゃない君は自力でインストール方法を調べてくれ。もしくはここで立ち去っても構わない。Macだけど実行できない君はHomebrew をインストールして出直して来い。
さぁPhantomJSとgroverのインストールが終わったらテストを実行しよう

$ grover testRunner.html



Fantastic!!

これでいつでも迅速にテストを実行出来るようになった。


カバレッジの測定

次はカバレッジを測定しよう。カバレッジを測定するにはテスト対象のtarget.jsを変換する必要がある。変換にはYUITestで提供されているjarを使う。https://github.com/yui/yuitest ここから java/build/yuitest-coverage.jar をダウンロードして早速変換だ。

$ java -jar yuitest-coverage.jar -o target-coveraged.js target.js


testRunner.html のテスト対象を target-coveraged.js に変えて実行すればカバレッジを測定できる。テストにエラーがあるとカバレッジを測定できないから先のエラーは直しておこう。

$ grover ―coverage testRunner.html


Terrific!!

いちいちファイルを変換するのは面倒だから一連の処理を1つのタスクとしてまとめてしまおう。


task.sh

#!/bin/sh
java -jar yuitest-coverage.jar -o target-coveraged.js target.js
grover --coverage testRunner.html


さぁこれで準備は整った。あとはこれを自動化していこう。



後編へ続く YUITest を使ってJavascriptの単体テストを自動化するまで (後編) - masayuki14 note

LinuxサーバーにS3をマウントして利用する方法 (FUSE + s3fs)

アプリケーションのログや大容量のファイルを扱う場合に
S3のBucketをサーバーにマウントして利用する方法です。
s3fsを使用してマウントしますが、FUSEをベースに作られているためこれらをインストールします。

1.事前準備

FUSEに必要なライブラリをインストールします。

$ yum install gcc
$ yum install gcc-c++
$ yum install libstdc++-devel
$ yum install pkgconfig
$ yum install make
$ yum install curl-devel
$ yum install libxml2-devel

2.FUSEのインストール

http://sourceforge.net/projects/fuse/files/fuse-2.X/
こちらからソースをダウンロードしてコンパイルします。
CentOS5で利用する場合はmountコマンドでno-canonicalizeオプションがないので、FUSE2.8.5を利用する必要があります。
CentOS6の場合は最新版を利用するとよいでしょう。現時点で2.9.2が最新です。

$ cd /usr/local/src
$ wget http://sourceforge.net/projects/fuse/files/fuse-2.X/2.9.2/fuse-2.9.2.tar.gz/download
$ tar xvf fuse-2.9.2.tar.gz
$ cd fuse-2.9.2
$ ./configure
$ make
$ make install

3.FUSEの設定

インストールしたFUSEの関連モジュールを読み込むための設定をします。

$ echo /usr/local/lib >> /etc/ld.so.conf
$ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
$ ldconfig

/etc/profile.d fuse.sh export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig

4.s3fsのインストール

https://code.google.com/p/s3fs/downloads/list
ここからソースをダウンロードしてコンパイルします。現時点で1.61が最新です

$ cd /usr/local/src
$ wget https://s3fs.googlecode.com/files/s3fs-1.61.tar.gz
$ tar xvf s3fs-1.61.tar.gz
$ cd s3fs-1.61
$ ./configure
$ make
$ make install

5.s3fsの設定

S3へのアクセスキーを記述したファイルを作成します。アクセスキーIDとシークレットアクセスキーをコロン(:)でつなぎます。

$ echo AccessKeyID:SecretAccessKey > /etc/passwd-s3fs
$ chmod 600 passwd-s3fs

パーミッションを正しく設定しないと
 s3fs: credentials file /etc/passwd-s3fs should not have others permissions
というエラーになります。

6.マウント

今回は various-logs というS3のバケットを /s3 というディレクトリにマウントします。

$ cd /
$ mkdir /s3
$ modprobe fuse
$ s3fs various-logs /3 -o default_acl=public-read -o allow_other

/bin/mount: unrecognized option `--no-canonicalize'
のエラーが出る場合はFUSE2.8.5を使用してください。
すでに別バージョンをインストールしている場合はFUSEのディレクトリに移動して
$ make uninstall
でアンインストールできます。

7.再起動時の設定

サーバー再起動時にfstabに記録します。

$ vim /etc/fstab
s3fs s3-bucket-name /s3 fuse allow_other,default_acl=public-read 0 0

Mysqlでログ系テーブルを運用するときやっておきたいこと

SNSソーシャルゲーム、アドネットワークなどのシステムではいろいろなログ情報をDBに保存することもあると思います。
そのさい、日々増えつづけるデータやパフォーマンスをどの様にさばいていくかが重要になってきます。
今回はログ系のデータをMysqlでどのように運用していくか、をテーマにいくつかのノウハウをまとめました。

ログ系テーブルの特徴

ログ系のデータとは、つまり何かのアクションの履歴データのことです。
一般的にはこのような形になるかと思います。

CREATE TABLE `t_logs` (
  `id` bigint(20) unsigned NOT NULL,
  `user_id` int(10) unsigned NOT NULL DEFAULT '0',
  `event_id` int(10) unsigned NOT NULL DEFAULT '0',
  `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8


基本的にはどんどんInsertが発生していきます。そして要求に応じてそのデータを参照します。
時系列でデータは増えていきますから、運用が長くなればそれに比例してログの量も増えていきます。


1レコード単位で参照が必要なのであれば過去の参照に期限を設けるとか、
集約したデータが参照したいのであれば集計結果を別テーブルに保存しておくなど
どこかでデータを切り捨てる事を考えておく必要があります。

データサイズに注意する

上記のようにどこかでデータ捨てるということを決めたとしましょう。
しかしInsertされたログデータはどこかで削除しないとずっとテーブルに残り続けます。
アクセスログ系のデータの場合人気のソーシャルゲームやアドネットワークだと1日に数千万〜のPVが発生するので
あっという間に億単位のデータサイズになってしまいます。こうなると物理的な容量が問題になってきます。


こういった場合よく用いる方法が、深夜のバッチプログラムなどでDeleteする、ということなのですがこの規模になってくると、
Deleteは非常に重くなります。また、その間ログテーブルもロックされてしまいますのでこちらも注意が必要です。
Limitを使い、100件、1000件ずつ削除する方法もありますが、いずれInsertの方がスピードがうわまってきます。

innodb_file_per_tableでファイル分割

InnoDBでテーブルを運用する場合、そのデータはibdata1というバイナリファイルに蓄積されていき、
Mysqlで使われるすべてのInnoDBで共有されています。
このibdata1ファイルはデータが蓄積されるごとに自動拡張を続け、ひとたび成長してしまったデータファイルを小さくする方法はありません。
そのため深夜バッチなどでDeleteしてもディスクサイズはへりません。


innodb_file_per_tableオプションを設定すると、テーブルごとにデータファイルが作成されるようになります。
my.cnfファイルに設定します。

[mysqld]
innodb_data_file_path=ibdata1:1G
innodb_file_per_table


作成されるデータファイルは各スキーマのフォルダに格納され .ibd という拡張子になります。
こちらのファイルも要求に応じて自動拡張されますが、テーブルを削除するとファイルも削除されます。
そのためテーブルが大きくなってからでもディスクの空き容量を増やすことが可能になります。

パーティションを使う

Mysql5.1以降で実装された機能で、1つのテーブルを「ある規則」に基づいて分割し別テーブルのようにデータを格納させる仕組みです。
「ある規則」は4つに分類されますが、ログのデータ削除という意味でよく用いられるのが「RANGEパーティション」です。
前述のinnodb_file_per_tableオプションのもとで行うと、パーティション毎にもデータファイルが分割されます。
テーブルは以下のようになります。

CREATE TABLE `t_logs` (
  `id` bigint(20) unsigned NOT NULL,
  `user_id` int(10) unsigned NOT NULL DEFAULT '0',
  `event_id` int(10) unsigned NOT NULL DEFAULT '0',
  `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`,`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
PARTITION BY RANGE (to_days(created))
(PARTITION p20120708 VALUES LESS THAN (to_days('2012-07-09')),
 PARTITION p20120709 VALUES LESS THAN (to_days('2012-07-10')),
 PARTITION p20120710 VALUES LESS THAN (to_days('2012-07-11')),
 PARTITION p20120711 VALUES LESS THAN (to_days('2012-07-12')),
 PARTITION p20120712 VALUES LESS THAN (to_days('2012-07-13')),
 PARTITION p20120713 VALUES LESS THAN (to_days('2012-07-14')),
 PARTITION pmax VALUES LESS THAN MAXVALUE)

実際のファイルはこのようになります。

-rw-rw---- 1 mysql mysql 98304  7月 10 11:36 2012 t_logs#P#p20120708.ibd
-rw-rw---- 1 mysql mysql 98304  7月 10 11:36 2012 t_logs#P#p20120709.ibd
-rw-rw---- 1 mysql mysql 98304  7月 10 11:36 2012 t_logs#P#p20120710.ibd
-rw-rw---- 1 mysql mysql 98304  7月 10 11:36 2012 t_logs#P#p20120711.ibd
-rw-rw---- 1 mysql mysql 98304  7月 10 11:36 2012 t_logs#P#p20120712.ibd
-rw-rw---- 1 mysql mysql 98304  7月 10 11:36 2012 t_logs#P#p20120713.ibd
-rw-rw---- 1 mysql mysql 98304  7月 10 11:36 2012 t_logs#P#pmax.ibd
-rw-rw---- 1 mysql mysql  8666  7月 10 11:35 2012 t_logs.frm
-rw-rw---- 1 mysql mysql    92  7月 10 11:35 2012 t_logs.par

この場合、pYYYYMMDD という名前のパーティションに、YYYY-MM-DDまでに作られたログデータが格納されます。
ここで一点注意が必要ですが、PARTITION節でしようされるカラムはPRIMARY KEYかそれに含まれている必要があります。
そのため、この例ではPARTITION節で使う ceratede カラムをPRIMARY KEYに含めています。
その他パーティションに関する詳しい内容はこちらを御覧ください。
http://dev.mysql.com/doc/refman/5.1/ja/partitioning-management.html


このようにパーティションを利用していると、例えば2012-07-08のデータはもう要らないから削除しよう、というときに

mysql> DELETE FROM t_logs WHERE created <= '2012-07-08';

としていたものが

mysql> ALTER TABLE t_logs DROP PARTITION p20120708;

このように書けます。前者の場合は巨大なInnoDBであるほど処理に時間がかかり非現実的な方法でしたが、
パーティションを使うことで後者のようにまるでテーブルをDropするかのようにデータを削除できます。


また、パーティションには「刈りこみ」という機能がMysql5.1.6から実装されています。
パーティション刈りこみのコンセプトは「合致する値が存在し得ないパーティションはスキャンしない」といものです。
詳しくはこちらを御覧ください。
http://dev.mysql.com/doc/refman/5.1/ja/partitioning-pruning.html

MEMORYテーブルでINSERTスピードアップ

やはりINSERTの実行速度はSELECTに比べると圧倒的に遅いのですが、
使用するテーブルのストレージエンジンによってもINSERTの実行速度は変化します。
以下の2つのテーブルで簡単な実験をしました。違いはInnoDBかMEMORYか、です。

CREATE TABLE `t_data_inno` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `value` int(11) DEFAULT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

CREATE TABLE `t_data_mem` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `value` int(11) DEFAULT NULL,
  `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=MEMORY DEFAULT CHARSET=utf8


これらにデータをINSERTするためのPHPは以下のとおりです。
それぞれのテーブルに10000件のINSERTを実行します。

# insert_inno.php
<?php
$link = mysql_connect('127.0.0.1:3306', 'root', '');
mysql_select_db('test', $link);
for ($i = 0; $i < 10000; $i++) {
    mysql_query("INSERT INTO t_data_inno (value) VALUES ($i)", $link);
}
mysql_close($link);
?>

# cat insert_mem.php
<?php
$link = mysql_connect('127.0.0.1:3306', 'root', '');
mysql_select_db('test', $link);
for ($i = 0; $i < 10000; $i++) {
    mysql_query("INSERT INTO t_data_mem (value) VALUES ($i)", $link);
}
mysql_close($link);
?>


これを実行すると以下のようになりました。

$ time  php insert_inno.php
real     0m3.983s
user     0m0.138s
sys     0m0.337s

$ time  php insert_mem.php
real     0m1.392s
user     0m0.130s
sys     0m0.321s

サーバ環境やMysqlのパラメータによって変化するでしょうが、この環境では3倍速いことがわかります。
このMemoryテーブルをうまく使えばINSERTにより処理時間を短縮することができます。
では、どの様に使用するのがいいのか?一つの使用方法を紹介します。

最終的にログを保存するテーブルはInnoDBで作成し、ログを直接書きこむテーブルをMemoryで作成します。
図のようにMemoryテーブルを2つ用意し、プログラムはログをMemoryテーブルの一方に書き込みます。
Switcherにより書きこむMemoryテーブルは切り替えられ、書き込みが起こっていない方のテーブルから
InnoDBにデータをコピーし、次の切り替えに備えます。


もう少し詳しく見ていきましょう。
まずSwitcherですがDBに設定用のテーブルを用意し、その値を参照し書きこむMemoryテーブルを切り替える、というふうにします。
以下のようにテーブルを作成しデータを設定します。

CREATE TABLE `settings` (
  `key` varchar(255) NOT NULL,
  `int_value` int NOT NULL,
  PRIMARY KEY (`key`)
) ENGINE=InnoDB 

mysql> select * From settings;
+----------+-----------+
| key      | int_value |
+----------+-----------+
| switcher |         1 |
+----------+-----------+


ログを書き込むプログラムでは、この値を参照してINSERTするテーブルを切り替えるようにします。
PHPだとこういう感じでしょうか。

<?php
/* Switcher の値を取得 */
$sql = "SELECT int_value FROM settings WHERE key = 'switcher'";
$result = $db->query($sql);
$switcher = $result['int_value'];

/* Switcher によってINSERT先を変更 */
if ($switcher == 1) {
    $sql = 'INSERT INTO t_logs_m1 VALUES(xxx, xxx, xxx)';
}
else if ($switcher == 2) {
    $sql = 'INSERT INTO t_logs_m2 VALUES(xxx, xxx, xxx)';
}
$db->query($sql);
?>


こうしておけばsettingsテーブルの値を変更するだけで、ログの書込み先を制御できます。
次は切り替えとログのコピーを自動化しましょう。
切り替えは settings テーブルのUPDATEで、コピーは t_logs へのINSERTで行います。
これらを一連に処理するためのストアドプロシージャを作成しましょう。

BEGIN
    /** switcher を参照して分岐 **/
    IF (SELECT int_value FROM settings WHERE key = 'switcher') = 1 THEN
        /* 出力先のテーブルを切り替え */
        UPDATE settings SET int_value = 2 WHERE key = 'switcher';
        /* データをコピーしてテーブルを空にする */
        INSERT INTO t_logs (SELECT * FROM t_logs_m1);
        TRUNCATE TABLE t_logs_m1;

    ELSE
        /* 出力先のテーブルを切り替え */
        UPDATE settings SET int_value = 2 WHERE key = 'switcher';
        /* データをコピーしてテーブルを空にする */
        INSERT INTO t_logs (SELECT * FROM t_logs_m2);
        TRUNCATE TABLE t_logs_m2;

    END IF;
END

このプロシージャを5分や10分毎にEventにて設定すれば自動でログの切り替えを行ってくれます。

ログ系テーブルのまとめ

  • データサイズに気をつける
    • ログの保存期間をきめましょう
  • innodb_file_per_table オプションを指定する
    • データファイルは分割しましょう
  • パーティションを使う
    • Mysql5.1以上がよいですね
  • Memoryテーブルを有効活用

最後に

いろいろとテクニックを紹介しましたが、どれもよく知られた方法だと思います。
またシステムの要件や環境によっていろいろ違ってくるのでだれにでも使えるわけではないですが、
だれかの助けになれば幸いですね。

ストアドプロシージャを作成しましょう

masayuki14.hatenablog.com

Slowquery を分析しましょう

masayuki14.hatenablog.com

PHPでmemcachedを使うときのモジュールパフォーマンス比較

PHPにはmemcachedを使うための主要モジュールが2種類あります。
機能的にいくつかの違いがありますが、今回は実行速度について比較してみました。

memcached の実行環境を整える

memcached のインストール

yum でインストールすることができます。今回のOSはFedora14です。

$ sudo yum install memcached
memcached サーバーの起動

インストールが完了すると、他のサービス同様に /etc/rc.d/init.d/memcached に起動スクリプトが設置されるので、

$ sudo /etc/rc.d/init.d/memcached start
$ sudo /sbin/service memcached start

など、各方法で memcached デーモンを起動することができます。一方コマンドからも起動することができます。

$ memcached -p 11211 -m 64m -vv

この状態だとmemcachedが起動しコマンドラインが返ってこないので
デーモン(バックグラウンド)で実行する -d オプションをつけます。

$ memcached -p 11211 -m 64m -d

これで準備完了です。


memcacheモジュールとmemcacedモジュールを比較する

PHPにはemcachedを利用するに当たって、memcacheモジュールとmemcachedモジュールの2種類があります。
これらもyumを使ってインストールできます。

$ sudo yum install php-pecl-memcache
$ sudo yum install php-pecl-memcached

どちらが早いのか検証してみましょう。以下のようなコードを用意しました。
100000件のデータの読み書き速度を比較します。

memcache module
store_mem.php
<?php
$memcache = new Memcache();
$memcache->addServer("localhost", 11211);
$memcache->flush();

for ($i = 0; $i < 100000; $i++) {
    $memcache->set(md5($i), crc32($i), 0, 1800);
}
load_mem.php
<?php
$memcache = new Memcache();
$memcache->addServer("localhost", 11211);

for ($i = 0; $i < 100000; $i++) {
    $memcache->get(md5($i));
}
実行結果
$ time php store_mem.php
real     0m5.773s
user     0m1.187s
sys     0m2.169s

$ time php load_mem.php
real     0m5.489s
user     0m1.259s
sys     0m2.103s
memcached module
store_memd.php
<?php
$memcache = new Memcached();
$memcache->addServer("localhost", 11211);
$memcache->flush();

for ($i = 0; $i < 100000; $i++) {
    $memcache->set(md5($i), crc32($i), 1800);
}
load_memd.php
<?php
$memcache = new Memcached();
$memcache->addServer("localhost", 11211);

for ($i = 0; $i < 100000; $i++) {
    $memcache->get(md5($i));
}
実行結果
$ time php store_memd.php
real     0m5.289s
user     0m1.310s
sys     0m1.497s

$ time php load_memd.php
real     0m4.667s
user     0m0.752s
sys     0m1.809s

memcached モジュールのほうが早いという結果になりました。

保存するデータ型の検証

PHPからmemcacheを使う場合、どのようなデータ型に対応しているのでしょうか。
int, string, array の3種類のデータ型について検証してみました。

<?php
$memcache = new Memcache();
$memcache->addServer('localhost', 11211);
$memcache->flush();

$v1 = 100;
$v2 = 'string';
$v3 = array(1, 2, 3);
$v4 = array('type' => 'reguler', 'color' => 'blue');

$memcache->set(1, $v1);
$memcache->set(2, $v2);
$memcache->set(3, $v3);
$memcache->set(4, $v4);

var_dump($memcache->get(1));
var_dump($memcache->get(2));
var_dump($memcache->get(3));
var_dump($memcache->get(4));
実行結果
int(100)
string(6) "string"
array(3) {
  [0]=>
  int(1)
  [1]=>
  int(2)
  [2]=>
  int(3)
}
array(2) {
  ["type"]=>
  string(7) "reguler"
  ["color"]=>
  string(4) "blue"
}

整数、文字列、配列などのデータ型を気にすることなく set/get できました。
内部的にはシリアライズされてmemcachedに保存されます。
ここでは試していませんがオブジェクトの保存にも対応しています。ただし読み出し側でもクラス定義の読み込みが必要です。
これは利用がとても簡単になりますね。


memcachedの状態を知る

memcached-tool コマンドでmemcachedの状態を知ることが出来ます。
オプション(引数)はdisplay stats dump の3つ。Host, Port を指定するときは以下のようにします。
memcached-tool [mode]

$ memcached-tool localhost:11211 display
  #  Item_Size  Max_age   Pages   Count   Full?  Evicted Evict_Time OOM
  2     120B     10827s       1    3874      no        0        0    0
  4     192B    588190s       1       2      no        0        0    0
  5     240B    946076s       2    1069      no        0        0    0
  6     304B    946076s      34   41925      no        0        0    0
  7     384B    946075s       2    2760      no        0        0    0

Full?:空きチャンクの有無
Evicted:期限切れ前に削除した回数

$ memcached-tool stats
#localhost:11211   Field       Value
         accepting_conns           1
               auth_cmds           0
             auth_errors           0
                   bytes    10950856
              bytes_read     6150939
           bytes_written      800696
              cas_badval           0
                cas_hits           0
              cas_misses           0
               cmd_flush           1
                 cmd_get           0
                 cmd_set      100000
             conn_yields           0
   connection_structures          11
        curr_connections          10
              curr_items      100000
               decr_hits           0
             decr_misses           0
             delete_hits           0
           delete_misses           0
               evictions           0
                get_hits           0
              get_misses           0
               incr_hits           0
             incr_misses           0
          limit_maxbytes    67108864
     listen_disabled_num           0
                     pid        5579
            pointer_size          64
               reclaimed           0
           rusage_system    1.773730
             rusage_user    0.308953
                 threads           4
                    time  1327019806
       total_connections          15
             total_items      100000
                  uptime         123
                 version       1.4.5

bytes:現在の使用メモリを表します。期限切れのデータも含まれるためこの値が容量いっぱいでもメモリ不足とは一概には言えません。
evictions:容量(メモリ)不足となり期限切れ前にデータを削除した回数。この回数が多いようだと容量不足となっています。
limit_maxbytes:memcacheの最大容量(バイト)
cmd_get:GETコマンド発行の累計
get_hits:リクエストでキーが見つかった数
get_misses:リクエストでキーが見つからなかった数

$ memcached-tool dump
Dumping memcache contents
  Number of buckets: 1
  Number of items  : 100000
Dumping bucket 2 - 100000 total items
add b3a8c9d7b4dd2c400ce3f7776f1f6cb8 768 1327021567 10
2027185355
add ba7c76b3377564c295f8afdfa298ca38 768 1327021568 10
3177254485
add 00c9157a614a13927382c42cc26dbfd4 768 1327021568 10
3244397292
add 1f376f49e57d4d7787a5b5b4489edd25 768 1327021567 10
1086221540
add 2a1d623c15bbdb68cf45130d7eefd312 768 1327021567 10
4031782350

memcachedのデータが表示されます。