sys.monitoring是Python 3.12唯一低开销原生运行时指标收集入口,仅在显式注册的事件点触发回调,默认禁用所有事件,需手动获取tool_id、注册回调并谨慎启用事件以避免性能反模式。

Python 3.12 的 sys.monitoring 是唯一低开销的原生运行时指标收集入口
Python 3.12 引入的 sys.monitoring 是官方明确设计用于「生产环境持续采样」的轻量级 API,它绕过了传统 sys.settrace 和 sys.setprofile 的高开销调用栈捕获机制。关键在于:它不拦截每一行字节码,而是只在预注册的事件点(如函数进入/退出、异常抛出)触发回调,且默认禁用所有事件——你得显式启用,开销才产生。
常见错误是误以为启用后就“自动采集”,其实必须手动注册监听器并处理事件;另一个坑是把 sys.monitoring.use_tool_id() 当成可重复调用的初始化函数,实际它只返回一个整数 ID,且同一进程内多次调用会返回不同 ID,但只有第一个有效,后续调用若未清理旧 ID 可能导致事件丢失。
- 使用前必须先调用
sys.monitoring.use_tool_id()获取唯一 tool_id - 仅支持有限事件类型:
sys.monitoring.events.CALL、sys.monitoring.events.RETURN、sys.monitoring.events.LINE(慎用)、sys.monitoring.events.EXCEPTION - 回调函数签名固定为
callback(code_obj, line_no, arg),其中arg含义依事件而异(如CALL时为None,EXCEPTION时为(exc_type, exc_value)) - 不能在回调中再调用
sys.monitoring相关函数,否则触发RuntimeError: monitoring callback reentrancy
如何安全注册函数调用监控并避免性能反模式
监控函数调用最常用也最易出问题。直接对所有函数启用 CALL 事件会导致大量系统函数(如 len、isinstance)也被捕获,反而拖慢程序。正确做法是白名单过滤 + 延迟注册。
示例:只监控用户模块中的函数,并在首次调用时动态注册:
import sys import my_module # 假设这是你要监控的模块 <p>tool_id = sys.monitoring.use_tool_id()</p><div class="aritcle_card flexRow artxards"> <div class="artcardd flexRow"> <a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill9965" title="macOS UI 自动化技能包 —— 用 Python 控制你的 Mac"><img src="https://img.php.cn/upload/skill/001/246/273/6adaf20dbae7c507.jpg" alt="macOS UI 自动化技能包 —— 用 Python 控制你的 Mac" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a> <div class="aritcle_card_info flexColumn"> <a rel="nofollow" href="/xiazai/skill9965" title="macOS UI 自动化技能包 —— 用 Python 控制你的 Mac" class="overflowclass">macOS UI 自动化技能包 —— 用 Python 控制你的 Mac</a> <p class="overflowclass">让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限</p> </div> <a rel="nofollow" href="/xiazai/skill9965" title="macOS UI 自动化技能包 —— 用 Python 控制你的 Mac" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a> </div> </div><p>def call_handler(code_obj, line_no, arg): if hasattr(code_obj, 'co_filename') and 'my_module.py' in code_obj.co_filename:</p><h1>记录函数名、文件、行号,避免字符串拼接或 print</h1><pre class="brush:php;toolbar:false;"> metrics.append((code_obj.co_name, code_obj.co_filename, line_no))
初始只启用 RETURN 事件占位(开销极低),CALL 先禁用
sys.monitoring.set_events(tool_id, 0) sys.monitoring.register_callback(tool_id, sys.monitoring.events.RETURN, call_handler)
后续按需开启 CALL(例如收到 HTTP 请求后)
sys.monitoring.set_events(tool_id, sys.monitoring.events.CALL | sys.monitoring.events.RETURN)
-
sys.monitoring.set_events(tool_id, 0)是关闭所有事件的安全方式,比逐个清零更可靠 - 回调中避免任何 I/O、日志格式化、对象创建;建议只存原始元组,批量导出
- 不要在回调里访问
code_obj.co_consts或co_code,它们会触发字节码解析,增加开销 - 如果用
LINE事件,务必限制在单个函数内(通过code_obj过滤),否则开销直逼settrace
为什么 sys.monitoring.get_events(tool_id) 返回值总为 0?
这不是 bug,而是设计使然:sys.monitoring.get_events(tool_id) 返回的是「当前已启用的事件掩码」,但它只反映最近一次 set_events() 调用后的状态,且**不包含其他工具(如调试器、coverage 工具)可能设置的事件**。更重要的是,CPython 内部对事件启用做了延迟提交——调用 set_events() 后,事件实际生效可能滞后 1~2 个字节码周期。
验证是否生效,别依赖 get_events(),改用以下方式:
- 在回调函数开头加一行
print("hit")并观察输出频率 - 用
sys.monitoring.get_tool_info(tool_id)查看注册状态(返回None表示未注册回调) - 检查是否被其他工具抢占:运行前设置环境变量
PYTHONMALLOC=debug并观察是否有monitoring tool conflict日志 - 确认 Python 版本确实是 3.12+:
sys.monitoring在 3.12.0a1 才首次引入,3.12.0rc1 修复了多个竞态 bug
生产环境部署前必须检查的三件事
sys.monitoring 在多线程、异步和 fork 场景下行为特殊,很多问题上线后才暴露。
- 多线程:每个线程需独立调用
use_tool_id(),主线程的 tool_id 在子线程无效;但回调函数本身是线程安全的(CPython GIL 保证) - asyncio:事件只在执行 Python 字节码的协程帧中触发,不会在
await挂起期间触发;若用loop.run_in_executor,子进程不继承监控配置 - fork:子进程不继承父进程的
sys.monitoring状态,必须在os.fork()后重新初始化(包括use_tool_id()和register_callback())
最容易被忽略的是信号处理——当进程收到 SIGUSR1 等信号时,若信号处理器中调用了监控相关函数,会立即崩溃。所有监控开关操作,必须确保在主线程、非信号上下文中执行。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!










