6
17
2016
1

Linux 作业控制实践

事情的起因是这样子的。

有一个非常常用的调试工具叫 strace。输出的信息是纯文本,一大片看起来累。在 Vim 里可以给它高亮一下,就好看多了。再加上各种搜索、清理,以及非常赞的 mark.vim 插件,用起来就舒服多了!

然而我并不想每次都让 strace 写到文件里然后再拿 Vim 去读,因为还得记着清理那些文件。如果数据量不大的话,直接通过管道传给 Vim 多好。

于是有了如下 zsh 函数:

(( $+commands[strace] )) && strace () { (command strace "$@" 3>&1 1>&2 2>&3) | vim -R - }

效果是达到了,但是这样子要中断 strace 的话,得去另一个终端里去 kill。按 Ctrl-C 的话,SIGINT 也会被发给 Vim,导致 Vim 显示空白。

所以嘛,得把 Vim 放到一个单独的进程组里,这样就不会在 Ctrl-C 的时候收到 SIGINT 了。但是,Vim 还得用终端啊。

一开始,我用自制的 expect.py 模块,给 Vim 分配了一个新的终端。这样子 Ctrl-C 好用了。然后我发现 Ctrl-Z 不好用了……

Ctrl-Z 还是挺方便的功能,临时需要执行个命令,不用开新的 shell(以及 ssh),直接按一下 Ctrl-Z,完事之后再回来,多好啊!就跟 zsh 的 Alt-q 一样方便好用呢。

于是就想还是不开 pty 了。直接子进程放新组里跑。这样 Vim 在尝试向终端输出时会收到 SIGTTOU 信号,因为它不是前台进程组。找了一下,用 tcsetpgrp 就可以把指定进程组放到前台了。然后发个 SIGCONT 让可能已经停下来了的 Vim 继续。

然后,当 Vim 收到 SIGTSTP 而停止的时候,我的程序该怎么知道呢?搜了一下,原来这种情况下也会收到 SIGCHLD 的!我以前一直以为只有子进程退出才会收到 SIGCHLD 啊……然后是一个关于 SIGCHLD 的坑,之前在 pssh 里看到过的,这次没有及时想到:不给 SIGCHLD 注册信号处理器时是收不到 SIGCHLD 的!不过诡异的是,我的这个程序有时却能够收到——在我使用 strace 跟踪它的时候……

于是,当 Vim 收到 SIGTSTP 时,把我们自己设置成前台进程组,然后给自己发一个 SIGTSTP 也停下来好了。令人意外的是,后台进程在调用 tcsetpgrp 时竟然也会收到 SIGTTOU。不过没关系,忽略掉就好了。

当用户 fg 时,就再把 Vim 设置成前台进程,并给它一个 SIGCONT 让它继续就好了。

最终的成品 vimtrace 在这里我的 zsh 配置是这样子的:

if (( $+commands[vimtrace] )); then
  (( $+commands[strace] )) && alias strace='vimtrace strace'
  (( $+commands[ltrace] )) && alias ltrace='vimtrace ltrace'
else
  (( $+commands[strace] )) && strace () { (command strace "$@" 3>&1 1>&2 2>&3) | vim -R - }
  (( $+commands[ltrace] )) && ltrace () { (command ltrace "$@" 3>&1 1>&2 2>&3) | vim -R - }
fi

后记:

strace 有时候还是会改变进程的行为的。这种时候更适合用 sysdig。Arch 刚刚更新的 sysdig 版本已经修正了崩溃的问题了~不过 Vim 对 sysdig 的输出就不像 strace 那样有好看的语法着色了。

其实我当时用 systemtap 来看信号发送情况更方便一些。不过那个需要内核调试符号,几百M的东西,装起来累啊……

Category: Linux | Tags: linux 终端 Python zsh
6
12
2016
0

SIGHUP, nohup, disown 以及 expect + sudo + bash + ssh

这些东西都和终端消失的时候(比如 ssh 连接中断)有关,但是细节上又各有不同。

