コンテンツにスキップ

WSL2から外付けSSDへの巨大ファイル転送を高速化するまで

WSL2 から Windows 側の外付け SSD へのコピーが遅かったため、読み出し・書き込み・実データの転送を分けて調べました。50GB 級の JSONL 数個で比較した結果と、dd、rclone、cpの使い分けを示します。大量の小ファイルは対象にしていません。

最初は WSL から Windows 側にマウントされた外付け SSD へcpしましたが、50GB 程度のファイルに約 16 分(単純計算で約 52MB/s)かかりました。そこで別のコピーコマンドを順番に試すのではなく、「コピー元から読む」「コピー先へ書く」「実データをコピーする」の 3 段階へ分けてボトルネックを調べました。

検証すると、同じ WSL から/mnt/dへのコピーでも、dd、rclone、cpで大きな速度差が出ました。単発の巨大ファイル転送ではddが速かった一方、ディレクトリ単位の増分コピーまで考えると rclone の方が扱いやすく、最終的には rclone を使う構成にしました。

検証環境と比較条件

検証環境は次のとおりです。

項目 環境
実行環境 Windows上のWSL2
転送元 WSLのLinuxファイルシステム
転送先 WSL から/mnt/dとして見える外付け SSD
外付けSSD BUFFALO SSD-PGC500U3-BC 500GB
接続 USB 3.2
データ 数GB〜50GB級のJSONL / JSONL.zst
rclone v1.75.0

以降は長いパスを何度も書かないため、コピー元をSRCとして表記します。

SRC=/home/wsl-path/to/change.jsonl

この記事の速度比較は、厳密なストレージベンチマークではありません。コマンドごとに、計測の終了条件が次のように異なります。

  • dd: conv=fdatasyncを指定する
  • cp: 最後にsyncを実行する
  • rclone: rclone 自身の統計と wall-clock time を確認する

終了条件がそろっていないため、ここでの値は数%単位の優劣ではなく、「今回の実行環境でどの程度の差が観測されたか」を判断する材料として扱います。

また、速度の単位は rclone が MiB/s、ddが MB/s で表示していたため、以降は rclone の値に MB/s への換算値を併記します。1MiB は約 1.049MB なので、たとえば rclone の 117MiB/s は約 123MB/s に当たります。

I/Oを分解してボトルネックを調べる

冒頭のcpの結果(50GB 程度で約 16 分)だけでは、コピー元、外付け SSD、USB、WSL から Windows 側へのアクセス経路のどこが遅いのか分かりません。

そこで、次の 3 点を個別に測りました。

  1. 読み込み: WSL 側のファイル → /dev/null
  2. 書き込み: /dev/zero → 外付け SSD
  3. 実データコピー: WSL 側のファイル → 外付け SSD(/mnt/d)

まず、コピー元の JSONL をどの程度の速度で読めるか確認します。/dev/nullへ出力すれば、元ファイルを変更せず読み込み性能を測れます。

1
2
3
4
5
dd \
  if="$SRC" \
  of=/dev/null \
  bs=16M \
  status=progress

約 21GB の JSONL で結果は 380MB/s でした。

21324356252 bytes copied, 56.1249 s, 380 MB/s

次に、コピー元ファイルの読み込みを除外し、外付け SSD への書き込みだけを測ります。

1
2
3
4
5
6
7
dd \
  if=/dev/zero \
  of=/mnt/d/wsl_write_test.bin \
  bs=16M \
  count=1024 \
  status=progress \
  conv=fdatasync

16GiB を書き込んだ結果は約 180MB/s でした。

17179869184 bytes copied, 95.3229 s, 180 MB/s

コピー元は 380MB/s で読め、コピー先には 180MB/s 程度で書けています。したがって、cpで観測した約 52〜61MB/s を、コピー元または SSD 単体の速度だけでは説明できません。

最後に、実際の JSONL をddで SSD へコピーしました。

1
2
3
4
5
6
dd \
  if="$SRC" \
  of=/mnt/d/wsl-bench/change-dd.jsonl \
  bs=16M \
  status=progress \
  conv=fdatasync

結果は 154MB/s でした。

21324356252 bytes copied, 138.13 s, 154 MB/s

