惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recorded Future
Recorded Future
S
Secure Thoughts
D
Docker
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
雷峰网
雷峰网
T
The Blog of Author Tim Ferriss
D
DataBreaches.Net
F
Fortinet All Blogs
月光博客
月光博客
S
Security @ Cisco Blogs
AI
AI
IT之家
IT之家
P
Proofpoint News Feed
Stack Overflow Blog
Stack Overflow Blog
大猫的无限游戏
大猫的无限游戏
N
News and Events Feed by Topic
Hacker News: Ask HN
Hacker News: Ask HN
人人都是产品经理
人人都是产品经理
Recent Announcements
Recent Announcements
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Google Online Security Blog
Google Online Security Blog
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
P
Proofpoint News Feed
Latest news
Latest news
Webroot Blog
Webroot Blog
GbyAI
GbyAI
L
Lohrmann on Cybersecurity
T
Threat Research - Cisco Blogs
S
Schneier on Security
G
Google Developers Blog
N
News | PayPal Newsroom
C
Check Point Blog
T
Tenable Blog
L
LINUX DO - 最新话题
C
CERT Recently Published Vulnerability Notes
T
Troy Hunt's Blog
Know Your Adversary
Know Your Adversary
SecWiki News
SecWiki News
TaoSecurity Blog
TaoSecurity Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
爱范儿
爱范儿
W
WeLiveSecurity
Project Zero
Project Zero
M
MIT News - Artificial intelligence
H
Hacker News: Front Page
Apple Machine Learning Research
Apple Machine Learning Research
V
V2EX
C
Cyber Attacks, Cyber Crime and Cyber Security

卡瓦邦噶!

服务器高性能网络调优 | 卡瓦邦噶! 为何写作 | 卡瓦邦噶! 读《金阁寺》 | 卡瓦邦噶! 雨季又来 | 卡瓦邦噶! MTU Probe 引起的初始延迟 | 卡瓦邦噶! 3.5 秒的固定延迟问题 | 卡瓦邦噶! 学习网络的一点经验 | 卡瓦邦噶! ARP 问题诊断 | 卡瓦邦噶! 网络断断续续…… | 卡瓦邦噶! Piccolo P2P 镜像分发 | 卡瓦邦噶! 一起看电影 | 卡瓦邦噶! 《征服C指针》 | 卡瓦邦噶! 我的姥姥 | 卡瓦邦噶! Python的哲学 Python 3.5的新特性 学校不教的计算机课 垃圾回收(GC)的三种基本方式 在编程中体验纯粹的快乐 从《美丽新世界》谈自由 在快钱实习 迷人的嗓音和迷人的故事——《Sleepyhead》 Python 的十个自然语言处理工具 记一个愚蠢的bug 一年炉石传说的游戏体验 《以撒的结合:重生》网页版图鉴 分清 C++的指针、引用和数组 笑话三则 自由比皇帝更伟大——《悲惨世界》笔记 Git 10 周年访谈:Linus 讲述背后故事 用 0x3f3f3f3f 设定最大int值的优点 Joel给计算机系学生的建议 一个词法分析器的简单实现 怎样才算健康的生活方式 MacVim 配置攻略 学习培训课程的视频效果好吗? 语言的控制 可悲的大多数 CSS样式思维导图 2014年终总结 奥巴马成为首位写程序的美国总统 青岛老城区的下水道好在哪里? 做优秀 UI 的七个建议(第二部分) 选择爱情的骑士 Git简明教程 Java集合总览 读 《1984》 java问答:终极父类(六)——等待/唤醒和接口 Java 问答:终极父类(五)——toString() Hyperlapse快速视频背后的技术细节 平庸之恶 Java程序员须知的七个日志管理工具 苏州 逛书摊随想 关于考试作弊 你的工作不仅仅是编程 推荐在线学习Java的英文资源 关注女性命运——《千禧年三部曲》 用好你的幻灯片——《演说之禅》 使用ReentrantLock和Lambda表达式让同步更纯净 新手学编程,从哪里开始? 程序员都是工程师吗? 不要学习代码,要学会思考 Java 问答:终极父类(四)——hashCode() Java 问答:终极父类(三)——finalize()和 getClass() 程序员职业之路的选择 我是一名摄影家 写给何小树的城市指南 为什么一些语言会比别的快? Java的常见误区与细节 跟朋友在一起玩游戏 死神永生——读《三体》 五种类型的程序员 创业圣经——读《黑客与画家》 纪念加西亚·马尔克斯 Junit中处理异常的另一种方式:catch-exception Java8采用Martin Fowler的方法创建内部DSL Linux HotSopt虚拟机GC线程的CPU占用率 J2EE概念介绍 如何成为一名黑客 Java 问答:终极父类(二)——equals()方法 为什么我喜欢Java Java 问答:终极父类(一)——clone()方法 七个改变世界的Java项目 java中默认类型转换的小问题 传统与创新 欢迎来到互联网 莫言和马尔克斯——读《生死疲劳》 位运算的妙用 读《人为什么活着》 写博客教会我的事情 中国特色操作系统 2013年总结 《永不妥协》影评 恨不相逢未嫁时——《廊桥遗梦》影评 如何优雅地使用PPT 给明年依然年轻的我们 天才与柱子 黑客守则和黑客精神 Looking for Freedom——《被解救的姜戈》 简洁之道
Debug 一个在 uWSGI 下使用 subprocess 卡住的问题
laixintao · 2021-10-22 · via 卡瓦邦噶!

