はじめに

CIU (CyberAgent group Infrastructure Unit) の西北(@nishi_network)です。
普段はプライベートクラウドで使用しているデータセンターの運用業務に従事しています。

今回は、サーバー間でのデータ転送の中でもブロックレベルのデータ(=ディスクに書き込まれたデータそのもの)の転送を高速化すべく、最適な方法について調査しました。

背景

サーバーのリプレイス時などで環境をまるごとお引越ししたい場合において、通常であればディスクを差し替えることで新しいサーバーでデータを利用することが出来ます。
しかし、RAIDコントローラーによるRAID環境下では、移行元と移行先のRAID設定に互換性がなくRAIDの再設定が必要(=データが失われる)であることが多く、移行元のサーバーから移行先のサーバーへデータを転送する必要が生じます。

この場合、ファイルシステム上のデータであれば scp などを用いて転送することで解決できますが、OSのブート領域を含む場合はブロックレベルのデータを転送する必要が生じます。
通常、ブロックレベルのデータを転送する場合は dd などを用いて行いますが、 これをネットワーク越しに行う必要があり、加えてネットワークでの転送にもサーバーのリソースを消費するため十分な性能がでないケースが多々あります。

こういった背景から、今回はサーバー間でのブロックレベルのデータ転送を高速化する手法について調査しました。

検証環境

今回は検証用のサーバーを2台用意し、この2台のサーバー間でデータを転送して速度を計測しました。
検証用サーバーのスペックとOSは次のとおりです。

CPU : AMD EPYC 7551P 32-Core Processor
MEM : DDR4 ECC RDIMM 64GB
DISK : NVMe U.2 3.2TB
NW : 25GbE x2 (LACP)
OS : Ubuntu 22.04.1 LTS (5.15.0-46-generic)

また、ネットワーク帯域とディスク性能が調査結果に影響していないか判断するため、事前に性能を把握しておきます。

iperfによるネットワーク帯域テスト

# iperf -c 10.0.0.2
------------------------------------------------------------
Client connecting to 10.0.0.2, TCP port 5001
TCP window size:  325 KByte (default)
------------------------------------------------------------
[  1] local 10.0.0.1 port 58212 connected with 10.0.0.2 port 5001
[ ID] Interval       Transfer     Bandwidth
[  1] 0.0000-10.0068 sec  27.1 GBytes  23.3 Gbits/sec

23.3Gbits/secと十分な性能が出ていることが確認できました。

fioによるディスク性能テスト (結果一部略)
Sequential read

$ fio -direct=1 -ioengine=libaio -bs=128k -iodepth=64 -numjobs=128 -runtime=30 -group_reporting -name=/dev/nvme0n1 -rw=read
  read: IOPS=24.5k, BW=3059MiB/s (3207MB/s)(90.7GiB/30353msec)
   bw (  MiB/s): min=  708, max= 7213, per=100.00%, avg=3148.29, stdev=12.45, samples=7400
   iops        : min= 5631, max=57683, avg=25158.76, stdev=99.60, samples=7400

Run status group 0 (all jobs):
   READ: bw=3059MiB/s (3207MB/s), 3059MiB/s-3059MiB/s (3207MB/s-3207MB/s), io=90.7GiB (97.4GB), run=30353-30353msec

Sequential write

$ fio -direct=1 -ioengine=libaio -bs=4k -iodepth=64 -numjobs=128 -runtime=30 -group_reporting -name=/dev/nvme0n1 -rw=write
  write: IOPS=674k, BW=2634MiB/s (2762MB/s)(77.2GiB/30019msec); 0 zone resets
   bw (  MiB/s): min= 1314, max= 4544, per=100.00%, avg=2637.92, stdev= 5.47, samples=7552
   iops        : min=336438, max=1163378, avg=675305.58, stdev=1400.69, samples=7552

Run status group 0 (all jobs):
  WRITE: bw=2634MiB/s (2762MB/s), 2634MiB/s-2634MiB/s (2762MB/s-2762MB/s), io=77.2GiB (82.9GB), run=30019-30019msec

こちらも、Readで3.2GB/s、Writeで2.7GB/sと十分な性能が出ていることが確認できました。

