The website is under heavy load + ROR

Learn the website is under heavy load + ror with practical examples, diagrams, and best practices. Covers ruby-on-rails, ruby, apache development techniques with visual explanations.

Taming the Beast: Optimizing Ruby on Rails Under Heavy Load with Apache and Passenger

Server racks with glowing lights, symbolizing heavy load and performance optimization

Learn how to diagnose and resolve performance bottlenecks in Ruby on Rails applications running on Apache with Passenger, focusing on common issues under high traffic.

Running a Ruby on Rails application can be a joy, but when traffic spikes, that joy can quickly turn into a nightmare of slow responses, timeouts, and frustrated users. This article delves into common performance issues encountered when a Rails application, served by Apache and Passenger on a CentOS environment, comes under heavy load. We'll explore diagnostic techniques, configuration adjustments, and best practices to keep your application responsive and stable, even during peak demand.

Understanding the Bottleneck: Where Does Performance Go Wrong?

Before optimizing, it's crucial to identify where the performance bottleneck lies. Is it the database? The application code? The web server? Or the application server itself? A common misconception is to immediately scale up resources without understanding the root cause. This often leads to throwing more money at the problem without a lasting solution. We'll start by looking at the typical architecture and potential choke points.

flowchart TD
    U[User Request] --> A[Apache Web Server]
    A --> P[Passenger Application Server]
    P --> R[Ruby on Rails Application]
    R --> D[Database (e.g., PostgreSQL/MySQL)]
    D -- Query Result --> R
    R -- Rendered Page --> P
    P -- HTTP Response --> A
    A -- HTTP Response --> U

    subgraph Bottlenecks
        A -- "High Apache Load" --> B1(Apache Configuration)
        P -- "Passenger Process Limits" --> B2(Passenger Configuration)
        R -- "Slow Code/N+1 Queries" --> B3(Rails Application Code)
        D -- "Slow Queries/Indexing" --> B4(Database Performance)
    end

    B1 & B2 & B3 & B4 -- "Impacts" --> U

Typical Ruby on Rails Application Stack and Potential Bottlenecks

As illustrated in the diagram, a request travels through several layers. Each layer can introduce latency or become a bottleneck. Apache handles initial connections, Passenger manages Rails processes, the Rails application executes code and interacts with the database, and the database stores and retrieves data. Identifying the specific layer under stress is the first step towards effective optimization.

Diagnosing Performance Issues on CentOS

CentOS provides a robust set of tools for system monitoring. When your Rails application is struggling, these tools are invaluable for pinpointing the problem. We'll focus on CPU, memory, I/O, and network usage, as well as Apache and Passenger specific metrics.

top -c
htop
vmstat 1
iostat -xz 1
free -m
apachectl status
passenger-status

Essential Linux commands for system monitoring

Look for processes consuming excessive CPU or memory. If httpd (Apache) processes are high, it could indicate an Apache configuration issue or too many concurrent connections. If ruby processes are high, it points to the Rails application or Passenger. High I/O wait times from iostat might suggest disk bottlenecks, often related to database activity or logging.

Optimizing Apache and Passenger Configuration

Once you have an idea of the bottleneck, you can start making targeted adjustments. For Apache and Passenger, the goal is to balance resource usage with the ability to handle concurrent requests efficiently.

Apache Configuration (httpd.conf or included files)

<IfModule prefork.c>
    StartServers        5
    MinSpareServers     5
    MaxSpareServers     10
    ServerLimit         256
    MaxRequestWorkers   256
    MaxConnectionsPerChild  0
</IfModule>

<IfModule worker.c>
    StartServers        2
    MinSpareThreads     25
    MaxSpareThreads     75
    ThreadsPerChild     25
    MaxRequestWorkers   250
    MaxConnectionsPerChild  0
</IfModule>

Example Apache MPM (Multi-Processing Module) configuration

