MySQL 复制心跳

在 MySQL 主从复制时,有时候会碰到这样的故障:在 Slave 上 Slave_IO_Running 和 Slave_SQL_Running 都是 Yes,Slave_SQL_Running_State 显示 Slave has read all relay log; waiting for the slave I/O thread to update it ,看起来状态都正常,但实际却滞后于主,Master_Log_File 和 Read_Master_Log_Pos 也不是实际主上最新的位置。一种可能是 Master 上的 binlog dump 线程挂了。但有时候,在 Master 上检查也是完全正常的。那 Slave 的延误又是怎么造成的呢?

在 MySQL 的复制协议里,由 Slave 发送一个 COM_BINLOG_DUMP 命令后,就完全由 Master 来推送数据,Master、Slave 之间不再需要交互。如果 Master 没有更新,也就不会有数据流,Slave 就不会收到任何数据包。但是如果由于某种原因造成 Master 无法把数据发送到 Slave ,比如发生过网络故障或其他原因导致 Master 上的 TCP 连接丢失,由于 TCP 协议的特性,Slave 没有机会得到通知,所以也没法知道收不到数据是因为 Master 本来就没有更新呢还是由于出了故障。

好在 MySQL 5.5 开始增加了一个复制心跳的功能。

stop slave;
change master to master_heartbeat_period = 10;
set global slave_net_timeout = 25;
start slave;

就会让 Master 在没有数据的时候,每 10 秒发送一个心跳包。这样 Slave 就能知道 Master 是不是还正常。slave_net_timeout 是设置在多久没收到数据后认为网络超时,之后 Slave 的 IO 线程会重新连接 Master 。结合这两个设置就可以避免由于网络问题导致的复制延误。master_heartbeat_period 单位是秒,可以是个带上小数,如 10.5。最高精度为 1 毫秒。

slave_net_timeout 的默认是 3600,也就是一小时。也就是说,在之前的情况下,Slave 要延误 1 小时后才会尝试重连。而在没有设置 master_heartbeat_period 时,将 slave_net_timeout 设得很短会造成 Master 没有数据更新时频繁重连。

很奇怪的是,当前的 master_heartbeat_period 值无法通过 show slave status 查看,而要使用 show status like ‘Slave_heartbeat_period’ 查看。此外,状态变量 Slave_last_heartbeat 表示最后一次收到心跳的时间,Slave_received_heartbeats 表示总共收到的心跳次数。

如:

mysql> show status like 'slave%';
+----------------------------+---------------------+
| Variable_name              | Value               |
+----------------------------+---------------------+
| Slave_heartbeat_period     | 5.000               |
| Slave_last_heartbeat       | 2014-05-08 11:48:57 |
| Slave_open_temp_tables     | 0                   |
| Slave_received_heartbeats  | 1645                |
| Slave_retried_transactions | 0                   |
| Slave_running              | ON                  |
+----------------------------+---------------------+
6 rows in set (0.00 sec)

找出进程当前系统调用

当一个程序发生故障时,有时候想通过了解该进程正在执行的系统调用来排查问题。通常可以用 strace 来跟踪。但是当进程已经处于 D 状态(uninterruptible sleep)时,strace 也帮不上忙。这时候可以通过

cat /proc/<PID>/syscall

来获取当前的系统调用以及参数。

这里用最近排查的一个问题为例。碰到的问题是,发现一台服务器在执行 pvcreate 创建物理卷的时候卡死,进程状态为 D

# ps aux|grep pvcreate
root      8443  0.0  0.0  27096  2152 ?        D    Apr04   0:00 pvcreate /dev/sddlmac
...

D 状态实际是在等待系统调用返回。那么来看看究竟在等待什么系统调用

B0313010:~ # cat /proc/8443/syscall
0 0x7 0x70f000 0x1000 0x0 0x7f33e1532e80 0x7f33e1532ed8 0x7fff3a6b8718 0x7f33e128cf00

第一个数字是系统调用号,后面是参数。不同的系统调用所需的参数个数不同。这里的字段数是按最大参数数量来的,所以不一定每个参数字段都有价值。那么怎么知道系统调用号对应哪个系统调用呢?在头文件 /usr/include/asm/unistd_64.h 中都有定义。也可以用个小脚本来快速查找:

#!/bin/bash
# usage: whichsyscall <syscall_nr>
nr="$1"
file="/usr/include/asm/unistd_64.h"
gawk '$1=="#define" && $3=="'$nr'" {sub("^__NR_","",$2);print $2}' "$file"

对于不同的系统调用的参数,可以通过 man 2 <系统调用名> 查阅。如 man 2 read。对刚才那个例子来说,0 就对应了 read 调用。而 read 调用的第一个参数是文件描述符。

之后用 lsof 找到 7 对应的是什么文件

#  lsof -p 8443
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF     NODE NAME
......
pvcreate 8443 root    5u   CHR 10,236      0t0    19499 /dev/mapper/control
pvcreate 8443 root    6u   BLK  253,1   0t8192 36340797 /dev/dm-1
pvcreate 8443 root    7u   BLK  253,5      0t0 35667968 /dev/dm-5

结果发现是个 device mapper 的设备文件。最后顺藤摸瓜,发现这个文件是 multipathd 创建的。而系统应当使用的是存储厂商提供的多路径软件。问题是由于同时开启了 multipathd 造成冲突导致的。

/proc/<PID>/syscall 对排查 D 状态进程很有用。不过在 2.6.18 内核上并不支持,具体从哪个内核版本开始有这个功能,还没查到。不过至少从在 2.6.32 以上版本都是支持的。

VLAN 和策略路由

VLAN(虚拟局域网)通过控制分派出入局域网的数据包到正确的出入端口,实现对不同设备进行逻辑分组管理。一个 VLAN 相当于广播域,它能将广播控制在一个 VLAN 内部。而不同 VLAN 之间或 VLAN 与 LAN / WAN 的数据通信必须通过网络层完成。VLAN 可以降低局域网内大量数据流通时,因无用数据包过多导致雍塞的问题,以及提升局域网的安全性。

对 Linux 来说,通常通过 vconfig 或 ip link add 来配置 vlan,创建虚拟 VLAN 网络接口。如

ip link add link eth0 name vlan100 type vlan id 100
ip link change vlan100 up
ip addr add 10.10.100.201/24 dev vlan100

创建了一个 VLAN 接口 vlan100,对应 VLAN 号是 100,并配置了 IP 10.10.100.201。此时,同一个 VLAN 中的机器相互间用 VLAN 的 IP 都可以访问。但是如果两个机器各自位于不同的 VLAN,则相互之间是无法通过 IP 访问,也无法 ping 通。

Linux 的路由通常情况下是按 IP 包的目的地址查询路由表的。如果 IP 包的来源和目的位于不同的 VLAN,或者一个位于 VLAN ,另一个是普通的链路,就会对应不同的网络接口。响应包根据路由表查询得到的网络接口,由于响应包的目的地址不在 VLAN 内,就不会从 VLAN 的接口返回,通常会路由到默认路由。而默认路由上并没有配置相应IP,因此无法通过该接口发生 IP 包,由此导致无法访问。

既然是由于这个原因,那想要解决就有两种途径。一是允许在这种情况下从不同的接口中发生 IP 包,再通过默认网关转发。可以通过更改系统 Linux 系统配置实现:

echo 1 >/proc/sys/net/ipv4/conf/all/accept_local
echo 2 >/proc/sys/net/ipv4/conf/all/rp_filter

配置后,相互就可以 ping 通了,通过 IP 也可以访问了。

另一种途径就是配置路由规则,让响应包从 VLAN 接口返回,再通过 VLAN 网关转发。也就是策略路由。
首先为 VLAN 定义一个路由表

echo "100 vlan100" >/etc/iproute2/rt_tables

这里 100 是路由表的 ID,vlan100 是路由表的名字。都可以随便起,只要不和现有 ID 和名字重复即可。这儿就用和 VLAN ID、VLAN 接口名相同的数字和名字。

然后为路由表设置默认网关

ip route add default via 10.10.100.1 dev vlan100 table vlan100

这里的 10.10.100.1 就是该 VLAN 的默认网关。dev vlan100 是 VLAN 接口名 table vlan100 是路由表名。

然后为 VLAN 对应的 IP 段添加源地址路由规则,使用刚定义的路由表。

ip rule add from 10.10.100.0/24 table vlan100

这样源地址为 VLAN 100 的包就会根据规则从 VLAN 接口转往 VLAN 的默认网关了。

配置完成后,同样可以实现互通。那这两种方式有什么区别呢?通过抓包看看。在一台不在 VLAN 中的机器持续 ping IP,在 VLAN 的机器上用 tcpdump 抓包。从实际网络接口上抓 ping 的 icmp 包,加上 -e 输出以太网头。
使用系统配置的结果如下:

# tcpdump -e -n icmp -i eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes
10:49:50.640326 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype 802.1Q (0x8100), length 102: vlan 100, p 0, ethertype IPv4, 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 559, length 64
10:49:50.640351 1a:42:cb:eb:bb:e9 > 90:b1:1c:42:9e:18, ethertype IPv4 (0x0800), length 98: 10.10.100.201 > 10.186.21.101: ICMP echo reply, id 20246, seq 559, length 64
10:49:51.641921 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype 802.1Q (0x8100), length 102: vlan 100, p 0, ethertype IPv4, 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 560, length 64
10:49:51.641942 1a:42:cb:eb:bb:e9 > 90:b1:1c:42:9e:18, ethertype IPv4 (0x0800), length 98: 10.10.100.201 > 10.186.21.101: ICMP echo reply, id 20246, seq 560, length 64
10:49:52.643386 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype 802.1Q (0x8100), length 102: vlan 100, p 0, ethertype IPv4, 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 561, length 64

使用策略路由的结果如下:

# tcpdump -e -n icmp -i eth0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes
10:36:13.098805 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype 802.1Q (0x8100), length 102: vlan 100, p 0, ethertype IPv4, 10.186.21.101 > 10.10.100.201: ICMP echo request, id 32277, seq 1695, length 64
10:36:13.098830 1a:42:cb:eb:bb:e9 > 90:b1:1c:42:9e:18, ethertype 802.1Q (0x8100), length 102: vlan 100, p 0, ethertype IPv4, 10.10.100.201 > 10.186.21.101: ICMP echo reply, id 32277, seq 1695, length 64
10:36:14.100301 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype 802.1Q (0x8100), length 102: vlan 100, p 0, ethertype IPv4, 10.186.21.101 > 10.10.100.201: ICMP echo request, id 32277, seq 1696, length 64
10:36:14.100323 1a:42:cb:eb:bb:e9 > 90:b1:1c:42:9e:18, ethertype 802.1Q (0x8100), length 102: vlan 100, p 0, ethertype IPv4, 10.10.100.201 > 10.186.21.101: ICMP echo reply, id 32277, seq 1696, length 64

可以看出,使用系统配置的方式由于没有从 VLAN 接口发出包,因此发出的包虽然 IP 头是对的,但是丢失了 VLAN TAG 数据。

以上环境是在 xen 虚拟化环境中实现的。在宿主机上的 VLAN 相应设备上抓包也可以看出差异:
使用系统配置:

# tcpdump -n -e -i xapi0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on xapi0, link-type EN10MB (Ethernet), capture size 65535 bytes
10:55:40.426937 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype IPv4 (0x0800), length 98: 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 908, length 64
10:55:41.428269 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype IPv4 (0x0800), length 98: 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 909, length 64
10:55:42.429593 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype IPv4 (0x0800), length 98: 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 910, length 64
10:55:43.430682 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype IPv4 (0x0800), length 98: 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 910, length 64

使用策略路由:

