大量・巨大なファイル操作を低負荷で行う方法

February 10, 2015

この記事は、ファイルの操作を低負荷で行いたい人のための記事です。

負荷をかけたくないケースとは

負荷は一般的に CPU 負荷と I/O 負荷に二分されますが、ここでは I/O 負荷にフォーカスして話を進めます。サーバ負荷をかけたくないケースの多くは、サービス運用中のサーバ上におけるメンテナンスです。少ない・小さいファイルを操作する分には問題ありませんが、大量・巨大なファイルを操作するとなるとシステム全体に影響を及ぼすような I/O 負荷がかかってしまいます。

これでは、本来優先すべき運用中のサービスに影響が出てしまいますのでよろしくありません。そのため、多くのサーバ管理者は、低負荷でファイル操作を行う必要があります。

I/O 負荷がかかるとどうなるのか

Linux サーバに I/O 負荷がかかった場合、サーバ上の他のサービスに影響が出る可能性があります。例えば、Webサーバ上で数百GBのファイルや数万個のファイルを一気に削除しようとすると、Web サーバのレスポンスタイムが大幅に遅延するという問題が生じます。具体的には、ファイル操作を行おうとする他のプロセスが I/O 待ちとなってしまい、処理が一時的に停止する事象(すなわちサイト表示不可)が発生します。

もちろん、軽微な非同期 I/O やディスクキャッシュへの I/O 処理についてはこの限りではありません。しかし、同期書き込みやディスクキャッシュが作成されていない状況で読み込みをする場合はこの問題が発生します。I/O の仕組みについては昨年書いた卒論の 2 章にもまとめたのでよかったら読んでみてください。

ionice を使えばいいのでは?

今まで負荷に配慮してファイル操作を行ったことがある人なら誰もが知っている ionice コマンドというものがあります。ionice は、引数に指定したプロセス・コマンドに関する I/O 処理の優先度を指定するためのコマンドです。nice コマンドは、CPU 割り当ての優先度を指定します。ionice は、nice コマンドの I/O 版です。I/O 負荷をかけないように処理をしたい場合は、ionice コマンドで優先度を下げて処理を行います。

ただし、ionice コマンドには落とし穴があります。詳しくは以下のスライドをご覧ください。

負荷をかけずにファイルを操作するコツ

本題に入ります。どのようにすれば負荷をかけずにファイルを操作すればよいかというと、ズバリ 少しずつゆっくり処理する ことに尽きます。 例えば、1TB のファイル削除を 10 秒で行う場合と 1000 秒かけて行う場合とでは、一度に発生する I/O 要求の数が大きく異なります。もちろん、後者のほうが一度に生じる I/O 要求は少なくて済みますので、実サービスに悪影響を及ぼすことなく巨大なファイル削除を行うことができます。

ちなみに、ionice コマンドで低優先度を指定した場合は、I/O スケジューラが I/O 要求の処理順序の入れ替えや保留を行うことで、対象の I/O 要求をゆっくり処理します。今回は、Linux におけるファイル操作をいくつかのパターンにわけ、それぞれについて具体的な手法を示します。ちなみに、nice, ionice は付与して損することはないのですべてのパターンにおいて利用することをオススメします。

大量のファイルを作成する方法

急がずに sleep しながらやりましょう

大量のファイルを移動する方法

方法1. スクリプトを書く

  1. ファイルのリスト取得
    • ls 使うなら -f オプションをつける
  2. リストを元に、ファイルパス指定で1つファイルを移動
  3. sleep
  4. 2 に戻る

#!/usr/bin/env python
# coding: utf-8
import os
import time
src_dir = ""
dst_dir = ""
import_file = ""
export_file = ""
if __name__ == '__main__':
file_list = open(import_file, 'r')
result_file = open(export_file, 'w')
for count, line in enumerate(file_list.readlines()):
filename = line.lstrip('./').rstrip('\r\n')
src_file = os.path.join(src_dir, filename)
if not os.path.exists(src_file):
result_file.write(line)
else:
dst_file = os.path.join(dst_dir, filename)
if not os.path.exists(dst_file):
os.rename(src_file, dst_file)
if count % 100 == 0:
time.sleep(0.1)
result_file.close()
file_list.close()
view raw rename.py hosted with ❤ by GitHub

