メモリ・リークのデバッグ¶
Scrapyでは、リクエスト、レスポンス、アイテムなどのオブジェクトのライフタイムは有限です。それらは作成され、しばらく使用され、最終的に破棄されます。
これらすべてのオブジェクトの中で、リクエストはおそらく最も長いライフタイムを持つものです。リクエストを処理するまでスケジューラのキューで待機しているためです。 詳細については、 アーキテクチャ概観 を参照してください。
これらのScrapyオブジェクトには(かなり長い)寿命があるため、それらを適切に解放せずにメモリに蓄積してしまい、「メモリ・リーク」と呼ばれるものを引き起こすリスクが常にあります。
メモリリークのデバッグを支援するために、Scrapyは trackref というオブジェクト参照を追跡するための組み込みメカニズムを提供します。また、 Guppy というサードパーティ・ライブラリを使用すると、より高度なメモリ・デバッグが可能になります(詳細は以下を参照)。両方のメカニズムは、Telnetコンソール から使用する必要があります。
メモリ・リークの一般的な原因¶
Scrapy開発者がリクエストで参照されるオブジェクトを渡し(たとえば、 cb_kwargs
または meta
属性またはリクエスト・コールバック関数の使用)、そして、事実上、それらの参照されたオブジェクトの寿命をリクエストの寿命に合わせます。これは、Scrapyプロジェクトでのメモリ・リークの最も一般的な原因であり、初心者にとってデバッグが非常に難しいものです。
大きなプロジェクトでは、スパイダーは通常、異なる人々によって作成され、それらのスパイダーの一部は漏出(leak)する可能性があり、したがって、同時に実行されると他の(ちゃんと書かれた)スパイダーに次々に影響を与え、クロール・プロセス全体に影響を与えます。
(以前に割り当てられた)リソースを適切に解放していない場合、作成したカスタムミドルウェア、パイプライン、または拡張機能からもリークが発生する可能性があります。たとえば、spider_opened
でリソースを割り当てても、 spider_closed
でリソースを解放しないと、プロセスごとに複数のスパイダー を実行している場合に問題が発生する可能性があります。
trackref
を使用したメモリ・リークのデバッグ¶
trackref
は、メモリ・リークの最も一般的なケースをデバッグするためにScrapyが提供するモジュールです。基本的に、すべての、生存中の、リクエスト、レスポンス、アイテム、セレクタ・オブジェクトへの参照を追跡します。
telnetコンソールに入り、 print_live_refs()
のエイリアスである prefs()
関数を使用して、(上記のクラスの)オブジェクトが現在何個生きているかを検査することができます。
telnet localhost 6023
>>> prefs()
Live References
ExampleSpider 1 oldest: 15s ago
HtmlResponse 10 oldest: 1s ago
Selector 2 oldest: 0s ago
FormRequest 878 oldest: 7s ago
ご覧のとおり、このレポートには各クラスの最も古いオブジェクトの「年齢」も表示されます。プロセスごとに複数のスパイダーを実行している場合、最も古い要求または応答を調べることで、どのスパイダーがリークしているかを把握できます。 get_oldest()
関数を使用して(telnetコンソールから)各クラスの最も古いオブジェクトを取得できます。
どのオブジェクトが追跡されますか?¶
trackrefs
によって追跡されるオブジェクトはすべてこれらのクラス(およびそのすべてのサブクラス)からのものです:
scrapy.selector.Selector
実例¶
メモリ・リークの仮定ケースの具体例を見てみましょう。 以下のようなスパイダーがあるとします:
return Request("http://www.somenastyspider.com/product.php?pid=%d" % product_id,
callback=self.parse, cb_kwargs={'referer': response})
この例は、レスポンスの有効期間をリクエストの有効期間と効果的に結び付けるレスポンス参照をリクエスト内に渡しているため、間違いなくメモリ・リークが発生します。
trackref
ツールを使用して、原因を(もちろん、事前に知らないものとして)発見する方法を見てみましょう。
クローラーが数分間実行され、メモリー使用量が大幅に増加したことに気付いたら、telnetコンソールに入り、生存中の参照(Live References)を確認できます:
>>> prefs()
Live References
SomenastySpider 1 oldest: 15s ago
HtmlResponse 3890 oldest: 265s ago
Selector 2 oldest: 0s ago
Request 3878 oldest: 250s ago
レスポンスはリクエストと比較して比較的短い寿命であるはずなので、非常に多くの生存中のレスポンスが存在するという事実(そしてそれらが非常に古いという事実)は間違いなく疑わしいです。レスポンスの数はリクエストの数と似ているため、何らかの形で結び付けられているように見えます。これで、スパイダーのコードを調べて、リークを生成している厄介な当該コード(リクエスト内でレスポンス参照を渡している)を発見できます。
生存中オブジェクトに関する追加情報が役立つ場合があります。
>>> from scrapy.utils.trackref import get_oldest
>>> r = get_oldest('HtmlResponse')
>>> r.url
'http://www.somenastyspider.com/product.php?pid=123'
最も古いオブジェクトを取得する代わりに、すべてのオブジェクトを反復処理する場合は、 scrapy.utils.trackref.iter_all()
関数を使用できます:
>>> from scrapy.utils.trackref import iter_all
>>> [r.url for r in iter_all('HtmlResponse')]
['http://www.somenastyspider.com/product.php?pid=123',
'http://www.somenastyspider.com/product.php?pid=584',
...
スパイダーが多すぎるのか?¶
プロジェクトで並行して実行されるスパイダーが多すぎる場合、 prefs()
の出力は読みにくい場合があります。このため、その関数には、特定のクラス(およびそのすべてのサブクラス)を無視するために使用できる ignore
引数があります。たとえば、以下はスパイダーへの生存中参照を表示しません:
>>> from scrapy.spiders import Spider
>>> prefs(ignore=Spider)
scrapy.utils.trackref モジュール¶
trackref
モジュールで利用可能な関数は以下のとおりです。
-
class
scrapy.utils.trackref.
object_ref
¶ trackref
モジュールを使用して生存中のインスタンスを追跡する場合は、(オブジェクトの代わりに)このクラスから継承します。
-
scrapy.utils.trackref.
print_live_refs
(class_name, ignore=NoneType)¶ クラス名でグループ化された生存中参照のレポートを出力します。
- パラメータ
ignore (class or classes tuple) -- 指定された場合、指定されたクラス(またはクラスのタプル)からのすべてのオブジェクトは無視されます。
-
scrapy.utils.trackref.
get_oldest
(class_name)¶ 指定されたクラス名で生きている最も古いオブジェクトを返すか、見つからない場合は
None
を返します。最初にprint_live_refs()
を使用して、クラス名ごとに追跡されているすべての生存中のオブジェクトのリストを取得します。
-
scrapy.utils.trackref.
iter_all
(class_name)¶ 指定されたクラス名で生きているすべてのオブジェクトのイテレータを返します。見つからない場合は
None
を返します。最初にprint_live_refs()
を使用して、クラス名ごとに追跡されているすべての生存中のオブジェクトのリストを取得します。
Guppyを使ってメモリ・リークをデバッグする¶
`` trackref``は、メモリ・リークを追跡するための非常に便利なメカニズムを提供しますが、メモリリークを引き起こす可能性が高いオブジェクト(リクエスト、レスポンス、アイテム、セレクタ)のみを追跡します。 ただし、他の(多かれ少なかれ不明瞭な)オブジェクトからメモリ・リークが発生する場合があります。そうした場合で、 trackref
を使用してリークを見つけることができない場合、更に別の方法があります。それは Guppy library です。そして、Python3を使用している場合は、 muppyを使ってメモリ・リークをデバッグする を参照してください。
pip
を使用する場合、次のコマンドでGuppyをインストールできます:
pip install guppy
telnetコンソールには、グッピー・ヒープ・オブジェクトにアクセスするための組み込みのショートカット( hpy
)も付属しています。 Guppyを使用して、ヒープで使用可能なすべてのPythonオブジェクトを表示する例を次に示します:
>>> x = hpy.heap()
>>> x.bytype
Partition of a set of 297033 objects. Total size = 52587824 bytes.
Index Count % Size % Cumulative % Type
0 22307 8 16423880 31 16423880 31 dict
1 122285 41 12441544 24 28865424 55 str
2 68346 23 5966696 11 34832120 66 tuple
3 227 0 5836528 11 40668648 77 unicode
4 2461 1 2222272 4 42890920 82 type
5 16870 6 2024400 4 44915320 85 function
6 13949 5 1673880 3 46589200 89 types.CodeType
7 13422 5 1653104 3 48242304 92 list
8 3735 1 1173680 2 49415984 94 _sre.SRE_Pattern
9 1209 0 456936 1 49872920 95 scrapy.http.headers.Headers
<1676 more rows. Type e.g. '_.more' to view.>
ほとんどのスペースが辞書(dict)によって使用されていることがわかります。次に、それらの辞書がどの属性から参照されているかを見たい場合は、以下のようにします:
>>> x.bytype[0].byvia
Partition of a set of 22307 objects. Total size = 16423880 bytes.
Index Count % Size % Cumulative % Referred Via:
0 10982 49 9416336 57 9416336 57 '.__dict__'
1 1820 8 2681504 16 12097840 74 '.__dict__', '.func_globals'
2 3097 14 1122904 7 13220744 80
3 990 4 277200 2 13497944 82 "['cookies']"
4 987 4 276360 2 13774304 84 "['cache']"
5 985 4 275800 2 14050104 86 "['meta']"
6 897 4 251160 2 14301264 87 '[2]'
7 1 0 196888 1 14498152 88 "['moduleDict']", "['modules']"
8 672 3 188160 1 14686312 89 "['cb_kwargs']"
9 27 0 155016 1 14841328 90 '[1]'
<333 more rows. Type e.g. '_.more' to view.>
ご覧のとおり、Guppyモジュールは非常に強力ですが、Python内部についての深い知識も必要です。 グッピーの詳細については、 Guppy documentation を参照してください。
muppyを使ってメモリ・リークをデバッグする¶
Python 3を使用している場合、 Pympler からmuppyを使用できます。
pip
を使用する場合、次のコマンドでmuppyをインストールできます:
pip install Pympler
muppyを使用してヒープ内で使用可能なすべてのPythonオブジェクトを表示する例を次に示します:
>>> from pympler import muppy
>>> all_objects = muppy.get_objects()
>>> len(all_objects)
28667
>>> from pympler import summary
>>> suml = summary.summarize(all_objects)
>>> summary.print_(suml)
types | # objects | total size
==================================== | =========== | ============
<class 'str | 9822 | 1.10 MB
<class 'dict | 1658 | 856.62 KB
<class 'type | 436 | 443.60 KB
<class 'code | 2974 | 419.56 KB
<class '_io.BufferedWriter | 2 | 256.34 KB
<class 'set | 420 | 159.88 KB
<class '_io.BufferedReader | 1 | 128.17 KB
<class 'wrapper_descriptor | 1130 | 88.28 KB
<class 'tuple | 1304 | 86.57 KB
<class 'weakref | 1013 | 79.14 KB
<class 'builtin_function_or_method | 958 | 67.36 KB
<class 'method_descriptor | 865 | 60.82 KB
<class 'abc.ABCMeta | 62 | 59.96 KB
<class 'list | 446 | 58.52 KB
<class 'int | 1425 | 43.20 KB
muppyの詳細については、 muppy documentation を参照してください。
Scrapyではリークしてないのにリークしてるorz¶
Scrapyプロセスのメモリ使用量は増加するだけで、決して減少しないことに気付く場合があります。 残念ながら、Scrapyもプロジェクトもメモリをリークしていなくても、これは起こり得ます。これは、Pythonの(あまりよくない)既知の問題が原因であり、場合によっては解放されたメモリをオペレーティングシステムに返さないことがあります。 この問題の詳細については以下を参照して下さい。
this paper で詳しく説明されている、エヴァン・ジョーンズによって提案された改善点は、Python 2.5に統合されましたが、これは問題を軽減するだけで、完全には修正しません。 当該箇所を引用すると:
残念ながら、このパッチは、オブジェクトが割り当てられていない場合にのみアリーナを解放できます。これは、断片化が大きな問題であることを意味します。アプリケーションには、すべてのアリーナに散らばった数メガバイトの空きメモリがありますが、どれも解放できません。これは、すべてのメモリ・アロケータで発生する問題です。これを解決する唯一の方法は、メモリ内のオブジェクトを移動できる圧縮ガベージコレクタに移動することです。これには、Pythonインタープリターの大幅な変更が必要になります。
メモリ消費を適切に保つために、ジョブをいくつかの小さなジョブに分割するか、 永続ジョブ・キュー を有効にして、スパイダーを時々停止/開始できます。