The choice between prefork and worker MPMs is critical. prefork is stable but uses more memory per process. worker is more memory-efficient by using threads but requires thread-safe modules. For Ruby on Rails with Passenger, worker or event MPMs are generally preferred due to their threading model aligning better with Passenger's architecture. Adjust MaxRequestWorkers based on your server's RAM and CPU. A good starting point is (Total RAM - OS/DB RAM) / (Avg Rails Process Size). MaxConnectionsPerChild 0 prevents processes from restarting after a certain number of requests, which can be good for long-running applications but might lead to memory leaks accumulating over time. Consider a small non-zero value if memory leaks are observed.

Passenger Configuration

PassengerMaxPoolSize 10
PassengerMaxInstancesPerApp 0
PassengerMinInstances 1
PassengerPoolIdleTime 300
PassengerMaxRequests 0
PassengerStatThrottleRate 5

Example Passenger configuration in Apache virtual host or global config

PassengerMaxPoolSize defines the total number of Rails application processes Passenger can spawn across all applications. PassengerMaxInstancesPerApp 0 means no limit per application (it will use up to PassengerMaxPoolSize). PassengerMinInstances ensures a certain number of processes are always running. PassengerPoolIdleTime controls how long an idle process stays alive before being shut down. PassengerMaxRequests can be used to restart processes after a certain number of requests, mitigating memory leaks.

Application-Level Optimizations for Ruby on Rails

Even with perfectly tuned server configurations, a poorly optimized Rails application will still struggle under load. This is often where the biggest gains can be made.

Database Query Optimization

N+1 queries are a notorious performance killer. They occur when an application executes N additional queries for each result of an initial query. For example, fetching a list of posts and then querying the author for each post individually.

# Bad: N+1 query
posts = Post.all
posts.each do |post|
  puts post.author.name # Each call to .author triggers a new DB query
end

# Good: Eager loading with `includes`
posts = Post.includes(:author).all
posts.each do |post|
  puts post.author.name # Author is already loaded, no new DB query
end

Avoiding N+1 queries with includes

Ensure your database has appropriate indexes on frequently queried columns, especially foreign keys. Use tools like bullet gem in development to detect N+1 queries.

Caching Strategies

Rails offers various caching mechanisms. Employing them effectively can drastically reduce the load on your application and database.

1. Page Caching

For static pages that rarely change, page caching can serve HTML files directly from the web server, bypassing the Rails stack entirely. This is the fastest form of caching.

2. Action Caching

Caches the output of an entire action, including filters. Useful for actions that produce the same output for all users.

3. Fragment Caching

Caches portions of a view. This is highly effective for dynamic pages where only certain parts change frequently. Use cache helper in your views.

4. Low-level Caching

Caches arbitrary values or results of expensive computations. Useful for caching query results or complex object graphs. Use Rails.cache.fetch.

# Example of fragment caching in a view
<% cache  ['v1', @product] do %>
  <div class="product-details">
    <h2><%= @product.name %></h2>
    <p><%= @product.description %></p>
  </div>
<% end %>

Fragment caching example in an ERB view

Background Jobs

Offload long-running tasks (e.g., sending emails, processing images, generating reports) to background job processors like Sidekiq or Resque. This frees up your web processes to handle user requests quickly, improving perceived performance.

sequenceDiagram
    actor User
    participant WebApp as Rails Web App
    participant JobQueue as Redis/Sidekiq
    participant Worker as Sidekiq Worker
    participant ExternalService as Email/Image Processor

    User->>WebApp: Request (e.g., Sign Up)
    WebApp->>WebApp: Process User Data
    WebApp->>JobQueue: Enqueue 'Send Welcome Email' Job
    WebApp-->>User: Respond 'Account Created!'
    JobQueue->>Worker: Dequeue Job
    Worker->>ExternalService: Send Email
    ExternalService-->>Worker: Email Sent Confirmation
    Worker->>JobQueue: Mark Job Complete

Offloading tasks with Background Jobs

By implementing these strategies, you can significantly reduce the load on your Ruby on Rails application, ensuring it remains performant and reliable even under the heaviest traffic.