Устранение проблем с утечками соединений в FastAPI с помощью SQLAlchemy и PostgreSQLPython

Программы на Python
Ответить
Anonymous
 Устранение проблем с утечками соединений в FastAPI с помощью SQLAlchemy и PostgreSQL

Сообщение Anonymous »

Я столкнулся с проблемой утечки соединения в моем проекте FastAPI. Соединения не закрываются должным образом и остаются в состоянии ожидания. Несмотря на мои попытки тщательно управлять подключениями, количество простаивающих подключений продолжает расти, что в конечном итоге приводит к проблемам при достижении максимального лимита подключений.
Подробнее о проекте:
  • Фреймворк: FastAPI (v0.104.0)
  • Веб-сервер: Uvicorn (v0.23.0) с 24 работниками
  • Библиотека базы данных: SQLAlchemy (v2.0.21)
  • База данных: PostgreSQL (через Docker)
Конфигурация PostgreSQL в docker-compose.yml :

Код: Выделить всё

postgres:
image: postgres:16
container_name: postgres_db
ports:
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 300s
timeout: 5s
retries: 3
Конфигурация базы данных:

Код: Выделить всё

DATABASE_URL = os.environ.get("DATABASE_URL")

engine = create_engine(
DATABASE_URL,
pool_size=24,  # Number of connections in the pool
max_overflow=12,  # Additional connections allowed beyond the pool size
pool_timeout=30,  # Wait time for a connection before timeout
pool_recycle=1800,  # Recycle connections after 1800 seconds (30 minutes)
pool_pre_ping=True,  # Ensure connections are valid before using
)
Менеджер базы данных:

Код: Выделить всё

class DatabaseManager:
_instance = None

def __new__(cls):
if not cls._instance:
cls._instance = super(DatabaseManager, cls).__new__(cls)
cls._instance.engine = engine
cls._instance.session_maker = scoped_session(
sessionmaker(autocommit=False, autoflush=False, bind=cls._instance.engine)
)
return cls._instance

def get_session(self):
session = self._instance.session_maker()
try:
yield session
finally:
session.close()
self._instance.session_maker.remove()
Маршрутизатор входа:

Код: Выделить всё

@user_router.post("/login")
def login_user(
request: Request,
login_data: LoginRequest,
usermanager=Depends(UserManager),
db_session: Session = Depends(DatabaseManager().get_session),
):
return usermanager.login_user(request, login_data, db_session)
Логика входа:

Код: Выделить всё

def login_user(self, request, login_data, db_session:  Session):
try:
user = self.authenticate_user(request, login_data, db_session)
if not user:
raise HTTPException(status_code=401, detail="Incorrect username or password")

if user.is_active:
tokens = self.create_tokens_for_user(user)
return tokens
else:
raise HTTPException(status_code=401, detail="User is inactive")

except HTTPException as e:
self.log_exception("login", e.status_code, e.detail, traceback.format_exc())
raise e

except Exception as e:
try:
self.log_exception("login retry", 500, str(e), traceback.format_exc())
db_session = next(DatabaseManager().get_session())
user = self.authenticate_user(request, login_data, db_session)
if user and user.is_active:
return self.create_tokens_for_user(user)
finally:
db_session.close()

raise HTTPException(status_code=500, detail="An error occurred while logging in")

Увеличение числа простаивающих соединений:

Я отслеживаю соединения PostgreSQL с помощью следующего запроса внутри контейнера:

Код: Выделить всё

SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
вывод:

Код: Выделить всё

state  | count
-------+-------
|     5
active |     1
idle   |    73
и количество простаивающих соединений растет!
Хотя я ожидаю максимум 24 простаивающих соединения (из-за размера пула), число продолжает увеличиваться и достигает максимального предела подключений.
Даже до того, как количество простаивающих подключений достигнет максимума, я иногда получаю эту ошибку:

Код: Выделить всё

sqlalchemy.exc.InvalidRequestError: This session is provisioning a new connection;
concurrent operations are not permitted
И когда он достигает максимума, я получаю это:

Код: Выделить всё

