Rails 批量数据该怎么处理才能减少耗时 (如何用 EventMachine 发送多 http 请求)

yutian · June 06, 2014 · Last by mystery replied at June 10, 2014 · 3472 hits

要处理业务系统一张表里面的数据,需要把每一个记录都遍历一次才能任务 job,大概有 10w 数据,通过前台发送一个请求后台使用 find_in_batches 来处理,因为 job 任务处理有些耗时,处理这些数据的时候服务器不能接受其他请求,想把任务转为后台处理,看到网上有说线程,也有说用进程的,试了试 sidekiq 但是效果好些没有什么改进,就采用下面的方法:但是任务处理的很慢

User.find_in_batches(:batch_size => 2000) do |users|
  sleep(50)
  users.each { |user|
    job(usreport)
  }
end

请教各位改怎么改进才能让任务转为后台处理或者加快处理?thx! 后来觉得没有必要采用 sidekiq,现在想只用 rack 来完成。

追问:(附 job 主要完成的任务) (1)判断数据库每个记录对应的两个文件 pdf 和 png 是否存在 (2)如果不存在要调用 Http 请求去生成对应的文件,并且必须有 pdf 才能生成 png

为了完成上面的任务,(经过@nickcen @mystery @martin @liwei78 的指导)首先采用 rack 任务,把文件不存在的查找一遍,并且标记文件文件是否存在(大概 500s 完成)。 之后再去遍历这个记录表,根据文件查找结果记录去请求 url 生成 pdf 和 png;

目前遇到的问题: 每次发送 Http 请求需要等待 500 - 700ms 才能执行完请求生成文件,随着程序执行时间越来越长(1000ms 以上),请求会花费更长时间。但是记录里面还有 4w 个文件没有生成的,如果一次性执行这么多请求会出现两个问题:(1)4G 内存可能爆掉,请求终端;(2)总的执行时间太长,大概 8-10h 完成(服务器用的是 Unicorn,多进程服务器)目前采用同步发送请求的方式断断续续的生成了 2w 多 pdf 文件(内存溢出,程序经常中断,花费好几天)

理想的程序执行结果: 采用采用多线程异步异步发送 Http 请求缩短程序总的发送请求时间,发送完成后接收方继续执行即可(缩短总的文件生成时间,数分钟即可执行完成); 如果请求失败再次执行请求,直到生成为止;

于是找到了 EventMachine 和 EM-HTTP-Request 这样的异步请求处理工具: 采用如下处理过程: 先将 pdf 没有生成的记录获取出来,并构造出请求 url 放在 urls 数组中, 然后用 EventMachine 发送请求,每次取出 25 个,发送完成后执行下一组 url 请求

代码如下:


desc 'use slice to perform  http request '
task gpdf: :environment do
  urls = []
  a = Time.now.to_i
  p 'generate     use event machine' << a.to_s
  report_ids = UsLog.find_by_sql("select distinct id  ,xml from  us_logs where  pdf ='false'  ")

  #store  all  url in urls
  report_ids.each_with_index do |rep, index|
    id = rep.id
    xml = rep.xml
    gpdf_url =URI("#{HOSTURI}/create_pdf?uuid=#{xml}")
    urls  << gpdf_url

    #record time
    if (index % 1000 == 0)
      b = Time.now.to_i
      c = b - a
      puts index
      puts c
    end
  end

  EventMachine.run {
    multi = EventMachine::MultiRequest.new
    urls.each_slice(25).with_index  do |bulk_urls ,arr_index|
      EM.next_tick do
        #bulk_urls.each {|url| multi.add(url,EventMachine::HttpRequest.new(url).get(:timeout => 5))}
        bulk_urls.each_with_index  {
            |url,index|

          # abc is the unique key of multi http request
          abc  = arr_index* 25 + index

          # add multiple requests to the multi-handler
          multi.add(abc,EventMachine::HttpRequest.new(url).get(:timeout => 5))

        }
      end
    end

    multi.callback {
      p multi.responses[:succeeded]
      p multi.responses[:failed]
      EventMachine.stop
    }
  }
end

程序执行后出现堆栈调用异常,如下:

