【问题标题】:Why is dry-struct initialization slow?为什么干结构初始化很慢?
【发布时间】:2020-04-02 08:37:01
【问题描述】:

我有一个 Rails 应用程序,它将其配置存储在 34 个 MySQL 表中,这些表由各种对象和关联组成,总共大约 900 条记录。直到最近,业务逻辑都建立在 ActiveRecord 上,但性能不稳定,因为我没有足够的控制来控制多少查询被触发。最近我将业务逻辑“移植”到Dry::Struct,将所有涉及的 ActiveRecord 类复制到Dry::Struct 值模型,并在Configuration 实例中预加载所有配置对象:这大大减少了查询的数量,使其数量很少且固定数量,并显着提高了性能,因为所有关联“行走”都是在内存中完成的,而且还有很多。

到目前为止一切顺利,但加载 34 个表(我在每次请求时都需要其中的大部分)仍然需要 34 个查询和大约 160 毫秒。该策略在 ActiveRecord 上处于低级别,将所有表中的所有记录作为普通哈希加载,然后使用这些初始化结构并将所有内容存储在 Configuration 对象中。

我想进一步提高性能,所以我想通过对所有表中的所有字段创建一个UNION 来在一次查询中获取所有数据。这构成了一个强大的 30 KB 的 SQL 查询,令人惊讶的是,它仅在 20 毫秒内执行。优秀的!现在我只需在大约 10 毫秒内展开这个大结构,我就赢了!

嗯,不。事实证明,仅扫描结果数组需要 48 毫秒(其中 20 是 SQL 查询),当在这里和那里解析一些 JSON 字段并准备哈希以初始化结构时,时间会增加到 78 毫秒......然后初始化它们本身需要额外的 89 ms。如果我没有通过重复算法 100 次来测量每一步(当然是在预热记忆值之后),我不会相信,但确实如此。总而言之,与之前单独加载每个表的简单得多的算法相比,尽管单个查询很高效,但没有任何性能提升。

下面是 SQL 的样子:

SELECT a1, a2, NULL, NULL, NULL, NULL FROM table_a
UNION ALL
SELECT NULL, NULL, b1, b2, NULL, NULL FROM table_b
UNION ALL
SELECT NULL, NULL, NULL, NULL, c1, c2 FROM table_c

产生这样的“对角线”结构

"string", 3, NULL, NULL, NULL, NULL -- from table_a
"other string", 5, NULL, NULL, NULL, NULL -- from table_a
NULL, NULL, 1, "{\"json\":true}", NULL, NULL -- from table_b
NULL, NULL, 2, NULL, NULL, NULL -- from table_b
NULL, NULL, NULL, NULL, 7, 10 -- from table_c
NULL, NULL, NULL, NULL, 9, 51 -- from table_c

然后以下算法将其展开为原始记录:

  def preload_all!
    ranges = self.class.preload_field_ranges.invert
    logger.measure_debug("Preloaded configuration") do
      ApplicationRecord.connection.execute(self.class.preload_query).each do |data|
        # finding where the first significant column is
        pos = data.index { |i| !i.nil? }
        # resolving the table name based on where the significant value was found, exiting early
        range, table_name = ranges.select { |k, v| break [ k, v ] if pos.in?(k) }
        v_class = self.class.tables_to_value_classes[table_name]
        values = data[range].map do |i|
          case i
          when String
            # horrible kludge to parse JSON fields, because I wasn't able to inspect AR
            # classes to ask them which fields are serialized, any help is appreciated
            case
            when i[0].in?([ "{", "[" ]) then JSON.parse(i)
            else i
            end
          else i
          end
        end
        # assignments are for clarity, doing these operations inline shaves about 10 ms
        ivar = "@#{table_name}"
        hash = self.class.ar_attribute_names[table_name].zip(values).to_h
        v_model = v_class.new(hash.merge(configuration: self))
        vhash = instance_variable_get(ivar) || {}
        instance_variable_set(ivar, vhash.tap { |h| h[v_model.id] = v_model })
      end
    end
    self
  end

我试图尽可能地收紧代码,但仅删除 v_class.new 部分就会将时间缩短一半。有没有改进的余地?

附带说明一下,从 Redis 加载 Marshaled 完成的 Configuration 对象只需 10 毫秒,但我想避免使用 Redis 来防止错位。

【问题讨论】:

  • 如果您需要有关 MySQL 的帮助,请向我们展示生成的 SQL。
  • @RickJames 我添加了一个类似 SQL 的 sn-p(原来的那个是巨大的),但 SQL 似乎不是问题,因为查询仅在 20 毫秒内执行。相反,我没想到从哈希创建结构会占用总执行时间的一半。当然,如果有更好的方法可以在单个查询中加载多个表中的所有数据,我愿意接受建议。
  • 请显示传入和传出数据的简短示例。那个 UNION 查询很奇怪。
  • 我添加了一个返回数据的示例:它实际上只是来自所有表的所有记录并排。真实数据是200列(所有表列的总和)乘900行(所有表中所有记录的总和),太大了,这里就不贴了。

标签: mysql ruby-on-rails performance dry-rb


【解决方案1】:

正如solnic本人所建议的那样:

.new 更改为.load,它只会设置不带应用类型的 ivars,这将为您带来巨大的速度提升。仅当您信任数据时才执行此操作。

【讨论】:

    猜你喜欢
    • 2016-02-22
    • 2017-06-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-18
    • 1970-01-01
    相关资源
    最近更新 更多