最速を究める! 2つのサーバ間で特盛りデータを30倍速で転送する方法 | サイバーエージェント 公式エンジニアブログ
こんにちは. エンジニアの平野です. ふだんはプライベートクラウドのサーバハードウェアとストレージを担当しています.

サーバのリプレイスや増設, 仮想サーバの移植などでテラバイトクラスのデータを2つのサーバ間で転送することがよくあります.

こんなとき, 転送終了を待ちながら「あと何時間掛かるのかなー」とか「もっと速く転送終わらないかなー」なんて考えたことはありませんか?

今回は下記のようなシーンで活躍する, 特盛りデータを30倍高速に転送する方法をご紹介します.

- サーバの交換でデータを移設したい
- MySQLスレーブサーバの増設したい
- 仮想サーバを別のホストに移植したい
- 大量のファイルを別のサーバに移設したい
- 大容量データをバックアップしたい

■ 環境を用意

ServerAとServerBの2つの物理サーバを用意し, 1Gbpsのスイッチ経由で接続します.

2つのサーバスペックは下記の通りです.

ServerA(送信側):
  DELL PowerEdge R630
  E5-2650L v3 x2 (合計47Core)
  DDR4 256GB
  1.2TB SAS HDD x2 RAID1

ServerB(受信側):
  DELL PowerEdge FC630
  E5-2650L v3 x2 (合計47Core)
  DDR4 256GB
  1.2TB SAS HDD x2 RAID1

下記のコマンドで, 転送するデータを2つ作っておきます.

ServerA# dd if=/dev/urandom of=rand.dat bs=1M count=1024
ServerA# dd if=/dev/zero of=zero.dat bs=1M count=1024


今回は, 圧縮率の良いファイルとそうではないものを試しますので, ファイルを2つ用意します.


■ テスト条件

各テストは3回実施し, 平均値を結果として取ります.
テスト開始前は下記コマンドでcacheを空っぽにしておきます

ServerA# sync; echo 3 > /proc/sys/vm/drop_caches

■ 転送してみる

準備ができたところで, 先ずは何も考えずにscpで転送してみます.

ServerA# scp zero.dat root@ServerB:./
100% 1024MB 102.4MB/s   00:10

結果: 102.4MB/s ≒ 819.2Mbps

普通ですね. The普通. 普通ofふつう.
この結果を基準に, 高速化をはかっていきます.

scpでの転送は, 暗号化・復号化の処理がボトルネックになっているみたいですので, 暗号化せずに転送してみましょう.
同じフロアであったり, 経路が暗号化されているなど, セキュアな環境同士であれば暗号化は必要ありません.

今回は, nc(netcat, nmap-ncat)を使ってみます.
ncは, たいていのLinuxディストリの標準リポジトリから導入できます.
ncを使ったファイルのコピーでは,  受け側でTCPポートをListenし, 送り側からそのポート宛にデータを流し込みますので, ServerBでncコマンドを叩いたあとで, ServerAからncコマンドを叩きます.

ServerB# nc -l 9999 > zero.dat
ServerA# dd if=zero.dat bs=1M | nc ServerB 9999
9.19443 s, 117 MB/s

結果: 117MB/s ≒ 936Mbps

ほぼワイヤスピードですね.
scpと比較して114%速くなりました.
サーバリソースは余裕ですが, ネットワークがボトルネックになっていて, これ以上多くは転送できません.

これくらい速ければ良さそうな気もしますが, ファイルの中身はゼロです. 効率よくネットワークを使うために, データを圧縮してみましょう.

てっとり早くgzipを使ってみます.

ServerB# nc -l 9999 | gzip -d > zero.dat
ServerA# dd if=zero.dat bs=1M | gzip -c |nc ServerB 9999
10.5008 s, 102 MB/s

結果: 102MB/s ≒ 816Mbps

非圧縮とくらべて遅くなってしまいました.
gzipはCPUリソースを食い過ぎるみたいです.

もっとCPUにやさしい圧縮アルゴリズムを使わないと, 速さは稼げないみたいなので, こんどはlzoを使ってみましょう.

lzo(lzop)は, OpenVPNにも使われている, ネットワークストリームの圧縮・解凍が得意なアルゴリズムです.
これもCentOSであればepelのリポジトリから導入できます.

ServerB# nc -l 9999 | lzop -d > zero.dat
ServerA# dd if=zero.dat bs=1M | lzop -c |nc ServerB 9999
3.84884 s, 279 MB/s

結果: 279MB/s ≒ 2,232Mbps

1Gbpsの壁を越えました!
scpと比較して, 272%の高速化です.

