Skip to content

Стабильность асинхронного httpx при длительной работе #9

Description

@GrigoryEvko

Зависимость от httpx в gigaevo-core идёт двумя путями. Прямое подключение есть в трёх модулях: gigaevo/memory/shared_memory/concept_api.py, problems/prompts/client.py, problems/chains/client.py. Транзитивно httpx тянется через openai 2.x, langchain-openai, langfuse, langsmith, langgraph-sdk, litellm и ещё с десяток пакетов из дерева зависимостей. Последний стабильный релиз httpx 0.28.1 вышел 6 декабря 2024 года. Ветка 1.0 пока находится в стадии dev3 без публичных сроков.

Картина деградации, которую я у себя наблюдаю в продакшене, не сопровождается никакими событиями в логах самого httpx. В какой-то момент работы asyncio-процесса время ответа тривиальных сетевых запросов вырастает с десятков миллисекунд до нескольких минут, и обратные прокси начинают отдавать 502. Полная замена ОС-процесса (то есть ротация воркеров, а не только клиента) ситуацию не исправляет: соединения с точки зрения httpcore остаются занятыми, при этом ни ConnectTimeout, ни PoolTimeout не возникает, поэтому на уровне приложения никакой обработки не запускается. Воспроизводится всё при довольно скромной нагрузке: пара параллельных конечных точек, два-три запроса в секунду, иногда реже. Я для себя временно нашел решение только перезагрузкой всего сервиса cron задачей (и миграцией на aiohttp).

Похожий профиль встречается во множестве апстримных сообщений, ни одно из которых не получило исправления в релизе 0.28.1. В encode/httpx Discussion #2556 описана ситуация, когда глобальный AsyncClient через несколько часов работы выходит в неработоспособное состояние и начинает постоянно возвращать httpx.PoolTimeout; воспроизведено на нескольких версиях, включая 0.28.1. encode/httpx Issue #1461 фиксирует утечку соединений при отмене asyncio.Task, из-за чего пул заполняется со временем. encode/httpx Discussion #3043 рассказывает про отдельный класс проблем, связанный с тем, что примитивы синхронизации httpcore не являются thread-safe при смешивании потоков и asyncio. Со стороны SDK поверх httpx картина зеркальная: в openai/openai-python #763 показано, что соединение не возвращается в пул при стриминговом ответе; #821, #2539 и #2688 описывают каскады PoolTimeout у пользователей в проде при трёх или шести запросах в секунду и при работе свыше суток; #874 посвящено обсуждению того, что нет понятного способа использовать AsyncClient в масштабе, и апстрим открытым решением не отвечает; #1596 собирает наблюдения о разнице в производительности конкурентных запросов между httpx и aiohttp с обходным путём через подмену транспорта. Аналогичная картина в смежных стеках задокументирована в pydantic-ai #3828, IBM mcp-context-forge #1731 и e2b-dev/E2B #1155.

Проблема не требует ни высоких QPS, ни длинных стриминговых ответов, ни смешения sync и async кода. Достаточно асинхронной нагрузки на несколько параллельных эндпоинтов и работы процесса от нескольких часов. httpx при этом не пишет в лог ничего, что говорило бы о деградации, и обнаружить происходящее можно только по росту задержки реквест/респонсов и по 502 со стороны апстримов.

Рабочая нагрузка gigaevo-core совпадает с профилем проблемы, долгоживущий asyncio-процесс, MAP-Elites с параллельной мутацией и оценкой, большое количество одновременных вызовов LLM через ChatOpenAI-инстансы. У каждого свой httpx.AsyncClient с дефолтными лимитами httpx.Limits(max_connections=100, max_keepalive_connections=20) и без явного keepalive_expiry.

Возможные направления удобно расставить по степени инвазивности. Самое мягкое: передавать в каждый ChatOpenAI и AsyncOpenAI при инициализации заранее настроенный httpx.AsyncClient с повышенными лимитами и явным keepalive_expiry. Следующее по сложности: обернуть роутер механизмом retry-and-rotate, который при втором подряд PoolTimeout заменяет клиент целиком и асинхронно вызывает aclose() на старом. Этот обходной путь предложен в Discussion #2556. Дальше идёт смена транспорта openai SDK на DefaultAioHttpClient, поставляемый в openai 2.x, чтобы вся работа SDK шла через aiohttp. Самый радикальный вариант состоит в том, чтобы переписать прямое использование httpx в трёх перечисленных модулях на aiohttp.ClientSession. Полностью убрать httpx из дерева зависимостей всё равно нельзя, потому что openai фиксирует httpx<1,>=0.23.0 в зависимостях. Но прямой контакт собственного кода с API httpx таким образом снимается.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions