Teng vs DBIx::Skinnyベンチマーク 2012

nekokakさんが以前TengとDBIx::Skinnyのベンチマークとってみたでとっていたのですが、1年半以上経っていてその後Tengはバージョンアップを重ねていますので、改めてとってみました。

 

ベンチマークスクリプトはnekokakさんのものをもとに少し改造してtestもつけたりしました。

結果はこちら。

はやくなってますね :)

インストールされたCPANディストリビューションのバージョンを特定できない問題について

Cartonを使っているとこの問題があって今までだましだましやっていたのだけどそれも限界になってきたので問題の解決を図ってみたんですが一筋縄じゃいかなそうだったので一旦問題の整理と周知のためにエントリを書いてみる次第。

用語の整理

CPAN author諸氏におかれましてはあたりまえのことだとは思いますが一応整理。

ディストリビューション(distribution)
CPANにおいて実際に配布されるファイルのこと。tar.gzかzipファイル。複数のモジュールやMETAファイル等が含まれている。リリース(release)とも。
モジュール(module)
ディストリビューションに含まれる *.pm ファイルのこと。
パッケージ(package)
モジュールに含まれるpackage宣言されたperlにおける名前空間のこと。http://www.perl.com/CPAN/modules/02packages.details.txt の一列目はこれに該当。ちなみに二列目はそのバージョンで、三列目はディストリビューションを指している。

階層としては下記のような関係

ディストリビューション <= モジュール <= パッケージ

cpanmの引数に渡すのはパッケージ名。cpanm Gearman が通らなかったりするのはこのあたりの事情だったりする。

問題の再現

cpanm と cartonの最新Dev版がインストールされているものとます。

# 作業ディレクトリの用意
mkdir /path/to/tmp/multipsgi
cd /path/to/tmp/multipsgi

# 過去のPSGIディストリビューションを大量にインストールしてみる
cpanm -L local http://search.cpan.org/CPAN/authors/id/M/MI/MIYAGAWA/PSGI-1.101.tar.gz
cpanm -L local http://search.cpan.org/CPAN/authors/id/M/MI/MIYAGAWA/PSGI-1.10.tar.gz
cpanm -L local http://search.cpan.org/CPAN/authors/id/M/MI/MIYAGAWA/PSGI-1.03.tar.gz
cpanm -L local http://search.cpan.org/CPAN/authors/id/M/MI/MIYAGAWA/PSGI-1.02.tar.gz
cpanm -L local http://search.cpan.org/CPAN/authors/id/M/MI/MIYAGAWA/PSGI-1.01.tar.gz
# 最後にインストールしたversionは1.01

# cpanfileの用意
echo "requires 'PSGI';" > cpanfile

carton install
carton list

最後のcarton listでインストールされたバージョンの1.01に関する情報が表示されて欲しいが実際には"PSGI-1.03"等と違うものが表示される。このcarton.lockをdeployするとPSGI-1.03がインストールされてしまう。

問題の原因

