2015/12/22
KAICHO: s_naray[at]yahoo[dot]co[dot]jp
※SPAM防止のため捻ってある

Yum Updateが遅い!

■はじめに

本文書は、CentOS5において、"yum update"を実行するとやたらめったら遅いよ! という問題を解決するまでの道のりを記録したものである。我輩自身の備忘録と、 後進の人々の参考までに残す所存ナリ。

■問題点

とにもかくにも、yum updateが遅いんじゃぁぁぁ!一回実行するだけで一時間近く かかる。いったいナニやってんの?どうしてそんなに遅いの?原因は?対策は? 再発防止策は!?…はぁはぁ。ことほど左様に、我輩はこの問題にストレスを 感じておったわけです。はい。

あ、今回はyum fastestmirrorとかで回避できる問題じゃないことはあらかじめ 調査済みなのであしからず。

■調査

「何が遅いのか」を、まずは探さねばならん。言えば、画面上に以下のように 表示されている部分がとにかく遅い。さくさく進んだかなー、と思ったら、 同じところで数分以上うんうんうなったりする。

# time yum upgrade
Loaded plugins: rhnplugin, security
You can use up2date --register to register.
(snip)
real    52m43.977s  ←遅ぇ!
user    1m45.447s
sys     1m22.121s
(snip)

●topを見てみる

どーせyumがCPU食ってるんだろ?と思ってtopを表示した結果が以下のとおり。 yumがCPUを食って…ないよ? あれ?

top - 16:32:38 up 13 days,  2:34,  3 users,  load average: 1.75, 0.60, 0.24
Tasks: 137 total,   1 running, 136 sleeping,   0 stopped,   0 zombie
Cpu0  :  0.0%us,  0.3%sy,  0.0%ni,  0.0%id, 99.7%wa,  0.0%hi,  0.0%si,  0.0%st
Cpu1  :  0.0%us,  0.0%sy,  0.0%ni,100.0%id,  0.0%wa,  0.0%hi,  0.0%si,  0.0%st
Mem:   5695124k total,  4983816k used,   711308k free,  1043052k buffers
Swap:  4194300k total,        0k used,  4194300k free,  3177136k cached

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 4669 root      20   0 12808 1060  768 R  0.3  0.0   0:00.13 top
    1 root      20   0 10416  708  592 S  0.0  0.0   0:08.46 init
    2 root      20   0     0    0    0 S  0.0  0.0   0:00.09 kthreadd
    3 root      20   0     0    0    0 S  0.0  0.0   0:00.45 ksoftirqd/0
    5 root      20   0     0    0    0 S  0.0  0.0   0:00.00 kworker/u:0
    6 root      RT   0     0    0    0 S  0.0  0.0   0:00.42 migration/0
    7 root      RT   0     0    0    0 S  0.0  0.0   0:02.64 watchdog/0
(snip)

そんかし、とにかくI/O Waitが高い。ほぼ100%張り付き状態。そのワリには、 上位にはDになってるプロセスやCPU食ってるプロセスは見当たらない。 ということは、本当にものスゴいI/O待ちをしてるということか?でもyum update くらいでそんな巨大なI/Oって要求されるものか?

●straceを取ってみる

こういうときは、上のレイヤーから探すに限る。まずはstraceを取って、yumが 何をしようとして止まっているのかを確認してみよう。さすれば、どこが遅いのか くらいはわかるんじゃなかろうか。取り方は以下の通り。CentOS5のころの yumは^Cで止まらないので、まず^Zで止めてkillするのがよろしかろう。

# strace -ttt -o yum_upgrade.txt /usr/bin/yum upgrade
Loaded plugins: rhnplugin, security
You can use up2date --register to register.
centos5_latest: [########################################               ] 7084/9588^Z
[1]+  Stopped      strace -ttt -o yum_upgrade.txt /usr/bin/yum upgrade
# kill %1
[1]+  Terminated   strace -ttt -o yum_upgrade.txt /usr/bin/yum upgrade
# 

で、これで取れたstraceを見てみると…、ファイル /var/cache/yum/centos5_latest/primary.xml.gz.sqlite をごそごそやってるときに、1024byteのwrite()がやたらめったら遅くなることが あるのがわかった。

1450767911.641136 open("/var/cache/yum/centos5_latest/primary.xml.gz.sqlite", O_RDWR|O_CREAT|O_LARGEFILE, 0644) = 8
(snip)
1450767920.360336 write(8, "\n\0\0\0\26\1V\0\1V\1u\1\224\1\263\1\322\1\361\2\20\2/\2N\2m\2\214\2\253"..., 1024) = 1024
1450767920.360455 _llseek(8, 13068288, [13068288], SEEK_SET) = 0
1450767920.360538 read(8, "\2\0\0\0\27\0]\0\0\1r\321\3\322\3\245\3x\3K\3$\2\375\2\326\2\255\2\204\2["..., 1024) = 1024
1450767920.360671 _llseek(8, 103409664, [103409664], SEEK_SET) = 0
1450767920.360771 write(8, "\n\0\0\0\17\2/\0\2/\2N\2m\2\214\2\253\2\312\2\351\3\10\3'\3F\3e\3\204"..., 1024) = 1024 ★これで5秒かかっている
1450767925.402961 _llseek(8, 7990272, [7990272], SEEK_SET) = 0
1450767925.403094 write(8, "\n\0\0\0\30\0\316\0\0\316\0\361\1\24\0017\1Z\1}\1\240\1\303\1\346\2\t\2,\2O"..., 1024) = 1024
1450767925.403260 _llseek(8, 91370496, [91370496], SEEK_SET) = 0
1450767925.403376 read(8, "\2\0\0\0\26\0\327\0\0\0\\e\1\32\0\372\0\327\1:\1~\1]\1\304\1\241\1\345\2\10"..., 1024) = 1024
1450767925.403499 _llseek(8, 34436096, [34436096], SEEK_SET) = 0

1024byteのwrite()に五秒以上かかるなんて!syncなfilesystemじゃないし、 open()にO_DIRECTやO_SYNCフラグが付加されているわけでもない。なぜ? さらによく見ると、seek()してwrite()してseek()してread()して…の 繰り返しなので、もしかしたら細かなランダムアクセスが原因かもしれない。

●kernel stackを見てみる

ちょい新しいkernelが必要なんだが、プロセスがkernel内のどこを実行しているかを リアルタイムに知る方法として、/proc//stack に実装された。だもんで、 ここを定期的に表示してみて、「遅いとき」にどのようになってるかを 確認してみよう。そしたら、代表的なのは以下のようなものだった。

# while : ; do cat /proc/3039/stack; sleep 1; done
(snip)
[] sleep_on_buffer+0xe/0x20
[] __wait_on_buffer+0x28/0x30
[] __sync_dirty_buffer+0x8d/0xd0
[] sync_dirty_buffer+0x13/0x20
[] journal_dirty_data+0x113/0x220 [jbd]
[] ext3_journal_dirty_data+0x24/0x60 [ext3]
[] journal_dirty_data_fn+0x25/0x30 [ext3]
[] walk_page_buffers+0x56/0xa0 [ext3]
[] ext3_ordered_write_end+0x7a/0x120 [ext3]
[] generic_perform_write+0x122/0x1a0
[] generic_file_buffered_write_iter+0x2b/0x60
[] __generic_file_write_iter+0x20b/0x3a0
[] __generic_file_aio_write+0x85/0xa0
[] generic_file_aio_write+0x5f/0xb0
[] do_sync_write+0xe1/0x120
[] vfs_write+0xce/0x190
[] sys_write+0x55/0x90
[] system_call_fastpath+0x16/0x1b
[] 0xffffffffffffffff
(snip)

おおぅなるほど!ext3のjournalか!journal書き込みは同期I/Oだから、ここで ディスクI/Oを待ってモッスゴ遅くなっておったのだな。確かに、/var/cache/yumは /(root)と同じパーティションにあって、/ は ext3 のdefault(つまり data=ordered)でmountされていた。

# mount | grep " / "
/dev/mapper/VolGroup00-LogVol00 on / type ext3 (rw)
#

■検証

回避策がないか、ということも含めて、「こうすればもう少し高速になるんじゃ ないか?」というのを検証してみることにする。

●tmpfs mount

んー、じゃぁ、/var/cache/yum/を、スッゲぇ高速なtmpfsに配置したらどうだろうか、 と思ってしまった。早速実行してみる。一応以前のは取っとく。

# cd /var/cache/yum/
# mv centos_latest  centos5_latest.org
# mkdir centos_latest
# mount -t tmpfs none centos5_latest
# time yum upgrade
(snip)
real    1m24.733s
user    0m34.308s
sys     0m8.343s
(snip)
# rm -rf centos5_latest
# mv centos5_latest.org centos5_latest

は や す ぎ るーッ! やっぱtmpfs(RAM diskみたいなもん)は偉大だな!Journalもないしな!

●ext2 mount

ext3のjournalが同期書き込みするから悪いんだろ、という見当がついたので、 これを裏付けるために、/ を ext2 でmountして、yum upgradeがどれだけ高速になるか 確認してみよう。/はinitrd中でmountされるので、ext3→ext2に変更するなら initrdの再作成とOS再起動が必要なことに注意。

# vi /etc/fstab
(/ の filesystem type を ext2 に変更)
# mkinitrd -f /boot/initrd-`uname -r` `uname -r`
# shutdown -r now
(再起動)
# time yum update
(snip)
real    23m16.943s
user    0m55.520s
sys     0m36.368s
(snip)
#

むむぅ、半分くらいにはなったが、思ったよりは早くならないな…。 このときの/proc//stackの典型的な出力は以下の通り。 やっぱりI/O待ちではある。うーん、ランダムアクセスも原因なのかな。

# cat /proc/1987/stack
[] sleep_on_page+0xd/0x20
[] wait_on_page_bit+0x5a/0x60
[] grab_cache_page_write_begin+0x94/0xc0
[] block_write_begin+0x28/0x70
[] ext2_write_begin+0x45/0x80 [ext2]
[] generic_perform_write+0xad/0x180
[] generic_file_buffered_write_iter+0x25/0x50
[] __generic_file_write_iter+0x261/0x510
[] __generic_file_aio_write+0x69/0x70
[] generic_file_aio_write+0x57/0xa0
[] do_sync_write+0xbd/0x100
[] vfs_write+0x9b/0x160
[] sys_write+0x42/0x70
[] sysenter_do_call+0x12/0x28
[] 0xffffffff

●ext3 mount with data=writeback

ext3のjournalの同期書き込みが遅いのはわかったので、 じゃぁjournalの書き込みモードを、data=journalからdata=writebackにしたら どうなるだろう、と思ってしまった。よーしやってみようぜー。なお、障害時の fsが壊れる恐れがあるとかスッゴいキケンなので、よい子はまねをしないように。

(設定して再起動した)
# mount | grep " / "
/dev/mapper/VolGroup00-LogVol00 on / type ext3 (rw,noatime,data=writeback)
# time yum update
(snip)
real    20m42.535s
user    0m54.448s
sys     0m36.884s
(snip)
#

うーん、そんなに大きくは変わらないねぇ。 じゃ、やっぱりランダムアクセスなのかな…。

■結論

今回の問題は、以下の三つが重なったために起こった性能問題だったことがわかった。

さらに、後で判ったが、ATF(4Kセクタ)問題も本件に大きな影を落としていた。 4KセクタなHDDに対し、伝統的なパーティショニング(先頭パーティションが63セクタ 目から始まる)を施してしまうと、I/O速度がとたんに1/2以下になっちゃうという アレ。今回はそれも絡んでた模様。

■対策

…有効な対策はないなぁ…。

  1. yum updateのときは /var/cache/yum/以下をtmpfsに移して作業する
  2. パーティションを切りなおし、先頭が2048セクタ目から始まるようにする

一応このくらいは考えられるけど。一番効果デカいのはtmpfsだった。 もういっそyum updateするラッパスクリプト書いて、そこでtmpfs使いながら updateするようにしたらどうかねぇ。

■おわりに

というわけで、遅い遅いとゆってたyum updateをなんとかするという調査はこれにて 終了。最後はちょいもやっとしたけど、原因はわかったのでよかったことにする。 ext3のjournalがかかわるので、これがXFSとかext4とかbtrfsだとまた変わって くるんだろうけど、CentOS5じゃそのあたりうまく使えないし、とりあえずは これで調査完了とする。なお、CentOS6とかだとデータベースが整列された 状態でyum repository上に配置されてるらしく、今回のような問題は起こらない。 CentOS5でもそういうことしてくれてればよかったのにね!