Python程序在调用子进程时一直挂起
pip的测试套件使用子进程来运行集成测试。最近有个提交(PR)去掉了一些旧的兼容性代码,具体来说是把一个叫b()
的函数替换成了直接使用b""
这种字面量。但是,这似乎导致了某个子进程调用永远挂起。更糟糕的是,这个问题只在Python 3.3(可能是3.3.5)上出现,而且在Travis以外的地方很难重现。
相关的提交链接:
- https://github.com/pypa/pip/pull/1901
- https://github.com/pypa/pip/pull/1900
- https://github.com/pypa/pip/pull/1878
其他提交也出现了类似的问题,不过它们在不同版本的Python和不同的测试用例上失败。这些提交包括:
今天有个用户在IRC上向我报告了类似的问题,他们说在Ubuntu 14.04上用deadsnakes的Python 3.3可以重现这个问题(但在OSX上不行),而不仅仅是在Travis上。我收到了他们的重现步骤:
$ git clone git@github.com:xavfernandez/pip.git
$ cd pip
$ git checkout debug_stuck
$ pip install pytest==2.5.2 scripttest==1.3 virtualenv==1.11.6 mock==1.0.1 pretend==1.0.8 setuptools==4.0
$ # The below should pass just fine
$ py.test -k test_env_vars_override_config_file -v -s
$ # Now edit pip/req/req_set.py and remove method remove_me_to_block or change its content to print('KO') or pass
$ # The below should hang forever
$ py.test -k test_env_vars_override_config_file -v -s
在上面的例子中,remove_me_to_block
方法并没有被调用,仅仅是它的存在就足以让测试不挂起,而如果它不存在(或者内容被改动),就会导致测试永远挂起。
大部分调试工作都是围绕这个提交进行的(https://github.com/pypa/pip/pull/1901)。我逐个提交进行测试,直到这个特定的提交被应用时,测试才失败 - https://github.com/dstufft/pip/commit/d296df620916b4cd2379d9fab988cbc088e28fe0。具体来说,使用b'\r\n'
或(entry + endline).encode("utf-8")
的改动会触发这个问题,但这两者都不在pip install -vvv INITools
这个命令的执行路径中,而这个命令正是导致失败的。
在试图追踪问题时,我发现如果我把至少一次对"something".encode("utf8")
的调用替换成(lambda: "something")().encode("utf8")
,问题就解决了。
在调试过程中,我还发现我尝试的各种方法(比如添加打印语句、无操作的atexit
函数、使用trollious进行异步子进程)只是将问题从特定的测试用例和Python版本转移到了不同的测试用例和Python版本。
我知道subprocess
模块如果直接读写subprocess.Popen().stdout/stderr/stdin
可能会导致死锁。但是这段代码使用的是communicate()
方法,理论上应该能避免这些问题。问题出现在wait()
调用中,communicate()
在这里导致进程永远挂起,等待pip进程退出。
其他信息:
- 这个问题很难捉摸,我尝试了各种方法让它消失或转移,但都没有效果。
- 我追踪了pip内部的执行,直到调用
sys.exit()
的代码路径结束。 - 把
sys.exit()
替换成os._exit()
可以解决所有挂起问题,但我不想这样做,因为那样会跳过Python解释器的清理工作。 - 没有其他线程在运行(通过
threading.enumerate
验证过)。 - 我尝试过的某些修改,即使没有使用
subprocess.PIPE
作为stdout/stderr/stdin,也会导致挂起,但其他组合在不使用这些的情况下又不会挂起(或者会转移到不同的测试用例/Python版本)。 - 这似乎与时间无关,任何特定的提交在受影响的测试用例/Python上要么100%失败,要么0%失败。
- 很多时候,修改的代码甚至没有被pip子进程的特定代码路径执行,但仅仅是修改的存在似乎就会导致问题。
- 我尝试禁用字节码生成,使用
PYTHONDONTWRITEBYTECODE=1
,在某些组合中有影响,但在其他组合中没有效果。 - 子进程调用的命令并不是每次都挂起(测试套件中会发出类似的命令),但对于特定的提交,它总是在同一个地方挂起。
- 到目前为止,我完全无法在测试套件以外的地方重现这个问题,但我不确定这是否与此有关。
我完全不知道是什么导致了这个问题。
更新 #1
使用faulthandler.dump_traceback_later()
我得到了这个结果:
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
[ Duplicate Lines Snipped ]
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/requests/packages/urllib3/response.py", line 287 in closed
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
[ Duplicate Lines Snipped ]
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/requests/packages/urllib3/response.py", line 287 in closed
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
[ Duplicate Lines Snipped ]
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
[ Duplicate Lines Snipped ]
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
[ Duplicate Lines Snipped ]
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
[ Duplicate Lines Snipped ]
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
[ Duplicate Lines Snipped ]
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
Timeout (0:00:05)!
Current thread 0x00007f417bd92740:
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
[ Duplicate Lines Snipped ]
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/requests/packages/urllib3/response.py", line 285 in closed
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
[ Duplicate Lines Snipped ]
File "/tmp/pytest-10/test_env_vars_override_config_file0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 24 in __getattr__
这让我觉得问题可能与垃圾回收和urllib3有关?pip._vendor.cachecontrol.filewrapper
中的Filewrapper
是用来包装一个urllib3响应对象(它是io.IOBase
的子类),这样我们可以在调用read()
方法时将每次读取的结果存储在一个缓冲区中,并返回它,然后在文件完全读取后运行一个回调,将缓冲区的内容存储到缓存中。这可能与垃圾回收有某种交互?
更新 #2
如果我在Filewrapper类中添加一个def __del__(self): pass
方法,那么在我尝试的情况下,一切都能正常工作。我测试过这不是因为我恰好定义了一个方法(有时“修复”它),我把它改成def __del2__(self): pass
,结果又开始失败。我不确定为什么这样做有效,而一个无操作的__del__
方法似乎不是最优的。
更新 #3
添加import gc; gc.set_debug(gc.DEBUG_UNCOLLECTABLE)
在执行挂起的pip命令时向stderr打印了两次信息,它们是:
gc: uncollectable <CallbackFileWrapper 0x7f66385c1cd0>
gc: uncollectable <dict 0x7f663821d5a8>
gc: uncollectable <functools.partial 0x7f663831de10>
gc: uncollectable <_io.BytesIO 0x7f663804dd50>
gc: uncollectable <method 0x7f6638219170>
gc: uncollectable <tuple 0x7f663852bd40>
gc: uncollectable <HTTPResponse 0x7f663831c7d0>
gc: uncollectable <PreparedRequest 0x7f66385c1a90>
gc: uncollectable <dict 0x7f663852cb48>
gc: uncollectable <dict 0x7f6637fdcab8>
gc: uncollectable <HTTPHeaderDict 0x7f663831cb90>
gc: uncollectable <CaseInsensitiveDict 0x7f66385c1ad0>
gc: uncollectable <dict 0x7f6638218ab8>
gc: uncollectable <RequestsCookieJar 0x7f663805d7d0>
gc: uncollectable <dict 0x7f66382140e0>
gc: uncollectable <dict 0x7f6638218680>
gc: uncollectable <list 0x7f6638218e18>
gc: uncollectable <dict 0x7f6637f14878>
gc: uncollectable <dict 0x7f663852c5a8>
gc: uncollectable <dict 0x7f663852cb00>
gc: uncollectable <method 0x7f6638219d88>
gc: uncollectable <DefaultCookiePolicy 0x7f663805d590>
gc: uncollectable <list 0x7f6637f14518>
gc: uncollectable <list 0x7f6637f285a8>
gc: uncollectable <list 0x7f6637f144d0>
gc: uncollectable <list 0x7f6637f14ab8>
gc: uncollectable <list 0x7f6637f28098>
gc: uncollectable <list 0x7f6637f14c20>
gc: uncollectable <list 0x7f6637f145a8>
gc: uncollectable <list 0x7f6637f14440>
gc: uncollectable <list 0x7f663852c560>
gc: uncollectable <list 0x7f6637f26170>
gc: uncollectable <list 0x7f663821e4d0>
gc: uncollectable <list 0x7f6637f2d050>
gc: uncollectable <list 0x7f6637f14fc8>
gc: uncollectable <list 0x7f6637f142d8>
gc: uncollectable <list 0x7f663821d050>
gc: uncollectable <list 0x7f6637f14128>
gc: uncollectable <tuple 0x7f6637fa8d40>
gc: uncollectable <tuple 0x7f66382189e0>
gc: uncollectable <tuple 0x7f66382183f8>
gc: uncollectable <tuple 0x7f663866cc68>
gc: uncollectable <tuple 0x7f6637f1e710>
gc: uncollectable <tuple 0x7f6637fc77a0>
gc: uncollectable <tuple 0x7f6637f289e0>
gc: uncollectable <tuple 0x7f6637f19f80>
gc: uncollectable <tuple 0x7f6638534d40>
gc: uncollectable <tuple 0x7f6637f259e0>
gc: uncollectable <tuple 0x7f6637f1c7a0>
gc: uncollectable <tuple 0x7f6637fc8c20>
gc: uncollectable <tuple 0x7f6638603878>
gc: uncollectable <tuple 0x7f6637f23440>
gc: uncollectable <tuple 0x7f663852c248>
gc: uncollectable <tuple 0x7f6637f2a0e0>
gc: uncollectable <tuple 0x7f66386a6ea8>
gc: uncollectable <tuple 0x7f663852f9e0>
gc: uncollectable <tuple 0x7f6637f28560>
然后是
gc: uncollectable <CallbackFileWrapper 0x7f66385c1350>
gc: uncollectable <dict 0x7f6638c33320>
gc: uncollectable <HTTPResponse 0x7f66385c1590>
gc: uncollectable <functools.partial 0x7f6637f03ec0>
gc: uncollectable <_io.BytesIO 0x7f663804d600>
gc: uncollectable <dict 0x7f6637f1f680>
gc: uncollectable <method 0x7f663902d3b0>
gc: uncollectable <tuple 0x7f663852be18>
gc: uncollectable <HTTPMessage 0x7f66385c1c10>
gc: uncollectable <HTTPResponse 0x7f66385c1450>
gc: uncollectable <PreparedRequest 0x7f66385cac50>
gc: uncollectable <dict 0x7f6637f2f248>
gc: uncollectable <dict 0x7f6637f28b90>
gc: uncollectable <dict 0x7f6637f1e638>
gc: uncollectable <list 0x7f6637f26cb0>
gc: uncollectable <list 0x7f6637f2f638>
gc: uncollectable <HTTPHeaderDict 0x7f66385c1f90>
gc: uncollectable <CaseInsensitiveDict 0x7f66385b2890>
gc: uncollectable <dict 0x7f6638bd9200>
gc: uncollectable <RequestsCookieJar 0x7f663805da50>
gc: uncollectable <dict 0x7f6637f28a28>
gc: uncollectable <dict 0x7f663853aa28>
gc: uncollectable <list 0x7f663853a6c8>
gc: uncollectable <dict 0x7f6638ede5f0>
gc: uncollectable <dict 0x7f6637f285f0>
gc: uncollectable <dict 0x7f663853a4d0>
gc: uncollectable <method 0x7f663911f710>
gc: uncollectable <DefaultCookiePolicy 0x7f663805d210>
gc: uncollectable <list 0x7f6637f28ab8>
gc: uncollectable <list 0x7f6638215050>
gc: uncollectable <list 0x7f663853a200>
gc: uncollectable <list 0x7f6638215a28>
gc: uncollectable <list 0x7f663853a950>
gc: uncollectable <list 0x7f663853a998>
gc: uncollectable <list 0x7f6637f21638>
gc: uncollectable <list 0x7f6637f0cd40>
gc: uncollectable <list 0x7f663853ac68>
gc: uncollectable <list 0x7f6637f22c68>
gc: uncollectable <list 0x7f663853a170>
gc: uncollectable <list 0x7f6637fa6a28>
gc: uncollectable <list 0x7f66382153b0>
gc: uncollectable <list 0x7f66386a5e60>
gc: uncollectable <list 0x7f663852f2d8>
gc: uncollectable <list 0x7f66386a3320>
[<pip._vendor.cachecontrol.filewrapper.CallbackFileWrapper object at 0x7f66385c1cd0>, <pip._vendor.cachecontrol.filewrapper.CallbackFileWrapper object at 0x7f66385c1350>]
这些信息有用吗?我从来没有使用过这个标志,所以不知道这是否不寻常。
1 个回答
在Python 2中,如果一组对象通过链条连接在一起(也就是形成了一个引用循环),而且至少有一个对象有一个叫做__del__
的方法,那么垃圾回收器就不会删除这些对象。简单来说,如果你有一个引用循环,添加一个__del__()
方法可能只是掩盖了潜在的问题(也就是变相解决了bug)。
根据你更新的第3条信息,看起来你遇到了这样的情况。