バックアップの際の圧縮をマルチコア対応してみた


年末に自宅サーバ群のメトリクスを見ていて気になったのは、毎日のバックアップに数時間かかっているということ。何も考えずに tar + bzip2 しているだけだったので、この際見直してみた。

そもそもこのご時世、よほど絞っていない限りはマルチコア構成になっている。だが、何も考えずに tar cfj しているとシングルコアで圧縮してしまい、非常に時間がかかってしまう。そこで、マルチコア対応させることにした。

マルチコアで圧縮ファイルを作る

まず、tar でマルチコアアーカイブできるかだが、結論から言うと、オプションで頑張ればできる。ただし、tar cfj のようにシンプルにはいかず、アーカイブした後に実行するプログラムをオプションで指定する必要がある。

そこで、マルチコア対応の圧縮プログラムを探してみたが、概ねメジャーどころは以下だろうか。

xz だと最近のディストリビューションには大体入っているので、特に何もインストールする必要はない。

どの圧縮フォーマットにするか

上記 3 つからどれを使うかだが、互換性を考えるなら pigz が堅いかもしれない。ただ、以下の理由から pixz にした。

  • 速度と圧縮率のバランスを取りやすい。
  • xz と比べると pixz はインデックスがあるので、一部ファイルを高速に抽出しやすい(らしい)。
  • やってみたい。

pixz のインストール

pixz のインストール自体は簡単で、Gentoo Linux の場合は標準 Portage tree にいるので emerge するだけになる。

# emerge pixz

実際にバックアップしてみる

pixz をインストール後、実際にバックアップスクリプトを修正して試してみた。気をつけるべきポイントは

  • 圧縮率
  • 何コア使うか

の 2 点になるかと思う。

まず前者については、0 〜 9 のうち 2 にした。

次にコア数だが、全コアを使うとサービスに影響が出るので、以下方針にした。

  • 4コア未満: 1コア
  • 4コア以上: 物理コア数の半分

コア数はスクリプト内にハードコーディングしてもいいのだが、いろいろなインスタンスで使いたいので動的に取得するようにした。

コア数の取得と利用コア数の計算

そこでコア数の取得が問題になる。自宅サーバ群の場合、以下 3 パターンがある。

  • Core i5 シリーズ 物理マシン
  • AMD Ryzen 1700 物理マシン
  • AMD Ryzen 1700 上の KVM + QEMU で動く仮想マシン

それぞれで正しくコア数を取得する必要がある。

まず、それぞれで /proc/cpuinfo を取得してみた。Core i5, Ryzen ともに cpu cores が物理コア数を表している。

# cat /proc/cpuinfo
# 中略

processor	: 15
vendor_id	: AuthenticAMD
cpu family	: 23
model		: 1
model name	: AMD Ryzen 7 1700 Eight-Core Processor
stepping	: 1
microcode	: 0x8001129
cpu MHz		: 1550.000
cache size	: 512 KB
physical id	: 0
siblings	: 16
core id		: 7
cpu cores	: 8
apicid		: 15
initial apicid	: 15
fpu		: yes
fpu_exception	: yes
cpuid level	: 13
wp		: yes
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc cpuid extd_apicid aperfmperf rapl pni pclmulqdq monitor ssse3 fma cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c rdrand lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw skinit wdt tce topoext perfctr_core perfctr_nb bpext perfctr_llc mwaitx cpb hw_pstate ssbd vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx smap clflushopt sha_ni xsaveopt xsavec xgetbv1 xsaves clzero irperf xsaveerptr arat npt lbrv svm_lock nrip_save tsc_scale vmcb_clean flushbyasid decodeassists pausefilter pfthreshold avic v_vmsave_vmload vgif overflow_recov succor smca sme sev
bugs		: sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass
bogomips	: 5987.99
TLB size	: 2560 4K pages
clflush size	: 64
cache_alignment	: 64
address sizes	: 43 bits physical, 48 bits virtual
power management: ts ttp tm hwpstate eff_freq_ro [13] [14]

