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

推荐订阅源

S
Security Affairs
S
Schneier on Security
T
Tenable Blog
G
GRAHAM CLULEY
Latest news
Latest news
D
Darknet – Hacking Tools, Hacker News & Cyber Security
A
Arctic Wolf
I
Intezer
Cyberwarzone
Cyberwarzone
T
The Exploit Database - CXSecurity.com
T
Tailwind CSS Blog
K
Kaspersky official blog
Blog — PlanetScale
Blog — PlanetScale
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Threat Research - Cisco Blogs
爱范儿
爱范儿
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
博客园 - 叶小钗
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recent Commits to openclaw:main
Recent Commits to openclaw:main
P
Palo Alto Networks Blog
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 司徒正美
The Cloudflare Blog
Help Net Security
Help Net Security
罗磊的独立博客
博客园 - 聂微东
Jina AI
Jina AI
Project Zero
Project Zero
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
L
LINUX DO - 最新话题
V
V2EX
人人都是产品经理
人人都是产品经理
美团技术团队
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
J
Java Code Geeks
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Security Latest
Security Latest
The Last Watchdog
The Last Watchdog
Stack Overflow Blog
Stack Overflow Blog
雷峰网
雷峰网
S
Securelist
Forbes - Security
Forbes - Security
博客园 - 三生石上(FineUI控件)
Microsoft Azure Blog
Microsoft Azure Blog
P
Privacy International News Feed
宝玉的分享
宝玉的分享
C
CERT Recently Published Vulnerability Notes

卡瓦邦噶!

服务器高性能网络调优 | 卡瓦邦噶! 为何写作 | 卡瓦邦噶! 读《金阁寺》 | 卡瓦邦噶! 雨季又来 | 卡瓦邦噶! 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——《被解救的姜戈》 简洁之道
一个十万行的 trackback
laixintao · 2023-05-22 · via 卡瓦邦噶!

之前写过一个服务发现系统,叫做 prometheus-http-sd,给 Prometheus 查找监控的目标使用。它的原理很简单,每当 Prometheus 请求 prometheus-http-sd 服务的时候,这个服务就执行用户的脚本,拿到 targets,然后返回。定位是作为一个中间系统,对接 Prometheus 和其他的 CMDB。用户通过写一个简单的脚本,可以将任何系统返回的结果,转换成 Prometheus 的 API 格式返回给 Prometheus.

于是就会有一个自然而然的需求:缓存。

如果部署了 100 个 Prometheus 采集端,那么每一个都需要来做服务发现,如果每 60s 请求一次,那么每分钟就有 100 次请求。监控目标的变更没有那么频繁,而且分钟级的不一致是可以接受的(Prometheus 原生的这个 HTTP 做服务发现的模式已经是分钟级不一致了)。所以我们可以把用户脚本的执行结果缓存起来,至少缓存1分钟,拿一次运行结果直接返回给这分钟内的其他请求。(考虑到内部系统一半写的比较差,这样可以节省巨大资源,笑)

以上是背景。这个缓存功能上线以后,我见到了10万行的一个错误 stack traceback:

这是异常日志的一部分,不知道读者是否能发现什么端倪。这个服务是开源的,存在 bug 的版本是这个

首先第一个比较明显的 bug 是,在用户脚本正常执行的时候才设置了缓存过期的时间,在发生异常的时候没有设置 expire 时间。这就导致了,一旦用户的脚本出发了 Exception,这个 Exception 就会进入到缓存中,并且不会过期,导致以后这个脚本永远会命中缓存返回 Exception。

但是为什么我会看到这么奇怪的 stack 呢?

看到这个 Exception,我的第一个想法是有什么地方拿到这个 Exception 之后,又作了修改,然后 set 回去,修改的时候又包上了当前的 stack,所以越包越长。但是在代码中看了好几遍,这个缓存都没有重新设置的地方。然后又用 PDB 去debug,跟着走了一遍代码,一切都是正常的。最后又用 watchpoint 这个库跟踪了一下,结果也是一样,cache 没有被重新修改过,只有第一次触发有 set。

那好吧,然后我又想到,会不会在 raise Exception 的时候,这个 stack 其实就是这样的了呢?又用 pdb 去跑了几遍代码,发现不存在这么深的 stack。(后来想了一下,Python 默认的 frame 最大好像是 1000,如果真是这样的 stack 的话肯定已经 stack overflow 了!)

想到这里已经没了思路,决定回家睡一觉。

第二天一醒,问题就解决了。

现在抛去一些无关的逻辑,最小化一个复现的 Case 是这样:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

import traceback

d = {"e": Exception("foobar")}

def raise1():

    raise d['e']

def raise2():

    raise1()

if __name__ == "__main__":

    for _ in range(3):

        try:

            raise2()

        except Exception as e:

            traceback.print_exception(type(e), e, e.__traceback__)

其中 d 是我们的缓存,上来就已经有了,然后,我们在代码中会 raise 这个 Exception 3 次。和 prometheus-http-sd 中是一样的逻辑。

