SSDとSATAのベンチマーク比較 第2ラウンド: サーバーアプリケーション

 昨日はBonnie++を用いてクライアントマシンにおけるソリッドステートドライブ(SSD:Solid State Drive)のベンチマーク評価を行い翻訳記事)、同じ予算で複数台のハードディスクを購入するのに比べて1台のSSDを購入することにどれだけメリットがあるかを論じた。今日はSSDのシークタイムが極めて短いことがサーバーにおいてどれだけ有利に働くかを見てみよう。

 SSDの応用例は専らモバイル志向でノートPCのハードディスクをSSDに置き換えることに関心が向けられており、そうした利用形態ではSSDの最大のメリットであるシークタイムの高速性が活かされることはない。シークタイムの短さに関して特にどん欲なサーバーアプリケーションのひとつにリレーショナルデータベースがある。今回テストに用いたSSDはサイズが非常に小さく、データベースのタプルそのものを格納することは多分できないが、インデックスを格納するには十分な大きさである。一般的なインデックスアクセスは、1つのインデックスブロックを読み込み、次のインデックスブロックがどれか確かめ、そのブロックを読み込む、というように行われる。さらに1つのクエリの評価過程で複数のインデックスが使われるなら、インデックスをSSDに格納することで高速化の効果がより顕著に表れる可能性もある。

 リレーショナルデータベースのインデックスをSSDに格納した場合の効果を実証するため、2.2GHzのAMD Athlon X2プロセッサ搭載マシンにハードウェアRAIDカード経由で比較用の6台のサムスン社製750GB SATA HDDを接続し、64ビットFedora 9環境でPostgreSQL 8.3.3を実行した。

 PostgreSQLではテーブル空間(tablespace)という機能によりテーブルまたはインデックスをデータベースオブジェクトとは別のディスクやファイルシステムに格納できる。インデックスを作成する際にtablespace句でインデックスの場所を明示的に指示してやるわけだ。後になってプライマリインデックス用のSSDをアップデートすることになった場合、以前のSSDを一時テーブル用に残してもよいだろう。これらのテーブルは大きなデータセットをソートするとき使われる。

 テスト用のデータセットを探すのは決まって面倒な仕事になる。必要なのは無料で入手でき、数百万規模のタプルを含み、そのデータが誰からも理解されるようなデータセットだ。インデックスをSSDに格納した場合のパフォーマンスを従来のハードディスクによるRAID構成と比較してテストするためには、データセットが十分大きいことが必要で、そうすれば、インデックスの格納場所の違いによるパフォーマンスの変化を測定できるだけの十分な大きさのインデックスをテーブルのいずれかの欄に作れるからである。数ある特性の中でいま相手にしようとしているのは、インデックスを付けるプロセスそのものを効率化するBツリーという仕組みだ。Bツリーでは膨大な数のタプルにインデックスを付けることができ、たった3回か4回のシーク動作でインデックス全体から必要な項目にたどり着いてしまう。

 UCIデータセットは、教師付き機械学習アルゴリズムをテストする目的で作られたデータセット集だ。これらのデータセットの中に非常に多くのタプルを持つものがいくつかあり、それをデータベースに読み込むと中規模のメインテーブルが作られる。たとえば、1990 USA Censusという未加工サンプルを読み込むと、約1GBのテーブルが1つ作られ、出生地(pob: place of birth)欄のインデックスは50MBになる。本稿では、インデックスをハードディスクに格納した場合とSSDに格納した場合についてメインテーブルでいくつかクエリを実行して、そのパフォーマンスをテストした。

 UCIのWebサイトでは、この人口調査データを未加工のタブ区切り形式のテキストファイルとして配布している。使用したデータベーススキーマは下記のとおり。以下のコマンドにより、テキストファイルが読み込まれ、データベースが解析され、ハードディスクにインデックスが作られる。一意のIDフィールドをプライマリキーとする都合上、copyコマンドにすべての欄を明示的に指定する必要がある。USCensus1990raw.data.txtファイルにはファイルの最後に空白行が1つあって、これを取り除かないとcopyコマンドはうまく動かない。

create database ucicensus1990;
\c ucicensus1990;