今天花了很长的时间在排查一个诡异的问题,值得记录一下。

本来是想写一个 HTTP 的服务,你告诉这个 HTTP 服务一个 IP 地址和端口,这个 HTTP 服务就可以返回通过 TCP 访问这个 IP 端口的延迟。因为我们每次做 chaos 注入的时候都要测试一下注入延迟成功了没有,有了这个服务,这个测试就可以自动化。

其实很简单,收到请求之后用 TCP ping 几次拿到延迟,然后返回就可以了。

之前测量 TCP 的延迟使用的都是 hping3,Redis 的作者 antirez 写的。然后就想到用这个工具来做测试好了。

搜索了一下发现没有 Python 的 binding,所以打算粗暴一些,直接在 HTTP 服务里面 fork 出来一个进程做测试,然后去处理 stdout,grep 出来延迟的数据。很快就写好了,代码大体如下:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

def hping3(request):

    """

    test latency using hping3

    params in json body:

        dest_ip: str

        dest_port: int

        count: int

    :return: list of floats, each one is a latency, if total count less than ``count``,

        it means some ping get timeout.

    """

    data = request.body

    logger.info(

        "received hping3 request, headers: {}, body: {}".format(request.headers, data)

    )

    data = json.loads(data)

    dest_ip = data.get("dest_ip")

    dest_port = data.get("dest_port")

    count = data.get("count")

    command = "/usr/sbin/hping3 {} -p {} -c {} --syn".format(dest_ip, dest_port, count)

    logger.info("hping3 run command: {}".format(command))

    complete_process = subprocess.run(

        command,

        capture_output=True,

        shell=True,

    )

    returncode = complete_process.returncode

    stdout = complete_process.stdout.decode()

    stderr = complete_process.stderr

    logger.info(

        "result returncode={}, stderr={}, stdout={}".format(returncode, stderr, stdout)

    )

    latency = []

    for line in stdout.split("\n"):

        matched = match_hping_ms.search(line)

        if matched:

            latency.append(float(matched.group(1)))

    return JsonResponse({"latency_ms": latency, "hping3_returncode": returncode})

很简单的一段代码,收到请求,调用命令,返回结果。框架使用的是 Django,在本地测试一切正常,然后发布到 staging, 噩梦开始了……

在 staging 环境中,测试的时候发现,HTTP 请求发过去永远收不到回应,最后会得到一个 504 Gateway Timeout 的结果。去容器(应用运行在一个容器里面)看,发现 hping3 进程一直没有结束,像是卡住了。

一开始有很多错误的怀疑,比如怀疑 hping3 需要 TTY 才能执行,以为 hping3 需要使用绝对路径等…… 但是想想同样的代码在本地可以运行正常,就应该不是这些原因。一个验证就是,我去应用运行的环境中开一个 Python 的 REPL 执行这段代码,是能正常得到结果的。在应用运行的环境直接运行 hping3 命令,也是没有问题的。

然后怀疑 hping3 没有足够的权限来运行,因为 hping3 发送的是 raw TCP/IP 包,需要 CAP_NET_RAW 才能执行。去容器里面检查了一下,发现这个 capability 也是有的。

到这里,其实已经花费了很多时间了,得到的事实有:

  1. 容器里面执行 hping3 是完全没有问题的,权限是足够的
  2. 直接使用 Python3 的 REPL 执行这段代码也是没有问题,代码逻辑是对的

