Hatena::ブログ(Diary)

C/pHeR Memo - Java とか。Eclipse とか。 このページをアンテナに追加 RSSフィード Twitter

2012-10-13

パフォーマンス比較 Cassandra、Mongodb、SQLite、H2、MySQL、Postgres  パフォーマンス比較 Cassandra、Mongodb、SQLite、H2、MySQL、Postgresを含むブックマーク  パフォーマンス比較 Cassandra、Mongodb、SQLite、H2、MySQL、Postgresのブックマークコメント

f:id:cypher256:20121013210623p:image:right

 下記のようなシステムでパフォーマンスが良さげな SQLite を使用予定ですが、もっと速いものが無いか確認のため他のデータベースのパフォーマンスを計測してみました。SQL 利用前提ですが、NoSQL が圧倒的な性能を出す場合検討する必要があるので KVS も確認しました。


  • データ件数は 1 億件程度、JDBC SQL 利用可能
  • INSERT、UPDATE はバッチ
  • SELECT は主キーアクセス性能を重視
  • 将来スケールアウトのための分散はありえるが、スタンドアロンで遅いのはだめ

データベースのパフォーマンス比較

計測したデータベース
データベース名タイプ形態評判計測についての備考
SQLiteRDB組み込み ※2おもちゃ、Android標準JDBC操作 ※1
H2RDB組み込み ※2組み込み最速JDBC操作 ※1
DerbyRDB組み込み ※2Java標準で付属JDBC操作 ※1
EHCacheKVS組み込み ※2OSCacheと2大巨頭1万件ごとにディスク書き込み設定
Redis(Jedis)KVSサーバー ※4爆速Jedis API 操作
MongodbKVSサーバー ※4高パフォーマンスMongodb 標準 API 操作 ※3
CassandraKVSサーバー大手導入事例多数、廃止も多数JDBC操作 (AutoCommit のみ可能)
MySQL(MyISAM)RDBサーバーサーバ型非トランザクションRDB最速JDBC操作 ※1
MySQL(InnoDB)RDBサーバーサーバ型トランザクションRDB最速JDBC操作 ※1
PostgreSQLRDBサーバーMySQLと異なりGPLじゃないJDBC操作 ※1

※1 JDBC 操作に関して INSERT は 1 万件ごとにコミット、SELECT は主キー指定で 1 件ずつ全件取得

※2 組み込みモード (ローカルファイル永続化) のみ計測、インメモリモードやサーバーモードは未計測

※3 全 INSERT 後の ensureIndex によるインデックス作成時間が計測結果に含まれる

※4 遅延書き込み (非同期書き込み) *1


計測結果

環境 CPU Core2 Duo 2GH 2GHz*2、メモリ 4GB、Windows XP 32bit*3、HDD (TOSHIBA MK8052GSX)*4、しょぼめのノートパソコン

データ 主キー:数値、値:文字列、レコード長:約 200 byte

スレッド数 1


線形的増加と指数関数的増加、臨界点

 データベースの処理数に対する処理時間は、上記結果の MySQL に見られるような線形的増加 (リニア、直線的) に増加するパターンと、Cassandra のように指数関数的 (雪ダルマ式) に増大するパターンがあります。また、線形的増加から指数関数的増加に移行する臨界点や動作不能になる臨界点がある場合が多いです。1 万件での処理時間はこうだから 1 億件の予想処理時間はその 1 万倍というような情報をたまに見かけますが、件数が増大するほど線形的増加ではないほうが多いと思います。


SQLite が予想以上に他を圧倒し高速

f:id:cypher256:20121013205423g:image:w190:right

 SQLite が INSERT、SELECT とも予想以上に他を圧倒し高速でした。計測結果に示すとおり 1 億件程度のデータ (データファイルサイズは 1.8 GB でした) なら大丈夫そうです。H2 のサイトに H2 が最速とするパフォーマンス比較 (H2) がありますが SQLite はトランザクションがテストされていないという理由で比較されていません。ちなみに実際には SQLite はトランザクションをサポートしていて、テストに関しても SQLite 本体コード 6万7000行 に対しテストコードは 4567万8000行 (publickey) だそうです。古い情報ですが、SQLiteを使うべき10の理由と5つのデメリット (CAP-LAB テクニカル) が色々参考になります。


 SSD で試してみたところ、1 億件 INSERT 647秒、1 億回 SELECT 383秒 でした。さすがに速い。SSD 環境は CPU i5 2.4GH、メモリ 8GB、Windows 7 64bit*5、SSD (TOSHIBA THNSNC128GMMJ)*6、これもノートパソコンです。


