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

推荐订阅源

量子位
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
The Hacker News
The Hacker News
Martin Fowler
Martin Fowler
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
U
Unit 42
F
Full Disclosure
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Recorded Future
Recorded Future
Security Archives - TechRepublic
Security Archives - TechRepublic
阮一峰的网络日志
阮一峰的网络日志
T
Threatpost
P
Privacy International News Feed
GbyAI
GbyAI
Stack Overflow Blog
Stack Overflow Blog
MongoDB | Blog
MongoDB | Blog
I
Intezer
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
P
Privacy & Cybersecurity Law Blog
A
Arctic Wolf
博客园 - 聂微东
博客园 - 叶小钗
Cisco Talos Blog
Cisco Talos Blog
H
Help Net Security
S
Schneier on Security
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
The Exploit Database - CXSecurity.com
T
Tor Project blog
月光博客
月光博客
NISL@THU
NISL@THU
A
About on SuperTechFans
Spread Privacy
Spread Privacy
Blog — PlanetScale
Blog — PlanetScale
D
DataBreaches.Net
雷峰网
雷峰网
C
CXSECURITY Database RSS Feed - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 【当耐特】
G
Google Developers Blog
W
WeLiveSecurity
P
Palo Alto Networks Blog
The Last Watchdog
The Last Watchdog
K
Kaspersky official blog
博客园 - 司徒正美
L
LINUX DO - 热门话题
小众软件
小众软件

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 如何基于LanguageServerProtocol来编写lint工具 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 谈谈游戏服务器的自动化测试 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器 终于给Silly的定时器增加了取消功能 一次虚拟内存排查经历 游戏服务器分布式数据的一种同步的思路 为silly增加了互斥锁 2022(完) Go语言之闭包篇 一例误用unsafe包引起的内存问题 Go语言之内存篇 初识Go语言 重新抽象图形API 给Lua实现了一个数学库 谈谈跨平台图形API的抽象 寻路和Flocking算法的结合 行为树的一种高效实现 内测过程中Shader出现的问题 彻底解决多国语言 谈谈数据库的选型 再谈Lua热更新(终) 初窥Rust 关于游戏服务器的服务拆分 ECS的初步实现 ECS初探 屏幕空间(SreenSpace)的想象力 一些对辐射度量学的理解 深度缓冲和半透明渲染 Mysql的间隙锁 更新一些GPU相关知识 2020 地形渲染之爬过的坑 Lua5.3 GC源码阅读(5) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 双向链表的三种实现 谈谈随机数的使用 再谈性能优化 2019 Lua中的函数式编程 重构登录逻辑 Unity资源管理(续) 谈谈Unity的资源管理 一次关于Cache的性能分析 历史之2018 DC3算法 移动平台native代码遭遇的坑 从CPU层面谈谈优化 开卷有益(UNIX编程艺术篇) GC竞争问题 通过Mesh投影来实现贴花系统 谈谈我对数据同步的理解 又一个类型提升引起的Bug Lua5.3 GC源码阅读(4) Lua5.3 GC源码阅读(3) Lua5.3 GC源码阅读(2) Lua5.3 GC源码阅读(1) 三角形光栅化时遇到的坑 一次git事故 再见2017 又一个lua调试器 客户端缓存落地方案 Paxos算法 HTTP服务器的特点 一次性能优化经历 关于CPU分支预测 C程序中让两个不同版本的库共存 实现了一个AOI模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
一次并发Bug
重归混沌 · 2020-06-14 · via 重归混沌的BLOG

最近碰到一个bug, 约7天发生一次,历时3周才找到问题并修复。

在找到bug并修复后,我仔细反思了一下。

我发现不仅这个bug是并发bug的一个典型,修复bug的过程也是直击我的弱点。

下面记录一下整个过程。

最近突然想对我的网络框架做一次RPC浸泡测试。

写了一段代码,开了6000个客户端,去并发RPC请求,服务端收到RPC之后会随机10~2500ms 延时之后再返回。 丢到我的VPS上之后,也没在意,过了两天就忘记这回事了。

过了大概一周,我突然想起来我还有一个浸泡测试在跑着呢,就上去看了一下log.

令我意外的是,网络和rpc没有出现bug,定时器出现Bug了。

就这样一边加log等重现,一边看代码,在历时3周后终于靠看代码修掉了。

定时器相关代码大概如下:

--core.lua(run in thread1)
function core.timeout(ms, func)
    local co = cocreate(func)
    local session = silly_timeout(ms)         --silly_timeout 等价于 silly_timer_timeout
    sleep_session_co[session] = co
    sleep_co_session[co] = session
    return session
