止められないオンラインサービスでデータベースを運用している場合、バックアップデータをどうやって作るかには頭を悩ませられる。Tkrzwにはバックアップのための手段がいくつかあるが、ファイルシステムのcopy-on-writeスナップショットを簡単に利用する機能を追加したので紹介する。また、通常のread/writeによるユーザ空間内コピー、sendfileによるカーネル空間内コピー、copy-on-writeによる擬似コピーの性能を比較する。BtrFSとEXT4の性能比較も行う。
更新操作がひっきり無しに行われるデータベースに対して、止めずにバックアップ作業を行うのは、よく考えてみると難しいことだ。バックアップファイルを作るということは、データベースファイルの全てのデータを、ある瞬間の状態を保ったまま、複製する必要がある。全てのデータを複製する処理には、当然データのサイズに比例する時間がかかる。ある瞬間の状態を保つということは、複製処理を行っている間は更新処理でファイルを変更してはならないということだ。したがって、バックアップの処理を実行している間は更新処理をブロックさせるのが率直な実装だ。
Tkrzwのデータベースで最も率直にバックアップファイルを作る方法は、CopyDataFileメソッドを呼ぶことである。これは内部的にはSynchronizeメソッドを呼んで以下の処理を順に行う。
- ロックをかけて他のスレッドをブロックする。
- キャッシュの内容をファイルシステムと同期させる。
- hardフラグが設定されていれば、msync/fsyncを呼んでファイルシステムとデバイスを同期させる。
- コールバック関数を呼んで、ファイルの複製処理を行う。
- ロックを解除する。
コールバック関数として同名のCopyDataFileという外部関数が呼ばれるのだが、それは、処理系に応じて最も高速なファイルコピー手段を用いるという仕様になっている。従来(0.9.33)の実装では、Linux上ではsendfileを呼んでカーネル空間でファイルコピーを行い、それ以外の処理系ではread/writeを呼んでユーザ空間でコピーを行っていた。read/writeによるコピーは、readでカーネル(ファイルシステム)内のバッファからユーザプロセスのバッファにデータをコピーしてから、writeでカーネル内の別のバッファにデータをコピーするという無駄がある。sendfileだとその無駄を省略して、カーネル内のバッファ間でコピーが完結する。
sendfileの方が効率的とはいえ、複製処理にかかる時間がデータ量と比例するという点は同じだ。今回実装したのは、それとは根本的に異なるcopy-on-write機能を利用するものだ。最新バージョン(0.9.34)からは、Linux上で、かつ対応するファイルシステム(BtrFSかXFS)にファイルがある場合には、自動的にcopy-on-write機能を利用してくれる。
copy-on-writeでは、データを複製するふりだけして、実際には該当の領域にcopy-on-writeフラグを立てるだけにする。以後の更新処理で該当の領域が変更される際には、その領域が新しいバージョンと古いバージョンに複製される。新しいバージョンを参照しているファイルディスクリプタでのアクセスでは更新後のデータが読み出されるが、古いバージョンを参照しているファイルディスクリプタでのアクセスでは古いバージョンのデータが読み出される。copy-on-writeフラグを立てたその瞬間のデータが維持できるので、この機能はスナップショットとも呼ばれる。copy-on-writeはフラグを立てるだけなので、データサイズにあまり影響されずに、一瞬またはそれに近い時間で処理が完了する。これはオンラインのデータベースをバックアップするのに最適だ。
BtrFSとXFSでは、copy-on-writeによるファイルの複製はreflinkと呼ばれる。この機能はGNUのcpコマンドでも利用でき、cp --reflink=auto foo bar などとすることで利用することができる。試してみればわかるが、どんなに大きいファイルでも1秒もかからずに処理が終わる。複製元と複製先が同じファイルシステム上にあることも条件となる。残念ながらEXT4やEXT3では利用できない。
TkrzwのSynchronizeメソッドは任意のコールバック関数を呼べるので、その中でsystem関数を呼んでcp --reflinkを実行するという手段は従来から利用できた。とはいえ、そんな小技を知っている人は少ないだろうし、なんだかハッキーな感じが否めない。そこで、条件が整った場合には自動的にcopy-on-wirte機能を利用するように変更したのだ。
Linux上では、copy-on-writeによるファイルの複製は、ioctlシステムコールのFICLONEコマンドとして実装されている。man ioctl_ficloneで仕様が見られる。複製先と複製元のファイルディスクリプタ並べるだけなので利用するのは簡単だ。対応していないファイルシステムで実行するとEOPNOTSUPエラーになるので、その場合にはsendfileなどのフォールバックを実行することになる。
int ioctl(int dest_fd, FICLONE, int src_fd);
実際にどれくらい性能が異なるのか、試してみよう。以下のコマンドを実行する。500バイトのレコードを1000万個を書き込んだ5GBのデータベースファイルを作り、その状態でCopyDataFileメソッドを呼んでバックアップファイルを作る。元のデータベースを構築する処理は測定対象ではないので実際には同じサイズのファイルが作れれば何でもよいのだが、ちゃんと統合されていることを示すために敢えてデータベース用のコマンドを使って測定する。
$ ./tkrzw_dbm_perf sequence --path /mnt/btrfs/dev/casket.tkh --iter 10000000 --buckets 20000000 --size 500 --set_only --copy /mnt/btrfs/dev/copied.tkh
公正な比較のため、全てBtrFS上で実験を行い、コードを書き換えて強制的にread/write、sendfile、ioctlを切り替えつつ、時間を計測した。read/writeはプロセス側のバッファのサイズ(何バイトずつ読み書きするか)も性能に影響を与えるので、8KB、32KB、128KBの三通りを試した。sendfileは64KB毎に呼ぶのが定石らしいが、一応8KBや512KBも試した。動作環境は私のノートPC(Dell XPS 13)で、ストレージはそれに乗っている廉価SSDである。
time | |
read/write 8KB | 5.912 |
read/write 32KB | 5.690 |
read/write 128KB | 5.65 |
sendfile 8KB | 5.86 |
sendfile 64KB | 5.74 |
sendfile 512KB | 5.89 |
ioctl_ficlone | 0.099 |
結果としては、期待通りにcopy-on-writeは一瞬で終わることが確認できた。この値はSynchronize全体にかかる時間であって、メタデータの書き込みも含む。つまり、実運用でスレッドがブロックされる時間だ。0.1秒くらいのブロックは大抵のサービスで許容範囲だろう。ファイルが大きくなってもこのブロック時間はほとんど変わらないというのが重要な点だ。
期待外れだったのは、sendfileの結果だ。read/writeとほとんど同じ時間がかかることが確かめられた。これが意味するのは、ストレージかファイルシステムが遅い場合、バッファのコピーを節約しても性能はほとんど改善しないということだ。もしかしてBtrFSならではの特性なのかと思ってEXT4上でも測定したところ、read/write 64KBもsendfile 64KBもともに9.73秒くらいで、誤差以上の差は見られなかった。
それより、BtrFSとEXT4の差が大きいのが興味深い。read/writeやsendfileによるコピー処理の性能は、BtrFS上では5.7秒くらい、EXT4で9.7秒くらいということで、BtrFSの圧勝である。ならば、copy-on-writeも使えるBtrFSの方が優秀なのかというと、そうでもない。データベースの構築をmmapによるメモリマップI/Oで行う処理では、BtrFS上で65秒くらい、EXT4上で12秒くらいと、EXT4の圧勝である。mmapを使わないでread/writeでデータベースを構築した場合、BtrFS上で174秒くらい、EXT上で34秒くらいと、こちらもEXT4の圧勝である。おそらく、ランダムアクセスの性能はEXT4が圧倒的に高く、シーケンシャルアクセスの性能はBtrFSの方が高い。
まとめ。BtrFSやXFSが備えるcopy-on-writeのコピー機能を使うと、スレッドをほとんど止めずに、オンライン系のデータベースのバックアップが行える。Tkrzwの新バージョンでは、単にCopyFileDataメソッドを呼ぶだけでOKだ。一方で、sendfileを使った高速化はあまり効果がない。
copy-on-writeを備えるBtrFSは素晴らしいのだが、通常運用時の性能は明らかにEXT4の方が良いので、どちらを選ぶか難しいところだ。バックアップ作成時に5秒だか10秒だかブロックして許されるならば、EXT4で運用した方が得なのだ。