看似无害,其实是有 bug 的。

它的输出如下:

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

41

42

Traceback (most recent call last):

  File "/private/tmp/1.py", line 17, in <module>

    raise2()

  File "/private/tmp/1.py", line 11, in raise2

    raise1()

  File "/private/tmp/1.py", line 7, in raise1

    raise d['e']

Exception: foobar

Traceback (most recent call last):

  File "/private/tmp/1.py", line 17, in <module>

    raise2()

  File "/private/tmp/1.py", line 11, in raise2

    raise1()

  File "/private/tmp/1.py", line 7, in raise1

    raise d['e']

  File "/private/tmp/1.py", line 17, in <module>

    raise2()

  File "/private/tmp/1.py", line 11, in raise2

    raise1()

  File "/private/tmp/1.py", line 7, in raise1

    raise d['e']

Exception: foobar

Traceback (most recent call last):

  File "/private/tmp/1.py", line 17, in <module>

    raise2()

  File "/private/tmp/1.py", line 11, in raise2

    raise1()

  File "/private/tmp/1.py", line 7, in raise1

    raise d['e']

  File "/private/tmp/1.py", line 17, in <module>

    raise2()

  File "/private/tmp/1.py", line 11, in raise2

    raise1()

  File "/private/tmp/1.py", line 7, in raise1

    raise d['e']

  File "/private/tmp/1.py", line 17, in <module>

    raise2()

  File "/private/tmp/1.py", line 11, in raise2

    raise1()

  File "/private/tmp/1.py", line 7, in raise1

    raise d['e']

Exception: foobar

可以看到,每一次这个 Exception 被 raise,它的 stack 都加深了。

到这里问题就明了了:raise Exception 这个操作本身就会修改这个 Exception。

PEP 3134 中发现,Python3 已经把和一个 Exception 有关的 traceback,局部变量等放到 Exception 对象中了。这里也说,你不能明确地指定一个 Python Exception 的 traceback。按照我们上面中代码的行为看,就是,Python 在 raise Exception 的时候,会把当前的 stack 写到 traceback 中,但是如果当前已经有 __traceback__ 了的话,就会把之前的 traceback 保留住。(但是没有找到相关资料和代码,欢迎读者补充)

所以,这里每次 raise 一次被缓存下来的 Exception,这个 stacktrace 就会加深一次。

那么问题如何解决呢?我觉得 Exception 不应该被缓存下来,从 Python 的机制可以看出,Exception 被 raise 的时候,应该是 Exception 真正发生的时候。而在我们的这个场景中,Exception 并没有发生,而只是我们返回了之前的一个缓存。缓存 Exception 对用户也有困扰,这个 Exception 看起来就像是:“我运行了你的代码,你的代码出错了”,而实际上用户的代码并没有执行过。所以我觉得这里应该缓存下来一个错误信息,string 就可以了。而不应该缓存 Exception 实例。


小插曲

在做以上实验的时候,我发现用这个代码,就会触发一个奇怪的错误:

Python

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

import traceback

e = Exception("foobar")

def raise1():

    raise e

def raise2():

    raise1()

if __name__ == '__main__':

    for _ in range(3):

        try:

            raise2()

        except Exception as e:

            traceback.print_exception(type(e), e, e.__traceback__)

错误如下:

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

Traceback (most recent call last):

  File "/Users/xintao.lai/Downloads/ex.py", line 15, in <module>

    raise2()

  File "/Users/xintao.lai/Downloads/ex.py", line 10, in raise2

    raise1()

  File "/Users/xintao.lai/Downloads/ex.py", line 6, in raise1

    raise e

Exception: foobar

Traceback (most recent call last):

  File "/Users/xintao.lai/Downloads/ex.py", line 15, in <module>

    raise2()

  File "/Users/xintao.lai/Downloads/ex.py", line 10, in raise2

    raise1()

  File "/Users/xintao.lai/Downloads/ex.py", line 6, in raise1

    raise e

          ^

NameError: name 'e' is not defined

Traceback (most recent call last):

  File "/Users/xintao.lai/Downloads/ex.py", line 15, in <module>

    raise2()

  File "/Users/xintao.lai/Downloads/ex.py", line 10, in raise2

    raise1()

  File "/Users/xintao.lai/Downloads/ex.py", line 6, in raise1

    raise e

          ^

NameError: name 'e' is not defined

说这个 e 没有定义,好奇怪。

然后在文档中找到了答案

原来 except 之后是有一个 clear 动作的。

想了一下,这应该是用来解决 GC 问题的。因为 Python3 把 trackback 放到 Exception 上,实际上是创建了一个环形的引用: err -> traceback -> stack frame -> err,引用计数无法清除,必须要等垃圾回收。而这部分环形的设计的对象又比较多,比如局部变量里面有已经关闭的文件的话,在 gc 之前,可能 file handle 会耗尽。所以这里解释器相当于做引用减 1 ,破掉 stack frame -> err 这个环。(不过感觉应该有办法区分开,当前的 e  不是 local,不要 del 吧?)