Mysql
 sql >> Datenbank >  >> RDS >> Mysql

Fehler bei der Verwendung von pymysql in Flask

Zunächst müssen Sie entscheiden, ob Sie eine dauerhafte Verbindung zu MySQL aufrechterhalten möchten. Letzteres funktioniert besser, benötigt aber ein wenig Wartung.

Standard wait_timeout in MySQL beträgt 8 Stunden. Immer wenn eine Verbindung länger als wait_timeout im Leerlauf ist es ist geschlossen. Wenn der MySQL-Server neu gestartet wird, werden auch alle bestehenden Verbindungen geschlossen. Wenn Sie also eine dauerhafte Verbindung verwenden, müssen Sie vor der Verwendung einer Verbindung prüfen, ob sie aktiv ist (und falls nicht, erneut verbinden). Wenn Sie eine Verbindung auf Anfrage verwenden, müssen Sie den Verbindungsstatus nicht beibehalten, da die Verbindung immer frisch ist.

Verbindung auf Anfrage

Eine nicht persistente Datenbankverbindung hat einen offensichtlichen Overhead für das Öffnen der Verbindung, das Handshaking usw. (sowohl für den Datenbankserver als auch für den Client) für jede eingehende HTTP-Anforderung.

Hier ist ein Zitat aus Flasks offiziellem Tutorial in Bezug auf Datenbankverbindungen :

Beachten Sie jedoch diesen Anwendungskontext wird pro Anfrage initialisiert (was durch Effizienzbedenken und Flasks Jargon verschleiert wird). Und daher ist es immer noch sehr ineffizient. Es sollte jedoch Ihr Problem lösen. Hier ist ein Ausschnitt dessen, was es für die Anwendung auf pymysql vorschlägt :

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)

Ständige Verbindung

Für eine dauerhafte Datenbankverbindung gibt es zwei Hauptoptionen. Entweder haben Sie einen Pool von Verbindungen oder Sie ordnen Verbindungen Worker-Prozessen zu. Da normalerweise Flask WSGI-Anwendungen von Thread-Servern mit einer festen Anzahl von Threads (z. B. uWSGI) bereitgestellt werden, ist Thread-Mapping einfacher und ebenso effizient.

Es gibt ein Paket, DBUtils , das beides implementiert, und PersistentDB für Thread-zugeordnete Verbindungen.

Ein wichtiger Vorbehalt bei der Aufrechterhaltung einer dauerhaften Verbindung sind Transaktionen. Die API für die Wiederverbindung ist ping . Es ist sicher für das automatische Festschreiben einzelner Anweisungen, aber es kann zwischen einer Transaktion stören (ein wenig mehr Details hier ). DBUtils kümmert sich darum und sollte die Verbindung nur bei dbapi.OperationalError wiederherstellen und dbapi.InternalError (standardmäßig gesteuert durch failures zum Initialisierer von PersistentDB ) außerhalb einer Transaktion erhoben.

So sieht das obige Snippet mit PersistentDB aus .

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)

Mikro-Benchmark

Um einen kleinen Hinweis darauf zu geben, welche Auswirkungen die Leistung in Zahlen hat, hier ein Mikro-Benchmark.

Ich lief:

  • 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

Und testete sie mit Parallelität 1, 4, 8, 16 über:

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

Beobachtungen (für meine lokale Konfiguration):

  1. Eine permanente Verbindung ist ca. 30 % schneller
  2. Bei Parallelität 4 und höher erreicht der uWSGI-Arbeitsprozess Spitzenwerte von über 100 % der CPU-Auslastung (pymysql muss das MySQL-Protokoll in reinem Python parsen, was der Engpass ist),
  3. Auf Parallelität 16, mysqld Die CPU-Auslastung von beträgt ca. 55 % pro Anfrage und ca. 45 % bei dauerhafter Verbindung.