件数が増えると EHCache が想定以上に遅い

f:id:cypher256:20121013205424p:image:w220:right

 EHCache は 1 万件取得で最速ですが、件数が増えると組み込み RDB の SQLite や H2 より遅いのは良い?にしても、クライアント・サーバー型 KVS の Mongodb や Redis より総合的に少し遅いのは予想外でした。overflowToDisk の設定にもよると思いますが、知らずに H2 や Mongodb のキャッシュとして EHCache を使ってまいそうです。すべてディスクに永続化するように設定していたのですが、100万件テストでは登録したデータを取得しようとしたときに欠落している場合があるため計測しませんでした。ところでロゴを見て気づいたのですが EHCache が回文になっているのを今初めて知りました。


Mongodb がクライアント・サーバー型としては登録性能に優れている

f:id:cypher256:20121013205425p:image:w220:right

 最速の SQLite と比較すると登録は 6 倍かかり、取得は 20 倍かかりますが、クライアント・サーバー型としては最も登録性能に優れています。ただし、分散環境ではデータがときどき消える、マルチコアでスケールしない (InfoQ)、などに注意する必要があります。もちろん、これらの問題は将来解消されるかもしれません。


NoSQL、KVS の代表とも言える Apache Casandra がとんでもなく遅い

f:id:cypher256:20121013205426p:image:w250:right

 分散 KVS をスタンドアロンの組み込み RDB と比較するのもあれですが、テストした環境での 100万件 SELECT は SQLite の 200 倍以上遅いです。分散によるネットワーク負荷係数を 2 とした場合、サーバーを 400 台程度用意すれば SQLite と同等の性能を出せるかもしれません。一時期 Twitter や Facebook が Cassandra をメインに使用することを検討していましたが断念しました。ちなみに Cassandra を元々開発したのは Facebook です。


NoSQL、KVS が終焉

f:id:cypher256:20121013205427p:image:w190:right

 NoSQL が終焉したと言われて久しいですが、Google のインフラ基盤や Facebook の HBase (Hadoop) で HBase (Hadoop データベース)*7バリバリ使われていて、Mixi では永続化機能付き memcached とも言える TokyoTyrant が使用され、DeNA では MySQL に HandlerSocket を組み込み NoSQL を実現しています。必要な場面があるので今後も NoSQL や KVS が無くなることはなく、逆に RDB も衰退することはありません。Twitter は今でも MySQL + memcached ですし、GAE では MySQL も用意されています。また、NoSQL に対して SQL が遅いというアンチテーゼとして、Postgres の設計者による分散インメモリ RDB の VoltDB が登場しています。


 業務システムから見ると SQL はそのままでスケールアウトしたいという要望に単純な NoSQL や KVS は合致しません。元々 RDB がスケールアウトによりリニアにスケールしない問題や耐障害性を解決するために Google の BigTable などにインスパイアされ登場したのが分散 KVS ですが、現在は RDB に NoSQL や KVS の要素が取り込まれています。例えば、MySQL 5.6 では memcached や HandlerSoclet などの概念が取り込まれ SQL なしで高速アクセス可能になっています。また MySQL Cluster や Oracle Exadata は分散 KVS と同じようにリニアにスケールアウト可能になっており、Postgres も 32 コア CPU 対応などスケールアップ可能になっています。


追加 2012/10/19

計測結果についての補足

 計測結果はあくまでも私が必要な環境と条件の計測結果であり、id:matsumoto_r さんがおっしゃられているとおりデータベースそのものの優劣を示しているものではありません。並行アクセスしたらとか、設定変えたらとか、たくさんご意見いただきましたが、もちろん、負荷条件を変えたりそれぞれ環境に合わせてチューニングすれば結果は変わると思います。今回の想定システムでは並行アクセスはほぼ発生せず意味がないのでマルチスレッドでの計測はせず、シングルスレッドでのデータ永続化の基本性能を確認するために実施しました。並行処理が得意(前提)とされるデータベースをシングル構成で計測に追加したのは差を確認するためです。並行性能の情報はたくさん公開されていると思いますが、ご自分の環境に合う方法で計測していただければと思います。


 なお、ソフトウェアやドライバはすべて最新安定版、設定はデフォルトです。ただし、全データのディスク同期が前提の計測であるため、EHCache はインメモリではなくディスク永続化モードです。ちなみに SQLite はもう少し触ってみたところ Beta 版にすると 20% 高速化、非同期モードにするとさらに 10% 高速化しました。


