1. 先搞明白 rosdep update 究竟在下载什么
rosdep update卡住然后抛 timeout,几乎是每个在国内网络环境下装机的人都要过的一关。我第一次遇到的时候,是在一台刚装完 ROS 的工控机上,rosdep init顺利跑完,rosdep update跑到 reading in sources list data 那一行就一动不动,等了四五分钟甩出一串urlopen error timed out。当时以为是 rosdep 装坏了,重装了三遍,后来才明白 —— 这不是软件问题,是它在按顺序去抓一堆远程文件,而其中某几个地址在当前网络下慢到超时。
先把话说清楚:rosdep是 ROS 体系里的系统依赖解析器。你在package.xml里写<depend>libeigen3-dev</depend>,真正知道这台 Ubuntu 上该装libeigen3-dev还是eigen3-dev的,就是 rosdep 背后那张巨大的映射表。这张表不在你本地,得从远程拉下来缓存到~/.ros/rosdep/里,rosdep update干的就是这件事。适合看这篇内容的人分三类:刚装完 ROS 想跑rosdep install的新手、要在内网批量部署 ROS 环境的运维、以及在做 CI 流水线镜像构建时被这一步堵住的工程师。不管你是哪一类,下面这套排查加解决的组合拳都能直接用。
1.1 init、update、install 三个子命令各管什么
很多人把这三个命令混着用,出了问题也说不清卡在哪。其实分工非常清晰:
sudo rosdep init:只做一件事 —— 把一份默认的源列表写到/etc/ros/rosdep/sources.list.d/20-default.list。这个文件里全是地址,不是真正的数据。它自己也会去下载一次默认列表,所以这一步也可能 timeout。rosdep update:读上面那个列表,把列表里每一行指向的 yaml 文件抓下来,解析、合并、缓存到~/.ros/rosdep/sources.cache和~/.ros/rosdep/sources.cache/下的分片文件里。这一步要发的请求最多,也最容易挂。rosdep install --from-paths src --ignore-src -r -y:读本地缓存,把当前工作空间里所有的依赖名翻译成 apt 包名,然后交给 apt 去装。这一步不联网抓 rosdep 数据,只走 apt 源。
理解这个分层极其重要,因为后面我给的离线方案,本质上就是「让 init 和 update 都去读本地文件」,而 install 阶段你什么都不用改。
1.2 update 阶段实际发出的请求链路
把这条链路拆开看,你就知道 timeout 到底断在哪个环节了。以较新的 rosdep(0.20 以上)为例,它大致会依次做这几件事:
- 读
/etc/ros/rosdep/sources.list.d/*.list,拿到所有源地址; - 去拉 rosdistro 的索引文件
index-v4.yaml(老版本是index.yaml)。这个文件里记录了每个 ROS 发行版的名字、状态,以及它对应的distribution.yaml相对路径; - 对每个发行版,去拉
releases/targets.yaml(走 rep3 机制的版本会走这一步)和各个distribution.yaml; - 按
sources.list里列出的yaml行,逐个拉base.yaml、python.yaml、ruby.yaml这些动辄几百 KB 的映射文件; - 如果有
gbpdistro行(比如 fuerte 这种老发行版),还要再拉一份对应的 yaml。
一个关键细节:所有这些地址,绝大多数都指向同一个域名下的 raw 内容服务。也就是说,你的成败集中在一两个域名的连通性上,而不是分散在几十个不同的站点。这就是为什么有时候你感觉「网络没问题啊,网页都能开」,但 rosdep 就是过不去 —— 它要去的地方恰好就是慢的那几个。
1.3 为什么报的是 timeout 而不是 404
这个区分很有价值。404 说明地址写错了、文件被删了、分支改名了;timeout 说明TCP 连接建立起来了但数据迟迟不来,或者连握手阶段都超时。前者你要改配置,后者你要改网络路径或绕开网络。
还有一种容易被误判的情况:报错里出现Connection reset by peer或者Remote end closed connection without response。这通常不是超时,而是链路上有设备在干扰长连接。遇到这种,单纯调大 timeout 是没用的,必须走「换源」或者「完全离线」这两条路。
注意:不要在没看清报错类型的情况下就去改 timeout 参数。把
urlopen error timed out和Connection reset当成一回事,是最常见的误诊。
2. 定位问题:别急着改代码,先让它把话说清楚
我见过太多人一上来就去搜「rosdep update 超时怎么改源码」,然后照着某个三年前的帖子把 python 文件改得乱七八糟,最后连rosdep本身都 import 不进去了。正确顺序是:先让命令吐出足够的信息,确认卡在哪一步、哪一条地址,再决定用哪种方案。
2.1 打开 verbose,让它把每次请求都打出来
rosdep 是 python 写的,--verbose会把中间过程全打出来。别省这一行命令:
rosdep update --verbose 2>&1 | tee /tmp/rosdep_update.log跑完之后看两样东西:日志最后停在哪一行、以及那一行对应的是哪个域名。我一般直接用 grep 把地址捞出来:
grep -oE 'https?://[^ "]+' /tmp/rosdep_update.log | sort -u这一条命令能把这次 update 尝试访问过的所有地址列出来,非常直观。你会看到它们的域名高度集中,这时候问题就从「rosdep 报错了」变成了「某个具体域名的可达性如何」,排查范围一下子缩小了。
如果 verbose 的输出量大到看不清最后停在哪儿,用timeout给整条命令加个上限,超时后看日志尾部:
timeout 120 rosdep update --verbose 2>&1 | tail -n 402.2 用 curl 单独验证某一个地址
把上面捞出来的地址挑一个,用 curl 做一次只测连通性、不测内容的探测:
curl -4 -I --connect-timeout 8 --max-time 20 \ https://raw.githubusercontent.com/ros/rosdistro/master/index-v4.yaml几个参数的含义值得说清楚,因为它们决定了你测出来的结论是否可信:
-4:强制走 IPv4。国内不少环境 IPv6 是「有地址但出不去」的状态,不强制 v4 的话,curl 会先尝试 IPv6、卡满超时再回落,测出来的时间完全是假的。-I:只发 HEAD 请求,不下载正文,快。--connect-timeout 8:握手阶段最多等 8 秒,这也是判断「是 DNS 慢、握手慢、还是传输慢」的关键。--max-time 20:整个请求的硬上限。
如果connect阶段就超了,那是 DNS 或者 TCP 层的问题;如果 connect 很快但总时间打满,那是传输阶段被限速。这两种情况对应的解法完全不同,一个可能只需要换个 DNS,另一个必须换数据源。
顺手也可以看一眼 DNS 解析结果,判断有没有被解析到奇怪的地址:
getent hosts raw.githubusercontent.com如果解析出来的地址明显不对,或者干脆解析不出来,那问题在 DNS 层,不在 rosdep。
2.3 报错信息对照表
我把这些年遇到的、以及同行反馈过的典型报错整理成了一张表,对着看能省很多时间。
| 报错关键词 | 实际含义 | 优先尝试的方案 |
|---|---|---|
urlopen error timed out | 握手或传输阶段超时 | 方案一(调超时)、方案二(换源) |
Connection reset by peer | 链路被中途打断,长连接维持不住 | 方案二、方案三(离线) |
Remote end closed connection without response | 服务端或中间设备主动断开 | 方案三(离线) |
[Errno -3] Temporary failure in name resolution | DNS 解析失败 | 检查/etc/resolv.conf,换 DNS |
[Errno 101] Network is unreachable | 根本没有出网路由 | 检查网卡、网关,或直接用离线方案 |
SSL: CERTIFICATE_VERIFY_FAILED | 证书校验失败,常出现在有中间设备的网络 | 先确认系统时间正确,再考虑本地源方案 |
ERROR: unable to process source [...yaml] | 地址拿到了但内容解析失败 | 检查文件是否被截断,重跑一次 |
提示:
SSL: CERTIFICATE_VERIFY_FAILED这个消息里,九成情况下第一步应该检查系统时间。虚拟机快照恢复后时间错乱,会让证书看起来「过期」,很多人在这里绕了很久。
3. 方案一:调大超时与重试,最省事的止血
如果你的网络只是「慢」而不是「断」,那这个方案是最低成本的。它的思路是:把 rosdep 内部的超时阈值放大,再给它加一层自动重试。慢就慢点,能跑完就行。
3.1 三个关键文件和需要改的地方
rosdep 的网络请求最终都落到 python 的urlopen(或者 requests)上,而很多调用点没有显式指定 timeout,用的是系统默认值。默认值在某些 python 版本下短得离谱,网络稍抖一下就抛异常。
你要找的文件通常在 site-packages 目录下,具体路径随安装方式和 python 版本变化,先用python3 -c把它定位出来:
python3 -c "import rosdep2, rosdistro, os; print(os.path.dirname(rosdep2.__file__)); print(os.path.dirname(rosdistro.__file__))"拿到目录之后,重点看这几个文件里的urlopen(调用:
rosdistro/__init__.py:get_index函数里拉索引文件的地方;rosdep2/sources_list.py:下载sources.list里各个 yaml 的地方;rosdep2/gbpdistro_support.py:处理gbpdistro类型源的地方;rosdep2/rep3.py:走 rep3 机制时拉 targets 文件的地方。
改法就是给每个没有 timeout 的调用补上参数。手动改容易漏,我一般用一个短脚本批量处理,先备份再改:
import pathlib, re, shutil targets = [ "rosdistro/__init__.py", "rosdep2/sources_list.py", "rosdep2/gbpdistro_support.py", "rosdep2/rep3.py", ] base = pathlib.Path("/usr/lib/python3/dist-packages") # 按上一步的实际路径替换 for rel in targets: p = base / rel if not p.exists(): print("跳过(不存在):", p) continue shutil.copy2(p, str(p) + ".bak") src = p.read_text(encoding="utf-8") # 只给单参数形式的 urlopen(x) 补 timeout,已有 timeout 的不动 new, n = re.subn(r"urlopen\(\s*([^,()]+?)\s*\)", r"urlopen(\1, timeout=120)", src) p.write_text(new, encoding="utf-8") print(f"{rel}: 替换 {n} 处")跑完检查一下没有语法错误:
python3 -c "import rosdep2, rosdistro; print('import ok')"打印出import ok就说明改动没破坏模块。然后重新跑rosdep update。
3.2 加一层自动重试,把偶发失败吃掉
即使超时调大了,链路抖动导致的偶发失败还是会有。与其盯着屏幕一次次手敲,不如写个循环:
#!/usr/bin/env bash # retry_rosdep_update.sh set -u MAX=30 for i in $(seq 1 "$MAX"); do echo "=== 第 $i 次尝试 ===" if rosdep update; then echo "=== 第 $i 次成功 ===" exit 0 fi sleep 3 done echo "连续 $MAX 次均失败,请改用离线方案" exit 1这个脚本的价值在于:rosdep update 实际是分多个步骤串行下载的,每次重跑,已经成功的部分有概率更快通过,多试几次的成功率比单次高不少。我实测过几次在弱网环境下,前二十次都失败、第二十三次突然全过,就是运气好赶上了一段通畅的窗口期。
3.3 这个方案的注意事项
有三点必须提前说清楚,否则你会踩坑。
第一,直接改 site-packages 里的文件是脆弱的。apt 升级 rosdep 或者重装系统包,改动会被覆盖,问题原封不动回来。所以我在脚本里强制留了.bak备份,改坏了能一键还原:
for f in /usr/lib/python3/dist-packages/rosdep2/*.bak; do mv "$f" "${f%.bak}" done第二,用 pip 装的 rosdep 路径完全不同,通常在~/.local/lib/python3.x/site-packages/下,而且不需要 sudo。这也是我推荐在容器里用 pip 装 rosdep 的原因 —— 改起来不需要提权,重建镜像时也干净。
第三,timeout 不是越大越好。设成 600 秒,一次失败就要等十分钟,重试三十次的脚本能跑五个小时。我一般设 60 到 120 秒,这个区间既给了慢链路足够的容错,又不至于让失败检测变得太迟钝。
4. 方案二:把索引源换成公共软件镜像站
如果调超时也没用,说明问题不是「慢」,而是「那条路根本走不通或者极不稳定」。这时候最直接的思路是:换一条走得更顺的路。好消息是,rosdistro 的数据本身就是开源仓库内容,国内多所高校和云厂商都做了公共镜像,这些镜像站是正规的开源软件分发服务,直接拿来用就行。
4.1 ROSDISTRO_INDEX_URL 这个环境变量
rosdep 支持通过环境变量覆盖索引地址,这是整件事里最关键的一个开关。默认情况下,索引地址指向上游的 raw 内容服务;你把它改成镜像站上的对应文件地址,整个下载链路就换了出口。
export ROSDISTRO_INDEX_URL=https://<镜像站域名>/rosdistro/index-v4.yaml rosdep update --verbose要注意两点:
- 老版本的 rosdep 用的是
index.yaml而不是index-v4.yaml。判断方法很简单:看rosdep update --verbose输出的第一个地址里写的是哪个文件名,照着换成镜像站上同名的文件即可。 - 这个环境变量是进程级的,当前终端生效。想持久化就写进
~/.bashrc,或者在 Dockerfile 里用ENV声明。CI 环境里我更推荐在流水线脚本里显式 export,避免隐式依赖 shell 初始化文件。
4.2 镜像地址的选择与校验
我一般会准备两三个候选,用 curl 快速筛一遍,谁快用谁:
for u in \ "https://mirrors.tuna.tsinghua.edu.cn/rosdistro/index-v4.yaml" \ "https://mirrors.ustc.edu.cn/rosdistro/index-v4.yaml" ; do printf "%-70s " "$u" curl -4 -o /dev/null -s -w "%{http_code} %{time_total}s\n" \ --connect-timeout 6 --max-time 20 "$u" done输出里第一个数字是 HTTP 状态码,200才算地址有效;第二个是总耗时,越小越好。看到404就说明该镜像站没有这个路径或者文件名不对,换一个试。
这一步绝对不能省。我见过很多帖子直接把某个地址写死,结果时间一长镜像站目录结构调整了,读者照着做全报 404,然后开始怀疑自己的系统有问题。
确认地址可用之后,还要顺手验证内容是不是完整的 yaml,避免镜像站返回一个半截文件或者错误页:
curl -4 -s --max-time 20 "https://mirrors.tuna.tsinghua.edu.cn/rosdistro/index-v4.yaml" \ | head -n 5正常情况下你应该看到标准的 yaml 头部和distributions:字段。如果看到 HTML 标签,说明这个路径返回的是网页而不是文件,不能用。
4.3 完整的换源操作步骤
把上面几步串起来,一套可直接复制的流程是这样:
# 1. 选出可用镜像,写入环境变量(临时生效) export ROSDISTRO_INDEX_URL=https://mirrors.tuna.tsinghua.edu.cn/rosdistro/index-v4.yaml # 2. 顺便把 sources.list 里的地址也换掉 sudo cp /etc/ros/rosdep/sources.list.d/20-default.list \ /etc/ros/rosdep/sources.list.d/20-default.list.bak sudo sed -i 's#https://raw.githubusercontent.com/ros/rosdistro/master#https://mirrors.tuna.tsinghua.edu.cn/rosdistro#g' \ /etc/ros/rosdep/sources.list.d/20-default.list # 3. 清掉可能已经写坏的缓存,避免旧数据干扰 rm -rf ~/.ros/rosdep/sources.cache # 4. 重新执行 rosdep update --verbose第 3 步容易被忽略但很重要。rosdep 的缓存是有状态的,如果你上一次在下载中途失败,缓存里可能留下不完整的文件,下一次 update 有可能直接读了这个坏缓存然后报一个莫名其妙的解析错误。改完源地址就清缓存,这是我反复验证过的最省事做法。
注意:
sed里的分隔符我用了#而不是默认的/,因为替换内容里包含大量斜杠,用/做分隔符需要疯狂转义,极易写错。
5. 方案三:完全离线,把数据喂到本地
前两个方案都有一个前提:你至少能联通某个镜像站。但现实里有相当一部分场景是彻底不通的 —— 内网隔离的编译机、只允许白名单出网的构建环境、以及网络极其糟糕的现场设备。这种情况下,唯一的出路是把 rosdistro 的整个仓库搬到本地,让 rosdep 直接读文件。
5.1 把 rosdistro 完整仓库拿到手
关键在于要拿整个仓库,而不是零散几个 yaml。因为索引文件里记录的是相对路径,目录结构必须完整,缺一个发行版目录就可能解析失败。
最省事的办法是在一台能上网的机器上操作,然后把结果拷过去:
mkdir -p /opt/ros-offline && cd /opt/ros-offline git clone --depth 1 https://github.com/ros/rosdistro.git如果连这一步都不通,就去公共镜像站或代码托管平台上找 rosdistro 的完整镜像仓库。这里同样建议先探测再克隆,用git ls-remote花几秒钟确认地址存在,比克隆到一半失败要划算得多:
git ls-remote --heads https://mirrors.tuna.tsinghua.edu.cn/git/rosdistro.git | head返回了分支列表,说明这个仓库可用,再执行git clone。克隆完确认几个关键文件在位:
ls /opt/ros-offline/rosdistro/index-v4.yaml \ /opt/ros-offline/rosdistro/rosdep/base.yaml \ /opt/ros-offline/rosdistro/noetic/distribution.yaml三个文件都在,目录结构就是完整的。
5.2 把 sources.list 和索引变量都指向本地路径
这一步是整个离线方案的核心:把所有https://地址替换成file://本地路径。
先手工生成一份全新的 sources.list,内容大致是下面这个样子(发行版名字按你实际使用的替换):
# os-specific listings first yaml file:///opt/ros-offline/rosdistro/rosdep/osx-homebrew.yaml osx # generic yaml file:///opt/ros-offline/rosdistro/rosdep/base.yaml yaml file:///opt/ros-offline/rosdistro/rosdep/python.yaml yaml file:///opt/ros-offline/rosdistro/rosdep/ruby.yaml gbpdistro file:///opt/ros-offline/rosdistro/releases/fuerte.yaml fuerte写进去:
sudo mkdir -p /etc/ros/rosdep/sources.list.d sudo tee /etc/ros/rosdep/sources.list.d/20-default.list > /dev/null <<'EOF' yaml file:///opt/ros-offline/rosdistro/rosdep/osx-homebrew.yaml osx yaml file:///opt/ros-offline/rosdistro/rosdep/base.yaml yaml file:///opt/ros-offline/rosdistro/rosdep/python.yaml yaml file:///opt/ros-offline/rosdistro/rosdep/ruby.yaml gbpdistro file:///opt/ros-offline/rosdistro/releases/fuerte.yaml fuerte EOF然后设置索引变量,同样指向本地:
export ROSDISTRO_INDEX_URL=file:///opt/ros-offline/rosdistro/index-v4.yaml rosdep update正常情况下这次会秒过,因为所有的读取都是磁盘操作。如果还是打印出 https 开头的地址,说明你用的 rosdep 版本还在走 rep3 机制,它内部硬编码了一个 targets 文件地址。处理办法是把那个文件也改成file://:打开rosdep2/rep3.py,找到那个被写死的地址常量,替换成本地路径即可,改完记得用python3 -c "import rosdep2"验证一下语法。
5.3 验证、迁移与回滚
离线跑通之后,第一件事是验证结果真的可用,别等到跑rosdep install才发现表是空的:
rosdep resolve python3-yaml rosdep resolve libeigen3-dev能正常输出 apt 包名(比如python3-yaml、libeigen3-dev),说明缓存里的映射表是有效的。
第二件事,也是我认为这套方案最有价值的地方:把成果直接复制到其他机器,让它们跳过整个 update 过程。你需要打包两个东西:
| 路径 | 内容 | 说明 |
|---|---|---|
~/.ros/rosdep/sources.cache | 解析后的映射缓存 | 核心数据,必须带 |
/etc/ros/rosdep/sources.list.d/20-default.list | 源列表定义 | 使缓存与源一一对应 |
tar czf rosdep-cache.tgz \ -C "$HOME" .ros/rosdep/sources.cache \ -C /etc/ros/rosdep sources.list.d/20-default.list新机器上解包到相同位置即可。这里有一个必须注意的前提:缓存和源列表是绑定的。如果你在新机器上把20-default.list改成别的地址,rosdep 会认为缓存失效并重新下载。所以要么两边完全一致,要么目标机器也把 rosdistro 仓库放在同样的绝对路径下。
回滚也很简单,把备份的.bak换回来、删掉环境变量、清理缓存即可:
unset ROSDISTRO_INDEX_URL sudo mv /etc/ros/rosdep/sources.list.d/20-default.list.bak \ /etc/ros/rosdep/sources.list.d/20-default.list rm -rf ~/.ros/rosdep/sources.cache rosdep update6. 常见问题速查与踩坑实录
方案讲完了,接下来是更实用的部分 —— 这些年我和同事在各类环境里踩过的坑,以及对应的处理方式。这些东西在官方文档里基本找不到,但真遇到的时候能省下大半天。
6.1 问题速查表
| 现象 | 最可能的原因 | 处理方式 |
|---|---|---|
| 换源后仍访问上游地址 | 环境变量没生效,或被别的脚本覆盖 | 在命令前显式export,用 `env |
| update 秒过但 resolve 报找不到包 | 读了旧的坏缓存 | 删掉~/.ros/rosdep/sources.cache重跑 |
| 某一行 yaml 永远失败,其他都正常 | 该文件在镜像站上缺失 | 把这一行暂时注释掉,其余照常 |
| 容器里改完重启就失效 | 改动没写进镜像层 | 在 Dockerfile 里ENV+ 复制文件,而不是运行期手改 |
| 多用户机器上有人跑有人不跑 | 缓存是 per-user 的 | 每个用户各自 update,或者统一分发缓存 |
--include-eol-distros后更慢 | 多拉了一堆已停止维护的发行版数据 | 只在确实需要老发行版时才加这个参数 |
关于最后一行,补充一句:rosdep update --rosdistro noetic这种显式指定发行版的写法,能显著减少一次 update 需要拉取的文件数量。只拉你真正要用的那个发行版,是最容易被忽略的优化。
6.2 几条我踩过的坑
第一条:不要在 Dockerfile 里用RUN rosdep update而不ENV索引地址。镜像构建时的网络环境和你本地可能完全不同,本地通了不代表构建机上通。我建议在 Dockerfile 里显式声明:
ENV ROSDISTRO_INDEX_URL=file:///opt/ros-offline/rosdistro/index-v4.yaml COPY rosdistro /opt/ros-offline/rosdistro RUN rosdep update把离线数据打进镜像,构建过程就完全不依赖网络了,这个做法在多阶段构建里我用了很久,复现性非常好。
第二条:rosdep init失败时,不要反复重试。这一步只是写一个文件,失败通常就是因为那份默认列表要在线拉取。直接手工创建20-default.list就能绕过,省下大量时间。很多人卡在 init 上以为容器有问题,其实完全没必要。
第三条:注意 shell 的作用域。export出来的变量只对当前 shell 及其子进程有效。你用sudo rosdep update时,sudo默认会清空大部分环境变量,导致你的ROSDISTRO_INDEX_URL根本没传进去!如果某个步骤需要 sudo,用sudo -E保留环境变量:
sudo -E rosdep update这个坑我卡过整整一个下午,一直以为镜像站地址是错的,最后发现是 sudo 把变量吃掉了。
第四条:备份一定要带时间戳。我习惯把每次改动过的配置都留一份带日期的副本,出问题时能快速回溯到「上次能用是什么状态」:
sudo cp /etc/ros/rosdep/sources.list.d/20-default.list \ "/etc/ros/rosdep/sources.list.d/20-default.list.$(date +%Y%m%d-%H%M%S)"6.3 长期维护上的几点经验
把 rosdep 这一步彻底解决之后,后续维护其实很轻。我总结下来就三件事。
一是定期同步离线仓库。rosdep 的映射表是持续更新的,新包、包改名都会反映在里面。如果用了离线方案,建议隔一两个月在一台能联网的机器上git pull一次,再重新分发缓存。频率不用太高,半年一次对大多数项目也够用。
二是把版本固定下来。rosdep 和 rosdistro 都有版本差异,index-v4.yaml和index.yaml的切换就是例子。团队协作的场景下,我倾向于把 rosdep 版本和 rosdistro 的快照都写进项目文档,出现「同事那边能跑我这边不行」的时候,第一件事就是比对这两个版本。
三是别在生产环境里依赖公网。这句话听起来像废话,但我见过太多线上设备因为某次重启后 rosdep 缓存失效、而恰好网络又不可用,导致部署卡死。凡是需要长期运行的机器,都应该把完整的 rosdep 缓存固化进系统镜像或配置管理流程里,而不是指望运行时能拉下来。
最后分享一个我现在的习惯:任何需要联网的构建步骤,我都会在动手之前先想清楚「如果这条路断了,我有没有离线兜底」。rosdep 这件事教会我的,其实不只是怎么改几个地址 —— 而是把「不确定性」从流程里挤出去。你现在遇到的那个 timeout,说到底就是一个不确定的环节,把它换成确定能跑通的东西,问题就永远消失了。