LVM の動作について 〜 ディスクデータ移行と pvmove, lvconvert を例題として

執筆者 : 岩本 俊弘


概要

クラウド全盛のこのご時世においても、物理マシンのお守りをしないといけないことはあって、古くなったディスクの交換作業が必要になった。 出社は避けられないものの、なるべくオフィスでの滞在時間を減らしたい。 pvmove コマンドというものがあって、online のままディスクを移行できる。 だが、このコマンドの解説記事は少なく、アンマウントしてから pvmove を実行しろといった記述があるものもある。以下はわりと正確なものである。

他のやり方もあって、lvconvert --mirror を使えという記事には以下のようなものがある。

https://linuxroutes.com/storage-migration-using-lvconvert-without-downtime/

見た目で記事の信憑性を判断するのはあまり科学的なアプローチとは言えないので、実際のところを軽く調査してみることとする。 (とはいえ、ソースコードのみから辿るのは大変なので公式ドキュメントは一応 OK ということにする。)

LVM について

LVM の解説なんて何を今更という方は読み飛ばして次の章へ。

公式ドキュメントといえばまず man page なので、lvm(8) から引用する。

DESCRIPTION
       The Logical Volume Manager (LVM) provides tools to create virtual block
       devices from physical devices.  Virtual devices may be easier to manage
       than physical devices, and can have capabilities beyond what the physi
       cal devices provide themselves.  A Volume Group (VG) is a collection of
       one  or  more  physical devices, each called a Physical Volume (PV).  A
       Logical Volume (LV) is a virtual block device that can be used  by  the
       system  or  applications.  Each block of data in an LV is stored on one
       or more PV in the VG, according to  algorithms  implemented  by  Device
       Mapper (DM) in the kernel.

物理デバイスのことを PV と呼ぶ。VG は1つ以上の PV から構成される。アプリケーションが使うものは LV で、LV は1つの VG に対応している。LV のデータは VG に属する1つ以上の PV に置かれて、データの取り扱いはカーネルに実装された Device Mapper が行うといったことが書かれている。 PV, VG, LV の関係を簡単に図にすると以下のようになる。

    +-----+   +-----+   +-----+
    | PV  |---|     |---| LV  |
    +-----+   |     |   +-----+
              | VG  |
    +-----+   |     |
    | PV  |---|     |
    +-----+   +-----+

pvmovelvconvert --mirror の動作

実験

まずはその辺にあるクラウド環境で ubuntu-20.04 の VM を作成して実験した。 /dev/vdb/dev/vdc が実験用ドライブで、LVM パーティションを作って PV を作成した。 実験用の VG test-vg を作成し、上で作成した2つの PV を追加した。

$ sudo fdisk /dev/vdb
$ sudo pvcreate /dev/vdb1
$ sudo fdisk /dev/vdc
$ sudo pvcreate /dev/vdc1
$ sudo vgcreate test-vg /dev/vdb1
$ sudo vgextend test-vg /dev/vdc1

ここでは以下の状態になっている。LV はまだ作成していない。

$ sudo vgs
  VG      #PV #LV #SN Attr   VSize VFree
  test-vg   2   0   0 wz--n- 1.99g 1.99g
$ sudo pvs
  PV         VG      Fmt  Attr PSize    PFree   
  /dev/vdb1  test-vg lvm2 a--  1020.00m 1020.00m
  /dev/vdc1  test-vg lvm2 a--  1020.00m 1020.00m

test-vg 上に LV を作成して pvmove で PV の移動を試みてみる。 以下のように5秒ほどで移動が完了した。

$ sudo lvcreate -L +800M test-vg /dev/vdb1
  Logical volume "lvol0" created.
$ sudo lvs -o+devices
  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices
  lvol0 test-vg -wi-a----- 800.00m                                                     /dev/vdb1(0)
$ sudo pvs
  PV         VG      Fmt  Attr PSize    PFree
  /dev/vdb1  test-vg lvm2 a--  1020.00m  220.00m
  /dev/vdc1  test-vg lvm2 a--  1020.00m 1020.00m
$ sudo pvmove -i 1 /dev/vdb1 /dev/vdc1
  /dev/vdb1: Moved: 5.50%
  /dev/vdb1: Moved: 30.00%
  /dev/vdb1: Moved: 55.00%
  /dev/vdb1: Moved: 78.00%
  /dev/vdb1: Moved: 100.00%
$ sudo lvs -o+devices
  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices
  lvol0 test-vg -wi-a----- 800.00m                                                     /dev/vdc1(0)
$ sudo pvs
  PV         VG      Fmt  Attr PSize    PFree
  /dev/vdb1  test-vg lvm2 a--  1020.00m 1020.00m
  /dev/vdc1  test-vg lvm2 a--  1020.00m  220.00m

pvmove の中で何が動いているかわかるように、同時に以下のコマンドを発行して観察する。

$ while true; do sudo lvs -o+devices; sudo dmsetup ls; sudo dmsetup table test--vg-lvol0; sudo dmsetup table test--vg-pvmove0; echo; sleep 1 ;done
  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices
  lvol0 test-vg -wi-a----- 800.00m                                                     /dev/vdb1(1)
test--vg-lvol0  (253:0)
0 1638400 linear 252:17 10240
device-mapper: table ioctl on test--vg-pvmove0  failed: No such device or address
Command failed.

  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices
  lvol0 test-vg -wI-a----- 800.00m                                                     pvmove0(0)
test--vg-pvmove0        (253:1)
test--vg-lvol0  (253:0)
0 1638400 linear 253:1 0
0 1638400 mirror core 1 4096 2 252:17 10240 252:33 2048

  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices
  lvol0 test-vg -wI-a----- 800.00m                                                     pvmove0(0)
test--vg-pvmove0        (253:1)
test--vg-lvol0  (253:0)
0 1638400 linear 253:1 0
0 1638400 mirror core 1 4096 2 252:17 10240 252:33 2048

  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices
  lvol0 test-vg -wI-a----- 800.00m                                                     pvmove0(0)
