Python/SQLAlchemyアプリのプロファイリングの経験がある人はいますか?そして、ボトルネックと設計上の欠陥を見つけるための最良の方法は何ですか?
データベースレイヤーがSQLAlchemyによって処理されるPythonアプリケーションがあります。アプリケーションはバッチ設計を使用するため、多くのデータベース要求が限られた時間内に順次実行されます。現在、少し時間がかかります。実行するには長すぎるため、ある程度の最適化が必要です。ORM機能は使用せず、データベースはPostgreSQLです。
単純なSQLロギング(Pythonのロギングモジュールまたはcreate_engine()
の_echo=True
_引数を介して有効化)だけで、どれくらいの時間がかかっているかがわかる場合があります。たとえば、SQL操作の直後に何かをログに記録すると、ログに次のようなものが表示されます。
_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]
_
操作の直後に_myapp.somemessage
_を記録した場合、SQL部分を完了するのに334ミリ秒かかったことがわかります。
ロギングSQLは、数十/数百のクエリが発行されているかどうかも示します。これは、結合を介してはるかに少ないクエリに整理する方が適切です。 SQLAlchemy ORMを使用する場合、「積極的な読み込み」機能が提供され、このアクティビティを部分的に(contains_eager()
)または完全に(eagerload()
、eagerload_all()
)自動化します。 ORMは、結合を使用することを意味します。これにより、深さが増すにつれてクエリの数を増やすのではなく、複数のテーブルにわたる結果を1つの結果セットにロードできます(つまり、_r + r*r2 + r*r2*r3
_ ...)
ロギングにより、個々のクエリに時間がかかりすぎることが判明した場合は、データベース内でクエリの処理、ネットワーク経由での結果の送信、DBAPIによる処理、そして最終的にSQLAlchemyの結果セットによる受信に費やされた時間の内訳が必要になります。および/またはORMレイヤー。これらの各段階では、詳細に応じて、独自のボトルネックが発生する可能性があります。
そのためには、cProfileやhotshotなどのプロファイリングを使用する必要があります。これが私が使用するデコレータです:
_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']
_
コードのセクションをプロファイリングするには、デコレータを使用して関数に配置します。
_@profile
def go():
return Session.query(FooClass).filter(FooClass.somevalue==8).all()
myfoos = go()
_
プロファイリングの出力を使用して、どこで時間が費やされているかを知ることができます。たとえば、cursor.execute()
内ですべての時間が費やされている場合、それはデータベースへの低レベルのDBAPI呼び出しであり、インデックスを追加するか、クエリや基になるものを再構築することによって、クエリを最適化する必要があることを意味します。スキーマ。そのタスクでは、pgadminとそのグラフィカルなEXPLAINユーティリティを使用して、クエリが実行している作業の種類を確認することをお勧めします。
行のフェッチに関連する何千もの呼び出しが表示される場合は、クエリが予想よりも多くの行を返している可能性があります。不完全な結合の結果としてデカルト積がこの問題を引き起こす可能性があります。さらに別の問題は、型の処理に費やされる時間です。Unicode
などのSQLAlchemy型は、バインドパラメーターと結果列に対して文字列のエンコード/デコードを実行します。これはすべての場合に必要なわけではありません。
プロファイルの出力は少し気が遠くなるかもしれませんが、いくつかの練習の後、それらは非常に読みやすくなります。メーリングリストに速度が遅いと主張する人がいたことがあり、プロファイルの結果を投稿してもらうと、速度の問題がネットワークの遅延(cursor.execute()内で費やされた時間)に起因することを示すことができました= Pythonメソッドは非常に高速でしたが、時間の大部分はsocket.receive()に費やされました。
野心を感じている場合は、SQLAlchemyユニットテスト内のSQLAlchemyプロファイリングのより複雑な例もあります http://www.sqlalchemy.org/trac/browser/sqlalchemy/trunk/test/ aaa_profiling 。そこでは、特定の操作に使用されるメソッド呼び出しの最大数をアサートするデコレータを使用したテストがあるため、非効率的なものがチェックインされた場合、テストによってそれが明らかになります(Pythonでは、関数呼び出しが最も多いことに注意してください)操作のオーバーヘッド、および呼び出しの数は、多くの場合、費やされた時間にほぼ比例します)。注目すべきは、方程式からDBAPIのオーバーヘッドを削減する派手な「SQLキャプチャ」スキームを使用する「ズームマーク」テストです。ただし、この手法は、園芸品種のプロファイリングには実際には必要ありません。
SQLAlchemy wiki には非常に便利なプロファイリングレシピがあります
いくつかの小さな変更を加えて、
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 __== '__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))
)
出力は次のようなものです。
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
次に、奇妙に遅いクエリを見つけた場合は、クエリ文字列を取得し、パラメータでフォーマットし(少なくとも、psycopg2では%
文字列フォーマット演算子を実行できます)、プレフィックスとして「EXPLAINANALYZE」を付けて突き出すことができます。 http://explain.depesz.com/ に出力されたクエリプラン( 経由で見つかりましたPostgreSQLに関するこの良い記事パフォーマンス )
私はcprofileを使用し、runsnakerunで結果を確認することにある程度成功しました。これは少なくとも、どの関数と呼び出しがどこで長い時間がかかるか、そしてデータベースが問題であったかどうかを教えてくれました。ドキュメントは ここ です。 wxpythonが必要です。 プレゼンテーション それはあなたが始めるのに良いです。
それは同じくらい簡単です
import cProfile
command = """foo.run()"""
cProfile.runctx( command, globals(), locals(), filename="output.profile" )
次に
python runsnake.py output.profile
クエリを最適化する場合は、 postgrsqlプロファイリング が必要になります。
クエリを記録するためにログオンすることも価値がありますが、長時間実行されるクエリを取得するために私が知っているパーサーはありません(そして、同時リクエストには役立ちません)。
sqlhandler = logging.FileHandler("sql.log")
sqllogger = logging.getLogger('sqlalchemy.engine')
sqllogger.setLevel(logging.info)
sqllogger.addHandler(sqlhandler)
そして、createengineステートメントがecho = Trueであることを確認します。
私がやったとき、それは実際に私のコードが主な問題だったので、cprofileのものが役に立ちました。
ライブラリsqltap
( https://github.com/inconshreveable/sqltap )を発見しました。 SQLAlchemyによって生成されたSQLクエリの検査とプロファイリングに役立つ適切なスタイルのHTMLページを生成します。
使用例:
profiler = sqltap.start()
run_some_queries()
statistics = profiler.collect()
sqltap.report(statistics, "report.html")
ライブラリは2年間更新されていませんが、今日の初めにアプリケーションでテストしたところ、問題なく機能しているようでした。