nohup,去 coreutils 看源码就知道,是忽略 SIGHUP 然后 exec 相应的命令。信号处理器如果被设置成忽略,那么在 exec 之后依旧是忽略(与设置成用户自定义函数的情况不一样)。man 7 signal 可以看到说明。

disown 这个,我去看 zsh 的源码了。然而只看到 zsh 把进程从它的任务列表里删掉了,根本没提 SIGHUP 的事情。后来看到这个答案才知道原来 shell 也会发 SIGHUP 信号。

当然内核也会发 SIGHUP。查阅 drivers/tty/tty_io.c 可知,内核会给 session leader 及其组发 SIGHUP 和 SIGCONT,也会给前台进程组发,但是不会给后台进程组发 SIGHUP。那个是 shell 发的,所以 disown 之后后台进程就不会被 SIGHUP 干掉了。

所以,前台进程组如果没有被信号杀掉的话,会收到两次 SIGHUP 信号,一次 SIGCONT 信号。而后台进程组只会收到一次 SIGHUP。disown 过的不会收到任何信号。当然那些没死的进程,如果去读写终端,还是会得到 EIO 错误,写的时候还会收到 SIGPIPE 信号。

strace 可以观察到这些过程。

最后一个,出了问题。通过 expect 调用 sudo,然后登录服务器。终端断开时,expect 被 SIGHUP 杀死。sudo 会把用户发的信号传给它的子进程,但是内核发的不传。而 zsh 给 sudo 发信号时会因为权限原因而发送失败。于是后边的 bash 和 ssh 都会收不到 SIGHUP 信号。但是终端消失它们是能感知到的,所以这个出问题的进程树才这么深嘛。ssh 发现终端消失了,它干了什么呢?当然是通知对端终端没啦,然后等回复。对端 sshd 收到消息之后说,「哦哦,我去把 /dev/ptmx 给关掉。」于是 sshd 关掉了三个 /dev/ptmx 中的其中一个。所以这个 sshd 下的 bash 进程并不会得到 EIO 错误,还继续跑着。于是 sshd 还继续等着它跑完。于是这边的 ssh 还在等对端的 sshd 回复。于是这棵进程树就 hang 在这里了……

结论:还是 systemd 好啊,会话关闭时直接干掉这个会话启动的所有进程。需要在会话结束之后依旧运行的,自己用 systemd-run --user --scope xxx 启动就好。留下这些 sudo、ssh、bash 的还好,占用的资源不多。supervisorctl 这种的就囧了,死循环地读 stdin 又读不到东西,浪费一颗 CPU。

Category: Linux | Tags: linux 终端
5
29
2016
7

配置 Postfix 通过外部 SMTP 服务器发邮件

程序要发邮件。

我不想自己去连外部 SMTP 服务器,因为我懒得自己去处理各种错误。我们服务器上很多程序用的是 heirloom mailx 软件的 mail 命令。结果是有些服务器上有好几个甚至几十个 mail 进程卡在那里了。还有不知道通过什么东西发邮件的,偶尔会有邮件没发出来,发出来了的还因为在一个字符的多个字节之间换行再编码导致 mutt 和 Android 的 GMail 程序显示为乱码的。

所以我要用 Postfix,让它来处理这些杂事。之前只把 Postfix 用作普通的 SMTP 服务器过。然而现在目标邮寄地址是腾讯企业邮箱,对由子域名发出的邮件很不友好,老是扔垃圾箱里,连加白名单都没用……于是搜了一下,Postfix 是可以作为客户端登录到 SMTP 服务器来发信的。不过资料比较少,好不容易配置好了,自然要记录一下。

要登录腾讯企业邮箱发信,main.cf 里写上:

relayhost = [smtp.exmail.qq.com]:587

smtp_sasl_auth_enable = yes
smtp_sasl_security_options = noanonymous
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_use_tls = yes
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_sender_dependent_authentication = yes
smtp_generic_maps = hash:/etc/postfix/generic

/etc/postfix/sasl_passwd 里边写用户名和密码,比如 user@example.com 用户的密码是「password」,就这么写:

