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

推荐订阅源

P
Proofpoint News Feed
V
V2EX
博客园_首页
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Recent Announcements
Recent Announcements
博客园 - 司徒正美
Microsoft Security Blog
Microsoft Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Latest news
Latest news
Vercel News
Vercel News
The Register - Security
The Register - Security
T
The Exploit Database - CXSecurity.com
S
Schneier on Security
N
Netflix TechBlog - Medium
WordPress大学
WordPress大学
小众软件
小众软件
L
Lohrmann on Cybersecurity
GbyAI
GbyAI
P
Privacy & Cybersecurity Law Blog
T
Tor Project blog
AWS News Blog
AWS News Blog
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
K
Kaspersky official blog
B
Blog RSS Feed
G
Google Developers Blog
量子位
大猫的无限游戏
大猫的无限游戏
Google DeepMind News
Google DeepMind News
Scott Helme
Scott Helme
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
Intezer
雷峰网
雷峰网
Martin Fowler
Martin Fowler
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Blog — PlanetScale
Blog — PlanetScale
IT之家
IT之家
F
Full Disclosure
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 【当耐特】
The Hacker News
The Hacker News
U
Unit 42
S
SegmentFault 最新的问题
I
InfoQ
aimingoo的专栏
aimingoo的专栏
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
罗磊的独立博客
Spread Privacy
Spread Privacy
C
CERT Recently Published Vulnerability Notes

重归混沌的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 写了个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) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 一次并发Bug 双向链表的三种实现 谈谈随机数的使用 再谈性能优化 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模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
谈谈观测 | 重归混沌的BLOG
重归混沌 · 2025-05-21 · via 重归混沌的BLOG

随着这几年我对 eBPFPrometheus 等工具的深入了解,我才逐渐意识到“可观测性”这个词背后蕴含的意义。

很早以前,我就在 Linux 上使用 /proc/topsar 等工具来排查问题,却从未意识到,“观测”竟然是一门独立的学问。

回顾我早期的编程经历,在设计模块时,我总是本能地选择时间复杂度最低的已知算法。

因为我并不知道这个模块的性能会对最终系统造成多大影响,只是凭着一种“贪心算法”的思维尽可能提升程序性能——也正是这种心态,造就了我后来的“性能强迫症”。

结果就是,代码变得不够简洁,而对整个系统性能的提升却微乎其微,甚至可能根本没有提升。我最近的重构再一次验证了这一点。

并不是我不想评估模块对系统的实际影响,而是现实中的负载情况实在太难模拟。

你测试出来的结果,往往和真实线上环境天差地别。多年来我也请教过很多人,但始终没有找到特别令人满意的答案。

在面对一个有问题的运行环境时,我们可以借助一些分析工具快速定位到热点函数,但这些工具通常都有一个弊端:它们往往会 Stop The World —— 比如 gdbgprof 等。

然而线上问题通常无法容许我们用 Stop The World 的方式进行排查。

这也正是“可观测性”弥足珍贵的原因之一:当系统出问题时,我们可以通过系统本身提供的可观测能力,去追踪和理解到底发生了什么。

不得不佩服 Linux 的设计者们,/proc 文件系统的设计在多年以前就已体现出极强的可观测性理念。


我并不想讲怎么样实现可观测性,毕竟我不是专家。

但我想谈谈观测给了我们一个什么样的视角。

想象一下,不管在任何工厂或车间,总会有各种各样的仪表盘。

一方面,说明这些仪表盘设计的非常合理, 另一方面,说明一个复杂的系统仅在宏观上暴露一些参数指标,足够我们排查很多问题。

而会看仪表的人并不需要对整个系统的细枝末节完全理解。

这从侧面也说明了,当我们通过观测来排查问题时,并不需要一上来就去了解整个系统的实现细节,从宏观视角就可以排查很多问题。

这一点很重要,前面铺垫了这么多,都是为了这个观点。

我也是这两年看完《性能之巅》才慢慢领会到的, 来举几个这两年真实发生的例子。


一次线上更新之后,一台机器的负载飙升到了 14.84,整台机器几乎被卡爆。将刚更新的程序进程杀掉之后,负载才明显下降。

运维怀疑是我们最近更新的代码引入了 Bug,要求我们自查代码。这是合理的猜测,也是最常见的原因。

巧的是,我这两年刚好研究完《性能之巅》,一直没什么机会实践,正所谓“拿着锤子到处找钉子”。

我首先用 top 查看了该进程的 CPU 和内存使用率,发现都只有 1.0% 左右,显然负载问题并不在计算上,初步判断是 IO 引起的。

于是我使用 iostat 1 查看磁盘负载,发现 %iowait 高达 75%,但 kB_read/skB_wrtn/s 加起来也才 200 左右。