一方で、仮想マシン上ではそうはいかない。

# cat /proc/cpuinfo
# 中略

processor	: 3
vendor_id	: AuthenticAMD
cpu family	: 23
model		: 1
model name	: AMD Ryzen 7 1700 Eight-Core Processor
stepping	: 1
microcode	: 0x8001129
cpu MHz		: 2993.897
cache size	: 512 KB
physical id	: 3
siblings	: 1
core id		: 0
cpu cores	: 1
apicid		: 3
initial apicid	: 3
fpu		: yes
fpu_exception	: yes
cpuid level	: 13
wp		: yes
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm rep_good nopl cpuid extd_apicid pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm cmp_legacy svm cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw perfctr_core ssbd vmmcall fsgsbase tsc_adjust bmi1 avx2 smep bmi2 rdseed adx smap clflushopt sha_ni xsaveopt xsavec xgetbv1 xsaves clzero xsaveerptr virt_ssbd arat npt nrip_save arch_capabilities
bugs		: sysret_ss_attrs null_seg spectre_v1 spectre_v2 spec_store_bypass
bogomips	: 6011.39
TLB size	: 1024 4K pages
clflush size	: 64
cache_alignment	: 64
address sizes	: 48 bits physical, 48 bits virtual
power management:

仮想マシン上では、マルチプロセッサシングルコア状態として認識されているため、cpu cores だけを信用するわけにはいかない。

そこで、以下方針でコア数を確定することにした。

  • cpu cores が 1 の場合: physical id をキーにコア数を判定
  • cpu cores が 2 以上の場合: cpu cores がコア数
  • それ以外: 1 コア

マルチプロセッサ・マルチコアではシングルプロセッサしか使わないが、いったんはこの方針でシェルスクリプトに落とすとこんな感じになる。

CORES=`fgrep 'cpu cores' /proc/cpuinfo | sort -u | sed 's/.*: //'`
if [ $CORES = "" ]; then
  CORES=1
else
  if [ $CORES = '1' ]; then
    CORES=`fgrep 'physical id' /proc/cpuinfo | sort -u | wc -l`
    if [ $CORES = "0" ]; then
      CORES=1
    fi
  fi
fi
MAX_USED_CORES=1
if expr "$CORES" : "^[0-9]*$" >&/dev/null; then
  if [ $CORES -gt 2 ]; then
    MAX_USED_CORES=`expr $CORES / 2`
  fi
fi

これで圧縮時に使うコア数が計算できるので、 pixz -p $MAX_USED_CORES のようにしてコア数を指定する。

スクリプトに落とす

上記を元に雑なスクリプトに落としてみた。なお、バックアップ先の仕様は以下とする。

  • バックアップ先ディレクトリは /home/shared/backup/dairy/YYYY-MM-DD/[ホスト名]
  • /home/shared は NFS マウントされていること

バックアップ先は NFS なので、NAS でもいいし別のサーバでもよい。

スクリプトは大体こんな感じ。

#!/bin/bash

TARGET_BASE_DIR="/home/shared"
TARGET_DIR="backup/daily"
NFS_IP="192.168.1.10"
WRITE_USER="meihong"

MOUNT=`which mount`
GREP=`which grep`
DATE=`which date`
TAR=`which tar`
ZIP="`which pixz` -2 -p $MAX_USED_CORES"
ZIP_EXT="xz"
SU=`which su`
CP=`which cp`
RM=`which rm`
WORK_DIR="/var/tmp"
TODAY=`$DATE +'%Y-%m-%d'`

# Backup
# backup prefix target_dir
#
function backup {
	$TAR cf - $2 2>/dev/null | $ZIP > $WORK_DIR/$1_$TODAY.tar.$ZIP_EXT 2>/dev/null
	copy $1 "tar"
}

