Movatterモバイル変換


[0]ホーム

URL:


cypher256's blog

Pleiades とか作った

パフォーマンス比較 Cassandra、Mongodb、SQLite、H2、MySQL、Postgres


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

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

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

計測したデータベース
データベース名タイプ形態評判計測についての備考
SQLiteRDB組み込み ※2おもちゃ、Android標準JDBC操作 ※1
H2RDB組み込み ※2組み込み最速JDBC操作 ※1
DerbyRDB組み込み ※2Java標準で付属JDBC操作 ※1
EHCacheKVS組み込み ※2OSCacheと2大巨頭1万件ごとにディスク書き込み設定
Redis(Jedis)KVSサーバー ※4爆速JedisAPI 操作
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

※1JDBC 操作に関して INSERT は 1 万件ごとにコミット、SELECT は主キー指定で 1 件ずつ全件取得
※2 組み込みモード (ローカルファイル永続化) のみ計測、インメモリモードやサーバーモードは未計測
※3 全 INSERT 後の ensureIndex によるインデックス作成時間が計測結果に含まれる
※4 遅延書き込み (非同期書き込み)*1

計測結果

環境 CPUCore2 Duo2GH 2GHz*2、メモリ 4GB、Windows XP 32bit*3、HDD (TOSHIBA MK8052GSX)*4、しょぼめのノートパソコン
データ 主キー:数値、値:文字列、レコード長:約 200 byte
スレッド数 1

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

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

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


 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*5SSD (TOSHIBA THNSNC128GMMJ)*6、これもノートパソコンです。

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


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

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


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

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


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

NoSQL、KVS が終焉


 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 がスケールアウトによりリニアにスケールしない問題や耐障害性を解決するためにGoogleBigTable などにインスパイアされ登場したのが分散 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 サイトで提供されているすべてのパフォーマンス・チューニングおよび最良の方法に従うこと

SLA - L-JWOG-6K4JSQ
計測ソース*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;publicclass JdbcTest {@Testpublicvoid 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();    }@Testpublicvoid 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();    }@Testpublicvoid 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();    }@Testpublicvoid mysql_myisam()throws Exception {        mysql("MyISAM");    }@Testpublicvoid mysql_innodb()throws Exception {        mysql("InnoDB");    }privatevoid 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 +")");    }@Testpublicvoid 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();    }@Testpublicvoid 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;privatestaticfinalint COUNT =10000 *10;privatestaticfinal String DATA ="12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";@Afterpublicvoid after()throws Exception {if (con !=null) {            con.close();        }    }privatevoid executeUpdate(Statement st, String sql) {try {            st.executeUpdate(sql);        }catch (Exception e) {            System.out.println(e.toString());        }    }privatevoid executeQuery()throws Exception {        executeQuery(con.getMetaData().getDatabaseProductName());    }privatevoid 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);    }privatevoid 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:50id:ysobj さんの指摘により追記

検索

引用をストックしました

引用するにはまずログインしてください

引用をストックできませんでした。再度お試しください

限定公開記事のため引用できません。

読者です読者をやめる読者になる読者になる

[8]ページ先頭

©2009-2025 Movatter.jp