Tag Archives: Linux

linux capabilities

在 linux 系统中,很多操作是需要 root 用户权限才能操作的。比如 chown,改变进程 uid,使用 raw socket 等等。要不就得用 sudo 提升权限,如果想让每个用户都能用特权来执行一个程序,配置管理和权限控制就很麻烦了。还有一个办法是使用粘滞位,通过 chmod +s,可以让一个 owner 为 root 的可执行文件再运行时具有 root 权限。在一些发型版中,ping 命令就是这么干的。给 ping 命令加上粘滞位以后,普通用户也可以使用这个命令通过 raw socket 发送 icmp 包了。不过这样一来,这个程序也就无所不能了。万一程序有啥漏洞,就容易造成严重后果。有没有办法只给这个程序开所需要的权限呢?其实是可以的。linux 有一套 capabilities 机制就是用来实现这个。

事实上,linux 本身对权限的检查就是基于 capabilities 的,root 用户有全部的 capabilities,所以啥都能干。如果想给一个可执行文件加上某个 capability,可以用 setcap 命令,如

setcap cap_net_raw=+ep ping

就可以给 ping 命令加上使用 raw socket 的权限。cap_net_raw 是 capability 的名字,后面是 mode,可以有

  • e:表示是否激活该 capability
  • p:是否允许进程设置该 capability
  • i:子进程是否能继承 capabilities

+ 表示启用,- 表示禁用。
这样执行以后,普通用户执行这个 ping 命令,也可以正常玩耍了。而且这个 ping 命令只获得了 raw socket 的权限。
通过 getcap ping 可以查看这个程序所拥有的 capabilities。

实现上,是通过 setxattr 系统调用,为文件加上 security.capability 的扩展属性。

man 7 capabilities 中可以看到所有可用的 capabilities。

man 3 cap_from_text 中可以看到关于 capability mode 的表达式说明。

page cache 造成 java 长时间 gc

最近在升级一个 java 应用时,在刚启动不久的时候发生了长时间的 gc,时间到了数秒,业务访问纷纷超时。

看了下 gc log:

2016-12-06T22:50:44.256+0800: 13.632: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 13631488 bytes, new threshold 1 (max 15)
- age   1:   27212320 bytes,   27212320 total
 13.632: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 4890, predicted base time: 28.66 ms, remaining time: 71.34 ms, target pause time: 100.00 ms]
 13.632: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 89 regions, survivors: 13 regions, predicted young region time: 6880.88 ms]
 13.632: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 89 regions, survivors: 13 regions, old: 0 regions, predicted pause time: 6909.54 ms, target pause time: 100.00 ms]
 22.969: [G1Ergonomics (Heap Sizing) attempt heap expansion, reason: recent GC overhead higher than threshold after GC, recent GC overhead: 65.85 %, threshold: 10.00 %, uncommitted: 0 bytes, calculated expansion amount: 0 bytes (20.00 %)]
, 9.3374979 secs]
...
      [Object Copy (ms):  8821.5  6481.7  8743.0  6477.4  8173.8  6477.4  6477.5  6477.3  6792.5  6477.2  6477.0  6476.7  9331.9  9250.5  6476.0  6476.0  6471.3  7438.9
       Min: 6471.3, Avg: 7211.0, Max: 9331.9, Diff: 2860.6, Sum: 129797.8]
...

这还是一次 young gc,说明 java 自身的堆是够用的,并没有升级为 mix。但是为什么用了这么长时间呢?注意到大部分时间都是花在了 Object Copy 上。内存 copy 哪怕是几个 G,都不可能花那么久。这里必有蹊跷。

搜索到的结果里,有提到内存不足,使用 swap 造成的情况。但是我们的内存是足够的,而且已经禁用了 swap。但是这倒是给了个提示。看了下当时的内存情况,有大量内存在 cache 中。于是想到了一种可能:有大量文件写入,使 page cache 填满了内存。而我们的 java 应用使用了 4G 的堆大小,刚启动的时候,大部分虚拟内存都还没有分配实际的物理内存。而在 java 在 gc 的过程中需要映射物理内存时,需要等待系统将这部分内存释放出来。如果这时候脏页的数量大,就需要等待脏页刷写到磁盘。一涉及 IO,时间就完全没谱了。

