【问题标题】:Efficient way to render ton of JSON on Heroku在 Heroku 上渲染大量 JSON 的有效方法
【发布时间】:2014-06-18 17:17:42
【问题描述】:

我用一个端点构建了一个简单的 API。它抓取文件,目前有大约 30,000 条记录。理想情况下,我希望能够通过一个 http 调用以 JSON 格式获取所有这些记录。

这是我的 Sinatra 视图代码:

require 'sinatra'
require 'json'
require 'mongoid'

Mongoid.identity_map_enabled = false

get '/' do
  content_type :json
  Book.all
end

我尝试了以下方法: 将 multi_json 与

一起使用
require './require.rb'
require 'sinatra'
require 'multi_json'
MultiJson.engine = :yajl

Mongoid.identity_map_enabled = false

get '/' do
  content_type :json
  MultiJson.encode(Book.all)
end

这种方法的问题是我收到错误 R14(超出内存配额)。当我尝试使用 'oj' gem 时,我得到了同样的错误。

我只想将所有内容连接到一个长 Redis 字符串,但 Heroku 的 redis 服务是每月 30 美元,用于我需要的实例大小 (> 10mb)。

我目前的解决方案是使用后台任务来创建对象并将其填充为接近 Mongoid 对象大小限制 (16mb) 的 jsonified 对象。这种方法的问题:渲染仍然需要将近 30 秒,并且我必须在接收应用程序上运行后处理才能正确地从对象中提取 json。

对于如何在一次调用中为 30k 条记录渲染 json 而无需离开 Heroku,是否有人有更好的想法?

【问题讨论】:

  • 对于单个 API 调用来说,即使是 100 个对象也很大。如果你真的想在一个 API 调用中使用它,你应该对请求/响应进行分页。然后在客户端使用每个页面作为单独的调用,因为您遍历完整的记录集。

标签: ruby json heroku sinatra mongoid


【解决方案1】:

听起来您想将 JSON 直接流式传输到客户端,而不是将其全部构建在内存中。这可能是减少内存使用的最佳方法。例如,您可以使用 yajl 将 JSON 直接编码为流。

编辑:我为yajl 重写了整个代码,因为它的API 更引人注目并且允许更简洁的代码。我还包括了一个以块为单位读取响应的示例。这是我编写的流式 JSON 数组助手:

require 'yajl'

module JsonArray
  class StreamWriter
    def initialize(out)
      super()
      @out = out
      @encoder = Yajl::Encoder.new
      @first = true
    end

    def <<(object)
      @out << ',' unless @first
      @out << @encoder.encode(object)
      @out << "\n"
      @first = false
    end
  end

  def self.write_stream(app, &block)
    app.stream do |out|
      out << '['
      block.call StreamWriter.new(out)
      out << ']'
    end
  end
end

用法:

require 'sinatra'
require 'mongoid'

Mongoid.identity_map_enabled = false

# use a server that supports streaming
set :server, :thin

get '/' do
  content_type :json
  JsonArray.write_stream(self) do |json|
    Book.all.each do |book|
      json << book.attributes
    end
  end
end

要在客户端解码,您可以分块读取和解析响应,例如使用em-http。请注意,此解决方案要求客户端内存足够大以存储整个对象数组。这是相应的流式解析器助手:

require 'yajl'

module JsonArray
  class StreamParser
    def initialize(&callback)
      @parser = Yajl::Parser.new
      @parser.on_parse_complete = callback
    end

    def <<(str)
      @parser << str
    end
  end

  def self.parse_stream(&callback)
    StreamParser.new(&callback)
  end
end

用法:

require 'em-http'

parser = JsonArray.parse_stream do |object|
  # block is called when we are done parsing the
  # entire array; now we can handle the data
  p object
end

EventMachine.run do
  http = EventMachine::HttpRequest.new('http://localhost:4567').get
  http.stream do |chunk|
    parser << chunk
  end
  http.callback do
    EventMachine.stop
  end
end

替代解决方案

当您放弃生成“正确”JSON 数组的需要时,您实际上可以大大简化整个事情。上述解决方案生成的是这种形式的 JSON:

[{ ... book_1 ... }
,{ ... book_2 ... }
,{ ... book_3 ... }
...
,{ ... book_n ... }
]

但是,我们可以将每本书作为单独的 JSON 流式传输,从而将格式简化为以下格式:

{ ... book_1 ... }
{ ... book_2 ... }
{ ... book_3 ... }
...
{ ... book_n ... }

服务器上的代码会更加更简单:

require 'sinatra'
require 'mongoid'
require 'yajl'

Mongoid.identity_map_enabled = false
set :server, :thin

get '/' do
  content_type :json
  encoder = Yajl::Encoder.new
  stream do |out|
    Book.all.each do |book|
      out << encoder.encode(book.attributes) << "\n"
    end
  end
end

还有客户:

require 'em-http'
require 'yajl'

parser = Yajl::Parser.new
parser.on_parse_complete = Proc.new do |book|
  # this will now be called separately for every book
  p book
end

EventMachine.run do
  http = EventMachine::HttpRequest.new('http://localhost:4567').get
  http.stream do |chunk|
    parser << chunk
  end
  http.callback do
    EventMachine.stop
  end
end

很棒的是,现在客户端不必等待整个响应,而是单独解析每本书。但是,如果您的客户之一需要一个大的 JSON 数组,这将不起作用。

【讨论】:

  • @MichaelQLarson 请看我的编辑,答案现在有真正的工作代码;-)
  • 谢谢!我是 OJ 的新手,无法让您的新代码在客户端(使用 Sinatra 端点)上运行。它抛出以下错误:Oj::ParseError: Hash/Object not terminated at line 1, column 30。基于documentation,我正在尝试运行oj_json = open("MySinatraApp.herokuapp.com"); records = Oj.load(oj_json)。你知道我做错了什么吗?
  • open-urioj 的组合似乎无法处理流式传输。我已经使用yajl 重写了代码,并包含了一个使用net/http 的示例客户端。干杯
猜你喜欢
  • 2015-04-16
  • 1970-01-01
  • 1970-01-01
  • 2014-05-21
  • 1970-01-01
  • 2015-09-30
  • 2018-01-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多