SQLAlchemy - работа с подключениями к БД и Сессиям (непонятное поведение и часть в документации) - PullRequest
0 голосов
/ 01 июля 2018

Я использую SQLAlchemy (действительно хороший ORM, но документация недостаточно ясна) для связи с PostgreSQL
Все было замечательно, пока не был достигнут один случай, когда postgres «завис» из-за максимальной границы соединения: больше не разрешено соединений (max_client_conn) .
Этот случай заставляет меня думать, что я делаю что-то неправильно. После нескольких экспериментов я понимаю, как не столкнуться с этой проблемой снова, но некоторые вопросы остались

Ниже вы увидите примеры кода (в Python 3+, настройки PostgreSQL по умолчанию) без и с упомянутой проблемой, и в конечном итоге я хотел бы услышать ответы на следующие вопросы:

  1. Что именно менеджер контекста делает с соединениями и сессиями? Закрытие сессии и удаление соединения или что?
  2. Почему первый рабочий пример кода ведет себя как пример с проблемой без NullPool как пул-класс в методе "connect"?
  3. Почему в первом примере я получил только 1 соединение с БД для всех запросов, но во втором примере я получил отдельное соединение для каждого запроса? (пожалуйста, поправьте меня, если я неправильно понял, проверял это с помощью "pgbouncer" )
  4. Как лучше всего открывать и закрывать соединения (и / или работать с Session), когда вы используете SQLAlchemy и PostgreSQL DB для нескольких экземпляров сценария (или отдельных потоков в сценарии), который прослушивает запросы и должен иметь отдельный сеанс для каждый из них? (Я имею в виду сырой SQLAlchemy, а не Flask-SQLAlchemy или что-то в этом роде)

    Рабочий пример кода без проблем:

подключение к БД :

from sqlalchemy.pool import NullPool  # does not work without NullPool, why?

def connect(user, password, db, host='localhost', port=5432):
    """Returns a connection and a metadata object"""
    url = 'postgresql://{}:{}@{}:{}/{}'.format(user, password, host, port, db)

    temp_con = sqlalchemy.create_engine(url, client_encoding='utf8', poolclass=NullPool)
    temp_meta = sqlalchemy.MetaData(bind=temp_con, reflect=True)

    return temp_con, temp_meta

функция для получения сеанса для работы с БД :

from contextlib import contextmanager

@contextmanager
def session_scope():
    con_loc, meta_loc = connect(db_user, db_pass, db_instance, 'localhost')
    Session = sessionmaker(bind=con_loc)

    """Provide a transactional scope around a series of operations."""
    session = Session()
    try:
        yield session
        session.commit()
    except:
        session.rollback()
        raise

пример запроса :

with session_scope() as session:
    entity = session.query(SomeEntity).first()


Неверный пример кода:

функция для получения сеанса для работы с БД :

def create_session():
    # connect method the same as in first example
    con, meta = connect(db_user, db_pass, db_instance, 'localhost')
    Session = sessionmaker(bind=con)

    session = Session()
    return session

пример запроса :

session = create_session()
entity = session.query(SomeEntity).first()


Надеюсь, у вас есть основная идея

Ответы [ 3 ]

0 голосов
/ 02 июля 2018

Прежде всего вы не должны создавать движки повторно в вашей функции connect(). Обычная практика заключается в том, чтобы в вашем приложении был один глобальный экземпляр Engine для URL базы данных . То же самое относится к классу Session, созданному sessionmaker().

  1. Что именно менеджер контекста делает с соединениями и сессиями? Закрытие сессии и удаление соединения или что?

Что вы запрограммировали для этого, и если это покажется неясным, прочтите о менеджерах контекста в целом В этом случае фиксирует или откатывает сеанс, если в блоке, управляемом оператором with, возникла исключительная ситуация. Оба действия возвращают соединение, используемое сеансом, в пул, который в вашем случае является NullPool, поэтому соединение просто закрывается.

  1. Почему первый рабочий пример кода ведет себя как пример с проблемой без NullPool в качестве пул-класса в методе "connect"?

и

from sqlalchemy.pool import NullPool  # does not work without NullPool, why?

Без NullPool движки, которые вы неоднократно создаете, также подключаются к пулу, поэтому, если они по какой-то причине не выходят за рамки, или их учетные записи в противном случае не обнуляются, они будут удерживать соединения, даже если сеансы возвращают их , Неясно, будут ли сеансы своевременно выходить из области действия во втором примере, поэтому они могут также удерживать соединения.

  1. Почему в первом примере я получил только 1 соединение с БД для всех запросов, но во втором примере я получил отдельное соединение для каждого запроса? (пожалуйста, поправьте меня, если я неправильно понял, проверял это с помощью "pgbouncer")

Первый пример завершает закрытие соединения из-за использования диспетчера контекста, который правильно обрабатывает транзакции, и NullPool, поэтому соединение возвращается баунсеру, который является другим уровнем пула.

Второй пример может никогда не закрывать соединения, поскольку в нем отсутствует обработка транзакций, но это неясно из-за приведенного примера. Также может удерживать подключения в отдельных созданных вами движках.

Четвертый пункт вашего набора вопросов в значительной степени описан в официальной документации в "Основы сеанса" , особенно "Когда я создаю сеанс, когда я его фиксирую и когда мне закрыть его? " и " Является ли сеанс потокобезопасным? ".