方法2. find && xargs && sleep

  • xargs は -n オプションを忘れない
    • 付与しない場合に引数の長さが execve(2) の仕様に制限される
    • 具体的な値は getconf ARG_MAX で取得可能

find . -print -exec sleep 0.1 \; | xargs -n1 -I%% mv %% dst_dir/
view raw slow_move.sh hosted with ❤ by GitHub

大量のファイルを削除する方法

方法1. スクリプトを書く

  1. ファイルのリスト取得
    • ls 使うなら -f オプションをつけてね
  2. PATH指定で1つファイルを削除
  3. sleep
  4. 2 に戻る

方法2. find -delete && sleep

  • -delete によって削除を行う
  • find で 1 ファイル削除するごとに sleep をはさむ
  • -print を用いることで、現在どのファイルを削除しているかがわかる

find ./ -exec sleep 0.1 \; -delete -print
view raw slow_remove.sh hosted with ❤ by GitHub

巨大なファイルを作成する方法

  • dd
    • oflag で同期書き込みを行うフラグを指定すれば、書き込みでも ionice が効くようになる
    • bs のサイズを調整することで I/O の粒度が調整できる
      • ibs, obs オプションを用いれば、読み込み/書き込みバイト数をそれぞれ別に制御できる

# dd で全部やるパターン
ionice -c3 dd if=/dev/zero of=100GB.img bs=1M count=102400 oflag=direct
ionice -c3 dd if=/dev/zero of=100GB.img bs=1M count=102400 oflag=sync
ionice -c3 dd if=/dev/zero of=100GB.img bs=1M count=102400 oflag=fdatasync
ionice -c3 dd if=/dev/zero of=100GB.img bs=1M count=102400 oflag=fsync
# 単位ファイルを作成して足し続けるパターン
ionice -c3 dd if=/dev/zero of=100MB.img bs=1M count=100 oflag=direct
for i in $(seq 1 1000); do ionice -c3 cat 100MB.img >> 100GB.img; sleep 5; done

それぞれのフラグの違いについてはまた別の機会に。。 以下の本に詳しく書いてあるのでオススメです。

巨大なファイルを移動する方法

Linux の仕組み上、ファイルを移動する場合はサイズにかかわらずメタ情報の更新のみで済みます。そのため、特に工夫は必要ありません。気の向くままに mv してください。

ただし、パーティションをまたいで移動する場合は注意が必要です。 この場合は、移動先のパーティションに対して新たにファイルを作成する処理が走ります。そのため、パーティションをまたぐ場合は、ゆっくりとファイルをコピーし、そのあとに元のファイルを削除しましょう。ファイルコピーをゆっくり行う場合には、scp, rsync や pv コマンドなどを用いるとよいかと思います。tar コマンドなどを組み合わせたい場合は、pv コマンドがオススメです。

# rsync コマンドを用いるケース
rsync -av --bwlimit 10000 src dst
# tar + pv コマンドを用いるケース
tar czvf - src | pv -L 2048k | cat - > /path/to/dist
view raw slow_copy.sh hosted with ❤ by GitHub

巨大なファイルを削除する方法

このケースが一番悩まされました。しかし、最近 truncate コマンド使えばいいのでは〜ということに気がつきました。

  • truncate コマンドを用いて少しずつファイルサイズを減らす
  • sleep をはさむ
  1. ファイルのサイズを n Byte 小さくする
  2. n 秒 sleep
  3. 1 に戻る

#/bin/bash
set -euo pipefail
filepath=$1
# 10MBytes/sec
filesize=$(du -m ${filepath} | awk '{print $1}')
sleep_time=0.1
for num in $(seq 1 ${filesize});
do
truncate -s $((${filesize}-${num}))M ${filepath}
sleep ${sleep_time}
done
\rm ${filepath}

まとめ

  • 調整の鬼になってメンテナンス権を獲得し、豪快にファイルを操作したい人生だった
  • 人生甘くないので安全にファイルを消す方法くらいは体得しておいて損はないと思う
  • ワンライナーはミスしやすいのでスクリプトをしっかり書くことを推奨します
    • 例示したプログラムやコマンドの品質は保証しませんのでご了承ください

Profile picture

Written by Narimichi Takamura (@nari_ex) who works at Topotal as CEO. He love engineering and fighting game.