【記事途中】各種Databaseの性能を Apache Bench でいろいろと比較【MySQL, MemSQL, Redis, MongoDB】

まだ途中の記事です。
いろいろなDBを追加していきます。

長い長い記事なのでまとめ↓

安定性: MySQL >= MongoDB >> MemSQL = Redis
高速性: MongoDB > Redis > MySQL > MemSQL
使うならMySQLかMongoDBですね(もしくはどちらも)。Redisは安定性がなさすぎて、MemSQLは早くもなければ安定でもないという誰得状態です(私のベンチのとり方が悪いのかな?)

※個人の検証です


☆ 動機

DBというものは複数からアクセスがあって初めてその性能が分かるものでしょう。

【まだ途中】SQLite, Redis の Insert 比較 (By PHP)【ツッコミ歓迎】 - 自分用備忘録
↑先日このように比較しましたが、自分で比較しておいて何ですがあまり参考にならないかと思いました。なぜなら先日のものは単一のアクセスだからです。一般的にデータベースは同時にたくさんのアクセスがくるために作られているので(ほんとう?)、不適切な比較だったかもしれません。

そんなわけで、Apache BenchでURLを叩いて比較してみようと思いました。これで本当の性能が分かることを祈りつつ。。。


☆ 環境

  • 鯖A (192.168.0.100、アプリケーション、データベース)
    • Pentium Dual Core E5200
    • DDR2-800 2GB * 2
    • Crucial m4 SSD 64GB
    • P5KPL-CM
    • GbE (AR8121/AR8113 ?)
    • CentOS 6.3
    • Apache/2.2.15 (Unix) <- httpd.conf は、access_logを出力しないようにしただけでほかはデフォルト。
    • MySQL 5.1.66, for redhat-linux-gnu (x86_64) using readline 5.1
    • MemSQL 5.5.8
    • Redis 2.6.7 (by gcc version 4.4.6)
    • MongoDB v2.0.7, pdfile version 4.5
    • phpredisを使ってredis-serverにアクセス
    • php
# php -v
PHP 5.3.3 (cli) (built: Jul  3 2012 16:53:21) 
Copyright (c) 1997-2010 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2010 Zend Technologies
    with Xdebug v2.1.4, Copyright (c) 2002-2012, by Derick Rethans

# php -m
[PHP Modules]
apc
bz2
calendar
Core
ctype
curl
date
dba
dom
ereg
exif
fileinfo
filter
ftp
gd
gettext
gmp
hash
iconv
igbinary
json
libxml
mbstring
mcrypt
mongo
mysql
mysqli
odbc
openssl
pcntl
pcre
PDO
pdo_mysql
PDO_ODBC
pdo_pgsql
pdo_sqlite
pgsql
Phar
readline
redis
Reflection
session
shmop
SimpleXML
soap
sockets
SPL
sqlite3
standard
tokenizer
uuid
wddx
xdebug
xml
xmlreader
xmlrpc
xmlwriter
xsl
zip
zlib

[Zend Modules]
Xdebug
  • クライアント (192.168.0.150、鯖AにApache Benchをかける用途)


☆ 接続とRTT
鯖A <= GbE => NECルーター(GbEAterm WR8700N) <= GbE => クライアント

# 鯖A -> クライアント のPING
[root@e5200]
# ping 192.168.0.150
PING 192.168.0.150 (192.168.0.150) 56(84) bytes of data.
64 bytes from 192.168.0.150: icmp_seq=1 ttl=64 time=0.098 ms
64 bytes from 192.168.0.150: icmp_seq=2 ttl=64 time=0.107 ms
64 bytes from 192.168.0.150: icmp_seq=3 ttl=64 time=0.118 ms
^C
--- 192.168.0.150 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2723ms
rtt min/avg/max/mdev = 0.098/0.107/0.118/0.014 ms

