Привет, Хабр!
Меня зовут Андрей Бирюков. Я — независимый эксперт в области ИТ и ИБ, преподаю в учебных центрах и пишу статьи и книги.
При отладке приложения мы можем столкнуться с множеством проблем.
Одно дело, когда приложение просто падает, но бывают более интересные случаи, когда оно просто зависает. При этом, нагрузочные тесты проходят, но в боевом контуре через 3 часа работы контейнер перестает отвечать на liveness‑проверки.
Вы стучитесь в поды, пытаетесь зайти в шелл — тишина. Единственный способ реанимации — жесткий kubectl delete pod --force.
Но в логах при этом — пустота. Разве что, можно найти пару ошибок ConnectionResetError, которые «успешно обработаны».
И вот тут начинается самое интересное: код сделал именно то, что вы ему написали. Он послушно выполнил инструкцию «Пережить любую бурю». И причина странного поведения приложения кроется как раз в этой послушности.
“Бабушкин” try‑except
Всё, как обычно, начинается с благих намерений. Мы пишем вечный воркер, который таскает сообщения из очереди (RabbitMQ/Kafka/Redis). Самый простой и «надежный» способ обернуть логику — это гигантский блок.try/except.
Здесь, на первый взгляд, всё выглядит вполне логично:
import time from queue import Queue def critical_business_logic(data): # Представьте, что тут парсинг JSON и запись в БД if data == "crash": raise ValueError("Invalid data format") return "OK" def worker(): queue = Queue() queue.put("valid") queue.put("crash") queue.put("valid") while True: try: item = queue.get() result = critical_business_logic(item) print(f"Success: {result}") time.sleep(0.5) except Exception as e: print(f"Logged error: {e}") # Мы залогировали и идем дальше. Мы же не хотим, чтобы воркер упал из-за одной плохой задачки? if name == "__main__": worker()
Однако, на самом деле здесь кроется несколько проблем. Прежде всего, мы что называется, съедаем ValueError, логируем его и продолжаем забирать следующие задачи. Вроде бы всё хорошо, приложение не падает.
При этом, есть еще вторая, скрытая проблема. Мы написали код по принципу except Exception. В Python это означает: «Поймай всё, что является исключением, кроме системных сигналов».
А теперь представьте: DevOps пытается перезапустить ваш под, чтобы обновить конфиг. Он отправляет сигнал SIGTERM (через docker stop или kubectl delete). Python преобразует этот сигнал в исключение KeyboardInterrupt (или SystemExit).
Что будет делать ваш код в такой ситуации?
Правильно, он поймает сигнал в блок except Exception. Он аккуратно печатает в лог: Logged error: KeyboardInterrupt и... продолжает крутить цикл while True.
Соответственно, контейнер не завершается за положенные 30 секунд, и Kubernetes убивает его принудительно. Все задачи, которые были взяты в работу (in‑flight), теряются. В итоге вы получаете Split‑Brain или двойную обработку данных.
Реальный кейс
Вот реальная история, наглядно демонстрирующая данную проблему. В компании был микросервис‑конвертер валют. Он висел на очереди, и автор кода, опасаясь, что курс ЦБ РФ на официальном ресурсе иногда падает с 500-й ошибкой, обернул весь цикл обработки в try: except Exception.
Вроде бы все вполне логично.
Но однажды нужно было обновить сертификаты в кластере. Все поды получили сигнал на перезапуск. В логах появилось следующее:

Сервис игнорировал команды на выход, пытался переподключиться к БД (которая уже закрывалась), плодил ошибки подключения (которые тоже ловились) и вместо 2 секунд на завершение висел 40 секунд.
В итоге автоматический скейлер поднял новые поды, а старые «висяки» блокировали порты. Сервис лег полностью.
Согласитесь, не очень веселая история.
Хирургия обработчиков ошибок
Теперь давайте поговорим о том, как можно лечить подобные проблемы. Здесь правильное лечение будет состоять из трех слоев защиты.
Мы должны четко разделить:
Бизнес‑ошибки (их мы логируем и продолжаем выполнение задачи).
Системные сигналы (их мы перехватываем отдельно, чтобы приложение могло корректно завершиться).
Неожиданные сбои (им мы позволяем уронить наше приложение, чтобы оркестратор перезапустил контейнер свежим).
На первом шаге мы ловим только то, что знаем. Мы не должны допускать никаких голых except Exception.
def worker_safe(): while True: try: item = queue.get() # Здесь только одна конкретная операция, которая может упасть из-за данных critical_business_logic(item) except ValueError as e: # Ошибка формата данных. Задача битая. Логируем и подтверждаем (ack), чтобы она не висела в очереди вечно. print(f"Business error, skipping message: {e}") # queue.ack() если это RMQ except ConnectionError as e: # БД упала. Бесполезно продолжать. Лучше вылететь и перезапуститься. print(f"Critical infrastructure error: {e}") raise # Пробрасываем дальше, пусть падает
Далее мы будем отдельно отлавливать «Системный выход». Python предоставляет иерархию: BaseException > Exception.
Нам нужно ловить системные исключения на самом внешнем уровне, но не подавлять их, а корректно закрывать ресурсы.
import signal import sys def shutdown_gracefully(signum, frame): print("Received SIGTERM, closing connections...") # Здесь закрываем сессии БД, сохраняем состояние sys.exit(0) # Привязываем обработчик сигнала signal.signal(signal.SIGTERM, shutdown_gracefully)
Но если сигнал придет во время работы critical_business_logic, он вызовет KeyboardInterrupt внутри блока except ValueError? На самом деле это не так. Он вызовет его там, где код выполняется.
Чтобы быть уверенным, мы не используем signal для сложной логики, а используем флаг или перехватываем BaseException на самом верху, но сразу перевыбрасываем.
Теперь давайте посмотрим, что же должно получиться в итоге.
Вот тот золотой стандарт, который можно использовать в продуктивной среде. Обратите внимание: except Exception здесь нет.
import time import sys def worker_production(): while True: try: # 1. Только бизнес-логика data = get_from_queue() process_data(data) except (ValueError, TypeError) as e: # 2. Понятные ошибки данных. Логируем и идем дальше. print(f"Data error: {e}") continue except (ConnectionError, TimeoutError) as e: # 3. Ошибки инфраструктуры. Бессмысленно повторять сейчас, уходим на перезапуск. print(f"Infra error: {e}. Exiting...") raise # Выход из процесса, оркестратор перезапустит except BaseException as e: # 4. Это наш спасительный якорь для KeyboardInterrupt и SystemExit print(f"System signal received: {e}. Shutting down gracefully...") # Здесь мы можем сделать последний flush логов raise # Обязательно пробрасываем, чтобы процесс действительно завершился
Рассмотрим подробнее, как различные исключения будут обрабатываться в этом коде. KeyboardInterrupt наследуется от BaseException, но не от Exception. То есть, в нашем коде он попадет в блок except BaseException. Мы его поймаем, но сразу сделаем raise, и процесс завершится.
При этом, ValueError и TypeError игнорируются. Данные исключения мы съедаем, но это безопасно, потому что мы не трогаем системные сигналы. В результате мы избегаем ситуации с зацикливанием работы приложения в случае исключения.
Как убить except Exception одной строкой
Если вы пишете библиотеку или декоратор и хотите быть уверены, что никогда не заглушите KeyboardInterrupt по ошибке, используйте модуль contextlib и проверку типа ошибки.
Модуль contextlib помогает упростить создание контекстных менеджеров и решить типовые задачи управления ресурсами (файлы, сетевые соединения и так далее).
Вот пример кода:
from contextlib import contextmanager @contextmanager def suppress_business_errors(): try: yield except Exception as e: # Здесь Exception — это именно НЕ-системные ошибки, потому что мы не трогали BaseException. print(f"Swallowed: {e}")
Но этот подход требует определенной дисциплины.
Во многих компаниях командам разработчиков запрещено писать except Exception без явного комментария # noqa (# noqa — это директива для линтеров, которая говорит им игнорировать предупреждения для конкретной строки).
В код‑ревью это красный флаг.
Итог
В заключение можно сделать немного философский вывод — ошибка «Гонка за исключением» возникает из‑за нашей гордыни.
Нам кажется, что мы все предусмотрели, поэтому мы ловим всё. Но на самом деле Python умнее нас.
Он дает нам инструмент (BaseException), чтобы различать «ошибки в данных» и «команды операционной системы».
При написании кода запомните простое правило:
except Exception — это «пережить плохую погоду».
except BaseException — это «услышать команду капитана покинуть корабль».
Если ваш воркер перестал реагировать на Ctrl+C или docker stop — ищите этот проклятый голый except.
Вырежьте его оттуда, и ваши контейнеры начнут умирать красиво, оставляя после себя чистые логи и корректно закрытые транзакции.

Иногда проблема в production скрывается не в самой ошибке, а в том, как система с ней справляется: сервис зависает, а логи не показывают причину. Важно уметь находить такие сбои и понимать, где искать проблему — в коде, инфраструктуре или архитектуре.
Главная задача инженера — не только исправлять инциденты, но и создавать устойчивые системы, которые предсказуемо работают под нагрузкой.
Разобраться в диагностике инцидентов, инструментах анализа и подходах к созданию надежных сервисов помогут открытые уроки от преподавателей курсов:
8 сентября, 20:00. «ИИ против бага: как разобрать инцидент в Python‑проекте от логов до исправления». Записаться
23 сентября, 20:00. «eBPF: рентгеновское зрение для production». Записаться
6 октября, 20:00. «Практические подходы к переходу от монолита на микросервисы». Записаться
Весь список бесплатных уроков сентября собрали в дайджесте.

