ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

dsh-codex-connect五大高频故障排错速查

dsh-codex-connect五大高频故障排错速查

dsh-codex-connect 这类基于 SSH 通道的批量连接工具,日常用起来确实省事,几十台节点一条命令全连上,命令执行完把 codex 返回码一并拉回来。但真到了排错环节,很多同事就卡住了:连接超时、认证失败、命令挂起、端口拒绝、批量节点一半成功一半失败,这五个现象几乎覆盖了我这两年遇到的所有 dsh-codex-connect 故障。这篇排错速查不跟你讲大道理,直接按现象给命令,每个命令我会说清楚为什么查、输出怎么看、下一步去哪,适合每天跟远程节点打交道的运维、交付工程师和刚上手 CLI 工具的测试同学。

1. 现象一:连接超时,先分清是网络还是服务端问题

1.1 判断网络通不通,别上来就排服务端

连接超时是 dsh-codex-connect 最频繁出现的问题,报错通常长这样:Connection timed out或者connect to host 192.168.1.10 port 22: timed out。第一次遇到这个,我的建议是先别打开服务端配置,先在本地把链路探一遍。

最基础的三板斧是 ping、telnet、nc。ping 看 ICMP 通不通,telnet 和 nc 看 TCP 端口通不通:

ping -c 4 192.168.1.10 nc -zv -w 5 192.168.1.10 22 telnet 192.168.1.10 22

很多人会问,telnet 看端口怎么才算通?其实很简单:能进入Connected to 192.168.1.10.这样的一行,然后光标停在Escape character is '^]'.下面等待输入,说明 TCP 三次握手已经完成,端口是通的。一旦出现Connection refused,说明网络能到但服务没开;出现Connection timed out,那基本可以断定中间链路有丢包或被防火墙丢弃。

如果系统没有 telnet,用 nc 的-z端口扫描模式和-w超时参数一样能测:nc -zv -w 5 目标IP 22,succeeded就是通。这一步只要 10 秒,能直接把问题切到“网络层”还是“服务层”,效率比直接去看服务端日志高很多。

1.2 路由和监听状态一起看,避免被表象骗

ping 不通不代表一定断网。很多环境出于安全考虑禁 ping,或者防火墙只放行了 TCP 端口,ICMP 一概丢弃。所以 ping 只能作为参考,关键时刻要用路由追踪命令来看中间经过哪些节点。

traceroute -n -T -p 22 192.168.1.10 mtr -n -T -p 22 192.168.1.10

-T表示用 TCP 而不是默认的 UDP 探测,-p 22指定目标端口,这样能更真实地模拟 dsh-codex-connect 的路径。输出里如果从某个跳数开始全是* * *,那问题大概率就出在那一段链路上,要么是路由黑洞,要么是防火墙丢弃。

同时要在服务端看一眼端口到底有没有在监听:

ss -lntup | grep 22 systemctl status sshd

ss -lntup的-l只显示监听状态,-n不做域名反解,-t只看 TCP,-p带出进程 PID。如果 grep 结果为空,说明 sshd 根本没起来,或者监听地址不是预期值。我踩过一个典型的坑:服务端 sshd 配置里写了ListenAddress 127.0.0.1,只监听本机回环,外部节点自然连不上,ss 输出和健康检查看着都正常,但远程就是超时。

1.3 服务端日志会告诉你真正卡在哪

如果网络层排查下来链路是通的,端口也监听着,但还是超时,那就去看 sshd 的系统日志:

journalctl -u sshd -n 200 --no-pager

日志里出现Connection reset by peer,多半是客户端或者中间设备主动断了连接;出现大量Failed password,那就是别人在暴力破解或者哪个自动任务在用错误凭据重试;如果日志里连 IP 都没有,那请求压根没到 sshd,问题还在网络链路。

这里分享一个经验:ping 通不代表 TCP 通,TCP 通不代表 SSH 服务正常。链路判断一定要一层一层往下走,ICMP、TCP、应用服务分别是三件事,不要混为一谈。我在排“连接超时”时最快的路径永远是nc -zv先做端口连通性测试,这命令 5 秒内能区分 70% 的问题方向。

2. 现象二:认证失败,九成是密钥或凭据不对

2.1 先把本地密钥状态查清楚

认证失败的报错常见是Permission denied (publickey)、Authentication failed,或者是跟Host key verification failed混在一起。很多人一看到认证失败就怀疑密钥写错了,其实更常见的是本地根本没加载对应密钥,或者 known_hosts 里的指纹已经变了。