test--vg-pvmove0        (253:1)
test--vg-lvol0  (253:0)
0 1638400 linear 253:1 0
0 1638400 mirror core 1 4096 2 252:17 10240 252:33 2048

  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices
  lvol0 test-vg -wI-a----- 800.00m                                                     pvmove0(0)
test--vg-pvmove0        (253:1)
test--vg-lvol0  (253:0)
0 1638400 linear 253:1 0
0 1638400 mirror core 1 4096 2 252:17 10240 252:33 2048

  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices
  lvol0 test-vg -wi-a----- 800.00m                                                     /dev/vdc1(0)
test--vg-lvol0  (253:0)
0 1638400 linear 252:33 2048
device-mapper: table ioctl on test--vg-pvmove0  failed: No such device or address
Command failed.

test--vg-pvmove0 という mirror デバイスが作成され、処理対象の lvol0 の linear mapping 先が test--vg-pvmove0 に切り替えられ、ミラーリング完了後に lvol0 の linear mapping が移動先の PV を指すようになった。

次に lvconvert を使って、PV を今度は /dev/vdc1 から /dev/vdb1 に移動してみる。

$ sudo lvconvert -m 1  test-vg/lvol0 /dev/vdb1Are you sure you want to convert linear LV test-vg/lvol0 to raid1 with 2 images enhancing resilience? [y/n]: y
  Logical volume test-vg/lvol0 successfully converted.
$ sudo lvs -o+devices
  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices                            
  lvol0 test-vg rwi-a-r--- 800.00m                                    75.34            lvol0_rimage_0(0),lvol0_rimage_1(0)
$ sudo lvs -o+devices
  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices                            
  lvol0 test-vg rwi-a-r--- 800.00m                                    100.00           lvol0_rimage_0(0),lvol0_rimage_1(0)
$ sudo dmsetup table test--vg-lvol0
0 1638400 raid raid1 7 0 rebuild 1 region_size 4096 2 253:1 253:2 253:3 253:4
$ sudo dmsetup ls
test--vg-lvol0_rmeta_1  (253:3)
test--vg-lvol0_rmeta_0  (253:1)
test--vg-lvol0  (253:0)
test--vg-lvol0_rimage_1 (253:4)
test--vg-lvol0_rimage_0 (253:2)
$ sudo dmsetup table test--vg-lvol0_rimage_0
0 1638400 linear 252:33 2048
$ sudo dmsetup table test--vg-lvol0_rimage_1
0 1638400 linear 252:17 10240
$ sudo lvconvert -m 0  test-vg/lvol0 /dev/vdc1
Are you sure you want to convert raid1 LV test-vg/lvol0 to type linear losing all resilience? [y/n]: y
  Logical volume test-vg/lvol0 successfully converted.
$ sudo lvs -o+devices
  LV    VG      Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert Devices     
  lvol0 test-vg -wi-a----- 800.00m                                                     /dev/vdb1(1)
$ sudo dmsetup ls
test--vg-lvol0  (253:0)
$ sudo dmsetup table test--vg-lvol0
0 1638400 linear 252:17 10240

mirrorraid1 の違いなどはあるが、lvconvertpvmove も同じようなことをしていることがわかった。 (細かいことを言うと、ミラー領域の状態を記録する dirty log がメモリ上かディスク上かといった違いはあるので、もしコピー中 (ミラーリング中) に電源断があったりすると動作は違うかもしれない。)

実は lvm2/doc/pvmove_outline.txt という資料があって、一時的に mirror を用意するとか LV が複数の PV から成るときにどう動くかといったことが書かれている。 また、pvmove は指定された PV 上の LV 全てに対して処理を行うが、lvmirror は指定された単一の LV に対して処理を行うという違いもある。 今回、ディスクを交換しようとしている環境では PV も LV も1つしかなくて、何も複雑な構成はとっていないので、正直もうどっちでも良さそうではある。

以下では pvmove に絞ってちょっと詳しく見ていく。 まず strace でどんな syscall が発行されているか調べる。