[smtp.exmail.qq.com]:587 user@example.com:password

/etc/postfix/generic 里配置信封上的发件人(MAIL FROM 命令)的地址重写,不然腾讯不收:

@hostname user@example.com

hostname 就是 Postfix 里那个 myhostname,默认是机器的主机名。这句配置的意思是,本来发件人地址是这个主机名的,全部改写成 user@example.com 这个。

然后生存 hash 数据库,并更改权限(更好的做法当然是创建的时候就弄好权限):

postmap /etc/postfix/sasl_passwd
postmap /etc/postfix/generic
chmod 600 /etc/postfix/sasl_passwd*

就绪,启动或者重新加载 Postfix 就可以了:

systemctl reload postfix
Category: Linux | Tags: 电子邮件 Postfix
5
21
2016
13

当 SSD 坏掉之后

某天,我注意到系统日志中有如下报错:

ata2.00: exception Emask 0x0 SAct 0x80 SErr 0x0 action 0x0
ata2.00: irq_stat 0x40000008
ata2.00: failed command: READ FPDMA QUEUED
ata2.00: cmd 60/18:38:64:ad:96/00:00:00:00:00/40 tag 7 ncq 12288 in
         res 41/40:00:64:ad:96/00:00:00:00:00/00 Emask 0x409 (media error) <F>
ata2.00: status: { DRDY ERR }
ata2.00: error: { UNC }
ata2.00: configured for UDMA/133
sd 1:0:0:0: [sdb] tag#7 UNKNOWN(0x2003) Result: hostbyte=0x00 driverbyte=0x08
sd 1:0:0:0: [sdb] tag#7 Sense Key : 0x3 [current] [descriptor] 
sd 1:0:0:0: [sdb] tag#7 ASC=0x11 ASCQ=0x4 
sd 1:0:0:0: [sdb] tag#7 CDB: opcode=0x28 28 00 00 96 ad 64 00 00 18 00
blk_update_request: I/O error, dev sdb, sector 9874788
bcache: bch_count_io_errors() sdb1: IO error on reading dirty data from cache, recovering
ata2: EH complete

啊,看来我的 SSD 要坏掉了么?!拿 bcache-status 看了一下状态,已经500多个报错了,但是我并没有遇到什么问题。看了一下 SSD 的 SMART 信息,也是500多个报错。这种报错出现已经有几天了,但是似乎影响并不大?SMART 报告也说「状况正常」。那就等有时间再处理好了,先把 bcache 缓存策略从「writeback」改成「writethrough」好了。我当时是这么想的。

出事的 SSD 是笔记本自带的,16G,被我用作缓存了。使用的是 bcache,一年之前我写文章讲过的。没想到刚刚一年,就开始出问题了。

过了一两天之后。我正上班中,同步 MediaWiki 数据时发现自己的机器又连不上了。我起初以为是网络问题,因为我记得很清楚我的机器是开着的。但是过了一会儿它还是不能访问。我 ping 了一下,却发现是通的!登陆到我作为路由器的树莓派上却发现,网络是正常的。能 ping 通笔记本,但是从树莓派上也 ssh 不了,访问 nginx 也没回应。所以笔记本还在线,但是系统出问题了。奇怪的是,访问另一端口上我用 Tornado 写的 Web 程序时却是正常的。

终于等到了下班。回到家里之后就听见笔记本风扇呼呼地响着。赶紧打开查看。htop 显示 journald 正在使用 CPU。于是去看 journald 日志,心里顿时凉了半截:日志只记录到上午,但满是上边那样的 SSD 报错。进一步的检查发现,/ 分区已经被挂载为只读,syslog-ng 日志比 journald 日志记录了更多的报错。火狐已经不知道什么时候挂掉了,并且启动不了。sudo 还是好的,但是运行 zsh 时会段错误。运行 bash 的话,会报一点错,可以看到有些文件已经变成无意义的数据了。bash 能启动,但是很奇怪的是,输入不了字符 e……想起还装了 dash,于是用这个连最基本的补全都没有的 shell,配合还没有挂掉的连着 VPS 的 mosh 上的 IRC 上网友们的帮助,尝试禁用 SSD 缓存。

