Improving Rails App Performance with Database Refactoring & Caching

  

coder.jpgPerformance is a huge priority for any developer. However, people often don’t worry about performance until it starts to dip and there’s an immediate cause for concern. Performance needs to be something we focus on up front—not just when the walls come tumbling down. It needs to be part of the process, not something that’s reviewed occasionally.

In this post, we’ll consider a few things you can do to improve performance, starting with the database.

Database Performance

When using Rails, ORMs—such as ActiveRecord or DataMapper—make it easy to get data from an application. However, they also often make it easy to neglect optimization and the importance of refactoring to improve database interaction. Relying on ORMs to do all the work can lead to issues further down the road of the application’s lifecycle. As we add features, we need to focus on refactoring our ORM calls.

It’s difficult to spot what we call “N+1 problems,” which occur in Rails without refactoring. N+1 means one object was called and then a second object was called, creating a second query. This then compounds. So, you may be running 100 queries to get one result instead of running one query with 100 results. This is tough to see in development, because a tiny dataset is being used, so performance issues are difficult to spot and may only become apparent when moving to a production-sized database.

One way to avoid the N+1 issue is through eager loading. This can be accomplished by using .includes on querying code.

# app/views/customers/index.html.erb
<% @customers.each do |customer| %>
    <%= content_tag :h1, customer.name %>
    <%= content_tag :h2, customer.addresses.first.city %>
<% end %>

# will produce 101 queries if you have a database with 100 customers

 

Here's what it looks like when adding eager loading by adding the .includes:

# app/controller.customers_controller.rb
class CustomersController < ApplicationController

    def index
        @customers = Customer.includes(:addresses).all
    end
…
…
end
# this should produce 2 queries on the same 100 customers

 

This may help prevent performance creep. If it’s part of later refactoring, it may be difficult to go through the SQL backlog to discover where the issue is occurring. Tools such as New Relic come into play to find where the issue is and to help resolve it more succinctly.

Another issue that may affect database performance—and overall application performance—is the issue of slow queries. Slow queries refer to any query that drags on the performance of the database and takes longer to process than it should. If you’re using MySQL, the slow query log should help you locate some issues.

This log can be found by issuing the following on the database instance:

  cat /db/mysql/log/slow_query.log  

 

Once you know where the issue is taking place, consider adding indexes to troubled tables. Searching an index on a table with 1,000 rows is 100 times faster than searching the same table without an index. When adding an index, it’s important to note that the table will lock—so no inserts will occur while the index is being built.

To add an index, use the following example:

  class AddIndexForStuff
    def change
      add_index :stuff, :stuff_id
    end
  end

Caching

Caching is storing things in memory for repeated or future use. Rails makes caching easy, though the best is the type used without involving the application.

It’s possible to leverage things like Nginx to cache static files. Page-caching of static files when using Rails and Nginx is as easy as the following:

# creates on #index, #show, etc
caches_page :index
        
# expires on #creates, #update, #destroy, etc
expire_page :action => :index

 

To properly serve these cached objects, it will be necessary to set up Nginx to do so. Using the front-end server, you can do the following (this example assumes you are using Unicorn as your webserver. For more info on Rails webservers, see this article):

upstream upstream_enki {
    server unix:/var/run/engineyard/unicorn_enki.sock fail_timeout=0;
}

location ~ ^/(images|assets|javascripts|stylesheets)/ {
    try_files $uri $uri/index.html /last_assets/$uri /last_assets/$uri.html @app_enki;
    expires 10y;
}

location / {
    if (-f $document_root/system/maintenance.html) {return 503; }
        try_files $uri $uri/index.html @app_enki;
}

Moving static assets like images or assets upstream so that we can page-cache and improve performance is a standard setup for an Engine Yard account. If nothing matches the paths we have established, it will then hit the application and serve what is there.

If page-caching is not an option, the best choice for performance improvement is memcache. This is the standard caching technique in Rails and is easy to use. Simply set your cache_store to mem_cache_store and add memcache servers as follows:

# config/intializers/memcached.rb
config.cache_store = :mem_cache_store,
    "server-1:11211",
       "server-2:11211",
       "server-3:11211",
       "server-4:11211"

 

Rails will handle hashing the memcache out so that it can be charted. As in the example above, we recommend using multiple memcache servers to improve performance and get the expected boost.

Action-caching is another way to improve app performance. It is like page-caching, except the entire contents of the action will be stored on a cache store. The benefit is that any before_filters will still be called. Usually this is for ensuring any validation or log-in functionality will be called, while other cached items can be stored to improve performance:


before_filter :make_sure_things_are_ok
caches_action :all_the_things

def all_the_things
    @all_things = Thing.all_in_some_way
end

def expire
    expire_action :action => :all_the_things
end

Caching and database performance are only two areas to look at when considering how to make your application run at peak condition. The goal of every developer is to deliver the best possible user experience, and better performance is one way to ensure this.

Also check out this post on Rails Performance.

Free Ebook:
Should I Hire DevOps or Outsource to a Provider?

You have to invest in your infrastructure: Do you hire DevOps for this critical function, assign it to your already overworked engineers, or outsource to a provider that offers full-stack capabilities?

Should I Hire DevOps?

PJ Hagerty

 
Programmer, writer, musician, random person! Chaotic Neutral #rubykaraoke cofounder
Find me on:

Comments

Subscribe Here!