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.