Есть одно исключение: несколько экземпляров скрипта. Вы не должны совместно использовать ядро ​​между процессами , поэтому для объединения соединений между ними вам необходим внешний пул, такой как PgBouncer.

0 голосов
/ 04 июля 2018

@ Ilja Everilä ответ был в основном полезен
Я оставлю отредактированный код здесь, может быть, он кому-нибудь поможет

Новый код, который работает так, как я ожидал, выглядит следующим образом:

подключение к БД: :

from sqlalchemy.pool import NullPool  # will work even without NullPool in code

def connect(user, password, db, host='localhost', port=5432):
   """Returns a connection and a metadata object"""
   url = 'postgresql://{}:{}@{}:{}/{}'.format(user, password, host, port, db)

   temp_con = sqlalchemy.create_engine(url, client_encoding='utf8', poolclass=NullPool)
   temp_meta = sqlalchemy.MetaData(bind=temp_con, reflect=True)

   return temp_con, temp_meta 


один экземпляр соединения и создатель сеанса для приложения, например, где ваша основная функция :

from sqlalchemy.orm import sessionmaker

# create one connection and Sessionmaker to each instance of app (to avoid creating it repeatedly)
con, meta = connect(db_user, db_pass, db_instance, db_host)
session_maker = sessionmaker(bind=con) enter code here


функция для получения сеанса с with оператором :

from contextlib import contextmanager
from some_place import session_maker

@contextmanager
def session_scope() -> Session:
    """Provide a transactional scope around a series of operations."""
    session = session_maker()  # create session from SQLAlchemy sessionmaker
    try:
        yield session
        session.commit()
    except:
        session.rollback()
        raise


Завершение транзакции и использование сеанса :

with session_scope() as session:
    entity = session.query(SomeEntity).first()
0 голосов
/ 01 июля 2018
  1. Что именно менеджер контекста делает с соединениями и сессиями? Закрытие сеанса и удаление соединения или что?

Менеджер контекста в Python используется для создания контекста времени выполнения для использования с оператором with. Просто, когда вы запускаете код:

with session_scope() as session:
    entity = session.query(SomeEntity).first()

сессия - полученная сессия. Итак, на ваш вопрос, что делает менеджер контекста с соединениями и сессиями. Все, что вам нужно сделать, это посмотреть, что происходит после выхода, чтобы увидеть, что происходит. В этом случае это просто посмотреть:

try:
    yield session
    session.commit()
except:
    session.rollback()
    raise

Если вы не вызовете никаких исключений, это будет session.commit (), который в соответствии с документацией SQLAlchemy будет «сбрасывать ожидающие изменения и фиксировать текущую транзакцию».

  1. Почему первый рабочий пример кода ведет себя как пример с проблемой без NullPool в качестве пулакласса в методе «connect»?

Аргумент poolclass просто говорит SQLAlchemy, почему использовать подкласс Pool. Однако в случае, когда вы передаете здесь NullPool, вы говорите SQLAlchemy не использовать пул, вы фактически отключаете пул соединений, когда вы передаете NullPool. Из документов: «чтобы отключить пул, установите вместо« poolclass »значение NullPool.» Я не могу сказать наверняка, но использование NullPool, вероятно, способствует вашим max_connection проблемам.

  1. Почему в первом примере я получил только 1 соединение с БД для всех запросов но во втором примере я получил отдельное соединение для каждого запроса? (поправьте меня, если я неправильно понял, проверял "Pgbouncer")

Я не совсем уверен. Я думаю, что это связано с тем, как в первом примере вы используете менеджер контекста, поэтому все в блоке with будет использовать генератор session. Во втором примере вы создали функцию, которая инициализирует новый Session и возвращает его, поэтому вы не получите генератор. Я также думаю, что это связано с вашим NullPool использованием, которое предотвращает пул соединений. С NullPool каждое выполнение запроса получает соединение самостоятельно.

  1. Каковы лучшие практики для открытия и закрытия соединений (и / или работы с сеансом), когда вы используете SQLAlchemy и PostgreSQL DB для нескольких экземпляры скрипта (или отдельных потоков в скрипте), который слушает запросы и должен иметь отдельный сеанс для каждого из них? (Я имею в виду сырой SQLAlchemy не Flask-SQLAlchemy или что-то вроде этого)

См. Раздел Безопасен ли сеанс для потока? для этого, но вам нужно использовать подход "ничего не передавать" к вашему параллелизму. Так что в вашем случае вам нужен каждый экземпляр скрипта, чтобы ничего не делить между собой.

Возможно, вы захотите проверить Работа с двигателями и соединениями , я не думаю, что возиться с сессиями - это то, где вы хотите быть, если вы работаете над параллелизмом. Там есть больше информации о NullPool и параллелизме:

Для многопроцессорного приложения, использующего системный вызов os.fork, или, например, многопроцессорный модуль Python, он обычно требуется, чтобы отдельный Engine использовался для каждого дочернего процесса. это потому что Engine поддерживает ссылку на пул соединений, который в конечном итоге ссылается на соединения DBAPI - они, как правило, не портативный через границы процесса. Двигатель, который не настроен использовать пул (что достигается с помощью NullPool) не есть это требование.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...