然后在测试环境试着重现一下,先用 dd 写几个大文件,直到剩余的空闲内存很少,大部分都在 cache 中,然后再次启动应用,果然重现了。临时处理办法就是在启动应用的时候,如果发现空闲内存不够,就先用 vmtouch 刷一下占用 cache 的文件缓存(通常是日志)。为了更好的避免类似情况发生,就要控制 page cache 里脏页数量,这样就算是需要释放 cache 也不会花多少时间。可以通过 vm.dirty_background_ratiovm.dirty_ratio 内核参数来控制。

一次连接超时问题排查的历程

我们有一个 java 应用,启动的时候要初始化连接池,在连接一堆 sharding 过的 DB 时,经常会有一部分连接超时失败的,集中在一两台后端机器上,但每次失败的后端服务器却又不固定,也并不是每次启动都能遇到。超时时间设为了 50ms,看起来有点短但是对局域网,和压力并不算大的 DB 来说,这个时间已经长得匪夷所思了。后来尝试调大成 100ms,还是有失败的。但是如果启动成功后,却没再记录到过连接超时的情况。

排查网络问题首先是抓包,本来打算看看是不是对端响应慢有啥重传的,结果发现了更神奇的事情:发起 TCP 连接的 SYN 包不够数!也就是说,有几个连接根本连 SYN 包都没发出去过。还发现有一两个连接收到了 DB 服务器的 SYN/ACK 后,居然发了 RST !所有服务器有响应的 SYN/ACK,包括被 RST 的,延迟都不到 0.2ms,速度挺正常的。那些个丢了的 SYN 和被 RST 的是怎么回事呢?

然后再用 strace 套着启动试试。这回也顺带了解了 java 在 linux 上连接超时的实现方式。首先发起一个非阻塞的 connect,然后用 poll 来等待直到超时,如果超时,则把 socket shutdown 了。然而在 strace 的记录里,所有的 connect 系统调用一个不少,只是在 poll 的时候超时了。这倒是可以解释前面抓包里 RST 的原因。在服务端 SYN/ACK 返回的时候,客户端已经超时 shutdown 了 socket,这时候自然就会返回 RST。但是奇怪的是为啥 connect 了却没看到 SYN 包,从被 RST 的现象推断,是从 connect 到发出 SYN 有了延迟。有的延迟发出后,返回的时候超时,于是就被 RST,绝大部分一直延迟到超时都没发出,于是就再也没了。但是为啥从 connect 到发出 SYN 包会有那么大的延迟呢?

于是去看了一下内核 connect 的实现,connect 系统调用对 TCP 来说,就是一路从数据构造 TCP 包、IP 包、Ethernet 包进入网卡的 QDisc。对非阻塞的 connect 来说就到此为止,返回 EINPROGRESS。对阻塞 connect,还会继续等待三次握手完成。进入 QDisc 后就是网卡驱动通过 DMA Engine 来发包了。因为是非阻塞 connect,这一路构造包的过程中没想出可以阻塞的点,于是怀疑是不是在 QDisc 或者 DMA Engine 发生了什么事情,研究实验了很久而无所得。

再一次碰到问题时,又抓了次包。这回凑巧没过滤 ARP 包,于是在结果里看到了一些查询 DB 服务器 MAC 地址的 ARP 请求。突然想起来,如果本地 ARP 缓存没命中的话,ARP 请求也是一个可能会有延迟的点。这个过程是发生在从 IP 包到构造 Ethernet 包的过程中的。于是把所有 ARP 包过滤出来看,真的发现对连接失败的 IP,ARP 请求第一次没有响应,隔 1s 重试以后才成功。就是这 1s 足以让超时时间是 50ms 的连接失败好多回了。

至于为啥 ARP 请求会超时倒不是啥难题。网络上有广播限流,之前也碰到过 ARP、VRRP 包被干的情况。至于启动成功后,应用和后端 DB 一直会有数据来往,ARP 缓存也就不会再被清掉,这也就解释了为啥问题只会出现在启动的时候。搞清楚原因以后,就不担心运行当中会出故障了。至于启动失败,重试就行了,或者 ping 一把后端 IP,产生一次 ARP 缓存就能绕过。彻底解决问题反倒不是个很紧迫的事了。

中间费了这么大劲,最后发现的问题却如此操蛋没技术含量,感觉挺失望的。不过大部分时候总是这样。隔壁老王有句名言,每个匪夷所思的问题背后,都有一个啼笑皆非的原因。

