mirrors 修复 Debian 源缓存不一致问题

mirrors 在此前两个星期,Debian 源有时 Packages 文件与软件包有时会 MD5 不一致,这是因为 SSD cache 没有及时更新。本来是希望每次同步之后更新当前源,每6小时整体更新(failover,以防万一)。但实际出现了三次问题:

  1. 对 rsync 参数理解不对,旧缓存文件没有删除,导致 SSD 满了。现在自己写了个很丑的 bash 脚本来实现。
  2. ftpsync 的同步写在文件末尾,而末尾的 runmirrors 有可能失败,导致脚本提前退出,不执行更新。
  3. ftpsync 的同步写在 stage1 后,stage2 更新 Packages 和索引文件后并没有更新缓存。

ftpsync 真是个比较复杂的东西。非常抱歉这种间歇性的缓存不一致给 Debian 用户带来的麻烦。

mirrors 修复缓存更新慢的问题

现在设置了 trigger,每个源同步完成后都会立即更新这个源的 cache,而不用等每天凌晨统一更新 cache 了。这使得缓存不一致的时间缩短到秒级。

此外,由于 rsync 同步不够灵活,全部删掉再重新复制又太浪费,我重新发明了轮子来做同步。算法见下面的注释:

# There are actually 3 file lists:
#  - cache_list: files to be cached (param 1 of this func)
#  - WWWROOT: authoritative source
#  - CACHEROOT: cache files
# After sync, CACHEROOT should contain up-to-date files in and only in the intersection of cache_list and WWWROOT.
# So comes the algorithm:
#  1. Remove files in CACHEROOT but not in cache_list
#  2. Remove files in CACHEROOT but not in WWWROOT or not up-to-date
#  3. Copy non-cached files in cache_list to CACHEROOT, if it exists in WWWROOT

完整的代码 git clone https://gitgeek.net/mirrors/ssd-cache.git

mirrors 部分源从磁盘阵列迁移到本地磁盘

由于磁盘阵列负载过高,io util 常年 100%,现将一些大源移到本地磁盘。选取目标磁盘主要有两个原则,一是空余空间足够,二是目前负载不高。

移到 sdd 上的源是 qtproject。sdd 上原有的源主要有 debian 和 gentoo。

移到 sde 上的源是 ubuntu 和 ubuntu-releases。sde 上原有的源主要有 CPAN,CTAN,deepin,opensuse 和 cygwin。

移到 sdf 上的源是 debian-cd。sdf 上原有的源主要有 centos,eclipse 和 archlinux。

To Mirrors Maintainers:移动源的步骤:

  1. 以 mirror 用户登录 sync LXC
  2. 在 screen 或 byobu 中 rsync -av /mnt/original-disk/mirror /mnt/new-disk/
  3. 修改 ~/etc/ 中对应的配置文件
  4. git commit -a -m “commit message” –author=”Your Name <you@gmail.com>”
  5. git push
  6. rsync 结束后,原子地替换掉 HTTP 根目录中的符号链接:ln -s /mnt/new-disk/mirror /tmp/
  7. mv /tmp/mirror /srv/www/
  8. 检查 HTTP 是否可以正常访问。
  9. 如果有提供 FTP 服务,要到主机里修改 /etc/fstab,并 mount -a
  10. 如果有提供 rsync 服务,要到主机里修改 /etc/rsyncd.conf

修复 mirrors 缓存过期问题

mirrors 的源有一些没有更新,原因是使用了过期的 SSD cache。SSD cache 本来会与源目录同步,不幸的是 SSD 满了。撑满 SSD 的原因是同步时没有删除已经不在缓存文件列表中的文件,这又是因为我对

rsync --delete --file-list

的理解有误。我以为这样会删除源目录中不在 file-list 中的文件,事实上不会。

改成了根据缓存文件列表生成符号链接到临时目录,再同步文件到 SSD:

rsync --delete --copy-links $TMP_LINK_ROOT/ $SSD_CACHE_ROOT/

commit: https://gitgeek.net/mirrors/ssd-cache/commit/6602b3b47d55d47d2185a9ed8ad1220a54cd905a

