当前位置:论坛首页 > Linux面板 > 建议

【BUG反馈】FTP存储空间插件 SFTP 上传无超时与 keepalive 机制...

发表在 Linux面板昨天 23:01 [复制链接] 0 18

【BUG反馈】FTP存储空间插件 SFTP 上传无超时与 keepalive 机制,对端掉线后进程永久阻塞,配合 flock -xn 导致后续所有计划备份静默失效

一、问题概述

使用「计划任务 → 备份网站(所有)」+「FTP存储空间(SFTP)」将备份上传到异地 NAS 时,
若上传过程中对端网络中断,备份进程会永久阻塞不退出。由于计划任务使用 flock -xn
非阻塞锁,该僵死进程会一直持有锁,导致此后每天的备份任务启动即静默退出,
不执行、不报错、不告警。

我的站点因此连续 39 天没有任何网站备份,且期间面板无任何异常提示,
直到手动排查才发现。


二、环境信息

宝塔面板版本:13.0.0(已于 2026-08-13 升级到最新版,问题依旧存在)
FTP存储空间插件版本:6.0(插件 info.json 标注日期为 2020-05-23)
paramiko 版本:2.7.1
Python 版本:3.7.16
操作系统:Debian GNU/Linux 12 (bookworm)
内核版本:6.1.0-39-amd64
备份目标:家庭宽带 NAS,SFTP,通过 DDNS 域名连接(公网 IP 会变动)

补充说明:本次反馈前我已将面板升级至最新版本。升级后面板核心文件
(class/common.py、tools.py、BT-Panel 等)均已更新,但
plugin/ftp/ftp_main.py 的文件修改时间与 MD5 校验值均未发生变化,
grep -c "set_keepalive" 结果仍为 0,即本文所述问题在最新版中依然存在。


三、现象

计划任务「备份网站[所有]」配置为每天 02:30 执行,但自 2026-07-05 之后再无任何
新备份产生。面板计划任务页面显示任务正常,无失败标记;任务日志文件的最后写入
时间停留在 2026-07-05 02:42,之后再无任何新增内容。

任务日志最后几行:

    |-备份网站:example.com
    |-目录大小:16.04 GB
    |-开始压缩文件:2026-07-05 02:30:06
    |-文件压缩完成,耗时749.08秒,压缩包大小:15.53 GB
    |-网站已备份到:/www/backup/site/example.com/web_example.com_20260705_023001_XXXXXX.tar.gz
    |-正在上传到SFTP,请稍候...
    |-正在上传文件到 /bt_backup/site/example.com/web_example.com_20260705_023001_XXXXXX.tar.gz
    (日志到此为止,无后续输出)

NAS 端只收到约 2.26GB 的不完整文件(完整应为 15.53GB)。


四、排查过程与证据

1) 僵死进程仍然存在,已运行 39 天

    # ps -p 3464252,3464254 -o pid,etime,stat,cmd
        PID     ELAPSED STAT CMD
    3464252 39-20:13:37 S    /bin/bash /www/server/cron/9e5b3566xxxxxxxx
    3464254 39-20:13:37 Sl   /www/server/panel/pyenv/bin/python3 -u
                             /www/server/panel/script/backup.py site ALL 3 9e5b3566xxxxxxxx

   进程状态 Sl,WCHAN 为 futex_wait_queue,即阻塞在线程同步等待上。

2) 进程仍持有指向 NAS 的 TCP 连接,且为"半死连接"

    # ss -tnp | grep <NAS_IP>
    ESTAB  0  0  <本机IP>:48174  <旧NAS_IP>:22  users("python3",pid=3464254,fd=4))
    ESTAB  0  0  <本机IP>:48186  <旧NAS_IP>:22  users("python3",pid=3464254,fd=5))

   连接状态为 ESTABLISHED,但收发队列均为 0。经测试该 IP 已完全不可达
   (NAS 为家庭宽带动态 IP,上传过程中 IP 发生了变更,对端未发送 FIN/RST)。

   关键点:因 send-q 为 0,内核没有待重传数据,tcp_retries2 不会触发;
   而 paramiko 未启用 SO_KEEPALIVE,tcp_keepalive_time 同样不生效。
   因此内核层面永远无法发现对端已消失,只能依赖应用层超时——而应用层没有。

