彷徨えるフジワラ

年がら年中さまよってます

4KBセクタHDDとZFSミラー 〜 実装調査編

自宅の Solaris (OpenIndiana) マシンのルートプール (rpool) は、HDD x3 ミラーリングで運用しているのですが、その内の一台が先日お亡くなりになってしまいました。

最近は 4KB セクタな HDD がすっかり定着したようで、現在 rpool の運用に使用している 512B セクタな HDD は、一般的な販売店の店頭では入手できないか、できても割増価格になっている印象があります。

しかし 512B セクタの HDD で運用している ZFS ミラーに、4KB セクタの HDD を追加しようとすると、zpool attach が以下のようなエラーメッセージを出力します。

% zpool attach -f rpool c8t1d0s0 c8t0d0s0
cannot attach c8t0d0s0 to c8t1d0s0: devices have different sector alignment

移行の手間を惜しんで、512B セクタな HDD を割増価格で購入しても良いのですが、妥当な価格で購入可能な状態が今後も続くとは限りません。

そろそろ本腰を入れて 4KB セクタ HDD に移行せざるを得ない時期なんでしょうねぇ。

512B セクタの HDD で運用している ZFS ミラーを、4KB セクタの HDD に移行するにあたって、調べたことや、実施した作業手順等を、備忘録代わりに公開しようと思います。

以下は、実装周りに関する調査結果の記録です。作業手順等は別エントリで公開予定です。

ZFS におけるセクタ境界違反の検出

