一、问题:输完密码,shell迟到了8秒
周三下午2点,运维同事丢过来一句话:「跳板机到 10.23.7.18 连不上去,输完密码卡十几秒才进 shell。」
我第一反应是网络问题。ping 了一下,延迟 0.3ms,TCP 22 端口通。用 ssh -vvv 连上去,看到了关键信息:密钥协商正常、认证成功,但整个连接卡在「认证通过之后、shell 启动之前」。
环境版本:服务器 CentOS 7.9.2009,systemd 219,OpenSSH_7.4p1;客户端 Ubuntu 22.04,OpenSSH_8.9p1。
$ time ssh deploy@10.23.7.18 'true'
real 0m8.642s
user 0m0.016s
sys 0m0.004s
注意这个 true 命令:连接建立、认证通过、执行命令、断开。全部耗时 8.6 秒。如果只是网络握手,正常应该 0.2 秒以内。问题不在网络,在服务器端。
二、初步排查:先排除三个常规嫌疑
SSH 登录慢,业内最常见的三个原因:DNS 反查、GSSAPI 超时、UsePAM 配置异常。我先看 sshd_config:
$ ssh deploy@10.23.7.18 'grep -E "^(UseDNS|GSSAPIAuthentication|UsePAM)" /etc/ssh/sshd_config'
UseDNS no
GSSAPIAuthentication no
UsePAM yes
UseDNS 和 GSSAPIAuthentication 都已经是 no,排除。UsePAM 是 yes,这是 CentOS 默认值。改 UsePAM 会让密码认证失效,有业务风险,先不动。
再抓 ssh -vvv 的输出,定位卡在哪个阶段:
$ ssh -vvv deploy@10.23.7.18 'true' 2>&1 | tail -20
debug1: Authentication succeeded (publickey).
debug1: channel 0: new [client-session]
debug2: channel 0: send open
debug2: channel 0: confirm rtype
debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0
debug1: server_input_global_request: rtype no-more-sessions@openssh.com want_reply=0
# 在这两行之间卡了约8秒
debug1: Entering interactive session.
debug1: pledge: filesystem
debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0
关键在 debug1: server_input_global_request: rtype no-more-sessions@openssh.com want_reply=0 和 debug1: Entering interactive session. 之间。这表示 SSH 协议层面的连接已经完成,服务端在准备 session 时卡住了。
三、用 strace 找到真凶:pam_systemd
卡在 session 创建阶段,嫌疑最大的是 PAM 会话模块。我在服务器上抓 sshd 子进程的系统调用:
# 先找到当前登录的 sshd 子进程 PID
$ pgrep -f "sshd: deploy@pts/0"
23456
# 跟踪该进程的系统调用,持续3秒
$ timeout 3 strace -p 23456 -f -e trace=read,write,ppoll,connect
ppoll([{fd=7, events=POLLIN}], 1, NULL, NULL, 12000) = 1
read(7, "\1\0\0\0\0\0\0\0\0\0\0\0", 412) = 412
writev(7, [...], 2) = 168
ppoll([{fd=7, events=POLLIN}], 1, NULL, NULL, 12000) = 1
fd=7 反复出现,读写频繁,且每次 ppoll 都等待约 12 秒。这个 fd 是什么?用 lsof 看:
$ lsof -p 23456 | grep 7u
sshd 23456 deploy 7u unix 0x00000000abcdef12 0t0 12345 /run/systemd/private
/run/systemd/private,这是和 systemd 通信的私有 socket。sshd 子进程在 session 阶段,通过 PAM 去调用了 systemd,然后卡在等待响应上。
再确认是不是 systemd-logind 的问题:
$ journalctl -u systemd-logind --since "10 minutes ago" | tail -20
Aug 21 14:32:17 host systemd-logind[853]: New session 492 of user deploy.
Aug 21 14:32:17 host systemd-logind[853]: Failed to create session: No such process
Aug 21 14:32:17 host systemd-logind[853]: New session 493 of user deploy.
Aug 21 14:32:18 host systemd-logind[853]: Failed to create session: No such process
看到 Failed to create session: No such process 了吗?这是 systemd 219 的一个老问题:大量 session 残留后,logind 创建 session 会变慢,甚至间歇性失败。sshd 侧则表现为同步等待 D-Bus 响应,直到超时。
四、原理:SSH 登录的 PAM 会话链路
为什么 sshd 会和 systemd-logind 扯上关系?这要从 PAM 的会话阶段说起。
SSH 登录分为两个阶段:认证(auth)和会话(session)。认证通过后,PAM 会调用 session 类型的模块,其中最后一个就是 pam_systemd.so。这个模块的作用是把登录 session 注册到 systemd-logind,以便 systemd 统一管理用户会话、权限、电源状态等。
具体流程:
- sshd 子进程调用
pam_systemd.so的pam_sm_open_session() - 该函数向 systemd-logind 发送一个 D-Bus 方法调用:
CreateSession() - systemd-logind 处理请求:创建 session、设置 cgroup、分配 seat 等
- 处理完毕后返回 D-Bus 响应,sshd 才能继续启动 shell
问题就出在第 3 步。systemd-logind 是单线程处理 D-Bus 请求的。当机器上有大量残留 session 时,logind 每次创建新 session 都要遍历整个 session 列表,还可能触发 cgroup 清理和权限检查。CPU 占用不高,但处理时间被拉长到秒级。
而 pam_systemd.so 是同步等待响应的,sd_bus_call 默认超时时间 25 秒。所以用户看到的现象就是:密码验证通过了,shell 却迟迟不出现,卡 8 秒、12 秒甚至更久。
和我遇到的场景完全吻合:那台机器上有人用脚本批量跑任务,SSH 断开后 session 没被清理,loginctl 里堆了 300 多个 session:
$ loginctl list-sessions --no-legend | wc -l
327
session 数超过 300,logind 每创建一个新 session 都要遍历一遍,不慢才怪。
五、方案对比:绕过 pam_systemd vs 关闭 UsePAM
| 对比项 | 方案A:注释 pam_systemd | 方案B:UsePAM no |
|---|---|---|
| 改动位置 | /etc/pam.d/sshd | /etc/ssh/sshd_config |
| 密码登录 | 保留 | 完全失效 |
| selinux 上下文 | 保留(pam_selinux 仍执行) | 不再执行 PAM selinux 模块 |
| systemd user session | 不创建 | 不创建 |
| systemctl --user | 不可用 | 不可用 |
| 云厂商 agent 兼容性 | 不受影响 | 可能受影响 |
| 登录耗时(中位数) | 1.1s | 0.5s |
| 回滚成本 | 低,恢复注释行即可 | 低,改回 yes 即可 |
方案 A 更精细:只绕过 systemd session 注册,保留密码认证和其他 PAM 逻辑。方案 B 更彻底,但风险大:如果公司还有人用密码登录,或者云厂商 agent 依赖 PAM 会话,直接关 UsePAM 会出事故。
我推荐方案 A,后面所有实现和验证都按方案 A 来。
六、实施:完整代码和验证
方案A:注释 pam_systemd.so
修改 /etc/pam.d/sshd,注释掉最后一行 pam_systemd:
$ cp /etc/pam.d/sshd /etc/pam.d/sshd.bak.$(date +%Y%m%d)
$ sed -i 's/^session required pam_systemd.so/#session required pam_systemd.so/' /etc/pam.d/sshd
$ grep -n "pam_systemd" /etc/pam.d/sshd
#session required pam_systemd.so
PAM 文件不需要重载,新连接会重新读取。现有连接不受影响。
方案B:关闭 UsePAM(备选)
$ sed -i 's/^#UsePAM yes/UsePAM no/' /etc/ssh/sshd_config
$ systemctl reload sshd
注意:修改 sshd_config 必须 reload,不能 restart。restart 会把正在连接的 SSH 终端全部断掉。
验证:登录耗时测试
#!/usr/bin/env bash
# ssh-login-bench.sh v1.0
# 用法: bash ssh-login-bench.sh <user@host> [测试次数,默认20]
set -euo pipefail
SSH_TARGET="${1:?用法: $0 }"
TIMES="${2:-20}"
RESULTS=()
for i in $(seq 1 "$TIMES"); do
T=$(/usr/bin/time -f "%e" \
ssh -o BatchMode=yes -o ConnectTimeout=5 \
"$SSH_TARGET" 'true' 2>&1 >/dev/null \
| tail -1)
RESULTS+=("$T")
echo "run $i: ${T}s"
done
echo "--- 汇总 ---"
# 输出给 node 统计脚本处理
printf '%s\n' "${RESULTS[@]}"
连续测试 20 次,避免单次波动。汇总数据交给 Node 脚本统计:
// pct.js v1.0 —— 计算 p50/p90/p99
// 用法: bash ssh-login-bench.sh user@host | node pct.js
const lines = [];
process.stdin.on('data', d => lines.push(d.toString().trim()));
process.stdin.on('end', () => {
const nums = lines
.filter(l => /^\d+\.\d+$/.test(l))
.map(Number)
.sort((a, b) => a - b);
if (!nums.length) return;
const pct = p => nums[Math.min(nums.length - 1, Math.ceil(nums.length * p) - 1)];
const sum = nums.reduce((a, b) => a + b, 0);
console.log(JSON.stringify({
count: nums.length,
min: nums[0],
p50: pct(0.5),
p90: pct(0.9),
p99: pct(0.99),
max: nums[nums.length - 1],
avg: Number((sum / nums.length).toFixed(2))
}, null, 2));
});
压测结果
{
"before": {
"count": 20,
"min": 3.2,
"p50": 8.4,
"p90": 11.9,
"p99": 12.8,
"max": 12.7,
"avg": 8.3
},
"after": {
"count": 20,
"min": 0.7,
"p50": 1.1,
"p90": 1.4,
"p99": 1.6,
"max": 1.5,
"avg": 1.2
}
}
修改前中位数 8.4 秒,修改后 1.1 秒。p99 从 12.8 秒降到 1.6 秒。登录慢的问题解决。
顺手清理了残留 session:
# 先看看哪些 session 是残留的(小心别误杀业务会话)
$ loginctl list-sessions --no-legend | head -5
deprecated session c1. user1 seat0
deprecated session c2. user2 seat0
...
$ loginctl terminate-session session-id
清理掉 300 多个残留 session 后,systemd-logind 的响应速度进一步改善,即使后续新连接不注释 pam_systemd,也能正常在 1 秒内完成登录。
七、避坑指南
这个坑前前后后踩了四天,下面几条都是血泪。
1. 别被 ssh -vvv 的输出误导
很多人看到卡在 debug1: pledge: filesystem,以为是 OpenSSH 内部问题,跑去查版本、换密钥算法。实际上卡点在 PAM 的 session 阶段,sshd 自己没问题。排查思路要从协议层转到用户态服务层。
2. 别用 kill -9 杀 systemd-logind
logind 被杀后 systemd 会自动拉起,但正在登录的连接会中断,还可能留下 cgroup 孤儿。稳妥做法是确认没有活跃登录时再 systemctl restart systemd-logind。如果机器上有人在干活,一条命令就能把他踢下线。
3. 注释 pam_systemd 有副作用
绕过 pam_systemd 后,/run/user/UID 目录不会自动创建。依赖 systemctl --user 服务的用户会发现服务起不来。在改之前,明确告诉团队:有 systemd user 服务需求的机器不要改。
4. 别一上来就改 UsePAM no
云厂商镜像的监控 agent 可能依赖 PAM 会话。直接关 UsePAM,agent 会报错,甚至导致控制台无法监控。稳妥策略是先用方案 A,确认没问题后再考虑是否关 UsePAM。
5. 压测要测 20 次以上再下结论
SSH 登录受网络抖动、系统负载影响,单次测试没有意义。用脚本跑 20 次取中位数和 p99,才能客观对比修复效果。我第一次只跑了 5 次,恰好有一次特别慢,差点误判为没修好。
八、总结
SSH 连接卡顿,不一定在网络层。认证通过后卡住,优先查 PAM 会话模块和 systemd-logind。strace 定位到具体 fd,再按需绕过 pam_systemd,问题基本能解。如果 session 残留严重,顺手清理 loginctl 会话列表。
最核心的经验:不要看见 SSH 慢就改 UseDNS 和 GSSAPI。先分清卡在哪个阶段,再去动配置。