3) flock -xn 使故障永久化

   计划任务的 crontab 条目:

   30 2 * * * flock -xn /www/server/cron/9e5b3566xxxxxxxx.lock -c /www/server/cron/9e5b3566xxxxxxxx >> ... 2>&1

   -n 表示拿不到锁立即退出。僵死进程一直持有该锁,因此从 2026-07-05 起
   每天 02:30 的任务都是"启动即退出",既不执行也不留下任何日志或告警。


五、根本原因(代码定位)

文件:/www/server/panel/plugin/ftp/ftp_main.py

1) 第 1696-1697 行,paramiko 连接未设置任何超时,也未启用 keepalive:

    transport = paramiko.Transport((self.__host, int(self.__port)))
    transport.connect(username=self.__user, password=self.__password)

   paramiko.Transport() 未传 timeout;transport.connect() 未传 timeout;
   全文件未出现任何 transport.set_keepalive() 调用。

2) 第 1892 行 get_client() 虽然接收 timeout 参数,但从未向下传递:

    def get_client(use_sftp=None, load_config=True, timeout=None):
        ...
        if use_sftp:
            _client = BTSFTPClient(load_config=load_config)   # timeout 被丢弃
        else:
            _client = BTFTPClient(load_config=load_config)    # timeout 被丢弃

   且第 1585 行 BTSFTPClient.__init__() 的签名本身就不接收 timeout:

    def __init__(self, load_config=True, config_file=None):

   而父类 FTPClient.__init__()(第 1070 行)是支持 timeout 的。
   因此 self.timeout 恒为 None,后续所有 subprocess 调用中的
   timeout=self.timeout 实际等于"永不超时"。

   对比:同文件第 1518 行的 lftp 上传路径硬编码了 timeout=3600,
   说明超时保护在其他路径是有考虑的,SFTP 这条路径属于遗漏。


六、影响范围

- 任何使用 SFTP 存储空间做异地备份的用户都可能触发
- 家庭宽带/动态 IP/移动网络等不稳定链路的用户触发概率更高
- 大文件备份因传输时间长,暴露窗口更大(我的站点压缩包 15.53GB,上传需 1 小时以上)
- 最严重的是"静默失败":面板不报错、不告警、计划任务显示正常,
  用户完全无法察觉,直到需要恢复数据时才发现无备份可用

自动备份的价值在于无人值守。如果需要用户每天人工核对是否成功,就失去了意义。


七、建议修复

1) 为 paramiko 连接增加超时与保活(关键)

    transport = paramiko.Transport((self.__host, int(self.__port)))
    transport.banner_timeout = 30
    transport.connect(username=self.__user, password=self.__password)
    transport.set_keepalive(30)     # 每 30 秒发送保活包,对端消失时可及时感知

   同时建议给底层 socket 设置读写超时。

2) 修复 get_client() 的 timeout 参数传递链

   使 BTSFTPClient.__init__() 接收并向父类传递 timeout,
   避免 self.timeout 恒为 None。

3) 为单次备份任务增加整体执行时长上限

   超过阈值主动终止并在日志中记录失败原因,而不是无限等待。

4) 增加"僵死任务"检测与告警

   计划任务启动时若发现锁被占用,应记录日志并告警,
   而不是静默退出。当前实现下用户完全无从察觉。

5) 建议升级 paramiko

   当前捆绑版本 2.7.1 发布于 2020 年,已较为陈旧。

6) 建议将 FTP存储空间插件纳入常规维护

   该插件 info.json 标注日期为 2020-05-23,版本 6.0,
   且面板升级时不会同步更新该插件文件。
   但它承担着异地备份这一关键职责,建议纳入常规维护与更新计划。



八、临时规避方式(供遇到同样问题的用户参考)

1) 检查是否存在僵死进程:

    ps -eo pid,etime,stat,cmd | grep "backup.py"

   若发现 ELAPSED 达到数天,即为本问题。

2) 杀掉僵死进程以释放锁(会同时杀掉父 bash,否则锁不释放):

    kill -9 <python进程PID> <父bash进程PID>
    rm -f /www/server/cron/<任务ID>.pl

3) 确认锁已释放:

    flock -xn /www/server/cron/<任务ID>.lock -c "echo 锁已释放"

处理后下一次计划任务即可正常执行。

希望官方能在后续版本中修复,感谢。

使用道具 举报 只看该作者 回复
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

紧急运维服务

响应时间:3分钟

问题处理方式:宝塔专家1对1服务

工作时间:工作日:9:00 - 18:30

宝塔专业团队为您解决服务器疑难问题

点击联系技术分析

工作时间:09:00至18:30

快速回复 返回顶部 返回列表