服务器优化 TCP 参数

blog 和 mirrors 服务器调整了 ip_conntrack 配置:

net.ipv4.netfilter.ip_conntrack_max=655360</code>

# should not be less than net.ipv4.tcp_keepalive_time (default 7200)
net.netfilter.nf_conntrack_tcp_timeout_established=14400

blog,mirrors 和 gitlab 调整了 TCP 发送窗口大小限制,以充分利用高延迟和高带宽的线路:

net.core.wmem_max=12582912
net.core.rmem_max=12582912
net.ipv4.tcp_rmem= 10240 87380 12582912
net.ipv4.tcp_wmem= 10240 87380 12582912

blog,mirrors 和 gitlab 在 HTTP 持久连接期间不再慢启动,以加快第二次及以后访问页面:

net.ipv4.tcp_slow_start_after_idle=0

增加 TCP 发送和接收默认窗口大小到 10 个 MSS,以减少小 HTTP 请求和 HTTPS 握手的来回次数。对 2.6.33 以下内核只能调整默认发送窗口大小(以下命令仅为示例):

# example on Debian wheezy
ip route change default via 202.141.176.126 initrwnd 10 initcwnd 10
# example on Debian squeeze
ip route change default via 202.141.176.126 initcwnd 10

Mirrors 扩充磁盘阵列空间

为了给即将入驻的 sourceforge 源腾地方,mirrors 将磁盘阵列的 XFS 扩充到 24T,现有 14T 可用空间。

删除无用内容

首先删除一些未同步成功又占据大量空间的源,释放了约 3T 磁盘空间。

  • android-src
  • android-releases
  • sourceforge
  • google-v8
  • chromiumos

扩容 XFS

XFS 号称能在线扩容,不过那是在 XFS 建立在 LVM 上时才可以。由于害怕 LVM 影响性能,mirrors 的 XFS 是直接建立在磁盘阵列的 GPT 上的。扩容 XFS 分为三步:

  1. 修改分区表
  2. 让内核重新载入分区信息,而这需要卸载文件系统
  3. 在线扩容 XFS

卸载磁盘阵列文件系统

以往卸载磁盘阵列的文件系统是全手工操作,折腾一次至少要十几分钟,其间磁盘阵列上的源都无法访问,整个网络服务(nginx 等)也会有一两分钟中断。这次我们使用了脚本,基本服务(nginx)每次中断时间不超过5秒,磁盘阵列的中断时间不超过2分钟(第一次中断了1分多钟是由于脚本里忘了 mount -a 重新挂载,收到报警短信后赶紧 mount -a)。

  1. 停止 LXC(因为 LXC 的根文件系统在磁盘阵列上)
  2. 杀掉所有服务进程
  3. 卸载已挂载的磁盘阵列文件系统(如果这一步出现问题,需要用 lsof 排查)
  4. 做想做的事
  5. 重新挂载文件系统
  6. 启动服务进程
  7. 启动 LXC

一开始的脚本是这样的:

sudo service rsync stop; sudo service vsftpd stop; sudo service nginx stop; mount | grep /dev/sdh1 | awk '{print $3}' | while read dir; do sudo umount $dir; sudo lsof $dir; done; sudo service nginx start; sudo service vsftpd start; sudo service rsync start

但发现 lsof 里还有很多 rsync 和 vsftpd 占着文件描述符,因此把 vsftpd 和 rsync 改成了杀气腾腾的 pkill:

sudo service nginx stop; sudo pkill vsftpd; sudo pkill rsync

还是有一些 rsync 进程发了 SIGTERM 信号仍无动于衷,因此改成了 SIGKILL(kill -9 所用的不可捕获杀进程信号)。

sudo service nginx stop; sudo pkill vsftpd; sudo pkill -SIGKILL rsync

现在 umount 成功了,但 rsync 启动失败了。这是由于没有删掉 rsyncd 的 pid 文件,强制删除即可。

sudo service nginx stop; sudo pkill vsftpd; sudo pkill -SIGKILL rsync; mount | grep /dev/sdh | awk '{print $3}' | while read dir; do sudo umount $dir; done; sudo service nginx start; sudo service vsftpd start; sudo rm -f /var/run/rsyncd.pid; sudo service rsync start