# tcpdump -n -e -i xapi0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on xapi0, link-type EN10MB (Ethernet), capture size 65535 bytes
10:55:09.392195 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype IPv4 (0x0800), length 98: 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 877, length 64
10:55:09.392586 1a:42:cb:eb:bb:e9 > 90:b1:1c:42:9e:18, ethertype IPv4 (0x0800), length 98: 10.10.100.201 > 10.186.21.101: ICMP echo reply, id 20246, seq 877, length 64
10:55:10.393333 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype IPv4 (0x0800), length 98: 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 878, length 64
10:55:10.393646 1a:42:cb:eb:bb:e9 > 90:b1:1c:42:9e:18, ethertype IPv4 (0x0800), length 98: 10.10.100.201 > 10.186.21.101: ICMP echo reply, id 20246, seq 878, length 64
10:55:10.394210 90:b1:1c:42:9e:18 > 1a:42:cb:eb:bb:e9, ethertype IPv4 (0x0800), length 98: 10.186.21.101 > 10.10.100.201: ICMP echo request, id 20246, seq 879, length 64
10:55:10.394433 1a:42:cb:eb:bb:e9 > 90:b1:1c:42:9e:18, ethertype IPv4 (0x0800), length 98: 10.10.100.201 > 10.186.21.101: ICMP echo reply, id 20246, seq 879, length 64

可以看到,只有请求的包通过了 VLAN,而响应包并没有出现在 VLAN 中。

使用系统配置方式的副作用是,造成了实际上返回包并不属于该 VLAN。这样的话,在更严格的网络配置下可能无法使用,同时也失去了 VLAN 在流量隔离、流量监控、流量限制方面的意义。因此,正确的方式还是要通过策略路由实现。

某些 linux 版本中 netstat 的 bug

之前在 SLES 11 SP2 上观察到 netstat -lntp 时,PID 一列有部分进程显示为 – 。

# netstat -lntp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:42659           0.0.0.0:*               LISTEN      32555/rpc.statd
tcp        0      0 0.0.0.0:4996            0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:4998            0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:4999            0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      2095/rpcbind
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      2445/sshd
tcp        0      0 :::111                  :::*                    LISTEN      2095/rpcbind
tcp        0      0 :::9010                 :::*                    LISTEN      26369/java
tcp        0      0 :::22                   :::*                    LISTEN      2445/sshd
tcp        0      0 ::1:631                 :::*                    LISTEN      2457/cupsd
tcp        0      0 ::1:25                  :::*                    LISTEN      2559/master
tcp        0      0 :::57626                :::*                    LISTEN      32555/rpc.statd

因为是以 root 用户运行的,显然与权限无关。经过 google,发现这是 net-tools 包的一个 bug。当Socket id 大于 2^31 时,会造成无法显示进程信息。这个 bug 不仅仅存在于 SUSE。如:

https://bugs.launchpad.net/ubuntu/+source/net-tools/+bug/1015026

http://lists.alioth.debian.org/pipermail/pkg-net-tools-maintainers/2009-March/000075.html

如果无法通过升级解决,还可以用一个替代命令来查看哪些端口被那个进程使用。

# lsof -n -P -i TCP | gawk '$10=="(LISTEN)"'
rpcbind    2095    root    8u  IPv4     4766      0t0  TCP *:111 (LISTEN)
rpcbind    2095    root   11u  IPv6     4771      0t0  TCP *:111 (LISTEN)
mysqld     2168 demo3ad   13u  IPv4 13203151      0t0  TCP *:4996 (LISTEN)
sshd       2445    root    3u  IPv4     4868      0t0  TCP *:22 (LISTEN)
sshd       2445    root    4u  IPv6     4870      0t0  TCP *:22 (LISTEN)
cupsd      2457    root    1u  IPv6     6482      0t0  TCP [::1]:631 (LISTEN)
master     2559    root   12u  IPv6     7254      0t0  TCP [::1]:25 (LISTEN)
mysqld    12060 demo1ad   13u  IPv4  6956029      0t0  TCP *:4999 (LISTEN)
mysqld    23358 test1ad   13u  IPv4  6967097      0t0  TCP *:4998 (LISTEN)
java      26369    root   55u  IPv6  4321975      0t0  TCP *:9010 (LISTEN)
rpc.statd 32555    root    9u  IPv4   339698      0t0  TCP *:42659 (LISTEN)
rpc.statd 32555    root   11u  IPv6   339706      0t0  TCP *:57626 (LISTEN)

如果需要增加过滤条件,也可以直接在 gawk 中进行如

# lsof -n -P -i TCP | gawk '$1=="mysqld" && $10=="(LISTEN)"'

清理未正确删除的逻辑卷

最近在一个项目中用到了光存储设备,服务器使用多路径软件实现高可用和负载均衡,并在路径合并后的块设备上使用 LVM 来管理。