cartonではインストールされているディストリビューションの特定に cpanmで作成される lib/perl5/$Config{archname}/.meta/*/install.json を使っている。先程の例だと ls local/lib/perl5/i686-linux/.meta/等とすると

PSGI-1.01  PSGI-1.02  PSGI-1.03  PSGI-1.10  PSGI-1.101

となっており、過去インストールされたすべてのバージョンの情報が残っておりそのすべての情報を取得してしまい、その後のHash化等の影響で過去インストールされたいずれかのバージョンが非決定的に特定されるということになってしまっている。

これは一見するとcartonの問題のようにも見えるが、そう言い切れるほど問題は単純ではない。

解決策?

  1. 最新バージョンをインストールバージョンとする

    問題の99%はこれで解決するだろうけど、ダウングレードをサポートできなくなってしまう。

  2. perllocal.podを解析する

    perllocal.podというのはディストリ ビューションのイ ンストール履歴のようなものでlib/perl5/{archname}/perllocal.podにある。現状の情報でできることなのでなかなかよい 気もするのだけども上書きによるダウングレードには対応できてもpm-uninstall等が使われると難しいか。(pm-uninstallでperllocalも編集してしまうという可能性もある)

  3. インストールされているパッケージのバージョンからディストリビューションのバージョンを特定する

    install.jsonは

    {"provides":{"PSGI":{"version":"1.02","file":"PSGI.pm"}},"target":"http://search.cpan.org/CPAN/authors/id/M/MI/MIYAGAWA/PSGI-1.02.tar.gz","version":"1.02","name":"PSGI","dist":"PSGI-1.02","pathname":"http:/search.cpan.org/CPAN/authors/id/M/MI/MIYAGAWA/PSGI-1.02.tar.gz"}

    のような情報を提供しており一見providesに指定されているものとインストールされているパッケージのバージョンを比較すればディストリビューションを特定できそうであるが、ディストリビューションはモジュール以外のファイルの変更によるアップグレードが行われる可能性もあるし、またPSGI-1.01等においては

    {"provides":{"PSGI":{}},"target":"http://search.cpan.org/CPAN/authors/id/M/MI/MIYAGAWA/PSGI-1.01.tar.gz","version":"1.01","name":"PSGI","dist":"PSGI-1.01","pathname":"http:/search.cpan.org/CPAN/authors/id/M/MI/MIYAGAWA/PSGI-1.01.tar.gz"}

    のように一切パッケージのバージョンを提供しない場合もあるので特定を汎用的に行うのは不可能。

  4. ディストリビューションのインストール時に既存のmetaディレクトリを消してしまう

    これが現実的なのかなーとか。ちなみに今までのだましだましの解決はこれを手動でやっていました。

  5. インストールされたディストリビューションのリストを別途管理する

    大掛かりすぎるか…

 

rubygemsはどうしているか

そもそも複数バージョンを並行してインストールできて利用するときにもバージョンを指定できるので、現在インストールされている(ただひとつの)バージョンという特定は必要無い。ただしバージョンを指定されない時は最新バージョンを利用する模様。

 

ucs2エンコーディングのススメ

はじめに

MySQL Casual Advent Calendar 2011 - MySQL Casualで24日目を担当します。nihenです。 といいながら、25日になっちゃっています。すみませんすみません。

さて、みなさんはテキスト系カラムのキャラクタセットは何を指定していますか?

  • cp932 に決まってるでござる
  • いやむしろ sjis でござる
  • いやいや5c問題を避けるために ujis でござる
  • 当然决定为Big5
  • 2010年代に utf8mb4 使わないで許されるのは小学生までだよねー
  • 漢はだまって binary

ここから述べることは、カジュアルに"utf8"キャラクタセットを使っている方々へのカジュアル情報です。上記に当てはまった漢のみなさまはそっとブラウザをお閉じください。

診断

SELECT SUM(LENGTH(column) - CHAR_LENGTH(column)*2) FROM table;

おもむろに、上記のクエリを適当なデータ量の多いutf8キャラクタセットにしているカラムに対して実行してみましょう。(column, tableを書き換えて実行してください)

帰ってきた値が正の値だった場合はその値のバイト数だけディスクスペースを節約できる可能性があります。負の値だった場合は残念でした。

方法

簡単です。そのカラムを"ucs2"キャラクタセットに変更するだけです。

ALTER TABLE table CHANGE COLUMN column column VARCHAR(255) CHARACTER SET 'ucs2' COLLATE 'ucs2_bin' NOT NULL;

などの方法で行えますね。これだけでディスクスペースが縮小されます。utf8とucs2は文字集合が共通(unicodeのBMP)なため、round-trip問題は発生しません。

え、でもプログラムからはutf-8mysqlに接続してるよと思った方はご安心ください。mysqlはカラムのキャラクタセットからcharacter_set_clientに指定されているキャラクタセットに変換を自動で行います。プログラム側に修正は必要ありません。

ucs2にするとutf8の場合の3byteキャラクタ(日本語とかがそうですね!)が2byteになるというメリットがあります。反面、1byteキャラクタ(asciiですね)が2byteに増えるというデメリットがあります。ですので日本語とasciiのどちらが多いかということが判断基準となります

実演

mysqlでのディスクスペースの削減といえば、InnoDB PluginのROW_FORMAT=COMPRESSEDなわけですが、以前id:sh2さんがMySQL InnoDB Pluginのデータ圧縮機能 - SH2の日記においてWikipedia日本語版のデータベースの圧縮を実演されてましたので、真似してみたいと思います。とはいえ時間があまりなかったので変更対象はpageテーブルのpage_titleカラムだけにしています。

xml2sql

wget http://ftp.tietew.jp/pub/wikipedia/xml2sql-0.5.tar.gz
tar xzf xml2sql-0.5.tar.gz
cd xml2sql-0.5
./configure
make
sudo make install

インポート用sql用意

mysqlimport用のデータを作るのがデフォルトなのですが、ucs2でのインポートはできないのでsqlファイルの作成を行っています。

wget http://dumps.wikimedia.org/jawiki/20111217/jawiki-20111217-pages-articles.xml.bz2
bunzip2 jawiki-20111217-pages-articles.xml.bz2
cat jawiki-20111217-pages-articles.xml | sed -e 's/<redirect \/>//' | xml2sql -m

sandbox_dbの用意

wget "http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/maintenance/tables.sql?view=co" -O tables.sql

echo 'create database sandbox_utf8;' | mysql -u root -p
echo 'create database sandbox_ucs2;' | mysql -u root -p

mysql -u root -p sandbox_utf8 < tables.sql
mysql -u root -p sandbox_ucs2 < tables.sql
echo "ALTER TABLE page CHANGE COLUMN page_title page_title VARCHAR(255) COLLATE 'ucs2_bin' NOT NULL;" | mysql -u root -p sandbox_ucs2

インポート

時間かかりますよー。

mysql -u root -p sandbox_utf8 --default-character-set=utf8 < page.sql
mysql -u root -p sandbox_ucs2 --default-character-set=utf8 < page.sql

確認

mysql>use sandbox_utf8
mysql> show table status like 'page';
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
| Name | Engine | Version | Row_format | Rows    | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time         | Update_time | Check_time | Collation | Checksum | Create_options | Comment |
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
| page | InnoDB |      10 | Compact    | 1541446 |             98 |   151683072 |               0 |    220758016 |   5242880 |        2484792 | 2011-12-24 22:46:27 | NULL        | NULL       | utf8_bin  |     NULL |                |         |
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
1 row in set (0.00 sec)

mysql>use sandbox_ucs2
mysql> show table status like 'page';

+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
| Name | Engine | Version | Row_format | Rows    | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time         | Update_time | Check_time | Collation | Checksum | Create_options | Comment |
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
| page | InnoDB |      10 | Compact    | 1553463 |             93 |   145375232 |               0 |    210255872 |   5242880 |        2484792 | 2011-12-25 00:27:57 | NULL        | NULL       | utf8_bin  |     NULL |                |         |
+------+--------+---------+------------+---------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------+----------+----------------+---------+
1 row in set (0.01 sec)

というわけで、Data_lengthが151683072 => 145375232(6MB,5%弱削減)、Index_lengthが220758016 => 210255872(10MB,5%弱削減)という結果になりました。もうちょい減るかなーと期待してたのですが若干しょぼい結果にはなりましたが削減自体は成功ということで…。この5%を多いとみるか少ないと見るかは…。いややっぱ少ないかなー。まあカジュアル!ということで。

まとめ

utf8のキャラクタセットを採用しているカラムをucs2に変更するとスペースの削減が行える場合があることを紹介しました。ucs2にした場合のベンチマークなどの話は3年前の下記記事に書いてありますので興味のある方は併せてご覧ください

mysqlの内部キャラセットはucs2にするといいんじゃないだろうか | へぼい日記

mysqlでcharsetをucs2にした場合のasciiのみのフィールド | へぼい日記

さ〜て、明日は大トリ、 y_wakai さんです!んがんぐっ!

split git またはこぼれたギットに泣かないで

CPANモジュールのdistを分割したい時などにgit repositoryも分割したいということになる。

が、分割先のrepositoryに含まれないファイルのコミットログなどが一緒に分割されてくるとログが肥大すぎていろいろとうざいときに泣かないために。

まあ、基本git filter-branch使いましょうという話です。

サブディレクトリをルートディレクトリとする場合

CPANモジュールだとこういう場合はないはずなので普通はこの方法はとれないけど一応メモ。

repository中の"hogedir"だけを取り出したい場合。

git clone ParentModule/.git SplitModule
cd SplitModule
git filter-branch --subdirectory-filter hogedir

基本これだけで十分なのだが、 git show commit-id を直接指定すると変更履歴がみれてしまう。別に問題ないんだろうけど以下もやっとくと完全に見れなくなる。

cd ../
git clone SplitModule/.git SplitModuleClean
cd SplitModuleClean
git gc --prune=now
cd ../
rm -rf SplitModule
mv SplitModuleClean SplitModule

なんか面倒ですね。多分もっと簡単な方法ある。

特定のファイルを取り除く

CPANモジュールだとこっちの方法になるとおもう。例えば

├── Changes                                                                                                                                                                       
├── MANIFEST.SKIP                                                                                                                                                                 
├── Makefile.PL                                                                                                                                                                   
├── README                                                                                                                                                                        
├── README.mkdn                                                                                                                                                                   
├── TODO                                                                                                                                                                          
├── author                                                                                                                                                                        
│   ├── assets.pl                                                                                                                                                                
│   └── test-externals.pl                                                                                                                                                        
├── lib                                                                                                                                                                           
│   ├── Amon2                                                                                                                                                                    
│   │   ├── Config                                                                                                                                                              
│   │   ├── Declare.pm                                                                                                                                                          
│   │   ├── Plugin                                                                                                                                                               
│   │   ├── Setup                                                                                                                                                               
│   │   ├── Trigger.pm                                                                                                                                                          
│   │   ├── Util.pm                                                                                                                                                              
│   │   ├── Web                                                                                                                                                                  
│   │   └── Web.pm                                                                                                                                                               
│   └── Amon2.pm                                                                                                                                                                 
└── script                                                                                                                                                                        
    └── amon2-setup.pl

のようなディレクトリ構成のモジュールがあったとして、libの下は lib/Amon2/Setup 以外は消したいとき。

git clone Amon/.git Amon2-Setup
git filter-branch --prune-empty --tree-filter '\
 rm -f lib/Amon2.pm;\
 rm -rf lib/Amon2/Config/;\
 rm -f lib/Amon2/Web.pm;\
 rm -rf lib/Amon2/Web/;\
 rm -rf lib/Amon2/Plugin/;\
 rm -f lib/Amon2/Declare.pm;\
 rm -f lib/Amon2/Trigger.pm;\
 rm -f lib/Amon2/Util.pm\
'

で、関係するコミットログが消えます。こちらもgit showで直接指定すればまだ見れますのでgit gc --prune=nowをcloneした別ディレクトリで実行するとよいです。

TODO

あとで書く

  • cpanm -lとcpanm -Lの違い(いつも忘れて調べてるので)
  • cpanfileについての自分の理解
  • cartonのmultiple mirrorパッチ
  • TengとHandlerSocketとわたし
  • rjbsの言っていたMoose switch to Any::Mooseが危険なはなし
  • こぼれたギットに泣かないで => 書いた
  • うわ…私のPerlのバージョン低すぎ…

DBIとforkの関係

実際ググれば正解はいっぱい出てくるしここに自分もコメントで書いてたりしていまさら書く必要もないかなと思ってたけど一応自分のブログでもまとめておくということで。

一般的な解

DBIx::ConnectorとかDBIx::Handler経由でかならず$dbhを取得してからDBIを使う。 もしくはfork-safeなORM(DBIx::Class, DBIx::Skinny, Teng)を使う。

DBIを直接使っている場合

一般的なコネクションを保持するクライアントと同様にDBIもforkした子供が親のコネクションをそのまま使うことはバグの原因になります。特にトランザクションの処理等で重大な問題が起こる可能性がある。 解決策は、

  1. DBIのコネクションを親で作らないで、子供で独自に作る
  2. 親で作ってしまったコネクションを子供が安全にDESTROYし、再接続する

のどちらかになります。ここで問題は2で「安全にDESTROY」しないとどうなるかというと、DBIはDESTROY時に自動的にdisconnectするようになっているので子供が勝手に親の接続を切ってしまい、その後親がその接続を使おうとすると使えないという事が起こる場合がある。(DBDによっても違うので「場合」としている)

安全にDESTROYするには、親で$dbh->{AutoInactiveDestroy} = 1;を設定する(> DBI 1.6.14)か、子で$dbh->{InactiveDestroy} = 1;を設定する必要がある。

で、再接続は$dbh->cloneを使うと楽。まとめるとこんな感じがおすすめですね。

use DBI;
my $dbh = DBI->connect('dbi:mysql:database=sandbox;host=localhost', 'sandbox', 'sandbox');

if ( fork ) {
    # 親
    wait;
    my $row = $dbh->selectrow_hashref('select * from sandbox limit 1');
}
else {
    # 子
    $dbh->{InactiveDestroy} = 1;
    $dbh = $dbh->clone({InactiveDestroy => 0}); # 新しく作成する$dbhはactive destroyでおk
    my $row = $dbh->selectrow_hashref('select * from sandbox limit 1');
}

 追記: 上記コード例のs/$dbh->clone;/$dbh->clone({InactiveDestroy => 0})/

Amon2はAmon2::Setupを別distにしたほうがいいんじゃないかという話

carton をつかう人がふえてくるだろうという予測のもと、依存をへらす変更をしています

http://d.hatena.ne.jp/tokuhirom/20111114/1321232436

自分もcartonが普及するとプロジェクトのextlib(local)に依存はすべていれる方式が一般化していくと思っています。

で、そうすると開発環境のシェル上で使えるperlにはたいしてモジュールが入ってない状態になっていることが往々にしてあるというかそもそもそんなにいれる必要がないが、amon2-setup.plみたいなのはプロジェクト外のperlで使いたいという要求がでてくる。その時にAmon2自体を入れなきゃいけないと結構依存が多くて面倒。なので別distにしてあげて通常のシェル上で使っているperlのlibにはAmon2::Setupが入っていて、プロジェクトのextlib(local)にはAmon2::Setup抜きのAmon2のdistが入っていると効率がよくなると思う。

という話を#soozyでした。

追記: patches welcomeとのことなのでやってみる