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)。
三、挂起问题排查步骤
- 监控进程状态:用
top/htop查看Ruby进程的CPU、内存占用。若CPU持续100%,可能是Roo陷入死循环;若内存飙升,是内存溢出导致的GC阻塞。 - 添加细粒度日志:在
Roo::Excelx.new前后增加日志,同时开启Ruby GC日志(export RUBY_GC_LOG_ENABLED=1),排查是否存在长时间GC停顿。 - 单独测试解析逻辑:将大文件下载到本地,用独立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
相关产品推荐
相关产品推荐

