SOP — SSH 连接复用在本工程落地(slurm_ins 副本与共享库对齐)¶
主题目录:
conn_reuse_20261007/|执行日期:2026-10-07|类型:C 类(改实现体)
1. 背景¶
ops-execution-efficiency §2 的口径是「要降的是会话数(握手数),不是命令条数」。
measured-baselines.md §三 记着 2026-09-30 已完成「SSH 连接复用」改造,实测 deploy 幂等复跑握手 44 → 14、耗时 26.5 s → 17.1 s。
但本次核查发现:那次改造落在共享库 /home/rain/py_public_modules/paramiko_module.py(备份见 k8s_ins/conn-reuse-refactor-20260930/backup/home/rain/py_public_modules/),
而 slurm_ins 的 deployer.py 用相对导入 from .utils.paramiko_module import ...,取的是本工程内的副本 slurm_ins/utils/paramiko_module.py —— 该副本从未同步那次改造。
副本的缺陷(改前实测):
SSHConnectionPool.get_connection()的 docstring 直书「每次创建新连接」,self._connections字典从未被写入(只在close_connection/shutdown里被读)→ 池化形同虚设;exec_linux/upload_file/download_file都在finally里conn.close()→ 每次远端调用建一条新连接、用完即关。
因此本工程(Slurm 6 节点)的每次 --dry-run 都在重复握手。2026-10-07 基线:slurm 主部署器 dry-run 墙钟 221.0 s(已含同期 sync_status 批量化的收益)。
另发现:
slurm_ins/utils/paramiko_module.py存在两份模块内容拼接的历史痕迹(第 1–423 行是一份含SSHConnection的模块,第 424 行起是第二份含SSHConnectionPool等;Python 后定义覆盖前定义,恰好能运行)。本次不动该结构,仅在 SOP 留痕,供后续清理参考。
2. 目标¶
| # | 目标 | 判据 |
|---|---|---|
| 1 | 本工程副本与共享库行为对齐(_pooled 归还语义) |
离线自测通过;close() 对池内连接为「归还」、对池外为「真关」 |
| 2 | 任务内每台主机只建连一次 | shutdown() 日志「本任务共新建连接 N 次」,N = 主机数 |
| 3 | 判定结果不变 | dry-run 变更记录逐条一致(225 条 / 待执行 25 / 幂等 200) |
3. 前置条件¶
- 项目根
/home/rain/Documents/slurm/slurm_ins,.venv可用; - 到 6 台节点的 SSH 凭据沿用
config/slurm_deploy.yaml(600,凭据不入文档); - 变更前完成 5.1 备份。
4. 影响范围¶
| 项 | 内容 |
|---|---|
| 变更对象 | utils/paramiko_module.py(单文件) |
| 影响面 | 全工程所有远端操作(exec_linux / upload_file / download_file 是唯一入口) |
| 是否停服 | 否;验证全程 --dry-run(只读探测) |
| 窗口期 | 无要求;集群无在跑作业 |
| 并发影响 | 同主机远端操作经 per-host RLock 串行;不同主机仍并发 |
5. 分步操作¶
5.1 备份(高危前置)¶
cd /home/rain/Documents/slurm/slurm_ins
mkdir -p conn_reuse_20261007/backup/slurm_ins/utils
cp -a utils/paramiko_module.py \
conn_reuse_20261007/backup/slurm_ins/utils/paramiko_module.py.bak.$(date +%Y%m%d-%H%M%S)
ls -l conn_reuse_20261007/backup/slurm_ins/utils/
预期:1 个 .bak.<时间戳>(本次 STAMP = 20261007-043236)。坑:必须在任何编辑前完成。
5.2 SSHConnection:close() 拆成「归还 / 真关」¶
在 SSHConnection.__init__ 末尾加标记位;把 close() 的语义按共享库对齐(实现体:utils/paramiko_module.py):
# 概念等价(Python 侧实现,非 shell)
# __init__: self._pooled = False
# close(): if self._pooled and self._is_active: 释放 SFTP、保留 transport、return ← 归还
# else: close_force()
# close_force(): self._cleanup_internal(); self._is_active = False ← 真关
坑:_pooled 默认为 False,故 with SSHConnection(...) as c: 等池外用法行为不变(仍真关);只有 get_connection() 返回的连接会被置 True。
5.3 SSHConnectionPool:池化命中 + 计数 + per-host 可重入锁¶
# 概念等价(Python 侧实现)
# get_connection(host):
# host_lock = acquire_host_lock(host) # RLock,可重入
# with host_lock:
# cached = self._connections.get(host)
# if cached and cached.is_active(): return cached ← 命中复用
# if cached: cached.close_force(); pop ← 失活则清掉重建
# conn = SSHConnection(...); conn.connect()
# conn._pooled = True; self._connections[host] = conn
# self._active_connections += 1; self._connect_count += 1
# return conn
# shutdown(): 打印「本任务共新建连接 N 次 | 结束时保有 M 条」,逐条 close_force()
坑(必须留痕):
_host_locks由threading.Lock()改为threading.RLock():exec_linux持锁后再调get_connection,后者内部还要拿同一把锁 —— 用普通Lock会自锁死。close_connection()/shutdown()/ 失活清理一律用close_force():这些场景要的是真关并从池移除,用close()会变成「归还」,连接留在池里泄漏。_connect_count只是诊断计数,用于验证复用效果,不参与任何判定。
5.4 三处调用点恢复「归还」语义¶
exec_linux / upload_file / download_file 三个函数的 finally 保留 conn.close()(此时语义已是「归还」):
# 概念等价
# finally:
# if conn:
# conn.close() # 池内连接 → 归还(保留 transport 供复用);池外 → 真关
坑:这一步不能省——它保证 SFTP 通道及时释放;连接由 run() 收尾的 connection_pool.shutdown() 统一真关。
5.5 离线自测(上机前)¶
cd /home/rain/Documents/slurm/slurm_ins
.venv/bin/python -m py_compile utils/paramiko_module.py && echo COMPILE_OK
.venv/bin/python conn_reuse_20261007/selftest.py
预期:COMPILE_OK + 4 项断言 + CONN_REUSE_SELFTEST_OK connect_calls=3。
5.6 上机复验(只读)¶
cd /home/rain/Documents/slurm/slurm_ins
timeout 400 .venv/bin/python main.py --config config/slurm_deploy.yaml --dry-run 2>&1 | tee /tmp/connreuse.txt | tail -20
6. 验证¶
验证一:语义等价 —— dry-run 变更记录与基线逐条一致:
变更记录总数: 225 待执行变更: 25 幂等跳过: 200 (三次运行均同)
验证二:连接复用生效(本次新增的实证出口):
SSH连接池已关闭 | 本任务共新建连接 6 次 | 结束时保有 6 条
6 台主机 → 每台恰好 1 次建连,全程复用(改前为「每次远端调用一条连接」)。
验证三:墙钟(slurm 主部署器 --dry-run,同一命令):
| 状态 | 墙钟 | 建连 |
|---|---|---|
原始(sync_status 未批量化、无复用) |
247.7 s | 每次调用建连 |
+ sync_status 批量化(另一主题) |
221.0 s | 同上 |
| + 连接复用(本变更) | 134.6 / 135.8 s(两次) | 6 次 |
相对原始累计 -113 s(-45.6%);其中连接复用贡献 -86 s,是比 sync_status 批量化(-27 s)更大的杠杆。
验证四:离线自测 4 项 + POOLED_SEMANTICS_OK(池内 close() 为归还、close_force() 为真关)。
失败判断:任一 dry-run 变更记录与 225/25/200 不符,或建连数明显大于主机数 → 按第 7 章回滚。
7. 回滚¶
- 备份位置:
conn_reuse_20261007/backup/slurm_ins/utils/,台账见conn_reuse_20261007/backup/README.md。 - 回滚步骤:
cd /home/rain/Documents/slurm/slurm_ins
cp -a conn_reuse_20261007/backup/slurm_ins/utils/paramiko_module.py.bak.* utils/paramiko_module.py
- 回滚后验证:
py_compile+ 跑一次--dry-run,确认变更清单仍为 225/25/200。 - 回滚触发条件:变更记录不符;建连数 > 主机数;出现连接相关异常(
Transport失效、命令超时激增)。
8. 风险说明¶
| 风险 | 缓解 |
|---|---|
| 长驻连接被 sshd 侧超时断开 | connect() 本身有「活跃则复用」判断;get_connection 命中前先验 is_active(),失活即清理重建 |
| 同主机并发使用同一连接 | 引入 per-host RLock 串行化;_remote_query_many 本就按主机分线程,不同主机仍并发 |
| 连接泄漏(任务结束不关) | BaseDeployer.run() 收尾固定调用 connection_pool.shutdown(),逐条 close_force() 并打印保有数 |
| 池外用法行为被改坏 | _pooled 默认 False,with 语法与直接构造仍走真关路径 |
| 与共享库再次分叉 | 本次按共享库同一语义实现(_pooled + close_force),行为已对齐;两处并存属历史结构问题,见附录 B |
附录 A 交付物清单¶
| 文件 | 说明 |
|---|---|
SOP.md |
本文件 |
selftest.py |
离线自测(monkeypatch connect/is_active,不连主机) |
backup/slurm_ins/utils/paramiko_module.py.bak.20261007-043236 |
改前快照 |
backup/README.md |
备份台账 |
附录 B 遗留(不在本次范围)¶
- 本工程副本 vs 共享库双份并存:
slurm_ins/utils/paramiko_module.py(本文件,相对导入在用)与/home/rain/py_public_modules/paramiko_module.py(.pth挂载,其它工程用)各存一份。本次已做行为对齐,但结构分叉仍在——后续是否统一到共享库,需单独决策(涉及.venv/.pth与导入路径)。【2026-10-11 结案】 已由paramiko_unify_20261007决策并落地:删本工程 3 份同名副本、统一走共享库.pth。 - 文件内含两份模块内容拼接(1–423 行 / 424 行起),当前可运行但易误读,建议另起一轮清理。【2026-10-11 结案】 副本删除后该拼接结构已消失(共享库为拍平版),无需再清理。
sync_status批量化在submit_node/storage_layout/scratch_gc/local_scratch/node_env+module_env上的遗留已于 2026-10-07 全部完成(submit_node_deployer.py::_sync_host的收尾见../submit_node_batch_20261007/SOP.md)。原「仍未做」表述已过期——2026-10-07 复核时曾据此误判,附录已一并更正。
来源:
/home/rain/Documents/slurm/slurm_ins/conn_reuse_20261007/SOP.md| 最后更新:2026-10-11 21:15(由build/sync.py同步,请勿在站点侧修改)