# クライアント -> 鯖A のPING
[root@g530: ~]
# ping 192.168.0.100
PING 192.168.0.100 (192.168.0.100) 56(84) bytes of data.
64 bytes from 192.168.0.100: icmp_seq=1 ttl=64 time=0.101 ms
64 bytes from 192.168.0.100: icmp_seq=2 ttl=64 time=0.137 ms
64 bytes from 192.168.0.100: icmp_seq=3 ttl=64 time=0.099 ms
^C
--- 192.168.0.100 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2948ms
rtt min/avg/max/mdev = 0.099/0.112/0.137/0.019 ms

# 鯖A -> 鯖A のPING(localhost) (DBが起動してListen状態のデータ)
[root@e5200: ~]
# ping localhost
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.024 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.042 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.030 ms
^C
--- localhost ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2428ms
rtt min/avg/max/mdev = 0.024/0.032/0.042/0.007 ms

# クライアント -> クライアント のPING(localhost)
[root@g530: ~]
# ping localhost
PING localhost.localdomain (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=64 time=0.023 ms
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=2 ttl=64 time=0.029 ms
64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=3 ttl=64 time=0.027 ms
^C
--- localhost.localdomain ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2483ms
rtt min/avg/max/mdev = 0.023/0.026/0.029/0.004 ms


☆ 初期状態

[root@g530: ~]
# curl http://192.168.0.100/benchmarks/static_file.html
counter: 12345

[root@g530: ~]
# curl http://192.168.0.100/benchmarks/memsql.php
counter: 307636

[root@g530: ~]
# curl http://192.168.0.100/benchmarks/mysql.php
counter: 990695

[root@g530: ~]
# curl http://192.168.0.100/benchmarks/phpredis.php
counter: 999832

[root@g530: ~]
# curl http://192.168.0.100/benchmarks/mongodb.php
counter: 547848


☆ DBのテーブル・構造

# MySQL・MemSQL
mysql> desc incr;
+--------+-------------+------+-----+---------+-------+
| Field  | Type        | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| key1   | varchar(16) | YES  |     | NULL    |       |
| number | int(11)     | YES  |     | NULL    |       |
+--------+-------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

# Redis
redis 127.0.0.1:6379> get counter
"256598"

# MongoDB
> db.benchmarks.insert({"name":"key1", "number":1})
> db.benchmarks.update({"name":"key1"}, {"$inc": {"number":1} })
> db.benchmarks.find();
{ "_id" : ObjectId("50c8dc945139fe0dcad11646"), "name" : "key1", "number" : 11 }


☆ 計測方法
Apache Benchという、HTTPサーバーの性能を調べるソフトウェアを使って、様々なDBが単位時間あたりにどのぐらいのRequestをさばけるか調査しました。インクリメントするベンチマークです。具体的なコマンドは、

# ab -n 10000 -c 10 http://192.168.0.100/benchmarks/phpredis.php

です。クライアント(192.168.0.150)のターミナルから、↑のコマンドを打ち鯖A(192.168.0.100)がそれを捌きます。n: リクエスト数、c: 同時接続数らしいです。nは固定で、cを変えて計測します(Apache Benchについてよく分かっていないのでアレですが、↑の例だと10同時に接続して、合計1000回のリクエストになるまで計測するという意味なのでしょうか?誰か教えてください><)。リクエスト数は1万で固定、同時接続数をいろいろと変えて試してみます。ベンチ後、Failed Requestが出ていたら、topコマンドで確認してapacheのプロセスが残っていたらapache再起動して一度だけcurlでアクセスします。また、異なるDBを使う前にはやはりApacheを再起動して一度だけcurlでアクセスしてから計測しています。計測回数は1回だけです。apache benchなのかサーバーのapacheが原因なのか分かりませんが、同時接続数が250を超えてくるとエラーが出たりするようになりますのでそこで打ち止めにしています。”apr_socket_recv: Connection reset by peer (104)" ←こういうエラーが出たりします。

参考のために、静的なhtmlファイルも測定しました。

レコードが極めて小さいので、インメモリ処理の速度のベンチマークになっていますが。。。

MySQLですがMyISAMを使っていました。

mysql>show table status\G
*************************** 1. row ***************************
&#160; &#160; &#160; &#160; &#160; &#160;Name: incr
&#160; &#160; &#160; &#160; &#160;Engine: MyISAM
&#160; &#160; &#160; &#160; Version: 10
&#160; &#160; &#160;Row_format: Dynamic
&#160; &#160; &#160; &#160; &#160; &#160;Rows: 1
&#160;Avg_row_length: 20
&#160; &#160; Data_length: 20
Max_data_length: 281474976710655
&#160; &#160;Index_length: 1024
&#160; &#160; &#160; Data_free: 0
&#160;Auto_increment: NULL
&#160; &#160; Create_time: 2012-12-13 00:36:56
&#160; &#160; Update_time: 2012-12-13 23:32:54
&#160; &#160; &#160;Check_time: NULL
&#160; &#160; &#160; Collation: utf8_general_ci
&#160; &#160; &#160; &#160;Checksum: NULL
&#160;Create_options:&#160;
&#160; &#160; &#160; &#160; Comment:&#160;
1 row in set (0.00 sec)

☆ 結果

同時接続数 1 (ab -n 10000 -c 1 http://192.168.0.100/benchmarks/***.php)

DB名 Request Per Sec [#/sec]
静的ファイル 2575.24
MemSQL 786.83
MySQL 972.82
Redis 958.11
MongoDB 997.91



同時接続数 5 (ab -n 10000 -c 5 http://192.168.0.100/benchmarks/***.php)

DB名 Request Per Sec [#/sec]
静的ファイル 9072.13
MemSQL 1935.80
MySQL 2137.21
Redis 2446.51
MongoDB 2964.15



同時接続数 10 (ab -n 10000 -c 10 http://192.168.0.100/benchmarks/***.php)

DB名 Request Per Sec [#/sec]
静的ファイル 14391.80
MemSQL 1861.21
MySQL 2071.94
Redis 2411.20
MongoDB 2935.10



同時接続数 25 (ab -n 10000 -c 25 http://192.168.0.100/benchmarks/***.php)

DB名 Request Per Sec [#/sec]
静的ファイル 13811.39
MemSQL 1880.41
MySQL 2035.98
Redis 2430.54
MongoDB 2921.33



同時接続数 50 (ab -n 10000 -c 50 http://192.168.0.100/benchmarks/***.php)

DB名 Request Per Sec [#/sec]
静的ファイル 13505.19
MemSQL 1850.73
MySQL 1986.39
Redis 2378.09
MongoDB 2904.16



同時接続数 75 (ab -n 10000 -c 75 http://192.168.0.100/benchmarks/***.php)

DB名 Request Per Sec [#/sec]
静的ファイル 13720.13
MemSQL 1868.24
MySQL 2057.14
Redis 2438.65
MongoDB 2893.70



同時接続数 100 (ab -n 10000 -c 100 http://192.168.0.100/benchmarks/***.php)

DB名 Request Per Sec [#/sec]
静的ファイル 13178.53
MemSQL 1868.09
MySQL 2038.22
Redis 2422.57
MongoDB 2894.46



同時接続数 250 (ab -n 10000 -c 250 http://192.168.0.100/benchmarks/***.php)

DB名 Request Per Sec [#/sec] 備考
静的ファイル 7024.53 Failed Requestこそないものの、ガクンと遅くなりました。同時接続数が多すぎる感です
MemSQL 1590.09 httpdが発生していました。
MySQL 1595.75 httpdが発生していました。
Redis 1600.28 やはりhttpdが出てき始めました。
MongoDB 1600.16 ここでもhttpdが発生

# 同時接続数250あたりからエラーが頻発しはじめたのでここで打ち止めにします。

おまけ: リクエスト数10万、同時接続数50でやってみました。具体例-> "ab -n 100000 -c 50 http://192.168.0.100/benchmarks/***.php"。各DBの測定前に毎回 httpd を再起動しています。

DB名 Request Per Sec [#/sec] Failed requests [%] 備考
静的ファイル 13927.32 0 単純なhtmlはそりゃまあ高速ですね
MemSQL 642.98 19.7 エラー率が20%近いですね
MySQL 2001.63 0 Failed requestsなし!さすが安定のMySQL
Redis 686.96 19.3 Failed requestが20%!/(^o^)\。ただ、phpredisを使っている影響があるかも?
MongDB 2822.92 0 エラーなし、そしてこの速度!

☆ 総評

まずは計測の感想から。同時接続数が250を超えてくるとエラーが出始めます。何が原因かいまいちわかっていません。同時接続数は、OS、ApacheNICになどにより左右されそうですので。。。

以下、それぞれについて書いていきます。

静的なファイル: 単なるhtmlです。このシステムでの限界性能が測れるはず、です。10K req/sec 以上出ていますね。これを100K とかにしたいところなのですがどうすればいいのでしょうね。。。

MemSQL: MySQLのタイポではなく、MemSQLです。MySQLをインメモリデータベースにしてみたよー、というコンセプトらしいです。http://www.memsql.com/#download ←ここでフォームを入力してダウンロードできます。割と期待していましたが、(´・ω・`)ガッカリ…な速度でした。インメモリなのは結構なのですが昨今のRDBはデータがメモリに載るならほぼ間違いなくインメモリで動くので、優位性がなさそうですね。ただ、MemSQLの実行時に、

WARN: SSE4.2 is not supported. Resorting to software CRC32C.
Warning: The recommended configuration for MemSQL is at least 8 GB of RAM

と言われました。今回、DBサーバーはCore2時代のPentium Dual Core、E5200ですのでSSE4.2に対応していないのでしょう。また、DDR2-800の4GBDRAMしか詰んでいません。最新のIvyBridgeを使い、DDR3-2400のメモリを4channelにすればだいぶ結果は異なってくるかも知れません(たぶん他のDBも同様に早くなりますがね!)。

MySQL(MyISAM): いろんなところで使用されているだけあってそれなりの速度です。↑のおまけの測定結果を見てもらうと分かると思いますが、非常に安定性が高いです。むちゃなリクエストを出しても何とかしてくれます。本や資料、サポート会社がたくさんありますので、DB選択の本命と言えるでしょう。InnoDBのときも比較したいですね。

Redis: MySQLと比較して+20%の速度ですね。なかなかです。しかし安定性にかなり疑問。。。Failed Request出まくり。。。

MongoDB: この中で一番高速です。MySQL(これは5.1ですが)より50%近く高速です。人気があるだけあるなぁという印象です。安定性も高く、RDBが嫌でNoSQLを使いたいならMongoDBが良さそうです。ちなみに、最大で 600KB / s 程度のTransfer Rateになっていました。DB鯖だと、10MbpのNICで足りるのかもしれませんね(最新のNICのRTTなどは改善されている可能性は十分あるので何とも言えませんが)。

まとめ:
安定性 MySQL >= MongoDB >> MemSQL = Redis
高速性 MongoDB > Redis > MySQL > MemSQL
使うならMySQLかMongoDBですね(もしくはどちらも)。Redisは安定性がなさすぎて、MemSQLは早くもなければ安定でもないという誰得状態です(私のベンチのとり方が悪いのかな?)

追記@2012年12月14日

何となく鯖Aとクライアント(Bとします)を入れ替えてみました。そしたら、MongoDBやRedisが、4000 req/sec を超えてきました。静的ファイルだと16512.85 req/sec も出ています。MySQL(MyISAM)だと2911.94!。50%近く向上しています。うーん、鯖AはCore2時代のCPUでDDR2-800、クライアントBは、Sandy世代のCeleron G530でDDR3-1066で、そこまでスペックに差がある感じではないのですが。。。ただ、Sandy世代ということでCPU性能の向上、キャッシュへのヒット率の向上、メモリがちょっぴり高速、あたりが原因かもしれません。

ちなみにi5-3570Kを持っていてWindowsを入れているのですが、こちらでも実験したくなって来ました。MongoDBで6000 req/secぐらい行くのかも? また、研究室にSandyBridge-Eなi7があるのでそれで検証してみたいですね。L2キャッシュてんこ盛りなCPUだとすごいことになるのではと思ったり。

また、MySQLのPDOで、

$pdo = new PDO($mysql, $user, $pass, array(PDO::ATTR_PERSISTENT => true));

というものを使ってみました。コネクションが保持されるらしいです。使いまわしするみたいですね。たしかにいちいち切断するのは無駄ですね。

接続プーリング OFF↓

# ab -n 100000 -c 10 http://192.168.0.100/benchmarks/mysql.php
Requests per second:    2007.30 [#/sec] (mean)


接続プーリング ON ↓

# ab -n 100000 -c 10 http://192.168.0.100/benchmarks/mysql2.php
Requests per second:    2518.83 [#/sec] (mean)

ひゃー、30%近く高速化してしまった!すごいですね!効果絶大。MySQL最強フラグな気がしないでもないですね(MyISAMではなくInnoDBも試さねば。。。)


まとめ:
測定に用いたデータ(レコード)がメモリで収まることに留意しつつも、MySQLが予想外に早いです。phpredisは+20%ぐらい早く見えると思いますがとにかく安定性が悪いです。かなり苦労しました。しょっちゅうFailed requestsを出してきます。リクエスト件数が多くても同時接続数が多くても出ます。同じ条件でも出る時と出ない時があります。一方でMySQLは総じて安定しています。MongoDBもかなり安定しています。

ソースを見てもらえると分かると思いますが、phpredisの方は、incr一回、MySQLの方はクエリ二回なので明らかにMySQLが不利だろうと思っていましたが、結果的にはかなり均衡していました。

とにかく安定性が段違いですし、速度もそう変わらないので、RedisとMySQLだけを比較すればMySQLを使うべきなのではないかと感じました。

MongoDBはかなり高速ですね。3000req/sec近いのは魅力的です。

書いていて思いましたが、メモリにおさまるデータサイズならどのDBも2000req/sec程度出そうな雰囲気です。だからどれを使ってもいいんじゃないでしょうか(ぉ。安定性重視だとやはりMySQLですね。RedisはインメモリDBだから高速!と思っていたら他のDBもメモリにデータがおさまるならメモリアクセスしちゃうので実質インメモリDB状態だったでござるの巻ですね。

ちなみに、クライアント側のNICIntelNIC(PCI-Ex版の一番安いもの)に変えてみると、+10%程度になりました。MongoDBで最高3000 req/secを超えました。サーバー側の方も交換して計測してみたいですね。

ToDo:
グラフ化する


☆ 疑問点
localhostでのRTTが0.1msecぐらいということは、秒間に1万request程度しか原理的に出来ない計算になります。さらに鯖A、鯖B間のpingのRTTはもっとあります。そんな状態で2000req/s程度出ていたのは立派な気がします。ただ、巷では、10万req/s行ったぜーなんて話を聞きますが、本当にそんな速度出るのかなと疑問です。NICをたくさん使うとかするのでしょうか。今回は鯖一つにつきNIC一個でしたが。高速化技術を知りたいですね。今回、ルーターを一つ挟んで、すべてGbEで接続して計測しましたが、ネットワークがボトルネックにも思えます。さらに言えばたとえば鯖A内でMySQLを走らせた場合、鯖Aがクライアントからリクエストを貰った時にunix socketでMySQLと通信すると思いますが、そのときのunix socketもネックではないでしょうか(これはおそらく ping localhost のRTTですよね)。

また、PHPで比較するのが適切なのが疑問になって来ました。せめてJavaを使うべきだったかもしれません。

うーん、今回のシステムでは、アプリケーションとDBが一体化しています。tcp/ipを使わないほうが早いだろうと思ってそうしたのですが、アプリケーション・サーバー10台、DBサーバー1台の構成であれば、もっとQuery Per Secが出るのかもしれませんね。。。

☆ ソース

static_file.html

counter: 12345

memsql.php

<?php

try {
        $mysql = "mysql:host=127.0.0.1;port=3307;dbname=MemSQL_tutorial";
        $user = "root";
        $pdo = new PDO($mysql, $user);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        //$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

        $stmt1 = $pdo->prepare("update incr set number = number +1");
        $stmt2 = $pdo->prepare("select number from incr");

        //$pdo->beginTransaction();
        $stmt1->execute();
        $stmt2->execute();
        //$pdo->commit();
        $row = $stmt2->fetch(PDO::FETCH_ASSOC);

        printf("counter: %s\n", $row["number"]);
} catch (PDOException $e) {
        printf("Error: データベース接続に失敗しました。\n");
        print $e->getMessage();
        die();
}
?>

mysql.php

<?php

try {
        $mysql = "mysql:host=localhost;port=3306;dbname=benchmark";
        $user = "bm-user";
        $pass = "bm-user";
        $pdo = new PDO($mysql, $user, $pass);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        //$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);

        $stmt1 = $pdo->prepare("update incr set number = number +1");
        $stmt2 = $pdo->prepare("select number from incr");

        $pdo->beginTransaction();
        $stmt1->execute();
        $stmt2->execute();
        $pdo->commit();
        $row = $stmt2->fetch(PDO::FETCH_ASSOC);

        printf("counter: %s\n", $row["number"]);
} catch (PDOException $e) {
        printf("Error: データベース接続に失敗しました。\n");
        print $e->getMessage();
        die();
}
?>

phpredis.php

<?php

$redis = new Redis();
if ($redis->connect('localhost', 6379)) {
        //print("redis connect OK");
} else {
        die("Connection to Redis Failed.");
}

print "counter: " . $redis->incr("counter") . "\n";

?>

mongodb.php

<?php

try {
        $link = new Mongo('localhost:27017');
        $db = $link->test;
        $col = $db->benchmarks;

        //はじめに追加するときはこれ。一旦加えたらあとはコメントアウト。
        //$doc = array('key' => 'value', 'number' => 1);
        //$col->insert($doc);
        
        //increment
        $col->update(array('key' => 'value'), array('$inc' => array('number' => 1)));
        $obj = $col->findOne();
        printf("counter: %s\n", $obj['number']);
} catch (MongoConnectionException $e) {
        die('error');
}

☆ おまけ

各種DB実行方法:

MemSQL (CentOS)

# MemSQLの公式サイトからバイナリを落としてきます

# ↓ memsqldを実行(DRAMが8GB以上ないとWarning出ますが気にしない)
# ./memsqld --port 3307 -u root

# ↓他の端末で行う、memsqlのcliモードに入る
#  mysql -u root -h 127.0.0.1 -P 3307 --prompt="memsql> "

# データベースとテーブルを作って値を挿入
memsql> CREATE DATABASE MemSQL_tutorial;
memsql> USE MemSQL_tutorial;
memsql> create table incr(key1 varchar(16), number int);
memsql> insert into incr values("value1", 1);
memsql> update incr set number = number +1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
memsql> select * from incr;
+--------+--------+
| key1   | number |
+--------+--------+
| value1 |      2 |
+--------+--------+
1 row in set (0.00 sec)

# memsqldを落とすときは↓
# ps aux | grep mem
root       358  0.2  2.4 1483012 197424 pts/0  Sl+  19:05   0:03 ./memsqld --port 3307 -u root
root       362  0.0  0.0 147204  1920 pts/0    Sl+  19:05   0:00 ./memsqld --port 3307 -u root
root     20633  0.0  0.0 107464   992 pts/1    S+   19:31   0:00 grep --color mem
[root@g530: ~]
# kill 358
[root@g530: ~]
# kill 362