計測になぜ Oracle が含まれてないの?

私は臆病者なので良い結果を出さないと Oracle に怒られる気がするからです。OTN ライセンスに下記の条項が含まれています。

オラクルの事前承諾なく、プログラムのベンチマークテストの結果を開示すること。

OTN開発者ライセンス

蛇足ですが、DB2 は設定を完璧にして最新パッチをあてて最高性能を出さないと怒られるかもしれません。

(A) ベンチマーク・テストで使用した方法 (例えば、ハードウェアおよびソフトウェアのセットアップ、導入手順および構成ファイル) を公開し、

(B) 「プログラム」に対する IBM または IBM 製品を提供する第三者 (以下「第三者」といいます。) から提供される最新の適用可能な更新、パッチ、修正が適用された所定の稼働環境で 「プログラム」を実行してベンチマーク・テストを行い

(C) 「プログラム」の資料ならびに IBM がサポートする「プログラム」用の Web サイトで提供されているすべてのパフォーマンス・チューニングおよび最良の方法に従うこと

IBM LI docs and SaaS terms - view LI document

計測ソース*8

 今回の計測対象のメインとなる JDBC のソースはこちらになります。INSERT のあとの SELECT なのでキャッシュ云々の話もありますが、それも含めて各データソースに対して同じ操作をしています。SQLite のみ Class.forName しているのはドライバがサービスプロバイダーフレームワークに対応していないためです。(bitbucket リポジトリの最新ソースでは 2012/09 対応)

package test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;

import org.junit.After;
import org.junit.Test;

public class JdbcTest {

    @Test
    public void sqlite() throws Exception {
        Class.forName("org.sqlite.JDBC");
        con = DriverManager.getConnection("jdbc:sqlite:test.sqlite3");
        Statement st = con.createStatement();
        executeUpdate(st, "drop table if exists person");
        executeUpdate(st, "create table person (id integer primary key, name string)");
        executeQuery();
    }

    @Test
    public void h2() throws Exception {
        con = DriverManager.getConnection("jdbc:h2:testh2", "sa", "");
        Statement st = con.createStatement();
        executeUpdate(st, "drop table if exists person");
        executeUpdate(st, "create table person (id integer primary key, name varchar)");
        executeQuery();
    }

    @Test
    public void derby() throws Exception {
        con = DriverManager.getConnection("jdbc:derby:derby;create=true");
        Statement st = con.createStatement();
        executeUpdate(st, "drop table person");
        executeUpdate(st, "create table person (id int primary key, name varchar(200))");
        executeQuery();
    }