到这里你能猜到问题出在哪里了吗?

其实还有一个运行环境没有提到,就是 uWSGI. 这个 Python 写的服务是作为 WSGI 应用跑在 uWSGI 里面的。不知道和 uWSGI 有没有关系(直觉告诉我是有的,比直觉更厉害的同事也告诉是有关系的)。于是我打算直接使用 python manage.py runserver 在容器里面跑起来试试……

一切正常了。

所以 python 直接跑应用没问题,用 uWSGI 运行就有问题。现在问题锁定在 uWSGI 上面了。为了复现这个问题,我写了一个最小的测试用例。

首先需要一个文件,叫做 pingapp.py

pingapp.py

Python

1

2

3

4

5

6

7

8

vagrant@vagrant:~$ cat pingapp.py

import subprocess

def application(env, start_response):

    start_response('200 OK', [('Content-Type','text/html')])

    p = subprocess.run("hping3 --syn 104.244.42.1 -p 80 -c 3", shell=True, capture_output=True)

    print("return code={}".format(p.returncode))

    return [p.stdout]

然后使用 uWSGI 运行这个程序:uwsgi --http-socket :9090 --wsgi-file pingapp.py --threads=4.

uWSGI 的版本是 2.0.20,Python 的版本是 3.7.5, 但其实这不重要。

最后,访问这个程序,即 curl localhost:9090.

如果是 uWSGI 的问题的话,我们期望这里已经可以复现问题了,即 curl 命令会卡在这里,然后进程( ps -ef )里面出现一个 hping3 的进程,结束不了。

如同……下面这样:

图1 – 卡住的 hping3

但现实是……这个程序一点问题没有,运行地丝般顺滑。

这就见鬼了,直接没了思路。我的应用和这个最小的复现代码根本没什么(太大的)区别啊!我又没有用一些奇奇怪怪的 lib。

后面我实在解决不了了,找了(大佬)同事帮忙,花了很多时间,找到以下事实:

  1. hping3 卡住了,发现 SIGTERM 结束不了它,只能 kill -9
  2. 然后发现 uwsgi 进程也有一样的行为了,只能 kill -9 去杀它
  3. hping3 程序被 uwsgi 正常起起来是没问题的,起来之后运行不了
  4. …… 以及中间奇奇怪怪的现象,就不细说了,其实都不重要

最后虽然也找到了根因,但是走得路太弯了。本文从这里开始,就以事后诸葛亮的视角,看看有了上面的信息,我们怎么从正确的思路一步一步找到问题。

首先,同样的环境,在 shell 里面可以正常执行 hping3 但是 uWSGI 里面却不可以,既然 uWSGI 能正常开 hping3 进程,我们就可以看看这个进程到底卡在了哪里?

通过 strace 可以发现它一直在 poll 4 这个 fd,然后查看这个 fd,发现它是一个正常的 socket,应该就是 ping tcp 端口使用的那个 socket.

然后,我们应该去看一下,正常的 hping3 的 trace 是什么样子的。

strace hping3 104.244.42.1 -p 80 --syn -c 1

可以看到 poll 附近很神奇,下面突然就开始 write 到 stdout 内容了。不知道怎么结束的。

但是往上看,有一段代码获取了当前的 pid,然后给自己发送了 SIGALRM。再往前,发现注册了 SIGALRM 的 handler.

SIGALRM 是什么呢?

SIGALRM is an asynchronous signal. The SIGALRM signal is raised when a time interval specified in a call to the alarm or alarmd function expires.

看起来是用来做异步的。

搜索了一下 hping3代码。发现代码里确实是用这个信号的。

C

1

2

3

4

5

6

7

/* use SIGALRM to send packets like ping do */

Signal(SIGALRM, send_packet);

/* binding */

if (ctrlzbind != BIND_NONE) Signal(SIGTSTP, inc_destparm);

Signal(SIGINT, print_statistics);

Signal(SIGTERM, print_statistics);

那么我们的 uWSGI 下的异常 hping3 是否是因为没有收到这个 SIGALRM 而一直在傻 poll 呢?

在上面的 图1 中,卡住的 hping3 pid 是 4285,我们看下这个进程能否处理信号:

1

2

3

4

5

6

root@vagrant:/home/vagrant# cat /proc/4285/status | grep Sig

SigQ:   5/3571

SigPnd: 0000000000000000

