Canonical な JSON エンコーディング

あるデータ表現を JSONエンコーディングする際、常に同じキーの順番でエンコーディングして欲しいことがあります。 例えば JSON Web Token (参考) は JSON 文字列をトークンとして用いるので(正確には更に Base64 エンコーディングしたもの)、 中身は同じ内容でも JSON へのエンコーディングの仕方によっては最終的なトークン文字列が異なるものになってしまいます。 特に Perl の場合は Perl v5.18 からハッシュのランダム化 がされるので、 JSON エンコーディングする度に異なる文字列が得られる可能性が高いです。

Canonical JSON

常に同じキーの順番でエンコーディングされる JSON のことを Canonical (正準) JSON と言ったりするそうです。 Perl では JSON.pm の canonical メソッドCanonical モードにすることが出来ます。

use JSON;

my $json = JSON->new->canonical;

Example

#!/usr/bin/env perl

use strict;
use warnings;
use v5.18;

use JSON;

for (1..3) {
    my $hash = +{ foo => 1, bar => 2, baz => 3 };
    say JSON->new->encode($hash);
}

say '';

for (1..3) {
    my $hash = +{ foo => 1, bar => 2, baz => 3 };
    say JSON->new->canonical->encode($hash);
}

結果

{"bar":2,"baz":3,"foo":1}
{"foo":1,"baz":3,"bar":2}
{"baz":3,"bar":2,"foo":1}

{"bar":2,"baz":3,"foo":1}
{"bar":2,"baz":3,"foo":1}
{"bar":2,"baz":3,"foo":1}

CanonicalJSON は常にキーでソートされた順番でエンコーディングされていることがわかると思います。 JSON Web Token など常に同じ JSON 文字列を期待する場面では Canonical JSON を使用しましょう。

ISUCON4 予選参加しました

ISUCON4の予選二日目に参加してきました。

結果は惨敗です。最終スコアは12,500くらい。 ランキング上位陣と同じようにインメモリなデータの持ち方でスコアを稼ごうとしましたが、 バグが取りきれず、最終的にはDBにインデックスを張ったりSQL最適化をしたりで終わってしまいました。

以下反省点

問題の切り分けができなかった

Redisを使ったコードへの書き換えを行いましたが、一発でベンチマークが通るわけもなく、デバッグをすることに。しかし、そもそもオリジナルのコードの仕様把握で間違えているのか、書き換えたコードにバグあるのか、その辺の切り分けができていなく、無駄に時間が過ぎていく一方でした。

改善案としては、

  1. クエリ最適化などを手始めに行い堅実にスコアを伸ばしつつ、仕様を正しく把握していく
  2. 全てを一気に書き換えるのではなく、インクリメンタルに書き換えて検証を行う

例えばDBの書き込みはそのまま残しつつ、少しずつ参照部分をRedisの方に向けていく、なんてやり方がよかったのかもしれません。

推測ではなく計測

今回計測方法としてはdstatでCPUやI/O使用率を見るということくらいしかしていなく、あとはほとんど「ここが重そうだよね」程度の推測で動いてしまいました。上位陣は適切にリクエスト時間やDBアクセス時間を計測し、地道に最適化していたみたいです。やはり推測ではなく計測大事ですね。

地味な最適化を怠らない

2日目暫定トップのfujiwara組のブログエントリーを読みましたが、こんな所まで最適化していたのか(base.txの削除やCSS参照削減など)と驚かずにはいられませんでした。特に印象に残ったのは、

100msかかっていたモノを99msにしてもスコアは1%しかあがらないですが、 2msかかっていたモノを1msにすると同じ1msの短縮でもスコア自体は2倍になる

という言葉です。考えてみれば当たり前のことですが、確かにその通りだなぁと。 今回はそういうマイクロチューニング的なことをする以前の段階で終わってしまいましたが、 日々の業務や、来年のISUCON(あるかな?)出場時には意識したいポイントです。

最後に

結果としては何も残せませんでしたが、参加して得られたものは大きいです。 後日公開されるであろう本選のISUCON問題、今回の反省点を生かして取り組むのが今から楽しみです!

Perlでカジュアルに単体テストのフィクスチャを扱う

PerlでDBを使った単体テストを行う時は Test::mysqldHarrietを使ってDBのプロセスを立ち上げたりしているのですが、 テスト毎にフィクスチャを定義するカジュアルなやり方がないなーと感じてました。 そこで__DATA__セクションにSQLをそのまま書きフィクスチャデータとして扱える Data::Section::Fixtureというモジュールを作ったので紹介します。

