CPU问题定位与解决方案
问题背景回顾
- 部署方式:uWSGI + Python Web( Flask或者Django)
- 现象:线上某些时刻,某个进程 CPU 飙升到 100%+,但 内存、IO、网络请求并发、句柄数都稳定
- 初步判断:Python 进程自身行为异常,且非规律性触发,无法测试环境重现
排查思路概览
排查步骤 | 工具 | 目的 |
1. 确认高 CPU 的进程 | top / htop / ps | 定位占用 CPU 的进程 PID |
2. 查看进程系统调用行为 | strace | 查看进程是否频繁访问某个资源 |
3. 查看调用栈情况 | perf | 初步尝试获取进程调用栈(适用于原生语言) |
4. Python 专用分析 | py-spy | 获取 Python 层完整调用栈和函数占用 CPU 情况 |
5. 代码分析修复 | —— | 结合火焰图分析源码逻辑问题 |
Python代码重现demo,模拟「国际化翻译文件频繁读取 + 全局列表不断膨胀导致CPU升高」
import threading
import time
# 模拟翻译文件内容
TRANSLATION_FILE = "translations.txt"
# 全局缓存,理论上应该只加载一次
translations_cache = {}
# 出问题的全局列表,错误地不断累加“未找到的key”
missing_keys_list = []
def load_translation_file():
"""模拟从文件读取翻译数据"""
# 这里模拟文件读取开销
time.sleep(0.01)
# 模拟文件内容
return {
"hello": "你好",
"bye": "再见",
}
def get_translation(key):
"""获取翻译,找不到时错误地把key加到全局列表"""
if not translations_cache:
# 仅第一次加载翻译文件
global translations_cache
translations_cache = load_translation_file()
if key in translations_cache:
return translations_cache[key]
else:
# 出错点:每次找不到都append,导致列表不断增长
missing_keys_list.append(key)
# 误以为要重新加载文件,导致频繁IO(模拟)
_ = load_translation_file()
return f"[Missing: {key}]"
def worker_thread():
"""模拟请求线程,反复请求翻译"""
keys = ["hello", "bye", "invalid_key_1", "invalid_key_2"]
while True:
for k in keys:
val = get_translation(k)
# 模拟响应时间
time.sleep(0.001)
if __name__ == "__main__":
print("启动模拟翻译服务,按 Ctrl+C 停止")
threads = []
for _ in range(5): # 模拟5个请求线程
t = threading.Thread(target=worker_thread)
t.daemon = True
t.start()
threads.append(t)
try:
while True:
# 每秒打印状态,观察missing_keys_list长度
print(f"missing keys count: {len(missing_keys_list)}")
time.sleep(1)
except KeyboardInterrupt:
print("退出模拟")
演示说明
- translations_cache 理应只加载一次,但错误代码中每次get_translation找不到 key 时都会调用load_translation_file()模拟重新加载文件。
- missing_keys_list 是个全局累积的列表,随着请求不断追加找不到的 key,内存和CPU负载持续增加。
- 你可以运行这段程序,然后用 top 观察 Python 进程的 CPU 会逐渐升高。
- load_translation_file() 通过 time.sleep(0.01) 模拟文件IO的延迟,放大CPU负载表现。
1、top命令查看cpu
top - 12:27:42 up 115 days, 11:35, 0 users, load average: 1.61, 0.41, 0.31
Tasks: 104 total, 1 running, 103 sleeping, 0 stopped, 0 zombie
%Cpu(s): 47.3 us, 4.1 sy, 0.0 ni, 45.9 id, 0.0 wa, 2.2 hi, 0.5 si, 0.0 st
MiB Mem : 1871.0 total, 857.9 free, 386.5 used, 781.9 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 1484.5 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
148782 ecs-ass+ 20 0 983604 31128 7828 S 100.0 1.6 0:24.19 python3
136866 root 20 0 139236 30232 19672 S 1.3 1.6 193:21.10 AliYunDunMonito
1357 root 20 0 1484692 16064 8776 S 0.7 0.8 753:28.56 argusagent
136845 root 20 0 97752 20372 13056 S 0.7 1.1 110:51.73 AliYunDun
63761 root 20 0 474944 23024 9412 S 0.3 1.2 156:07.86 tuned
分析结果
1. 负载状况
1分钟负载上升到了 1.61(高于系统核心数为1的情况),但5分钟和15分钟均较低,说明是短时间内某个进程突发性地占用了CPU。
2.CPU使用情况
%Cpu(s): 47.3 us, 6.2 sy, 45.9 id
- us(用户态)占比 47.3%,说明是应用层代码耗CPU,不是系统内核问题。
- sy(系统态)也有一定占比,可能有频繁系统调用。
- 还有 45.9% 空闲,系统还没崩,但这个 python3 进程已经吃掉了一个完整核心
3.python3 进程
PID 148782 python3 %CPU 100.0 %MEM 1.6
- 这个进程吃了一个核心(单线程100%)。
- 内存 1.0% 并不高,说明不是内存泄漏,而是CPU密集型任务陷入了死循环或高频率操作(如I/O、字符串、字典操作)。
- 持续时间只有 24 秒:这是刚发生的异常高CPU,属于典型“偶发性线上高CPU问题”
2、top 查看线程详细信息
top -p 148782 -H
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
top - 12:54:10 up 115 days, 12:01, 0 users, load average: 1.83, 1.90, 1.74
Threads: 11 total, 1 running, 10 sleeping, 0 stopped, 0 zombie
%Cpu(s): 48.7 us, 3.0 sy, 0.0 ni, 46.5 id, 0.0 wa, 1.4 hi, 0.3 si, 0.0 st
MiB Mem : 1871.0 total, 837.0 free, 406.8 used, 782.5 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 1464.2 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
148843 ecs-ass+ 20 0 1003176 51684 7784 R 11.0 2.7 1:58.37 python3
148845 ecs-ass+ 20 0 1003176 51684 7784 S 11.0 2.7 1:55.57 python3
148844 ecs-ass+ 20 0 1003176 51684 7784 S 10.7 2.7 1:55.95 python3
148847 ecs-ass+ 20 0 1003176 51684 7784 S 10.3 2.7 1:52.59 python3
148838 ecs-ass+ 20 0 1003176 51684 7784 S 10.0 2.7 1:56.71 python3
148839 ecs-ass+ 20 0 1003176 51684 7784 S 9.7 2.7 1:55.11 python3
148840 ecs-ass+ 20 0 1003176 51684 7784 S 9.7 2.7 1:55.12 python3
148842 ecs-ass+ 20 0 1003176 51684 7784 S 9.7 2.7 1:54.67 python3
148841 ecs-ass+ 20 0 1003176 51684 7784 S 8.7 2.7 1:52.63 python3
148846 ecs-ass+ 20 0 1003176 51684 7784 S 8.7 2.7 1:55.23 python3
148837 ecs-ass+ 20 0 1003176 51684 7784 S 0.0 2.7 0:00.10 python3
表示过去这三段时间的平均可运行进程数。整体偏高,CPU 比较忙,特别是 1 分钟内负载明显高于其他时间。
CPU 使用率非常高(主要是用户进程),负载高的主要原因是 CPU 密集型任务。%wa低,没有 I/O 等待。
内存是够用的,没有出现内存吃紧的情况。
进程详情(Python)
几乎所有线程 CPU 占用都很高(9~11%),说明是多线程并发在执行高 CPU 操作。
3、用 strace 挂载到主进程
strace -f -p 148837 -tt -T -e trace=file
或者
strace -f -p 148837 -tt -T -o strace.log
结果
[pid 148839] 13:05:35.561729 openat(AT_FDCWD, "translations.txt", O_RDONLY|O_CLOEXEC) = 9 <0.000285>
[pid 148842] 13:05:35.568763 openat(AT_FDCWD, "translations.txt", O_RDONLY|O_CLOEXEC) = 8 <0.001088>
[pid 148841] 13:05:35.572165 openat(AT_FDCWD, "translations.txt", O_RDONLY|O_CLOEXEC) = 10 <0.000055>
[pid 148846] 13:05:35.577015 openat(AT_FDCWD, "translations.txt", O_RDONLY|O_CLOEXEC) = 4 <0.000115>
[pid 148840] 13:05:35.580729 openat(AT_FDCWD, "translations.txt", O_RDONLY|O_CLOEXEC) = 5 <0.000037>
[pid 148844] 13:05:35.583458 openat(AT_FDCWD, "translations.txt", O_RDONLY|O_CLOEXEC) = 11 <0.000045>
或者聚焦某个高 CPU 线程:
sudo strace -p 148843 -tt -T
strace 工具的 syscall(系统调用)类别
1.File I/O 类(文件操作)
- open, openat
- read, pread64
- write, pwrite64
- stat, fstat, lstat
- close
- access
作用:文件的打开、读取、写入、关闭、权限检查。
异常表现:
- 频繁打开读取同一个文件(如 i18n 配置),会导致高 CPU。
- stat 每次请求都执行,说明文件缓存或路径判断未优化。
open("/app/config/i18n.json", O_RDONLY) = 3 read(3, "...", 4096) = 4096 close(3) = 0
这种情况通常出现在未正确缓存配置文件、日志文件等内容。
2.网络 I/O 类
- recv, recvfrom, recvmsg
- send, sendto, sendmsg
- accept, connect, bind, listen
- poll, select, epoll_wait, epoll_ctl
异常表现:
- 大量 recv/send 表示可能有高频 socket 请求或长连接处理不当。
- 高频 accept/connect 表示连接过多或频繁重建。
如果 Python 用了 requests、urllib 等未连接池化,容易引发大量连接开销。
3.进程/线程同步类
- futex(Fast Userspace Mutex)
- clone, pthread_create
- wait4, nanosleep, usleep
异常表现:
- 高频 futex:可能是线程之间的锁竞争激烈(如全局解释器锁 GIL 争抢)。
- nanosleep/usleep: 可能是某种低效的循环等待机制。
GIL 争抢通常表现在 Python 多线程(非多进程)中,反而降低并发效率。
4.内存类
- mmap, munmap
- brk, mprotect
- madvise
异常表现:
- 高频 mmap:大概率是某个模块反复加载文件(如 .so 动态库或 data 文件)。
- brk 增加表示内存分配多,未必与 CPU 高直接相关,但可能是副作用。
比如 Python 的 pickle.load()、Pandas 的大文件处理可能频繁触发 mmap。
5.信号/定时器类
- rt_sigaction, rt_sigprocmask
- timer_create, timer_settime
- alarm, getitimer
异常表现:
- 高频信号处理可能来自定时器或异步事件触发(比如轮询、超时重试机制)。
4、py-spy 无侵入式地查看 Python 层函数火焰图
pip install py-spy
py-spy record -o profile.svg --pid 148837
生成的 profile.svg 火焰图可以用浏览器打开
5、分析火焰图发现问题根因
load_translation_file
-> open()
-> read()
-> append_to_global_translation_list()
问题核心:
- 业务使用了某第三方国际化(i18n)库
- 本应全局缓存的翻译内容,却在找不到翻译时往全局变量里不断添加新的“空值”
- 每次找不到翻译项都会重新尝试读取文件
- 请求多了之后,这个列表变得超级大,查找翻译变慢,CPU 不断爆表
为什么线下环境没有复现?
- 因为触发条件是“翻译 key 缺失”
- 而测试/灰度环境数据健康,没有非法 key
- 灰度环境进程重启频繁,也避免了内存不断累积
6、修复后的示例代码
import threading
import time
TRANSLATION_FILE = "translations.txt"
# 缓存翻译字典,初始化为空
translations_cache = {}
# 用集合避免重复记录
missing_keys_set = set()
# 加锁保护共享变量(线程安全)
cache_lock = threading.Lock()
def load_translation_file():
"""模拟从文件读取翻译数据"""
time.sleep(0.01)
return {
"hello": "你好",
"bye": "再见",
}
def get_translation(key):
with cache_lock:
# 如果缓存为空则加载一次
if not translations_cache:
translations_cache.update(load_translation_file())
if key in translations_cache:
return translations_cache[key]
# 只添加没记录过的key,且不重复加载文件
if key not in missing_keys_set:
missing_keys_set.add(key)
# 可以选择这里记录日志,提示缺失翻译
print(f"[Warning] Missing translation key: {key}")
return f"[Missing: {key}]"
def worker_thread():
keys = ["hello", "bye", "invalid_key_1", "invalid_key_2"]
while True:
for k in keys:
val = get_translation(k)
time.sleep(0.001)
if __name__ == "__main__":
print("启动修复后模拟翻译服务,按 Ctrl+C 停止")
threads = []
for _ in range(5):
t = threading.Thread(target=worker_thread)
t.daemon = True
t.start()
threads.append(t)
try:
while True:
with cache_lock:
print(f"missing keys count: {len(missing_keys_set)}")
time.sleep(1)
except KeyboardInterrupt:
print("退出模拟")
关键改动说明
- 使用了 cache_lock 锁保护全局共享变量,防止多线程竞争问题。
- translations_cache 只加载一次(第一次调用时加载),后续复用。
- missing_keys_set 用集合替代列表,避免重复增长。
- 找不到翻译时,不再调用 load_translation_file(),消除无谓IO。
- 可以加日志提醒缺失的 key,方便后续补全。
程序不会频繁读文件,内存和CPU都会稳定,线上CPU异常高的问题就能解决。