加えて今回はデータを圧縮しての転送も試すため、ディスク内のデータがより現実的なデータとなるように事前に対象ディスクをランダムなデータで埋めておきました。
ランダムなデータの書き込みには /dev/urandom を利用しました。ただし /dev/urandom 自体はお世辞にも早いとは言えないため3.2TBのディスクを埋め終わるまでかなりの時間を要しました。

$ dd if=/dev/urandom of=/dev/nvme0n1 bs=4k status=progress

単純に転送してみる

まずは最も単純な転送方法の1つである dd over ssh を試してみます。

dd over sshはその名の通りddをsshトンネル上で利用する手法です。これによりリモートホストに対してddコマンドで読み取ったデータを転送することが出来ます。

bs=512byte (default)

ddコマンド自体は1回で読み書きするブロックサイズを指定することが出来ますが、まずは指定せずに実行してみます。
ブロックサイズを指定しない場合は512byteが設定されます。

$ dd if=/dev/nvme0n1 status=progress | ssh 10.0.0.2 dd of=/dev/nvme0n1
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 27119.9 s, 118 MB/s

このときの転送速度は118MB/sでした。これでもネットワークとしては1Gbpsの帯域を必要としますので十分な性能ではありますが、用意されたネットワークやディスクなどの環境を考えると満足できる結果とは言えません。
加えて、3.2TBの転送に約7時間半の時間を要しており、もっと高速化したいところです。
また、転送元・転送先の双方でのCPU使用率はともにsshプロセスが1コアの50%程度を利用している状況で、ブロックサイズが小さすぎることがボトルネックとなっているようです。
ddコマンドのデフォルトのブロックサイズである512byteという値はハードディスクが登場した頃の古典的なブロックサイズであるため、現代のディスクに対しては適切ではないと言えます。
そこで、ブロックサイズを変えて何パターンか試してみます。

bs=4kbyte

$ dd if=/dev/nvme0n1 status=progress bs=4k | ssh 10.0.0.2 dd of=/dev/nvme0n1 bs=4k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 8826.93 s, 363 MB/s

転送速度は363MB/sとブロックサイズを指定しない場合と比べて大幅に高速化されました。このときのCPU使用率は転送元・転送先ともにsshプロセスが1コアの90%以上を利用している状況でしたので、sshプロセスの処理限界に近いと言えます。

bs=32kbyte

$ dd if=/dev/nvme0n1 status=progress bs=32k | ssh 10.0.0.2 dd of=/dev/nvme0n1 bs=32k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 8628.44 s, 371 MB/s

転送速度は371MB/sと4kbyteの場合と比べてわずかに速くなりましたが、4kbyteの場合と比較して大幅な速度向上は見られず、やはりsshプロセスの限界のようです。

bs=64kbyte

$ dd if=/dev/nvme0n1 status=progress bs=64k | ssh 10.0.0.2 dd of=/dev/nvme0n1 bs=64k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 8313.07 s, 385 MB/s

転送速度は385MB/sとこれもわずかに速くなりましたが、大幅な速度向上は見られませんでした。

bs=512kbyte

$ dd if=/dev/nvme0n1 status=progress bs=512k | ssh 10.0.0.2 dd of=/dev/nvme0n1 bs=512k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 9864 s, 324 MB/s

転送速度は324MB/sで遅くなってしまいました。

bs=4Mbyte

$ dd if=/dev/nvme0n1 status=progress bs=4M | ssh 10.0.0.2 dd of=/dev/nvme0n1 bs=4M
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 9713.39 s, 330 MB/s

転送速度は330MB/sでこちらも遅くなってしまいました。
今回の検証環境ではブロックサイズは64kbyteを使用するのが適切のようです。

暗号化せずに転送

リモートホストとの接続にsshを利用した場合、転送されるデータは暗号化されます。
この処理は転送元では暗号化、転送先では復号化として計算資源を少なからず消費するため、この暗号化・復号化の処理性能がボトルネックとなる可能性があります。

しかし、通常この様にディスク間でデータを転送したい場合、データセンターの中など閉じたネットワーク内で実施するケースが多く、暗号化する必要性を感じません。
そこで、ncコマンドを利用して暗号化せずにTCPで直接データ転送を行ってみます。