结果是失败了。关于 bcache 有些资料与我看到的情况并不一致。重启会报一些错,然后我进入了 initramfs 里的 shell,尝试禁用 SSD,依旧失败中,脏数据写不回去,所以禁用不了缓存……不过看到了缓存中脏数据只有几百K。当然单位是什么我就不知道了……

放弃。又一天,我改从救援系统进入。记得有个工具能把普通分区转成 bcache。手机上网找到了,blocks。但是它并不能把 bcache 转回来。好在是开源项目,我读了一下代码,结合别人的说法,结论是:把普通分区转为 bcache,只要把分区缩小,在起始处空出一点空间,然后写入 bcache 的超级块就可以了。于是反过来,只要跳过 bcache 的超级块来读,就可以读到原来的分区数据了。

但是,bcache 超级块有多大呢?不知道。不过之前看到过一个扫数据的脚本,用类似的办法找一下分区数据的位置好了:

for i in {1..20}; do dd if=/dev/sda2 skip=$i | file -; done

file 显示在第8块的地方发现了「LUKS encrypted file」。这就是我要找的文件系统了!

为保险起见,先读读看。使用 losetup 把 /dev/sda2 映射成只读的 loop 设备,但是跳过开头的 8192 字节(dd 的块是 512 字节一个)。然后用 cryptsetup 解密。解密成功了。只读挂载解密之后的 ext4 文件系统。挂载成功了。看了一下内核日志,没有报错,没有警告。chroot 进去,正常,没有段错误,配置文件没有变成无意义的乱码。一切进行得好顺利!损坏程序比我以为的要轻不少呢!

为了确认损坏情况,我进行了一次模拟备份,也就是 rsync 的「--dry-run」选项。这样与最近的一次备份作比较,看看差异。结果很乐观,rsync 只报了十来个 I/O 错误。不过这也说明文件系统还是有问题的。

又一天。开始恢复系统了。还是从救援系统进入。进入 gdisk,删除原来的 sda2 分区,新建一个小了 8192 字节的分区。经验表明这样子删掉再重建分区表是没有问题的。也确实没出什么问题。不带 bcache 的文件系统出来了。解密之,然后使用 fsck 修复。修复过程中,fsck 问了我一大堆是不是要修复的问题。修复完毕之后,更改内核选项以便适应新的分区表,重启。

一切正常。不过根据之前 rsync 和修复过程中提到的文件名,去看了一下那些文件。都是最近更新系统时的一些 Python 文件。有一些文件消失了,另一些文件的权限和类型变得很奇怪。删掉,重装安装对应的软件包。完事了~不过没有了 SSD 缓存,能够感觉到有些操作会慢不少。

这 SSD 也真容易坏啊。刚刚一年呢。而且坏的过程很快,预警期只有几天。现在 SMART 报告已经出现过1000多个错误了,但整体评估还是「磁盘状况正常」……

好在我用的都是自由软件,虽然可能找遍整个互联网都没有个说法,但是还有实现它们的源码可以阅读,可以自己去思考解决方案。当然啦,就算这个 bcache 完全坏掉了,我也不需要太惊慌,因为我有备份嘛,更麻烦,但最多只损失几天的数据,不会突然之间什么都没有了。

Category: Linux | Tags: linux bcache 数据恢复
4
30
2016
11

愛される花 愛されぬ花

很早就听过这首歌,但现在才知道这首歌讲的是什么。很喜欢这首歌的歌词,不过像《ひとり上手》一样,好悲伤啊。

找到正确的对照版歌词并不是那么容易的,因为大陆网站莫名其妙地对日文汉字进行了「简化」。标注读音的就更难得了,所以我专门制作了标注汉字读音的中日对照版歌词,有兴趣的朋友可以看看:愛される花 愛されぬ花.pdf。感谢 farseerfc 进行校对。

