但凡在命令行里跟 pip 打过交道的人,大概率都见过这一幕:光标闪了几下,屏幕上冒出一串Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ReadTimeoutError(...)',接着那个数字从 4 一路减到 0,最后甩给你一个ERROR: Could not install packages due to an EnvironmentError。你换了个包名重试,还是 Retrying;你加了sudo,还是 Retrying;你甚至重启了终端,它依然在原地转圈。Python 包安装这件事本身不难,难的是这条看似笼统的报错把五六种截然不同的病因压缩成了一句"重试中",让人无从下手。
这篇内容想做的事情很具体:把pip输出的Retrying当成一份诊断报告来读,逐层拆出它的真实含义,然后把工程实践中最高频的五种成因——链路太远导致超时、TLS 握手失败、依赖回溯引发的反复下载、本地缓存与临时目录故障、多源配置互相打架——一条条摊开讲清楚。每一种成因都会配上判断依据、可直接抄的命令、参数取值背后的计算逻辑,以及我在真实排查里踩过的坑。无论你是刚装完 Python 环境、连pip install requests都跑不通的新手,还是维护着几十台构建机、天天跟私有源打交道的老手,应该都能从里面找到对得上号的那一段。
1. 先把这行 Retrying 读明白,别急着改配置
1.1 一行报错里其实藏了三个关键信息
Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None))这一串括号,是底层 HTTP 库 urllib3 的Retry对象在打印自己的状态。total=4不是"总共重试 4 次",而是"还剩 4 次机会"——pip 默认--retries 5,所以第一次失败时打印的是 4,随后 3、2、1、0,跑到 0 才真正放弃。connect=None、read=None表示连接阶段和读取阶段的重试额度没有单独设置,跟随 total 走。
真正有价值的是后半句after connection broken by 'XXX'。这里的XXX才是病灶:NewConnectionError说明 TCP 三次握手就没成功,问题在 DNS 解析、端口连通性或出口策略;ReadTimeoutError说明连上了但数据没在规定时间内传完,问题在带宽、源站响应速度或包体积;SSLError说明 TLS 握手阶段证书校验没过;ProtocolError或ChunkedEncodingError往往意味着中间有设备改写了响应内容。同样是"Retrying",这四个词指向的排查方向完全不同,先认准它,比盲目换源有用得多。
1.2 pip 为什么要重试五次,这个设计是好是坏
公共镜像站背后通常是 CDN,回源瞬间偶发 502、503 是常态;家庭宽带和移动网络在晚高峰丢包率也会抬头。pip 设定重试的初衷就是吞掉这类瞬时故障——绝大多数情况下,第一次失败、第二次成功,用户根本感知不到。而且重试间隔不是固定值,底层做了退避处理,越往后等得越久,避免短时间内对着已经过载的源站猛砸请求。
问题出在放大效应上。默认单次超时 15 秒,重试 5 次,理论上最坏情况光一个包就要耗掉一分半以上,而这还只是一个包的其中一个文件。当你在装一个带二三十个依赖的项目时,每个依赖都卡一遍,十分钟就没了,日志里全是滚动刷屏的 Retrying,看起来像是"卡死",实际上它在很认真地执行你并没意识到的重试策略。所以重试机制本身没错,错的是在链路本身就不通的情况下,还让它傻等。
注意:看到 Retrying 时先别关终端。让它跑完,拿到最后那行
ERROR的完整文本,那里面往往写着真正的原因,比如No matching distribution found或Could not find a version that satisfies the requirement。
1.3 三个动作,三十秒分清故障方向
我习惯用三个动作快速定位。第一个是ping或者curl -I打一下源站地址,看 DNS 解析是否正常、TCP 能否建立、TLS 握手需要多久。第二个是pip install -v 包名,-v会把每个请求的完整 URL、状态码、耗时都打出来,能清楚看到它卡在第几个包、卡在连接阶段还是下载阶段。第三个是拿一个体积很小的纯 Python 包做对照实验,比如装个只有几十 KB 的小工具包,小包秒过而大包卡死,基本就是带宽或超时问题;小包也卡,那就是链路或配置问题。
这三步的意义在于把"玄学"变成"可观测"。很多人一上来就改 index-url、加 trusted-host、关掉缓存,一次改五处,结果好了也不知道是哪一处起的作用,下次出问题照样抓瞎。一次只动一个变量,是这个流程里最容易被忽略、却最省时间的纪律。
2. 原因一:源站链路太远,默认的 15 秒根本不够用
2.1 超时参数是怎么算出来的
pip 的两个默认值值得记住:--timeout 15,单位秒,指的是单次 socket 读操作的超时;--retries 5,指的是失败后的重试次数。这里的 15 秒不是"整个下载必须 15 秒完成",而是"连续 15 秒没有收到新数据就判超时"。所以一个 80 MB 的 wheel 包,如果在跨境链路上平均速度只有 200 KB/s,传输过程本身是连续的,通常不会触发超时;真正触发超时的是链路中途出现几秒到十几秒的静默——TCP 拥塞窗口收缩、丢包重传、CDN 节点切换,都会造成这种静默。
反过来说,如果你的网络对某些地址存在明显的限速或丢包,静默时间很容易越过 15 秒这条线,于是第一次超时、重试、再超时,五次跑完正好一分半。这时候把--timeout提到 60,往往第一次就能过去;把--retries从 5 提到 10 反而没用,因为你面对的不是瞬时抖动,而是持续的链路质量问题。这两个参数的调整逻辑是不同的,别混着改。
2.2 换源是性价比最高的一步,但要换对
包索引源的地理位置直接决定了往返时延。默认源在境外,国内访问时单次请求的往返时延就可能上百毫秒,一个包含几十个依赖的项目会发出成百上千次请求,累积起来非常可观。换成地理位置更近的公共镜像站,通常能把整体安装时间压掉一大截,这个收益比调任何参数都明显。
配置建议写进配置文件而不是每次命令行敲,这样团队里所有人、所有项目都统一。用pip config set global.index-url <地址>写入用户级配置,用pip config list检查是否生效。配置文件的位置随平台而异,排查"为什么我改了不生效"时经常要回到这张表:
| 平台 | 用户级配置路径 | 全局配置路径 |
|---|---|---|
| Linux | ~/.config/pip/pip.conf(旧版也读~/.pip/pip.conf) | /etc/pip.conf |
| macOS | ~/Library/Application Support/pip/pip.conf | /Library/Application Support/pip/pip.conf |
| Windows | %APPDATA%\pip\pip.ini | %PROGRAMDATA%\pip\pip.ini |
手写配置的话,内容大致长这样,注意index-url要带/simple后缀,少了这个后缀会出现 404 而不是超时,症状不一样:
[global] index-url = https://<镜像站域名>/simple timeout = 60 retries = 32.3 调参的三个实操心得
第一,--timeout不是越大越好。设成 300 秒,一旦真的连不上,你要等五分钟才看到报错,排查效率极低。我的习惯是日常 60,排查期临时降到 20,快速拿到失败信号。
第二,--retries建议往下调而不是往上调。既然已经换到近距离源站,瞬时故障本来就少,3 次足够;在 CI 环境里我甚至设成 1,让失败尽快暴露出来,由流水线层面统一重试整个任务,比在 pip 内部重试更容易观测。
第三,如果某个包特别大,可以用pip download先把它单独拉下来,再用pip install --no-index --find-links=<目录>从本地装。这一步的意义是把"下载"和"安装"解耦,下载失败时你能专心处理网络,安装失败时专心处理依赖和编译,两者不混在一起。
3. 原因二:TLS 握手失败,被误当成网络不好
3.1 证书校验链路到底在哪一环断的
现代 pip 走 HTTPS,握手时客户端要验证服务器证书是否由受信任的根证书签发、域名是否匹配、是否在有效期内。pip 依赖的certifi包内置了一份根证书清单,同时也会读取系统证书库。校验链路断在三个地方最常见:一是企业内网出口对流量做了检查,用自签证书重新签发了连接,而这份自签根证书没有装进客户端的信任库;二是操作系统根证书过期或缺失,多见于精简版容器镜像;三是certifi版本太老,不认识新签发机构。
这三种情况的报错文本高度一致,都是SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate'))。注意它出现在after connection broken by后面时,外层依然会打印 Retrying,于是很多人第一反应是"网络又抽风了",反复重试却永远过不去——因为证书问题是确定性的,重试一百次结果都一样。
3.2 三种处理方式,风险等级完全不同
| 方式 | 命令写法 | 适用场景 | 风险 |
|---|---|---|---|
| 添加信任主机 | --trusted-host <域名> | 明确知道该域名使用自签证书,内网环境 | 中,跳过该域名的证书校验 |
| 指定 CA 证书包 | --cert <路径> | 内网有统一根证书,已导出 PEM | 低,推荐 |
| 全局关闭校验 | --trusted-host配合所有源 | 临时救急 | 高,不建议长期使用 |
我的建议是把内网根证书导出成 PEM,通过pip config set global.cert <路径>写进配置,一次配置长期有效,而且没有降低安全等级。--trusted-host更省事,但它只对域名生效,一旦源站换了 CDN 域名就得重新加,属于治标。至于彻底关闭校验,除了在一次性容器里做验证实验,我不建议在任何长期环境里使用。
3.3 用两条命令把证书问题钉死
先做外部验证,绕开 Python 直接用底层工具试探:curl -Iv https://<源站域名>/simple/,观察输出的握手信息,如果 curl 也报证书错误,问题就已经确认在 TLS 层,跟 pip 无关。再看 Python 侧的判断依据:python -c "import ssl; print(ssl.get_default_verify_paths())",输出的 cafile 和 capath 就是当前解释器会去加载的证书位置,如果这两个路径指向的文件不存在或为空,说明环境本身缺证书库。
还有一种很隐蔽的情况:公司内网机器上同时装了多个 Python 版本,其中一个是独立打包的运行时,它用的是自带的certifi,和你系统里升级过的证书库不是同一份。表现就是命令行curl正常、浏览器正常、唯独某个 Python 环境里 pip 一直报证书错误。判断方法很直接:python -m pip -V看到底调的是哪个解释器,再python -c "import certifi; print(certifi.where())"看它加载的是哪一份证书文件。
提示:如果报错里出现
self-signed certificate in certificate chain而不是unable to get local issuer certificate,基本可以确定中间存在自签证书,方向应从"补根证书"入手,而不是从"网络重试"入手。
4. 原因三:依赖回溯,让重试看起来像死循环
4.1 新解析器为什么会反复下载
pip 从 20.3 版本开始引入新依赖解析器,思路从"按顺序逐个装、遇到冲突再说"改成了"全局求解":先把所有候选版本枚举出来,检查约束是否同时满足,不满足就回退,换一个候选版本再算。这个机制的好处是能提前发现冲突,坏处是回溯过程中会反复去索引里查询、甚至反复下载不同版本的 wheel 来做元数据判断。
于是你会看到一种很迷惑的现象:日志里同一个包名出现了好几次,版本号还不一样,中间夹杂着几条 Retrying,整体耗时极长,但最后要么成功、要么报ResolutionImpossible。很多人误以为这是网络问题,于是一直调超时、一直换源,实际上网络根本没毛病,是解析器在"试算"。
4.2 怎么认出这是回溯而不是网络故障
有三个特征可以帮助区分。第一,看重复出现的包名——网络问题的日志里同一个 URL 会原样重试,而回溯的日志里包名相同但版本号不同。第二,看有没有Collecting和Downloading交替刷屏,回溯时这两行会来回跳。第三,看结束语,回溯型最终往往报ERROR: Cannot install ... because these package versions have conflicting dependencies,并附上一大段依赖树说明;网络型则报连接错误。
还有一个更实用的判断手段:把要装的包用pip download --no-deps单独拉一次。如果单个包下载飞快,一装完整依赖就卡住,那几乎必然是依赖关系复杂导致的回溯,跟链路无关。
4.3 让解析器少走弯路的四个手法
第一条也是最有效的一条:固定版本。pip install "包名==1.2.3"比pip install 包名让搜索空间小一个量级,回溯次数自然大幅下降。第二条是用约束文件,把整个项目的版本约束写进constraints.txt,用-c constraints.txt引入,比在命令行里堆一长串版本号好维护得多:
python -m pip install -r requirements.txt -c constraints.txt第三条是优先用二进制包,--only-binary=:all:可以彻底避免源码包进入构建流程,减少"下载源码、尝试构建、构建失败、换版本"这种循环。第四条是分层安装,先把底层公共依赖(比如数值计算、网络请求这类基础库)单独装好,再装业务包,解析器面对已经满足的约束就不需要再回溯。
注意:网上不少老教程会让你加
--use-deprecated=legacy-resolver回到旧解析器,这个开关在较新版本的 pip 里已经被移除,照抄会直接报"no such option"。与其退回去,不如把版本钉死。
5. 原因四:缓存和临时目录的隐性故障
5.1 缓存目录里到底存了什么
pip 有两层缓存:一层是 HTTP 响应缓存,把下载过的索引页面和 wheel 文件存下来,下次装同一个版本直接命中,不再发请求;另一层是本地构建的 wheel 缓存,把源码包编译产物存下来复用。前者默认位于用户缓存目录,Linux 是~/.cache/pip,macOS 是~/Library/Caches/pip,Windows 是%LocalAppData%\pip\Cache。
缓存本身极少出问题,但它有三种典型故障模式:下载过程中断导致留下半截文件、磁盘写入失败导致缓存条目损坏、以及缓存目录的属主和当前用户不一致(常见于用sudo装过一次包之后)。这时候 pip 会反复尝试读取这个坏条目、反复失败,日志里同样会出现 Retrying 或者 hash 校验不匹配的提示。
5.2 什么时候该清,什么时候根本不用管
判别方法很简单:如果错误文本里出现HashMismatch、Cached response ... is invalid、File ... does not exist,优先怀疑缓存。清理命令是pip cache purge,只想看不想清的话用pip cache list和pip cache dir先确认目录位置。
但绝大多数 Retrying 跟缓存没关系,别把它当万能药。真要用,我更倾向用--no-cache-dir做单次绕过而不是整体清除,这样能立刻判断问题是否出在缓存上,同时保留其他包的缓存命中率:
python -m pip install --no-cache-dir -v 包名5.3 权限、磁盘和临时目录,三个容易被忽略的点
用sudo pip install装过一次包之后,用户缓存目录里会留下一批 root 属主的文件,之后普通用户跑 pip 就没法覆盖写入,表现出来就是莫名其妙的失败。正确做法是永远用python -m pip install --user装到用户目录,或者干脆用虚拟环境隔离,从根上避免权限混战。
临时目录也值得留意。pip 在构建源码包时会在临时目录展开文件,如果/tmp挂载时带了noexec选项,或者剩余空间不足,构建就会失败,而报错信息有时会误导到网络层。用df -h /tmp看一眼剩余空间,用mount | grep /tmp确认挂载选项,两秒钟的事。
Windows 上还有一个特有坑:路径长度限制。用户目录名较长、包名较长、再加上临时目录层级,很容易超过 260 字符,导致解压失败。这时候把临时目录改到短路径下(设置TMP和TEMP环境变量指向C:\tmp)通常立刻见效。
6. 原因五:多源配置打架,请求打到了不该去的地方
6.1 配置优先级,以及那些你忘了自己写过的文件
pip 的配置来源优先级从高到低依次是:命令行参数、环境变量、用户级配置文件、全局配置文件。问题在于,这些配置可能散落在好几处,而且很多是几个月前为了应急随手加的,自己早忘了。表现就是命令行明明写了-i,实际请求却跑到另一个地址去,于是必然超时。
排查第一步永远是pip config list,把所有生效的配置打到屏幕上;较新版本还有pip config debug,会列出它读取了哪些文件、每个文件贡献了什么键值。我遇到过最典型的情况是:用户配置文件里写了企业私有源,而这个源只能在办公网访问,回家之后就一直是 Retrying,本人却因为不记得写过这条配置而反复怀疑网络。
6.2 extra-index-url 的两个陷阱
extra-index-url允许在主索引之外追加备用索引,方便同时用公共源和私有源。它有两个坑。第一个是依赖混淆:同名包在两个源上都存在时,pip 不保证从哪个源取,可能装到非预期的版本。第二个是性能拖累:追加的源如果不可达,pip 会为每个包都去试一次,失败后再回退到主源,整体耗时成倍增加,日志里就是大量 Retrying。
对私有源,更稳妥的做法是显式指定而不依赖回退顺序,或者用--index-url指向私有源、把公共依赖预先装好;在 CI 里我倾向配置成一次只用一个源,用不同的命令区分场景,不让 pip 自己去猜。
6.3 一条命令把配置问题照出原形
最省时间的做法是拿完整 URL 直接试,绕开所有配置:pip install -v --index-url https://<源站域名>/simple --no-cache-dir 包名。如果这条命令顺畅通关,说明包和网络都没问题,锅一定在配置文件里;如果这条也卡,那就是源站本身或链路的问题。这个"最小复现命令"的思路在排查里价值极高,因为它把所有变量都收到了最小集合。
另外提醒一句:环境变量也会影响索引地址。有些 CI 平台的构建镜像里预置了索引地址的环境变量,你在本地怎么调都对,一上流水线就失败。判断方法是在流水线里先打一行pip config list,把生效配置打印到构建日志里,问题一目了然。
7. 一套能落地的排查流程与速查表
7.1 五步定位法,正常三分钟内出结论
第一步,看错误尾部的关键字。NewConnectionError走连通性方向,ReadTimeoutError走超时与带宽方向,SSLError走证书方向,ResolutionImpossible走依赖方向。这一步不花时间,但能砍掉八成无效操作。
第二步,跑pip config list,确认生效的索引地址和超时参数,排除"配置里藏着一条老规则"这种低级问题。
第三步,用-v重跑一次,看清楚卡在第几个包、卡在哪个阶段。日志里Starting new HTTPS connection之后如果长时间没动静,是连接层;出现了Downloading之后卡住,是传输层。
第四步,做对照实验。小包能过、大包不过就是带宽或超时;不加缓存能过就是缓存问题;指定完整 URL 能过就是配置问题;虚拟环境里能过、全局环境不过就是权限或版本冲突。
第五步,才动手改。一次只改一个参数,改完立刻复测,把结论记下来。这一步的纪律性比技术本身更重要,因为 pip 的失败往往是多因素叠加,一次改五处等于放弃了归因能力。
7.2 常见问题速查表
| 日志特征 | 最可能原因 | 首选处理 |
|---|---|---|
NewConnectionError反复出现 | DNS 或端口不通、出口策略拦截 | 换源,检查网络出口 |
ReadTimeoutError且集中在大包 | 链路抖动、默认超时偏小 | --timeout 60,换近源 |
SSLCertVerificationError | 根证书缺失或自签证书 | 指定 CA 证书或--trusted-host |
同名包多个版本反复Collecting | 依赖回溯 | 固定版本、约束文件、只装二进制 |
HashMismatch或缓存相关报错 | 缓存条目损坏 | --no-cache-dir或pip cache purge |
| 装了私有源但公共包也变慢 | 索引回退拖累 | 单次只用一个源 |
| 报错在构建阶段而非下载阶段 | 缺少编译工具链或临时目录受限 | 改用二进制包,检查临时目录 |
| 只在 CI 里失败、本地正常 | 环境变量或镜像预置配置 | 流水线里打印pip config list |
7.3 几条踩坑攒下来的经验
第一,养成用python -m pip而不是裸pip的习惯。裸pip在多 Python 环境里可能指向另一个解释器,你在这边改配置、在那边装包,怎么都排查不通。python -m pip -V输出里带了解释器路径和 pip 版本,一眼就能看出有没有错位。
第二,把"先复现、再最小化、后修改"当成固定动作。我见过太多情况是一顿操作猛如虎,最后发现只是包名里多打了一个下划线。
第三,日志要留。CI 里给 pip 加-v并把输出存档,出问题时翻历史日志比现场重跑快得多,尤其是那种"偶发一次、重跑就好"的故障,没有日志就永远查不清。
第四,别迷信"关掉校验就好了"。SSL 报错被--trusted-host掩盖之后,后续所有包都会走不校验的通道,短期省事,长期埋雷。
第五,私有源和公共源尽量分环境配置,用虚拟环境或容器把不同项目的依赖边界划清楚。混淆的源头一旦消失,Retrying 出现的概率会断崖式下降。
我个人的体会是,pip这条 Retrying 从来不是"运气不好",它只是把底层的一次具体失败用一种很粗糙的方式呈现出来。真正省时间的做法不是记住更多命令,而是建立一条稳定的判断链条:先认错误类型,再验证配置,再做对照实验,最后才动参数。这条链条跑顺之后,绝大多数安装问题在打开第二个终端之前就已经有答案了。