Как организовать хранение и обращение к счетчику обращений к эндпоинтам в FastAPI?
Что проверяют
Понимаете ли вы, где живёт состояние FastAPI-приложения и что с ним происходит при нескольких воркерах. Глобальная переменная работает на ноутбуке и ломается в продакшене. Следом спрашивают про атомарность инкремента и про то, как не раздуть число ключей.
Как отвечать
Сначала хранилище:
- память процесса (
dict,collections.Counter): только для прототипа. Приuvicorn --workers 4или под Gunicorn у каждого процесса свой счётчик, при рестарте всё обнуляется; - Redis: общий для всех воркеров и инстансов,
INCRиHINCRBYатомарны. Клиентredis.asyncioсоздаётся вlifespanи кладётся вapp.state; - Prometheus: если счётчик нужен для мониторинга,
Counterизprometheus_clientс меткамиmethod,route,statusи эндпоинт/metrics. При нескольких процессах нужен multiprocess mode черезPROMETHEUS_MULTIPROC_DIRили готовыйprometheus-fastapi-instrumentator; - база: если счётчик это бизнес-данные (просмотры статьи), пачечный сброс из Redis раз в минуту, чтобы не писать в базу на каждый запрос.
Затем место вызова. Middleware видит все запросы, включая 404, но шаблон маршрута там приходится доставать после call_next. Проще зависимость на роутере: APIRouter(dependencies=[Depends(count_hit)]), внутри неё request.scope["route"].path отдаёт шаблон /items/{item_id}, а не конкретный URL.
Чтение отдавайте отдельным закрытым эндпоинтом: HGETALL по хэшу со счётчиками.
Где спотыкаются
Считают по фактическому пути или по параметру, как request_counter[item_id]. Каждый id становится новым ключом, память и число меток Prometheus растут без предела.
Думают, что += в Python безопасен всегда. В async def без await между чтением и записью гонки нет, но синхронные def-обработчики FastAPI выполняет в пуле потоков, и там инкремент словаря без блокировки уже не атомарен.
Делают GET и SET в Redis вместо INCR. Между чтением и записью другой воркер успевает записать своё значение, и часть обращений теряется.