我没有在 YouTube 上找到中岛美雪演唱的版本,所以只好用网易云音乐这个歌词并不正确版本啦: http://music.163.com/song?id=624802

泱泱大国,连周边国家的文字都不能写对,我也是醉了……

Category: 未分类 | Tags: 音乐 日语 中国特色
4
29
2016
4

一个系统,两套网络

创建并配置一个新的网络命名空间

公司 VPN 用的网段是 192.168.1.0/24,而我家里的网络,就像大多数人的一样,也是 192.168.1.0/24。网段冲突了。当然啦,隔离得相当好的 lxc 可以解决这个问题。可是它隔离得太好了,我可不想再多维护一套环境。那么,就只隔离网络好了!

正好之前看到 iproute2 套件可以管理网络命名空间。那么,就用它啦。

首先,创建一个名为 vpn 的网络命名空间:

ip netns add vpn

现在如果进去看的话,会发现里边只有一个孤零零的 lo 网络接口,所以里边是一片与世隔绝的黑暗。我没有其它的网络接口给它用。桥接当然是可以尝试的,只是需要更改已有的网络配置。所以还是用另外的方案吧。弄根网线来,把里边和外边连接起来好了。

创建一对 veth 接口。这就是我们的「网线」~

ip link add vpn0 type veth peer name vpn1

一端留外边,另一端移到 vpn 里边去:

ip link set vpn1 netns vpn

接好网线的两端:

ip link set vpn0 up
ip netns exec vpn ip link set vpn1 up

好了,现在两个网络之间可以通信了~当然,只是链路层。为了使用 TCP/IP,我们得分配 IP 地址。在外边的这个就不需要 IP 地址了,因为我要把它接到一个网桥上,网桥自己有 IP 的。

brctl addif br0 vpn0

这里 br0 是我已有的网桥(给 lxc 用的那个)。如果你没有的话,就自己按以下方法弄一个。我用的是 bridge-utils 里的 brctl 命令。iproute2 也可以做的。

brctl addbr br0
ifconfig br0 192.168.57.1
iptables -t nat -A POSTROUTING -s 192.168.57.1/24 -j MASQUERADE

最后的 iptables 命令是做 NAT 啦。当然内核的 IPv4 转发功能还要开启的。

vpn0 这端弄好了,再给 vpn1 分配 IP,并设置相关路由表项:

ip netns exec vpn ip address add dev vpn1 192.168.57.101/24
ip netns exec vpn ip route add default via 192.168.57.1

现在就可以在里边连 VPN 啦~

ip netns exec vpn openvpn ...

当然也可以去里边开个 zsh 用

ip netns exec vpn zsh

不过要注意如果跑 GUI,或者连接 D-Bus 的话,需要手动把相关环境变量移进去。虽然是独立的网络命名空间,但是因为共享了文件系统,所以虽然看不到在监听的 UNIX 套接字,但是连起来还是没有问题的。X 和 D-Bus 都可以良好工作的。

export DISPLAY=:0 LANG=zh_CN.UTF-8 LANGUAGE=zh_CN:zh_TW
export GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx XMODIFIERS=@im=fcitx
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
# 如果在 tmux 里的话
export TMUX=1

再加上 mount 命名空间吧

我这里在里边得用 IP 地址,因为我的 DNS 是 127.0.0.1,在里边当然是用不了的。怎么办呢?我不想改 DNS 服务器地址,因为里外的文件系统是共享的。那就再加上 mount 命名空间吧,这样就可以 bind mount 一个修改过的文件到 /etc/resolv.conf 上了。

其实呢,ip netns 是通过把网络命名空间(/proc/<pid>/ns/net)bind mount 到文件来实现命名空间的持久化的(不然使用这个命名空间的进程都退出,该命名空间就销毁了)。其文件位于 /run/netns 下。对于 mount 命名空间我们可以手工这么做:

mkdir -p /var/run/ns
mount --bind /var/run/ns /var/run/ns
# 命名空间只能 bind mount 到 private 挂载的文件系统上
mount --make-private /var/run/ns
# 随意找个普通文件就行。一般是用空文件;我这样可以省一个文件~
cp /etc/resolv.conf /var/run/ns