先看本地 ssh-agent 里加载了哪些密钥:

ssh-add -l ssh-keygen -y -f ~/.ssh/id_rsa

ssh-add -l会列出 agent 里所有密钥的指纹,如果输出是The agent has no identities.,说明 agent 是空的。ssh-keygen -y -f可以从私钥文件反推出公钥字符串,方便你和服务端 authorized_keys 里的内容做对比。这两个命令组合起来基本能确定“本地密钥是否存在”以及“是否已加载”。

如果怀疑是 known_hosts 的问题,重新抓取服务端公钥:

ssh-keyscan -t rsa 192.168.1.10 >> ~/.ssh/known_hosts

Host key verification failed的本质是客户端本地保存的服务端公钥指纹,和实际服务端返回的不一致。常见原因是服务端重装系统或重装 sshd 后密钥变了,这时候ssh-keyscan重新抓一次即可。注意,虽然可以把 known_hosts 清空来解决,但建议保留这个机制,它能帮你发现中间人风险,别为了省事直接StrictHostKeyChecking no。

2.2 用 verbose 输出定位具体卡在哪个认证环节

dsh-codex-connect 这类工具通常提供调试模式,以我用过的版本为例是-vvv,SSH 本身也可以直接 verbose:

dsh-codex-connect --connect 192.168.1.10 -vvv ssh -vvv user@192.168.1.10

verbose 输出里最关键的是debug1: Next authentication method这一行。它会按顺序列出尝试过的认证方式:publickey、password、keyboard-interactive。如果反复输出Offering public key然后又被拒绝,说明服务端不认这把公钥;如果压根没走到 publickey 就直接提示Permission denied,说明客户端这边的IdentitiesOnly配置可能有问题,或者 agent 里加载了一堆无关密钥导致服务端拒绝。

另外,很多人忽略 authorized_keys 的文件权限。服务端如果.ssh目录权限不是 700、authorized_keys不是 600,sshd 会直接忽略这个文件,公钥写得再对也没用。排查命令:

ls -ld ~/.ssh ls -l ~/.ssh/authorized_keys chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys

2.3 服务端日志里找“拒绝”的原因

认证失败最终要以服务端日志为准。这里有个区分技巧:日志里有Invalid user,说明账号不存在;有Failed password说明密码错;有Authentication refused: bad ownership or modes说明权限问题;如果日志里完全没有这次连接记录,那流量可能都没到 sshd,还是网络层的事。

journalctl -u sshd -n 300 --no-pager | grep -Ei "failed|invalid|refused|error"

我自己的排查习惯是,先看本地 agent 有没有加载密钥,再看服务端日志关键字,最后才去检查公钥内容。这个顺序能避免一个很蠢的坑:你花了二十分钟重新配公钥,结果问题只是 ssh-agent 没启动,密钥文件在但压根没生效。另外,如果公司网络环境里有跳板机/堡垒机,要注意不同跳板机上加载的密钥上下文可能不同,ssh-add -l在每台机器上都要单独确认。

3. 现象三:命令一执行就卡住,多半卡在会话与缓冲

3.1 先用 timeout 兜底,别让命令无限等下去

命令挂起是比连接失败更难受的问题,因为进程既不退出也不报错,就那么耗着。最常见的场景是执行dsh-codex-connect --exec 'df -h'后终端一直无输出,最后只能 Ctrl+C。

这背后的原因有很多,可能是服务端命令在等待 stdin 输入,比如cat不带参数;也可能是程序输出到管道时因为缓冲区没满迟迟不 flush;还有可能是远程进程执行后没有正常退出,导致 SSH 通道一直开着。第一个建议是给所有远程执行命令加一个超时兜底:

timeout 10 dsh-codex-connect --connect 192.168.1.10 --exec 'hostname'

timeout是 GNU coreutils 自带的命令,10表示最多等 10 秒,超时后直接杀掉本地工具进程并返回退出码 124。这样至少能明确告诉你“这条命令确实卡住了”,而不是“输出还没到”。如果加了 timeout 超时报错,接下来就可以专心排查远端命令为什么不退。

3.2 用 stdbuf 和 script 定位卡点

缓冲问题是命令挂起的一个大头。很多程序在输出到终端时是行缓冲,一行写完就输出;但输出重定向到管道时变成全缓冲,要攒满 4K 甚至 8K 才 flush。远程执行的程序如果 stdout 被重定向,输出就可能一直憋在缓冲区里,看起来就像死了一样。解决办法是用 stdbuf 强制行缓冲:

dsh-codex-connect --connect 192.168.1.10 --exec "stdbuf -oL -eL tail -f /var/log/syslog"

其中-oL表示标准输出按照行缓冲,-eL表示错误输出也按行缓冲。

如果想看命令到底卡在哪一步,用 script 命令录制整个会话,然后实时观察输出停在哪一行:

script -q -c "dsh-codex-connect --connect 192.168.1.10 --exec 'ls -l /var/log'" /tmp/dsh_session.log tail -f /tmp/dsh_session.log

如果输出停在某个文件列表里,后面就没有了,那可能是在访问某个挂载点或网络文件系统时阻塞;如果ls都执行完了但会话不退出,那基本是远端进程没有断开通道。

更深一层,可以用 strace 跟踪系统调用:

strace -f -tt -T -e trace=network dsh-codex-connect --connect 192.168.1.10 --exec 'uptime'

strace 会输出每个网络相关系统调用的时间戳和耗时,-T能显示每次调用的耗时。如果看到某个read调用一直阻塞没有返回,那就是在等远端数据;如果write阻塞,说明本地管道或终端的对端没有消费数据。这一步比较重,建议前两个命令无效时再用。

3.3 tty 分配是很多人忽略的隐藏因素

命令挂起还有一个高频原因,是远程执行时没有分配伪终端(pseudo-terminal)。一些命令比如sudo、交互式脚本、需要检测终端的程序,在无 tty 环境下行为会变得非常诡异:要么直接挂起等待输入,要么输出格式错乱。

dsh-codex-connect 通常有--force-tty或-t参数,SSH 对应的就是ssh -t。在执行交互类命令时强制分配 tty:

dsh-codex-connect --connect 192.168.1.10 --force-tty --exec 'sudo systemctl status nginx'