Data::Section::Fixture

インストール

$ cpanm Data::Section::Fixture

使い方

まず以下のサンプルコードのように、テストの__DATA__セクションにsetupとteardown用のSQLをベタ書きします。 このSQLwith_fixture関数に渡したコードが実行される前と後に呼ばれて、フィクスチャをセットアップすることができます。 あとはテスト本体でwith_fixtureを使うと、その関数の中だけフィクスチャデータにアクセスできるようになります。

use Test::More;
use DBI;
use Data::Section::Fixture qw(with_fixture);

my $dbh = DBI->connect('dbi:mysql:test', 'root', '');

subtest 'get all data' => sub {
    with_fixture($dbh, sub {
        my $rows = get_all_from_db();
        is_deeply $rows, [1, 2, 3];
    });
};

subtest 'delete all data' => sub {
    with_fixture($dbh, sub {
        delete_all_from_db();
        my $rows = get_all_from_db();
        is_deeply $rows, [];
    });
};

__DATA__
@@ setup
CREATE TABLE t (a int);
INSERT INTO t (a) VALUES (1), (2), (3);

@@ teardown
DROP TABLE t;

ちなみにwith_fixtureが呼ばれるたびにフィクスチャデータはリフレッシュされるので、他のテストの影響を受けることなく各テストを実行できて便利です。

おわりに

各テストが安心・安全にフィクスチャを扱えるモジュールの紹介でした。ガンガン単体テストを書いていきましょう。

InnoDBが実際に読んだレコード数を取得する

MySQLサーバが返すレコード数でなく、InnoDBが実際に読んだレコード数知りたいなーと思ってたら、普通にステータス変数の Innodb_rows_read で取れました。

SHOW SESSION STATUS LIKE 'Innodb_rows_read';

Innodb_rows_read は今までの累積値なので、クエリ実行前と後の差分を見るといいでしょう。 (FLUSH STATUS;を実行してもInnodb_%関係のステータス変数はリセットされないみたいです。サーバ再起動しかリセット方法はないのでしょうか?)

EXPLAIN結果のrows列が想定値よりずれてる時に見てみるといいと思います。

InnoDBのオプティマイザとロックの範囲の関係

MySQLInnoDBのロックの挙動を色々調べていたのですが、レコードの数によってロックの範囲が変わる現象に頭を悩まされたので、メモがてら少しまとめてみます。

どこまでロックする?

以下のようなテーブルがあるとします。 id列に1〜6までの数値が入っています。

