リアルタイムのREST APIをPython3 + Bottle/UWSGIを使用して開発しています。コードでレイテンシーが発生していました。アプリでは数百ミリ秒になることもありましたが、これは重要です。
logging
モジュールを使用して、コードの遅い部分を特定し、個々のコードブロックの実行にかかった時間を出力しようとしました。これはコードをプロファイリングする非常に貧弱な方法であることを私は知っていますが、時にはそれは非常にうまく仕事をすることができます。
遅い部分をいくつか特定しましたが、それでも何かが足りませんでした。個々の部分には数十ミリ秒かかるように見えましたが、全体として数百ミリ秒かかることがよくありました。私をほぼ完全に狂わせたいくつかのますます非常識な実験の後、私は次のようになりました:
_t = round(100*time.time())
logging.info('[%s] Foo' % t)
logging.info('[%s] Bar' % t)
_
驚いたことに、それは与えます:
_2014-07-16 23:21:23,531 [140554568353] Foo
2014-07-16 23:21:24,312 [140554568353] Bar
_
これは信じがたいようですが、結果として2つのlogging.info()
呼び出しがあり、何らかの理由で、それらの間にはほぼ800ミリ秒のギャップがあります。誰かが何が起こっているのか教えてもらえますか?複数のinfo()
呼び出しがある場合、レイテンシはAPIメソッド全体で1回だけ表示され、最も頻繁には最初の呼び出し(最初の呼び出しの後)で表示されることに注意してください。私の唯一の仮説はディスクの待ち時間です。同時に実行しているワーカーは数人(ただしそれほど多くはありません!)で、SSDではなく回転ディスクを使用しています。しかし、バッファがあり、OSがディスクフラッシュを非同期的に実行すると思いました。私の仮定は間違っていますか?待ち時間を回避するために、ログ記録を完全に回避する必要がありますか?
[〜#〜]編集[〜#〜]
Vinay Sajipの提案に基づいて、次の初期化コードに切り替えました。
_log_que = queue.Queue(-1)
queue_handler = logging.handlers.QueueHandler(log_que)
log_handler = logging.StreamHandler()
queue_listener = logging.handlers.QueueListener(log_que, log_handler)
queue_listener.start()
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(message)s", handlers=[queue_handler])
_
正常に動作しているように見えますが(queue_listener.start()
にコメントすると、出力はありません)、まったく同じレイテンシが表示されます。どうすればそれが可能かわかりません。通話は非ブロッキングである必要があります。また、各リクエストの最後にgc.collect()
を付けて、問題がガベージコレクターによって引き起こされていないことを確認します。また、一日中ロギングをオフにしようとしました。レイテンシーがなくなったので、それらのソースはlogging
モジュールにあるに違いないと思います...
Hasanが提案したように、非同期ログハンドラーがその方法です。
最近、私はLogbook
を使用してみましたが、これに必要なものがすべて提供されます- ZeroMQHandler および ZeroMQSubscriber
非同期ハンドラー( QueueHandler
および対応するQueueListener
を使用でき、Python 3.2で追加され、 this post )そして、ログイベントのI/O処理を別のスレッドまたはプロセスで実行します。
ロギングハンドラーに依存する場合があります。私の経験はそれです例えばロギングハンドラーとして使用されるPostgreSQLは、速度の点で非常に貧弱な選択です。 FileHandlerを使用すると非常に良い結果が得られる場合がありますが、システムのI/Oが非常に重い場合は、単純なファイルの書き込みでも遅くなる可能性があります。非同期ハンドラーを使用することをお勧めします。ログをUDP経由で専用プロセスに送信します。
まず、エビクションキュー(サークルバッファ)から始めます。これにより、キューハンドラが使用可能なすべてのRAMを消費できないようになります。
class EvictQueue(Queue):
def __init__(self, maxsize):
self.discarded = 0
super().__init__(maxsize)
def put(self, item, block=False, timeout=None):
while True:
try:
super().put(item, block=False)
except queue.Full:
try:
self.get_nowait()
self.discarded += 1
except queue.Empty:
pass
次に、ルートにあるすべてのハンドラーを置き換えます...通常の構成後のものは何でも..。
def speed_up_logs():
rootLogger = logging.getLogger()
log_que = EvictQueue(1000)
queue_handler = logging.handlers.QueueHandler(log_que)
queue_listener = logging.handlers.QueueListener(log_que, *rootLogger.handlers)
queue_listener.start()
rootLogger.handlers = [queue_handler]
効果:
ロギングは非常に高速になります
ドライブに書き込むよりも速くログに記録すると、書き込まれていない古いエントリはサイレントに破棄されます。
1分ごとに破棄されたエントリの数を1つのエントリに記録することで、これを強化するのは良いことかもしれません(破棄されたエントリをゼロに交換します)。