create table census (

AAGE boolean, AANCSTR1 boolean, AANCSTR2 boolean, AAUGMENT boolean, ABIRTHPL boolean, ACITIZEN boolean,
ACLASS boolean, ADEPART  boolean, ADISABL1 boolean, ADISABL2 boolean, AENGLISH boolean, AFERTIL boolean,
AGE int, AHISPAN  boolean, AHOUR89  boolean, AHOURS boolean, AIMMIGR  boolean, AINCOME1 int,
AINCOME2 int, AINCOME3 int, AINCOME4 int, AINCOME5 int, AINCOME6 int, AINCOME7 int, AINCOME8 int,
AINDUSTR boolean, ALABOR   boolean, ALANG1   boolean, ALANG2   boolean, ALSTWRK  boolean,
AMARITAL boolean, AMEANS   boolean, AMIGSTAT boolean,  AMOBLLIM boolean, AMOBLTY  boolean,
ANCSTRY1 int, ANCSTRY2 int, AOCCUP boolean, APERCARE boolean, APOWST boolean, ARACE boolean,
ARELAT1 boolean, ARIDERS boolean, ASCHOOL boolean, ASERVPER boolean, ASEX boolean, ATRAVTME boolean,
AVAIL int, AVETS1 boolean, AWKS89 boolean, AWORK89 boolean, AYEARSCH boolean, AYRSSERV boolean,
CITIZEN int, CLASS int, DEPART int, DISABL1 int, DISABL2 int, ENGLISH int, FEB55 boolean, FERTIL int,
HISPANIC int, HOUR89 int, HOURS int, IMMIGR int, INCOME1 int, INCOME2 int, INCOME3 int, INCOME4 int,
INCOME5 int, INCOME6 int, INCOME7 int, INCOME8 int, INDUSTRY int, KOREAN boolean, LANG1 int, LANG2 int,
LOOKING int, MARITAL int, MAY75880 boolean, MEANS int, MIGPUMA int, MIGSTATE int, MILITARY int,
MOBILITY int, MOBILLIM int, OCCUP int, OTHRSERV boolean, PERSCARE int, POB int, POVERTY int, POWPUMA int,
POWSTATE int, PWGT1 int, RACE int, RAGECHLD int, REARNING int, RECTYPE text, RELAT1 int, RELAT2 int,
REMPLPAR int, RIDERS int, RLABOR int, ROWNCHLD boolean, RPINCOME int, RPOB int, RRELCHLD boolean,
RSPOUSE int, RVETSERV int, SCHOOL int, SEPT80 boolean, SERIALNO text, SEX int, SUBFAM1 int,
SUBFAM2 int, TMPABSNT int, TRAVTIME int, VIETNAM boolean, WEEK89 int, WORK89 int, WORKLWK int,
WWII boolean, YEARSCH int, YEARWRK int, YRSSERV int,

 id serial primary key
);

copy census ( AAGE, AANCSTR1,  AANCSTR2, AAUGMENT, ABIRTHPL, ACITIZEN, ACLASS,
   ADEPART, ADISABL1, ADISABL2, AENGLISH, AFERTIL, AGE, AHISPAN, AHOUR89, AHOURS, AIMMIGR,
   AINCOME1, AINCOME2, AINCOME3, AINCOME4, AINCOME5, AINCOME6, AINCOME7, AINCOME8, AINDUSTR,
   ALABOR, ALANG1, ALANG2, ALSTWRK, AMARITAL, AMEANS, AMIGSTAT, AMOBLLIM, AMOBLTY, ANCSTRY1,
   ANCSTRY2, AOCCUP, APERCARE, APOWST, ARACE, ARELAT1, ARIDERS, ASCHOOL, ASERVPER, ASEX,
   ATRAVTME, AVAIL, AVETS1, AWKS89, AWORK89, AYEARSCH, AYRSSERV, CITIZEN, CLASS, DEPART,
   DISABL1, DISABL2, ENGLISH, FEB55, FERTIL, HISPANIC, HOUR89, HOURS, IMMIGR, INCOME1, INCOME2,
   INCOME3, INCOME4, INCOME5, INCOME6, INCOME7, INCOME8, INDUSTRY, KOREAN, LANG1, LANG2, LOOKING,
   MARITAL, MAY75880, MEANS, MIGPUMA, MIGSTATE, MILITARY, MOBILITY, MOBILLIM, OCCUP, OTHRSERV,
   PERSCARE, POB, POVERTY, POWPUMA, POWSTATE, PWGT1, RACE, RAGECHLD, REARNING, RECTYPE, RELAT1,
   RELAT2, REMPLPAR, RIDERS, RLABOR, ROWNCHLD, RPINCOME, RPOB, RRELCHLD, RSPOUSE, RVETSERV, SCHOOL,
   SEPT80, SERIALNO, SEX, SUBFAM1, SUBFAM2, TMPABSNT, TRAVTIME, VIETNAM, WEEK89, WORK89, WORKLWK,
  WWII, YEARSCH, YEARWRK, YRSSERV )