然后用 unshare 建立新的 mount 命名空间,并进入之前的 vpn 网络命名空间(当然用 nsenter 进入也是可以的):

unshare --mount=/var/run/ns/resolv.conf ip netns exec vpn zsh

创建了之后就可以用 nsenter 进去玩儿了:

nsenter --mount=/var/run/ns/resolv.conf --net=/var/run/netns/vpn zsh

可以在里边各种 bind mount,不会影响外边的哦:

# 在新的命名空间里边
mount --bind /var/run/ns/resolv.conf /etc/resolv.conf
vim /etc/resolv.conf

组合更多的命名空间

当然也可以组合更多种的命名空间的。我还试过 pid 命名空间,不过 pid 命名空间比较特殊:它在 fork 后才生效,当 init 进程(pid=1 的进程)退出之后所有位于此 pid 命名空间的进程都会被杀死,并且再也进不去了。所以不是很好玩的啦。

创建与进入的命令如下:

unshare --mount-proc -f --pid=/var/run/ns/pid --mount=/var/run/ns/resolv.conf nsenter --net=/var/run/netns/vpn su - lilydjwg
# 进入时用 -t 选项,或者重新 bind mount 文件(不然新进程会在原 pid 命名空间)
nsenter -t 9416 --mount=/var/run/mountns/resolv.conf --net=/var/run/netns/vpn --pid su - lilydjwg

后记

除了可能偶尔连接公司 VPN 会用到这个技术之外,我又给其找到了一个更好的使用场合:Wine QQ!于是本地的 nginx 日志里终于不会再有 /srv/http/cgi/reccom 找不到的提示了,CGI 服务也不会被不必要地启动了。

2016年5月13日更新:自用的脚本放在 Gist 上了。

Category: Linux | Tags: linux 网络 lxc
4
17
2016
6

用 nfqueue + Python 回复 IPv6 DNS 请求

发生了这么一件事:服务器访问某些 URL 时经常会花费好几秒的时间。重现并分析 strace 记录之后,发现是 DNS 的 AAAA 记录的问题。

情况是这样的:CentOS 6 默认启用了 IPv6,于是 glibc 就会同时进行 A 和 AAAA 记录查询。然后呢,上游 DNS 是运营商的,不给力,经常在解析 AAAA 记录时花费几秒甚至十几秒,然后超时或者返回个 SERVFAIL。

不过咱在天朝,也没有 IPv6 网络可用,所以就禁用 IPv6 吧。通过 sysctl 禁用 IPv6 无效。glibc 是通过创建 IPv6 套接字的方式来决定是否进行 AAAA 请求的。查了一下,禁用掉 ipv6 这个内核模块就可以了。可是,rmmod ipv6 报告模块正在使用中。

lsof -nPi | grep -i ipv6

会列出几个正在使用 IPv6 套接字的进程。重启服务,或者重启机器太麻烦了,也不知道会不会有进程会起不来……于是我想起了歪心思:既然是因为 AAAA 记录回应慢,而咱并不使用这个回应,那就及时伪造一个呗。

于是上 nfqueue。DNS 是基于 UDP 的,所以处理起来也挺简单。用的是 netfilterqueue 这个库。DNS 解析用的是 dnslib

因为 53 端口已经被 DNS 服务器占用了,所以想从这个地址发送回应还得用底层的方法。尝试过 scapy,然而包总是发不出去,Wireshark 显示 MAC 地址没有正确填写……实在弄不明白要怎么做,干脆用 RAW 套接字好了。man 7 raw 之后发现挺简单的嘛,因为它直接就支持 UDP 协议,不用自己处理 IP 头。UDP 头还是要自己处理的,8 字节,其中最麻烦的校验和可以填全零=w= 至于解析 nfqueue 那边过来的 IP 包,我只需要源地址就可以了,所以就直接从相应的偏移取了~(其实想想,好像 dnslib 也不需要呢~)