坑爹的 udev rule

扩容顺利完成了,不过 partprobe 之后,sdh 变成了跟 sdh1 一样的大小,分区表也不见了。重新启动 iscsi,问题依然如故。

$ sudo service nginx stop; sudo pkill vsftpd; sudo pkill -SIGKILL rsync; mount | grep /dev/sdh | awk '{print $3}' | while read dir; do sudo umount $dir; done; sudo /etc/init.d/open-iscsi restart; sudo service nginx start; sudo service vsftpd start; sudo rm -f /var/run/rsyncd.pid; sudo service rsync start
Stopping nginx: nginx.
Unmounting iscsi-backed filesystems: Unmounting all devices marked _netdev.
Disconnecting iSCSI targets:Logging out of session [sid: 1, target: iqn.2002-10.com.infortrend:raid.sn8223150.001, portal: 192.168.10.1,3260]
Logout of [sid: 1, target: iqn.2002-10.com.infortrend:raid.sn8223150.001, portal: 192.168.10.1,3260] successful.
.
Stopping iSCSI initiator service:.
Starting iSCSI initiator service: iscsid.
Setting up iSCSI targets:
Logging in to [iface: default, target: iqn.2002-10.com.infortrend:raid.sn8223150.001, portal: 192.168.10.1,3260] (multiple)
Login to [iface: default, target: iqn.2002-10.com.infortrend:raid.sn8223150.001, portal: 192.168.10.1,3260] successful.
.
Mounting network filesystems:.
Starting nginx: nginx.
Starting FTP server: vsftpd.
Starting rsync daemon: rsync.

$ sudo fdisk -l /dev/sdh

Disk /dev/sdh: 24189.3 GB, 24189254763008 bytes
255 heads, 63 sectors/track, 2940842 cylinders, total 47244638209 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

Disk /dev/sdh doesn't contain a valid partition table

$ sudo fdisk -l /dev/sdh1

Disk /dev/sdh1: 24189.3 GB, 24189254763008 bytes
255 heads, 63 sectors/track, 2940842 cylinders, total 47244638209 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00000000

Disk /dev/sdh1 doesn't contain a valid partition table

线索在 syslog 里:

udevd[13846]: kernel-provided name 'sdh1' and NAME= 'sdh' disagree, please use SYMLINK+= or change the kernel to provide the proper name

原来是下面的 udev rule 捣的鬼,内核把 sdh1 和 sdh 都 probe 出来了,sdh1 占用了 sdh 这个名字(因为它们都符合 udev rules,不信可以自己执行 /lib/udev/scsi_id 验证)……

$ cat /etc/udev/rules.d/80-persistent-iscsi.rules
KERNEL=="sd*", SUBSYSTEM=="block", PROGRAM="/lib/udev/scsi_id --whitelisted --replace-whitespace /dev/$name", RESULT=="3600d0231000da93e75966be33fd9a2b4", NAME="sdh"

修改方法很简单,加个 %n 就行了。

$ cat /etc/udev/rules.d/80-persistent-iscsi.rules
KERNEL=="sd*", SUBSYSTEM=="block", PROGRAM="/lib/udev/scsi_id --whitelisted --replace-whitespace /dev/$name", RESULT=="3600d0231000da93e75966be33fd9a2b4", NAME="sdh%n"

扩容过程演示

下面我们演示将磁盘阵列 XFS 从 22T 扩容到 24T 的完整过程(为了文章清晰,删除了一些输出)。

1. 查看分区表信息。GPT 分区表要用 gdisk,而非 fdisk。

boj@mirrors:~$ sudo gdisk /dev/sdh
GPT fdisk (gdisk) version 0.8.5

Partition table scan:
MBR: protective
BSD: not present
APM: not present
GPT: present

Found valid GPT with protective MBR; using GPT.

Command (? for help): i
Using 1
Partition GUID code: EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 (Microsoft basic data)
Partition unique GUID: 4592A37B-886C-40C9-A2AC-D9145B1D33D1
First sector: 2048 (at 1024.0 KiB)
Last sector: 47244640256 (at 22.0 TiB)
Partition size: 47244638209 sectors (22.0 TiB)
Attribute flags: 0000000000000000
Partition name: 'array'

