本文分析了SSH登录卡顿的问题,指出可能是UseDNS和GSSAPI握手以及MTU黑洞导致。建议通过详细日志定位问题,解决UseDNS反向解析超时和GSSAPI认证超时,提高SSH登录效率。
前段时间接手两台刚初始化的云主机,刚装完系统就碰上特别恶心的事情。
在本地终端敲下 ssh root@ip,按完回车光标就定在那儿,足足干等了十五秒,才慢吞吞跳出输入密码的提示;好不容易连上去,敲 ls、pwd 看着一切正常,结果顺手打了个 journalctl -n 200 准备看日志,终端瞬间彻底锁死。按 Ctrl+C、Ctrl+Z 全都没反应,过了两分钟直接弹出一行断开连接的报错:client_loop: send disconnect: Broken pipe。
很多人碰到这种情况第一反应是服务器带宽被占满了或者公网丢包严重。但如果你顺手 ping 一下服务器,延迟常常稳定在三四十毫秒,一个包都不丢。
服务器网络本身并没有毛病,本质是两个老问题撞在了一起:登录阶段遇到了 OpenSSH 服务端的应用层握手超时,会话阶段又掉进了 MTU 路径黑洞。
别凭直觉瞎猜,先用客户端详细日志定位卡点
排查 SSH 的任何卡顿,第一步永远是让客户端把握手全过程吐出来。
在终端执行:
ssh -vvv root@你的服务器IP
参数里的 -vvv 是最高级别的调试输出。这时候屏幕上会滚出一大堆日志,关键不是看它输出了什么,而是看光标在哪一行停顿了最长时间。
在实际排查中,最常见的两个停顿位置非常固定:
一个是刚连上 TCP 端口,还没开始交换密钥版本信息的时候,光标直接卡住十几秒没有任何新日志;另一个是已经开始协商认证方式,屏幕停在下面这一行不动:
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic,password
debug1: Next authentication method: gssapi-with-mic
只要看清楚是在哪一行停住,问题就解决了一大半。
坑一:UseDNS 反向解析超时(最常见的 15 秒卡顿元凶)
如果刚建完 TCP 连接就卡住,等了十几秒才继续往下走,百分之九十是服务端的 DNS 反向解析在搞鬼。
OpenSSH 服务端的配置文件 /etc/ssh/sshd_config 里,有一项历史遗留参数叫 UseDNS。在不少 Linux 发行版或者早期的默认模板里,这项配置默认是开着的。
它的工作机制是这样的:当客户端从某个 IP 发起 SSH 连接时,服务端 sshd 进程拿到这个客户端 IP,会主动去找本机配置的 DNS 服务器做一次 PTR(反向指针)查询,想把这个 IP 解析成对应的主机名。拿到主机名之后,甚至还会再做一次正向查询,对比解析出来的 IP 跟客户端 IP 是不是一致,用来防范所谓的 IP 欺骗。
但在实际场景中,绝大多数客户端都是办公室内网 IP、家庭宽带分配的动态公网 IP,或者云厂商内网私有地址。上游的公共 DNS 服务器根本不可能给这些 IP 配置反向 PTR 记录。
服务端拿着你的 IP 去问 DNS 服务器,DNS 查不到又只能等超时,通常每个请求要卡 5 到 15 秒。超时失败之后,sshd 才死心放行,继续后面的认证流程。
排查与解决
登录服务器,检查 /etc/ssh/sshd_config 文件:
grep -i "usedns" /etc/ssh/sshd_config
如果看到是 #UseDNS yes 或者显式配置了 UseDNS yes,直接改掉:
UseDNS no
修改完成之后,先用测试命令检查语法有没有写错:
sshd -t
只要没有报错输出,就重新加载服务配置:
systemctl reload sshd
不需要重启整个系统服务,reload 足够让新连接生效,而且不会踢掉当前已经连上的 SSH 会话。改完这一行,退出终端重新连一次,很多原来卡十几秒的主机瞬间就能秒进。
坑二:GSSAPI 认证超时(停在 gssapi-with-mic 的罪魁祸首)
如果日志显示停在 Next authentication method: gssapi-with-mic,那就是碰到了 GSSAPI 认证阻塞。
CentOS、RHEL 以及部分基于红帽衍生的系统,默认会把 GSSAPI 认证开启。这个机制主要服务于企业内网里的 Kerberos 单点登录环境。
当客户端连上来的时候,sshd 会优先尝试使用 GSSAPI 协议找 Kerberos KDC 密钥分发中心进行凭据协商。问题是大多数个人项目、普通业务服务器压根就没有部署 Kerberos 基础设施。
这时候无论客户端还是服务端,都会反复尝试向内网或默认域寻找 KDC 服务器,直到通信超时失败,才会退回到后面的 publickey(公钥认证)或者 password(密码认证)。这一个来回又要耗掉好几秒。
服务端彻底关闭
在服务器的 /etc/ssh/sshd_config 里确认并关闭:
GSSAPIAuthentication no
同样先跑 sshd -t 验证,再执行 systemctl reload sshd。
客户端本地规避
如果有时候连的是别人的服务器或者公司堡垒机,自己没有远程主机的 root 权限去改 sshd_config,也可以直接在自己的本地电脑上关闭客户端的 GSSAPI 协商。
编辑自己电脑上的 ~/.ssh/config,加入以下内容:
Host *
GSSAPIAuthentication no
这样本地客户端在发起握手时就会主动跳过 GSSAPI 这一步,直接使用密钥或密码认证,同样可以省去这段等待时间。
坑三:连上后小命令正常,一大段输出就假死?抓出 MTU 路径黑洞
解决了登录慢,很多人接着就会碰上第二个怪病:
输入 pwd、cd、uptime 一切丝滑,可一旦执行 cat test.log 看长文件、运行 docker logs,或者通过 git clone 拉取大仓库,光标闪两下就彻底死掉。键盘敲任何按键都没有回显,甚至 Ctrl+C 都中断不了,只能强行关掉终端标签页。
这个现象十有八九是触发了 PMTUD(路径最大传输单元发现)黑洞。
为什么只有大输出会卡死
标准以太网数据包的最大传输单元(MTU)通常是 1500 字节。TCP 握手时,两端协商出来的最大报文段长度(MSS)一般是 1460 字节(1500 减去 20 字节 IP 头和 20 字节 TCP 头)。
当你敲 ls 时,输出内容一共就几十个字节,打包成一个 IP 包飞过去,长度远小于 1500,一路畅通无阻。
但是当你敲 cat big_file 时,服务端一次性吐出大量数据,TCP 协议栈会按照最大的 MSS(1460 字节)拼装完整数据包往客户端发送。
如果客户端和服务器之间的网络链路不是直连,而是途经了 VPN、WireGuard、GRE 隧道、PPPoE 拨号光猫或者云厂商的 VxLAN 虚拟网络,这些隧道协议本身需要额外的封装包头,会导致中间某一段路由链路的实际 MTU 缩小到了 1420 甚至 1380。
正常的 TCP/IP 规范要求:当中间路由器发现一个数据包大于当前链路的 MTU,且数据包头打上了 DF(Don't Fragment,禁止分片)标志时,路由器应该把这个包丢掉,同时给发送端返回一个 ICMP 类型 3 代码 4 的差错报文(Fragmentation Needed),告诉发送端“我这里装不下,请把数据包改小到 1420 再重发”。
然而现实里许多防火墙、中继设备或者云厂商的安全组,出于所谓的安全考虑,在入站规则里直接把所有的 ICMP 报文一刀切全过滤掉了。
这就造成了黑洞:
- 服务端发出了 1500 字节的大包,中间路由器塞不进去,直接扔掉;
- 中间路由器试图给服务端发 ICMP 缩小包的通知,被路上的防火墙拦截了;
- 服务端永远不知道包已经丢了,一直卡在 TCP 重传等待 ACK;
- 客户端一直在等后半截数据,整个 SSH 会话在等待中彻底假死。
用 ping 探测链路的真实 MTU
要验证是不是这个问题,可以在本地机器上用带禁止分片标志的 ping 命令探测:
Linux 或 macOS 下执行:
ping -M do -s 1472 你的服务器IP
这里 -s 1472 加上 28 字节的 ICMP/IP 头正好是 1500。如果终端提示 Frag needed and DF set 或者干脆 100% 丢包收不到任何回包,说明链路上根本跑不过 1500 的包。
逐步减小数据包大小,比如测试 1400:
ping -M do -s 1372 你的服务器IP
如果 1372(加上报头共 1400)能正常收到回包,再慢慢往上加,就能测出这条通路上允许通过的最大封包尺寸。也可以直接用系统的路径探测命令:
tracepath 你的服务器IP
输出里会直接标出哪一跳路由把 pmtu 给降下来了。
修复方法:调整网卡 MTU 或启用 iptables MSS 钳制
有两种常用的解决方式。
方式一:直接调小网卡 MTU
如果明确知道是某一块虚拟网卡或者特定网卡有问题,可以在服务器或者本地把对应网卡的 MTU 调小:
ip link set dev eth0 mtu 1400
测试恢复之后,记得把这个配置写进对应发行版的网络配置文件(如 /etc/netplan/ 或 /etc/sysconfig/network-scripts/),防止机器重启后失效。
方式二:在网关或宿主机开启 TCP MSS 钳制(最省心)
如果你管理的是网关服务器,或者在 Docker/K8s 宿主机上,最稳妥的办法是用 iptables 的 mangle 表自动修剪 TCP 握手时的 MSS:
iptables -t mangle -A POSTROUTING -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
这条规则的意思是:在 TCP 三次握手建立连接时,防火墙会自动侦测当前路径的实际 MTU,把握手阶段声明的 MSS 自动压小到适合路径的大小。从根本上避免后续发送方组装出超限的大包。
坑四:切出几分钟就报 Broken pipe 断连?配置 KeepAlive 保活
很多人把连接慢和卡顿排查完之后,还会遇到另一个日常烦心事:切去浏览器查个资料,或者切到微信回复两条消息,两三分钟后再回到终端,敲一下回车直接卡住,几秒后提示:
packet_write_wait: Connection to 1.2.3.4 port 22: Broken pipe
这通常不是服务器挂了,而是中间 NAT 网关的会话超时了。
无论是家用路由器的 NAT 表、公司的防火墙,还是云厂商的公网 NAT 网关,都要维护一份 TCP 会话跟踪表(conntrack)。为了节约内存资源,网关通常会对没有数据交互的“空闲连接”设置极短的超时阈值(有些路由器甚至只有 60 秒到 120 秒)。
只要你几分钟没有打字,中间网关就会把这条映射关系悄悄清理掉。等你切回终端再敲键盘时,发出去的包在网关看来属于未知连接,直接丢弃或返回 RST。
优雅解决:让客户端或服务端主动发心跳
解决办法是在 SSH 加密通信层定期发送无害的空信令,刷新中间路由器的 NAT 超时计数器。
在本地电脑的 ~/.ssh/config 加上这组配置:
Host *
ServerAliveInterval 30
ServerAliveCountMax 3
这两行的含义是:本地客户端每隔 30 秒主动向服务器发一个心跳探测包;如果连续发了 3 次(即 90 秒)服务器都没有应答,才认定连接断开并主动退出。
这样无论你在本地停顿多久,后台都会每半分钟向外轻微“敲一下门”,中间的 NAT 映射表永远不会因为空闲而老化被剔除。
生产环境推荐配置清单
把上述几项配置整合起来,在排查新机器时,服务端 /etc/ssh/sshd_config 建议保持以下几项明确声明:
# 关闭反向 DNS 解析,消除连接建立时的 10-15 秒卡顿
UseDNS no
# 关闭未部署环境下的 GSSAPI 协商,消除认证阶段超时
GSSAPIAuthentication no
# 允许服务端保活探测(可选,适合从服务端防断连)
ClientAliveInterval 30
ClientAliveCountMax 3
而在自己常驻的开发机上,本地 ~/.ssh/config 推荐常备这组全局配置:
Host *
# 本地跳过 GSSAPI
GSSAPIAuthentication no
# 客户端保活心跳,防 NAT 偷摸断开
ServerAliveInterval 30
ServerAliveCountMax 3
# 开启 TCP 层的保持连接
TCPKeepAlive yes
把这些配置捋清楚,以后不管是接手新服务器登录卡顿,还是大输出时终端莫名假死,基本一分钟内就能快速顺藤摸瓜定位到底卡在哪一层。
文章标题:SSH 登录死活卡顿 10 秒、连上敲命令总假死?排查 UseDNS、GSSAPI 握手与 MTU 黑洞我踩过的几个坑
文章链接:https://www.llbbs.cn/jishujaocheng/212.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?