Performance 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.
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:
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 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):
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.