2. 删除原有分区并新建分区、修改分区名称。注意分区起始扇区、GUID 都必须与原来的相同,否则文件系统无法识别!

Command (? for help): d 1
Using 1

Command (? for help): n
Partition number (1-128, default 1):
First sector (34-54684213214, default = 2048) or {+-}size{KMGTP}:
Last sector (2048-54684213214, default = 54684213214) or {+-}size{KMGTP}: 24T
Current type is 'Linux filesystem'
Hex code or GUID (L to show codes, Enter = 8300): EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
Changed type of partition to 'Microsoft basic data'

Command (? for help): c 1
Using 1
Enter name: array

Command (? for help): i
Using 1
Partition GUID code: EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 (Microsoft basic data)
Partition unique GUID: A5298F7D-A675-41DD-9AA3-BE119CA73DB3
First sector: 2048 (at 1024.0 KiB)
Last sector: 51539607552 (at 24.0 TiB)
Partition size: 51539605505 sectors (24.0 TiB)
Attribute flags: 0000000000000000
Partition name: 'array'

Command (? for help): w

Final checks complete. About to write GPT data. THIS WILL OVERWRITE EXISTING
PARTITIONS!!

Do you want to proceed? (Y/N): Y
OK; writing new GUID partition table (GPT) to /dev/sdh.
Warning: The kernel is still using the old partition table.
The new table will be used at the next reboot.
The operation has completed successfully.

3. 卸载磁盘阵列文件系统以便 partprobe 让内核重新载入分区表(详见上文)

boj@mirrors:~$ sudo lxc-list
RUNNING
  lxr
  pypi
  sync (auto)

boj@mirrors:~$ sudo lxc-stop -n pypi
boj@mirrors:~$ sudo lxc-stop -n sync
boj@mirrors:~$ sudo lxc-stop -n lxr

boj@mirrors:~$ sudo service nginx stop; sudo pkill vsftpd; sudo pkill -SIGKILL rsync; mount | grep /dev/sdh | awk '{print $3}' | while read dir; do sudo umount $dir; done; sudo partprobe; sudo mount -a; sudo service nginx start; sudo service vsftpd start; sudo rm -f /var/run/rsyncd.pid; sudo service rsync start
Stopping nginx: nginx.
Starting nginx: nginx.
Starting FTP server: vsftpd.
Starting rsync daemon: rsync.

boj@mirrors:~$ sudo lxc-start -n pypi -d
boj@mirrors:~$ sudo lxc-start -n sync -d
boj@mirrors:~$ sudo lxc-start -n lxr -d

4. 检查分区信息,使用 xfs_growfs 对 XFS 分区进行扩容。

boj@mirrors:~$ sudo gdisk -l /dev/sdh
GPT fdisk (gdisk) version 0.8.5

Partition table scan:
MBR: protective
BSD: not present
APM: not present
GPT: present

Found valid GPT with protective MBR; using GPT.
Disk /dev/sdh: 54684213248 sectors, 25.5 TiB
Logical sector size: 512 bytes
Disk identifier (GUID): 5E620981-EBFB-4EEA-BCF9-0052707F6859
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 54684213214
Partitions will be aligned on 2048-sector boundaries
Total free space is 3144607676 sectors (1.5 TiB)

Number Start (sector) End (sector) Size Code Name
1 2048 51539607552 24.0 TiB 0700 array

boj@mirrors:~$ sudo xfs_growfs /dev/sdh1
meta-data=/dev/sdh1 isize=256 agcount=22, agsize=268435455 blks
= sectsz=512 attr=2
data = bsize=4096 blocks=5905579776, imaxpct=5
= sunit=0 swidth=0 blks
naming =version 2 bsize=4096 ascii-ci=0
log =internal bsize=4096 blocks=521728, version=2
= sectsz=512 sunit=0 blks, lazy-count=1
realtime =none extsz=4096 blocks=0, rtextents=0
data blocks changed from 5905579776 to 6442450688