有一回因为存储设备上的逻辑单元(LU)先删除了,服务器上的逻辑卷就无法再用 lvremove 之类命令删除了,就连 lvs 之类命令也会卡死。于是就直接删除了那些逻辑卷对应的块设备文件。结果是,用 lvs、vgs、pvs 等命令都已经无法看到逻辑卷,似乎是正常了,但是多路径软件却无法删除设备,认为该设备仍然在使用中。说明这样的删除操作是不对的,还留下了些不干净的东西。但是用 LVM 相关命令已经找不到任何东西了。想到 LVM 是通过 device mapper 实现的,那么用 device mapper 相关的命令也许能看出点啥。

于是试试 dmsetup ls ,果然看到了之前那个没删干净的逻辑卷

# dmsetup ls
lv_demo1dat       (253, 8)
...
...

用 dmsetup remove lv_demo1dat 把它干掉,之后就一切正常了。

禁用 MYISAM 引擎的 MySQL 插件

在无法控制最终用户使用 MySQL 时,就可能出现混合使用 InnoDB 和 MYISAM 的情况。这时就需要考虑为两种引擎各自配置缓存等资源,即使最后实际只会用 InnoDB 的时候也是如此,这就造成了无法充分利用资源,也增加了资源限制的难度。同时由于 MYISAM 引擎的稳定性问题,为了减少运维工作,通常也不希望用户去使用 MYISAM 引擎。这时候就想禁用掉它。但是 MYISAM 引擎被 MySQL 系统表使用,还可能用于内部临时表,所以不可能直接禁用,因此 mysqld 也没有 –skip-myisam 这样的参数。所以只能想别的办法。比如,只禁止创建新的 MYISAM 表,对于原有的表则没有影响。于是就做了这么一个插件

https://github.com/xiezhenye/mysql-plugin-disable-myisam

其实这个插件很简单,只是替换了 MYISAM 引擎的 create 入口,改为返回一个包装过的,替换了 create 方法的 ha_myisam 对象。在试图创建表的时候直接错误返回。

按装插件后,就无法再创建任何新的 MYISAM 表了

mysql> CREATE TABLE `test4` (
  `id` int(11) AUTO_INCREMENT,
  `value` varchar(30),
  PRIMARY KEY (`id`)
) ENGINE=MyISAM;
ERROR 1030 (HY000): Got error 1 from storage engine

用 cgruops 管理进程内存占用

cgroups 中有个 memory 子系统,用于限制和报告进程的内存使用情况。

其中,很明显有两组对应的文件,一组带 memsw ,另一组不带

memory.failcnt
memory.limit_in_bytes
memory.max_usage_in_bytes
memory.usage_in_bytes

memory.memsw.failcnt
memory.memsw.limit_in_bytes
memory.memsw.max_usage_in_bytes
memory.memsw.usage_in_bytes

带 memsw 的表示虚拟内存,即物理内存加交换区。不带 memsw 的那组仅包括物理内存。其中,limit_in_bytes 是用来限制内存使用的,其他的则是统计报告。

# echo 10485760 >/sys/fs/cgroup/memory/foo/memory.limit_in_bytes

即可限制该组中的进程使用的物理内存总量不超过 10MB。对 memory.memsw.limit_in_bytes 来说,则是限制虚拟内存使用。memory.memsw.limit_in_bytes 必须大于或等于 memory.limit_in_byte。这些值还可以用更方便的 100M,20G 这样的形式来设置。要解除限制,就把这个值设为 -1 即可。

这种方式限制进程内存占用会有个风险。当进程试图占用的内存超过限制,访问内存时发生缺页,又没有足够的非活动内存页可以换出时会触发 oom ,导致进程直接被杀,从而造成可用性问题。即使关闭控制组的 oom killer,进程在内存不足的时候,虽然不会被杀,但是会长时间进入 D (等待系统调用的不可中断休眠)状态,无法继续执行,导致仍然无法服务。因此,我认为,用 memory.limit_in_bytes 或 memory.memsw.limit_in_bytes 限制进程内存占用仅应当作为一个保险,避免在进程异常时耗尽系统资源。如,预期一组进程最多只会消耗 1G 内存,那么可以设置为 1.4G 。这样在发生内存泄露等异常情况时,可以避免造成更严重问题。

