尝试让 cgo 调用绕过线程池

go 语言中,可以通过 cgo 来调用 C 库。但是由于 goroutine 的机制,外部的 C 函数调用可能能够很快返回,也可能执行很长时间。为了 goroutine 调度不被阻塞,就一律对每个 cgo 调用都从线程池中取一个线程来执行,完成后再返回原 goroutine。这样一来,每个 cgo 调用都带来了巨大的额外开销。所以 go 的很多库在实现时,都没有通过包装 C 库,而是选择完全用 go 来实现。这就使得 go 少了大量现有的 C 库可以利用。

读过 go 的代码后发现,要让 cgo 调用不通过线程池调用并不算麻烦,所以就自己修改了一下 cgo 命令。如下面的代码中

package cgo

//
// int add(int a, int b) {
//   int ret = a + b;
//   return ret;
// }
//
import "C"

func CAdd(a, b int) int {
  return int(C.add(C.int(a), C.int(b)))
}

func AsmCAdd(a, b int) int {
  return int(c.add(C.int(a), C.int(b)))
}

C.add 是传统的 cgo 调用方式。c.add 则是修改后不经过线程池的方式。两者可以并存,程序员可以自己判断 C 函数的执行时间,来考虑使用哪种方式。

性能测试代码:

import "testing"

func BenchmarkNormal(b *testing.B) {
  for i:= 0; i < b.N; i++ {
    CAdd(i, i);
  }
}

func BenchmarkDirect(b *testing.B) {
  for i:= 0; i < b.N; i++ {
    AsmCAdd(i, i);
  }
}

测试结果:

testing: warning: no tests to run
PASS
BenchmarkNormal	5000000	      307 ns/op
BenchmarkDirect	50000000	       31.0 ns/op
ok  	cgo	3.437s

可以看出,直接调用的性能大约是传统调用方式的 10 倍。

虽然目前的实现不算很漂亮,但是这玩意给了 go 更强的能力。而且只修改了 cgo 工具,并没有影响 go runtime 和标准库,不破坏兼容性。

代码在此,基于 go 1.4 修改。

https://github.com/xiezhenye/go/tree/directly-cgo/src/cmd/cgo

但是进一步实验发现这样还是有问题。

首先无法通过正常的方式调用 export 出来的 go 函数。这是因为正常的 cgo 调用会先 entersyscall,完成后 exitsyscall。在回调 go 时,会先 exitsyscall,然后 reentersyscall。既然之前跳过了那一步,这里自然就会出错。这点要解决就得改 runtime 了。

然后是个更要命的问题。这种方式下运行的 C 函数是运行在 goroutine 的栈里的。而 goroutine 的栈是由 go runtime 管理的。初始很小,按需扩大。但是 runtime 不知道 C 函数的情况,无法为其扩栈。如果 C 函数使用的栈空间超过了 goroutine 剩余的栈空间可能破坏其他 goroutine 的栈。这一点就基本不可能解决了。因为即使在 cgo 调用前预先扩栈,也不知道究竟需要扩多少才够,也无法对其进行保护,避免越界。

获取 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 了。

找到 mysql 数据库中的不良索引

为了演示,首先建两个包含不良索引的表,并弄点数据。

mysql> show create table test1\G
*************************** 1. row ***************************
       Table: test1