開始から終了まで速度はおおむね一定でした。少なくとも今回使用した約 21GB のファイルでは、書き込み途中で数十 MB/s まで急落する挙動は確認できませんでした。

ここまでの結果を整理します。

計測 実測 判断
WSL側JSONL → /dev/null 380MB/s コピー元の読み込みは十分速い
/dev/zero → SSD 180MB/s SSD への書き込みは約 180MB/s
実JSONLをddでコピー 154MB/s 実データでも150MB/s前後出る
最初のcp 約52MB/s 上記だけでは説明できない

この時点で分かったのは、ストレージ単体の性能だけではなく、WSL から/mnt/dへデータを渡す際のコピー方法によって結果が大きく変わることです。ただし、この検証だけではcp内部のどの処理や WSL のどの層が遅延の原因になっているかまでは特定していません。

同じファイルでもcpとddに大きな差が出た

比較条件を近づけるため、ddで 154MB/s 出た約 21GB の JSONL をcpでも転送しました。cp終了後にはsyncを実行しています。

1
2
3
sync

/usr/bin/time -v sh -c "cp '$SRC' /mnt/d/wsl-bench/change-cp.jsonl && sync"

結果は 5 分 48.79 秒でした。

Elapsed (wall clock) time (h:mm:ss or m:ss): 5:48.79
Exit status: 0

約 21.3GB を約 349 秒で処理しているため、単純計算では約 61MB/s です。ddの 154MB/s と比べると約 2.5 倍の差になりました。

dd : 154 MB/s
cp : 約61 MB/s

前述したとおり、ddのfdatasyncとcp && syncでは終了条件が厳密には同一ではなく、特にsyncは対象ファイル以外の dirty data の影響も受ける可能性があります。そのため、この結果から「ddは常にcpの 2.5 倍速い」と一般化はできません。

それでも、同じマシン、同じコピー元、同じ/mnt/d配下への書き込みで数分単位の差が再現したため、今回の用途ではcp以外の方法を検討する価値があると判断しました。

ddは速いが増分コピーには使いにくい

単発で巨大ファイルを 1 本コピーするだけなら、今回試した方法ではddが最も速い結果になりました。

しかし実際の用途では、ディレクトリ全体について次のように処理したい場合があります。

  • コピー先に存在しないファイルは転送する
  • 更新されたファイルは上書きする
  • 同一ファイルは読み直さずスキップする
  • コピー先だけに存在するファイルは残す
  • サブディレクトリも再帰的に処理する

これをddだけで実現するには、ファイルの存在確認や更新判定を自前で実装することになります。転送速度だけではなく、増分コピーの管理まで含めて考えた結果、rclone を試しました。

rcloneは管理機能込みで約117MiB/s

まず単一ストリームで rclone を実行しました。

1
2
3
4
5
6
7
8
rclone copyto \
  "$SRC" \
  /mnt/d/wsl-bench/change-rclone.jsonl \
  --transfers 1 \
  --buffer-size 16M \
  --ignore-checksum \
  --progress \
  --stats 1s

約 19.86GiB を転送したとき、rclone 自身の表示では 117.022MiB/s でした。10 進表記へ換算すると約 123MB/s です。

Transferred: 19.860 GiB / 19.860 GiB, 100%, 117.022 MiB/s
Elapsed time: 3m0.8s

/usr/bin/timeでプロセス全体を測ると約 3 分 9 秒でした。

Elapsed (wall clock) time (h:mm:ss or m:ss): 3:09.34
Exit status: 0

rclone 内部の転送時間とプロセス全体の実時間には差があるため、117MiB/s とddの 154MB/s をそのまま同じ測定値として比較するべきではありません。ただ、cpより明確に短時間で終了し、ディレクトリ走査や更新判定も rclone へ任せられるため、今回の用途では有力な候補になりました。

ここでは速度測定のため--ignore-checksumを付けています。このオプションを付けた構成を、データの完全性を重視する用途へそのまま適用するつもりはありません。最終設定では、安全側と速度優先側を分けます。

rcloneの並列化は逆効果だった

50GB 級の巨大ファイルなら、複数ストリームへ分割するとさらに速くなる可能性も考えました。そこで 2 ストリームを試しました。

