You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

分块上传S3大文件至SFTP报Net::SFTP::StatusException(5,"bad message")错

问题描述

从S3向SFTP服务器分多块传输大文件时,抛出Net::SFTP::StatusException (5, "bad message")错误,仅需单块即可传输的小文件可正常上传。

场景与复现代码

在RoR应用中需要将大文件发送至客户的SFTP服务器,读取S3文件使用Fog::Storage组件,原有实现代码如下:

file_paths = get_file_names()
Net::SFTP.start(sftp_domain, sftp_user, :password => sftp_password) do |sftp|
  file_paths.each do |file_to_upload|
    filename = nil
    if Rails.env == "production"
      filename = file_to_upload.file.path.split("/").last
                            
      # 初始化S3连接
      connection = Fog::Storage.new(provider: 'AWS'...)

      # 获取目标桶
      bucket = connection.directories.get(ENV['MY_S3'])

      # 分块逐次上传文件
      sftp.file.open(sftp_folder + "/" + filename, "w") do |sftp_file|
        bucket.files.get(file_to_upload.file.path) do |chunk|
          sftp_file.write(chunk)
        end
      end
    end
  end
end

触发条件

仅当单文件大小超过默认分块大小、需要分多块传输时,抛出如下错误:

Excon::Error::Socket: Net::SFTP::StatusException (5, "bad message") (Net::SFTP::StatusException)

环境信息

  • 框架版本:rails 4.2.5.1
  • Ruby版本:ruby 2.3.4p301 (2017-03-30 revision 58214) [aarch64-linux]
  • 依赖版本:Gem锁定net-sftp (3.0.0)、net-ssh (6.1.0)
  • 单块可容纳的小文件上传无异常

已尝试方案

  • 强制指定文件编码,未解决问题,仍报相同错误
  • 尝试将大文件一次性单块读取上传,触发内存错误无法执行

根因分析

错误码5对应SFTP协议的SSH_FX_BAD_MESSAGE,本质是服务端收到了顺序/格式不符合协议预期的报文,核心诱因是net-sftp默认的异步写入逻辑没有做流控:
调用sftp_file.write时方法不会阻塞等待服务端写入确认,会直接把写请求加入本地发送队列后返回。Fog拉取S3分块的速度远快于SFTP公网传输的速度,多块传输时会瞬间在发送队列堆积大量未确认的写请求,导致net-ssh层的SSH包序号错乱,服务端收到乱序报文后直接返回bad message错误。小文件仅触发1次写入,不存在队列堆积,因此不会报错。


解决方案

不要自己实现裸的分块写入逻辑,直接用net-sftp内置的流式上传能力,它内部已经实现了写入流控、包顺序校验,不会出现乱序问题,同时天然支持IO流式读取,不会把整个文件加载到内存触发OOM。

修正后代码

file_paths = get_file_names()
# S3连接初始化移到循环外,避免重复建连开销
s3_connection = Fog::Storage.new(provider: 'AWS'...)
s3_bucket = s3_connection.directories.get(ENV['MY_S3'])

Net::SFTP.start(sftp_domain, sftp_user, password: sftp_password) do |sftp|
  file_paths.each do |file_to_upload|
    next unless Rails.env.production?
    filename = file_to_upload.file.path.split("/").last
    remote_path = File.join(sftp_folder, filename)
    
    # 获取S3文件的流式IO对象,不会一次性加载全量内容到内存
    s3_file = s3_bucket.files.get(file_to_upload.file.path)
    # 调用net-sftp内置的流式上传,内部自动处理分块、流控、顺序校验
    sftp.upload!(s3_file, remote_path)
  end
end

自定义分块场景的适配方案

如果必须手动控制分块写入流程,每次写入后必须阻塞等待当前块写入完成再拉取下一块,避免请求堆积:

sftp.file.open(remote_path, "w") do |sftp_file|
  s3_bucket.files.get(file_to_upload.file.path) do |chunk|
    sftp_file.write(chunk)
    # 阻塞等待当前所有待处理写请求完成,再处理下一块
    sftp.loop unless sftp_file.pending_writes.empty?
  end
end

额外优化点

  • 原有代码在每个文件的循环内重复初始化S3连接,会产生大量不必要的TLS握手开销,移到循环外即可大幅提升上传效率
  • 路径拼接建议用File.join代替字符串拼接,自动处理路径分隔符问题,避免多写/漏写斜杠的bug

内容的提问来源于stack exchange,提问作者MordechayS

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 02:57:24