Jak mogę profilować aplikację SQLAlchemy?

Czy ktoś ma doświadczenie w profilowaniu aplikacji Python/SQLAlchemy? A jaki jest najlepszy sposób na znalezienie wąskich gardeł i wad projektowych?

Mamy aplikację Pythona, w której warstwa bazy danych jest obsługiwana przez SQLAlchemy. Aplikacja wykorzystuje projekt wsadowy, więc wiele zapytań do bazy danych odbywa się sekwencyjnie i w ograniczonym czasie. Obecnie trwa to nieco zbyt długo, więc potrzebna jest pewna optymalizacja. Nie używamy funkcji ORM, a bazą danych jest PostgreSQL.

Author: goxe, 2009-07-23

3 answers

Czasami zwykłe logowanie SQL (włączone przez moduł logowania Pythona lub przez argument echo=True na create_engine()) może dać ci wyobrażenie, jak długo to trwa. Na przykład, jeśli zalogujesz coś zaraz po operacji SQL, zobaczysz coś takiego w swoim dzienniku:

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] 

Jeśli zalogowałeś się myapp.somemessage zaraz po operacji, wiesz, że ukończenie części SQL zajęło 334ms.

Logowanie SQL pokaże również, czy są wydawane dziesiątki/setki zapytań, które może być lepiej zorganizowany w znacznie mniej zapytań za pomocą łączy. Podczas korzystania z SQLAlchemy ORM, funkcja "eager loading" jest udostępniana częściowo (contains_eager()) lub w pełni (eagerload(), eagerload_all()) automatyzuj tę czynność, ale bez ORM oznacza to po prostu użycie łączników, aby wyniki w wielu tabelach mogły być ładowane w jednym zestawie wyników zamiast mnożenia liczb zapytań w miarę dodawania większej głębokości (np. r + r*r2 + r*r2*r3...)

Jeśli logowanie ujawni, że poszczególne zapytania trwają zbyt długo, potrzebujesz Podział czasu spędzonego w bazie danych na przetwarzaniu zapytania, wysyłaniu wyników przez sieć, obsługiwaniu przez DBAPI i wreszcie odbieraniu przez SQLAlchemy ' s result set i/lub warstwę ORM. Każdy z tych etapów może prezentować własne indywidualne wąskie gardła, w zależności od specyfiki.

W tym celu musisz użyć profilowania, takiego jak cProfile lub hotshot. Oto dekorator, którego używam:

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']

Aby profilować fragment kodu, umieść go w funkcji z dekorator:

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

Wyjście z profilowania może być użyte, aby dać wyobrażenie, gdzie spędzany jest czas. Jeśli na przykład widzisz cały czas spędzony w cursor.execute(), jest to niskopoziomowe wywołanie DBAPI do bazy danych i oznacza to, że Twoje zapytanie powinno zostać zoptymalizowane poprzez dodanie indeksów lub restrukturyzację zapytania i/lub podstawowego schematu. Do tego zadania polecam użycie pgadmin wraz z jego graficznym narzędziem wyjaśniającym, aby zobaczyć, jaki rodzaj pracy wykonuje zapytanie.

Jeśli Zobacz wiele tysięcy wywołań związanych z pobieraniem wierszy, może to oznaczać, że Twoje zapytanie zwraca więcej wierszy niż oczekiwano - produkt kartezjański w wyniku niekompletnego połączenia może spowodować ten problem. Kolejnym problemem jest czas spędzony na obsłudze typów-Typ SQLAlchemy, taki jak Unicode, wykona kodowanie/dekodowanie łańcuchów znaków na parametrach bind i kolumnach wynikowych, które mogą nie być potrzebne we wszystkich przypadkach.

Wyjście profilu może być trochę zniechęcające, ale po pewnej praktyce są bardzo łatwy do odczytania. Kiedyś na liście mailingowej był ktoś, kto twierdził, że powolność, a po tym, jak opublikował wyniki profilu, byłem w stanie wykazać, że problemy z prędkością były spowodowane opóźnieniem sieci - czasem spędzonym w kursorze.execute () jak i wszystkie metody Pythona były bardzo szybkie, podczas gdy większość czasu poświęcano na socket.receive ().

Jeśli czujesz się ambitny, jest też bardziej zaangażowany przykład profilowania SQLAlchemy w testach jednostkowych SQLAlchemy, jeśli poke around http://www.sqlalchemy.org/trac/browser/sqlalchemy/trunk/test/aaa_profiling . Tam, mamy testy z dekoratorami, które zapewniają maksymalną liczbę wywołań metod używanych do poszczególnych operacji, tak że jeśli coś nieefektywnego zostanie sprawdzone, testy to ujawnią(ważne jest, aby pamiętać ,że w Pythonie wywołania funkcji mają najwyższy narzut z każdej operacji, a liczba wywołań jest częściej niż Nie prawie proporcjonalna do czasu spędzonego). Na uwagę zasługują "zoomark" testuje, które wykorzystują fantazyjny schemat "przechwytywania SQL", który odcina narzut DBAPI z równania-chociaż ta technika nie jest tak naprawdę konieczna do profilowania odmian ogrodowych.

 66
Author: zzzeek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-07-24 03:54:46

Na SQLAlchemy wiki

Z kilkoma drobnymi modyfikacjami,

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))
    )

Wyjście to coś w stylu:

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

Następnie, jeśli znajdziesz dziwnie wolne zapytanie, możesz wziąć łańcuch zapytania, sformatować w parametrach (można to zrobić za pomocą operatora % string-formatowanie, przynajmniej dla psycopg2), przedrostek "EXPLAIN ANALYZE" i wrzucić wyjście planu zapytania do http://explain.depesz.com/ (znalezione przez to dobry artykuł o wydajności PostgreSQL )

 41
Author: dbr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-08 09:06:54

Odniosłem pewien sukces w użyciu cprofile i patrząc na wyniki w runsnakerun. To przynajmniej powiedział mi, jakie funkcje i połączenia, gdzie trwa długo i czy baza danych była problemem. Dokumentacja jest TUTAJ . Potrzebujesz wxpython. Prezentacja na it jest dobry, aby zacząć.
Its as easy as

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

Then

Python runsnake.py wyjście.profil

Jeśli chcesz zoptymalizować swoje zapytania będziesz potrzebował postgrsql profilowanie .

Warto również włączyć logowanie, aby rejestrować zapytania, ale nie ma do tego parsera, o którym wiem, Aby uzyskać długo działające zapytania (i nie będzie to przydatne dla jednoczesnych żądań).

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

I upewnienie się, że Twoje polecenie create engine ma echo = True.

Kiedy to zrobiłem, to był mój kod, który był głównym problemem, więc rzecz cprofile pomogła.

 3
Author: David Raznick,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:01