end

local MSG = {}
function MSG.expire(session, _, _)
    local co = sleep_session_co[session]
    assert(sleep_co_session[co] == session)
    sleep_session_co[session] = nil
    sleep_co_session[co] = nil
    wakeup_co_queue[#wakeup_co_queue + 1] = co
end
//silly_timer.c
uint32_t
silly_timer_timeout(uint32_t expire)
{
    struct node *n = newnode();
    if (unlikely(n == NULL)) {
        silly_log("silly timer alloc node failed\n");
        return -1;
    }
    lock(T);
    n->expire = expire / TIMER_RESOLUTION + T->ticktime;
    assert((int32_t)(n->expire - T->expire) >= 0);
    add_node(T, n);
    unlock(T);
    return n->session;
}

// run thread2
void
silly_timer_update()
{
//检查所有struct node, 是否超时,
//如果超时,则向lua层抛出expire消息,然后将node结点Free掉
//在Lua层收到expire消息后,会调用MSG.expire函数
}

发现bug时,是发现在MSG.expire函数的第二行assert失败了。

这时我就告诉自己,柯南君的名言“排除一切不可能,剩下的再不可能也是真相”。

即然assert失败了,那只有三种可能。

A1. co 为 nil, 这时sleep_co_session[co]为nil
A2. co 不为nil 且sleep_co_session[co] 为nil
A3. co 不为nil 且 sleep_co_session[co]不等于session

我检查了一下代码,发现sleep_session_co 和sleep_co_session都是一起操作的,因此我将A2排除了。

为了进一步排除A1和A3, 我在MSG.expire加了点log,当assert失败时,打印一下session和co的值。

过了几天后确定是A1.

而导致A1产生的可能情况有以下几种可能:

B1. silly_timer_timeout返回了两个相同的session
B2. silly_timer_update 对同一个session返回了2次
B3. silly_timer_update 返回了一个陌生的session

我查了一下rpc client端的log,我发现在相同的时间确实有一次rpc请求失败了,排除了B2.

如果是情况3, 那就代表silly_timer_update时返回的是脏数据,我查看了silly_timer.c中的代码,发现node->session只有在newnode时被赋值,其他情况并没有被改动过。因此我将B3排除了。

现在就剩下B1,出现B1 有两种可能。

C1. session回绕了
C2. 在++session时有并发问题出现了某种竞争

于是,我在session回绕和session每增加1000时,分别打印了一行log。以期望可以在下次重现时,获得一些有用的信息。

再次出现之后,我发现session并没有回绕,而session每增加1000打印的log似乎对我并没有什么帮助。

我开始把矛头对准C2分析。但是我反复检查,以我现有的知识来讲,session都不可能存在并发问题。

线突然断了,我知道一定是我哪里弄错了,但是我思维已经造成了定式。反复思考也没看到哪里有问题。

之后,每天打开电脑第一件事,就是去看一遍之前发生过的log和代码,以便突然有点灵感。

在过了大约7天之后,我突然发现,session每增加1000时的log L1和assert失败时的log L2发生在同一秒,而出错的session比L1中打印出来的session要小。

这也意味在有一个session在timeout之后立即就收到了expire消息。

于是我重新去看了一下silly_timer_timeout函数,我终于发现问题所在了。

在silly_timer_timeout时由于超时时间非常短的情况下。如果有以下步骤,就是必现的。

  1. silly_timer_timeout加入node N1, 在执行完unlock之后,silly_timer_timeout所在的线程T1突然被切换而暂停执行了。
  2. 这时silly_timer_update在线程T2执行了,它检查到N1已经达到了超时标准,于是将其关联的session用expire消息推送到lua线程并将N1释放(free)。
  3. 其他代码调用malloc重新分配到了刚刚释放N1的内存,并写入数据
  4. T1 重新唤醒被执行。silly_timer_timeout返回了错误的session, 但是silly_timer_update却推送了正确的session.

我在unlock之后加入了usleep(5000)之后,不一会,bug就重现了。


我复盘了一下整个bug分析流程,我发现整个思路都没有什么大问题。

惟一有问题的是,我做了一个错误的假设B1。

B1 应该被修改为,返回了一个无效的session(重复的session同样是无效的session)。

之所以我做了错误的B1假设,是因为潜意识告诉我,silly_timer_timeout返回的session不可能问题了。我在一个错误的假设上做了各种推理。

这次经历告诉我,当相关代码出bug时,一定要忘记所有背景知识,仅对出问题的相关代码进假设(一切皆有可能)。

然后再根据相关信息进行逐个排查。

即使如此,打破思维定式,真的很难 ^_^!