    @Test
    public void mysql_myisam() throws Exception {
        mysql("MyISAM");
    }
    @Test
    public void mysql_innodb() throws Exception {
        mysql("InnoDB");
    }
    private void mysql(String engine) throws Exception {
        con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "");
        Statement st = con.createStatement();
        executeUpdate(st, "drop table if exists person");
        executeUpdate(st, "create table person (id integer primary key, name varchar(200)) engine = " + engine);
        executeQuery(con.getMetaData().getDatabaseProductName() + "(" + engine + ")");
    }

    @Test
    public void postgres() throws Exception {
        con = DriverManager.getConnection("jdbc:postgresql:postgres", "postgres", "postgres");
        Statement st = con.createStatement();
        executeUpdate(st, "drop table if exists person");
        executeUpdate(st, "create table person (id integer primary key, name varchar)");
        executeQuery();
    }

    @Test
    public void cassandra() throws Exception {
        con = DriverManager.getConnection("jdbc:cassandra://localhost:9160/test");
        Statement st = con.createStatement();
        executeUpdate(st, "drop table person");
        executeUpdate(st, "create table person (id int primary key, name text)");
        executeQuery();
    }

    // 共通メンバー -------------------------------------------

    private Connection con;
    private static final int COUNT = 10000 * 10;
    private static final String DATA = "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";

    @After
    public void after() throws Exception {
        if (con != null) {
            con.close();
        }
    }

    private void executeUpdate(Statement st, String sql) {
        try {
            st.executeUpdate(sql);
        } catch (Exception e) {
            System.out.println(e.toString());
        }
    }

    private void executeQuery() throws Exception {
        executeQuery(con.getMetaData().getDatabaseProductName());
    }

    private void executeQuery(String databaseName) throws Exception {

        boolean isCassandra = databaseName.contains("Cassandra");
        boolean isAutoCommit = isCassandra;

        System.out.printf("%-14s", databaseName);
        if (!isAutoCommit) {
            con.setAutoCommit(false);
        }

        long insertStart = System.currentTimeMillis();
        PreparedStatement insertPs = con.prepareStatement("insert into person (id, name) values(?, '" + DATA + "')");
        for (int i = 0; i < COUNT; i++) {
            insertPs.setInt(1, i);
            insertPs.executeUpdate();
            if (!isAutoCommit && i % 10000 == 0) {
                con.commit();
            }
        }
        if (!isAutoCommit) {
            con.commit();
        }
        double insertSec = (double) (System.currentTimeMillis() - insertStart) / 1000;

        long selectStart = System.currentTimeMillis();
        PreparedStatement selectPs = con.prepareStatement("select * from person where id = ?");
        for (int i = 0; i < COUNT; i++) {
            selectPs.setInt(1, i);
            selectPs.executeQuery().next();
        }
        double selectSec = (double) (System.currentTimeMillis() - selectStart) / 1000;

        String countSql = "select count(1) from person";
        if (isCassandra) {
            countSql += " limit 100000000";
        }
        ResultSet rs = con.createStatement().executeQuery(countSql);
        rs.next();
        logProcessTime(rs.getInt(1), insertSec, selectSec);
    }

    private void logProcessTime(long count, double insertSec, double selectSec) {

        System.out.printf("%4d万件 ", count / 10000);
        System.out.printf("%7.2f秒 ", insertSec);
        System.out.printf("%7.2f秒", selectSec);
        System.out.println();
    }
}

*1:2012/10/20 1:00 @kimutansk さんの指摘により追記

*2:2012/10/14 11:57 @showyou さんの指摘により修正

*3:2012/10/14 12:15 @showyou さんの指摘によりビット数追記

*4:2012/10/15 23:51 @showyou さんの指摘により追記

*5:2012/10/14 12:15 @showyou さんの指摘によりビット数追記

*6:2012/10/14 12:15 @showyou さんの指摘により型番追記

*7:2012/10/14 11:57 @shiumachi さんの指摘により修正

*8:2012/10/19 22:50 id:ysobj さんの指摘により追記

通りすがり通りすがり 2012/10/14 18:26 大変興味深く読ませていただきました。
> NoSQL が終焉したと言われて久しいですが
これについて参考になる記事などあれば教えていただきたいのですが、
よろしくお願いします。

cypher256cypher256 2012/10/14 23:33 InfoQ:NoSQL、NewSQL、そしてその先
http://www.infoq.com/jp/news/2011/04/newsql

Publickey:リレーショナルデータベースはNoSQLを取り込み始めた。NewSQLの登場とNoSQLの終わり、という予想
http://www.publickey1.jp/blog/11/nosqlnewsqlnosql.html

ご参考:こっちは Oracle が NoSQL に本気でぶつけてきた? NoSQL 分散 KVS
@IT:オラクルが作るNoSQL DBの正体は?
http://www.atmarkit.co.jp/fdb/rensai/dbwatch2011/dbwatch201111_01.html

celebrityfighterjcelebrityfighterj 2012/10/15 00:43 ぼくは面倒くさい比較とか技術検証とかしたくないので google app engine high replication datastore 一択です!
アウトソーシングバンザイ!

ysobjysobj 2012/10/18 01:19 とても参考になりました(思ったよりH2は性能劣化するんだなあとか)
もしよろしければ、実際の検証コードがどんな感じだったのか掲載していただけないでしょうか。

igapyonigapyon 2012/10/19 10:45 Good Job!

cypher256cypher256 2012/10/19 23:14 > ysobj
H2 おっかけの方ですよね。ご参考になれば幸いです。
検証対象の JDBC 操作のソースを追加しました。

cypher256cypher256 2012/10/19 23:15 > igapyon
ありがとうございます!

ysobjysobj 2012/10/20 00:06 検証コード、ありがとうございます。H2おっかけの人でした。

otasuketyanotasuketyan 2014/01/15 08:45 参考になりました。ありがとうございます

otasuketyanotasuketyan 2014/01/15 08:45 参考になりました。ありがとうございます

リンク元