from '/.../USCensus1990raw.data.txt';

create index pob on census ( pob );
analyse;

SELECT relname, reltuples, relpages * 8 / 1024 AS "MB" FROM pg_class ORDER BY relpages DESC;
              relname              |  reltuples  |  MB
-----------------------------------+-------------+------
 census                            | 2.45828e+06 | 1010
 census_pkey                       |  2.4583e+06 |   52
 pob                               | 2.45828e+06 |   52
...

explain select count(*) from census where pob = 33;
                                   QUERY PLAN
--------------------------------------------------------------------------------
 Aggregate  (cost=33994.42..33994.43 rows=1 width=0)
   ->  Index Scan using pob on census  (cost=0.00..33965.20 rows=11686 width=0)
         Index Cond: (pob = 33)
(3 rows)

 テストで特定のインデックスを評価するときは、それ以外のインデックスが存在しない状況でテストすることにしている。そこで、SSD上のインデックスをテストするときはハードディスク上のインデックスを事前に削除し、ハードディスク上のインデックスをテストするときはSSD上のインデックスを削除する。インデックスごとに3種類のselectコマンドを実行した。各コマンドはそれぞれコールドキャッシュを使う操作、ホットキャッシュを使う操作、"クロス"キャッシュを使う操作に対応する。

 コールドキャッシュによる操作では、いったんPostgreSQLデータベースを停止し、データベースとSSDテーブル空間の置かれているディスクをマウント解除してからテストを行った。ホットテストでは、同じクエリを2回実行した。クロステストのためには、ある特定の状態で最初にコールドテストとホットテストを実行してから別の状態でクエリを実行する必要がある。最初の状態のコールドテストとホットテストでPostgreSQLは、その状態に対応するインデックスページをRAMにキャッシュする。次に別の状態でクエリを実行すると、一部のインデックスページはRAMにキャッシュされているものが利用されるかもしれないが、必要なすべてのページでキャッシュが使われるわけではない。絶えずアクセスされるデータベースで、しかもパフォーマンスを気にしてインデックス用にSSDを装備しようと考えるほどのデータベースの場合、インデックスページのどれかがRAMにキャッシュされないことなどPostgreSQLではほとんどあり得ず、SSDの優位性は低下する。一方、ホットテストは現実的でない。必要なインデックスページがすべてキャッシュされる可能性が非常に高いからだ。クロステストは折衷的になるように工夫されている。つまり、一部のインデックスページはキャッシュに存在するだろうが、クエリに必要なすべてのインデックスページがキャッシュに含まれることはない。

 クエリからどれだけの数のタプルが返される見込みがあるかでデータベース側のクエリの解決方法が変化するため、PostgreSQLで常にインデックスが使われるように、あまり多くの結果を返さないようなクエリを使用した。具体的には、出生地(pob: places of birth)のリストからFlorida(pob=12)、Puerto Rico(pob=72)、Nevada(pob=32)を選択したわけだが、SQLのexplainコマンドによると、これらのクエリでpob上のインデックスが使われることがわかったからだ。これらのクエリはそれぞれ54,445個、10,134個、4,722個のタプルをテーブル内の合計2,458,285個のタプルの中から見つける。クロステストの順番はFlorida、Puerto Rico、次にPuerto Rico、Nevada、次にNevada、Floridaの順である。SQLクエリは以下に示すとおり、いたって簡単なものだ。PostgreSQLに不慣れなら、先頭にexplainキーワードを付けてクエリを実行するとよい。クエリを実行したとき実際にどのようなことが起こるかをPostgreSQLが教えてくれる。以下の例では、インデックスが本当に使われるか確認するためにexplainを使用している(結果はIndex Scan行に示される)。インデックススキャン後に実行されるAggregateは、返されたタプルの数をカウントするだけである。

