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 таким образом снимается.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions