【问题标题】:How can I speed up my Rails App JSON rendering?如何加快 Rails App JSON 渲染速度?
【发布时间】:2015-12-31 16:48:55
【问题描述】:

我的 rails 4.2.4 应用程序有一个 JSON API。一个动作被非常非常频繁地调用,所以我想让它尽可能快(每个请求大约 150 毫秒是我的目标)。

控制器方法如下所示:

def update
  t_update = 0
  t_render = 0
  total = Benchmark.measure do
    success = false
    t_update = Benchmark.measure do
      success = @character.update_attributes(char_params)
    end
    t_render = Benchmark.measure do
        if success
          respond_to do |format|
            format.html { redirect_to @character }
            format.json { render json: char_to_json(@character) }
          end
        else
          respond_to do |format|
            format.html { redirect_to @character }
            format.json { render json: char_to_json(@character), status: 500 }
          end
        end
    end
  end
  logger.warn("##### Update: #{(t_update.real*1000).to_i}ms")
  logger.warn("##### Render: #{(t_render.real*1000).to_i}ms")
  logger.warn("##### Update + Render: #{(total.real*1000).to_i}ms")
end

我添加了一些基准测量来了解时间花费在哪里。我得到这样的输出:

App 6144 stdout: ##### Update: 195ms
App 6144 stdout: ##### Render: 265ms
App 6144 stdout: ##### Update + Render: 461ms

这是在生产服务器上,当然是在 Rails 生产模式下。显然太慢了。由于渲染需要更多时间,我想先优化它。

  • 最初,我使用 jbuilder 视图呈现我的 JSON。这花了大约 400 毫秒。
  • 我将 JSON 渲染器切换到 Oj,将我带到了大约 300 毫秒
  • 我完全放弃了 jbuilder。现在我从我的模型生成一个 Hash 并将其直接传递给 Oj(这里推荐:http://brainspec.com/blog/2012/09/28/lightning-json-in-rails/

char_to_json 看起来像这样:

def char_to_json(char)
  hash = nil
  json = nil
  t_hash = Benchmark.measure do
    hash = char.as_json
  end
  t_render = Benchmark.measure do
    json = Oj.dump(hash, mode: :object, indent: 0)
  end
  logger.warn("####  Hashing: #{(t_hash.real*1000).to_i}ms")
  logger.warn("####  Rendering: #{(t_render.real*1000).to_i}ms")
  json
end

这里的基准显示:

App 6144 stdout: ####  Hashing: 263ms
App 6144 stdout: ####  Rendering: 0ms

所以所有的时间都花在了创建哈希上!诚然,它是一个相当大且复杂的对象图,但它基本上只是从我的模型对象创建一个哈希,所有内容都已经初始化(ActiveRecord 整个请求大约需要 20 毫秒):

def as_json
  grouped_items = items.group_by {|i| i.container }

  json = {
    'id' => id,
    'name' => name,
    'status' => status,
    'creating' => creating?,
    'title' => title,
    'level' => level,
    'level_count' => level_count,
    'rules_compliant' => rules_compliant?,
    'health' => health,
    'max_health' => max_health,
    'stamina' => stamina,
    'mana' => mana,
    'mana_mult_buff' => mana_mult_buff,
    'wounds' => wounds,
    'armor_buff' => decimal_number(armor_buff),
    'damage_incoming' => decimal_number(damage_incoming),

    'rolled_damage_left' => rolled_damage_left,
    'rolled_damage_right' => rolled_damage_right,
    'initiative_roll' => initiative_roll,

    'offensive_buff' => offensive_buff,
    'defensive_buff' => defensive_buff,
    'evasion_buff' => evasion_buff,
    'speed_buff' => speed_buff,

    'notes' => notes,

    'race' => race.as_json,
    'birthsign' => birthsign.as_json,
    'specialization' => specialization.as_json,
    'fav_attribute1' => fav_attribute1.as_json(mode: :fav),
    'fav_attribute2' => fav_attribute2.as_json(mode: :fav),

    'attributes' => character_attributes.map {|attr| attr.as_json },
    'skills' => skills.map {|skill| skill.as_json },
    'resistances' => resistances.map {|resi| resi.as_json },

    'containers' => Item.containers.map do |container|
      {
        'key' => container[0],
        'name' => I18n.t("activerecord.attributes.item.container.#{container[0]}"),
        'weight' => decimal_number(send("#{container[0]}_weight")),
        'max_weight' => respond_to?("max_#{container[0]}_weight") ?
                                             decimal_number(send("max_#{container[0]}_weight")) :
                                             nil,
        'items' => (grouped_items[container[0]] || []).
            sort {|a,b| a.index <=> b.index}.
            map {|item| item.as_json }
      }
    end,

    'slots' => slots.map {|slot| slot.as_json },
    'spells' => spells.map {|spell| spell.as_json self }
  }
  formulas.each do |formula|
    json[formula.property.abbr] = decimal_number(formula.points)
  end
  json
end

我该如何进一步调查?时间可以花在哪里?或者在 ruby​​ 中创建一个大的嵌套哈希只是很慢?我不想相信!

感谢您的建议!

【问题讨论】:

  • 继续。在 as_json 中计时,看看是否可以加快慢点。 Ruby 中的方法调用有很多开销。你可能会看那里。
  • 你确定你只为请求访问数据库一次吗? (我没有详细检查代码)
  • @DamianoStoffie:是的,我绝对确定所有数据都已急切加载。
  • @seph :如何避免使用 ActiveRecords 进行方法调用?如果我访问我的类的任何属性(如 id、name、...),每个属性都是一个方法调用。有没有可能避免这些?

标签: ruby-on-rails ruby json ruby-on-rails-4


【解决方案1】:

预加载

检查N+1 数据库命中。在您的as_json 中使用了很少的关联(如种族、技能、阻力),当您阅读@character 实例时,可能并非所有在第一个查询中选择的关联。附加信息http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations

考虑缓存

  • 'race' =&gt; race.as_json - 我想你们没有那么多比赛,而且更新也不频繁。你可以像

    一样把比赛 json 放到 Rails.cache
    'race' => Rails.cache.fetch("json_race_#{race.id}", expires_in: 1.day) do
      race.as_json
    end
    

    如果您更改种族,请不要忘记使缓存无效:)

  • 'containers' =&gt; Item.containers.map ... - 看起来这部分代码对于任何字符总是相同的。也可以缓存。

【讨论】:

  • 我 99.99% 确信我的所有关联都已热切加载。但是你所说的缓存是非常有趣的。我会看看这个并报告结果。谢谢!
  • 好的,我为属性“race”、“birthsign”、“specialization”、“fav_attribute1”和“fav_attribute2”添加了缓存。这使得 as_json 方法快了大约 30 毫秒。仍然不是我想要的,而是一个开始。感谢您的提示!
  • 现在我还为“属性”、“技能”、“阻力”和“项目”添加了缓存。尽管它们比以前的属性更频繁地更改,但事实证明绝对值得缓存它们。 as_json 方法的时间缩短到 80 毫秒!惊人的!缓存是正确的做法。并且通过适当的失效,一切仍然按预期工作。非常感谢!
猜你喜欢
  • 2014-12-20
  • 2013-08-18
  • 2015-07-17
  • 2013-04-25
  • 2010-09-13
  • 2015-05-07
  • 2017-05-25
  • 2012-08-07
  • 2011-05-05
相关资源
最近更新 更多