$ sudo strace -f pvmove -i 4 /dev/vdb1 /dev/vdc1
(略)
openat(AT_FDCWD, "/dev/mapper/control", O_RDWR) = 3
openat(AT_FDCWD, "/proc/devices", O_RDONLY) = 4
fstat(4, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(4, "Character devices:\n  1 mem\n  4 /"..., 1024) = 562
close(4)                                = 0
ioctl(3, DM_VERSION, {version=4.0.0, data_size=16384, flags=DM_EXISTS_FLAG} => {version=4.41.0, data_size=16384, flags=DM_EXISTS_FLAG}) = 0
ioctl(3, DM_LIST_VERSIONS, {version=4.1.0, data_size=16384, data_start=312, flags=DM_EXISTS_FLAG} => {version=4.41.0, data_size=487, data_start=312, flags=DM_EXISTS_FLAG, ...}) = 0
ioctl(3, DM_LIST_VERSIONS, {version=4.1.0, data_size=16384, data_start=312, flags=DM_EXISTS_FLAG} => {version=4.41.0, data_size=487, data_start=312, flags=DM_EXISTS_FLAG, ...}) = 0
(略)
ioctl(3, DM_TABLE_STATUS, {version=4.0.0, data_size=16384, data_start=312, dev=makedev(0xfd, 0), flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_SKIP_BDGET_FLAG|DM_NOFLUSH_FLAG} => {version=4.41.0, data_size=353, data_start=312, dev=makedev(0xfd, 0), name="test--vg-lvol0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", target_count=1, open_count=0, event_nr=2, flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_ACTIVE_PRESENT_FLAG|DM_SKIP_BDGET_FLAG|DM_NOFLUSH_FLAG, ...}) = 0
(略)
ioctl(3, DM_DEV_CREATE, {version=4.0.0, data_size=16384, name="test--vg-pvmove0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWR88kdymWLz05gSt2A3J626ldcdCJ3roCj", flags=DM_EXISTS_FLAG|DM_SKIP_BDGET_FLAG} => {version=4.41.0, data_size=305, dev=makedev(0xfd, 0x1), name="test--vg-pvmove0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWR88kdymWLz05gSt2A3J626ldcdCJ3roCj", target_count=0, open_count=0, event_nr=0, flags=DM_EXISTS_FLAG|DM_SKIP_BDGET_FLAG}) = 0
ioctl(3, DM_TABLE_STATUS, {version=4.0.0, data_size=16384, data_start=312, dev=makedev(0xfd, 0x1), flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_STATUS_TABLE_FLAG} => {version=4.41.0, data_size=305, data_start=312, dev=makedev(0xfd, 0x1), name="test--vg-pvmove0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWR88kdymWLz05gSt2A3J626ldcdCJ3roCj", target_count=0, open_count=0, event_nr=0, flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_STATUS_TABLE_FLAG}) = 0
ioctl(3, DM_TABLE_LOAD, {version=4.0.0, data_size=16384, data_start=312, dev=makedev(0xfd, 0x1), target_count=1, flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_SKIP_BDGET_FLAG, ...} => {version=4.41.0, data_size=305, data_start=312, dev=makedev(0xfd, 0x1), name="test--vg-pvmove0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWR88kdymWLz05gSt2A3J626ldcdCJ3roCj", target_count=0, open_count=0, event_nr=0, flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_INACTIVE_PRESENT_FLAG|DM_SKIP_BDGET_FLAG}) = 0
ioctl(3, DM_TABLE_STATUS, {version=4.0.0, data_size=16384, data_start=312, dev=makedev(0xfd, 0), flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_STATUS_TABLE_FLAG} => {version=4.41.0, data_size=365, data_start=312, dev=makedev(0xfd, 0), name="test--vg-lvol0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", target_count=1, open_count=0, event_nr=2, flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_STATUS_TABLE_FLAG|DM_ACTIVE_PRESENT_FLAG, ...}) = 0
ioctl(3, DM_TABLE_LOAD, {version=4.0.0, data_size=16384, data_start=312, dev=makedev(0xfd, 0), target_count=1, flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_SKIP_BDGET_FLAG, ...} => {version=4.41.0, data_size=305, data_start=312, dev=makedev(0xfd, 0), name="test--vg-lvol0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", target_count=1, open_count=0, event_nr=2, flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_ACTIVE_PRESENT_FLAG|DM_INACTIVE_PRESENT_FLAG|DM_SKIP_BDGET_FLAG}) = 0
(略)
mlock(0x55aecf27d000, 208896)           = 0
mlock(0x55aecf2b0000, 1695744)          = 0
mlock(0x55aecf44e000, 684032)           = 0
(略)
ioctl(3, DM_DEV_STATUS, {version=4.0.0, data_size=16384, uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", flags=DM_EXISTS_FLAG} => {version=4.41.0, data_size=305, dev=makedev(0xfd, 0), name="test--vg-lvol0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", target_count=1, open_count=0, event_nr=2, flags=DM_EXISTS_FLAG|DM_ACTIVE_PRESENT_FLAG|DM_INACTIVE_PRESENT_FLAG}) = 0
ioctl(3, DM_TABLE_DEPS, {version=4.0.0, data_size=16384, data_start=312, dev=makedev(0xfd, 0), flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG} => {version=4.41.0, data_size=328, data_start=312, dev=makedev(0xfd, 0), name="test--vg-lvol0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", target_count=1, open_count=0, event_nr=2, flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_ACTIVE_PRESENT_FLAG|DM_INACTIVE_PRESENT_FLAG, ...}) = 0
ioctl(3, DM_DEV_STATUS, {version=4.0.0, data_size=16384, uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd-real", flags=DM_EXISTS_FLAG} => {version=4.41.0, data_size=16384, uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd-real", flags=DM_EXISTS_FLAG}) = -1 ENXIO (No such device or address)
ioctl(3, DM_DEV_STATUS, {version=4.0.0, data_size=16384, uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd-cow", flags=DM_EXISTS_FLAG} => {version=4.41.0, data_size=16384, uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd-cow", flags=DM_EXISTS_FLAG}) = -1 ENXIO (No such device or address)
ioctl(3, DM_DEV_STATUS, {version=4.0.0, data_size=16384, dev=makedev(0xfd, 0), flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_SKIP_BDGET_FLAG} => {version=4.41.0, data_size=305, dev=makedev(0xfd, 0), name="test--vg-lvol0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", target_count=1, open_count=0, event_nr=2, flags=DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_ACTIVE_PRESENT_FLAG|DM_INACTIVE_PRESENT_FLAG|DM_SKIP_BDGET_FLAG}) = 0
ioctl(3, DM_DEV_SUSPEND, {version=4.0.0, data_size=16384, dev=makedev(0xfd, 0), flags=DM_SUSPEND_FLAG|DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_SKIP_BDGET_FLAG|DM_SKIP_LOCKFS_FLAG} => {version=4.41.0, data_size=305, dev=makedev(0xfd, 0), name="test--vg-lvol0", uuid="LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", target_count=1, open_count=0, event_nr=2, flags=DM_SUSPEND_FLAG|DM_EXISTS_FLAG|DM_PERSISTENT_DEV_FLAG|DM_ACTIVE_PRESENT_FLAG|DM_INACTIVE_PRESENT_FLAG|DM_SKIP_BDGET_FLAG|DM_SKIP_LOCKFS_FLAG}) = 0
(略)

/dev/mapper/control を open して ioctl で device mapper を操作していること、 特に ioctl(DM_DEV_SUSPEND) を発行していること、その前に mlock を発行していることがわかる。 pvmove-vv オプションをつけるなどして strace の結果とあわせてソースを見ていくと _lv_update_and_reload 関数でこの suspend と resume が行われていることがわかる。 しかし、ioctl(DM_TABLE_LOAD) はこの suspend & resume の前に発行されている。 デバイスのサスペンドの解除も ioctl(DM_DEV_SUSPEND) なのだが、これらの ioctl で挟まれたクリティカルセクションで発行される ioctl は状態を変更するものはなさそうで、どうにも怪しい。 なので gdb でステップ実行しながらさらに様子を見ていくことにする。

gdb を使うにあたってデバッグシンボルが必要になるので lvm2-dbgsym パッケージをインストールして先に進む。(参照: https://wiki.ubuntu.com/Debug%20Symbol%20Packages)

先程の _lv_update_and_reload 関数の処理を更に追って _resume_node 関数の入口で止めたところでの呼び出しスタックはこんな感じである。

Breakpoint 9, _resume_node (
    name=0x5555556fb469 <_uuid_prefix_matches+89> "\205\300u\n\270\001",
    major=32767, minor=4294957936, read_ahead=21845,
    read_ahead_flags=1437370184, newinfo=0x555555ac89c8,
    cookie=0x555555ac864c, udev_flags=21845, already_suspended=0)
    at device_mapper/libdm-deptree.c:1320
1320    {

stack trace (click to expand)

(gdb) info stack
#0  _resume_node (
    name=0x5555556fb469 <_uuid_prefix_matches+89> "\205\300u\n\270\001",
    major=32767, minor=4294957936, read_ahead=21845,
    read_ahead_flags=1437370184, newinfo=0x555555ac89c8,
    cookie=0x555555ac864c, udev_flags=21845, already_suspended=0)
    at device_mapper/libdm-deptree.c:1320
#1  0x00005555556ff893 in dm_tree_activate_children (dnode=0x555555ac8700,
    uuid_prefix=0x5555562d2418 "LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", uuid_prefix_len=36)
    at device_mapper/libdm-deptree.c:2058
#2  0x00005555556ff5b8 in dm_tree_activate_children (dnode=0x555555ac8538,
    uuid_prefix=0x5555562d2418 "LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd", uuid_prefix_len=36)
    at device_mapper/libdm-deptree.c:2011
#3  0x00005555556ca68d in _tree_action (dm=0x5555562d2050, lv=0x5555562e6518,
    laopts=0x7fffffffde10, action=ACTIVATE) at activate/dev_manager.c:3702
#4  0x00005555556ca7a1 in dev_manager_activate (dm=0x5555562d2050,
    lv=0x5555562e6518, laopts=0x7fffffffde10) at activate/dev_manager.c:3729
#5  0x00005555555f72b7 in _lv_activate_lv (lv=0x5555562e6518,
    laopts=0x7fffffffde10) at activate/activate.c:1436
#6  0x00005555555fa734 in _lv_resume (cmd=0x555555a71520, lvid_s=0x0,
    laopts=0x7fffffffde10, error_if_not_active=0, lv=0x5555562e6518)
    at activate/activate.c:2356
#7  0x00005555555fa877 in lv_resume_if_active (cmd=0x555555a71520, lvid_s=0x0,
    origin_only=0, exclusive=0, revert=0, lv=0x5555562e6518)
    at activate/activate.c:2389
#8  0x00005555555fc1f4 in resume_lv (cmd=0x555555a71520, lv=0x5555562ce4e8)
    at activate/activate.c:2934
#9  0x000055555565d0ab in _lv_update_and_reload (lv=0x5555562ce4e8,
    origin_only=0) at metadata/lv_manip.c:6710
#10 0x000055555565d136 in lv_update_and_reload (lv=0x5555562ce4e8)
    at metadata/lv_manip.c:6724
#11 0x00005555555c8607 in _update_metadata (lv_mirr=0x5555562ce8f8,
    lvs_changed=0x555555aa84c8, exclusive=0) at pvmove.c:546
#12 0x00005555555c8e24 in _pvmove_setup_single (cmd=0x555555a71520,
    vg=0x5555562ce040, pv=0x5555562ce1e8, handle=0x555555aa6990)
    at pvmove.c:703
#13 0x00005555555e0b07 in _process_pvs_in_vg (cmd=0x555555a71520,
    vg=0x5555562ce040, all_devices=0x7fffffffe350, arg_devices=0x7fffffffe330,
    arg_tags=0x7fffffffe310, process_all_pvs=0, process_all_devices=0, skip=0,
    error_flags=0, handle=0x555555aa6990,
    process_single_pv=0x5555555c87ab <_pvmove_setup_single>) at toollib.c:4275
#14 0x00005555555e114e in _process_pvs_in_vgs (cmd=0x555555a71520,
    read_flags=1310720, all_vgnameids=0x7fffffffe340,
    all_devices=0x7fffffffe350, arg_devices=0x7fffffffe330,
    arg_tags=0x7fffffffe310, process_all_pvs=0, process_all_devices=0,
    handle=0x555555aa6990,
    process_single_pv=0x5555555c87ab <_pvmove_setup_single>) at toollib.c:4397
#15 0x00005555555e188c in process_each_pv (cmd=0x555555a71520, argc=1,
    argv=0x7fffffffe3f0, only_this_vgname=0x0, all_is_set=0,
    read_flags=1310720, handle=0x555555aa6990,
    process_single_pv=0x5555555c87ab <_pvmove_setup_single>) at toollib.c:4526
#16 0x00005555555c9669 in pvmove (cmd=0x555555a71520, argc=1,
    argv=0x7fffffffe690) at pvmove.c:877
#17 0x00005555555b7520 in lvm_run_command (cmd=0x555555a71520, argc=2,
    argv=0x7fffffffe688) at lvmcmdline.c:3118
#18 0x00005555555b8d0e in lvm2_main (argc=4, argv=0x7fffffffe678)
    at lvmcmdline.c:3648
#19 0x00005555555f2a15 in main (argc=4, argv=0x7fffffffe678) at lvm.c:22

この時点でのドライブの状態を dmsetup コマンドで確認してみる。 LVM のコマンドを実行するとロックがかかっていて返事が帰ってこないので注意が必要である。

$ sudo dmsetup ls
test--vg-pvmove0        (253:1)
test--vg-lvol0  (253:0)
$ sudo dmsetup table test--vg-pvmove0

$ sudo dmsetup table test--vg-lvol
device-mapper: table ioctl on test--vg-lvol  failed: No such device or address
Command failed.
$ sudo dmsetup table test--vg-lvol0
0 1638400 linear 252:17 2048
$ ps xa|grep pv
$ sudo dmsetup info test--vg-lvol0
$ sudo dmsetup info test--vg-lvol0
Name:              test--vg-lvol0
State:             SUSPENDED
Read Ahead:        256
Tables present:    LIVE & INACTIVE
Open count:        0
Event number:      2
Major, minor:      253, 0
Number of targets: 1
UUID: LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd

$ sudo dmsetup info test--vg-pvmove0
Name:              test--vg-pvmove0
State:             ACTIVE
Read Ahead:        256
Tables present:    INACTIVE
Open count:        1
Event number:      0
Major, minor:      253, 1
Number of targets: 0
UUID: LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRLrPsQNjTAK4x174waVadurAGk7Di1AgK

この時点ではまだ古い構成のままで動いているが、上記 dmsetup info の出力でわかるように inactive table が設定されている。253:1 というのは test--vg-pvmove0 のことなので、inactive table が有効になれば test--vg-lvol0test--vg-pvmove0 を指すようになる。

$ sudo dmsetup table --inactive test--vg-lvol0
0 1638400 linear 253:1 0
$ sudo dmsetup table --inactive test--vg-pvmove0
0 1638400 mirror core 1 4096 2 252:17 2048 252:33 2048

この _resume_node は2回呼ばれる。一回目が実行されると test--vg-pvmove0 が設定される。

$ sudo dmsetup info  test--vg-pvmove0
Name:              test--vg-pvmove0
State:             ACTIVE
Read Ahead:        256
Tables present:    LIVE
Open count:        1
Event number:      1
Major, minor:      253, 1
Number of targets: 1
UUID: LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRLrPsQNjTAK4x174waVadurAGk7Di1AgK

$ sudo dmsetup table  test--vg-pvmove0
0 1638400 mirror core 1 4096 2 252:17 2048 252:33 2048

もう一度 _resume_node が呼ばれると test--vg-lvol0 も設定される。

$ sudo dmsetup info  test--vg-lvol0
Name:              test--vg-lvol0
State:             ACTIVE
Read Ahead:        256
Tables present:    LIVE
Open count:        0
Event number:      2
Major, minor:      253, 0
Number of targets: 1
UUID: LVM-yXZJ0vs4vMOgRJdnprKJdbnCXjkyqZWRQY7J8fLzVE7pnV5d7eVOpaBwwVGn3IJd

$ sudo dmsetup table test--vg-lvol0
0 1638400 linear 253:1 0

解説

前章でおおよその動きはわかったので、以下ではソースコードを見ながら確認していく。

先程の _resume_nodelvm2/device_mapper/libdm-deptree.c から引用する。 ソースコードは https://sourceware.org/git/?p=lvm2.git;a=tree から参照できる。

lvm2 では ioctl を直接発行するのではなく、 struct dm_task を作成して dm_task_run 関数経由で行うようになっていて若干読みづらいが、実行しているのは ioctl(DM_DEV_SUSPEND) による suspend の解除である。

/* FIXME Merge with _suspend_node? */
static int _resume_node(const char *name, uint32_t major, uint32_t minor,
                        uint32_t read_ahead, uint32_t read_ahead_flags,
                        struct dm_info *newinfo, uint32_t *cookie,
                        uint16_t udev_flags, int already_suspended)
{
        struct dm_task *dmt;
        int r = 0;

        log_verbose("Resuming %s (" FMTu32 ":" FMTu32 ").", name, major, minor);

        if (!(dmt = dm_task_create(DM_DEVICE_RESUME))) {
                log_debug_activation("Suspend dm_task creation failed for %s.", name);
                return 0;
        }
(略)
        if (!(r = dm_task_run(dmt)))
                goto_out;
(略)
}

対応するカーネルの実装も見ていく。 上の実験で使った Ubuntu に (ほぼ) 合わせて、以下では linux-5.4.0 を参照する。 ちなみに、最近 make_request 系の関数名がごっそり submit_bio に変更されたが、5.4.0 はその変更以前である。

ioctl の実装は linux/drivers/md/dm-ioctl.c にあり、ioctl(DM_DEV_SUSPEND) の実装は以下のようになっている。 resume 時の処理 do_resume を見ると new_map があるときは table を swap していることがわかる。呼ばれたときに suspend されていなければ一旦 dm_suspend を呼んでから swap するという安心設計である。