但要注意,分配 tty 后输出里会混入终端控制字符,比如[0m这类 ANSI 转义序列。如果你想把输出保存到文件做自动化判断,不要加 tty,改用--exec并配合stdbuf;如果你需要交互式操作,再加 tty。我见过一个同事在自动化脚本里加了 tty,结果日志里全是乱码,解析正则怎么都对不上,就是这个原因。

实践中还有一个非常实用的检查方法:命令卡住后,在另一个终端窗口查看目标服务器上是否有残留进程:

ps -ef | grep "你的命令关键字"

如果远端进程已经结束但 dsh-codex-connect 还挂着,说明问题在 SSH 通道关闭层面,可能是某个子进程继承了 stdout 文件描述符没有释放;如果远端进程还在跑,那就是命令本身没执行完。两种情况的处理方向完全不同。

4. 现象四:连接被拒,服务根本没在监听

4.1 端口、进程、容器三层都要查

Connection refused和Connection timed out最大的区别是:refused 说明网络链路是通的,数据包已经到达目标机器,但目标机器上没有进程接受这个 TCP 连接。这种情况排错速度可以很快,第一件事就是确认端口监听状态。

ss -lntup | grep 22 netstat -lntup | grep 22 lsof -i :22

如果ss输出为空,说明 22 端口没人监听。这时候再看进程状态:

systemctl status sshd ps -ef | grep sshd

这里有个很容易混淆的点:systemctl status显示 active 不代表端口一定在监听。比如 sshd 启动过程中卡住,或者配置错了监听地址,systemd 可能认为主进程还在运行,但端口根本没绑定成功。所以一定要以ss -lntup的结果为准。

容器化环境里还要多看一层。现在很多服务跑在容器中,你通过 dsh-codex-connect 连接的是一个 Pod 或容器的 IP,但容器引擎和宿主机之间的端口映射可能并没有生效。排查命令:

docker ps --format "table {{.Names}}\t{{.Ports}}" crictl ps -a docker logs --tail 50 <容器名>

特别是使用 containerd 或 CRI 兼容运行时的时候,crictl ps -a可以列出所有容器状态,如果容器一直处于ContainerCreating或Exited,那外面连不上再正常不过。

4.2 防火墙和云安全组是重灾区

端口明明监听了,但客户端连接还是被拒绝,十有八九是中间防火墙丢包或者 REJECT。在服务端执行:

firewall-cmd --list-ports firewall-cmd --list-services iptables -L -n | grep 22

注意 REJECT 和 DROP 的区别:iptables里 REJECT 会立即回一个拒绝包,客户端表现为 Connection refused;DROP 直接丢弃不响应,客户端表现更像超时。所以出现 refused 时优先查有没有 REJECT 规则。

云环境里还要检查安全组和网络 ACL。很多人在本地服务器上放行了端口,但忘了云控制台的安全组规则,或者安全组只放行了某个 IP,其他源被拒绝。这种问题在服务端用ss看永远都是 listening,但客户端就是连不上。

4.3 端口冲突和旧进程残留要会看 PID

还有一类比较隐蔽的问题:服务端多个进程抢同一个端口,或者旧进程没完全退出导致新进程绑定失败。判断方法看 PID 是否一致:

ss -lntup | grep 22 ps -ef | grep sshd

如果 ss 输出里的 PID 和当前 sshd 进程的 PID 不一致,说明端口被一个“幽灵进程”占着,真正的 sshd 没绑定成功。这时候看lsof -p <PID> -a -i tcp可以进一步确认那个进程的身份。遇到这种情况,不要犹豫,先确认占端口进程的业务归属,再决定是 kill 还是等服务自己恢复。

我自己排 Connection refused 的标准顺序是:先ss -lntup看端口,再systemctl status看进程,再iptables -L -n看防火墙,最后看云安全组。这个顺序能覆盖 95% 的 refused 场景,剩下的才需要考虑 SELinux 策略或者 TCP Wrapper 这类偏门配置。

5. 现象五:批量节点部分失败,别忽略退出码和日志

5.1 退出码是最高效的分诊信号

批量执行的时候最头疼的不是全部失败,而是部分成功部分失败。有些人喜欢把几十行报错贴到群里问,实际上第一件事应该是看退出码。

dsh-codex-connect 这类工具通常会透传远端命令的退出码,本地拿echo $?就能看到:

dsh-codex-connect --connect 192.168.1.10 --exec 'test -f /etc/nginx/nginx.conf' echo $?

echo $?输出 0 表示命令成功,非 0 表示失败。具体非 0 值代表什么,要看sysexits.h或者程序自己的约定,一般 1 是通用错误,127 是命令不存在,126 是权限不足无法执行。这一步能立刻把问题分成两类:是 dsh 本身连接失败(连接层),还是命令执行后返回了非 0 码(命令层)。这两类问题的排查路径完全不同。

如果一条命令里有多段逻辑,注意用&&和||控制短路:

dsh-codex-connect --connect 192.168.1.10 --exec 'test -f /etc/nginx/nginx.conf && echo CONFIG_OK || echo CONFIG_MISSING'

这样即使命令执行失败,dsh 仍然能拿到一个明确的输出文本,而不是只有一个退出码,日志可读性会好很多。

5.2 拆分批次,把失败节点“逼”出来

批量节点太多时,一次性执行几十个节点会很难定位。我习惯把节点文件拆成小组,逐批测试并用循环记录每个节点的结果:

while read host; do echo "=== $host ===" dsh-codex-connect --connect "$host" --exec 'uptime; echo REMOTE_OK' echo "$host:$?" >> /tmp/dsh_result.log sleep 1 done < nodes.txt

循环脚本的好处是每条记录都带上了节点名和退出码,失败节点会被明确写到结果文件里。如果某个节点反复失败,单独拿出来测试:

dsh-codex-connect --connect 10.0.0.5 --exec 'uptime' -vvv

这种方法比一次性并发所有节点更容易看清单个节点的行为差异。特别是当你怀疑是某个节点配置不同时,单独跑一遍 verbose 就能看到认证方式、路径解析、shell 类型是否一致。

关于 shell 参数,还有一个很容易踩的坑。如果你用sh -c执行远程命令,位置参数会被吞掉,比如:

dsh-codex-connect --connect 10.0.0.5 --exec "sh -c 'echo $1'"

这里的$1大概率在本地 shell 就已经被展开了,到了远端就变成空字符串。用shift处理参数之前,一定要用单引号把远端命令包起来,或者在本地用\$1转义。远程执行时变量展开的时机是最容易被忽略的细节,我建议所有复杂命令都在本地先跑一遍echo看一下最终拼接结果。

5.3 日志与 stderr 是你最好的排错伙伴

批量部分失败时,很多人只盯着 stdout 看,忽略了 stderr。ssh 类的工具在连接失败时会有大量信息输出到 stderr,用2>&1把错误输出合并到标准输出,日志才完整:

dsh-codex-connect --connect 10.0.0.5 --exec 'ls /nonexist' 2>&1 | tee -a /tmp/dsh_debug.log

如果工具自身有日志级别参数,开启 debug 级别是排查批量失败最直接的方式:

dsh-codex-connect --host-file nodes.txt --exec 'hostname' --log-level debug journalctl -u dsh-codex-connect --since "10 minutes ago" --no-pager

我需要坦白说一句:批量节点部分失败,绝大多数情况不是 dsh-codex-connect 本身坏了。常见原因是某些节点缺某个依赖包、某个路径的符号链接指向不存在、部分节点没放公钥、磁盘满导致命令执行到一半退出。所以每一条失败的日志,都要去区分“连接层报错”和“命令执行报错”:前者日志里会有connect refused、timeout、auth failed这类关键字;后者一般只是远端 shell 输出了command not found、Permission denied或者非 0 退出码。这两类问题混在一起处理会浪费大量时间,分开查会快得多。

6. 高频命令速查表与我的排错习惯

6.1 五个现象对照速查表

下面这张表是我实际工作中使用频率最高的命令组合,整理成速查格式,方便直接照着敲:

现象第一组命令第二组命令最常见原因
连接超时ping -c 4 <host>、nc -zv -w 5 <host> <port>traceroute -n -T -p <port> <host>、ss -lntup | grep <port>中间链路丢包、防火墙 DROP、服务监听地址错误
认证失败ssh-add -l、ssh-keygen -y -f ~/.ssh/id_rsassh -vvv user@<host>、journalctl -u sshd | grep -i failed密钥未加载、known_hosts 指纹变化、authorized_keys 权限不对
命令挂起timeout 10 dsh-codex-connect --exec '<cmd>'stdbuf -oL -eL <cmd>、script -q -c "<cmd>" <log>程序等 stdin、stdout 全缓冲未 flush、无 tty 导致交互命令挂起
连接被拒ss -lntup | grep <port>、systemctl status sshdiptables -L -n | grep <port>、docker ps | crictl ps -a服务没监听、REJECT 规则、容器端口映射失效
批量部分失败echo $?、dsh-codex-connect --exec '<cmd> && echo OK || echo FAIL'for/while循环记录退出码、--log-level debug节点依赖不一致、远程参数被吞、部分节点公钥没配

这张表不是让你死记硬背,而是排错时有一份可以对照的路径。超时问题和拒绝问题的第一组命令完全不同,前者重网络链路,后者重本地监听和防火墙,这是最容易混的两类问题。

6.2 我的排错顺序:先分类,再定向排查

根据我自己的经验,最快的排错流程不是从第一条命令开始试,而是先给现象分类再定向排查。每次遇到 dsh-codex-connect 连不上或者执行失败,我都会先问自己三个问题:

第一个问题,报错是超时、拒绝还是认证失败?这三个关键字对应的链路层级完全不同,超时在中间链路,拒绝在端口监听,认证失败在密钥和服务端配置。分类定了,命令链自然就出来了。

第二个问题,是一个节点失败还是多个节点失败?单个节点失败优先看节点本身的配置和网络;批量部分失败优先看节点间的差异,比如系统版本、依赖包、公钥配置是否有遗漏。

第三个问题,是连接层失败还是命令层失败?连接层失败看 sshd 日志和网络命令,命令层失败看远端 shell 的 stderr 和退出码。把日志里Connection refused和command not found混在一起看,是非常容易浪费时间的做法。

我在实际使用中养成了一个习惯,所有批量执行的脚本里都强制记录退出码和 stderr,就算命令本身很简短也加上|| echo FAILED:$?。这不是多此一举,因为一旦自动化任务跑了半小时,你根本记不清每个节点当时的输出长什么样,只有套上了统一的日志格式,事后排查才有据可查。

另外,给刚接触 dsh-codex-connect 的同事一个建议:不要怕用-vvv这类调试参数,它输出的信息虽然多,但每一行都有价值。你现在花三十秒看的 verbose 日志,可能帮你省下后面三十分钟的猜测。排错这件事,本质上就是把“可能的原因”一个个排除掉,而命令的作用就是帮你快速排除,不是帮你记住所有细节。

我最后想分享的是,这份速查表不是银弹,每个环境都有自己的差异,比如有的服务端用非标准端口、有的节点配置了双网卡、有的命令依赖特殊的环境变量。但只要抓住“超时、拒绝、认证、挂起、批量差异”这五个高频现象,并且每个现象都能对应出一条命令链,排错思路就不会乱。你可以把这张表稍作修改,补充上你们环境真实的端口号和节点命名规则,贴在工作文档里,下次再遇到 dsh-codex-connect 的问题,照着走一遍,基本都能在十分钟内定位到根因。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进