転送先のサーバー上でTCP port 9999でListenしておきます。

$ nc -l 9999 > /dev/nvme0n1

続いて、転送元のホストからデータを転送します。

$ dd if=/dev/nvme0n1 status=progress bs=64k | nc 10.0.0.2 9999
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 20031.9 s, 160 MB/s

転送速度は160MB/sと遅くなってしまいました。
ncコマンド自体が大容量のデータを転送するのにあまり向いていないようです。

圧縮して転送

では、転送したいデータを圧縮して転送するとどうなるでしょうか。
ランダムなデータとはいえ多少はサイズが小さくなるので、圧縮・解凍の性能が十分に発揮できれば高速化が実現できそうです。

gzip

ssh with gzip

gzipで圧縮したデータをsshで転送してみます。

$ dd if=/dev/nvme0n1 status=progress bs=64k | gzip -c | ssh 10.0.0.2 gzip -d | dd of=/dev/nvme0n1 bs=64k
513446903808 bytes (513 GB, 478 GiB) copied, 21149.2 s, 24.3 MB/s

転送速度としては約24MB/sとあまりに遅すぎたため、7時間経過した時点で止めてしまいました。

nc with gzip

先程と同様にgzipで圧縮したデータをncで転送してみます。
転送先サーバーでListenしておきます。

$ nc -l 9999 | gzip -d > /dev/nvme0n1

転送元サーバーからデータを圧縮しつつ転送します。

$ dd if=/dev/nvme0n1 status=progress bs=64k | gzip -c | nc 10.0.0.2 9999
1943782227968 bytes (1.9 TB, 1.8 TiB) copied, 69754.1 s, 27.9 MB/s

こちらも転送速度が約28MB/sとあまりにも遅すぎたので途中で止めてしまいました。

gzip自体は圧縮をシングルコアで行います。転送中は転送元・転送先の両方のサーバーでgzipのプロセスが1コアを使い切っている状況でした。
そのためこの速度はシングルコアでの圧縮・解凍性能の限界であると言えます。

lzo

lzo(lzop)は圧縮・解凍がgzipよりも非常に高速な圧縮アルゴリズムです。

ssh with lzo

lzoで圧縮したデータをsshで転送してみます。

$ dd if=/dev/nvme0n1 status=progress bs=64k | lzop -c | ssh 10.0.0.2 lzop -d | dd of=/dev/nvme0n1 bs=64k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 20023.9 s, 160 MB/s

転送速度は160MB/sと先程のgzipを使用した時よりは速くなりました。

nc with lzo

先程と同様にlzoで圧縮したデータをncで転送してみます。
転送先サーバーでListenしておきます。

$ nc -l 9999 | lzop -d > /dev/nvme0n1

転送元サーバーからデータを圧縮しつつ転送します。

$ dd if=/dev/nvme0n1 status=progress bs=64k | lzop -c | nc 10.0.0.2 9999
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 11568.5 s, 277 MB/s

転送速度は277MB/sと、gzipと比べてもssh with lzoと比べても格段に速くなりました。
しかし、ともにdd over sshには劣っています。

pigz

pigzはgzipの処理を複数のCPUコアを利用して並列に行うツールです。
並列で処理が行えるためより高速な圧縮・解凍が実現できます。

ssh with pigz

pigzで圧縮したデータをsshで転送してみます。

$ dd if=/dev/nvme0n1 status=progress bs=64k | pigz -c | ssh 10.0.0.2 pigz -d | dd of=/dev/nvme0n1 bs=64k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 19662.6 s, 163 MB/s

転送速度は163MB/sと思ったより速くならない印象です。

nc with pigz

先程と同様にpigzで圧縮したデータをncで転送してみます。
転送先サーバーでListenしておきます。

$ nc -l 9999 | pigz -d > /dev/nvme0n1

転送元サーバーからデータを圧縮しつつ転送します。

$ dd if=/dev/nvme0n1 status=progress bs=64k | pigz -c | nc 10.0.0.2 9999
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 8063.87 s, 397 MB/s