でも, まだ満足できません!
今回使っているサーバはCPUコアが48コアありますが,
使われているのはncとlzopの2プロセス(2コア)だけです.
他の46個のプロセッサも無駄なく使ってあげれば, より高速化できるはずです.
分散圧縮ができる pigz を使ってみましょう.

pigzコマンドをオプション無しで叩くと, サーバで利用可能なプロセッサ数を自動判別して, すべてのプロセッサを使って並列処理してくれます.

pigzはgzipと同じアルゴリズムですが, 並列化すれば速度は他と比べものにならないくらい速くなるはずです.
pigzもepelのリポジトリから導入できます.

ServerB# nc -l 9999 | pigz -d > zero.dat
ServerA# dd if=zero.dat bs=1M | pigz -c | nc ServerB 9999
3.29683 s, 326 MB/s

結果: 326MB/s ≒ 2,608Mbps

記録更新! scpと比較して, 318%の高速化です!

サービスが動いていないサーバでは有効ですが, CPUを限界まで使っているので, KVMのホストマシンやサービスが動いている環境など,  共有リソース下でこれを使ってしまうと他のサービスの動作に影響が出てしまいます.

共有リソースの場合はlzopを,
専用リソースの場合はpigzを,

使う場面にあわせて使い分けるようにしましょう.

zero.datはとてもよく圧縮が掛かるデータですが, 圧縮が掛かりにくいrand.datはどうなるんでしょうか.
こっちのファイルでも試してみましょう.

ServerB# nc -l 9999 | lzop -d > rand.dat
ServerA# dd if=rand.dat bs=1M | lzop -c | nc ServerB 9999
26.5191 s, 40.5 MB/s

ServerB# nc -l 9999 | pigz -d > rand.dat
ServerA# dd if=rand.dat bs=1M | pigz -c |nc ServerB 9999
9.06602 s, 118 MB/s

結果:
  lzop: 40.5MB/s ≒ 326.4Mbps
  pigz: 118MB/s ≒ 994Mbps

pigzはほぼワイヤスピードが出ましたが, lzopは圧縮・解凍がボトルネックになってしまい, 速度が落ちてしまいました.

圧縮が効きにくいデータはlzopで転送しない方が良さそうですね.

■ 遅いネットワーク環境でのテスト

ここまで, 1Gbpsのストレスフリーなネットワーク環境でテストを行ってきましたが, 拠点を跨ぐ場合やクライアントVPNで接続している環境では, 転送速度はこんなに出ません.

ネットワーク速度がプアな環境も想定して, 試しに, ServerAとServerBの接続を100Mbpsに落としてテストをしてみましょう.

ServerB# ethtool -s p4p1 speed 100 autoneg off duplex full
ServerB# ethtool p4p1 | grep Speed
    Speed: 100Mb/s

これでネットワーク接続が100Mbpsになりましたので, 各テストを実行してみましょう.

ServerB# scp zero.dat root@ServerB:./
100% 1024MB  11.1MB/s   01:32

結果: 11.1MB/s ≒ 88.8Mbps, 100%

ServerB# dd if=zero.dat bs=1M | nc ServerB 9999
91.1991 s, 11.8 MB/s

結果: 11.8MB/s ≒ 94.4Mbps, 106%

ServerB# dd if=zero.dat bs=1M | lzop -c | nc ServerB 9999
3.73494 s, 287 MB/s

結果: 287MB/s ≒ 2,296Mbps, 2,580%

ServerB# dd if=zero.dat bs=1M | pigz -c | nc ServerB 9999
3.19667 s, 336 MB/s

結果: 336MB/s ≒ 2,688Mbps, 3,027%

ServerB# dd if=rand.dat bs=1M | lzop -c | nc ServerB 9999
91.1698 s, 11.8 MB/s

結果: 11.8MB/s ≒ 94.4Mbps, 106%

ServerB# dd if=rand.dat bs=1M | pigz -c | nc ServerB 9999
90.9118 s, 11.8 MB/s

結果: 11.8MB/s ≒ 94.4Mbps, 106%

100Mbpsでは, 全体的にscpより高速となりました.
zero.dataをpigz圧縮で流した結果で, なんと30倍の高速化! 最高速度をたたき出しました. ネットワーク転送に割かれるCPUリソースが少なくなったためだと思います.

1Gbpsでrand.datをlzop経由で転送した時はlzopプロセスがボトルネックでしたが, 今回は100Mbpsなのでネットワークがボトルネックになっています. 実効速度300Mbpsを切るネットワークで, 圧縮率が悪いデータはは圧縮せずに転送した方が良さそうです.

