Ruby on RailsSeniorSystem design

Каковы лучшие практики производительности Rails-приложения при высокой нагрузке?

Лучшие практики производительности Rails при высокой нагрузке: устранить N+1 через eager loading, применять database indexes, кэшировать на уровне Redis (fragment + low-level), использовать пагинацию вместо загрузки всей коллекции, выносить тяжёлые операции в Sidekiq, настраивать Puma concurrency и connection pool.

Производительность Rails при высокой нагрузке

1. Устранение N+1 запросов

# Диагностика: Bullet гем
# config/environments/development.rb
config.after_initialize do
  Bullet.enable        = true
  Bullet.alert         = true
  Bullet.rails_logger  = true
  Bullet.add_footer    = true
end

# Исправление: includes / preload / eager_load
@posts = Post.includes(:author, tags: :category)
             .where(published: true)
             .order(created_at: :desc)
             .page(params[:page]).per(20)

# Для counter: counter_cache вместо .count
class Comment < ApplicationRecord
  belongs_to :post, counter_cache: true
end
# migration: add_column :posts, :comments_count, :integer, default: 0

2. Database: индексы и запросы

# migration
class AddPerformanceIndexes < ActiveRecord::Migration[7.2]
  def change
    add_index :posts, [:published, :created_at]
    add_index :posts, :author_id  # foreign key без индекса — частая ошибка
    add_index :users, :email, unique: true
    # Partial index для мягкого удаления
    add_index :posts, :created_at, where: "deleted_at IS NULL"
  end
end
# EXPLAIN ANALYZE через ActiveRecord
Posts.where(published: true).order(:created_at).explain
# => Показывает план запроса

# Избегать SELECT * — используйте select
User.select(:id, :name, :email).where(active: true)

3. Кэширование

# Fragment caching с Russian Doll
# config/environments/production.rb
config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"], pool_size: 5 }

# Шаблон
<% cache ["v2", @post, current_user&.id] do %>
  <%= render "post", post: @post %>
<% end %>

# Low-level для дорогих вычислений
def sidebar_stats
  Rails.cache.fetch("sidebar_stats", expires_in: 10.minutes) do
    {
      total_users: User.count,
      active_today: User.where("last_seen_at > ?", 24.hours.ago).count
    }
  end
end

4. Фоновые задачи и async

# Тяжёлые операции — в Sidekiq
class GenerateReportJob < ApplicationJob
  queue_as :low_priority

  def perform(report_id)
    report = Report.find(report_id)
    data = ReportBuilder.new(report).build  # тяжёлая агрегация
    report.update!(data: data, status: :ready)
    ReportMailer.ready(report).deliver_later
  end
end

# Пакетная отправка emails
class BulkNotificationJob < ApplicationJob
  def perform(user_ids, message)
    User.where(id: user_ids).find_each(batch_size: 100) do |user|
      UserMailer.notification(user, message).deliver_later
    end
  end
end

5. Конфигурация Puma

# config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY") { 2 }  # = CPU cores
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count

preload_app!

on_worker_boot do
  ActiveRecord::Base.establish_connection
end

6. Asset pipeline и CDN

# config/environments/production.rb
config.asset_host = "https://cdn.example.com"
config.public_file_server.enabled = true
config.public_file_server.headers = {
  "Cache-Control" => "public, max-age=31536000, immutable"
}

7. Профилирование

# rack-mini-profiler для dev
gem "rack-mini-profiler"
gem "memory_profiler"
gem "stackprof"  # CPU profiling

# Просмотр медленных запросов в логе
# config/environments/production.rb
config.log_level = :warn
ActiveRecord::Base.logger = nil  # убрать AR лог в production

Подводные камни

  • Puma workers * threads должно быть меньше или равно pool в database.yml — при несоответствии ActiveRecord::ConnectionTimeoutError под нагрузкой.
  • preload_app! в Puma с CoW экономит память, но форкает соединения с БД — нужен on_worker_boot { ActiveRecord::Base.establish_connection }.
  • Redis cache_store без pool_size создаёт новое соединение на каждый запрос — при 1000 RPS исчерпывается лимит соединений Redis.
  • Fragment cache без версионирования ключа ("v1") показывает устаревший HTML после деплоя с изменением шаблона.
  • find_each не поддерживает order — если нужна сортировка при batch-обработке, используйте in_batches с явным order по первичному ключу.
  • Rack::Deflater (gzip) добавляет CPU overhead — при большом числе мелких ответов замеряйте реальный выигрыш vs nginx gzip.
  • Bullet в production вызывает overhead — включайте только в development/staging.
  • HTTP/2 push через ActionDispatch::Flash headers не работает с большинством CDN — не полагайтесь на server push для критичных ресурсов.

Common mistakes

  • Сводить performance high load к названию метода без lifecycle и failure path.
  • Игнорировать модель runtime: Rails 8.1 строит приложение вокруг Rack, routing, controllers, Active Record, views и conventions over configuration.
  • Не отделять validation, authorization, transaction boundary и business logic.
  • Не обсуждать idempotency, retries, shutdown и observability.

What the interviewer is testing

  • Объясняет performance high load через конкретную точку lifecycle в Ruby on Rails.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.
  • Связывает решение с метриками, backpressure, retry policy и graceful shutdown.

Sources

Related topics