5. 检查扩容是否成功。

boj@mirrors:~$ df -lh /dev/sdh1
Filesystem Size Used Avail Use% Mounted on
/dev/sdh1 24T 11T 14T 43% /srv/ftp/ubuntu-old-releases

boj@mirrors:~$ sudo lxc-list
RUNNING
  lxr
  pypi
  sync (auto)

FROZEN

STOPPED
  mirror-lab
  mirror-lab_
  php-mirror
  root

最后提醒 mirrors 维护者,这类危险操作必须在 screen 或 byobu(screen 的封装,可以有选项卡)中进行,以免你的网络突然中断,脚本被 SIGHUP,将系统留在不可预测的状态。

mirrors 启用 SSD cache

优化之前,mirrors 磁盘阵列(sdh)的 io util 常常在 90% 以上,有些时候甚至“稳定”在 100%。从sda 往 sdh 复制一些小文件,竟然不到 5MB/s。尝试调整了 iscsi 的 MTU、txqueuelen 等网络参数,没有效果,瓶颈可能不在网络上。以一块 7500rpm SATA 磁盘随机读 75~100 iops 计算,RAID6 的 iops 大约是 450~600 iops,与我们的测试结果基本相符,因此磁盘阵列可能本来就这么慢。

SSD(/dev/sdb)被格式化为 ext4 文件系统(UUID=8e19059d-358e-41ca-8896-b9b26a2d4f59),用 tune2fs 去掉了 journal。挂载参数为 nodev,nosuid,noatime,discard,挂载点为 /mnt/ssd。

用 fio 测试结果如下(使用 1G 文件):

  • 连续读 139327KB/s,34831 iops
  • 连续写 1001.1MB/s,256500 iops
  • 同时连续读写,与单独测试相比没有明显变化
  • 随机读 15708KB/s,3926 iops
  • 随机写 762047KB/s,190511 iops
  • 同时随机读写,与单独测试相比没有明显变化

写的性能这么好,估计是由于 buffer。连续读性能不高,是因为 SSD 在 RAID 卡后面,不过对我们来说足够了。我们最关心的随机读性能 3926 iops 达到了预期,目前磁盘阵列的随机读性能不到 500 iops。

SSD cache 的缓存策略基于 HTTP access log。每天凌晨,选出前一周的 HTTP 200 请求,按照 URL 访问频率降序排序,填满 SSD cache 为止。

注册登录 USTC LUG GitLab,即可查看源代码:https://gitgeek.net/mirrors/ssd-cache

cron-cache.sh 每天运行一次,生成缓存:

  1. 读出最近7天的 HTTP access log
  2. 取出 200 请求,统计 URL 访问频率
  3. 筛选至少访问过2次的文件,按频率降序排序,得到 filefreq-20131010.gz 这样的文件列表。
  4. 从上述文件列表中依次取出文件,获取其大小,添加到缓存列表(tocache-20131010)中,直到达到设定的缓存总大小。
  5. 用 rsync 把缓存列表中的文件同步到缓存区。用 rsync 的好处是文件没有被修改时不需要重新复制,由于多数文件的访问频率是相对稳定的,每天复制的数据量并不大。

每个源同步后,要刷新 cache,这是 update-cache.sh 的职责。它同样使用了 rsync。

200G 的缓存共缓存了 3696 个文件(其中 iso 文件占 422 个),根据前7天的 HTTP 200 日志,最多的在7天内被访问了 1036142 次,最少的在7天内被访问了 97 次。

SSD cache 的效果怎么样呢?200G 的 cache 同步完成后,

  • 磁盘阵列(sdh)的负载从一直 90%+,降到了时而 90%+,时而 50%~60%
  • 总流量增加了约 50Mbps(原来是 ~350 Mbps,后来是 ~400 Mbps)
  • load average 似乎没有明显变化
  • 被缓存文件的校内下载速度可达 50~70 MB/s,能够吃满 mirrors 的千兆网卡

希望 status.lug.ustc.edu.cn 和 mirrors 的 collectd-web 尽快恢复,以便对 SSD 的效果做更多更直观的观察和评估。