52 votes

Comment puis-je profiler une application utilisant SQLAlchemy?

Quelqu'un at-il de l'expérience dans le profilage d'une application Python / SQLAlchemy? Et quel est le meilleur moyen de détecter les goulots d'étranglement et les défauts de conception?

Nous avons une application Python où la couche de base de données est gérée par SQLAlchemy. L'application utilise une conception par lots, de sorte qu'un grand nombre de demandes de base de données sont effectuées de manière séquentielle et dans un laps de temps limité. Cela prend actuellement un peu trop de temps à exécuter, donc une optimisation est nécessaire. Nous n'utilisons pas la fonctionnalité ORM et la base de données est PostgreSQL.

74voto

zzzeek Points 22617

Parfois, juste en plaine SQL (enregistrement activé par du python module de journalisation ou par l'intermédiaire de l' echo=True argument sur create_engine()) peut vous donner une idée de combien de temps les choses sont en train de prendre. Par exemple, si vous vous connectez quelque chose de bien après une opération SQL, vous devriez voir quelque chose comme ceci dans votre journal:

17:37:48,325 INFO  [sqlalchemy.engine.base.Engine.0x...048c] SELECT ...
17:37:48,326 INFO  [sqlalchemy.engine.base.Engine.0x...048c] {<params>}
17:37:48,660 DEBUG [myapp.somemessage]

si vous vous êtes connecté myapp.somemessage immédiatement après l'opération, vous savez qu'il a pris 334ms pour compléter le SQL part des choses.

La journalisation SQL illustre également si des dizaines/centaines de requêtes sont émises qui pourrait être mieux organisé dans beaucoup moins de requêtes via les jointures. Lors de l'utilisation de la SQLAlchemy ORM, le "désireux de chargement" fonctionnalité est fournie partiellement (contains_eager()) ou totalement (eagerload(), eagerload_all()) automatiser cette activité, mais sans l'ORM il signifie juste à utiliser les jointures de sorte que les résultats provenant de plusieurs tables peuvent être chargés dans un jeu de résultats, au lieu de multiplier les nombres de requêtes que plus la profondeur est ajoutée (c - r + r*r2 + r*r2*r3 ...)

Si l'enregistrement révèle que les requêtes individuelles sont trop long, vous auriez besoin d'une ventilation de la façon dont beaucoup de temps a été passé à l'intérieur de la base de données du traitement de la requête, de l'envoi des résultats sur le réseau, géré par l'DBAPI, et enfin d'être reçu par SQLAlchemy de l'ensemble de résultats et/ou de la couche ORM. Chacune de ces étapes peuvent présenter leur propre goulets d'étranglement, en fonction de spécificités.

Pour cela vous devez utiliser le profilage, comme cProfile ou hotshot. Voici un décorateur, j'utilise:

import cProfile as profiler
import gc, pstats, time

def profile(fn):
    def wrapper(*args, **kw):
        elapsed, stat_loader, result = _profile("foo.txt", fn, *args, **kw)
        stats = stat_loader()
        stats.sort_stats('cumulative')
        stats.print_stats()
        # uncomment this to see who's calling what
        # stats.print_callers()
        return result
    return wrapper

def _profile(filename, fn, *args, **kw):
    load_stats = lambda: pstats.Stats(filename)
    gc.collect()

    began = time.time()
    profiler.runctx('result = fn(*args, **kw)', globals(), locals(),
                    filename=filename)
    ended = time.time()

    return ended - began, load_stats, locals()['result']

Le profil d'une section de code, placez-le dans une fonction avec le décorateur:

@profile
def go():
    return Session.query(FooClass).filter(FooClass.somevalue==8).all()
myfoos = go()

La sortie de profilage peuvent être utilisés pour donner une idée de l'endroit où le temps passé. Si par exemple vous voyez tout le temps dépensé dans cursor.execute(), c'est le faible niveau DBAPI appel à la base de données, et cela signifie que votre requête doit être optimisé, soit par l'ajout d'index ou de la restructuration de la requête et/ou le schéma sous-jacent. Pour cette tâche, je vous conseille d'utiliser pgadmin avec son interface graphique EXPLIQUER utilitaire pour voir quel genre de travail que la requête est en train de faire.

Si vous voyez plusieurs milliers d'appels liés à la récupération des lignes, cela peut signifier que votre requête est de retour plus de lignes que prévu - un produit cartésien comme un résultat d'une incomplète joindre peuvent causer ce problème. Encore un autre problème est le temps passé au sein du type de manipulation - une SQLAlchemy, type Unicode effectuera chaîne de codage/décodage sur lier des paramètres et des colonnes de résultat, qui peut ne pas être nécessaire dans tous les cas.

La sortie d'un profil peut être un peu intimidant, mais après un peu de pratique, ils sont très faciles à lire. Il était une fois quelqu'un sur la liste de diffusion revendiquant la lenteur, et après lui avoir publier les résultats de profil, j'ai été en mesure de démontrer que la vitesse de problèmes étaient dus à la latence du réseau, - le temps passé à l'intérieur du curseur.execute() ainsi que toutes les méthodes Python a été très rapide, alors que la majorité du temps a été passé sur la socket.recevoir().