# Copy to NFS
# copy prefix middle_extension
#
function copy {
	$SU -l $WRITE_USER -c "exec $CP $WORK_DIR/$1_$TODAY.$2.$ZIP_EXT $TARGET_BASE_DIR/$TARGET_DIR/$TODAY/$HOSTNAME"
	$RM $WORK_DIR/$1_$TODAY.$2.$ZIP_EXT
}

# Confirm if the directory to store backup data is mounted
#
function check_mount {
	$MOUNT | $GREP -q "$NFS_IP:$TARGET_BASE_DIR "
	if [ $? != 0 ]; then
		echo "Backup failed."
		exit 1
	fi
}

# Calculate the number of CPU cores used
#
function calc_cpu_core {
	CORES=`fgrep 'cpu cores' /proc/cpuinfo | sort -u | sed 's/.*: //'`
	if [ $CORES = "" ]; then
    CORES=1
	else
    if [ $CORES = '1' ]; then
      CORES=`fgrep 'physical id' /proc/cpuinfo | sort -u | wc -l`
        if [ $CORES = "0" ]; then
        CORES=1
      fi
    fi
	fi
	MAX_USED_CORES=1
	if expr "$CORES" : "^[0-9]*$" >&/dev/null; then
    if [ $CORES -gt 2 ]; then
      MAX_USED_CORES=`expr $CORES / 2`
    fi
	fi
}

# Confirm if we have not done today's backup yet
#
function create_dir
	if [ -e "$TARGET_BASE_DIR/$TARGET_DIR/$TODAY/$HOSTNAME" ]; then
		echo "Already done."
		exit
	else
		$SU -l $WRITE_USER -c "exec mkdir -p $TARGET_BASE_DIR/$TARGET_DIR/$TODAY/$HOSTNAME"
	fi
}

# Initiate backup
#
check_mount
calc_cpu_core
create_dir

# homedir
#
backup "home" "/home"

# etc
#
backup "etc" "/etc"

実際のスクリプトは RDBMS のダンプ等も行っているので、もう少し複雑なものになっている。

どう変わったか

問題は、マルチコア対応をすることでどれくらいインパクトがあるか = どれくらい CPU および IO が占有される時間を減らせるかにある。

4 コアあるゲートウェイサーバの場合、スクリプトを改修する前は、

朝 3:30 にバックアップ開始していたが、2 時間 40 分以上かかっていた。

これに対して改修後はバックアップするディレクトリを増やしたにもかかわらず

ほぼ 19 分で終わっている。なお、改修後は常時 CPU リソースを使い続けているように見えているが、これはモニタリングもかねてずっと Zabbix のグラフを表示していたからだ。なので、実際はもう少し早く終わる可能性が高い。

上記の通り、改修前は CPU リソースを 25% しか使っていないのに、改修後はほぼ 75% 使っている。xz で 2 コア使うだけなのに、と思うかもしれないが、これには理由がある。

tar cfJ する場合まず tar アーカイブを作ってから圧縮する = 常に CPU コアは 1 つしか使わないのに対し、今回は

tar cf - /path/to/dir | pixz -2 -p 2 > /path/to/archive/2021-12-30.tar.xz

と実行しているとおり、パイプを使うことでアーカイブ作成と同時に xz で圧縮もしているからだ。tar で 1 コア、xz で 2 コア使っており、4 コア中 3 コアを占有することになるので 75% 前後は想定通りとなる。

なので 4 コアの場合は結構ギリギリまで使い倒すことにはなるが、ゲートウェイサーバで一番 CPU リソースを使うタスクはバックアップか Zabbix のフロントエンドなのでこれで問題はないだろう。


ということで、無事にバックアップにかかる時間を短縮できた。

各マシンからファイルサーバにバックアップ → ファイルサーバでバックアップという手順にしているので crontab の調整が面倒だったが、それもやりやすくなった。この手のバックアップは割と何も考えずに tar cvz/cvj しがちだが、せっかくのマルチコアを生かさない手はないと思う。