Ошибка при использовании pymysql в колбе

Я использую клиент pymysql для подключения к mysql в моем API фляги, все работает нормально в течение нескольких дней (около 1-2 дней) после этого внезапно он начинает выдавать эту ошибку

Traceback (most recent call last):
  File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 1039, in _write_bytes
    self._sock.sendall(data)
TimeoutError: [Errno 110] Connection timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "Main.py", line 194, in post
    result={'resultCode':100,'resultDescription':'SUCCESS','result':self.getStudentATData(studentId,args['chapterId'])}
  File "Main.py", line 176, in getStudentATData
    cur.execute("my query")
  File "/usr/local/lib/python3.4/dist-packages/pymysql/cursors.py", line 166, in execute
    result = self._query(query)
  File "/usr/local/lib/python3.4/dist-packages/pymysql/cursors.py", line 322, in _query
    conn.query(q)
  File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 855, in query
    self._execute_command(COMMAND.COM_QUERY, sql)
  File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 1092, in _execute_command
    self._write_bytes(packet)
  File "/usr/local/lib/python3.4/dist-packages/pymysql/connections.py", line 1044, in _write_bytes
    "MySQL server has gone away (%r)" % (e,))
pymysql.err.OperationalError: (2006, "MySQL server has gone away (TimeoutError(110, 'Connection timed out'))")

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

def connect(self):
        #db connect here
    def cursor(self):
        try:
            cursor = self.conn.cursor()
        except Exception as e:
            print(e)
            self.connect()
            cursor = self.conn.cursor()
        return cursor

И потребляя его как DB (). Cursor ()


person Bhargav    schedule 08.12.2017    source источник
comment
Подключитесь к базе данных один раз в коде, а не внутри исключения.   -  person Rick James    schedule 27.12.2017
comment
разве это не то, что я сделал? Пытаюсь получить курсор, и если не получается, то переподключение   -  person Bhargav    schedule 27.12.2017
comment
какой сервер вы используете для запуска своего флеш-приложения?   -  person ffeast    schedule 29.12.2017


Ответы (4)


Как я вижу, у вас есть два варианта:

  • Создавайте новое соединение для каждого запроса, а затем закрывайте его. Нравится:

    def db_execute(query):
         conn = MySQLdb.connect(*)
         cur = conn.cursor()
         cur.execute(query)
         res = cur.fetchall()
         cur.close()
         conn.close()
         return res
    
  • Лучше использовать пул подключений, например SqlAlchemy.pool с аргументом pool_pre_ping. и настраиваемая функция подключения.
person nndii    schedule 27.12.2017
comment
закрытие соединения помогло, но я беспокоюсь о производительности, поэтому пока не назначаю награду. - person Bhargav; 02.01.2018

Прежде всего, вам нужно решить, хотите ли вы поддерживать постоянное соединение с MySQL. Последний работает лучше, но требует небольшого обслуживания.

По умолчанию wait_timeout в MySQL составляет 8 часов. . Когда соединение простаивает дольше wait_timeout, оно закрывается. Когда сервер MySQL перезагружается, он также закрывает все установленные соединения. Таким образом, если вы используете постоянное соединение, вам необходимо проверить перед использованием соединения, живо ли оно (а если нет, переподключиться). Если вы используете соединение по запросу, вам не нужно поддерживать состояние соединения, потому что соединение всегда новое.

Подключение по запросу

Непостоянное соединение с базой данных имеет очевидные накладные расходы на открытие соединения, квитирование и т. Д. (Как для сервера базы данных, так и для клиента) на каждый входящий HTTP-запрос.

Вот цитата из официального руководства Flask относительно подключений к базе данных:

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

Однако обратите внимание, что контекст приложения инициализируется для каждого запроса (что отчасти завуалировано соображениями эффективности и жаргоном Flask). Таким образом, это все еще очень неэффективно. Однако это должно решить вашу проблему. Вот вырезанный фрагмент того, что он предлагает применительно к pymysql:

import pymysql
from flask import Flask, g, request    

app = Flask(__name__)    

def connect_db():
    return pymysql.connect(
        user = 'guest', password = '', database = 'sakila', 
        autocommit = True, charset = 'utf8mb4', 
        cursorclass = pymysql.cursors.DictCursor)

def get_db():
    '''Opens a new database connection per request.'''        
    if not hasattr(g, 'db'):
        g.db = connect_db()
    return g.db    

@app.teardown_appcontext
def close_db(error):
    '''Closes the database connection at the end of request.'''    
    if hasattr(g, 'db'):
        g.db.close()    

@app.route('/')
def hello_world():
    city = request.args.get('city')

    cursor = get_db().cursor()
    cursor.execute('SELECT city_id FROM city WHERE city = %s', city)
    row = cursor.fetchone()

    if row:
      return 'City "{}" is #{:d}'.format(city, row['city_id'])
    else:
      return 'City "{}" not found'.format(city)

Постоянное соединение

Для постоянного подключения к базе данных есть два основных варианта. Либо у вас есть пул подключений, либо сопоставьте подключения с рабочими процессами. Поскольку обычно приложения Flask WSGI обслуживаются многопоточными серверами с фиксированным числом потоков (например, uWSGI), отображение потоков проще и эффективнее.

Есть пакет DBUtils, который реализует оба, и _ 5_ для подключений с отображением потоков.

Одно важное предостережение при поддержании постоянного соединения - транзакции. API для повторного подключения: ping. Это безопасно для автоматической фиксации отдельных операторов, но это может мешать между транзакциями (немного подробнее здесь). DBUtils позаботится об этом и должен повторно подключаться только к dbapi.OperationalError и dbapi.InternalError (по умолчанию, под контролем failures к инициализатору PersistentDB), инициированных вне транзакции.