Si vous vous sentez ambitieux, il est aussi plus impliqués exemple de SQLAlchemy de profilage dans la SQLAlchemy des tests unitaires, si vous faites le tour d' http://www.sqlalchemy.org/trac/browser/sqlalchemy/trunk/test/aaa_profiling . Là, nous avons des tests à l'aide de décorateurs qui imposent un nombre maximum d'appels de méthode utilisée pour des opérations particulières, de sorte que si quelque chose d'inefficace obtient vérifié dans, les tests révèlent qu'elle (il est important de noter que, dans Python, les appels de fonction ont le plus de frais généraux de fonctionnement et le nombre d'appels est le plus souvent pas à peu près proportionnelle au temps passé). Notez le "zoomark" tests qui utilisent une fantaisie "SQL de capture du régime, qui coupe la surcharge de la DBAPI à partir de l'équation - bien que cette technique n'est pas vraiment nécessaire pour le jardin-variété de profilage.

51voto

dbr Points 66401

Il est extrêmement utile de profilage recette sur le wiki SQLAlchemy

Avec un couple de modifications mineures,

from sqlalchemy import event
from sqlalchemy.engine import Engine
import time
import logging

logging.basicConfig()
logger = logging.getLogger("myapp.sqltime")
logger.setLevel(logging.DEBUG)

@event.listens_for(Engine, "before_cursor_execute")
def before_cursor_execute(conn, cursor, statement, 
                        parameters, context, executemany):
    context._query_start_time = time.time()
    logger.debug("Start Query:\n%s" % statement)
    # Modification for StackOverflow answer:
    # Show parameters, which might be too verbose, depending on usage..
    logger.debug("Parameters:\n%r" % (parameters,))


@event.listens_for(Engine, "after_cursor_execute")
def after_cursor_execute(conn, cursor, statement, 
                        parameters, context, executemany):
    total = time.time() - context._query_start_time
    logger.debug("Query Complete!")

    # Modification for StackOverflow: times in milliseconds
    logger.debug("Total Time: %.02fms" % (total*1000))

if __name__ == '__main__':
    from sqlalchemy import *

    engine = create_engine('sqlite://')

    m1 = MetaData(engine)
    t1 = Table("sometable", m1, 
            Column("id", Integer, primary_key=True),
            Column("data", String(255), nullable=False),
        )

    conn = engine.connect()
    m1.create_all(conn)

    conn.execute(
        t1.insert(), 
        [{"data":"entry %d" % x} for x in xrange(100000)]
    )

    conn.execute(
        t1.select().where(t1.c.data.between("entry 25", "entry 7800")).order_by(desc(t1.c.data))
    )

La sortie est quelque chose comme:

DEBUG:myapp.sqltime:Start Query:
SELECT sometable.id, sometable.data 
FROM sometable 
WHERE sometable.data BETWEEN ? AND ? ORDER BY sometable.data DESC
DEBUG:myapp.sqltime:Parameters:
('entry 25', 'entry 7800')
DEBUG:myapp.sqltime:Query Complete!
DEBUG:myapp.sqltime:Total Time: 410.46ms

Alors si vous trouvez une étrange requête lente, vous pourriez prendre la chaîne de requête, format dans les paramètres (peut être fait à l' % chaîne de mise en forme de l'opérateur, pour psycopg2 au moins), faites-le précéder d'EXPLIQUER "ANALYSER" et d'enfoncer le plan de requête de sortie en http://explain.depesz.com/ (trouvé via ce bon article sur les performances de PostgreSQL)

3voto

David Raznick Points 5074

J'ai eu quelques succès dans l'utilisation de cprofile et de regarder les résultats dans runsnakerun. Cela, au moins, m'a dit ce que les fonctions et les appels de prendre un long temps, et si la base de données de la question. La documentation est ici. Vous avez besoin de wxpython. La présentation sur ce qui est bon pour vous obtenir a commencé.
Son aussi facile que de

import cProfile
command = """foo.run()"""
cProfile.runctx( command, globals(), locals(), filename="output.profile" )

Alors

python runsnake.py de sortie.profil

Si vous cherchez à optimiser vos requêtes que vous aurez besoin postgrsql de profilage.

Il est également intéressant de mettre l'enregistrement sur pour enregistrer les requêtes, mais il n'y a pas d'analyseur syntaxique pour ce que je connais, pour obtenir le long de l'exécution de requêtes (et il ne sera pas utile pour les demandes simultanées).

sqlhandler = logging.FileHandler("sql.log")
sqllogger = logging.getLogger('sqlalchemy.engine')
sqllogger.setLevel(logging.info)
sqllogger.addHandler(sqlhandler)

et faire en sorte de créer de moteur de déclaration a echo = True.

Quand je l'ai fait, c'était en fait mon code qui a été le problème principal, de sorte que le cprofile chose a aidé.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X