Create Table: CREATE TABLE `test1` (
  `id` int(11) NOT NULL,
  `f1` int(11) DEFAULT NULL,
  `f2` int(11) DEFAULT NULL,
  `f3` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `k1` (`f1`,`id`),
  KEY `k2` (`id`,`f1`),
  KEY `k3` (`f1`),
  KEY `k4` (`f1`,`f3`),
  KEY `k5` (`f1`,`f3`,`f2`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> show create table test2\G
*************************** 1. row ***************************
       Table: test2
Create Table: CREATE TABLE `test2` (
  `id1` int(11) NOT NULL DEFAULT '0',
  `id2` int(11) NOT NULL DEFAULT '0',
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id1`,`id2`),
  KEY `k1` (`b`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
1 row in set (0.00 sec)

mysql> select count(*) from test2 group by b;                                                                                                        
+----------+
| count(*) |
+----------+
|       32 |
|       17 |
+----------+
2 rows in set (0.00 sec)

1. 包含主键的索引
innodb 本身是聚簇表,每个二级索引本身就包含主键,类似 f1, id 的索引实际虽然没有害处,但反映了使用者对 mysql 索引不了解。而类似 id, f1 的是多余索引,会浪费存储空间,并影响数据更新性能。包含主键的索引用这样一句 sql 就能全部找出来。

mysql> select c.*, pk from 
    ->   (select table_schema, table_name, index_name, concat('|', group_concat(column_name order by seq_in_index separator '|'), '|') cols 
    ->     from INFORMATION_SCHEMA.STATISTICS 
    ->     where index_name != 'PRIMARY' and table_schema != 'mysql'
    -> group by table_schema, table_name, index_name) c,
    ->   (select table_schema, table_name, concat('|', group_concat(column_name order by seq_in_index separator '|'), '|') pk 
    ->     from INFORMATION_SCHEMA.STATISTICS 
    ->     where index_name = 'PRIMARY' and table_schema != 'mysql'
    -> group by table_schema, table_name) p  
    -> where c.table_name = p.table_name and c.table_schema = p.table_schema and c.cols like concat('%', pk, '%');
+--------------+------------+------------+---------+------+
| table_schema | table_name | index_name | cols    | pk   |
+--------------+------------+------------+---------+------+
| test         | test1      | k1         | |f1|id| | |id| |
| test         | test1      | k2         | |id|f1| | |id| |
+--------------+------------+------------+---------+------+
2 rows in set (0.04 sec)

2. 重复索引前缀
包含重复前缀的索引,索引能由另一个包含该前缀的索引完全代替,是多余索引。多余的索引会浪费存储空间,并影响数据更新性能。这样的索引同样用一句 sql 可以找出来。

mysql> select c1.table_schema, c1.table_name, c1.index_name,c1.cols,c2.index_name, c2.cols from
    ->   (select table_schema, table_name, index_name, concat('|', group_concat(column_name order by seq_in_index separator '|'), '|') cols 
    ->     from INFORMATION_SCHEMA.STATISTICS 
    ->     where table_schema != 'mysql' and index_name!='PRIMARY'
    -> group by table_schema,table_name,index_name) c1,   
    ->   (select table_schema, table_name,index_name, concat('|', group_concat(column_name order by seq_in_index separator '|'), '|') cols 
    ->     from INFORMATION_SCHEMA.STATISTICS 
    ->     where table_schema != 'mysql' and index_name != 'PRIMARY'
    -> group by table_schema, table_name, index_name) c2 
    -> where c1.table_name = c2.table_name and c1.table_schema = c2.table_schema and c1.cols like concat(c2.cols, '%') and c1.index_name != c2.index_name;
+--------------+------------+------------+------------+------------+---------+
| table_schema | table_name | index_name | cols       | index_name | cols    |
+--------------+------------+------------+------------+------------+---------+
| test         | test1      | k1         | |f1|id|    | k3         | |f1|    |
| test         | test1      | k4         | |f1|f3|    | k3         | |f1|    |
| test         | test1      | k5         | |f1|f3|f2| | k3         | |f1|    |
| test         | test1      | k5         | |f1|f3|f2| | k4         | |f1|f3| |
+--------------+------------+------------+------------+------------+---------+
4 rows in set (0.02 sec)

3. 低区分度索引
这样的索引由于仍然会扫描大量记录,在实际查询时通常会被忽略。但是在某些情况下仍然是有用的。因此需要根据实际情况进一步分析。这里是区分度小于 10% 的索引,可以根据需要调整参数。

mysql> select p.table_schema, p.table_name, c.index_name, c.car, p.car total from
    ->   (select table_schema, table_name, index_name, max(cardinality) car
    ->     from INFORMATION_SCHEMA.STATISTICS
    -> where index_name != 'PRIMARY'
    -> group by table_schema, table_name,index_name) c,
    ->   (select table_schema, table_name, max(cardinality) car
    ->     from INFORMATION_SCHEMA.STATISTICS
    -> where index_name = 'PRIMARY' and table_schema != 'mysql'
    -> group by table_schema,table_name) p
    -> where c.table_name = p.table_name and c.table_schema = p.table_schema and p.car > 0 and c.car / p.car < 0.1;
+--------------+------------+------------+------+-------+
| table_schema | table_name | index_name | car  | total |
+--------------+------------+------------+------+-------+
| test         | test2      | k1         |    4 |    49 |
+--------------+------------+------------+------+-------+
1 row in set (0.04 sec)

4. 复合主键
由于 innodb 是聚簇表,每个二级索引都会包含主键值。复合主键会造成二级索引庞大,而影响二级索引查询性能,并影响更新性能。同样需要根据实际情况进一步分析。

mysql> select table_schema, table_name, group_concat(column_name order by seq_in_index separator ',') cols, max(seq_in_index) len
    ->    from INFORMATION_SCHEMA.STATISTICS
    ->    where index_name = 'PRIMARY' and table_schema != 'mysql'
    ->    group by table_schema, table_name having len>1;
+--------------+------------+-----------------------------------+------+
| table_schema | table_name | cols                              | len  |
+--------------+------------+-----------------------------------+------+
| test         | test2      | id1,id2                           |    2 |
+--------------+------------+-----------------------------------+------+
1 rows in set (0.01 sec)

自定义 Pacemaker OCF 资源

Pacemaker / Corosync 是 Linux 下一组常用的高可用集群系统。Pacemaker 本身已经自带了很多常用应用的管理功能。但是如果要使用 Pacemaker 来管理自己实现的服务或是一些别的没现成的东西可用的服务时,就需要自己实现一个资源了。

Pacemaker 的资源主要有两类,即 LSB 和 OCF。其中 LSB 即 Linux 标准服务,通常就是 /etc/init.d 目录下那些脚本。Pacemaker 可以用这些脚本来启停服务。在 crm ra list lsb 中可以看到。另一类 OCF 实际上是对 LSB 服务的扩展,增加了一些高可用集群管理的功能如故障监控等和更多的元信息。可以通过 crm ra list ocf 看到当前支持的资源。要让 pacemaker 可以很好的对服务进行高可用保障就得实现一个 OCF 资源。

Pacemaker 自带的资源管理程序都在 /usr/lib/ocf/resource.d 下。其中的 heartbeat 目录中就包含了那些自带的常用服务。那些服务的脚本可以作为我们自己实现时候的参考。

每个 OCF 资源是一个可执行文件。通过命令行参数和环境变量来接受来自 Pacemaker 的输入。下面是一个简单的例子,创建了一个名叫 test 的资源。crm ra meta test的结果如下:

crm(live)# ra meta test
ocf:heartbeat:test

A test resource

Parameters (* denotes required, [] the default):

service_path* (string): the path of service script
    This is a required parameter. The path of service script.

probe_url* (string): the url to detect the status of the service
    This is a required parameter. The url to detect the status of the service.

Operations' defaults (advisory minimum):

    start         timeout=60s
    stop          timeout=30s
    status        timeout=30s
    monitor_0     interval=10s timeout=30s

和其他的资源一样,这个 test 资源也会接受若干的参数,以及对 start、stop 等命令有超时默认值等。下面就是这个资源的 bash 实现代码。实际也可以使用任何语言来实现。

#!/bin/bash
# OCF parameters:

#   OCF_RESKEY_service_path    : service path
#   OCF_RESKEY_probe_url    : probe url
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/resource.d/heartbeat}
. ${OCF_FUNCTIONS_DIR}/.ocf-shellfuncs

SERVICE_PATH="${OCF_RESKEY_service_path}"
PROBE_URL="${OCF_RESKEY_probe_url}"
COMMAND=$1
SERVICE_NAME=test

metadata_test() {
    cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="test">
<version>1.0</version>
<longdesc lang="en">
A test resource
</longdesc>
<shortdesc lang="en">test</shortdesc>
<parameters>
    <parameter name="service_path" required="1" unique="1">
        <longdesc lang="en">
        This is a required parameter. The path of service script.
        </longdesc>
        <shortdesc>the path of service script</shortdesc>
        <content type="string" default=""/>
    </parameter>
    <parameter name="probe_url" required="1" unique="1">
        <longdesc lang="en">
        This is a required parameter. The url to detect the status of the service.
        </longdesc>
        <shortdesc>the url to detect the status of the service</shortdesc>
        <content type="string" default=""/>
    </parameter>
</parameters>
<actions>
    <action name="start" timeout="60s" />
    <action name="stop" timeout="30s" />
    <action name="status" timeout="30s" />
    <action name="monitor" depth="0" timeout="30s" interval="10s" />
    <action name="meta-data" timeout="5s" />
    <action name="validate-all"  timeout="5s" />
</actions>
</resource-agent>
END
    return $OCF_SUCCESS
}

monitor_test() {
    return $OCF_SUCCESS
}

start_test() {
    return $OCF_SUCCESS
}

stop_test() {
    return $OCF_SUCCESS
}

status_test() {
    return $OCF_SUCCESS
}

validate_all_test() {
    ocf_log info "validate_all_test"
    if [[ ! -x "$SERVICE_PATH" ]]; then
        ocf_log err "service_path is not found"
        exit $OCF_ERR_CONFIGURED
    fi
    if [[ ! -x "$PROBE_URL" ]]; then
        ocf_log err "probe_url is required"
        exit $OCF_ERR_CONFIGURED
    fi
    return $OCF_SUCCESS
}

run_func() {
    local cmd="$1"
    ocf_log debug "Enter $SERVICE_NAME $cmd"
    $cmd
    local status=$?
    ocf_log debug  "Leave $SERVICE_NAME $cmd $status"
    exit $status
}

case "$COMMAND" in
    meta-data)
        run_func meta-data
        ;;
    start)
        run_func start
        ;;
    stop)
        run_func stop
        ;;
    status)
        run_func status
        ;;
    monitor)
        run_func monitor
        ;;
    validate-all)
        run_func vaidate-all
        ;;
    *)
        exit $OCF_ERR_ARGS
        ;;
esac

首先说说如何接受输入。同标准的 Linux 服务脚本一样,OCF 资源也会以命令名为参数被调用,只是多了几个命令而已。如通过 /usr/lib/ocf/resource.d/heartbeat/test start 来启动服务,/usr/lib/ocf/resource.d/heartbeat/test monitor 来监控服务状态。根据执行的返回码来判断是否成功。具体的返回值含义如下:

OCF_SUCCESS=0
OCF_ERR_GENERIC=1
OCF_ERR_ARGS=2
OCF_ERR_UNIMPLEMENTED=3
OCF_ERR_PERM=4
OCF_ERR_INSTALLED=5
OCF_ERR_CONFIGURED=6
OCF_NOT_RUNNING=7

对于 start、stop 等命令,一定要在服务启动/停止完成后返回。要保证在返回后,通过 monitor 命令检测状态时要能够成功。如果在未完成启动就返回如直接将命令放后台执行,会导致通过 monitor 命令检测状态时,由于此时还未启动完成而失败,导致被认为是故障,从而导致重启或切换。所有命令执行成功时应当返回 OCF_SUCCESS ,即 0。出错时根据具体情况返回对应错误码。如 start 启动失败,monitor 监控到服务故障时应当返回 OCF_NOT_RUNNING 。在检查参数错误时应当返回 OCF_ERR_CONFIGURED。其他错误一般可以返回 OCF_ERR_GENERIC。

参数则是通过环境变量传递。如 test 资源定义的两个参数:service_path 和 probe_url 会分别通过环境变量 $OCF_RESKEY_service_path 和 $OCF_RESKEY_service_path 传递。

meta-data 命令会输出一段 xml。作为资源的元信息,crm ra meta test 的结果也是由此而来的。参见代码中那一大段 xml。每个资源的说明,参数定义,命令定义都由这个 xml 说明。基本上参照例子就能明白。

定义参数的 parameter/content 支持的类型有 string、integer、boolean。default 属性可以定义参数的默认值。需要注意的是,定义资源未指定参数时,指定的默认值并不会出现在 OCF_RESKEY_<参数名> 变量中,而是会放在另外的变量 OCF_RESKEY_<参数名>_default 中。

参考资料:

  • http://www.linux-ha.org/doc/dev-guides/ra-dev-guide.html
  • http://www.linux-ha.org/wiki/LSB_Resource_Agents
  • http://www.linux-ha.org/wiki/OCF_Resource_Agents
  • http://refspecs.linux-foundation.org/LSB_3.2.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
  • http://www.opencf.org/

不使用 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'

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)"'