CREATE TABLE t (
    `id` int unsigned NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB;

INSERT INTO t VALUES (1),(2),(3),(4),(5),(6);

ではidが3より小さなレコードを取得するクエリを投げると、どのレコードをロックするでしょうか。

BEGIN;
SELECT id FROM t WHERE id < 3 LOCK IN SHARE MODE;

1〜2のレコードでしょうか。それとも1〜3までロックするでしょうか。

実際にInnoDBロックモニタで確認してみると、実は1〜6全て(supremumレコード含む)に共有ロック(レコードのロック+ギャップロック)がかかっていることがわかります。

(cf. InnoDBロックモニタhttp://dev.mysql.com/doc/refman/5.1-olh/ja/innodb-general-monitor.html

BEGIN;
SELECT id FROM t WHERE id < 3 LOCK IN SHARE MODE;
SHOW ENGINE INNODB STATUS\G

RECORD LOCKS space id 0 page no 45 n bits 80 index `PRIMARY` of table `D`.`t` trx id 0 63305 lock mode S
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 00000001; asc     ;;
1: len 6; hex 00000000f748; asc      H;;
2: len 7; hex 80000000340110; asc     4  ;;

Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 00000002; asc     ;;
1: len 6; hex 00000000f748; asc      H;;
2: len 7; hex 8000000034011d; asc     4  ;;

Record lock, heap no 4 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 00000003; asc     ;;
1: len 6; hex 00000000f748; asc      H;;
2: len 7; hex 8000000034012a; asc     4 *;;

Record lock, heap no 5 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 00000004; asc     ;;
1: len 6; hex 00000000f748; asc      H;;
2: len 7; hex 80000000340137; asc     4 7;;

Record lock, heap no 6 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 00000005; asc     ;;
1: len 6; hex 00000000f748; asc      H;;
2: len 7; hex 80000000340144; asc     4 D;;

Record lock, heap no 7 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 00000006; asc     ;;
1: len 6; hex 00000000f748; asc      H;;
2: len 7; hex 80000000340151; asc     4 Q;;

なぜ全レコードにロックが掛かってしまうか

感覚的には1,2,3のみロックが掛かるように思えるので、不思議な挙動です。 自分も小一時間悩んだんですが、クエリの実行計画を見ると理由が推測できました。

EXPLAIN SELECT id FROM t WHERE id < 3 LOCK IN SHARE MODE\G

> *************************** 1. row ***************************
>            id: 1
>   select_type: SIMPLE
>         table: t
>          type: index
> possible_keys: PRIMARY
>           key: PRIMARY
>       key_len: 4
>           ref: NULL
>          rows: 4
>         Extra: Using where; Using index

type = index なのでインデックスのフルスキャンです。 つまりInnoDBは全てのレコードをフルスキャンしMySQLサーバに渡し、MySQLサーバ側がWHERE句のフィルタ(id < 3)を処理します。 結果、InnoDBは全てのレコードにロックを掛けてしまったということです。

データ件数が多い場合には?

次にデータを1000件投入して同じことをやってみます。 データの投入には、ダミーデータを自動生成する拙作のdatagen_from_ddl(1) 参照 を使いました。

$ echo 'CREATE TABLE t ~~~;' | datagen_from_ddl -n 1000 | mysql -u root testdb

同様にSELECT文を実行すると、今度は予想通り1〜3までのレコードをロックしました。

BEGIN;
SELECT id FROM t WHERE id < 3 LOCK IN SHARE MODE;
SHOW ENGINE INNODB STATUS\G

RECORD LOCKS space id 0 page no 7424 n bits 744 index `PRIMARY` of table `D`.`t` trx id 0 63312 lock mode S
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 00000001; asc     ;;
1: len 6; hex 00000000f74f; asc      O;;
2: len 7; hex 80000000340110; asc     4  ;;

Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 00000002; asc     ;;
1: len 6; hex 00000000f74f; asc      O;;
2: len 7; hex 8000000034011d; asc     4  ;;

Record lock, heap no 4 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 00000003; asc     ;;
1: len 6; hex 00000000f74f; asc      O;;
2: len 7; hex 8000000034012a; asc     4 *;;

EXPLAINを見てみます。

EXPLAIN SELECT id FROM t WHERE id < 3 LOCK IN SHARE MODE\G

> *************************** 1. row ***************************
>            id: 1
>   select_type: SIMPLE
>         table: t
>          type: range
> possible_keys: PRIMARY
>           key: PRIMARY
>       key_len: 4
>           ref: NULL
>          rows: 2
>         Extra: Using where; Using index 

type = range なのでインデックスを用いた範囲検索です。 つまり、id < 3がfalseになるまでInnoDBはレコードを取得 & ロックをかけていき、最終的に3のレコードを読み終了します。 よってロックされるのは1〜3のレコードのみで済みました。

まとめ

結局の所、ロックされる範囲はInnoDBがどこまでレコードを読んだかが決め手になります。 つまりロックの挙動を見る時は、オプティマイザがどうクエリを処理するかも同時に確認する必要があるという話でした。

DDLからダミーデータを自動生成するData::Generator::FromDDLを作りました

開発時に、ダミーデータを大量にDBに入れたい場面がありますが(SQLの実行計画やスケーラビリティを確認したい時など)、 DBのDDLを変更する度にデータ生成のスクリプトを書き直すのはめんどくさいですよね。

そこで、CREATE TABLE文やALTER TABLE文などのDDLから、適切なダミーデータを自動生成するData::Generator::FromDDLというCPANモジュールを作りました。

https://metacpan.org/pod/Data::Generator::FromDDL https://github.com/addsict/Data-Generator-FromDDL

インストール

$ cpanm Data::Generator::FromDDL

インストールすると datagen_from_ddl(1) というCLIのコマンドもインストールされるので、その使い方を説明したいと思います。

使い方

datagen_from_ddl(1)DDLの書かれたファイル(もしくは標準入力)を受け取って、ダミーデータを作成するINSERT文を標準出力に出力します。

$ echo 'CREATE TABLE T (a int PRIMARY KEY);' | datagen_from_ddl -n 3 --pretty

INSERT INTO
    `T` (`a`)
VALUES
    (1),
    (2),
    (3);

標準出力に出力されるので、そのままMySQLクライアントなどにパイプで渡せます。

$ datagen_from_ddl -n 3 ddl.sql | mysql -u user -p mydb

また、外部キー制約がある場合には制約を満たす順番でデータを生成します。

-- ddl.sql
CREATE TABLE A (
    a int,
    FOREIGN KEY (a) REFERENCES B (b)
);

CREATE TABLE B (
    b int PRIMARY KEY
);

このようにAテーブルのaカラムBテーブルのbカラムを参照する場合、B→Aの順番にデータが生成され、aカラムbカラムの値からランダムなものが選ばれます。

$ datagen_from_ddl -n 3 ddl.sql

INSERT INTO
    `B` (`b`)
VALUES
    (1),
    (2),
    (3);

INSERT INTO
    `A` (`a`)
VALUES
    (3),
    (2),
    (3);

その他、テーブルごとに異なるレコード数を生成するオプションなどがありますが、詳しくは datagen_from_ddl -h を見てみてください。

現状の制限

ツールをシンプルに保つためにいくつか制限があります。

  • 制約(主キー制約など)は全て数値型のみ
  • 複合主キーや複合外部キーには未対応
  • 使用できるデータ型は以下の通り
    • (unsigned) BIGINT
    • (unsigned) INT
    • (unsigned) MEDIUMINT
    • (unsigned) SMALLINT
    • (unsigned) TINYINT
    • TIMESTAMP
    • CHAR
    • VARCHAR
    • TINYTEXT
    • TEXT
    • MEDIUMTEXT
    • ENUM

終わりに

アプリケーションの開発時にはDDLは都度変化していくものです。 そんな場面に是非一度使ってもらえたらなと思います。

バグ報告・機能要望などありましたらこちらまで!  https://github.com/addsict/Data-Generator-FromDDL

perldocのレンダリング結果をキャッシュして高速に表示するモジュールを作りました

perldocコマンドはPerlを書く人にとっては手放せないツールですが、 割と大きいPodファイル(DBI, perltoc, perlfunc, ...)を表示したり、Pod::Text::Color::Delightを使ってカラーリング表示しようとすると、表示するまで少し待たされるのが気になっていました。

そこで、 perldocで出力したフォーマット結果をキャッシュしておき、次回以降素早く表示するPod::Perldoc::CacheというCPANモジュールを作ってリリースしました。

https://metacpan.org/pod/Pod::Perldoc::Cache

インストール

$ cpanm Pod::Perldoc::Cache

使い方

perldocには-MオプションでPodファイルのフォーマッターを指定することができるので、そこに本モジュールを指定します。 一度目は通常通りの時間がかかりますが、二度目以降はキャッシュを読むようになります。

$ perldoc -MPod::Perldoc::Cache DBI # doesn't use cache
$ perldoc -MPod::Perldoc::Cache DBI # use cache!

また、Pod::Text::Color::Delightなどの他のフォーマッターを同時に使いたい場合は-w parserオプションを使います。

$ perldoc -MPod::Perldoc::Cache -w parser=Pod::Text::Color::Delight DBI

一々こんな長ったらしいコマンド書いていられないので、alias もしくは PERLDOC環境変数でオプションを設定するといいと思います。

$ alias perldoc='perldoc -MPod::Perldoc::Cache -w parser=Pod::Text::Color::Delight'
$ export PERLDOC='-MPod::Perldoc::Cache -w parser=Pod::Text::Color::Delight'

キャッシュ先

キャッシュの保存先はデフォルトで~/.pod_perldoc_cacheというディレクトリになっています。 そんな所に保存したくない!って方はPOD_PERLDOC_CACHE_DIR環境変数に保存先をセットしておけばそこを使ってくれます。

またキャッシュした際のPodの内容はハッシュ値を計算してあるので、元のPodが更新された場合に古いキャッシュは参照しないようになっています。

パフォーマンス

では実際にどれくらいPodの表示速度が速くなるか、実験してみました。 横軸がPodの行数、縦軸が表示までの秒数(小さいほうがベター)です。

f:id:furuyamayuuki:20140427222007p:plain

赤が標準のperldocコマンドで実行した結果、青が本モジュールを使用した結果ですが、 赤がPodの行数に比例して遅くなってる一方、青は行数に関係なく常に高速に表示できていることがわかります。

終わりに

perldocのレンダリング結果をキャッシュするなんて相当ニッチな機能ですが、 体感的にも表示が少し速くなるので、とりあえず設定しておくといいと思います!

機能追加やバグ報告はgithubまでお願いします!

https://github.com/addsict/Pod-Perldoc-Cache