■ 10Gbpsネットワークでのテスト

1Gbpsの通常環境と, 遅い100Mbpsとテストしてみましたが, 逆に速いネットワーク環境ではどうなるのでしょうか. 来たるべく未来の10Gbpsでも試してみましょう.

あらたにServerCを用意し10Gbps接続でServerAに直結します.
あまりにも速く転送が終わってしまうので, データも10GBに増やしてテストしてみます.

ServerB# scp zero.10GB.dat root@ServerC:./
123.4MB/s

結果: 123.4MB/s ≒ 987.2Mbps, 100%

ServerB# dd if=zero.10GB.dat bs=1M | nc ServerC 9999
51.7511 s, 207 MB/s

結果: 207MB/s ≒ 1,656Mbps, 134%

ServerB# dd if=zero.10GB.dat bs=1M | lzop -c | nc ServerC 9999
36.4958 s, 294 MB/s

結果: 294MB/s ≒ 2,352Mbps, 190%

ServerB# dd if=zero.10GB.dat bs=1M | pigz -c | nc ServerC 9999
35.0364 s, 306 MB/s

結果: 306MB/s ≒ 2,448Mbps, 198%

ServerB# dd if=rand.10GB.dat bs=1M | lzop -c | nc ServerC 9999
221.463 s, 48.5 MB/s

結果: 48.5MB/s ≒ 388Mbps, 34%

ServerB# dd if=rand.10GB.dat bs=1M | pigz -c | nc ServerC 9999
41.6055 s, 258 MB/s

結果: 258MB/s ≒ 2,064Mbps, 167%

想定通り, 圧縮しやすいデータは速いのですが, 圧縮の効きにくいデータはlzopでは高速化できませんでした.
pigzは両方のデータとも, 高速化できました.

あらたに用意したServerCは, 型落ちの12コアのサーバ機で, DISK性能もあまり良くないので, 48コアのServerBほど速度は出ませんでした.
送り側と受け側のCPU性能差がありすぎると, 高速化にも限界があるみたいです.

■ まとめ

scpは論外ですが, 状況によってベターな転送方法で行わなければ, 最高速度は出せないという結果となりました.

 - 送信側/受信側で利用可能なCPUリソース
 - 受信側/受信側のDISK IO性能
 - ネットワーク帯域
 - ネットワーク経路の圧縮率(VPNトなどで, 経路が圧縮されてる場合, 2重圧縮すると遅くなる)
 - ネットワークに流す前のデータ圧縮率

それぞれを考慮してベストな選択をしましょう.

正しい選択をすれば, 30倍速でお仕事が終わりますので, 余った時間はゆっくりお昼寝他のお仕事に割くことができます.

回線やリソースの状況がよくわからないときは, 下記のコマンドで少量のデータを"味見"してから, 全部のデータを転送開始すると良いと思います.

ServerA# dd if=unknoun.dat bs=1M | head -c 100000000 | nc ServerB 9999
ServerA# dd if=unknoun.dat bs=1M | head -c 100000000 | lzop -c | nc ServerB 9999
ServerA# dd if=unknoun.dat bs=1M | head -c 100000000 | pigz -c | nc ServerB 9999

ファイルではなくディレクトリツリーを転送する場合は, tarで丸めると転送できます.

ServerB# cd /var/lib/
ServerB# nc -l 9999 | pigz -d | tar xv
ServerA# cd /var/lib/
ServerA# tar cf - mysql | pigz -c | nc ServerB 9999

■ さいごに

この方法を使うようになったのは何年か前からですが, 劇的にお仕事が捗るので, 100MB程度の転送でも, 日常的にnc+圧縮の方法で転送しています.
ncだけだとネットワークエラーの検知ができず, ファイルサイズだけで成功/失敗の判断をするしかありませんが, 何らかの圧縮アルゴリズムを挟めば, 途中でエラーを吐いてくれるので, できるだけ圧縮を挟んだ転送をおすすめします. (エラー判定は, tee経由でmd5sumに渡せばhash値が取れますが, CPUコストが掛かってしまいますし, 何より面倒です)

今回は導入が楽ですぐに使える lzop と pigz を使って高速化を試しましたが, snappyなど高速な圧縮アルゴリズムを使ったり, 圧縮レベルを変えてみたりしても良いかもしれません.
また, ncはTCPセッションが1本だけですが, 複数のセッションを使って分散化できれば, ネットワークリソースをより効率的に使えるかもしれません.

効率化って, やり始めるとキリがないですけど, いろいろな視点でベストを考えるのも楽しいものですね.