Optimizing Rails for Memory Usage Part 1: Before You Optimize
This is part one in a four-part series on optimizing a potentially memory-heavy Rails action without resorting to pagination. The posts in the series are:
We recently built an API server for a mobile application that had an interesting requirement: the mobile application needed to work offline. To support this, we built an API action that can generate a dump of a user’s records from the database. The action constructs a very large JSON response. To keep the action atomic we did not use pagination.
We quickly ran out of memory on Heroku, and the memory was not reclaimed after the dump action finished. The dyno stayed over Heroku’s memory limit with degraded performance until Heroku’s daily dyno restart.
Because we ran into problems almost immediately, it suggested to us that we should fix the memory needed by our application rather than trying to work around the problem. This series is what I wish had been written when I began to optimize our memory usage.
These posts describe my recommended steps to optimize the memory usage of a Rails app. Even though it is inspired by our particular problem with one large JSON index action, many of these steps apply broadly to optimizing any Ruby memory issue.
Parts 2-4 will discuss various optimization strategies. However, before you actually optimize you need to do a couple things first: verify that you really need to optimize and then set up metrics.
Do You Need to Optimize?
The first rule of optimization is, “Don’t do it.” Of course, experienced developers still optimize from time to time because “don’t do it” is really shorthand for several warnings:
- Optimization may not be necessary.
- Optimization may not be feasible and will waste your time.
- You are going to be tempted to optimize the wrong thing.
- Even successful optimization can make your code harder to understand.
Consequently, before considering optimization ask these questions:
- Do you even need to optimize? Is there an actual pain point, or just a speculated pain point? If it’s only speculation, don’t optimize. YAGNI it and work on more important functionality.
- Do you know the root of the problem? If not, find it. Otherwise you will guess and probably guess wrong.
- Is there a clean way to solve the problem? Can it be solved at another layer? Think creatively. You want to keep your application code clean. For example, to avoid the complexities of optimizing, intermittent memory problems might be solved by setting up a worker killer on your server to restart any Ruby process that starts using too much memory. Search the netz for <my server here> worker killer. Restarting workers is a band-aid, but if your memory problems are minor it may effectively defer those problems for a long time. YAGNI.
So you’ve asked yourself these questions and discovered there’s no other way out. You know it’s time to optimize. The next pre-optimization step is to set up metrics.
Set Up Metrics
Pink and plaid do not go together, so if you get dressed in the dark you may surprise your coworkers when you walk into the office. Similarly, you can’t optimize code in the dark: you need numbers to show that you are making progress. It’s time to set up metrics.
You need to measure the memory used after tens or hundreds of requests. One request is not sufficient because Ruby’s memory allocation isn’t perfect. Even if your app is not technically leaking memory there may be fragmentation that grows the memory usage over time.
Use this template script to measure the total memory usage of your Rails process after 30 requests. Modify the script as necessary for your setup.
The script only measures the resident set size of the Rails application. The script will show you if you are actually making improvements, but you will need other tools to discover where exactly all the memory is coming from in your application. We will mention some of those tools in part 3, but first you should do something easier: optimize Ruby’s garbage collection parameters.