MySQL OperationalError при запуске сервера Flask (Apache) в течение нескольких дней

У меня есть сервер Flask под Apache, который я использую в качестве API-интерфейса для приложения, и когда сервер работает в течение 2 - 3 дней, он внезапно перестает работать и поднимает OperationalError: MySQL Connection not available.

Ошибка всегда выполняется с помощью метода login, потому что она открывается первым при открытии приложения (но все методы следуют одному и тому же шаблону).

Это метод login:

@app.route(LOGIN_API_URL, methods=['POST'])
def login():
    if (request.method == 'POST'):
        cursor = connection.cursor(buffered=True, dictionary=True)
        cursor.execute('select * from users where username = %s', (request.form['username'],))
        user = cursor.fetchone()
        if user is None or user['password'] != str(request.form['password']):
            abort(403)
        else:
            cursor.execute('update users set last_login = (%s) where user_id = %s', str(int(round(time.time() * 1000))), user['user_id'],)
            utils.safe_commit(connection, cursor)
            return utils.sanitize_response({'status':200, 'message':'Logged in'})

Оба safe_commit и sanitize_response следует:

def sanitize_response(response, is_array=False):
    if response is None:
        return '[]' if is_array else '{}'
    else:
        return jsonify(response)

def safe_commit(connection, cursor):
    try:
        connection.commit()
    except:
        connection.rollback()
    finally:
        cursor.close()

Сначала я думал, что проблема происходит, потому что я не использовал buffered=True в курсоре, который вызывает метод fetchone. Но я добавил этот параметр после прочтения этого.

Это мой файл wsgi:

#!/usr/bin/python
import sys
sys.path.append("/var/www/protestr/")
from protestr import app as application

Это мой файл sites-available conf (я хотел сказать, что я пробовал множество комбинаций параметров threads и processes, и эта комбинация является той, которая поддерживает работу сервера в наибольшей степени время, обычно 2 - 3 дня):

<VirtualHost *:80>
    ServerName protestr.tk
    DocumentRoot /var/www/protestr/

    WSGIDaemonProcess protestr user=www-data group=www-data processes=2 threads=25
    WSGIScriptAlias / /var/www/protestr/protestr.wsgi

    <Directory /var/www/protestr>
        WSGIProcessGroup protestr
        WSGIApplicationGroup %{GLOBAL}
        Require all granted
    </Directory>
</VirtualHost>

Это содержимое файла error.log:

[Fri May 12 03:34:14.967624 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727] [2017-05-12 03:34:14,963] ERROR in app: Exception on /api/v1/users/login [POST]
[Fri May 12 03:34:14.967812 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727] Traceback (most recent call last):
[Fri May 12 03:34:14.967861 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]   File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1982, in wsgi_app
[Fri May 12 03:34:14.967900 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]     response = self.full_dispatch_request()
[Fri May 12 03:34:14.967937 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]   File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1614, in full_dispatch_request
[Fri May 12 03:34:14.967973 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]     rv = self.handle_user_exception(e)
[Fri May 12 03:34:14.968007 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]   File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1517, in handle_user_exception
[Fri May 12 03:34:14.968043 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]     reraise(exc_type, exc_value, tb)
[Fri May 12 03:34:14.968076 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]   File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1612, in full_dispatch_request
[Fri May 12 03:34:14.968111 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]     rv = self.dispatch_request()
[Fri May 12 03:34:14.968144 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]   File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1598, in dispatch_request
[Fri May 12 03:34:14.968179 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]     return self.view_functions[rule.endpoint](**req.view_args)
[Fri May 12 03:34:14.968251 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]   File "/var/www/protestr/protestr.py", line 89, in login
[Fri May 12 03:34:14.968290 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]     cursor = connection.cursor(buffered=True, dictionary=True)
[Fri May 12 03:34:14.968326 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]   File "/usr/local/lib/python2.7/dist-packages/mysql/connector/connection.py", line 809, in cursor
[Fri May 12 03:34:14.968363 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727]     raise errors.OperationalError("MySQL Connection not available.")
[Fri May 12 03:34:14.968399 2017] [wsgi:error] [pid 18673:tid 2849002544] [remote 192.168.1.139:25727] OperationalError: MySQL Connection not available.

ДОПОЛНИТЕЛЬНАЯ ИНФОРМАЦИЯ:

Я запускаю Apache/2.4.10 под armbian (Debian) в банановой Pi.

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


EDIT: Я добавил также cursor.close(), прежде чем выбросить ошибку 403 в метод login. Но это не актуально, поскольку я единственный, кто регистрируется в приложении, и я всегда ввожу правильные учетные данные.

РЕДАКТИРОВАТЬ 2: Как сказал мне @stamaimer, если я добавлю connection.ping(), прежде чем получить какой-либо курсор, он будет работать хорошо, но этот подход кажется мне взломанным способом и не знаю, является ли это хорошим решением или даже почему сервер MySQL отключает соединение.

Ответы

Ответ 1

Основываясь на @9000s answer, ping должен работать нормально при вызове с параметром reconnect=True, см. соответствующий код. Это вызовет ping, и в случае неудачного ping попытайтесь подключиться к базе данных.

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

Как уже упоминалось, это может быть результатом нескольких источников, поэтому, возможно, вы можете использовать следующий список, чтобы вести поиск:

  • Существуют ли другие сообщения об ошибках в журнале, прежде чем вы отправили сообщение?
  • Вы подключаетесь к MySQL через TCP/IP или через сокет? Возможно, попробуйте другой вариант, если он работает, а другой - нет, это может помочь выявить проблему.
  • Увеличьте max_timeout до большого значения, просто чтобы убедиться, что он имеет эффект.
  • Вы также можете увеличить max_allowed_packet, поскольку это помогло в других случаях.
  • Проверьте журналы сервера MySQL на наличие возможных проблем с потерянными соединениями.
  • Также проверьте syslog операционной системы, потому что MySQL может быть убит из-за низкой памяти, который может быть исправлен установив лучшие значения в config.

Надеемся, что это поможет вам найти основную причину.

Ответ 2

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

В приведенном ниже коде предполагается использование retry; при необходимости вы можете использовать другую логику повтора.

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

def reconnect_on_failure(func):
    @retry(OperationalError, delay=0.25, backoff=1.5, max_delay=5)
    @wraps(func)
    def reconnecting_func(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except OperationalError as e:
            if 'connect' in e.msg.lower():
                force_reconnection_somehow()  # Look at your connection pool API.
                raise  # We want to retry on it
            raise Exception('Unhandled MySQL error', e)  # Will not retry.
    return reconnecting_func


@reconnect_on_failure
def something(...):
    connection = get_connecton_somehow()  # Look at the framework API.
    # A transaction implicitly begins with the first statement executed.
    cursor = connection.cursor()
    result = cursor.execute(...)  # do stuff
    connection.commit()

Вместо чрезмерно широкого Exception вы можете использовать более узкий класс, например. для вашего приложения; идея заключается в том, что повышение чего-либо, кроме OperationalError, не приведет к запуску повторных попыток, и оно немедленно поднимет Exception, чтобы сообщить о проблеме.

ИЗМЕНИТЬ из Grender: Я добавил @wraps decorator, чтобы избежать AssertionError как показано здесь.