rclone copyto \
  "$SRC" \
  /mnt/d/wsl-bench/change-rclone.jsonl \
  --transfers 1 \
  --buffer-size 16M \
  --ignore-checksum \
  --multi-thread-streams 2 \
  --multi-thread-chunk-size 64M \
  --multi-thread-write-buffer-size 8M \
  --progress

結果は 87.528MiB/s(約 91.8MB/s)でした。

single stream : 117.022 MiB/s ≒ 122.7 MB/s
2 streams     :  87.528 MiB/s ≒  91.8 MB/s

今回の環境では、2 ストリームにすると rclone 内部の表示速度が約 25%低下しました。

書き込み先は 1 台の外付け SSD なので、ストリーム数を増やしても SSD の帯域自体は増えません。並列化によるオーバーヘッドや書き込みパターンの変化が影響した可能性はありますが、この検証では低下の内部原因までは追っていません。

少なくとも今回の構成では、マルチスレッド化を採用する根拠は得られなかったため、単一ストリームを使うことにしました。

転送済みファイルの判定に数分かかった

rclone の転送速度は実用範囲に入りましたが、増分コピーとして使ったときに別の性能問題が見つかりました。

同じファイルをもう一度copytoすると、実際には何も転送していないにもかかわらず数分かかりました。

Transferred: 0 B / 0 B

この問題を理解する鍵は、既存ファイルの判定です。今回の local-to-local コピーでは、サイズと更新時刻が同一と判断できれば、巨大ファイルの中身を読み直さずスキップできます。

ところが詳細ログを確認すると、サイズは一致しているものの、更新時刻に差がありました。

size = 21324356252 OK
Modification times differ by -158.919992ms

os.stat()でも確認しました。

1
2
3
4
5
6
7
source size : 21324356252
dest size   : 21324356252

source mtime_ns: 1788417007158919992
dest   mtime_ns: 1788417007000000000

difference ms  : 158.919992

今回確認したファイルでは、コピー元の mtime が...07.158919992だったのに対し、/mnt/d側では...07.000000000になっていました。

ここから「Windows 側の mtime は常に秒精度である」と一般化はしません。今回確認した WSL から/mnt/dへの転送経路では、少なくともこのファイルについて秒未満の mtime が保持されていませんでした。

この約 159ms の差があるため、rclone は更新時刻だけでは同一と判断できませんでした。その後のログでは MD5 計算へ進んでいます。

1
2
3
md5 = 549256f27c671d518ba009c5e737a9c5 OK
Updated modification time in destination
Unchanged skipping

つまり転送量は 0B でも、巨大ファイルの内容を読み直していました。約 20GB のファイルでは、この同一性確認だけで数分かかります。

注意したいのは、--ignore-checksumを指定していても、このケースでは MD5 計算が発生したことです。今回の実測から、--ignore-checksumだけでは「mtime 不一致による既存ファイルの内容確認」を避けられないと分かりました。

--modify-window 1sで0.15秒まで短縮できた

mtime の差を同一とみなすため、最初は--modify-windowに1msを指定しました。

1
2
3
4
5
6
7
rclone copyto \
  "$SRC" \
  /mnt/d/wsl-bench/change-rclone.jsonl \
  --modify-window 1ms \
  --ignore-checksum \
  --transfers 1 \
  -vv

しかし、実測した mtime 差は約 159ms です。1ms では許容範囲に入らず、再び MD5 計算が発生しました。

1
2
3
4
Modification times differ by -158.919992ms
...
md5 = 549256f27c671d518ba009c5e737a9c5 OK
Unchanged skipping

この実行では、何も転送していないにもかかわらず完了まで数分かかりました。

そこで--modify-windowを 1 秒へ変更しました。

1
2
3
4
5
6
7
rclone copyto \
  "$SRC" \
  /mnt/d/wsl-bench/change-rclone.jsonl \
  --modify-window 1s \
  --ignore-checksum \
  --transfers 1 \
  -vv

別の検証ファイルでは mtime に約 173ms の差がありましたが、1 秒以内なので同一と判断されました。