psycopg2.OperationalError: connection to server at "postgres", port 5432 failed:
FATAL:  sorry, too many clients already
Замеченное поведение:
Даже когда количество простаивающих соединений достигает максимума, логика повтора в функции login_user, похоже, работает правильно. Я гарантирую, что все сеансы закрываются должным образом (например, с помощью наконец), если я не использую внедрение зависимостей FastAPI. Кроме того, я использую сеанс с ограниченной областью действия, поэтому для каждого потока поддерживается один сеанс. Однако проблема остается.
Я перепробовал все, что мог придумать, в том числе обратился за помощью к инструментам искусственного интеллекта. К сожалению, ничего не помогло.
Дополнительная информация:
Я запустил клон своего приложения без обработки запросов, и вот статистика соединений. примерно через 14 часов:

Код: Выделить всё

p_db=# SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
state  | count
--------+-------
|     5
active |     1
idle   |    24
(3 rows)
Это имеет смысл, поскольку сеанс ограничен областью действия и имеет одно соединение на поток.
Однако в моем основном приложении, где единственное соединение, связанное с PostgreSQL, операция — вход в систему, вот статистика:

Код: Выделить всё

p_db=# SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state;
state  | count
--------+-------
|     5
active |     1
idle   |    89
Не понимаю, почему в основном приложении продолжает расти количество простаивающих подключений!
Итак, количество простаивающих подключений достигло максимума , и теперь я понимаю следующее:
Я говорил это раньше и скажу еще раз — тем не менее, из-за этой ошибки мой вход в систему (ну, повторная попытка) работает. Как?

(psycopg2.OperationalError) подключение к серверу в «postgres», порт 5432 не удалось: FATAL: извините, уже слишком много клиентов
Я даже отправляю 1000 запросов к клону своего приложения, но количество простаивающих соединений остается на уровне 24!
Обновить
я даже удалю пул соединений и ничего не меняется
Я не знаю, как я могу изолировать эту проблему больше, чем это!
Результатом этого запроса была та же длина, что и у моего неактивного соединения, но вся строка была то же самое, поэтому я просто скопирую сюда несколько из них!

Код: Выделить всё

p_db=# SELECT pid, state, query, backend_start, query_start, client_addr
FROM pg_stat_activity
WHERE state = 'idle';
pid | state |  query   |         backend_start         |          query_start          | client_addr
-----+-------+----------+-------------------------------+-------------------------------+-------------
35 | idle  | ROLLBACK | 2025-01-17 10:08:44.755878+00 | 2025-01-17 10:31:27.250991+00 | 172.30.30.6
36 | idle  | ROLLBACK | 2025-01-17 10:08:44.77771+00  | 2025-01-17 10:37:32.826234+00 | 172.30.30.6
37 | idle  | ROLLBACK | 2025-01-17 10:08:45.007471+00 | 2025-01-17 10:38:24.326404+00 | 172.30.30.6
38 | idle  | ROLLBACK | 2025-01-17 10:08:45.064301+00 | 2025-01-17 10:22:07.79541+00  | 172.30.30.6
39 | idle  | ROLLBACK | 2025-01-17 10:08:45.139814+00 | 2025-01-17 10:22:04.841665+00 | 172.30.30.6
40 | idle  | ROLLBACK | 2025-01-17 10:08:45.146128+00 | 2025-01-17 10:37:32.806969+00 | 172.30.30.6
41 | idle  | ROLLBACK | 2025-01-17 10:08:45.190972+00 | 2025-01-17 10:37:30.432942+00 | 172.30.30.6
44 | idle  | ROLLBACK | 2025-01-17 10:08:45.206284+00 | 2025-01-17 10:37:30.130094+00 | 172.30.30.6
42 | idle  | ROLLBACK | 2025-01-17 10:08:45.205131+00 | 2025-01-17 10:37:32.81468+00  | 172.30.30.6
172.30.30.6 — это IP-адрес моего контейнера fastapi.

Подробнее здесь: https://stackoverflow.com/questions/793 ... postgresql
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Python»