MySQL

October 21, 2010

Compressing URLs in your Webapp, for size and speed

Last year I had a chance to talk about the internals of our service: Pathtraq at Percona Performance Conference (slides), in which I described the methods we use to compress the URLs in our database to below 40% of the original size, however had not released the source code since then.  I am sorry for the delay, but have finally uploaded the code to github.com/kazuho/url_compress.

It is generally considered difficult to achieve high ratio for compressing short texts.  This is due to the fact that most compression algorithms are adaptive, i.e., short texts reach their end before the compressors learn how to encode them efficiently.

Our approach uses an algorithm known as "static PPM (prediction by partial matching)", that uses a pre-built table for compressing tables.

By using static PPM it is possible to achieve high compression ratio against hostnames or domains (e.g. "www" or "com") or English words that occasionally appear in paths or query parameters (e.g. "search").  And even in case of compressing unknown words in URLs the method works fairly well by utilizing the lower-order probability prediction tables for syllable-level compression.

The repository includes the source code of the compressor / decompressor (the core of the compressor; an optimized range coder using SSE, based on the work by Daisuke Okanohara, is actually git submoduleed from github.com/kazuho/rangecoder), Perl scripts for building compression tables, a small URL corpus for building and evaluating compression tables, and source code of a MySQL plugin that can be used to compress / decompress URLs using SQL.

By following the instructions in the README you will be able to build the compressor that compresses the URLs in the corpus to 30.5% in average, and use it in your MySQL database to store URLs more efficiently.  And one more thing, the benefit is not only for space efficiency.  Since the compressor is designed so that prefix search can be performed without actually decompressing them, in some cases your query becomes faster if you store the URLs in compressed form :-)

June 29, 2010

Q4M 0.9.4 released

I have just uploaded Q4M (Queue for MySQL) 0.9.4 to q4m.31tools.com.  There has been no bug fixes since 0.9.3, the only change is the newly added function queue_compact(table_name) that can be used to trigger table compaction manually.

If you were looking for a way to control queue compaction timing, it would be worth considering upgrading to 0.9.4.

For more information of what compaction is, please refer to my last entry on Q4M describing concurrent compaction.

April 26, 2010

MySQL and the XFS stack overflow problem

I have heard that there have been talks at MySQL UC on running MySQL (InnoDB) on top of XFS for better write concurrency than ext3.

Last weekend I had a chance to discuss on Twitter the xfs stack overflow problem (that became apparent this month, on x86_64 systems with 8k stack) and how it would affect (if any) MySQL servers.  If I understand correctly, the problem is that stack overflow might occur when a dirty page needs to be paged-out to xfs.

The conclusion was that for stability of MySQL running on xfs:

  1. xfs should not be used on top of LVM or any other MD (i.e. software RAID, etc.)
  2. xfs volumes should only contain ibdata files (that are accessed using O_DIRECT) so that the files on xfs would never exist as dirty pages within the OS

References:

March 25, 2010

Q4M 0.9.3 prerelease (with support for "concurrent compaction")

Q4M (Queue for MySQL) periodically performs an operation called "compaction", which is sort of a garbage collection, that collects empty space from a queue file and returns to the OS.

The pitfall that exists until now was that during compaction, all operation on the queue table was being blocked.

My opinion was (is) that it is not a serious problem for most users, since the time required for compaction will be small in most cases (the time depends on the number (and size) of the rows alive on the queue table, and the number of the rows alive will be mostly small).

But for usecases where fast response is a requirement, I have added a "queue_use_concurrent_compaction" option to Q4M in the 0.9.3 prerelease.  When the variable is set to one in my.cnf, INSERTs will not be blocked during compaction.  Another configuration variable queue_concurrent_compaction_interval is also available to fine-tune response time of INSERTs during compaction.  The response of INSERTs during compaction will become faster as you set the variable smaller, although the compaction will become slower as a side effect.

my.cnf
# enable / disable concurrent compaction (0: disabled (default), 1: enabled)
queue_use_concurrent_compaction=1

# handle INSERTs for every N bytes of data is compacted (default: 1048576)
queue_concurrent_compaction_interval=1048576

If you are already using Q4M without any problems, I recommend sticking to the version you are using, since the stability of Q4M might have degraded due to the introduction of the feature.

On the other hand if you were having problems with the issue or planning to use Q4M for a new application, I recommend using this release.  Have fun!  And if you find any problems, please send stacktraces to me :-)

January 22, 2010

Q4M 0.9.2 prerelease avaiable fixing data corruption on 32bit systems

Thanks to a user of Q4M, I have found a bug that would likely lead to data corruption on 32bit versions of Q4M.  64bit versions are unaffected.

Q4M by default uses mmap(2) to read from data files.  On 32bit systems, it tries to map max. 1GB per each table into memory using mmap.  When mmap fails to map memory due to low memory, Q4M falls back to file I/O to read the data.

However there was a bug in handling the response from mmap, that led to reading corrupt data from database files when mmap(2) failed after the size of the underlying file was grown / shrunk by Q4M.  And since Q4M writes back the corrupt data into the database file when rows are being consumed, the bug will likely destroy the database files.

I have fixed the bug and have uploaded Q4M 0.9.2, into the prerelease directory at q4m.31tools.com/dist/pre.  Source tarball and prebuilt binaries for MySQL 5.1.42 for 32bit linux are available.

If you are using 32bit versions of Q4M, I highly recommend either to update to 0.9.2 or switch to 64bit versions if possible.

BTW in 0.9.2 release, I also changed the maximum mmap size per table from 1GB to 256MB, to lower the possiblity of low memory.  However this countermeasure might not be sufficient in some cases, i.e. databases having many Q4M tables or if other storage engines also used a lot of memory.  I am considering of just disabling the use of mmap(2) on 32bit systems in future releases.  If you have any comments on this, please let me know.

January 20, 2010

Building a highly configurable, easy-to-maintain backup solution for LVM-based VMs and MySQL databases

Motives and the Features

For the servers running in our new network, I was in need for a highly configurable, but easy-to-use backup solution that can take online backups of VMs and MySQL databases running multiple storage engines.

Since my colleagues are all researchers or programmers but there are no dedicated engineers for managing our system, I decided to write a set of command line scripts to accomplish the task instead of using an existing, highly-configurable but time-taking-to-learn backup solutions, like Amanda.

And what I have come up with now is a backup solution with following characteristics, let me introduce them.

  • a central backup server able to take backup of other servers over SSH using public-key authentication
  • no need to install backup agents into each server
  • LVM snapshot-based online, incremental backups (capable of taking online backups of LVM-based VMs)
  • taking online backups of MySQL databases running mulitple storage engines with sophisticated lock control
  • no configuration files, only use crontab and shell-scripts

The solution consists of two tools, blockdiff (kazuho's blockdiff at master - GitHub), and cronlog script of kaztools (kazuho's kaztools at master - GitHub).

Blockdiff

Blockdiff is a set of scripts for taking block-based diffs of files or volumes on a local machine or on remote machines over the network using SSH.  The script below takes online backup of three LVM volumes from three servers.  In the form below, a full backup will be taken once a month, and incremetal backups will be taken during every month.

backup.sh
#! /bin/bash

export YEARMONTH=`date '+%Y%m'`

# backup a LVM volume (using snapshot) at /dev/pv/lv on srv00
blockdiff_backup /var/backup/srv00-pv-lv-$YEARMONTH ssh_lvm_dump --gzip \
  root@srv00 /dev/pv/lv \
  || exit $?

# backup another LVM volume on an another server
blockdiff_backup /var/backup/srv01-pv-lv-$YEARMONTH ssh_lvm_dump --gzip \
  root@srv01 /dev/pv/lv \
  || exit $?


# backup a MySQL database stored on volume /dev/pv/lv on server db00
BLOCKSIZE=16384 \
  LVCREATE_PREFIX='mysqllock --host=db00 --user=root --password=XXXX' \
  blockdiff_backup /var/backup/db00-pv-lv-$YEARMONTH ssh_lvm_dump --gzip \
  root@db00 /dev/pv/lv \
  || exit $?

The backup command of the last volume uses mysqllock command included in blockdiff to keep "FLUSH TABLES WITH WRITE LOCK" running while taking a snapshot of the LVM volume on which the database files exist.  It is also possible to implement other kinds of locks so as not to issue the "FLUSH TABLES WITH WRITE LOCK" while long-running queries are in execution.  Since the flush statement blocks other queries until all of the already running queries complete, issuing the flush query when long-running queries exist will lead to the database not responding to other queries for a certain amount of time.

Crontab and the cronlog script

The backup script is invoked by cron via cronlog, a script that logs the output of the executed task, as well as controlling the output passed to cron so that an alert mail will be sent when the backup script fails.  It uses setlock command of daemontools for holding an exclusive lock while running the backup script (and to alert the administrator on when failing to acquire the lock).

Crontab script
MAILTO=admin@example.com
5 3 * * * cd /var/backup && exec setlock -nX /tmp/backup.lock cronlog -l /var/backup/backup.log -t -- ./backup.sh 2>&1

This is all that needs to be set up to backup LVM volumes including MySQL databases.  Output of the log will be like the following.

backup.log
------------------------------------------------------------------------------
[Sat Jan  9 03:05:02 2010] backup-srv starting: ./backup.sh
[Sat Jan  9 03:05:02 2010] creating snapshot...
[Sat Jan  9 03:05:07 2010]   Logical volume "lvm_dump" created
[Sat Jan  9 03:05:07 2010] running: ssh_blockdiff_dump --gzip "root@srv00" "/dev/pv/lv"...
[Sat Jan  9 03:19:22 2010] removing snapshot /dev/pv/lvm_dump...
[Sat Jan  9 03:19:23 2010]   Logical volume "lvm_dump" successfully removed
[Sat Jan  9 03:19:23 2010] backup completed successfully
(snip)
[Sat Jan  9 03:35:56 2010] creating snapshot...
[Sat Jan  9 03:35:56 2010] issuing lock statement: FLUSH TABLES WITH READ LOCK
[Sat Jan  9 03:36:00 2010]   Logical volume "lvm_dump" created
[Sat Jan  9 03:36:00 2010] issuing unlock statement: UNLOCK TABLES
[Sat Jan  9 03:36:00 2010] running: bin/ssh_blockdiff_dump --gzip "root@db00" "/dev/pv/lv"...
[Sat Jan  9 04:18:44 2010] removing snapshot /dev/pv/lvm_dump...
[Sat Jan  9 04:18:46 2010]   Logical volume "lvm_dump" successfully removed
[Sat Jan  9 04:18:46 2010] backup completed successfully
[Sat Jan  9 04:18:46 2010] command exited with code:0

The files in the backup directory will be like below.  The .gz files contain the backup data, and .md5 files contain per-block checksums used for taking incremental or differential backups.

The backup files
% ls -l db00-pv-lv-201001*
-rw-r--r-- 1 backup backup 50289166539 2010-01-01 05:35 db00-pv-lv-201001.1.gz
-rw-r--r-- 1 backup backup   131072004 2010-01-01 05:35 db00-pv-lv-201001.1.md5
-rw-r--r-- 1 backup backup 10914423057 2010-01-02 04:32 db00-pv-lv-201001.2.gz
-rw-r--r-- 1 backup backup   131072004 2010-01-02 04:32 db00-pv-lv-201001.2.md5
-rw-r--r-- 1 backup backup 13648250036 2010-01-03 04:33 db00-pv-lv-201001.3.gz
-rw-r--r-- 1 backup backup   131072004 2010-01-03 04:34 db00-pv-lv-201001.3.md5
(snip)
-rw-r--r-- 1 backup backup           3 2010-01-18 04:34 db00-pv-lv-201001.ver

For more information, please read the source code and the accompanying documentation.

Conclusion

As can be seen, this is a powerful backup solution that can be built up with minimum setup. It will work well if you work in a small number of experienced engineers, while it might not be suitable for large-scale deployments with many admins.  If you are interested, please give it a try.  I am looking forward to your ideas and / or suggestions.

PS. The blockdiff_merge command can be used to restore the backups.

January 18, 2010

blockdiff を使ったお手軽ホットバックアップ環境の構築 (Linux, MySQL, etc.)

 一昨日に開催された hbstudy #7 にバックアップの話を聞きに行ってきました。Amanda を中心にした話で、とても勉強になりました。が、設定がめんどくさそうだなぁ、とも。自分の需要にはあわない感じでした。

 勉強会が終わったあとで、自作のバックアップスクリプト blockdiff に関する話を何人かの方とさせていただいたのですが、思いのほか反応が良かったので、あらためて紹介したいと思います。

 blockdiff は、一言でいうと、パーティションやデータベースのデータファイルの差分バックアップツールです。rsnapshot に似ていますが、rsnapshot ではデータベースのホットバックアップ不可能です。逆に blockdiff はディレクトリ単位でのバックアップには対応していないかわり、ファイルシステムやデータベースを、一貫性を保ちつつ実質無停止で差分バックアップすることができます

 blockdiff の具体的な特徴は以下のとおりです。

  • 設定ファイルが不要
  • 複雑な設定がありません。コマンドライン (あるいは crontab) で、適当な引数をつけてバックアップコマンドを呼び出すだけの簡単インターフェイスです。コマンドラインで動作を制御するため、(ホットバックアップを含む) 様々なバックアップロジックを組むことも可能です。

  • バックアップを取るサーバにソフトウェアのインストールが不要
  • ssh 経由でリモートサーバのバックアップを取ることができます。サーバにバックアップソフトウェアをインストールする必要はありません (バックアップを取るために必要なプログラムは自動的に送り込まれます) 注1

  • フルバックアップと増分バックアップに対応
  • フルバックアップと増分バックアップに対応しています。また、下位レイヤのスクリプト (blockdiff_dump) を直接実行すれば、差分バックアップも可能です。

  • ファイル単位、あるいは LVM スナップショットを使ったバックアップが可能
  • ファイル単位のバックアップと、LVM スナップショットを使ったホットバックアップに対応しています。

  • MySQL のホットバックアップが可能
  • 同梱の mysqllock コマンドと組み合わせることで、LVM スナップショットを使った MySQL データベースのホットバックアップが可能です。PostgreSQL については確認はしていませんが、ロックコマンドを使わなくてもバックアップが取れるんじゃないかと思います。

  • LVM ベースの VM のホットバックアップが可能
  • LVM ベースの仮想ディスクを用いた Xen の DomU のホットバックアップが可能です注2。私は使っていないので試していませんが、KVM の仮想マシンについても同等のことが可能だと思われます。LVM ベースではない VM についても、スナップショット機構によってはホットバックアップが可能だと思われます。

 blockdiff を使った LVM ボリュームのバックアップ方法については Kazuho@Cybozu Labs: リモートからXenのDomUとかLVMやファイルを差分バックアップするスクリプトを書いた で触れたので、ここでは MySQL データベースをホットバックアップする場合の設定を紹介したいと思います。前提として、MySQL のデータが LVM ボリューム上に保存されている必要があります。

 バックアップスクリプトは、以下のような感じになります。

#! /bin/bash

export BLOCKSIZE=16834
export LVCREATE_PREFIX="mysqllock --host=db-host --user=root --password=XXX"
export YEARMONTH=`date '+%Y%m'`

blockdiff_backup /var/backup/db-backup-$YEARMONTH bin/ssh_lvm_dump --gzip \
  root@db-host /dev/mapper/logical_volume_of_mysql

このバックアップスクリプトは以下のことを行っています。

  • バックアップのブロックサイズを InnoDB のブロックサイズである 16KB に設定
  • スナップショットを取る瞬間に mysqllock コマンドを使って FLUSH TABLES WITH READ LOCK を使うことで、MyISAM テーブル等の一貫性のあるバックアップを実現
  • 1ヶ月毎にフルバックアップ。それ以内は増分バックアップ

 バックアップスクリプトを実行すると、最初に db-backup-YYYYMM.1.gz, db-backup-YYYYMM.1.md5, db_backup-YYYYMM.ver という3つのファイルが作成されます。YYYYMM.1.gz がバックアップデータ、YYYYMM1.md5 がブロック単位のチェックサム情報です。次に実行すると YYYYMM.2.* が、その次は YYYYMM.3.* が、という形で増分バックアップが増えていきます。最新のバックアップの番号は .ver ファイルが記憶しているので、バックアップが途中で失敗しても問題ありません。次回バックアップ時に壊れたバックアップファイルが上書きされます。

% ls -l db-backup-201001*
-rw-r--r-- 1 backup backup 50289166539 2010-01-01 05:35 db-backup-201001.1.gz
-rw-r--r-- 1 backup backup   131072004 2010-01-01 05:35 db-backup-201001.1.md5
-rw-r--r-- 1 backup backup 10914423057 2010-01-02 04:32 db-backup-201001.2.gz
-rw-r--r-- 1 backup backup   131072004 2010-01-02 04:32 db-backup-201001.2.md5
-rw-r--r-- 1 backup backup 13648250036 2010-01-03 04:33 db-backup-201001.3.gz
-rw-r--r-- 1 backup backup   131072004 2010-01-03 04:34 db-backup-201001.3.md5
...
-rw-r--r-- 1 backup backup           3 2010-01-18 04:34 db-backup-201001.ver

 バックアップスクリプトが正しく動作することを確認したら、crontab を設定して毎日バックアップを取るようにします。

0 4 * * * setlock -nX /var/backup/backup.lock cronlog -l /var/backup/backup.log -t -- /var/backup/backup.sh 2>&1

 ここでは、daemontools の setlock コマンドを使って万が一の重複起動を抑止しています。また、kaztools の cronlog コマンドを使うことで、バックアップの進捗をログに保存しつつ、バックアップが失敗した場合は cron 経由でアラートメールが送信されるようになっています。

 ちなみに、バックアップのログ (backup.log) は、毎日次のような感じで追記されていきます。データベースの停止期間 (アプリケーションからのクエリがロックされる期間) は 03:35:56 から 03:36:00 までの4秒間であり、実質無停止でのバックアップができていることがわかります。

------------------------------------------------------------------------------
[Sat Jan  9 03:05:02 2010] backup-srv starting: /var/backup/backup.sh
[Sat Jan  9 03:05:02 2010] creating snapshot...
[Sat Jan  9 03:05:07 2010]   Logical volume "lvm_dump" created
[Sat Jan  9 03:05:07 2010] running: bin/ssh_blockdiff_dump --gzip "root@db-host" "/dev/db/lvm_dump"...
[Sat Jan  9 03:19:22 2010] removing snapshot /dev/db/lvm_dump...
[Sat Jan  9 03:19:23 2010]   Logical volume "lvm_dump" successfully removed
[Sat Jan  9 03:19:23 2010] backup completed successfully
[Sat Jan  9 03:19:23 2010] creating snapshot...
[Sat Jan  9 03:35:56 2010] issuing lock statement: FLUSH TABLES WITH READ LOCK
[Sat Jan  9 03:36:00 2010]   Logical volume "lvm_dump" created
[Sat Jan  9 03:36:00 2010] issuing unlock statement: UNLOCK TABLES
[Sat Jan  9 03:36:00 2010] running: bin/ssh_blockdiff_dump --gzip "root@db-host" "/dev/x25m.1/lvm_dump"...
[Sat Jan  9 04:18:44 2010] removing snapshot /dev/x25m.1/lvm_dump...
[Sat Jan  9 04:18:46 2010]   Logical volume "lvm_dump" successfully removed
[Sat Jan  9 04:18:46 2010] backup completed successfully
[Sat Jan  9 04:18:46 2010] command exited with code:0

 このように、blockdiff を使うことで VM 単位のバックアップだけでなく、データベースのホットバックアップも簡単に取ることができます。パストラックでは、これらバックアップスクリプトに処理を追加し、バックアップ中は統計処理を抑止するといったことも行っています。

 blockdiff や kaztools は、いずれも私の github リポジトリ (github.com/kazuho) からダウンロードして perl Makefile.PL && make all test && make install でインストール可能です。あるいは nopan がインストールされていれば、nopan http://github.com/kazuho/blockdiff http://github.com/kazuho/kaztools と1コマンドでインストールすることができます。いずれもパストラックで実際に運用しているツール群ですが、興味のある方は (at your own risk で) お試しあれ。

注1: バックアップを取るサーバに Perl 5.8 以降がインストールされている必要があります。また、公開鍵認証を用いて SSH ログインできる必要があります
注2: XenServer のスナップショット機能とは併用できません

January 06, 2010

Q4M 0.9 released

Q4M 0.9 is now available from q4m.31tools.com, along with prebuilt binaries for MySQL 5.1.42 (running on linux and Mac OS X 10.5).

The release fixes a deadlock issue under high load.  Thank you to the users for sending the necessary information to resolve the issue.

If you still find any deadlocks or stability problems of Q4M, please send stacktraces of all threads (by attaching gdb to mysqld and executing "thread apply all bt").

December 14, 2009

Comparing InnoDB performance on HDD, SSD, in-memory

The chart shows benchmark results taken using sysbench.  Rough understanding would be that (for this scenario) the performance ratio is HDD:SSD:in-memory = 1:10:50.

transactions/sec. read/write reqs./sec.
buffer_pool=8M, HDD 19.93 378.59
buffer_pool=8M, SSD (Intel X25-M) 207.70 3946.29
buffer_pool=2048M, HDD 998.82 18977.51

Details:

The benchmark was taken using MySQL 5.1.41 using innodb_plugin running on linux 2.6.31/x86_64 (Ubuntu 9.10 server).  Options passed to sysbench were: --test=oltp --db-driver=mysql --mysql-table-engine=innodb --oltp-table-size=1000000 --mysql-user=root --mysql-db=test --oltp-table-name=test_t --num-threads=20 .  My.cnf was set as follows.

max_allowed_packet=16777216
query_cache_size=0
default-storage-engine=INNODB
innodb_buffer_pool_size=2048M
innodb_read_io_threads=4           # set to 10 for X25-M
innodb_log_file_size=64M
innodb_flush_method=O_DIRECT
innodb_file_per_table
innodb_flush_log_at_trx_commit=2   # sufficient for our task
key_buffer_size=64M
myisam_sort_buffer_size=32M
slow-query-log
long_query_time=1

December 03, 2009

MySQLをロックしてほげほげするツール「mysqllock」を書いた

 MySQLを使っていると書込みロックをかけてバックアップを取る、ってのは一般的だと思います。実際、標準添付の mysqlhotcopy や Xtrabackup もそういうことをやっています。

 しかし、これらはいずれもロックの管理とバックアップ処理が密結合になっている(ですよね?)のが玉に瑕。

 特にボリュームレベルのスナップショット機能を使ってバックアップを取る場合、スナップショットを取るためのコマンドは環境(LVM とか XenServer とか VMware ESXi とか...)によって異なるので、ロック管理とバックアップコマンドは疎結合にしておきたい。と思ったので、書くことにしました。というか、疎結合なのを探すより書いたほうが早かった。コードはこちら↓。

kazuho's blockdiff at master - GitHub

 使い方は、以下のとおり。setlock 風のインターフェイスなので、慣れている人には簡単だと思います。

% mysqllock --help
Usage: ./mysqllock [options] cmd
Options: --user=db_user    username (default: current user)
         --password=pass   password (default: none)
         --host=db_host    db host (default: 127.0.0.1)
         --port=db_port    db port (default: 3306)
         --db=db_name      database name (default: "mysql")
         --before=stmt     statement to execute before running the command
                           (default: "FLUSH TABLES WITH READ LOCK")
         --after=stmt      statement to execute after running the command
                           (default: "UNLOCK TABLES")

 自作のバックアップツール(Kazuho@Cybozu Labs: リモートからXenのDomUとかLVMやファイルを差分バックアップするスクリプトを書いた)も、1行パッチをあてるだけで、テーブルロックをした状態でスナップショットを取ってから、バックアップを取れるようになりました。こんな感じで。

% SNAPSHOT_SIZE=5G BLOCKSIZE=65536 LVCREATE_PREFIX='bin/mysqllock --host=mysqld.local --user=root --password=XXXXXXXX'  bin/blockdiff_backup backup-name bin/ssh_lvm_dump -z root@mysqld.local /dev/XXXXXXXX/XXXXXXXX
creating snapshot...
issuing lock statement: FLUSH TABLES WITH READ LOCK
  Logical volume "lvm_dump" created
issuing unlock statement: UNLOCK TABLES
running: bin/ssh_blockdiff_dump -z "root@mysqld.local" "/dev/XXXXXXXX/lvm_dump"...
removing snapshot /dev/XXXXXXXX/lvm_dump...
  Logical volume "lvm_dump" successfully removed
backup completed successfully

 結論:djb++

参考: Kazuho@Cybozu Labs: setlock を使って cron をぶんまわす方法

December 01, 2009