代码如下(Gist 上也放了一份):

#!/usr/bin/env python3

import socket
import struct
import traceback
import subprocess
import time
import signal

import dnslib
from dnslib import DNSRecord
from netfilterqueue import NetfilterQueue

AAAA = dnslib.QTYPE.reverse['AAAA']
udpsock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)
PORT = 53

def handle_packet(pkt):
  s = time.time()
  try:
    ip = pkt.get_payload()
    # 28 = 20B IPv4 header + 8B UDP header
    dns = DNSRecord.parse(ip[28:])
    if dns.q.qtype == AAAA:
      ret = dns.reply()
      src = socket.inet_ntoa(ip[12:16])
      sport = struct.unpack('!H', ip[20:22])[0]
      p = ret.pack()
      # print(ret, p)
      checksum = 0
      p = struct.pack('!HHHH', PORT, sport, len(p) + 8, checksum) + p
      udpsock.sendto(p, (src, sport))
      pkt.drop()
    else:
      pkt.accept()
  except KeyboardInterrupt:
    pkt.accept()
    raise
  except Exception:
    traceback.print_exc()
    pkt.accept()
  e = time.time()
  print('%.3fms' % ((e - s) * 1000))

def main():
  nfqueue = NetfilterQueue()
  nfqueue.bind(1, handle_packet)
  try:
    nfqueue.run()
  except KeyboardInterrupt:
    print()

def quit(signum, sigframe):
  raise KeyboardInterrupt

if __name__ == '__main__':
  signal.signal(signal.SIGTERM, quit)
  signal.signal(signal.SIGQUIT, quit)
  signal.signal(signal.SIGHUP, quit)
  subprocess.check_call(['iptables', '-I', 'INPUT', '-p', 'udp', '-m', 'udp', '--dport', str(PORT), '-j', 'NFQUEUE', '--queue-num', '1'])
  try:
    main()
  finally:
    subprocess.check_call(['iptables', '-D', 'INPUT', '-p', 'udp', '-m', 'udp', '--dport', str(PORT), '-j', 'NFQUEUE', '--queue-num', '1'])

写好之后、准备部署前,我还担心了一下 Python 的执行效率——要是请求太多处理不过来就麻烦了,得搞多进程呢。看了一下,一个包只有 3ms 的处理时间。然后发现 Python 其实也没有那么慢嘛,绝大部分时候不到 1ms 就搞定了~

部署到咱的 DNS 服务器上之后,AAAA 记录回应迅速,再也不会慢了~

调试网络程序,Wireshark 就是好用!

PS: 后来有人告诉我改 gai.conf 也可以。我试了一下,如下设置并没有阻止 glibc 请求 AAAA 记录——它压根就没读这个文件!

precedence ::ffff:0:0/96  100

PPS: 我还发现发送这两个 DNS 请求,glibc 2.12 用了两次 sendto,但是 Arch Linux 上的 glibc 2.23 只用了一次 sendmmsg~所以大家还是尽量升级吧,有好处的呢。

Category: 网络 | Tags: python DNS iptables 中国特色
4
11
2016
15

MongoDB 到底要吃多少内存?

发现一只32G内存的服务器,上边跑了几个 sharding 模式的 mongod,把内存吃到只剩下4G,8G swap 更是丁点不剩。

我见过吃内存的 mongod,可没见过大胃口的 mongod 啊。不过以前我也没怎么见到在这么大内存的机器上跑的 mongod。不过不管如何,把 swap 全吃掉总归是不对的。

于是翻了翻 mongodb 源码,发现出现这种情况还真是机器的配置的问题。代码里有这么一段(在 GitHub 上的位置):

        if (cacheSizeGB == 0) {
            // Since the user didn't provide a cache size, choose a reasonable default value.
            // We want to reserve 1GB for the system and binaries, but it's not bad to
            // leave a fair amount left over for pagecache since that's compressed storage.
            ProcessInfo pi;
            double memSizeMB = pi.getMemSizeMB();
            if (memSizeMB > 0) {
                double cacheMB = (memSizeMB - 1024) * 0.6;
                cacheSizeGB = static_cast<size_t>(cacheMB / 1024);
                if (cacheSizeGB < 1)
                    cacheSizeGB = 1;
            }
        }