static int do_resume(struct dm_ioctl *param)
{
        int r = 0;
        unsigned suspend_flags = DM_SUSPEND_LOCKFS_FLAG;
        struct hash_cell *hc;
        struct mapped_device *md;
        struct dm_table *new_map, *old_map = NULL;

        down_write(&_hash_lock);

        hc = __find_device_hash_cell(param);
        if (!hc) {
                DMDEBUG_LIMIT("device doesn't appear to be in the dev hash table
.");
                up_write(&_hash_lock);
                return -ENXIO;
        }

        md = hc->md;

        new_map = hc->new_map;
        hc->new_map = NULL;
        param->flags &= ~DM_INACTIVE_PRESENT_FLAG;

        up_write(&_hash_lock);

        /* Do we need to load a new map ? */
        if (new_map) {
                /* Suspend if it isn't already suspended */
                if (param->flags & DM_SKIP_LOCKFS_FLAG)
                        suspend_flags &= ~DM_SUSPEND_LOCKFS_FLAG;
                if (param->flags & DM_NOFLUSH_FLAG)
                        suspend_flags |= DM_SUSPEND_NOFLUSH_FLAG;
                if (!dm_suspended_md(md))
                        dm_suspend(md, suspend_flags);

                old_map = dm_swap_table(md, new_map);
(略)
}

/*
 * Set or unset the suspension state of a device.
 * If the device already is in the requested state we just return its status.
 */
static int dev_suspend(struct file *filp, struct dm_ioctl *param, size_t param_size)
{
        if (param->flags & DM_SUSPEND_FLAG)
                return do_suspend(param);

        return do_resume(param);
}

dev_suspend 関数のコメントを見ると既に request state になっていたら何もしないということが書いてあるが、これは嘘であって、実際の動作は上に書いた通りである。 linux/include/uapi/linux/dm-ioctl.h の以下のコメントがわかりやすい。

/*
 * A traditional ioctl interface for the device mapper.
 *
 * Each device can have two tables associated with it, an
 * 'active' table which is the one currently used by io passing
 * through the device, and an 'inactive' one which is a table
 * that is being prepared as a replacement for the 'active' one.
 *
(略)
 * DM_SUSPEND:
 * This performs both suspend and resume, depending which flag is
 * passed in.
 * Suspend: This command will not return until all pending io to
 * the device has completed.  Further io will be deferred until
 * the device is resumed.
 * Resume: It is no longer an error to issue this command on an
 * unsuspended device.  If a table is present in the 'inactive'
 * slot, it will be moved to the active slot, then the old table
 * from the active slot will be _destroyed_.  Finally the device
 * is resumed.
 *
(略)
 * DM_TABLE_LOAD:
 * Load a table into the 'inactive' slot for the device.  The
 * device does _not_ need to be suspended prior to this command.

ここまで調べてきて、device mapper は suspend&resume の仕掛けを使って現在使用中のデバイスの設定を変えられるように作られていることと、pvmove がそれを使って動いていることがわかった。

root filesystem でなければもう pvmove を実行してディスク移行を完了してよい気がする (が後処理についてはまだ検討していないので後述)。 しかし、dm_suspend を実行した後に運悪く pvmove のテキストを読もうとディスクアクセスが発生したりすると大変なことになる。 開発者もその辺は当然承知の上で mlock をしているのだが、lvm2/lib/mm/memlock.c を見ると /proc/self/map を見て必要なものだけ選択的に mlock するということをやっていて、相当昔からあるコードなので大丈夫なはずではあるがちょっと命を賭ける気はしないなという印象である。(設定で mlockall を使うようにはできる)

念のため memlock に至る流れを確認しておく。以下のように _lv_update_and_reload 関数から _lv_suspend が呼ばれるところである。 critical_section_inc 関数は第2引数を見ていて、"suspend" のときだけ memlock が実行されるという不思議な関数である。

#0  _lock_mem_if_needed (cmd=0x555555a78048) at mm/memlock.c:581
#1  0x00005555556a9a3b in critical_section_inc (cmd=0x555555a71520, 
    reason=0x555555755f25 "suspending") at mm/memlock.c:623
#2  0x00005555555f9db5 in _lv_suspend (cmd=0x555555a71520, lvid_s=0x0, 
    laopts=0x7fffffffde00, error_if_not_suspended=0, lv=0x5555562d24f8, 
    lv_pre=0x5555562ce4e8) at activate/activate.c:2200
#3  0x00005555555fa1f9 in lv_suspend_if_active (cmd=0x555555a71520, 
    lvid_s=0x0, origin_only=0, exclusive=0, lv=0x5555562d24f8, 
    lv_pre=0x5555562ce4e8) at activate/activate.c:2276
#4  0x00005555555fc142 in suspend_lv (cmd=0x555555a71520, lv=0x5555562ce4e8)
    at activate/activate.c:2914
#5  0x000055555565cfaa in _lv_update_and_reload (lv=0x5555562ce4e8,
    origin_only=0) at metadata/lv_manip.c:6698
#6  0x000055555565d136 in lv_update_and_reload (lv=0x5555562ce4e8)
    at metadata/lv_manip.c:6724

pvmove の後処理

pvmove コマンドは一時 LV を作成して mirroring を設定してコピーした後に、再度設定を変更してミラーリングを解除して一時 LV を削除するといった処理を行う。 しかし strace などで調べてもそれらの処理が実行されている気配はない。

LVM の設定に依存するが、この後処理は lvmpolld から実行される。 実際の処理は lvm2/daemon/lvmpolld/lvmpolld-cmd-utils.ccmdargv_ctr で作成したコマンドラインに従ってプロセスを起動することによって行われる。 今回の例では以下のようになる。 (--interval 引数と LV 名は pvmove への引数に依存する)

/sbin/lvm lvpoll --interval 4 --polloperation pvmove test-vg/pvmove0 -An

lvmpolld の man page を見ると、polling 時のバックグラウンドプロセス数を減らすためにこうなっていると書いてあるが、上記のように fork してはその目的を達成できない気がして若干謎ではある。

処理の実体は lvm2/tools/pvmove_poll.c にある。lv_update_and_reload 関数を呼んでいるので先程の処理と同様に ioctl(DM_SUSPEND) と memlock が実行されているはずであり、strace で追跡した限りではそう動作しているように見える。

dm_suspend / dm_resume の詳細

カーネル側の処理は ioctl の入口あたりのコードを上で簡単に触れた。 以下では ioctl(DM_SUSPEND) を実行するのが do_suspenddo_resume であり、 それぞれから linux/drivers/md/dm.c にある dm_suspenddm_resume が呼ばれる。 do_suspend は単純なラッパーだが、do_resume は同時に table を切り替える処理が行われるというのは上に述べた。

bio request をブロックしてデータの整合性を保つのが dm_suspenddm_resume の役割であり、以下ではこれらの動作についてちょっと調べる。

dm_suspend の処理は __dm_suspend に実装されている。 今回は noflush が true で do_lockfs は false で呼ばれている。 noflush suspending では処理途中の bio を実行せず queue に積みなおすという処理が行われているようであるが、細かい所まで処理が追えなかったので詳細は省略する。 最終的には DMF_BLOCK_IO_FOR_SUSPEND ビットをセットすることで新規の bio が始まるのを阻止している。

static int __dm_suspend(struct mapped_device *md, struct dm_table *map,
                        unsigned suspend_flags, long task_state,
                        int dmf_suspended_flag)
{
        bool do_lockfs = suspend_flags & DM_SUSPEND_LOCKFS_FLAG;
        bool noflush = suspend_flags & DM_SUSPEND_NOFLUSH_FLAG;
        int r;

        lockdep_assert_held(&md->suspend_lock);

        /*
         * DMF_NOFLUSH_SUSPENDING must be set before presuspend.
         * This flag is cleared before dm_suspend returns.
         */
        if (noflush)
                set_bit(DMF_NOFLUSH_SUSPENDING, &md->flags);
        else
                pr_debug("%s: suspending with flush\n", dm_device_name(md));
(略)
        /*
         * Here we must make sure that no processes are submitting requests
         * to target drivers i.e. no one may be executing
         * __split_and_process_bio. This is called from dm_request and
         * dm_wq_work.
         *
         * To get all processes out of __split_and_process_bio in dm_request,
         * we take the write lock. To prevent any process from reentering
         * __split_and_process_bio from dm_request and quiesce the thread
         * (dm_wq_work), we set BMF_BLOCK_IO_FOR_SUSPEND and call
         * flush_workqueue(md->wq).
         */
        set_bit(DMF_BLOCK_IO_FOR_SUSPEND, &md->flags);
        if (map)
                synchronize_srcu(&md->io_barrier);

        /*
         * Stop md->queue before flushing md->wq in case request-based
         * dm defers requests to md->wq from md->queue.
         */
        if (dm_request_based(md))
                dm_stop_queue(md->queue);

        flush_workqueue(md->wq);
(略)

DMF_BLOCK_IO_FOR_SUSPEND ビットを参照しているのは dm_make_request で、 このビットがセットされていると dm_process_bio を呼ばずに queue_io を呼んでデバイスの deferred リストに追加して処理を後回しにする。

後回しにした処理は dm_resume が呼ばれたときに実行される。 以下の dm_queue_flush が呼ばれ、 dm_wq_work から dm_process_bio が呼ばれて処理が実行される。 (今回は request based dm は使っていないのでそれに関しては省略した。)

ざっと見た限りでは特に難しいことはなく、シンプルに実装されているように見える。

/*
 * Process the deferred bios
 */
static void dm_wq_work(struct work_struct *work)
{
        struct mapped_device *md = container_of(work, struct mapped_device,
                                                work);
        struct bio *c;
        int srcu_idx;
        struct dm_table *map;

        map = dm_get_live_table(md, &srcu_idx);

        while (!test_bit(DMF_BLOCK_IO_FOR_SUSPEND, &md->flags)) {
                spin_lock_irq(&md->deferred_lock);
                c = bio_list_pop(&md->deferred);
                spin_unlock_irq(&md->deferred_lock);

                if (!c)
                        break;

                if (dm_request_based(md))
                        (void) generic_make_request(c);
                else
                        (void) dm_process_bio(md, map, c);
        }

        dm_put_live_table(md, srcu_idx);
}

static void dm_queue_flush(struct mapped_device *md)
{
        clear_bit(DMF_BLOCK_IO_FOR_SUSPEND, &md->flags);
        smp_mb__after_atomic();
        queue_work(md->wq, &md->work);
}

実験

pvmove の対象となる LV に対してアクセスを行いながら pvmove を実行し、 ioctl(DM_SUSPEND) の影響を調べた。 具体的には LV に ext4 ファイルシステムを作成して mount し、以下の python スクリプトを実行しながら pvmove を実行した。 checksum を計算してデータ化けを検出しようとしているが、read はおそらく全て page cache を見にいっているだけなので気休めである。 スクリプト内の os.fsync を省略すると pvmove 実行中もスクリプトの動作が止まることなく約 5ms 間隔でタイムスタンプが出力され続けることも確認できる。

import binascii
import os
import sys
import time

def tick(f):
    f.seek(0)
    sum = f.read(9)
    f.seek(9)
    if len(sum):
        sum2 = binascii.crc32(f.read())
        assert int(sum, 16) == sum2
        f.seek(0, 2)
    f.write(("%f\n" % time.time()).encode('ascii'))
    f.flush()
    os.fsync(f.fileno())
    f.seek(9)
    sum3 = binascii.crc32(f.read())
    f.seek(0)
    f.write(("%08x\n" % sum3).encode('ascii'))
    
if __name__ == '__main__':
    fp = open(sys.argv[1], 'wb+')
    while True:
        time.sleep(.005)
        tick(fp)

以下のように ftrace を使ってカーネルのどの処理が呼ばれているかを調べた。

sudo trace-cmd record  -p function_graph -e syscalls -g dm_make_request python3 test.py /mnt/test

上で見た通り、ioctl(DM_SUSPEND) の影響で dm_make_request から queue_io が呼ばれるはずであるが、以下のようなログが得られた。 jbd2 というのは ext4 のジャーナルを書きこむ kernel thread である。

          pvmove-2849  [001] 67172.031115: sys_enter_ioctl:      fd: 0x00000003, cmd: 0xc138fd06, arg: 0x555f3aba1070
     jbd2/dm-0-8-1309  [001] 67172.031464: funcgraph_entry:                   |  dm_make_request() {
     jbd2/dm-0-8-1309  [001] 67172.031464: funcgraph_entry:        0.188 us   |    __srcu_read_lock();
     jbd2/dm-0-8-1309  [001] 67172.031465: funcgraph_entry:        0.176 us   |    __srcu_read_unlock();
     jbd2/dm-0-8-1309  [001] 67172.031465: funcgraph_entry:                   |    queue_io() {
     jbd2/dm-0-8-1309  [001] 67172.031465: funcgraph_entry:        0.173 us   |      _raw_spin_lock_irqsave();
     jbd2/dm-0-8-1309  [001] 67172.031465: funcgraph_entry:        0.179 us   |      _raw_spin_unlock_irqrestore();
     jbd2/dm-0-8-1309  [001] 67172.031466: funcgraph_entry:                   |      queue_work_on() {
     jbd2/dm-0-8-1309  [001] 67172.031466: funcgraph_entry:                   |        __queue_work() {
     jbd2/dm-0-8-1309  [001] 67172.031466: funcgraph_entry:        0.186 us   |          get_work_pool();

0xc138fd06 というのが ioctl(DM_SUSPEND) であって、その直後に呼ばれた dm_make_request から queue_io が呼ばれていることがわかる。

他にも以下のようなパターンも記録された。こっちでは、上記のスクリプトから実行された fsync が発行した IO が queue_io に渡されている。 いずれも write なので queue_io は一瞬で完了し、処理は続行する。 dm_suspend のために処理が中断するのは、 read の場合か fsync のように write の完了を待つ処理が入ったときである。

             lvm-2887  [001] 67180.401042: sys_enter_ioctl:      fd: 0x00000003, cmd: 0xc138fd06, arg: 0x55f423d00140
         python3-2847  [000] 67180.401044: sys_enter_newfstat:   fd: 0x00000003, statbuf: 0x7ffe3090b080
         python3-2847  [000] 67180.401050: sys_exit_newfstat:    0x0
         python3-2847  [000] 67180.401053: sys_enter_read:       fd: 0x00000003, buf: 0x015fac50, count: 0x00004d59
         python3-2847  [000] 67180.401096: sys_exit_read:        0x4d58
         python3-2847  [000] 67180.401098: sys_enter_read:       fd: 0x00000003, buf: 0x015ff9a8, count: 0x00000001
         python3-2847  [000] 67180.401103: sys_exit_read:        0x0
         python3-2847  [000] 67180.401227: sys_enter_lseek:      fd: 0x00000003, offset: 0x00000000, whence: 0x00000000
         python3-2847  [000] 67180.401229: sys_exit_lseek:       0x0
         python3-2847  [000] 67180.401242: sys_enter_select:     n: 0x00000000, inp: 0x00000000, outp: 0x00000000, exp: 0x00000000, tvp: 0x7ffe3090b350
         python3-2847  [000] 67180.406363: sys_exit_select:      0x0
         python3-2847  [000] 67180.406382: sys_enter_write:      fd: 0x00000003, buf: 0x015a0460, count: 0x00000009
         python3-2847  [000] 67180.406428: sys_exit_write:       0x9
         python3-2847  [000] 67180.406434: sys_enter_lseek:      fd: 0x00000003, offset: 0x00000000, whence: 0x00000000
         python3-2847  [000] 67180.406436: sys_exit_lseek:       0x0
         python3-2847  [000] 67180.406441: sys_enter_lseek:      fd: 0x00000003, offset: 0x00000000, whence: 0x00000001
         python3-2847  [000] 67180.406442: sys_exit_lseek:       0x0
         python3-2847  [000] 67180.406445: sys_enter_read:       fd: 0x00000003, buf: 0x015a0460, count: 0x00001000
         python3-2847  [000] 67180.406467: sys_exit_read:        0x1000
         python3-2847  [000] 67180.406479: sys_enter_lseek:      fd: 0x00000003, offset: 0x00000000, whence: 0x00000001
         python3-2847  [000] 67180.406481: sys_exit_lseek:       0x1000
         python3-2847  [000] 67180.406484: sys_enter_lseek:      fd: 0x00000003, offset: 0x00000000, whence: 0x00000001
         python3-2847  [000] 67180.406486: sys_exit_lseek:       0x1000
         python3-2847  [000] 67180.406487: sys_enter_newfstat:   fd: 0x00000003, statbuf: 0x7ffe3090b080
         python3-2847  [000] 67180.406493: sys_exit_newfstat:    0x0
         python3-2847  [000] 67180.406495: sys_enter_read:       fd: 0x00000003, buf: 0x015fbc70, count: 0x00003d62
         python3-2847  [000] 67180.406509: sys_exit_read:        0x3d61
         python3-2847  [000] 67180.406511: sys_enter_read:       fd: 0x00000003, buf: 0x015ff9d1, count: 0x00000001
         python3-2847  [000] 67180.406516: sys_exit_read:        0x0
         python3-2847  [000] 67180.406647: sys_enter_lseek:      fd: 0x00000003, offset: 0x00000000, whence: 0x00000002
         python3-2847  [000] 67180.406649: sys_exit_lseek:       0x4d61
         python3-2847  [000] 67180.406666: sys_enter_write:      fd: 0x00000003, buf: 0x015a0460, count: 0x00000012
         python3-2847  [000] 67180.406702: sys_exit_write:       0x12
         python3-2847  [000] 67180.406704: sys_enter_lseek:      fd: 0x00000003, offset: 0x00000000, whence: 0x00000001
         python3-2847  [000] 67180.406706: sys_exit_lseek:       0x4d73
         python3-2847  [000] 67180.406711: sys_enter_fsync:      fd: 0x00000003
         python3-2847  [000] 67180.406742: funcgraph_entry:                   |  dm_make_request() {
         python3-2847  [000] 67180.406743: funcgraph_entry:        0.715 us   |    __srcu_read_lock();
         python3-2847  [000] 67180.406744: funcgraph_entry:        0.435 us   |    __srcu_read_unlock();
         python3-2847  [000] 67180.406745: funcgraph_entry:                   |    queue_io() {
         python3-2847  [000] 67180.406745: funcgraph_entry:        0.436 us   |      _raw_spin_lock_irqsave();
         python3-2847  [000] 67180.406751: funcgraph_entry:                   |      _raw_spin_unlock_irqrestore() {
         python3-2847  [000] 67180.406752: funcgraph_entry:                   |        smp_apic_timer_interrupt() {
         python3-2847  [000] 67180.406753: funcgraph_entry:                   |          irq_enter() {

まとめ

単純な LVM 構成を例として、オンラインで PV を変更する pvmove コマンドを中心に動作を調べた。 カーネルの device mapper は suspend&resume の仕掛けで動作中の table を切り替えられることと、 lvm コマンドはその仕掛けを利用して動作していることを述べた。 suspend により pvmove の対象となった LV へのアクセスが一時停止するので、lvm バイナリが存在する root filesystem のような LV の適用は若干の不安があるものの、この調査のきっかけとなったシステムでの PV 移行は pvmove を発行してしばらくじっと待つことで無事にリモートから完了することができた。

だが、後日おそらくハードウェア障害によって IO error が発生して夜中にリセットボタンを押しにいく羽目になった。調べてみたところどうも BIOS が古くて NVMe の安定性に問題があるようで、思わぬところで足をすくわれるものだと思った。 こうなっては、また pvmove して元に戻すべきか悩むところである。