Прежде всего, вам нужно решить, хотите ли вы поддерживать постоянное соединение с 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
Наблюдения (для моей локальной конфигурации):
- Постоянное соединение на ~ 30% быстрее,
- При параллелизме 4 и выше рабочий процесс uWSGI достигает пика загрузки ЦП более чем на 100% (
pymysql
должен анализировать протокол MySQL на чистом Python, что является узким местом),
- При параллелизме 16 загрузка ЦП
mysqld
составляет ~ 55% для каждого запроса и ~ 45% для постоянного соединения.
person
saaj
schedule
02.01.2018