Making Ruby’s garbage collector copy-on-write friendly, part 6 (final?)

I added some minor optimizations and cleaned up the code a little bit. I also fixed many bugs in mongrel_light_cluster. It’s time for another test round.

Test plan

The goal is to measure the memory usage of my Ruby GC patch + mongrel_light_cluster, compared to default Ruby and mongrel_cluster. The test plan is as follows:

  • Measure current memory usage.
  • Start 10 instances of mongrel_cluster.
  • Measure current memory usage.
  • Load the front page of all 10 mongrel_cluster instances 50 times.
  • Measure current memory usage.
  • Shutdown mongrel_cluster, and repeat the above steps with patched Ruby + mongrel_light_cluster.

Software used in this test

Ruby on Rails 1.2.5
Mongrel 1.0.1
mongrel_cluster 1.0.2
Mephisto 0.7.3

Environment: production
config.action_controller.perform_caching = false

Memory usage of 10 mongrel_cluster instances

Before mongrel_cluster is started:

             total       used       free     shared    buffers     cached
Mem:          1011        576        435          0         16        185
-/+ buffers/cache:        373        637
Swap:          996         13        982

After starting 10 instances of mongrel_light_cluster:

             total       used       free     shared    buffers     cached
Mem:          1011        861        149          0         16        185
-/+ buffers/cache:        659        352
Swap:          996         13        982

After downloading the front page 50 times from all instances:

             total       used       free     shared    buffers     cached
Mem:          1011        901        109          0         16        186
-/+ buffers/cache:        698        312
Swap:          996         13        982

Conclusion:
10 instances of mongrel_cluster will use 435-109 = 326 MB memory.
So mongrel_cluster uses 32.6 MB per instance.

Memory usage of 10 mongrel_light_cluster instances with patched Ruby

Before:

             total       used       free     shared    buffers     cached
Mem:          1011        587        424          0         18        193
-/+ buffers/cache:        375        635
Swap:          996         13        982

After starting 10 instances:

             total       used       free     shared    buffers     cached
Mem:          1011        752        259          0         18        193
-/+ buffers/cache:        540        470
Swap:          996         13        982

After downloading the front page 50 times from all instances:

             total       used       free     shared    buffers     cached
Mem:          1011        803        207          0         18        193
-/+ buffers/cache:        591        419
Swap:          996         13        982

Conclusion:
10 instances of mongrel_light_cluster uses 424-207 = 217 MB memory.
So 1 mongrel_light_cluster instance uses 21.7 MB.

Overall conclusion

Everybody likes graphs, so here is one:
copy-on-write-friendly-ruby-memory-usage.png
Memory usage of an instance went down from 32.6 MB to 21.7 MB. A 33% saving! :)
This seems to be the end of the road. I can’t seem to be able to add more optimizations.

You can download the latest Ruby 1.8.6 patch here.
You can install mongrel_light_cluster by typing:

svn checkout http://public.railsplugins.net/repos/mongrel_light_cluster/trunk mongrel_light_cluster
cd mongrel_light_cluster
rake
gem install pkg/mongrel_light_cluster-0.1.gem

Please read the README for further documentation.

Finally, I’d like to ask your help:

  • I want to turn this patch and mongrel_light_cluster into a real RubyForge project. Can anyone help me with the website and documentation?
  • Is there anyone willing to test my patch and mongrel_light_cluster? I’m not confident enough to use these on my business website (which handles payments) but I don’t have any other Rails sites that are large enough.

Please post here if you’re interested.

13 Comments »

  1. Pratik said,

    October 15, 2007 @ 3:01 pm

    Just wondering if your patch would work fine with LiteSpeed ? In which case I could probably try it on my one of my server. I’d love to help you out with my limited C knowledge and decent ruby knowledge. Also, why do you have config.action_controller.perform_caching set to false ? Well, I keep forgetting your irc nick, mine is “lifo”, so do catch me in there :)

    Thanks.

  2. Hongli said,

    October 15, 2007 @ 4:37 pm

    Hey. :)

    I will work fine with LiteSpeed. The patch is only for making Ruby GC copy-on-write friendly. To properly integrate this with LiteSpeed I have to figure out how LiteSpeed runs Rails. If it’s just FastCGI then the solution should be pretty straightforward. Do you have a website running on LiteSpeed? I only have experience with Mongrel and lighttpd.

    I set perform_caching to false so that each request actually triggers the controller, in order to test the garbage collector. If perform_caching is set to true, then Mephisto will generate .html files which Mongrel will serve directly, thus bypassing Rails.

  3. Jeremy said,

    October 15, 2007 @ 4:45 pm

    I am testing this now with two Rails apps, and swiftiply_mongrel_rails. Startup is 17 megs of RAM, fully loaded is 21-22 megs.

  4. Hongli said,

    October 15, 2007 @ 5:27 pm

    I’m not sure whether mongrel_light_cluster is compatible with swiftiply_mongrel_rails. You can check whether mongrel_light_cluster is actually active by checking this:

    module Cluster
      module ExecBase
        if @@light_cluster_initialized
            puts "mongrel_light_cluster is active"
        end
      end
    end
    
  5. Jeremy said,

    October 15, 2007 @ 5:51 pm

    Already did that. :) – It is.

  6. Jeremy said,

    October 15, 2007 @ 6:03 pm

    Actually, upon further review, it doesn’t. They don’t call start. I had a check inside the module, which was outputting, but start never gets called. I’ll just run with the standard mongrel for now, and change up “swiftiply_mongrel_rails” to use your stuff instead later.

  7. Roger Pack said,

    October 22, 2007 @ 10:11 pm

    You should submit this to the mongrel guys :)

  8. lloyd hilaiel said,

    October 26, 2007 @ 9:30 pm

    hi.

    I’m embarked on a similar mission. make ruby use less memory without going too much slower. I’ve got a set of very simple test cases, and ran then on 1.8.6 after applying your patch. it’s interesting to note that you’ve got about a 8% performance hit in combination with the 33% memory usage gain according to my toy tests:

    vanilla 1.8.6:
    running cases/growarray.rb
    time 1.355438
    running cases/plist.rb
    time 3.693722
    running cases/shrinkarray.rb
    time 1.351735

    cowfriendly 1.8.6:
    running cases/growarray.rb
    time 1.485687
    running cases/plist.rb
    time 4.006008
    running cases/shrinkarray.rb
    time 1.48557

    (/ 3.693722 4.006008)
    (/ 1.351735 1.48557)

    ~8% performance penalty.

    I’d imagine for a lot of folks this is a tradeoff they’re willing to make (8% is a lot better than swapping and paging :) , but thought it might be interesting to you anyways.

  9. 赖洪礼的 blog » Making Ruby’s garbage collector copy-on-write friendly, part 7 said,

    January 14, 2008 @ 1:50 pm

    [...] turns out that part 6 was not final. I’ve been working on the garbage collector again. These are the [...]

  10. roger said,

    March 20, 2008 @ 6:01 am

    I assume most of the memory-savings is because of using fixed-length “heap blocks”?

  11. Hongli said,

    March 20, 2008 @ 11:07 am

    No, the memory savings are caused by copy-on-write semantics.

  12. Crystal said,

    April 17, 2008 @ 1:35 am

    Wow these are very big savings, thanks again.

  13. trickyco.de » Blog Archive » hacking on ruby’s garbage collector said,

    May 6, 2009 @ 9:10 am

    [...] (this guy gets the credit on this idea) Because we do scanning copy on write semantics are blown. you do a fork, and as soon as GC runs, your whole heap is resident and private memory. [...]

RSS feed for comments on this post · TrackBack URI

Leave a Comment