在 memory 子系统中,还有一个 memory.soft_limit_in_bytes 。和 memory.limit_in_bytes 的差异是,这个限制并不会阻止进程使用超过限额的内存,只是在系统内存不足时,会优先回收超过限额的进程占用的内存,使之向限定值靠拢。

前面说控制组的 oom killer 是可以关闭的,就是通过 memory.oom_control 来实现的。cat memory.oom_control 可以看到当前设置以及目前是否触发了 oom 。echo 1 >memory.oom_control 就可以禁用 oom killer。

usage_in_bytes、max_usage_in_bytes、failcnt 则分别对应 当前使用量,最高使用量和发生的缺页次数。

memory 子系统中还有一个很重要的设置是 memory.use_hierarchy 这是个布尔开关,默认为 0。此时不同层次间的资源限制和使用值都是独立的。当设为 1 时,子控制组进程的内存占用也会计入父控制组,并上溯到所有 memory.use_hierarchy = 1 的祖先控制组。这样一来,所有子孙控制组的进程的资源占用都无法超过父控制组设置的资源限制。同时,在整个树中的进程的内存占用达到这个限制时,内存回收也会影响到所有子孙控制组的进程。这个值只有在还没有子控制组时才能设置。之后在其中新建的子控制组默认的 memory.use_hierarchy 也会继承父控制组的设置。

memory.swappiness 则是控制内核使用交换区的倾向的。值的范围是 0 – 100。值越小,越倾向使用物理内存。设为 0 时,只有在物理内存不足时才会使用交换区。默认值是系统全局设置: /proc/sys/vm/swappiness 。

memory.stat 就是内存使用情况报告了。包括当前资源总量、使用量、换页次数、活动页数量等等。

用 cgroups 管理进程磁盘 io

linux 的 cgroups 还可以限制和监控进程的磁盘 io。这个功能通过 blkio 子系统实现。

blkio 子系统里东西很多。不过大部分都是只读的状态报告,可写的参数就只有下面这几个:

blkio.throttle.read_bps_device
blkio.throttle.read_iops_device
blkio.throttle.write_bps_device
blkio.throttle.write_iops_device
blkio.weight
blkio.weight_device

这些都是用来控制进程的磁盘 io 的。很明显地分成两类,其中带“throttle”的,顾名思义就是节流阀,将流量限制在某个值下。而“weight”就是分配 io 的权重。

“throttle”的那四个参数看名字就知道是做什么用的。拿 blkio.throttle.read_bps_device 来限制每秒能读取的字节数。先跑点 io 出来

dd if=/dev/sda of=/dev/null &
[1] 2750 

用 iotop 看看目前的 io

  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
 2750 be/4 root       66.76 M/s    0.00 B/s  0.00 % 68.53 % dd if=/dev/sda of=/dev/null
...

然后修改一下资源限制,把进程加入控制组

echo '8:0   1048576' >/sys/fs/cgroup/blkio/foo/blkio.throttle.read_bps_device
echo 2750 >/sys/fs/cgroup/blkio/foo/tasks

这里的 8:0 就是对应块设备的主设备号和副设备号。可以通过 ls -l 设备文件名查看。如

# ls -l /dev/sda
brw-rw----. 1 root disk 8, 0 Oct 24 11:27 /dev/sda

这里的 8, 0 就是对应的设备号。所以,cgroups 可以对不同的设备做不同的限制。然后来看看效果

  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
 2750 be/4 root      989.17 K/s    0.00 B/s  0.00 % 96.22 % dd if=/dev/sda of=/dev/null
...

可见,进程的每秒读取立马就降到了 1MB 左右。要解除限制,写入如 “8:0 0” 到文件中即可

不过需要注意的是,这种方式对小于采样间隔里产生的大量 io 是没用的。比如,就算在 1s 内产生一个每秒写入 100M 的峰值,也不会因此被限制掉。

再看看 blkio.weight 。blkio 的 throttle 和 weight 方式和 cpu 子系统的 quota 和 shares 有点像,都是一种是绝对限制,另一种是相对限制,并且在不繁忙的时候可以充分利用资源,权重值的范围在 10 – 1000 之间。

测试权重方式要麻烦一点。因为不是绝对限制,所以会受到文件系统缓存的影响。如在虚拟机中测试,要关闭虚机如我用的 VirtualBox 在宿主机上的缓存。如要测试读 io 的效果,先生成两个几个 G 的大文件 /tmp/file_1,/tmp/file_2 ,可以用 dd 搞。然后设置两个权重

# echo 500 >/sys/fs/cgroup/blkio/foo/blkio.weight
# echo 100 >/sys/fs/cgroup/blkio/bar/blkio.weight

测试前清空文件系统缓存,以免干扰测试结果

sync
echo 3 >/proc/sys/vm/drop_caches

在这两个控制组中用 dd 产生 io 测试效果。

# cgexec -g "blkio:foo" dd if=/tmp/file_1 of=/dev/null &
[1] 1838
# cgexec -g "blkio:bar" dd if=/tmp/file_2 of=/dev/null &
[2] 1839

还是用 iotop 看看效果

  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
 1839 be/4 root       48.14 M/s    0.00 B/s  0.00 % 99.21 % dd if=/tmp/file_2 of=/dev/null
 1838 be/4 root      223.59 M/s    0.00 B/s  0.00 % 16.44 % dd if=/tmp/file_1 of=/dev/null

两个进程每秒读的字节数虽然会不断变动,但是大致趋势还是维持在 1:5 左右,和设定的 weight 比例一致。blkio.weight_device 是分设备的。写入时,前面再加上设备号即可。

blkio 子系统里还有很多统计项

blkio.time
​​​各​​​设​​​备​​​的​​​ io 访​​​问​​​时​​​间,单位毫秒
blkio.sectors
换入​​​者​​​或​​​出​​​各​​​设​​​备​​​的​​​扇​​​区​​​数
blkio.io_serviced
各设​​​备​​​中​​​执​​​行​​​的各类型​​​ io 操​​​作​​​数,分read、​​​write、​​​sync、async 和 total​​​
blkio.io_service_bytes
各类型​​​ io ​​​换入​​​者​​​或​​​出​​​各​​​设​​​备​​​​​​的​​​字​​​节​​​数​​​
blkio.io_service_time
各设​​​备​​​中​​​执​​​行​​​的各类型​​​ io 时间,单位微秒​​​
blkio.io_wait_time
各设​​​备​​​中各类型​​​ io 在队列中的 等待时间​​​
blkio.io_merged
各设​​​备​​​中各类型​​​ io 请求合并的次数​​​
blkio.io_queued
各设​​​备​​​中各类型​​​ io 请求当前在队列中的数量​​​

通过这些统计项更好地统计、监控进程的 io 情况

echo 1 >blkio.reset_stats

可以将所有统计项清零。

用 cgroups 管理 cpu 资源

这回说说怎样通过 cgroups 来管理 cpu 资源。先说控制进程的 cpu 使用。在一个机器上运行多个可能消耗大量资源的程序时,我们不希望出现某个程序占据了所有的资源,导致其他程序无法正常运行,或者造成系统假死无法维护。这时候用 cgroups 就可以很好地控制进程的资源占用。这里单说 cpu 资源。

cgroups 里,可以用 cpu.cfs_period_us 和 cpu.cfs_quota_us 来限制该组中的所有进程在单位时间里可以使用的 cpu 时间。这里的 cfs 是完全公平调度器的缩写。cpu.cfs_period_us 就是时间周期,默认为 100000,即百毫秒。cpu.cfs_quota_us 就是在这期间内可使用的 cpu 时间,默认 -1,即无限制。

跑一个耗 cpu 的程序

# echo 'while True: pass'|python &
[1] 1532

top 一下可以看到,这进程占了 100% 的 cpu

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 1532 root      20   0  112m 3684 1708 R 99.6  0.7   0:30.42 python
...

然后就来对这个进程做一下限制。先把 /foo 这个控制组的限制修改一下,然后把进程加入进去。

echo 50000 >/sys/fs/cgroup/cpu/foo/cpu.cfs_quota_us
echo 1532 >/sys/fs/group/cpu/foo/tasks