1
2
3
Size and modification time the same
(differ by -173.246181ms, within tolerance 1s)
Unchanged skipping

/usr/bin/timeでの実時間は 0.15 秒です。

Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.15
Exit status: 0

数 GB〜数十 GB のファイルを読み直す処理そのものが発生しなくなったため、増分コピーとしての使い勝手が大きく変わりました。

--modify-window 1sは、どの環境でも付けるべき値という意味ではありません。今回は実際に 100ms 単位の mtime 差を観測し、秒未満が保持されていないファイルがあったため 1 秒を選びました。別のファイルシステムやマウント方法では、実際の mtime 差を確認してから決めるべきです。

実測結果の一覧

今回の実測結果を 1 つの表にまとめます。rclone の値は MB/s へ換算した値も併記しています。

方法 観測結果 用途上の判断
WSL側JSONLの読み込み 380MB/s コピー元は十分速い
SSDへの/dev/zero書き込み 180MB/s コピー先への書き込みは約 180MB/s
実データをdd 154MB/s 単発コピーでは最速
rclone 単一ストリーム 117.0MiB/s ≒ 122.7MB/s 増分コピー向け
rclone 2 ストリーム 87.5MiB/s ≒ 91.8MB/s 今回は逆効果
cp + sync 約61MB/s相当 今回の経路では遅かった
rclone、mtime許容不足 数分 MD5計算で全体を読み込む
rclone、--modify-window 1s 0.15秒 既存ファイルを即スキップ

この表の数値は、終了条件をそろえたベンチマークの結果ではないため、数%の性能差を比較する用途には使えません。一方、cpが数分、ddが約 2 分強、既存ファイル判定が数分から 0.15 秒になったといった桁の違いは、今回の運用方法を決める材料として十分でした。

最終的に採用した構成

データの完全性を優先する場合は、まず次の設定を基本形にします。

1
2
3
4
5
6
7
8
rclone copy \
  /home/nishikoh/path/to/source/ \
  /mnt/d/path/to/backup/ \
  --modify-window 1s \
  --transfers 1 \
  --buffer-size 16M \
  --progress \
  --stats 1s

今回rclone copyを選んだのは、転送元に存在しないファイルがコピー先に残っていても削除したくないためです。コピー先から転送元にないファイルを消してまで同じ状態へ揃える必要がある場合は、要件が異なります。

転送後の checksum 確認より速度を優先できるデータなら、--ignore-checksumを追加した構成も候補になります。

1
2
3
4
5
6
7
8
9
rclone copy \
  /home/nishikoh/path/to/source/ \
  /mnt/d/path/to/backup/ \
  --modify-window 1s \
  --transfers 1 \
  --buffer-size 16M \
  --ignore-checksum \
  --progress \
  --stats 1s

たとえば元データを再生成できる場合や、別のタイミングで hash 検証する運用なら検討できます。唯一のバックアップとして保存する重要データなら、速度だけを理由に検証を減らさない方が安全です。

まとめ

WSL から Windows 側の外付け SSD へ巨大ファイルをコピーするときは、最初にコピーコマンドを選ぶより、読み込みと書き込みを分けて測る方が原因を追いやすくなります。

今回の検証から得た判断基準は 3 つです。

  • 単発の巨大ファイルコピーなら、実データを使ってddなどの大きな I/O 単位を持つ方法も測ります。 今回はcpより明確に短時間で終了しました。
  • 差分・増分管理まで必要なら、転送速度だけでなく既存ファイル判定のコストを見ます。 rclone は転送性能だけでなく、不要な再転送を避けられる点を含めて採用しました。
  • WSL と Windows 側ストレージをまたぐ同期では、mtime 差を実測します。 今回は約 100ms 単位の mtime 差が原因で、巨大ファイルの checksum 計算が発生していました。--modify-window 1sを指定すると、0.15 秒でスキップできるようになりました。

「コピーが遅い」という現象だけでは、ストレージが遅いのか、コピー方式が遅いのか、既存ファイルの確認が遅いのかを判断できません。巨大ファイルでは 1 回の余計な全読み込みだけでも数分かかります。そのため、転送速度と同じくらい「何を読まずに済ませられるか」を確認することが重要でした。

参考