How to save 100m of RAM per mongrel

UPDATE: See Part 2 of this for a better solution

Note: This monkey-patch only works on Rails 2.2

We recently noticed our mongrels, upon startup, were 244M. Eek.

PID USER            PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
53749 some_user     15   0  337m 244m 2912 S    0  2.0   0:17.49 mongrel_rails

Running Bleakhouse on our rails app, i was amazed at the number of objects created. over 2M!

$ BLEAK_HOUSE=1 ruby-bleak-house ./script/runner 'puts 1'
** Bleakhouse: installed
1
** BleakHouse: working...
** BleakHouse: complete
** Bleakhouse: Run 'bleak /tmp/bleak.53749.0.dump' to analyze.

$ bleak /tmp/bleak.53749.0.dump
Displaying top 20 most common line/class pairs
2,047,025 total objects
2,047,025 filled heap slots
2,403,764 free heap slots
1779063 __null__:__null__:__node__
197423 __null__:__null__:String
16519 __null__:__null__:Array
12728 __null__:__null__:ActionController::Routing::DividerSegment
10495 __null__:__null__:Hash
5676 __null__:__null__:ActionController::Routing::StaticSegment
5436 __null__:__null__:Class
5132 __null__:__null__:Regexp
4525 __null__:__null__:ActionController::Routing::DynamicSegment
2524 __null__:__null__:ActionController::Routing::Route
1307 __null__:__null__:Gem::Version

Whats up with all those routing objects! So i tried a blank routes.rb file….

519,307 total objects
519,307 filled heap slots
233,503 free heap slots

Arghh…. 1.5M objects just because of routes.rb. I know our application is large, lots of resources and nested resources, but thats crazy.

$ rake routes | wc -l
2516

Ok, so taking a closer look, I realized something. Each resource route has a corresponding formatted_* route, lots of which we dont use. So now for some nasty monkey-patching.

ActionController::Routing::RouteSet::Mapper.class_eval do
protected

  def map_unnamed_routes(map, path_without_format, options)
    map.connect(path_without_format, options)
    #map.connect("#{path_without_format}.:format", options)
  end

  def map_named_routes(map, name, path_without_format, options)
    map.named_route(name, path_without_format, options)
    #map.named_route("formatted_#{name}", "#{path_without_format}.:format", options)
  end
end

I then manually added back the formatted routes we needed, careful to use named routes as “map.formatted_foo_bar”

$ rake routes | wc -l
1490
$ bleak /tmp/bleak.56554.0.dump
1,242,796 total objects
1,242,796 filled heap slots
1,224,309 free heap slots

Awesome. I no longer create almost 800k objects.

PID USER            PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
56554 someuser     15   0  249m 156m 2912 S    0  1.3   0:09.92 mongrel_rails

244m –> 156m

This is not a very elegant solution… but I saved almost 100m per mongrel. Ohh.. and it reduced startup time by 50%.

How big are your mongrels?

How does Merb’s router differ from Rails routing? Worth investigating?

11 Comments

  1. Ara says:

    Wait. Why do we need to workaround this? Why is Rails creating 100mb of extraneous objects *on startup*? Surely there is a problem with the default behavior in that case.

  2. Neeraj says:

    This is really really interesting. I will give the patch a try.

  3. Aaron,

    Routes double total RSS use for large applications.We had similar downsizing in our environment.

    http://github.com/methodmissing/routing_with_optional_formats/tree/master

    There’s yet another optimization pattern – only mapping routes for actions that exist iow. if a controller only responds to 1 REST action, only map that, not all 7.

    http://gist.github.com/23253

    The current Rails initializer sequence complicates clean optimizations and to get the Gist snippet to fire, one has to do stick it in config/initializers/ and

    RouteFilter.prune! if Rails.env.production?

    in environment.rb, after the config block.This however still doesn’t save on memory as downsizing an Enumerable doesn’t yield memory back to the OS, but yields noticeable differences in recognition overhead.

    The Anonymous module that installs named routes in ActionController, ActionView etc. is also a candidate for similar optimization ( undef_method ?, but see below ).

    It would be most excellent if Rails::Initializer could be tweaked to setup routing AFTER preloading application klasses ( current default for Prod. env ) to allow for a more granular routing table, but this may break existing plugins, assumptions etc.

    Getting this right has huge ramifications for the Rails hosting space.

    Thoughts ?

    - Lourens

  4. AkitaOnRails says:

    @Lourens, feels like a nice good addition for Rails Core. Have you tried opening up a new Ticket with your implementation so Routes become more flexible and save more memory?

  5. Hongli Lai says:

    Interesting. Have you also experimented with Phusion Passenger + Ruby Enterprise Edition? If you use those two together, then the memory occupied by the compiled routes will be shared between all Rails processes, which should also reduce your total memory usage significantly.

  6. Jeff says:

    I’m very interested in trying this, but I can’t seem to get it to work properly. What version of Rails are you using? Where are you placing the monkey-patch?

    Thanks
    -jeff

  7. Pratik says:

    Rails lighthouse already has a patch for this at http://rails.lighthouseapp.com/projects/8994/tickets/1215-add-actions-and-formatted-options-to-mapresources

    I imagine that should solve this problem.

  8. Tom says:

    Yes, please see my patch on Lighthouse, and add your voice there if you’d like to see it included in Rails. Your comments and benchmarks would really help convince the core team.

    Introspecting the controllers feels like a bit too much magic for me — loading order problems aside, I’m a fan of documenting the desired routes explicitly in routes.rb as you can see from the ticket — but anything that reduces the astonishing memory overhead of the default routing is great news!

  9. Anil says:

    It’s really nice. I will give it a try.

  10. Jens says:

    Sounds cool, but I don’t really get it. Which version of rails did you monkey patch? All versions I checked (originally I wanted to apply this to one of our apps using rails 2.0.5) didn’t had the methods you changed but had a lot of duplicated code in actionpack/lib/action_controller/resources.rb in the form:

    show_action_options = action_options_for("show", resource)
    map.named_route("#{resource.name_prefix}#{resource.singular}", resource.member_path, show_action_options)
    map.named_route("formatted_#{resource.name_prefix}#{resource.singular}", "#{resource.member_path}.:format", show_action_options)

    update_action_options = action_options_for("update", resource)
    map.connect(resource.member_path, update_action_options)
    map.connect("#{resource.member_path}.:format", update_action_options)

    destroy_action_options = action_options_for("destroy", resource)
    map.connect(resource.member_path, destroy_action_options)
    map.connect("#{resource.member_path}.:format", destroy_action_options)

    I tried to comment out the .format which worked ok, but that’s of course much worse than monkey patching and only worth the experiment.

  11. Dan says:

    It was things like this that ultimately lead me to build <a href=’http://rubywaves.com’>Waves</a>. No routes, just Resource classes with method "functors" to match requests. You can only monkey-patch for so long before you are really writing your own framework on top of Rails. Our 0.8.x release is pretty solid – we are using it within AT&T Interactive R&D to provide a services tier for some of our prototype apps.