结合这些数据,我怀疑是硬盘出现了问题。果然,负责该程序的同事自查代码后并未发现明显异常。最终联系云服务商确认,确实是他们的硬盘出了问题。

另一次是在某个凌晨,游戏服务器访问数据库时出现了 i/o timeout 错误。这里的 i/o timeout 是网络连接层的报错。

但如果网络连接迟迟没有返回,理论上也可能是数据库处理被卡住了。

我通过 Prometheus 查找出故障时段的监控数据,发现读取速度仅有 500KB/s 左右,而 IOPS 并无明显变化,但单次 IO 读取的耗时却突然飙升到了 200ms

综合来看,这很可能又是一场硬盘故障。与云服务商确认后,他们也承认是由于存储集群的某种原因触发了自动切换,导致了 IO 抖动。


上面两个案例中,我在分析问题时并没有去看程序的代码,甚至第一个程序的源码我根本没有阅读过。

这正说明,通过一些宏观指标,其实就可以定位出许多问题。不过,这两个案例还没有很好地凸显“视角”这个关键概念。

最近,我尝试对一个 Lua 程序的内存使用进行调优。同样地,我也没有看过这个 Lua 程序的源码。

我们可以假设这个程序只有一个接口 function foo(xx)foo 接收参数并返回计算结果,过程中会产生一堆垃圾对象。

也就是说,在调用 foo 前后各执行一次全量 GC,程序的内存使用应该没有任何变化。

在调优过程中,我发现每次调用 foo 函数之后,手动执行一次 lua_gc(LUA_GCSTEP, 10*1024),可以将程序的内存峰值从 560MB 降到 260MB

根据 Lua 手册中对 LUA_GCSTEP 的说明,10*1024 相当于让 GC 系统分配约 10MB 的对象。

不过我一开始测出的是一个错误的数据:我认为 foo 每次调用都会产生 50MB 的垃圾,其实只有首次调用才会产生这么多。

我原以为程序在每次调用 foo 后都会生成 50MB 的垃圾对象。但根据手册和我之前阅读 lgc.c 的经验,我始终无法理解:为什么在程序初始内存为 210MB(即执行 dofile("main.lua") 后)时,手动调用 lua_gc(LUA_GCSTEP, 10*1024) 居然能把内存峰值降低超过 300MB

根据默认 GC 参数,只要你分配了 100MB 内存,它就会尝试去 Mark200MB 的对象。也就是说,为了 Mark210MB 的初始内存,理论上需要实际分配 105MB

我虚假分配了 10MB,加上 foo 每次调用产生的 50MB,就需要实际再分配大约:105 / (50 + 10) * 50 = 87.5MB的内存,才能完成对所有存活对象的标记。

而根据 Lua增量 GC 实现,只有当对象被全部 Mark 完后,才会进入清理阶段。

因此,在进入清理阶段前,内存峰值至少已经达到了 297.5MB。而我还没有考虑,LUA_GCSTEP 实际上是要在 foo 分配完 50MB 之后才会执行,另外还有 SWEEPCOST 带来的额外内存负担。

按默认 GC 策略,GC 会等到内存分配到 297.5 * 2 = 595MB 时才会启动下一轮 GC,这个计算远远高于我的观查到的数据。

我和两位同学讨论了一下,他们都认为是 Lua 业务逻辑(非 VM 实现)干扰了 GC 行为,让我去分析业务代码。

但我并不这么认为——不管业务逻辑怎么写,在 GC 系统的视角下,它的行为是稳定的,它始终以内存分配量的 2 倍作为标记对象的阈值。

于是我逐步打印了每次调用 foo 后的内存变化,最终发现我原来的测量是错误的。实际上,只有第一次调用 foo 才会分配 50MB,后续每次调用只产生约 0.8MB 的垃圾。

这就合理了:调用 foo 十次产生约 8MB 的垃圾,加上我虚假分配的 10MB,共计约 108MB,就足够触发 GC 标记完 210MB 的内存。此时内存峰值大约为:210 + 0.8 * 10 ≈ 218MB

考虑到 GC 步长、首次 dofile 后仍残留的垃圾、以及 foo 首次调用产生的内存并非全是垃圾等因素,这个数字已经非常合理了,至少他是可解释的。


之所以举这三个例子,是希望引出这样一个宏观的抽象视角:

在一台计算机硬件上,运行着操作系统;在操作系统之上,运行着我们的应用程序;而我们的应用程序在设计上,往往又被进一步划分为多个抽象层。

当我们在排查问题时,可以尝试忽略其他抽象层与具体实现的干扰,只集中在某一个切面(Aspect)来分析问题究竟出在哪一层。

一旦我们确定了问题所在的抽象层,再去深入分析其具体实现细节,这将大大降低问题分析的复杂度,也能帮助我们更系统、更高效地定位和解决问题。