高度に進化した分散データストアは RDBMS と見分けがつかない? (shibuya.pm #12 スライド)

 昨日開催された shibuya.pm #12 - NoSQL特集で使用したスライドを slideshare にアップロードしました。

 開発しているシャーディングミドルウェアである Incline と Pacific については YAPC::Asia 2009 を始めいろいろな所で話をする機会をいただいてきたので、今回は、なぜ RDBMS ベースのアプローチを採用したのかという背景を中心に説明させていただきました。概念的な話が多くて分かりにくかったと思います(すみません)が、細かな点についてはパフォーマンスとスケーラビリティのためのデータベースアーキテクチャ (BPStudy#25発表資料)を参照いただければと思います。

 また、中で出てきた「実体化ビュー」については、Materialized view - Wikipedia, the free encyclopediaが良くまとまっているかと思います。Incline は一言でいうと、RDBで構成されるshard群の上で read-only かつ eventually consistent な materialized view を実現するためのツールです。

September 28, 2009

パフォーマンスとスケーラビリティのためのデータベースアーキテクチャ (BPStudy#25発表資料)

先週金曜日、BPStudy#25で、「パフォーマンスとスケーラビリティのためのデータベースアーキテクチャ」という題目で話をさせていただきました。その際に使用した発表資料は以下のとおりです。

1. Happy Optimization

最初に、最適化の考え方として、上限値を予測し、それを元にリソース配分を考える、という手法を説明しました。

2. Scaling?

次に、スケールアウトとスケールアップについて、技術の進化と要件の変化の両面から、自分がどう捉えているかを話しました。

3. Incline & Pacific - なぜ作るのか

現在開発している Incline と Pacific という、RDB Sharding のためのふたつのツールについて、開発している理由を説明しました。

4. A Clever Way to Scale-out a Web Application

Incline と Pacific の実装および実際の使い方について、デモを交えて説明しました。デモは途中で終わっちゃいましたが。

5. A Better Cached

そして最後に、MySQL に memcached プロトコル対応機能を追加する Mycached について説明し、また、何が Memcached の問題だと思っているか、について話しました。

話を終わって

一番うれしかったのは、多数の質問をいただけたことです。説明の足りない点を補えるのはもちろん、自分と違う視点から見て、Incline と Pacific にどのような問題/限界があるのか、改めて考えるいい機会になりました。聞きにきてくださった皆さん、運営の方々にも、改めて御礼申し上げたいと思います。

なお、紹介した各ソフトウェアのソースコードについては、以下の URL にあります。それぞれ成熟度に差がありますが、ダウンロードしてお試しいただければと思います。

September 11, 2009

A Clever way to scale-out a web application (YAPC::Asia 2009 Presentation)

For couple of months I have been writing middlewares for database shards, and today I made a presentation covering them.  It includes the following.

  • Incline - a trigger and queue based distributed materialized view manager
  • Pacific - a set of perl scripts to manage MySQL shards, a MySQL shard can be split into two in less than 10 seconds of write blocking (and no read blocks)
  • DBIx::ShardManager - a client API for accessing database shards using Incline and Pacific

With these middlewares I think it is no more difficult to write web applications that runs on database shards.  In fact IMHO it is as easy as writing a webapp that runs on a standalone database.

The presentation slides are available from slideshare.  If you have any question or suggestions, please leave a comment.  Thank you.

August 26, 2009

Mycached: memcached protocol support for MySQL

It is a well-known fact that the bottlenecks of MySQL does not exist in its storage engines, but rather in the core, for example, its parser and execution planner.  Last weekend I started to wonder how fast MySQL could be if those bottlenecks were skipped.  Not being able to stop my curiousity, I started  adding memcached proctol support to MySQL as a UDF.  And that is Mycached.

From what I understand, there are two advantages of using mycached (or the memcached protocol, in general) over using SQL.  One is faster access.  The QPS (queries per second) of mycached is roughly 2x compared to using SQL.  The other is higher concurrency.  As can be seen in the chart below, mycached can handle thousands of connections simultaneously.

And the lack of the described advantages has been the reasons for large-scale web applications to use memcached, even though it requires application developers to take care of keeping consistency between mysql and memcached.  In other words, mycached could become a good alternative here.

Mycached is still in very early stage of development.  I would never recommend using current version in a production environment, but if you are interested, the details of how to use it is as follows.

Source code of mycached is available at coderepos.org/share/browser/platform/mysql/mycached.  There is no Makefile or configure script yet, use GCC directly to create the UDF.  Source code of mysql is needed and should be set as include directories (you need to run make on the source code of mysql prior to compiling mycached).

% g++ -DMYCACHED_USE_EPOLL=1 -shared -fPIC -Wall -g -O2 -I ~/test/mysql-5.1.37/src/include -I ~/test/mysql-5.1.37/src/sql -I ~/test/mysql-5.1.37/src/regex mycached_as_udf.cc -o mycached_as_udf.so

Once the compilation succeeds, copy the shared library into the plugins directory of the lib directory of mysql, and use the CREATE FUNCTION statements to activate the necessary UDFs.  And to start mycached, call mycached_start.  The function takes three arguments: host, port, and number of worker threads to be used for handling requests.  To stop the mycached, call mycached_stop().

mysql> CREATE FUNCTION mycached_start RETURNS INT SONAME 'mycached_as_udf.so';
mysql> CREATE FUNCTION mycached_stop RETURNS INT SONAME 'mycached_as_udf.so';
mysql> SELECT mycached_start(0, 11211, 4);

The only memcached command currently supported by mycached is get (but you can do other things using SQL :-p).  As the following example shows, specify more than one db_name.table_name.primary_key to fetch a row of a table, separated by a whitespace.  The returned value will be a repetition of columns, each of them in the format of column_name:value_length:value.  It is also possible to specify JSON as the response format by adding ":JSON", however it will be slower, since the server needs to convert the values of the columns to UTF8 and to escape them.

$ telnet localhost 11211
Trying 127.0.0.1...
Connected to localhost.localdomain (127.0.0.1).
Escape character is '^]'.
get test.t1.1
VALUE test.t1.1 0 68
id:1:1message:51:hello world! hello world! hello world! hello world!
END
get test.t1.1:json
VALUE test.t1.1 0 72
{"id":1,"message":"hello world! hello world! hello world! hello world!"}
END

Thank you for reading.  If you have any ideas or suggestions, please leave a comment.

August 07, 2009

YAPC::Asia 2009 で「スケールするウェブアプリケーションを20分で作る方法」について話します

 このところ、MySQL と Perl 関連のエントリをいろいろ書いていますが、それは、スケールアウト可能で、かつ、管理が容易なウェブアプリケーションを、簡単に書けるようにしたい、という理由があるからです。

 ただ、ブログエントリだとどうしても細切れになるので、一連のモジュールやプログラムを組み合わせて、どうやってスケールするウェブアプリケーションを作るのかという話を YAPC::Asia 2009 でさせていただくことにしました。

 YAPC::Asia 2009 は9月10日(木)と11日(金)の2日間、東京工業大学大岡山キャンパスで開催されます。今日からチケット販売も始まったので、興味のある方はお越しいただければ、と思います。

August 06, 2009

Deployment of MySQL using daemontools, XtraBackup

I am sure many people have already done similar things, but to ease my pain of setting up mysqld on a large-scale environment (I am trying to create a set of database nodes, each node consists of a MySQL failover cluster using semi-sync replication, that can be administered easily), I have just finished writing a deployment script called mysqld_jumpstart.  The caveats are:

  • integration with daemontools (mysqld is automatically started)
  • setup of masters and slaves
  • can setup slaves from backup data generated by XtraBackup

The last feature was the one I especially needed, since thanks to the people at Percona, things have become much easier with XtraBackup (or with InnoDB Hot Backup) since there is no more need to detach a mysqld before creating a backups.

Setting up and starting a master database:

# ssh root@10.0.1.1 mysqld_jumpstart \
--mysql-install-db=/usr/local/mysql/scripts/mysql_install_db \
--mysqld=/usr/local/mysql/bin/mysqld --base-dir=/var/my_webapp/mysql \
--data-dir=/var/datadrive/my_webapp --server-id=10 \
--replication-user=repl --replication-password=replpass \
--replication-network='10.0.0.0/255.0.0.0'

To create and a start a slave from a backup by XtraBackup (specified by --data-dir):

# ssh root@10.0.1.2 mysqld_jumpstart \
--mysql-install-db=/usr/local/mysql/scripts/mysql_install_db \
--mysqld=/usr/local/mysql/bin/mysqld --base-dir=/var/my_webapp/mysql \
--data-dir=/var/datadrive/my_webapp --server-id=10 \
--replication-user=repl --replication-password=replpass \
--replication-network='10.0.0.0/255.0.0.0' \
--master-host=10.0.1.1 --from-innobackupex

Mysqld_jumpstart can be downloaded from coderepos.org/share/browser/platform/mysql/mysqld_jumpstart.  Use --help to find out how to use the script.  FYI to run the tests, you need to apply a patch to XtraBackup.

August 04, 2009

Perl のテスト用に MySQL 環境を自動で構築するモジュール Test::mysqld を書いた

 ORM やウェブアプリケーション関連のライブラリなどのテストケースを書くにあたっては、 RDBMS へのアクセスが必要になります。しかし、SQLite のようなスタンドアローンのデータベースと比較すると、サーバ型データベースである MySQL に接続してテストを書くのは、既存の MySQL の権限設定やデータベース名を気にする必要があったりと、いろいろ不便です。そこで、MySQL のインスタンスをテンポラリディレクトリに自動生成し、テストが終わったら削除してくれる Perl モジュール Test::mysqld を書きました。こんな感じで使います。

use DBI;
use Test::mysqld;
use Test::More;

my $mysqld = Test::mysqld->new(
    my_cnf => { 'skip-networking' => '' }, # TCP接続を使わない
) or plan skip_all => $Test::mysqld::errstr;

plan tests => 10;

my $dbh = DBI->connect(
    "DBI:mysql:$db_name;user=root;mysql_socket=" . $mysqld->my_cnf->{socket},
);

... 以下、テストコード ...

 テスト専用の mysqld サーバが立ち上がるので、既存のデータを壊したりすることもありません。データベースを作ったり消したりも自由自在。また、既存の mysqld の設定に引きずられることもないので、mysqld の権限設定を気にする必要もありません。テストが終了すると、作成された mysqld インスタンスは自動的に消去されます。

 結論。Test::mysqld を使えば MySQL にアクセスするテストが SQLite なみに簡単に書けるよ、きっと。ということで、お使いになりたい方は、CPAN か coderepos からどうぞ。

2009/8/5追記: Test-mysqld-0.04 で mysql 関連の必要なプログラムが見つからなかった場合の処理を整理したのにあわせ、サンプルをアップデートしました。

July 22, 2009

Using a statistical approach to analyze / monitor MySQL bottleneck queries

What comes up to your mind when you hear the phrase "MySQL performance tuning?"  Most of the discussions covering the topic are things like server parameter tuning and blog entries describing the use of the "EXPLAIN" command.  However, it is too time-consuming to check every single SQL query.  A better approach is at first to determine the SQL queries that are actually consuming the server resources, and then to optimize those queries.

So the question is how to find out the bottleneck queries.  At MySQL Conference & Expo 2009, Mark Callaghan explained in his keynote that Google was taking a statistical approach using the "SHOW PROCESSLIST" as a solution.  The same command is mentioned as a rival approach in the webpages of MySQL Query Analyzer as well.  Today, having a need for something alike, I wrote a set of tiny script files under the name "mprofile" that does the same thing.  It can be used as follows.

Sampling SQL queries

Use mpdump to sample running queries from mysqld periodically.  The default setting is to fetch 1,000 samples in 0.1 second interval  (in other words, it would take a little more than 100 seconds to execute).  Please use the --help option to find out the way to specify the number of samples, the interval, and connection parameters to mysqld can be specified as command line parameters.

% mpdump > mpdump.txt

Merging the queries and generating the report

Use mpfilter command to rewrite the variables in the sampled queries, then feed the result into mpreport command to sort the queries from the heaviest one to the lighter.  Each line of the report is in the format "probability:SQL query," where probability is the average percentage of if the SQL query is executed.  The example below is taken from the mysqld of Pathtraq.  It is obvious that which queries are consuming the resources (and that the heaviest query is running at the probability of 138.3%: it is run by 1.383 threads by average).

% mpfilter < mpdump.txt | mpreport | head -100
138.300:select pt_sum(hits,?) as cnt from url inner join counter on url.id=counter.url where (pt_url_compress(?)<=rurlc and rurlc<pt_url_compress(?,?))
99.100:insert into tt select t1.url,cnt_mixed&? as cnt,********** where url.status_code=?
94.200:select id,pt_url_decompress(rurlc) as rurl from url where crawl_cnt<? and crawl_at>=? and crawl_at<?-?*pow(?,crawl_cnt) limit ?
22.000:select id,status_code from url where rurlc=pt_url_compress(?)
14.300:select id,pt_url_decompress(rurlc) as rurl,cnt from (select url,cnt from counter_by_day **********
9.300:insert into counter (url,referer,hits) values (?,?,pt_cnt_add(null,?,?)) on duplicate key update hits=pt_cnt_add(hits,?,?)
7.400:insert ignore into **********
6.100:select pt_sum(hits,?) as cnt from counter where url=?
5.900:select (crawl_at<? and -crawl_at<? and pt_sum(hits,floor(-crawl_at/?))>?) as recrawl from url inner join counter on url.id=counter.url where id=?
5.700:insert into **********
5.700:select t1.id,pt_url_decompress(rurlc) as rurl,cnt,score from **********
4.600:select id,name,dispname from category_ja order by disporder,id
4.300:select addr from **********
3.200:update url set crawl_at=?,lang=?,status_code=if(status_code=?,?,?) where i
d=?
(snip)

By using the report, we can decide from which query to start optimizing, by using explain, or by letting their responses be cached by memcached, etc.  It is also possible to replace mpfilter with a house-made script to customize the report.

Conclusion

There are other ways to find out bottlneck queries, like using the slow query log or MySQL Enterprise Monitor.  Comparing with them, the merits of using the described solution using "SHOW PROCESSLIST" are

  • no need to change current environment
  • practically no overhead
  • potential to build a customized performance tuning / monitoring tool
As an example of using "SHOW PROCESSLIST" for other purposes than performance tuning, it would be possible to keep a daily sample log of SQL queries using mprofile, and use them to find out the long-term changes of the bottlenecks to build server reinforcement plans or for refactoring the design of the application.

Finally, the source code of mprofile is available from /platform/mysql/mprofile – CodeRepos::Share – Trac.  Have fun!

MySQL のボトルネックを統計的に監視・解析する方法

 MySQL のチューニング、と言った場合には、サーバーパラメータの調整や EXPLAIN コマンドを利用したクエリ実行計画の最適化が話題に上ることが多いです。しかし、発行する全ての SQL について、いちいち EXPLAIN コマンドを使って確認していては、いくら時間があってもたりません。チューニングを効率的に進めるには、まず、ボトルネックとなっている SQL クエリを特定し、次にその最適化を行うべきです。

 ではどのようにして、ボトルネックを特定するのか。MySQL Conference & Expo 2009 のキーノートにおいて Mark Callaghan 氏は、Google では SHOW PROCESSLIST コマンドを使った統計的アプローチを使っていると述べていらっしゃいます (参照: MySQLConf 09: Mark Callaghan, "This is Not a Web App: The Evolution of a MySQL Deployment at Google")。また、SHOW PROCESSLIST を使った統計的アプローチは商用製品である MySQL Query Analizer のウェブページでも競合手法として比較対象になっています。今回、ちょっと必要になったので、同様のことを行うスクリプト「mprofile」を書いてみました。こんな感じで使います。

SQL クエリをダンプ

 mpdump コマンドを利用して、定期的に実行中の SQL クエリ一覧を MySQL から取得します。デフォルトでは 0.1 秒間隔で 1,000 回サンプリングを行います (つまり、実行に100秒強かかります)。実行間隔やサンプリング回数、MySQL サーバ接続時のパラメータは引数で指定できるとで、詳しくは --help を参照ください。

% mpdump > mpdump.txt

SQL クエリをマージして統計表示

 mpfilter コマンドを使ってクエリの変数を「?」に書き換え、mpreport コマンドでクエリを負荷順にソートして表示します。mpreport は、「実行確率:SQLクエリ」の一覧を実行確率の降順で表示します。「クエリの実行確率」とは、そのクエリが実行されていた確率をパーセントで表示したものです。同一クエリが複数スレッドから実行されていれば、値は 100% を超えることもあります。以下の例は、パストラックの MySQL サーバの負荷を解析してみた結果です。どのようなクエリが一番負荷を与えているか (また、一番重たいクエリが、138.3% の確率=平均 1.383 スレッドによって実行されていること) が一目瞭然です。あとは、この一覧の順に、最適化が可能か、あるいは memcached でキャッシュすべきか、といったことを検討していくことになります。

% mpfilter < mpdump.txt | mpreport | head -100
138.300:select pt_sum(hits,?) as cnt from url inner join counter on url.id=counter.url where (pt_url_compress(?)<=rurlc and rurlc<pt_url_compress(?,?))
99.100:insert into tt select t1.url,cnt_mixed&? as cnt,********** where url.status_code=?
94.200:select id,pt_url_decompress(rurlc) as rurl from url where crawl_cnt<? and crawl_at>=? and crawl_at<?-?*pow(?,crawl_cnt) limit ?
22.000:select id,status_code from url where rurlc=pt_url_compress(?)
14.300:select id,pt_url_decompress(rurlc) as rurl,cnt from (select url,cnt from counter_by_day **********
9.300:insert into counter (url,referer,hits) values (?,?,pt_cnt_add(null,?,?)) on duplicate key update hits=pt_cnt_add(hits,?,?)
7.400:insert ignore into **********
6.100:select pt_sum(hits,?) as cnt from counter where url=?
5.900:select (crawl_at<? and -crawl_at<? and pt_sum(hits,floor(-crawl_at/?))>?) as recrawl from url inner join counter on url.id=counter.url where id=?
5.700:insert into **********
5.700:select t1.id,pt_url_decompress(rurlc) as rurl,cnt,score from **********
4.600:select id,name,dispname from category_ja order by disporder,id
4.300:select addr from **********
3.200:update url set crawl_at=?,lang=?,status_code=if(status_code=?,?,?) where i
d=?
(以下略)

 また、mpfilter を独自のフィルタに差し替えることで、特定のパラメータをもつ SQL クエリを別のグループとして統計に反映する、といったことも簡単に行えるようになっています。

まとめ

 ボトルネックとなっている SQL を解析する手段としては、他にスロークエリログを使う手法 (参考: MySQL 5.1のmysqldumpslowで快速チューニング - SH2の日記) や、MySQL Enterprise Monitor を使うという手もあります。これらと比較して、今回紹介した SHOW PROCESSLIST を使うメリットは、

  • 既存環境の変更が不要
  • サンプリングによるので負荷が軽い
  • スクリプトを組み合わせて、柔軟なチューニングや監視の仕組みを作ることができる

といった点になるかと思います。ボトルネックを解析する以外にも、たとえば apache に付属する rotatelogs と組み合わせて、mpdump 結果が日ごとに異なるファイルに保存されるようにしておけば、SQL クエリの負荷が長期的にどのように変化してきているかを把握して、サーバの増強計画に反映させたりすることもできるでしょう。

 最後になりましたが、今回書いた mprofile のソースコードは /platform/mysql/mprofile – CodeRepos::Share – Trac に置いてあります。それでは、have fun!

July 14, 2009

Intruducing Incline - a synchronization tool for RDB shards

For the last weeks, I have been writing a tool called "Incline," a program that automatically maintains consistency between sharded MySQL databases.  The aim of the software is to free application developers from hand-writing code for keeping consistency between RDB nodes, so that they can concentrate on writing the application logic.

Background

Denormalization is unavoidable in a sharded RDB environment.  For example, when a message is sent from a user to another user, the information should be stored on a database node where the sender of the message belongs to, and on another node where the receiver does.  In most cases, denormalization logic is hand-written by web application developers, and since it has been a burden for creating large-scale web services.  Incline takes off the load from developers.  By reading the definition files, Incline keeps the tables on a sharded MySQL environment in sync, by providing a trigger-generator and a replication program that, synchronously or asynchronously reflects the changes of source tables into materialized-view-like tables.

Installing Incline

Incline is written in C++ and uses autotools for building the tools.  However, since I have not yet added automatic lookup for libmysqlclient, you need to specify their location manually to build Incline.  My build procedure is as follows.

% svn co http://kazuho.31tools.com/svn/incline/trunk incline
% cd incline
% autoreconf -i
% ./configure CFLAGS=-I/usr/local/mysql/include/mysql CXXFLAGS=-I/usr/local/mysql/include/mysql LDFLAGS='-L/usr/local/mysql/lib/mysql -lmysqlclient'
% make
% make install

Defining Replication Rules

Replication rules of Incline are written using JSON files.  Consider creating a twitter-like microblog on a shared environment.  It would be consisted of four tables, each of them distributed through RDB shards by the user_id column.  The "tweet" table and "following" table are updated by user actions, while "followed_by" table and "timeline" table are denormalized tables that need to be kept synchronized to the former two.

CREATE TABLE tweet (
  tweet_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id INT UNSIGNED NOT NULL,
  body VARCHAR(255) NOT NULL,
  ctime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (tweet_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE following (
  user_id INT UNSIGNED NOT NULL,
  following_id INT UNSIGNED NOT NULL,
  PRIMARY KEY (user_id,following_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE followed_by (
  user_id INT UNSIGNED NOT NULL,
  follower_id INT UNSIGNED NOT NULL,
  PRIMARY KEY (user_id,follower_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE timeline (
  user_id INT UNSIGNED NOT NULL,
  tweet_user_id INT UNSIGNED NOT NULL,
  tweet_id INT UNSIGNED NOT NULL,
  ctime TIMESTAMP NOT NULL,
  PRIMARY KEY (user_id,tweet_user_id,tweet_id),
  KEY user_ctime_tweet_user_tweet (user_id,ctime,tweet_user_id,tweet_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Incline uses two JSON files to define a replication rule.  The first file, microblog.json defines the mapping of columns using directives "pk_columns" (primary key columns) and "npk_columns" (non-pk_columns) between the "source" table(s) and "destination" of the replication.  When merging more than two tables to a single destination, "merge_cond" attribute is used to define the inner join condition(s).

microblog.json
[
  {
    "source"      : [ "tweet", "followed_by" ],
    "destination" : "timeline",
    "pk_columns"  : {
      "followed_by.follower_id" : "user_id",
      "tweet.user_id"           : "tweet_user_id",
      "tweet.tweet_id"          : "tweet_id"
    },
    "npk_columns" : {
      "tweet.ctime" : "ctime"
    },
    "merge"       : {
      "tweet.user_id" : "followed_by.user_id"
    },
    "shard-key"   : "user_id"
  },
  {
    "source"      : "following",
    "destination" : "followed_by",
    "pk_columns"  : {
       "following.following_id" : "user_id",
       "following.user_id"      : "follower_id"
    },
    "shard-key"   : "user_id"
  }
]

The second file shard.json defines the mapping between user_id and RDB nodes.  Range-based sharding (on an integer column) is specified in the example.  Other algorithm currently supported are: range-str-case-sensitive and hash-int.

shard.json
{
  "algorithm" : "range-int",
  "map"       : {
    "0"    : "10.0.1.1:3306",
    "1000" : "10.0.1.2:3306",
    "2000" : "10.0.1.3:3306",
    "3000" : "10.0.1.4:3306"
  }
}

With the definition, tables will be synchorized in the direction described in the figure below (only illustrates two nodes).

Running Incline

To run Incline, queue tables should be created and database triggers need to be installed on each RDB node.  This can be done by calling "incline create-queue" and "incline create-trigger" commands for each node, and the setup is complete.

% incline --mode=shard --source=microblog.json --shard-source=shard.json --database=microblog --mysql-host=10.0.1.1 create-queue
% incline --mode=shard --source=microblog.json --shard-source=shard.json --database=microblog --mysql-host=10.0.1.1 create-trigger
% incline --mode=shard --source=microblog.json --shard-source=shard.json --database=microblog --mysql-host=10.0.1.2 create-queue
% incline --mode=shard --source=microblog.json --shard-source=shard.json --database=microblog --mysql-host=10.0.1.2 create-trigger
...

The installed triggers take care of synchronizing the denormalized tables within each node.  The next step is to run the forwarder (replicator between RDB nodes) for each node, so that the views should be kept in sync.  This can be done by calling "incline forward."The forwarding logic is defined in a way that the data stored in RDB nodes would become eventually synchronized even if either of the RDB nodes or the forwarder unexpectedly dies.  Use of daemontools or alike to automatically (re)start the forwarder is desirable.

% incline --mode=shard --source=microblog.json --shard-source=shard.json --database=microblog --mysql-host=10.0.1.1 forward &
% incline --mode=shard --source=microblog.json --shard-source=shard.json --database=microblog --mysql-host=10.0.1.2 forward &
...

And this is it.  The two tables: "followed_by" and "timeline" are updated automatically when the "tweet" or "following" table is modified.  Application developers do not need to take care of shard consistency any more, all that needs to be remembered is that changes should be written into the latter two (modification to the former two can be disabled by using different access privileges between the web application and the Incline forwarder).

Next Steps

Having started writing code late last month, Incline is still immature.  Current status is somewhere around early beta.  In the next months, I plan to polish up and optimize the code, add PostgreSQL support (currently Incline works only on MySQL), as well as extending it so that it could used together with Pacific, a framework I have been working, that provides dynamic node addition / removal of nodes on a sharded RDB environment.  Thank you for reading, please stay tuned.

June 29, 2009

MySQL のトリガーの実用性を確認するために InnoDB の SELECT COUNT(*) を高速化してみる

 最近 RDBMS のトリガーを色々書いているのですが、知らない人にトリガーが何かいちいち説明するのに簡単な例はないかな、というのと、MySQL の処理速度はトリガーによってどの程度変化するか、ということを確認するために、以下のような実験を行ってみました。

 InnoDB はしばしば、「SELECT COUNT(*) が遅い!」と批判されます。では、トリガーを使って行数を別のテーブルにキャッシュすればいいのではないでしょうか? 以下のように、極めて小さなテーブル t1 を作り、その行数を t1_cnt にキャッシュしてみることにします。

mysql> create table t1 (
    ->   id int unsigned not null primary key auto_increment,
    ->   v int unsigned not null
    -> ) engine=innodb;
Query OK, 0 rows affected (0.01 sec)

mysql> create table t1_cnt (
    ->   cnt int unsigned not null
    -> ) engine=innodb;
Query OK, 0 rows affected (0.01 sec)

mysql> insert into t1_cnt values (0);
Query OK, 1 row affected (0.00 sec)

 テーブルを定義し、t1_cnt に行数の初期値 (0) をセットしたら、次にトリガーを登録します。

mysql> delimiter |
mysql> create trigger t1_insert after insert on t1 for each row begin       
    ->   update t1_cnt set cnt=cnt+1;
    -> end|
Query OK, 0 rows affected (0.01 sec)

mysql> create trigger t1_delete after delete on t1 for each row begin
    ->   update t1_cnt set cnt=cnt-1;
    -> end|
Query OK, 0 rows affected (0.01 sec)

mysql> delimiter ;

 試しに t1 テーブルに値を出し入れして、t1_cnt の行数が変化することを確認します。

mysql> insert into t1 (v) values (1),(2),(3);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> select * from t1_cnt;
+-----+
| cnt |
+-----+
|   3 |
+-----+
1 row in set (0.00 sec)

mysql> delete from t1;   
Query OK, 3 rows affected (0.00 sec)

mysql> select * from t1_cnt;
+-----+
| cnt |
+-----+
|   0 |
+-----+
1 row in set (0.01 sec)

 では、トリガーをセットしたことで、パフォーマンスはどれほど劣化するのでしょう。mysqlslap を使って測定してみます。

# トリガーが有効な場合
$ time /usr/local/mysql51/bin/mysqlslap -u root -S tmp/mysql.sock -c 20 -i 1000 -q 'insert into test.t1 (v) values (1)'
Benchmark
Average number of seconds to run all queries: 0.008 seconds
Minimum number of seconds to run all queries: 0.003 seconds
Maximum number of seconds to run all queries: 0.016 seconds
Number of clients running queries: 20
Average number of queries per client: 1


real 0m9.893s
user 0m0.590s
sys 0m1.975s

# トリガーが無効な場合
$ time /usr/local/mysql51/bin/mysqlslap -u root -S tmp/mysql.sock -c 20 -i 1000 -q 'insert into test.t1 (v) values (1)'
Benchmark
Average number of seconds to run all queries: 0.003 seconds
Minimum number of seconds to run all queries: 0.002 seconds
Maximum number of seconds to run all queries: 0.014 seconds
Number of clients running queries: 20
Average number of queries per client: 1


real 0m4.851s
user 0m0.612s
sys 0m1.911s

 トリガーを使うことで、書き込みパフォーマンスが約半分になっていることが確認できます。トリガーを使うことで、1回の INSERT で更新する行数は2倍になっているのですから、この結果は妥当だと考えられます。逆に言うと、トリガーという機能特有のオーバーヘッドは、少なくともこの場合はない (クライアントサイドでがんばってチューニングしなくても、トリガーに頼ってよい) ということになります。

 実際の運用では、テーブルの1行のサイズは、このテストで使った t1 テーブルよりもずっと大きいでしょうし、また、INSERT や DELETE 以外の SELECT や UPDATE 実行時には、今回セットしたトリガーはそもそも実行されないので、行数をカウントすることによるオーバーヘッドは、もっと小さいでしょう。一方で、MySQL のトリガーは行ベース (1行毎に処理を行うタイプ) なので、多数の行を一度に更新するようなクエリを実行する場合は、注意が必要になってきます。

 というわけで、以上、トリガーの紹介と簡単なベンチマークでした。MySQL とトリガーについて詳しく知りたい方は、MySQL :: MySQL 5.1 リファレンスマニュアル :: 18 トリガ等をご参照ください。

 なお、上記ベンチマークは、MySQL 5.1.35 で innodb_flush_log_at_trx_commit=0 に設定して実行しました。

June 25, 2009

Q4M の mutex まわりを整理

 Q4M (Queue for MySQL) の mutex と rwlock のうち主要な部分を、「スレッド間で共有する変数のアクセス権制御を C++ コンパイラで強制する方法」で紹介させていただいたロック手法 (以下 CAC) を使うようリファクタリングしました。変更の過程でバグらしきものは見つかりませんでしたが、なにか未発見のバグが直っていたりする可能性、あるいは品質維持を考えて、今後は CAC ベースのリリースにしていこうと思っています。

 というわけで、非 CAC ベースの最終版 (0.8.6)、および CAC リリース (0.8.7) の最新版のプレリリース (バイナリ版は linux x86_64 のみ) が q4m.31tools.com/dist/pre においてあります。興味のある方はお試しください。しばらく様子を見て問題がなければ、0.8.7 を正式リリースにしたいと思います。

 また、(特に新しいバージョンの) Q4M が一度でも落ちたことがある、という方がいらっしゃいましたら、ぜひ詳細な情報をお寄せいただければと思います。よろしくお願いいたします。

注. 一点、--with-delete=msync の場合に、mysqld 起動後初回の Q4M 書き込み時に1回だけ、ロックが逆転する可能性があったことを確認しました