zpool attach が出力するメッセージ文字列を頼りに、エラーを判定している個所を探してみると、libzfs の [https://bitbucket.org/illumos/illumos-gate/src/d07a59219ab7fd2a7f39eb47c46cf083c88e932f/usr/src/lib/libzfs/common/libzfs_pool.c#cl-2653:title=zpool_vdev_attach()] が、直前の ioctil() でのエラーを判定してメッセージを出力していることがわかります。

    ret = zfs_ioctl(zhp->zpool_hdl, ZFS_IOC_VDEV_ATTACH, &zc);
〜〜〜〜
    switch (errno) {
〜〜〜〜
    case EDOM:
        /*
         * The new device has a different alignment requirement.
         */
        zfs_error_aux(hdl, dgettext(TEXT_DOMAIN,
            "devices have different sector alignment"));
        (void) zfs_error(hdl, EZFS_BADDEV, msg);
        break;

ZFSカーネルコード部分で EDOM を返している個所から、それらしいものを探してみると、ZFS_IOC_VDEV_ATTACH に対する ioctl() 処理を実装している [https://bitbucket.org/illumos/illumos-gate/src/d07a59219ab7fd2a7f39eb47c46cf083c88e932f/usr/src/uts/common/fs/zfs/spa.c#cl-4423:title=spa_vdev_attach()] で、該当すると思われる処理が実施されています。

    /*
     * The new device cannot have a higher alignment requirement
     * than the top-level vdev.
     */
    if (newvd->vdev_ashift > oldvd->vdev_top->vdev_ashift)
        return (spa_vdev_exit(spa, newrootvd, txg, EDOM));

vdeb 構造体(vdev_t)の定義によると、vdev_ashift は、ブロック境界整合サイズをビットシフト値で保持している模様です。

    uint64_t vdev_ashift; /* block alignment shift */

先述した spa_vdev_attach() での判定処理は、境界整合に対する新規デバイスnewvd)の要求値が、旧デバイスの属する仮想デバイスoldvd->vdev_top)の要求値よりも大きい場合にエラーとなります。

ミラーリング』が、全デバイスの同一位置(= 同一ブロック識別子)に同一内容のデータを書き込む、という仕組みであることを考えれば、至極当然の話ですね。

なお、/dev/dsk/c8t0d0s0 のようなブロックデバイスを、新規デバイスとして指定した場合、vdev_ashift の初期化は [https://bitbucket.org/illumos/illumos-gate/src/d07a59219ab7fd2a7f39eb47c46cf083c88e932f/usr/src/uts/common/fs/zfs/vdev_disk.c#cl-328:title=vdev_disk_open()] で実施されます。

    struct dk_minfo_ext dkmext;
〜〜〜〜
    /*
     * Determine the device's minimum transfer size.
     * If the ioctl isn't supported, assume DEV_BSIZE.
     */
    if (ldi_ioctl(dvd->vd_lh, DKIOCGMEDIAINFOEXT, (intptr_t)&dkmext,
        FKIOCTL, kcred, NULL) != 0)
        dkmext.dki_pbsize = DEV_BSIZE;

    *ashift = highbit(MAX(dkmext.dki_pbsize, SPA_MINBLOCKSIZE)) - 1;

ioctl(DKIOCGMEDIAINFOEXT) を受けたデバイス側は、struct dk_minfo_ext 領域にデバイスの記録媒体に関する情報を格納します。

[https://bitbucket.org/illumos/illumos-gate/src/d07a59219ab7fd2a7f39eb47c46cf083c88e932f/usr/src/uts/common/sys/dkio.h#cl-343:title=struct dk_minfo_ext]dki_pbsize フィールドは、記録媒体の物理ブロックサイズに関する情報を保持しています。

/*
 * Used for Media info or the current profile info
 * including physical block size if supported.
 */
struct dk_minfo_ext {
    uint_t     dki_media_type; /* Media type or profile info */
    uint_t     dki_lbsize;     /* Logical blocksize of media */
    diskaddr_t dki_capacity;   /* Capacity as # of dki_lbsize blks */
    uint_t     dki_pbsize;     /* Physical blocksize of media */
};

ちなみに、vdev_ashift フィールドに、ブロック境界整合サイズそのものではなく、ビットシフト値を保持しているのは、後々の処理で、ビットマスクを作ったり、ファイル内オフセット値をビットシフトしたりする際に、都度ビットシフト値を算出するオーバヘッドを削減するためだと思うのですがどうなんでしょうね?

セクタサイズの確認方法

ZFS の内部処理では、『ブロック境界整合サイズ』の一致/不一致を、上記のような要領で判定しています。

しかし、この『ブロック境界整合サイズ』を事前に取得するのは案外面倒です。

実は 4KB セクタサイズな HDD に対して prtvtoc コマンドを実施しても、セクタサイズは 512B と判定されます。

※ 512B セクタな HDD
% prtvtoc /dev/rdsk/c8t1d0s2
* /dev/rdsk/c8t1d0s0 partition map
*
* Dimensions:
*     512 bytes/sector
*     252 sectors/track
*     255 tracks/cylinder
*   64260 sectors/cylinder
*   60799 cylinders
*   60797 accessible cylinders
〜〜〜〜

※ 4KB セクタな HDD
% prtvtoc /dev/rdsk/c8t0d0s2
* /dev/rdsk/c8t0d0s2 partition map
*
* Dimensions:
*     512 bytes/sector
*     252 sectors/track
*     255 tracks/cylinder
*   64260 sectors/cylinder
*   60799 cylinders
*   60797 accessible cylinders
〜〜〜〜

バイスに対して stat コマンドを実行して、stat(2) システムコールで得られる『推奨 I/O サイズ』(st_blksize)を表示させると、こちらはいずれも 8KB (!)と判定されます(-L は『シンボリックリンク先情報の表示』のため)。

※ 512B セクタな HDD
root@witchhunt$ stat -L /dev/rdsk/c8t1d0s0
  File: `/dev/rdsk/c8t1d0s0'
  Size: 0       Blocks: 0  IO Block: 8192   character special file
〜〜〜〜

※ 4KB セクタな HDD
root@witchhunt$ stat -L /dev/rdsk/c8t0d0s0
  File: `/dev/rdsk/c8t0d0s0'
  Size: 0       Blocks: 0  IO Block: 8192   character special file
〜〜〜〜

記録媒体の物理ブロックサイズの取得のために、カーネル内コードでは ioctl(DKIOCGMEDIAINFOEXT) を使用していましたが、改めて onnv 同梱のソースコードを調べた限りでは、同等の処理を実施しているコマンドは無さそうです……

しかし、ありがたいことに、ioctl(DKIOCGMEDIAINFOEXT) を使って記録媒体の物理ブロックサイズを取得する簡易プログラムblocksize)を実装している方がいました(こちらからも入手可能)。

※ プログラム内容の妥当性は、必ず各自の責任で確認してください!(特にルート権限で実行されるプログラムの場合)

ソースコードコンパイルし、以下の要領で実行します。

※ 512B セクタな HDD
% blocksize /dev/rdsk/c8t1d0s0
dkmp.dki_capacity = 3907029168
dkmp.dki_lbsize   = 512
dkmp_ext.dki_capacity = 3907029168
dkmp_ext.dki_lbsize   = 512
dkmp_ext.dki_pbsize   = 512

※ 4KB セクタな HDD
% blocksize /dev/rdsk/c8t0d0s0
dkmp.dki_capacity = 3907029168
dkmp.dki_lbsize   = 512
dkmp_ext.dki_capacity = 3907029168
dkmp_ext.dki_lbsize   = 512
dkmp_ext.dki_pbsize   = 4096

記録媒体のブロックサイズが、期待通りに取得できていますね。

4KB セクタ対応の今後の動向

先述したように、4KB セクタの HDD に対する prtvtoc コマンドでも、セクタサイズを 512B とみなす場合があります。

このような HDD では、先述した簡易コマンド(blocksize)の出力でも、論理ブロックサイズ(dki_lbsize)が 512B であることが確認できます。

論理ブロックサイズが 512B ということは、512B 単位でのブロックアクセスが可能であることを意味します:物理ブロックサイズと一致しないため、論理ブロック単位での読み書きは、性能的に残念なことになるのが確実ですが……

このことから、境界整合の一致を『論理ブロックサイズで判定した方がよくね?』という提案もされています。

残念ながら、上記の提案はまだリリース版 illumos に含まれていない(2013-11-22 時点)ので、セクタサイズ違いの HDD を導入する場合は、一手間かける必要があります。

もっとも、上記提案の修正が適用された場合:

4KB セクタの HDD でも、論理ブロックサイズが 512B なら、512B セクタ HDD で運用されている ZFS ミラーに追加できる

という長所の一方で:

ZFS ミラーから 512B セクタの HDD が完全に取り除かれた後も、4KB セクタの HDD へのアクセス単位は、引き続き 512B のまま

という性能上の短所があります。

ルートプール (rpool) のように『単一のミラーリングしか許容しない』といった制約のあるケースでもなければ、HDD の移行方法は他にも色々やりようがあるので、この修正案は優先度的に微妙な扱いなのかもしれません。

とは言え、まさにルートプールへの HDD 追加で手間取っている身としては、この修正は是非とも入れて欲しいんですけどねぇ……