Когда прожорливый код угробит CPU в продакшене, а ставить py-spy или Pyroscope нельзя из-за политики безопасности или ограничений архитектуры, поможет связка стандартной библиотеки. Разработчики часто забывают, что семплер можно собрать за час без единой внешней зависимости, но допускают ошибки с потоками и задержками.
Идея и реализация
signal.setitimer запускает обработчик через заданные интервалы (например, 50-100 мс). Внутри него sys._current_frames() делает мгновенный снапшот стеков всех потоков. Ключевой нюанс: не используй traceback.format_stack() — это тормозит. Только сухие f_code.co_name и f_lineno.Production-ready пример для мониторинга CPU-горячих точек
import signal
import sys
import threading
import time
stacks_buffer = []
BUFFER_LOCK = threading.Lock()
def collect_stack(signum, frame):
try:
snapshot = {}
for tid, t_frame in sys._current_frames().items():
stack = []
while t_frame:
stack.append(f"{t_frame.f_code.co_name}:{t_frame.f_lineno}")
t_frame = t_frame.f_back
snapshot[tid] = stack[:15] # Ограничиваем глубину
with BUFFER_LOCK:
stacks_buffer.append((time.time(), snapshot))
except Exception:
pass # Тихий сброс ошибок
signal.signal(signal.SIGALRM, collect_stack)
signal.setitimer(signal.ITIMER_REAL, 0.05, 0.05) # 50 ms
# Далее обычный код приложения...
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
signal.setitimer(signal.ITIMER_REAL, 0, 0)
for ts, snap in stacks_buffer[:5]:
print(ts, snap)
Типичные ошибки и trade-offs
* Сигнал SIGALRM работает только в главном потоке — для многопоточности нужен ручной
signal.signal и отдельный поток-коллектор для записи в буфер (иначе потеря данных из-за рекурсии сигналов).* Минимальная частота — 50 мс; ниже уже жрёт CPU сам семплер. Для asyncio проще использовать
asyncio.Task.get_stack() из цикла событий.* C-расширения (например, numpy или lxml) не покажут стек — тут без py-spy или perf не обойтись.
Практический совет
Записывай в кольцевой буфер с фиксированной ёмкостью (например, 1000 сэмплов) и асинхронно сбрасывай на диск в фоновом потоке — это thread-safe и не фризит основной код.
Вывод: Связка
sys.setprofile + signal.setitimer даёт дешёвый семплинг стека для поиска CPU-горячих точек в продакшене без внешних зависимостей, но требует аккуратности с потоками, частотой и обработкой C-расширений.