転送速度は397MB/sと最速です。
おそらく、圧縮性能が向上した結果、少ないデータ量でより多くのデータを転送出来るようになり、大容量のデータを転送するのがあまり得意でないncコマンドでもそれなりの転送速度が出たと考えられます。
しかし、圧縮・解凍に使用するCPUや電力のことを考えるとほとんど差がないdd over sshでも良いのではないかと思います。

プロセス状況を見てみるとpigzの処理で詰まっているような状況でした。

各種圧縮アルゴリズムをいくつか試してみましたが、基本的に圧縮・解凍の処理性能がボトルネックとなってしまい、高速化は実現出来ないようです。

sshの暗号化方式を変えてみる

暗号化せずにデータ転送したり、圧縮してデータを転送したりしてみましたが、結果が芳しく有りません。
今の所素直にdd over sshを利用するのが良いように思います。

そこで、sshの暗号化方式を変えて転送速度を調査してみました。

aes128-ctr

$ dd if=/dev/nvme0n1 status=progress bs=64k | ssh -c "aes128-ctr" 10.0.0.2 dd of=/dev/nvme0n1 bs=64k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 5949.54 s, 538 MB/s

aes192-ctr

$ dd if=/dev/nvme0n1 status=progress bs=64k | ssh -c "aes192-ctr" 10.0.0.2 dd of=/dev/nvme0n1 bs=64k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 5245.96 s, 610 MB/s

aes256-ctr

$ dd if=/dev/nvme0n1 status=progress bs=64k | ssh -c "aes256-ctr" 10.0.0.2 dd of=/dev/nvme0n1 bs=64k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 5259.14 s, 609 MB/s

aes128-gcm@openssh.com

$ dd if=/dev/nvme0n1 status=progress bs=64k | ssh -c "aes128-gcm@openssh.com" 10.0.0.2 dd of=/dev/nvme0n1 bs=64k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 5066.69 s, 632 MB/s

aes256-gcm@openssh.com

$ dd if=/dev/nvme0n1 status=progress bs=64k | ssh -c "aes256-gcm@openssh.com" 10.0.0.2 dd of=/dev/nvme0n1 bs=64k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 4988.98 s, 642 MB/s

chacha20-poly1305@openssh.com

$ dd if=/dev/nvme0n1 status=progress bs=64k | ssh -c "chacha20-poly1305@openssh.com" 10.0.0.2 dd of=/dev/nvme0n1 bs=64k
3200631791616 bytes (3.2 TB, 2.9 TiB) copied, 7962.87 s, 402 MB/s

やはり、暗号化方式を変更することでCPUに対する負荷が大きく変わるようで、1コアで捌ける性能が大きく変化するようです。
その結果、転送速度に2倍近い差が確認できます。
今回の検証環境では、最も高速な暗号化方式は aes256-gcm@openssh.com でした。

しかし、CPUの世代によってサポートしている拡張命令セットが異なることから環境によって最適な暗号化方式は変わってくると考えられます。
このため、使用する環境上でいくつかの暗号化方式を試した上で使用する暗号化方式を決定するのが良いと思います。

まとめ

今回は、サーバー間でのブロックレベルのデータ転送を高速に実現する方法について調査した結果を紹介しました。
結果としては、次のようになりました。

転送方法 圧縮方式 速度
ssh none 385MB/s
nc none 160MB/s
ssh gzip 24.3MB/s
nc gzip 27.9MB/s
ssh lzo 160MB/s
nc lzo 277MB/s
ssh pigz 163MB/s
nc pigz 397MB/s
転送方法 暗号化方式 速度
ssh aes128-ctr 538MB/s
ssh aes192-ctr 610MB/s
ssh aes256-ctr 609MB/s
ssh aes128-gcm@openssh.com 632MB/s
ssh aes256-gcm@openssh.com 642MB/s
ssh chacha20-poly1305@openssh.com 402MB/s

また、sshの暗号化方式を変更して高速化を試みる方法は、scpでのファイル転送にも利用することができます。
環境によって最も早い転送方法は多少異なってくると思いますので、それなりに早いものをいくつか試してみて使用する転送方法を決定すると良いでしょう。