Я разрабатываю API, который обращается к MariaDB. Я использую SQLAlchemy для доступа к БД и столкнулся со странной утечкой памяти при вставке данных. Когда вставляю данные и ошибки нет, все в норме. Потребление оперативной памяти увеличивается, но после завершения транзакции оно снова снижается, как и ожидалось. Это не тот случай, когда ошибка IntegrityError возникает из-за уникального индекса в таблице. Каждый раз, когда я начинаю новую вставку и возникает ошибка, потребление оперативной памяти увеличивается и больше никогда не снижается. В моем проекте я использую FastAPI и сервер uvicorn для запуска API, но для дальнейшего исследования я сузил проблему до компактного 90-строчного процессора и имею такое же поведение. Тестовый скрипт автоматически создает таблицу, но БД (etetete) приходится создавать вручную.
Что мне здесь не хватает?
Питон версии 3.12 Последняя версия MariaDB в только что созданном контейнере Docker.
вывод списка пипов:
Package Version
----------------- -------
greenlet 3.0.3
mariadb 1.1.10
packaging 24.0
pip 24.0
SQLAlchemy 2.0.29
typing_extensions 4.11.0
Тестовый скрипт Python:
import time
from sqlalchemy import create_engine,Index,String
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import Session
class Base(DeclarativeBase):
pass
class Person(Base):
__tablename__ = "person"
__table_args__ = (
Index("age","children","number",unique=True),
)
id: Mapped[int] = mapped_column(primary_key=True,autoincrement=True)
age: Mapped[int]
children: Mapped[int]
number: Mapped[int]
name: Mapped[str] = mapped_column(String(20))
prename: Mapped[str] = mapped_column(String(20))
def generatePersons(nValues):
persons = []
for i in range(0,nValues):
persons.append(Person(
age=i,
children=i,
number=i,
name = "Smith",
prename = "John"
))
return persons
#DB TEST FUNCTIONS---------------
def testDb(nValues):
print("Start")
engine = create_engine("mariadb+mariadbconnector://root:12345@localhost:3306/etetete")
Base.metadata.create_all(engine)
s = Session(engine)
persons = generatePersons(nValues)
try:
s.add_all(persons)
s.commit()
except Exception as e:
print("error" + str(e))
s.rollback()
finally:
s.expunge_all()
s.expire_all()
s.close()
engine.clear_compiled_cache()
engine.dispose()
print("End")
def testDbContextManager(nValues):
print("Start")
engine = create_engine("mariadb+mariadbconnector://root:12345@localhost:3306/etetete")
Base.metadata.create_all(engine)
persons = generatePersons(nValues)
with Session(engine) as s:
try:
s.add_all(persons)
s.commit()
except Exception as e:
print("error"+str(e))
print("End")
#---------------DB TEST FUNCTIONS
def executeTest():
for i in range(0,100):
start = time.time()
# testDb(200000)
testDbContextManager(200000)
end = time.time()
res = end - start
print("ExecutionTime: " + str(round(res, 2))+"s")
input("Once again?")
print()
executeTest()
Я попробовал несколько разных подходов. С контекстным менеджером или без него. Различные версии SQLAlchemy. Использование сборщика мусора Python. Первая промывка и, наконец, фиксация в БД. Я наблюдаю за потреблением оперативной памяти с помощью диспетчера задач Windows, но также использую профилировщик памяти Python...






Хорошо, после многих часов исследований и множества различных попыток я наконец нашел решение этой проблемы.
С SQLAlchemy проблем не было, это было поведение Python при обработке исключений. При возникновении исключения создается обратная трассировка стека, и поскольку обратная трассировка исключения настолько велика из-за большого количества результатов, она потребляет много памяти и никогда не очищает эту обратную трассировку стека.
Решение заключалось в том, чтобы просто добавить это в блок исключений, чтобы вручную удалить обратную трассировку:
e.__traceback__ = None
Я реализовал это решение после прочтения этого сообщения в блоге: https://cosmicpercolator.com/2016/01/13/Exception-leaks-in-python-2-and-3/