Linux I/O のお話 write 編
- write はページに dirty フラグを立てるだけなので決してユーザープロセスを待たせない
って、本当にそうなんでしょうか?(否定しているわけではなく、純粋な疑問です。)
と質問をもらったので、最近追ったことをここでまとめます。かなり長文です、すいません。また、まだまだ不勉強なので間違っているところもあるかもしれません。ツッコミ大歓迎です。
まず、オライリーのカーネル本の 15章 ページキャッシュ 15.3 汚れたページのディスクへの書き込み から引用。
ご存知のように、カーネルは、ブロック型デバイスのデータを含むページをページキャッシュに蓄えています。プロセスが何らかのデータを更新した場合は、必ず対応するページに汚れている印をつけます。すなわち、PG_dirty フラグを設定します。
UNIX システムでは、汚れたページのブロック型デバイスへの書き込みを遅延することができます。この方法によって、システムの性能を著しく向上させることができます。キャッシュ内のページへの複数の書き込み操作を、対応する低速なディスクセクタに対して1回だけの物理的更新で済ませることができます。さらに書き込み操作は、読み取り操作ほどほかの処理に影響を与えません。というのも、通常、書き込みが遅延することによってプロセスが休止することはないからです。 一方、読み取りが遅延されるとほとんどの場合、プロセスは休止してしまいます。書き込みを遅延することによって、物理的なブロック型デバイスは、平均的に書き込み要求よりも読み取り要求の方を多く処理できるようになります。
だそうです。書き込みがプロセスを休止させないのはそうなんだろう。でもなんでそうなのか、が問題ですよね。ということで深追いの時間です。
Linux の書き出し処理のおおまかな仕組み
Linux が write(2) なり何なりでプロセスから書き込み要求を受け取ったあと、どのようにしてそれがブロック型デバイスに反映されるか、ですがざっくりまとめると
- ページ(仮想メモリ上の最小単位。カーネル内データ構造)にそのページは汚れてます = 後でブロック型デバイスに書き出す必要がありますとフラグを立てる。フラグを立てたらすぐプロセスに処理は戻る。
- カーネルスレッドの pdflush が定期的に汚れたページを検索して汚れたページとブロック型デバイスと同期を取る
というカーネルスレッドが別スレッドで処理する非同期書き込み方式になってます。(ここで言う非同期というのは AIO API の"非同期"ではないです。)
本当にそうなのか、検証してみましょう。
#!/usr/local/bin/perl use strict; use warnings; use IO::File; use Time::HiRes qw/usleep/; use constant DATA => "12345678" x 128; my $io = IO::File->new("/home/naoya/tmp/test.dat", 'w') or die $!; while (1) { $io->syswrite(DATA); usleep(100); }
という、0.1 秒毎に特定のファイルにひたすらテキストを追記していくプログラムを実行する。これを実行しつつ、別の端末で vmstat を1秒間隔で見てみます。
% vmstat -a 1 procs -----------memory---------- ---swap-- -----io---- --system-- ----cpu---- r b swpd free inact active si so bi bo in cs us sy id wa 2 0 262136 17696 261772 681116 0 1 5 16 104 31 11 1 88 0 0 0 262136 17632 261768 681168 0 0 0 0 104 115 35 1 64 0 0 0 262136 17568 261768 681220 0 0 0 0 102 108 33 0 67 0 0 0 262136 17504 261772 681268 0 0 0 424 102 114 45 0 55 0 0 0 262136 17440 261768 681320 0 0 0 0 102 109 50 0 50 0 0 0 262136 17376 261768 681372 0 0 0 0 102 108 45 0 55 0 0 0 262136 17312 261768 681420 0 0 0 0 102 110 42 0 58 0 0 0 262136 17312 261768 681472 0 0 0 0 102 109 41 0 59 0 0 0 262136 17248 261772 681520 0 0 0 276 102 109 34 0 66 0 0 0 262136 17184 261768 681572 0 0 0 0 102 108 38 0 62 0 0 0 262136 17120 261768 681624 0 0 0 0 102 109 44 0 56 0 0 0 262136 17056 261772 681672 0 0 0 0 102 111 40 0 60 0 0 0 262136 17056 261768 681724 0 0 0 0 102 108 45 0 55 0 0 0 262136 16992 261764 681780 0 0 0 292 102 110 44 0 56 0 0 0 262136 16928 261764 681828 0 0 0 0 102 109 42 0 58 0 0 0 262136 16864 261764 681880 0 0 0 0 102 108 47 0 53 0 0 0 262136 16800 261768 681928 0 0 0 0 102 108 36 0 64 0 0 0 262136 16800 261764 681980 0 0 0 0 102 109 46 0 54 0 0 0 262136 16736 261760 682036 0 0 0 276 102 109 41 0 59 0
ここで見るべきは bo = Blocks sent to a block device (blocks/s). です。プロセスが書き込み処理をするたびにブロック型デバイスにデータを書き出すとしたら、ここの値は常に 0 以上になるはずですが、実際には 5 秒に一回だけ要求が発生してるのがわかります。
5秒、というのはカーネルスレッドの pdflush が汚れたページを書き出す間隔です。これは sysctl の vm.dirty_writeback_centisecs で設定されています。
% /sbin/sysctl vm.dirty_writeback_centisecs vm.dirty_writeback_centisecs = 500
確かに5秒になってますね。
ブロック型デバイスとの同期は遅延していて、プロセスから書き込み要求を受け取った段階ではカーネルはページキャッシュに書き込みフラグを立てているだけというのはこれで分かります。ただしこれは比較的システムリソースに余裕があるときの状態で、ディスクへの書き出し処理は pdflush の定期的な処理以外にも発生します。
書き出し処理が発生する条件
書き出し処理が発生する条件を Linux カーネル 2.6 解読室 P.308 の表 17-6 ディスクへの書き出し処理を参考にリストアップしてみます。
- ユーザープロセスによる明示的な同期書き出しの指定
- ファイルを O_SYNC モードでオープンする
- ファイルシステムが -o sync オプションでマウントする
- fsync(2) / fdatasync(2) を発行する
- sync(2) を発行する
- カーネルスレッド (pdflush) によるバックグラウンドでの書き出し
以上になります。
ユーザーが明示的に同期を命令しない限り、ブロック型デバイスの書き出しはカーネルスレッドが非同期に行ってるのがわかります。従ってこの辺からも通常の write 時にはカーネルは決してユーザープロセスをブロックしない...という結論になります。
read は待ち合わせる
話は変わって、write ではなく read の話。
read がページキャッシュにまだ載ってないデータを読み取ろうとしたとき、ブロック型デバイスとの I/O を待ち合わせる必要があるのはまあ明らかです。その read 処理がどのような流れになっているかは同じくカーネル解読室の P.298 が分かりやすい。
- まずページキャッシュを検索
- ページキャッシュがあった場合
- そのページキャッシュにデータがちゃんと載ってるか確認 (同時に同じページにアクセスしてきた他の誰かがデータを読み込み中なのを割けるため。)
- 載ってなかった場合はほかの誰かが完了するまで待ち合わせ
- ページキャッシュのデータをプロセス空間にコピー
- そのページキャッシュにデータがちゃんと載ってるか確認 (同時に同じページにアクセスしてきた他の誰かがデータを読み込み中なのを割けるため。)
- ページキャッシュがなかった場合
- 新規ページキャッシュをアロケート
- そのページをページキャッシュの管理オブジェクト (address_space 構造体) に追加
- ファイルシステムにデータの読み取り命令を発行。
- I/O の完了を待ち合わせる
という流れになっているそうです。つまり、
- 他の誰かが同じデータを読み取ろうとしてるとき
- ページキャッシュに該当データがなかったとき
に待ちが発生します。このときプロセスは休止していて、ハードウェアの読み取りを待っている状態 = TASK_UNINTERUPPTIBLE = 割り込み不可能なタスクの一時停止状態です。
ちょっと脱線するけど 負荷とは何か - naoyaのはてなダイアリー で調べたように TASK_UNINTERUPPTIBLE なプロセスはロードアベレージとして換算されます。なので http://naoya.g.hatena.ne.jp/naoya/20070518/1179467301 で遭遇したような、バグでなんらかのプロセスが TASK_UNINTERUPPTIBLE 状態から返れなかったりするとロードアベレージが常に 1.00 とかおかしなことになったりもします。
とにかく read は write と違ってプロセスを待たせます。
「書き込み処理」とは / write と read + write は区別すること。
- write はプロセスをブロックしない
- read は待ち合わせが必要な場合プロセスをブロックする
なわけですが、ここで write とは何かのそもそも論を考えてみます。
ここまでに議論している "write" とは、あくまでカーネルの視点でみた場合の write。書き出し処理それだけ。一方アプリケーションのレベルで "write" "書き込み" と言った場合、必ずしも「書き込み」だけとは限りません。
例えばファイルの一部を更新する場合。この場合、書き込みは書き込みですが、一度書き込み位置までファイルオフセットを seek させる必要があるかもしれません。更にこの seek する場所を特定するためにもしかしたら何かしらの検索が必要かもしれません。するとその時 read が発生しますよね。
こういった点に注意。アプリケーション内の書き込み処理、と一言にいってもそれが本当に write だけなのか、それともある程度の read 処理を伴う一連の処理なのかは区別して考える必要があります。
Apache のログ
相当なアクセスのあるウェブサーバーではアクセスログへの書き込みが結構あります。tail -f access_log がまったく目で追えないぐらい速い、というのもざらです。Apache のログの書き込みって結構すごいけど、別にそれでシステムが重くなったりとかしないよなー、と不思議に思ったことがある方は多いかもしれません。
このアクセスログについて先の視点で考えてみましょう。ファイルの末尾にひたすら追記していくだけで良いので read が発生しない、純粋に write しまくりな状態です。ここで仮に write のたびにディスクと同期しているとしたら大変そうですね。
実際はログが結構な勢いで書き込まれてるウェブサーバーで vmstat を見てみましょう。
% vmstat 1 procs -----------memory---------- ---swap-- -----io---- --system-- ----cpu---- r b swpd free buff cache si so bi bo in cs us sy id wa 0 0 128 24644 33552 3686236 0 0 2 17 2 0 0 0 99 0 0 0 128 24644 33552 3686236 0 0 0 416 347 67 3 1 95 2 0 0 128 24652 33552 3686332 0 0 0 0 316 105 0 0 100 0 0 0 128 24652 33552 3686332 0 0 0 0 323 99 0 0 100 0 1 0 128 24660 33552 3686404 0 0 0 0 315 91 0 0 100 0 0 0 128 24660 33552 3686404 0 0 0 0 322 102 0 0 100 0 0 0 128 24544 33552 3686492 0 0 0 320 357 109 1 1 96 3 0 0 128 24544 33552 3686492 0 0 0 0 307 78 0 0 100 0 0 0 128 24552 33552 3686564 0 0 0 0 311 88 0 0 100 0 1 0 128 24552 33552 3686564 0 0 0 0 310 55 0 0 100 0 0 0 128 24552 33552 3686624 0 0 0 0 306 50 0 0 100 0 1 0 128 24552 33556 3686620 0 0 0 288 353 88 0 1 96 3
やっぱりブロックデバイスへの書き込みは 5秒おきです。Apache のプロセスはログがディスクへ書き出されたかどうかは関係なく動きますし、ログの書き込み命令でブロックされることはありません。
「え、でも tail -f すると、5 秒に一回しかブロックデバイスと同期されないはずなのに、ログは常に流れまくってるよ」と思った方。いえいえ、いままさに tail -f が端末に流している出力はページキャッシュからコピーされたデータです。
カーネルは書き込み要求を受け取ってページ(キャッシュ)をアロケートしてデータをページにコピーして、あとは 別スレッドで動いている pdflush 任せ。ブロック型デバイスからではなく ページキャッシュにコピーされたものから tail -f のプロセスのバッファにコピーされたものがあなたの目に届いている。
...と、Apache のログについて考えてみると write はプロセスを待たせない、というのがよくわかります。
ページキャッシュに確保できるメモリ量と書き込みの関係
では、書き込み処理が多いホストでディスクI/O待ちが発生しがちなのはなんででしょうか。たぶん原因はページキャッシュ用のメモリが足りない、というところにあるんじゃないかなと思います
Linux のページキャッシュ - naoyaのはてなダイアリー でも触れたようにディスクの内容をページキャッシュに展開するのに十分なメモリがあれば I/O wait はほとんど発生しないのは OK。これを前提に考えていきます。
- ページキャッシュに載せるのに十分なメモリがない場合、まずページキャッシュに載ってないページの読み出し時に read が待たされます。
- write が頻繁に起こっている環境でページキャッシュ分のメモリが足りないと、「書き出し処理が発生する条件」で挙がっている background_writeback() を実行するカーネルスレッドが頻繁に起き上がり、5秒に一回の定期書き出し以外でのブロック型デバイスへの書き込み要求が多くなります。
- ただでさえディスクに対して read が発生してそこでディスクは忙しいのに、write も頻繁に発生するようになって、ますます read は遅くなっていきます。
- 書き込み用にページを確保するために、すでにキャッシュされたページを解放してやらないといけない。 → ますますディスクに read が。
- 結果、I/O 待ちが多発する。
ということになるかと思います。
また、こういう状況に対処するにはディスクからの read を発生させないようメモリを積めばよいのと同じく、write もメモリを増やせばよいのが分かります。(あとは pdflush 周りの sysctl パラメータをいじるのでもある程度は緩和できるかもしれません。) 実際最近、はてなでも write が多くてひーひー言ってたあるホストに十分なメモリを積んだところ、I/O 待ちを無くすことができました。
さて、
また、free の結果はこんな感じなので、メモリに余裕はあるみたいです。
ということですが free の結果では、プロセスに割り当てるためのメモリが足りてるかどうかはわかりますが、ページキャッシュ用にメモリが足りてるかどうかはわかりません。
# free total used free shared buffers cached Mem: 2043756 2011160 32596 0 351016 847776 -/+ buffers/cache: 812368 1231388 Swap: 1052248
を見るにページキャッシュに割り当てられてるメモリは 800 MB強。実際そのサーバーが主な仕事で使っているデータは、合計でどのぐらいのサイズでしょう。おそらく、キューに溜まったメールその他 OS 上で必要なデータのサイズをあわせると 800MB を超えるんではないかなあと思います。外してたらごめん。
あと id:hirose31 さんがコメントしてますが、アプリケーションが SYNC モードでファイルを開いてたり、明示的に fsync() してたりするとそこで wait が発生するのは言わずもがな、です。
write はプロセスを待たせないのをコードで見る
最後にカーネルのコードを実際に追って、write がプロセスを待たせていないというのを見ていきます。ext2 を題材に。
操作対象のファイルオブジェクトにセットするコールバック一覧であるところの file_operations 構造体を見ると、read や write が発行されたときどういう関数が呼ばれるかがすぐわかります。
/* * We have mostly NULL's here: the current defaults are ok for * the ext2 filesystem. */ const struct file_operations ext2_file_operations = { .llseek = generic_file_llseek, .read = do_sync_read, .write = do_sync_write, .aio_read = generic_file_aio_read, .aio_write = generic_file_aio_write, .ioctl = ext2_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = ext2_compat_ioctl, #endif .mmap = generic_file_mmap, .open = generic_file_open, .release = ext2_release_file, .fsync = ext2_sync_file, .sendfile = generic_file_sendfile, .splice_read = generic_file_splice_read, .splice_write = generic_file_splice_write, };
と、write のときは do_sync_write() が呼ばれます。do_sync_write() は filp->f_op->aio_write のラッパ。filp->f_op->aio_write は上記の aio_write = generic_file_aio_write()。AIO API に使うためのコールバックを実行して、結果を待ち合わせることで同期処理としています。
generic_file_aio_write() の中を見てみます。
ssize_t generic_file_aio_write(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos) { struct file *file = iocb->ki_filp; struct address_space *mapping = file->f_mapping; struct inode *inode = mapping->host; ssize_t ret; BUG_ON(iocb->ki_pos != pos); mutex_lock(&inode->i_mutex); ret = __generic_file_aio_write_nolock(iocb, iov, nr_segs, &iocb->ki_pos); mutex_unlock(&inode->i_mutex); if (ret > 0 && ((file->f_flags & O_SYNC) || IS_SYNC(inode))) { ssize_t err; err = sync_page_range(inode, mapping, pos, ret); if (err < 0) ret = err; } return ret; }
- iノードオブジェクトのロックを獲得(write(2) を一度に1プロセスしか発行できないように)
- __generic_file_aio_write_nolock() = 関連するページに PG_dirty フラグを設定する (とカーネル本に書いてました.)
- 何かしらの条件で SYNC が明示的に命令されているときのみ sync_page_range() でページとブロック型デバイスを同期する
- 終わり
ということで、明示的に同期命令が加えられているとき意外はブロック型デバイスとページを同期させることはしていないのがわかります。
まとめ
- write はプロセスを待たせない
- ただし明示的に sync する場合は待たせる
- pdflush が別スレッドでブロック型デバイスへの書き出しを行っている。デフォルトでは5秒おき。
- Apache のログが書き込みまくっててもシステムは平気な理由はページキャッシュにあり
- 十分なメモリを足せば書き込み性能も向上させられる
- generic_file_aio_write() を見れば明示的に sync する場合以外はブロック型デバイスと同期しない、というのがコードで分かる
以上がわかりました。これで答えになってるかな?
参考
- 作者: Daniel P. Bovet,Marco Cesati,高橋浩和,杉田由美子,清水正明,高杉昌督,平松雅巳,安井隆宏
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2007/02/26
- メディア: 大型本
- 購入: 9人 クリック: 269回
- この商品を含むブログ (73件) を見る
- 作者: 高橋浩和,小田逸郎,山幡為佐久
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2006/11/18
- メディア: 単行本
- 購入: 14人 クリック: 197回
- この商品を含むブログ (118件) を見る