SigBlk: fffffffe7ffbfeff

SigIgn: 0000000000000000

SigCgt: 0000000180086002

这里可以发现 SigBlk 不是全 0 的,说明有 signal 被 block 了。SigBlk 是个 64 个 bit 组成的 bitmask,每一位代表一个 signal 是否被 block,1 是 block,0 是没有 block。从右到左分别表示 1-64 号信号。

举例:

1

2

3

4

5

6

7

8

9

10

11

00000000280b2603 ==> 101000000010110010011000000011

                     | |       | ||  |  ||       |`->  1 = SIGHUP

                     | |       | ||  |  ||       `-->  2 = SIGINT

                     | |       | ||  |  |`----------> 10 = SIGUSR1

                     | |       | ||  |  `-----------> 11 = SIGSEGV

                     | |       | ||  `--------------> 14 = SIGALRM

                     | |       | |`-----------------> 17 = SIGCHLD

                     | |       | `------------------> 18 = SIGCONT

                     | |       `--------------------> 20 = SIGTSTP

                     | `----------------------------> 28 = SIGWINCH

                     `------------------------------> 30 = SIGPWR

可以使用下面这段脚本去 parse 到底哪些信号被 block 了:

Shell

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

sigparse () {

    i=0

    # bits="$(printf "16i 2o %X p" "0x$1" | dc)" # variant for busybox

    bits="$(printf "ibase=16; obase=2; %X\n" "0x$1" | bc)"

    while [ -n "$bits" ] ; do

        i="$(expr "$i" + 1)"

        case "$bits" in

            *1) printf " %s(%s)" "$(kill -l "$i")" "$i" ;;

        esac

        bits="${bits%?}"

    done

}

grep "^Sig...:" "/proc/4285/status" | while read a b ; do

        printf "%s%s\n" "$a" "$(sigparse "$b")"

    done # | fmt -t  # uncomment for pretty-printing

可以看到,hping3 需要的 14 号信号正好被 block 了。

manual 中得知:

A child created via fork(2) inherits a copy of its parent’s signal mask; the signal mask is preserved across execve(2).

所以说,我们正常的 hping3 可以收到 signal,但是 uWSGI 里面 fork 出来的进程就不可以,可能是 hping3 从 uWSGI 里面继承了 signal mask,导致它也去 block 14 这个 signal 了。

我们可以去看下 uWSGI 里面是否也是 block 了这个 signal 的:

果然是的。

代码中发现,里面只有 core_id = 0 的 thread 是处理信号的,其余的 thread block 了所有的信号。

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

void uwsgi_setup_thread_req(long core_id, struct wsgi_request *wsgi_req) {

int i;

sigset_t smask;

pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &i);

pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &i);

pthread_setspecific(uwsgi.tur_key, (void *) wsgi_req);

if (core_id > 0) {

// block all signals on new threads

sigfillset(&smask);

#ifdef UWSGI_DEBUG

sigdelset(&smask, SIGSEGV);

#endif

pthread_sigmask(SIG_BLOCK, &smask, NULL);

这段代码是 core_id 如果大于0,那么 block 所有的信号,如果不大于 0,就处理信号。

所以到现在也就明白我写的那个最小的 case 为什么不能复现了:我使用了默认配置,只有一个 thread,core_id =0,它永远可以处理信号。而生产环境开了 64 个 thread,只有 1/64 的几率能够处理信号从而让应用正确返回。

我们可以重新验证一下,开 2 个 thread,预期是它会有 50% 的几率卡住:

uwsgi --http-socket :9090 --wsgi-file pingapp.py --threads=2

果然是,50% 能得到结果,50% 会卡住。

到这里就真相大白了。至于修改呢,我打算直接用 socket.connect 来测量 tcp 的连接时间。因为 TCP 是 3 次握手的,但是对于客户端来说基本上只花费了一个 RTT 的时间。测试下来,这样得到的时间和 hping3 也是一致的。

另外这个故事告诉我们,uWSGI 下 fork 出来的子进程最好都默认信号不工作,虽然 core_id =0 是可以处理信号的,但是这个作为 uWSGI 本身回应信号的设计就可以。发现 uWSGI 的代码中还有很多别的地方调用了 sigprocmask(2),可能还有其他屏蔽信号的地方。

有一位智者同事曾经跟我说过这么一句话:

Learn some eBPF, xintao!

看来是时候认认真真学一学 eBPF 啦。