SSH连接排障:pam_systemd让登录卡了15秒
发布日期: 2026/08/01 阅读总量: 0

一、问题:输完密码,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=0debug1: 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.sopam_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.1s0.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。先分清卡在哪个阶段,再去动配置。