等待一个独立进程退出并获取 exit code

linux 里,对于进程的子进程,父进程可以用 wait、waitpid 来等待结果。但是对于一个独立的进程就不行了。

有时候想监控一个进程,或者在父进程异常退出后想找回子进程状态,就只能另辟蹊径。于是,想了个通过 ptrace 来跟踪进程退出的办法,做了个小程序:

https://github.com/xiezhenye/waitpid/

可以通过 waitpid 来等待一个独立进程退出并获取 exit code。

supervisord 的 fd 泄露问题

线上发现有几个 supervisord 启动的 php 后台进程没有正常工作。上去 strace 了一下,发现卡在了写 stdout 上:

# strace -p 2509
Process 2509 attached - interrupt to quit
write(1, "[2015-05-22 18:48:19]  **************"..., 82

为啥写 stdout 会卡住呢?看了进程 2509 的 fd 1,原来是个管道,那就是管道对面的进程没有读取数据了。于是就查找了一下管道对面是哪个进程。结果却很神奇,管道对面居然也是进程 2509 自己!

# lsof | awk '$8=="124628537" && $9=="pipe"'
php        2509       www    1w     FIFO                0,8       0t0  124628537 pipe
php        2509       www    2w     FIFO                0,8       0t0  124628537 pipe
php        2509       www 1066r     FIFO                0,8       0t0  124628537 pipe
php        2509       www 1067w     FIFO                0,8       0t0  124628537 pipe

这就难怪了,显然 php 自己不会干自己读自己写的事,于是就卡住了。但是这又是怎么造成的呢?看到一个进程自己和自己管道,就想到了这应该是父子进程间用管道 ipc,然后没关干净造成的吧。php 是由 supervisord 启动的,又了解到写往 stdout 的是传给 supervisord 记日志用的,同时发现,这个 php 的父进程已经变成了 1,也就是说,supervisord 曾经挂过。那么这问题很可能就是 supervisord 造成的。那么究竟是怎么形成的,又如何避免呢?

于是就去翻了 supervisors 的代码,终于明白了原因。supervisors 启动子进程的流程是这样的。

  1. 调用 pipe 生成一对管道
  2. fork 生成子进程
    父进程
    1. 关闭管道自己不用的端
    子进程
    1. dup2 将管道的 fd 覆盖掉自己的 0 1 2 即 stdin, stdout, stderr
    2. 关闭 3 ~ minfds 的所有 fd,只保留 0 1 2
    3. 初始化,execve 执行程序替换自身
  3. 这里的 minfds 是通过 supervisord 的启动参数设置的,默认为 1024。在 supervisord 的初始过程中,如果发现环境中的 rlimit 小于设置的值,会试图用 minfd 和 minproc 去设置相应的 rlimit 来满足需求,但是当环境中的 rlimit 大于设置时却没有处理,造成实际使用的 RLIMIT_NOFILE 大于 minfd。

    出问题的 supervisord 并没有设置这个参数,而系统的 rlimit 的默认 max open files 是 10240。pipe 生成的 fd 是 1066,大于 1024,于是就没有被清理掉。原本正常的管道应该是

    php:1 ----------- supervisord: 1066
    

    结果成了

    php:1 ----------- supervisord: 1066
    php:1066 --/
    

    最后在 supervisord 异常退出时,
    原本应该是

    php:1 ----------- XXX
    

    结果成了

    php:1 ----------- XXX
    php:1066 --/
    

    正常情况下,supervisord 挂了,管道也就坏了。php 往坏了的管道写数据就会触发 Broken pipe,默认就也会被干掉,不会造成现在的后果。但是由于有一头没清理掉,管道就还没坏,只是存活两头都是 php 自己,再往里写数据,就由于没人去读而卡死了。

    要解决问题,就必须在启动 supervisord 时设置 `-a/–minfds` 不小于环境中 max open file 值。一个更好的习惯是,让系统的 soft RLIMIT_NOFILE 保持默认的 1024,调大 hard RLIMIT_NOFILE。然后通过 supervisord 的参数来设置需要的 rlimit。这个值也并不是越大越好,在所有进程退出时,和类似 supervisord 启动子进程时都需要遍历关闭 RLIMIT_NOFILE 的所有 fd。值太大会影响系统性能,同时每个进程的 fd 表也会增大,进程数量多的情况下,还是会多费些内存的。

    最后还是去给 supervisord 提交了一个 issue:https://github.com/Supervisor/supervisor/issues/690

设置 linux 命令缓冲模式

默认情况下,*nix 命令的 stdout 和 stdin 如果是在终端中是行缓冲,stderr 则是无缓冲。而这些标准输入输出如果是在管道中或重定向文件则是全缓冲。有时候使用管道处理数据的时候,并不希望管道后面的命令一直阻塞等待前一个的输出填满缓冲区刷新的时候才能处理,而是希望能即时看到数据。

有些命令提供了参数来设置缓冲模式,比如 tcpdump 可以使用 -l 参数来强制设置为行缓冲,awk、grep、sed 等也有这样的参数。但是也还是有很多命令行工具并没有提供这样的功能,这时候可以利用 stdbuf 命令来设置。

比如

stdbuf -oL tcpdump ... | grep ...

就可以把 tcpdump 的 stdout 设置为行缓冲,起到同样的效果。同样可以用 -i, -e 来设置 stdin 和 stderr 的缓冲模式。可以设置为 L 表示行缓冲,0 表示无缓冲,或者设置一个指定的缓冲区大小,如 4K 。这样无论命令是否支持设置缓冲模式都可以解决这些问题了。

获取 MySQL 崩溃时的 core file

对于一般进程,要让进程崩溃时能生成 core file 用于调试,只需要设置 rlimit 的 core file size > 0 即可。比如,用在 ulimit -c unlimited 时启动程序。

对 MySQL 来说,由于 core file 中会包含表空间的数据,所以默认情况下为了安全,mysqld 捕获了 SEGV 等信号,崩溃时并不会生成 core file,需要在 my.cnf 或启动参数中加上 core-file。

但是即使做到了以上两点,在 mysqld crash 时还是可能无法 core dump。还有一些系统参数会影响 core dump。以下脚本可供参考:

echo 2 >/proc/sys/fs/suid_dumpable
chmod 0777 /var/crash
echo /var/crash/core> /proc/sys/kernel/core_pattern
echo 1 >/proc/sys/kernel/core_uses_pid

由于 mysql 通常会以 suid 方式启动,所以需要打开 suid_dumpable 。对于 core_pattern,最好指定一个保证可写的绝对路径。

之后,就可以用 kill -SEGV 让 mysqld 崩溃,测试一下能不能正常产生 core file 了。

不使用 expect 实现自动化 ssh 密码认证

一般来说,自动化通过 ssh 执行操作或者通过 scp 传文件首先得过 ssh 认证这一关。采用公钥认证是最方便安全的方式。但是有时候不得不使用密码认证。而 ssh 默认是直接读写终端来输出提示信息和读入密码的,所以没法直接用 echo password | ssh ... 的方式来认证。expect 是最常用的用于解决这类问题的工具。但是这玩意实在是很不好用,也不能保证一定安装过。

好在 ssh 还是开了一扇窗,让我们可以实现这点。ssh 有个环境变量,SSH_ASKPASS ,设置了这个环境变量,并且当前会话不是终端时,ssh 在认证时会启动这个程序,从这个程序的标准输出来读取密码。这个功能本来是用于图形终端的,所以还要设置另一个环境变量 DISPLAY=’none:0’,让 ssh 不要试图访问 X11 。至于让进程脱离终端,使用 setsid 就可以了。下面这个例子就展示了自动化实现密码认证并执行命令。

echo  'echo BEGIN!; ls /' | setsid env SSH_ASKPASS='/root/pswd.sh' DISPLAY='none:0' ssh root@127.0.0.1 2>&1
Pseudo-terminal will not be allocated because stdin is not a terminal.
BEGIN!
bin
boot
data
dev
etc
home
lib
lib64
lost+found
media
mnt
opt
proc
root
sbin
selinux
srv
sys
tmp
usr
var

例子里的 /root/pswd.sh 只需要简单输出密码,并确保当前用户可执行就可以了。比如

#!/bin/bash
echo 'PASSWORD'

找出进程当前系统调用

当一个程序发生故障时,有时候想通过了解该进程正在执行的系统调用来排查问题。通常可以用 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 在流量隔离、流量监控、流量限制方面的意义。因此,正确的方式还是要通过策略路由实现。