Вот как будет выглядеть приведенный выше фрагмент с PersistentDB.

import pymysql
from flask import Flask, g, request
from DBUtils.PersistentDB import PersistentDB    

app = Flask(__name__)    

def connect_db():
    return PersistentDB(
        creator = pymysql, # the rest keyword arguments belong to pymysql
        user = 'guest', password = '', database = 'sakila', 
        autocommit = True, charset = 'utf8mb4', 
        cursorclass = pymysql.cursors.DictCursor)

def get_db():
    '''Opens a new database connection per app.'''

    if not hasattr(app, 'db'):
        app.db = connect_db()
    return app.db.connection()    

@app.route('/')
def hello_world():
    city = request.args.get('city')

    cursor = get_db().cursor()
    cursor.execute('SELECT city_id FROM city WHERE city = %s', city)
    row = cursor.fetchone()

    if row:
      return 'City "{}" is #{:d}'.format(city, row['city_id'])
    else:
      return 'City "{}" not found'.format(city)

Микро-тест

Чтобы дать небольшое представление о том, какое влияние на производительность выражается в цифрах, вот микротест.

Я побежал:

  • uwsgi --http :5000 --wsgi-file app_persistent.py --callable app --master --processes 1 --threads 16
  • uwsgi --http :5000 --wsgi-file app_per_req.py --callable app --master --processes 1 --threads 16

И протестировали их под нагрузкой с параллелизмом 1, 4, 8, 16 через:

siege -b -t 15s -c 16 http://localhost:5000/?city=london

Наблюдения (для моей локальной конфигурации):

  1. Постоянное соединение на ~ 30% быстрее,
  2. При параллелизме 4 и выше рабочий процесс uWSGI достигает пика загрузки ЦП более чем на 100% (pymysql должен анализировать протокол MySQL на чистом Python, что является узким местом),
  3. При параллелизме 16 загрузка ЦП mysqld составляет ~ 55% для каждого запроса и ~ 45% для постоянного соединения.
person saaj    schedule 02.01.2018

Я не верю, что это проблема Flask / pymysql, поскольку это симптом ваших конфигураций тайм-аута MySQL. Я предполагаю, что это какой-то экземпляр облачной базы данных?

Посмотри на это:

https://dba.stackexchange.com/questions/1558/how-long-is-too-long-for-mysql-connections-to-sleep

И я бы разместил там ваш вопрос с подробностями о вашей настройке, и вы, возможно, сможете получить ответ о конфигурации.

Решением Python было бы использовать что-то вроде sqlalchemy & flask-sqlalchemy, а затем установить переменную конфигурации SQLALCHEMY_POOL_RECYCLE = 3600 для повторного использования соединений через час (или любое другое значение, которое вы пожелаете). В качестве альтернативы, если вы не хотите добавлять такой объем в свой проект, вы можете реализовать функцию «таймера» подключения, чтобы самостоятельно перезапустить подключение в фоновом режиме:

from datetime import datetime, timedelta

class MyConnectionPool(object)
    """Class that returns a database connection <= 1 hour old"""
    refresh_time = timedelta(hours=1)

    def __init__(self, host, user, pass):
        self.host = host
        self.user = user
        self.pass = pass

        self.db = self._get_connection()

    @property
    def connection(self):

        if self.refresh <= datetime.now():
            self.db = self._get_connection()

        return self.db

    def _get_connection(self):
        self.refresh = datetime.now() + self.refresh_time
        return pymysql.connect(
            host=self.host,
            user=self.user,
            passwd=self.pass
        )
person abigperson    schedule 10.12.2017
comment
Это не сработало, я все еще сталкиваюсь с проблемами. я отредактирую вопрос с измененным кодом - person Bhargav; 19.12.2017

Вы пробовали выполнить пинг в дБ, и если это не удается, переподключение перед каждым вызовом? Другая вещь, которую я обнаружил с flask, заключалась в том, что если бы я не закрывал соединение после каждого вызова, я бы получал такие ситуации.

Извините за отсутствие подробностей, но я набираю это на телефоне и пролистываю весь ваш код :-)

class MyDatabase():
    def __init__(self, host, user, passwd, db, charset):
        self.host = host
        self.usr = user
        self.passwd = passwd
        self.db = db
        self.curclass = pymysql.cursors.DictCursor
        self.charset = charset
        self.connection = pymysql.connect(host=self.host, user=self.usr, passwd=self.passwd, db=self.db,
                                      cursorclass=self.curclass, charset=self.charset)


    def get_keywords(self):
        self.connection.connect()
        cur = self.connection.cursor()
        sql = """
    SELECT * FROM input_keywords
    """
        rows = None
        try:
            cur.execute(sql)
            rows = cur.fetchall()
        except Exception as e:
            print(e)
            self.connection.rollback()
        finally:
            cur.close()
            self.connection.commit()
            self.connection.close()
        return rows

Затем это позволяет Flask создавать соединение для каждого запроса и закрывать его в конце.

Таким образом, любой метод, который я вызываю, использует этот шаблон. Это также позволяет выполнять несколько запросов и т. Д. (Это делают веб-сайты)

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

Это очень просто, и, опять же, вы можете объединить его с ping () и позволить ему создать новое соединение и т. Д.

person Alex Hellier    schedule 26.12.2017
comment
Вы можете проверить правку, пожалуйста? Это то, что я пробовал, я пытался переподключиться и получить курсор - это ошибка - person Bhargav; 26.12.2017
comment
А вы хотите закрыть соединение или курсор? - person Bhargav; 26.12.2017
comment
Похоже, вы открываете соединение, оставляете его открытым, и его время истекает, и MySQL затем мешает вам подключиться. Я обновлю свой ответ системой, которую использую с Flask - person Alex Hellier; 27.12.2017