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

Rails应用使用Roo gem处理SFTP大Excel文件遇阻塞问题

问题描述

在Rails+Karafka应用中处理SFTP上的大Excel文件时遇到挂起问题:

  • 需下载并解析最大含100万条记录的.xlsx文件,每日最多2个文件,其中一个为大文件
  • 处理8MB/35000条记录的文件时,代码在Roo::Excelx.new(temp_file.path)处无限挂起,导致Karafka重复重试
  • 小文件(1000条记录)测试流程正常

环境版本:

  • Rails 7.0.5
  • Karafka 2.1.11
  • Roo 2.10.0

现有SFTP下载代码:

sftp.dir.glob(@file_path, "#{@file_regex}") do |file|
  begin
    Rails.logger.info "The File name is #{file.name}"
    file_data = sftp.download!("#{@file_path}#{file.name}")
    @downloaded_files << { file_name: file.name, file_data: file_data }
  rescue StandardError => e
    Rails.logger.error("Error occurred while pulling sftp data from file: #{@file_path}#{@file_regex} #{e.message}")
    raise StandardError.new(e.message)
  end
end

现有Excel解析代码:

def convert_excel_file_data
  temp_file = Tempfile.new(["sftp_#{SecureRandom.alphanumeric(8)}_#{file_name}", ".xlsx"])
  temp_file.write(file_data)
  temp_file.rewind
  Rails.logger.info "Tempfile created for file_name: #{file_name}"

  excel = Roo::Excelx.new(temp_file.path)
  header = excel.row(1)

  Rails.logger.info "Start fetching data form file_name: #{file_name}"
  items = []

  excel.each_row_streaming(offset: 1) do |row|
    row_data = Hash[header.zip(row.map(&:value))]
    items << row_data
  end

  Rails.logger.info "Data fetch completed for file_name: #{file_name}"

  temp_file.close
  temp_file.unlink # Remove the temporary file

  Rails.logger.info "Temfile deleted for file_name: #{file_name}, items count: #{items.length}"

  items
end
优化与排查方案

一、SFTP下载可靠性优化

当前用sftp.download!直接将整个文件加载到内存,大文件会导致内存飙升、GC阻塞,建议改为流式下载到本地临时文件,避免内存过载:

sftp.dir.glob(@file_path, "#{@file_regex}") do |file|
  begin
    Rails.logger.info "The File name is #{file.name}"
    local_temp_path = "#{Dir.tmpdir}/sftp_#{SecureRandom.alphanumeric(8)}_#{file.name}"
    # 流式下载到本地,避免一次性占满内存
    sftp.download!("#{@file_path}#{file.name}", local_temp_path)
    @downloaded_files << { file_name: file.name, local_path: local_temp_path }
  rescue StandardError => e
    Rails.logger.error("Error occurred while pulling sftp data from file: #{@file_path}#{file.name} #{e.message}")
    # 异常时清理临时文件,避免残留
    File.delete(local_temp_path) if File.exist?(local_temp_path)
    raise e
  end
end

优化优势:

  • 大文件不会一次性占用大量内存,降低OOM或GC停顿概率
  • 后续解析直接使用本地文件路径,减少内存到磁盘的二次写入步骤

二、Excel解析优化(解决Roo挂起问题)

1. 升级Roo版本

Roo 2.10.0存在大文件解析的性能缺陷,建议升级到最新稳定版(如2.10.5+),官方已修复部分流式解析的阻塞问题。

2. 优化Roo初始化与流式处理

确保each_row_streaming真正实现流式处理,同时取消将所有数据存入items数组(100万条记录会耗尽内存),改为边解析边分块入库:

def convert_excel_file_data
  Rails.logger.info "Start processing file_name: #{file_name}"
  # 直接使用本地下载的文件路径,跳过内存写入临时文件步骤
  excel = Roo::Excelx.new(local_path, streaming: true)
  header = excel.row(1)

  batch = []
  batch_size = 1000 # 根据数据库性能调整批次大小

  excel.each_row_streaming(offset: 1, pad_cells: true) do |row|
    row_data = Hash[header.zip(row.map(&:value))]
    batch << row_data

    if batch.size >= batch_size
      # 分块写入数据库
      YourModel.insert_all(batch)
      Rails.logger.info "Inserted #{batch.size} records for #{file_name}"
      batch.clear
    end
  end

  # 写入剩余未批量提交的记录
  unless batch.empty?
    YourModel.insert_all(batch)
    Rails.logger.info "Inserted remaining #{batch.size} records for #{file_name}"
  end

  Rails.logger.info "Data processing completed for file_name: #{file_name}"
  # 清理本地临时文件
  File.delete(local_path) if File.exist?(local_path)
end

关键优化点:

  • 初始化Roo时添加streaming: true参数,强制启用流式解析模式
  • 取消items数组,边解析边入库,彻底避免内存溢出
  • 用pad_cells: true确保行列数对齐,避免哈希键值匹配错误

3. 替换为更高效的解析库

如果升级Roo后仍有问题,可考虑替换为rubyXL(支持流式解析分支),或要求上游输出CSV格式(CSV解析性能远高于Excel)。

三、挂起问题排查步骤

  1. 监控进程状态:用top/htop查看Ruby进程的CPU、内存占用。若CPU持续100%,可能是Roo陷入死循环;若内存飙升,是内存溢出导致的GC阻塞。
  2. 添加细粒度日志:在Roo::Excelx.new前后增加日志,同时开启Ruby GC日志(export RUBY_GC_LOG_ENABLED=1),排查是否存在长时间GC停顿。
  3. 单独测试解析逻辑:将大文件下载到本地,用独立Ruby脚本测试Roo解析,排除Karafka或SFTP的影响:
require 'roo'

excel = Roo::Excelx.new('path/to/large_file.xlsx', streaming: true)
header = excel.row(1)
count = 0
excel.each_row_streaming(offset: 1) do |row|
  count +=1
  puts count if count % 1000 ==0
end
puts "Total rows: #{count}"

若脚本也挂起,说明是Roo本身的问题,直接升级或替换库即可。

四、Karafka配置优化

大文件处理耗时较长,需调整Karafka超时配置,避免被误判为任务失败重试:

# config/karafka.rb
Karafka.configure do |config|
  # 调整单个消息处理超时时间,例如设置为30分钟
  config.max_wait_time = 180_000
  # 配置合理的重试次数,避免重复处理大文件
  config.dead_letter_queue = {
    max_retries: 3,
    topic: 'dead_messages'
  }
end

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 07:55:40