大概这就是决定它自己要用多少内存的代码了。先留出1G,然后再留出40%,剩下的能吃就吃!于是,好几只 mongod 开始抢食了!默认vm.swappiness=60的内核看到内存快用完了,于是开始往 swap 挪。结果造成内核挪多少,mongod 吃多少……

这种情况在机器内存少的时候没有出现,大概是因为内存少的时候,mongod 留出的比例比较高,内核就没那么卖力地把数据往 swap 上挪了。而且这次是好几只 mongod 哄抢呢。

Category: 网络 | Tags: 数据库 linux mongodb
3
8
2016
0

ssh 会话复用及用户级的 sleep.target

这里看到 ssh 的 Control master 特性之后,就在~/.ssh/config里启用了这个特性:

ControlPath ~/.ssh/master-%r@%h:%p
ControlMaster auto
ControlPersist yes
Compression yes

会话连接复用,对于以交互操作的使用,很不错的!对低延迟的服务器可能只是少了用户认证过程,但对于连接国外服务器,少了 TCP 握手、SSH 握手与认证等来来回回的过程,连接会快非常多的,尤其是对于常用的服务器,比如 GitHub 之类的,提速非常明显。

然后,问题就来了:系统挂起再恢复之后,大部分连接会 stalled,需要手工断开连接。即使配置了超时,它也不一定及时。于是我想,既然 netctl-auto 之类的服务能够在挂起系统时适当处理,那么我是不是也能写一个用户级的 systemd 服务来处理这件事情呢?

于是我按系统级的配置方法弄好了,结果什么也没有发生……后来才明白,只有系统级的 sleep.target,没有用户级的啊。

在 Arch Linux 官方论坛看到有人这么尝试,让系统级的 systemd 调用用户级的 systemd。配置有点不对,但是想法是非常好的!

于是就有了我现在用的方案:

[Unit]
Description=sleep.target of a systemd user session
Before=sleep.target
StopWhenUnneeded=yes

[Service]
Type=oneshot
User=%I
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%I/bus
RemainAfterExit=yes
ExecStart=/usr/bin/systemctl --user start sleep.target
ExecStop=/usr/bin/systemctl --user stop sleep.target

[Install]
WantedBy=sleep.target

启用(enable)user-sleep@1000.service之后,系统挂起时,就会调用 ID 为 1000 的用户的用户级 systemd,也 reach sleep.target 啦。当然这个用户级的 sleep.target 也得自己写:

[Unit]
Description=Sleep
Documentation=man:systemd.special(7)
DefaultDependencies=no
StopWhenUnneeded=yes

然后就可以让用户级的服务WantedBy=sleep.target啦~

Category: 网络 | Tags: linux ssh systemd
1
10
2016
3

配置 spamassassin 来过滤垃圾邮件

虽然 GMail 的垃圾邮件过滤功能十分强大,误判率很低。但是我另有使用网易的邮件服务的域名邮箱,屡次教它哪些是垃圾邮件却依然不奏效。于是在本地配置一下 spamassassin 好了。

安装。在 Arch Linux 上这样子:

sudo pacman -S spamassassin
sudo sa-update

教学。之前就计划用 spamassassin,所以预先收集了不少垃圾邮件样本。喂给 sa-learn 学习一下:

sa-learn --spam --mbox < ~/.Mail/spam

过滤。编辑 ~/.procmailrc 配置文件,让 procmail 将邮件交给 spamassassin 检查,如果判定为垃圾邮件就放到特定的邮箱中:

:0fw: spamassassin.lock
* < 256000
| /usr/bin/vendor_perl/spamassassin

:0:
* ^X-Spam-Status: Yes
autospam

完成。其实挺简单的。

部分静态文件存储由又拍云存储提供。 | Theme: Aeros 2.0 by TheBuckmaker.com