使用多个线程进行数据库更新会导致每次更新的写入时间更长

时间:2016-08-15 18:06:52

标签: sql ruby-on-rails ruby multithreading sequel

所以我有一个脚本应该更新一个巨大的表(Postgres)。由于该表有大约150米的行,我想尽快完成它,使用多个线程似乎是一个完美的答案。但是,我看到的东西非常奇怪。

当我使用单个线程时,更新的写入时间比使用多个线程时要低得多。

require 'sequel'
.....

DB = Sequel.connect(DB_CREDS)
queue = Queue.new

read_query = query = DB["
    SELECT id, extra_fields
    FROM objects
    WHERE XYZ IS FALSE
"]

read_query.use_cursor(:rows_per_fetch => 1000).each do |row|
    queue.push(row)
end

到目前为止,IMO并不重要,因为我们只是从数据库中读取内容并且与写入无关。从这里开始,我尝试了两种方法。单线程和多线程。

注意 - 这不是我想要执行的实际UPDATE查询,它只是用于演示目的的伪查询。实际的查询要长得多,并且使用JSON和东西,所以我无法使用单个查询更新整个表。

单线程

until queue.empty?
    photo = queue.shift
    id = photo[:id]
    update_query = DB["
        UPDATE objects
        SET XYZ = TRUE
        WHERE id = #{id}    
    "]

    result = update_query.update
end

如果我执行此操作,我会在数据库日志中看到每个更新查询花费的时间少于 0.01秒

  

I,[2016-08-15T10:45:48.095324#54495] INFO - :(0.001441s)更新   对象SET XYZ = TRUE WHERE id = 84395179

     

I,[2016-08-15T10:45:48.103818#54495] INFO - :(0.008331s)更新   对象SET XYZ = TRUE WHERE id = 84395181

     

I,[2016-08-15T10:45:48.106741#54495]信息 - :(0.002743s)更新   对象SET XYZ = TRUE WHERE id = 84395182

多线程

MAX_THREADS = 5
num_threads = 0
all_threads = []

until queue.empty?
    if num_threads < MAX_THREADS
        photo = queue.shift
        num_threads += 1
        all_threads << Thread.new {
            id = photo[:id]
            update_query = DB["
                UPDATE photos
                SET cv_tagged = TRUE
                WHERE id = #{id}    
            "]

            result = update_query.update
            num_threads -= 1
            Thread.exit
        }
    end
end

all_threads.each do |thread|
    thread.join
end

现在,理论上它应该更快吧?但每次更新大约需要 0.5秒。我很惊讶那是什么情况。

  

I,[2016-08-15T11:02:10.992156#54583] INFO - :(0.414288s)                 UPDATE对象                 SET XYZ = TRUE                 WHERE id = 119498834

     

I,[2016-08-15T11:02:11.097004#54583] INFO - :(0.622775s)                 UPDATE对象                 SET XYZ = TRUE                 WHERE id = 119498641

     

I,[2016-08-15T11:02:11.097074#54583] INFO - :(0.415521s)                 UPDATE对象                 SET XYZ = TRUE                 WHERE id = 119498826

关于 -

的任何想法
  1. 为什么会这样?

  2. 如何提高多线程方法的更新速度。

1 个答案:

答案 0 :(得分:0)

我在一个项目中经历了类似的事情(“将所有历史从遗留数据库导入到具有完全不同结构和组织的新数据库”)。除非你设法在别人的脚下射击自己,否则你有两个基本的瓶颈可供选择:

  1. 数据库的磁盘IO
  2. ruby​​进程'CPU
  3. 一些建议,

    1. 数据库IO:使用数据库事务,每个事务更新1000条记录(你可以调整确切的数字,但1000通常是好的) - 巨大的数据库表通常也意味着很多索引,每一次更新操作都会触发REINDEX和数据库中的AUTOVACUUM操作会导致更新速度大幅下降,事务基本上允许您在没有REINDEX和AUTOVACUUM的情况下推送1000个更新的记录,然后执行这两个操作,结果更快(类似于一个数量级)
    2. 数据库IO:更改索引,在更新过程中删除您可以使用的每个索引,理想情况下,您将只有一个非常简化的索引,允许进行更新目的的唯一行查找
    3. ruby​​ CPU:除非您使用的是JRuby或Rubinius,或者真正为您的数据库支付网络延迟的代价,线程对您没什么大的好处,使用fork / processes(see GIL)。你为这个
    4. 选择Sequel over AR做得很好
    5. ruby​​ CPU:如果你决定去线程+ JRuby,不要忘记尝试插入jProfiler,它在追踪Java和author of SideKiq swears it is amazing for JRuby too的瓶颈方面是惊人的 - 不幸的是,afaik,那里并不等同于C Ruby的jProfiler(有分析工具,但无处可用)
    6. 在您实施这些建议后,您知道在以下情况下您已尽力而为:

      1. Ruby框上的所有CPU都是100%加载
      2. 数据库的硬盘IO处于100%吞吐量
      3. 找到这个最佳位置,之后不要添加额外的ruby更新线程/进程(或添加更多硬件),那就是

        PS签出https://github.com/ruby-concurrency/concurrent-ruby - 这是一个很棒的并行化库