# explain  select count(*) from census where pob = 12;
                                   QUERY PLAN
--------------------------------------------------------------------------------
 Aggregate  (cost=36740.79..36740.80 rows=1 width=0)
   ->  Index Scan using pobssd on census  (cost=0.00..36708.78 rows=12800 width=0)
         Index Cond: (pob = 12)
...
select count(*) from census where pob = 12;
select count(*) from census where pob = 72;
select count(*) from census where pob = 32;

 SSDは、XFSでフォーマットし、最大のパフォーマンスを得るためにnobarrierオプションを指定してマウントした。nobarrierオプションを指定すると、突然電源が切れた場合にジャーナル内のトランザクションが失われる恐れがあるが、サーバー環境ではUPSが突然の電源故障からマシンを保護するものと考えられるので、nobarrierオプションを指定しても問題はない。また、PostgreSQLでpobssdインデックスが使われるようにするためにテーブル空間(tablespace)としてSSDを指定した。

mkfs.xfs -f  -i size=512 -l lazy-count=1 /dev/disk/by-id/ata-MTRON_MSD-SATA3025_0HB3331303546-part1
mount -o "nobarrier" /dev/disk/by-id/ata-MTRON_MSD-SATA3025_0HB3331303546-part1 /mnt/ssd
mkdir /mnt/ssd/postgresql-data
chown postgres:postgres  /mnt/ssd/postgresql-data

psql ucicensus1990
CREATE TABLESPACE ssd LOCATION '/mnt/ssd/postgresql-data';

drop index pob;
create index pobssd on census ( pob ) tablespace ssd;

 平均すると、ホットキャッシュによるパフォーマンスは、SSDとハードディスクのどちらのインデックスでも速度的によく似ており、SSDでの必要な時間はハードディスクのインデックスで同じクエリを使用したときの70~85%になった。しかし、Nevadaに関するデータを検索したとき特異的な現象が起こり、ホットキャッシュの使用時にSSDのインデックスでは5ミリ秒しかかからないのに、ハードディスクのインデックスでは約200ミリ秒もかかった。Nevadaだけパフォーマンスが異なる理由は、よくわからない。

 以下のグラフは、コールドキャッシュとクロスキャッシュ関して、SSDのインデックスとハードディスクのインデックスのパフォーマンスを比較したものだ。コールドキャッシュの場合、SSDのインデックスはハードディスクのインデックスのときの3~10%程度の時間でクエリを解決できている。コールドキャッシュでパフォーマンスが最低となるクエリはFloridaで、これは他のケースよりも多くのタプルをフェッチする必要があるからだ。また、FlordaについてはSSDインデックスで必要な時間がハードディスクインデックスでの時間の10%となっているが、これは多分、インデックスそのものよりもベーステーブルに関する負荷が大きいせいだ。クロスキャッシュを使うクエリの結果は興味深い。前回のクエリでインデックスの一部のページがRAMにキャッシュされていても、SSDはハードディスクキャッシュのときの20%以下で同じクエリを実行できている。

ssd_vs_hdd_thumb.png
SSD vs HDD for Postgres index

まとめ

 SSDの最大容量は従来のハードディスクに比べるとまだ小さく、ギガバイト当たりのコストもかなり高く付くが、1台のSSDにより得られるシークタイムは6台のハードディスクで構成されるハードウェアRAIDよりも優れている。データベースサーバーを運用していて、インデックスページのキャッシュに使えるRAMを既に限界まで増設している場合、さらにパフォーマンスを上げる必要があるなら、データベースのインデックスのパフォーマンスを引き上げるために32GBか64GBのSSDを増設することを検討してもよいだろう。

Ben Martinは、ここ10年以上ファイルシステムに取り組んできた。博士号を取得し、現在はlibferris、各種ファイルシステム、検索ソリューションを中心にコンサルティングサービスを手がけている。

Linux.com 原文