9.0 KiB
9.0 KiB
Где и почему может виснуть
- Синхронный диск/JSON в обработчиках запросов
_save_media_file_idsделаетopen/json.load/json.dumpпрямо в пути запроса, безto_thread. При большомdata/media_file_ids.jsonэто блокирует event loop.
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
existing_data = json.load(f)
...
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(existing_data, f, ensure_ascii=False, indent=2)
- Кеш истории также читает/пишет файлы синхронно:
with open(cache_file, 'rb') as f:
cache_data = pickle.load(f)
...
with open(cache_file, 'wb') as f:
pickle.dump(cache_data, f)
-
Итог: под нагрузкой любой долгий sync I/O останавливает обработку всех запросов на время операции.
-
CPU‑тяжёлое формирование RSS/HTML в event loop
- Парсинг/санитизация/генерация RSS идут синхронно в корутинах (bleach, feedgen, склейка больших строк). На больших лимитах это “съедает” цикл.
final_posts = await _render_messages_groups(...)
...
rss_feed = fg.rss_str(pretty=True)
final_posts = await _render_messages_groups(...)
...
html = '\n<hr class="post-divider">\n'.join(html_posts)
- Глобальный
python-magicв потоках- Один общий
Magic()используется из разных потоков — это не потокобезопасно, возможны зависания внутри libmagic.
- Один общий
media_type = await asyncio.to_thread(magic_mime.from_file, file_path)
- BaseHTTPMiddleware над стримингом файлов
BaseHTTPMiddlewareоборачиваетcall_next. ДляFileResponseэто может ломать/буферизовать стриминг и усиливать задержки.
class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
...
response = await call_next(request)
- Отсутствие таймаутов на вызовах Telegram API
- Если
get_messages/download_media“подвисли”, запрос к/mediaвисит долго.
- Если
file_path, delete_after = await download_media_file(...)
message = await client.client.get_messages(...)
file_path = await client.client.download_media(...)
- Шумное логирование в middleware
- Лог всех заголовков и запросов при каждом хите может создавать I/O‑бутылочное горлышко под нагрузкой.
logger.info(f"Request: {request.method} {request.url}")
logger.info(f"Headers: {dict(request.headers)}")
- Конкурентная запись и разрастание
media_file_ids.json- Записи без блокировок → гонки и порча JSON. В фоне потом ремонт json-repair (тяжёлый CPU).
except json.JSONDecodeError:
media_files = await asyncio.to_thread(fix_corrupted_json, file_path)
- Один воркер uvicorn
- Любая тяжёлая задача “монополизирует” процесс — остальное, включая отдачу файлов, ждёт.
uvicorn.run(..., reload=True, loop="uvloop")
Что сделать (короткий чеклист)
-
Убрать sync I/O из пути запроса
- Обернуть чтение/запись JSON и pickle в
asyncio.to_thread. - Для
media_file_ids.jsonвезде использовать единые sync‑хелперы с временным файлом и заменить прямыеopen/json.dump:- Вынести в отдельный модуль функции уже имеющихся
read_json_file_sync/write_json_file_syncи звать их черезto_thread.
- Вынести в отдельный модуль функции уже имеющихся
- Добавить один
asyncio.Lockна операции записиmedia_file_ids.jsonчтобы не было гонок.
- Обернуть чтение/запись JSON и pickle в
-
Разгрузить CPU из event loop
- В RSS/HTML:
- После того как данные получены из Telegram, вынести “склейку HTML”, bleach‑санитизацию и
fg.rss_strвasyncio.to_thread. - Ограничить
limitпо умолчанию до 50–80. Для больших значений — отдавать 429 или 400 с рекомендацией. - Опционально — кэшировать итоговые RSS/HTML на пару минут.
- После того как данные получены из Telegram, вынести “склейку HTML”, bleach‑санитизацию и
- В RSS/HTML:
-
Исправить
python-magic- Создавать
Magic()per‑call внутриto_threadИЛИ защищать глобальный объектthreading.Lock. - Если не критично — падать на
mimetypesи не вызыватьmagicдля известных расширений.
- Создавать
-
Middleware для логов
- В проде отключить текущий
RequestLoggingMiddlewareили перевести на ASGI‑middleware без вмешательства в стриминг. - Снизить уровень и объём логов (заголовки — только на DEBUG, семплирование).
- В проде отключить текущий
-
Таймауты телеграма
- Оборачивать
get_messages/download_mediaвasyncio.wait_for(..., timeout=30–60)с понятной ошибкой/503. - На FloodWait в
/media— отдавать 429 сRetry-After.
- Оборачивать
-
Запись
media_file_ids.json- Причесать размер: периодически чистить/ротация по размеру/возрасту.
- В фоне перепись файла уже есть — не трогать event loop; убедиться, что на записи тоже используется temp‑файл +
os.replace(как вwrite_json_file_sync).
-
Процессный параллелизм
- Запускать с несколькими воркерами, без
reload:- пример:
uvicorn api_server:app --host 0.0.0.0 --port 80 --workers 2 --loop uvloop --http httptools
- пример:
- Или за Gunicorn:
--workers 2 --worker-class uvicorn.workers.UvicornWorker.
- Запускать с несколькими воркерами, без
-
Пул потоков
- Явно ограничить/увеличить пул для
to_threadесли есть много фоновых задач, чтобы не выжирался из‑за долгих операций.
- Явно ограничить/увеличить пул для
Минимальные целевые правки (по месту)
_save_media_file_ids— переписать наawait asyncio.to_thread(read_json_file_sync/ write_json_file_sync)+ общийasyncio.Lock. Убрать прямыеopen/json.dump.tg_cache— обернутьpickle.load/dumpвto_thread.prepare_file_response— перестать шаритьmagic_mime; создавать локальныйmagic.Magic(mime=True)внутриto_threadили защитить глобальный объектLock.RequestLoggingMiddleware— удалить/заменить на ASGI‑middleware и логировать только метод+путь, без заголовков по умолчанию.get_messages/download_media— таймауты черезasyncio.wait_for.uvicorn.run— убратьreload=True, добавитьworkers=2+.
Как быстро проверить
- Нагрузить
/rssc большимlimitпараллельно с/mediaи измерить задержку отдачи файлов до/после:- до фиксов статик “замирает” во время генерации;
- после выноса CPU/I/O в
to_threadи увеличения воркеров — файлы продолжают отдаваться.
Коротко: основные причины — синхронный диск и CPU в корутинах, общий python-magic, и единичный воркер. Перенос I/O/CPU в to_thread, фиксы гонок JSON, таймауты на Telegram и 2–4 воркера решат подвисания.
- Нашёл проблемные места: sync I/O и CPU в обработчиках,
python-magic, middleware, таймауты и один воркер. Дал точечные действия для устранения подвисаний.