f42cb438000-7f42cb45a000 r-xp 00000000 08:01 269491                     /lib/x86_64-linux-gnu/ld-2.15.so
7f42cb53b000-7f42cb641000 rw-p 00000000 00:00 0 
7f42cb64c000-7f42cb64d000 rw-p 00000000 00:00 0 
7f42cb64d000-7f42cb654000 r--s 00000000 08:01 3809707                    /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache
7f42cb654000-7f42cb655000 ---p 00000000 00:00 0 
7f42cb655000-7f42cb65a000 rw-p 00000000 00:00 0                          [stack:26307]
7f42cb65a000-7f42cb65b000 r--p 00022000 08:01 269491                     /lib/x86_64-linux-gnu/ld-2.15.so
7f42cb65b000-7f42cb65d000 rw-p 00023000 08:01 269491                     /lib/x86_64-linux-gnu/ld-2.15.so
7ffff0940000-7ffff0962000 rw-p 00000000 00:00 0 
7ffff09b2000-7ffff09b3000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]


[NOTE]
You may have encountered a bug in the Ruby interpreter or extension libraries.
Bug reports are welcome.
For details: http://www.ruby-lang.org/bugreport.html

不知为何会出现上面的错误,并且 IDEshell 中和系统的终端执行结果也不一样; Ubuntu 自带系统终端中没有错误输出,最终 callback 中打印的信息为 null IDE(rubymine)的 shell 输出如上,提示段转储错误

最终请求没有发送成功,接收方的 server 没有任何日志输出,请教做过类似多请求并发处理的大大们给些指导建议,再次感谢! @hooopo @pzgz @skandhas @luikore @zw963

@yutian 改成异步就可以了

SomeWorker. perform_async

sleep 去掉。

10w 的数据,应该用 sql 了。

我是用 resque + resque-state 来做大数据任务,可以看到完成度。

#1 楼 @martin 看过 sidekiq 的文档后,我用的是

HardWorker.perform_async(user.id) 

来处理,效果不够显著,设置了参数后一直在 retry,执行了一夜也没执行完,有种虐心的感觉。。

#2 楼 @liwei78 担心内存溢出看文档上有 sleep 就比葫芦画瓢的加上了,因为 遍历的时候还要执行 http 请求好像没法用 sql 直接处理(http://api.rubyonrails.org/v2.3.8/classes/ActiveRecord/Batches/ClassMethods.html

@yutian 你的用户查询没必要放在 controller 里面的,在 worker 里面遍历用户即可,controller 仅仅是调起 worker,这个压根不会花时间。

然后你的 worker 可以多跑几个 threads。至于 job 本身的优化,这个只能根据你的业务优化了。

#5 楼 @martin 原来 sidekiq 是这样用的 一语中的 改下试试 😄谢谢

你应该先说明你的 job 想要干什么,依赖哪些数据列。没准直接把表 dump 成 csv 来处理更快了。

#7 楼 @nickcen 这边 job 主要用来判断数据库每个记录对应的两个文件 pdf 和 png 是否存在,如果不存在要调用 http 请求去生成对应的文件,并且必须有 pdf 才能生成 png

如何判断 pdf 是否存在,是基于某个数据列存不存在?如果是,那就直接把这一列查出来就好了。不要用 ActiveRecord 生成对象。

#9 楼 @nickcen 是根据磁盘存储文件来判断文件是否存在,若仅仅是根据数据列就非常容易实现了

#10 楼 @yutian 那你应该给是否生成了 pdf 加个一个字段来记录,再写个脚本再更新一下现有记录,这样一劳永逸呀亲!

#11 楼 @mystery 因为工程遗留问题文件丢失了一部分,只好把所有的记录再读一遍,用 File.exists?(filename) 来判断文件存在

@yutian 那就把确定路径所需要的字段取出来就好了,不要生成 ActiveRecord 记录,这样慢。

#12 楼 @yutian 先写个 rake 查找一下 filename 然后记录下来,这样就不用每次 File.exists?(filename) 时间 (IO) 上会节约好多,以后再处理 job 逻辑上也好优化。

#13 楼 @nickcen #14 楼 @mystery
谢谢两位给的指导 正在改进

You need to Sign in before reply, if you don't have an account, please Sign up first.