可见,修改设置只需要写入相应文件,将进程加入 cgroup 也只需将 pid 写入到其中的 tasks 文件即可。这里将 cpu.cfs_quota_us 设为 50000,相对于 cpu.cfs_period_us 的 100000 即 50%。再 top 一下看看效果。

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
 1532 root      20   0  112m 3684 1708 R 50.2  0.7   5:00.31 python
...

可以看到,进程的 cpu 占用已经被成功地限制到了 50% 。这里,测试的虚拟机只有一个核心。在多核情况下,看到的值会不一样。另外,cfs_quota_us 也是可以大于 cfs_period_us 的,这主要是对于多核情况。有 n 个核时,一个控制组中的进程自然最多就能用到 n 倍的 cpu 时间。

这两个值在 cgroups 层次中是有限制的,下层的资源不能超过上层。具体的说,就是下层的 cpu.cfs_period_us 值不能小于上层的值,cpu.cfs_quota_us 值不能大于上层的值。

另外的一组 cpu.rt_period_us、cpu.rt_runtime_us 对应的是实时进程的限制,平时可能不会有机会用到。

在 cpu 子系统中,cpu.stat 就是用前面那种方法做的资源限制的统计了。nr_periods、nr_throttled 就是总共经过的周期,和其中受限制的周期。throttled_time 就是总共被控制组掐掉的 cpu 使用时间。

还有个 cpu.shares, 它也是用来限制 cpu 使用的。但是与 cpu.cfs_quota_us、cpu.cfs_period_us 有挺大区别。cpu.shares 不是限制进程能使用的绝对的 cpu 时间,而是控制各个组之间的配额。比如

/cpu/cpu.shares : 1024
/cpu/foo/cpu.shares : 2048

那么当两个组中的进程都满负荷运行时,/foo 中的进程所能占用的 cpu 就是 / 中的进程的两倍。如果再建一个 /foo/bar 的 cpu.shares 也是 1024,且也有满负荷运行的进程,那 /、/foo、/foo/bar 的 cpu 占用比就是 1:2:1 。前面说的是各自都跑满的情况。如果其他控制组中的进程闲着,那某一个组的进程完全可以用满全部 cpu。可见通常情况下,这种方式在保证公平的情况下能更充分利用资源。

此外,还可以限定进程可以使用哪些 cpu 核心。cpuset 子系统就是处理进程可以使用的 cpu 核心和内存节点,以及其他一些相关配置。这部分的很多配置都和 NUMA 有关。其中 cpuset.cpus、cpuset.mems 就是用来限制进程可以使用的 cpu 核心和内存节点的。这两个参数中 cpu 核心、内存节点都用 id 表示,之间用 “,” 分隔。比如 0,1,2 。也可以用 “-” 表示范围,如 0-3 。两者可以结合起来用。如“0-2,6,7”。在添加进程前,cpuset.cpus、cpuset.mems 必须同时设置,而且必须是兼容的,否则会出错。例如

# echo 0 >/sys/fs/cgroup/cpuset/foo/cpuset.cpus
# echo 0 >/sys/fs/cgroup/cpuset/foo/cpuset.mems

这样, /foo 中的进程只能使用 cpu0 和内存节点0。用

# cat /proc/<pid>/status|grep '_allowed_list'

可以验证效果。

cgroups 除了用来限制资源使用外,还有资源统计的功能。做云计算的计费就可以用到它。有一个 cpuacct 子系统专门用来做 cpu 资源统计。cpuacct.stat 统计了该控制组中进程用户态和内核态的 cpu 使用量,单位是 USER_HZ,也就是 jiffies、cpu 滴答数。每秒的滴答数可以用 getconf CLK_TCK 来获取,通常是 100。将看到的值除以这个值就可以换算成秒。

cpuacct.usage 和 cpuacct.usage_percpu 是该控制组中进程消耗的 cpu 时间,单位是纳秒。后者是分 cpu 统计的。

P.S. 2014-4-22

发现在 SLES 11 sp2、sp3 ,对应内核版本 3.0.13、 3.0.76 中,对 cpu 子系统,将 pid 写入 cgroup.procs 不会实际生效,要写入 tasks 才行。在其他环境中,更高版本或更低版本内核上均未发现。