在Heroku上渲染大量JSON的有效方法

我用一个端点构建了一个简单的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?

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

编辑:我重写了yajl的整个代码,因为它的API更引人注目,并允许更清晰的代码。 我还提供了一个以块的forms读取响应的示例。 这是我写的流式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 

要在客户端进行解码,您可以以块的forms读取和解析响应,例如使用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数组的需要时,你实际上可以简化整个事情。 上面的解决方案生成的是这种forms的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数组,这将不起作用。