Ruby on RailsSeniorExperience

Какие production-риски есть у Ruby on Rails: blocking code, connection pooling, config, auth, observability, deploy или graceful shutdown?

Основные production-риски Rails: блокирующий I/O в синхронных контроллерах, исчерпание пула БД, утечки соединений, небезопасные дефолты auth (mass assignment, IDOR), отсутствие structured logging и проблемы с graceful shutdown при деплое.

Production-риски Ruby on Rails

1. Blocking code и модель потоков

Puma использует thread-per-request модель. Один медленный запрос (внешний HTTP, файловая операция) блокирует поток. При RAILS_MAX_THREADS=5 пять одновременных медленных запросов исчерпывают пул.

# Плохо: синхронный HTTP внутри контроллера
def show
  result = Net::HTTP.get(URI("https://slow-api.example.com/data"))
  render json: result
end

# Хорошо: вынести в фоновый джоб
def show
  FetchExternalDataJob.perform_later(current_user.id)
  render json: { status: "queued" }
end

2. Connection Pooling

ActiveRecord использует пул соединений из config/database.yml. Значение pool должно соответствовать RAILS_MAX_THREADS. Превышение вызывает ActiveRecord::ConnectionTimeoutError.

# config/database.yml
production:
  adapter: postgresql
  url: <%= ENV["DATABASE_URL"] %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  checkout_timeout: 5
  idle_timeout: 300
  connect_timeout: 5

При Sidekiq (concurrency: 25) пул должен быть не меньше 25 + запас для web.

3. Конфигурация и секреты

Не передавайте секреты через config/credentials.yml.enc без rotation-процесса. Используйте переменные окружения или Vault:

# Проверить что секреты не попали в git
git log --all --full-history -- config/master.key
# Ротация ключа
RAILS_ENV=production rails credentials:diff

4. Безопасность аутентификации и авторизации

  • Mass assignment: без строгого params.require(:user).permit(:name, :email) возможна инъекция admin: true.
  • IDOR: Post.find(params[:id]) без проверки владельца даёт доступ к чужим данным. Всегда используйте current_user.posts.find(params[:id]).
  • Session fixation: вызывайте reset_session после логина.
# Безопасный паттерн
def update
  @post = current_user.posts.find(params[:id]) # scope к пользователю
  @post.update!(post_params)
end

private

def post_params
  params.require(:post).permit(:title, :body) # whitelist
end

5. Observability

Rails по умолчанию пишет plain-text лог. В production нужен structured JSON для Datadog/Loki:

# config/environments/production.rb
config.log_formatter = proc do |severity, datetime, progname, msg|
  { level: severity, time: datetime, msg: msg }.to_json + "\n"
end

# Или через гем lograge
config.lograge.enabled = true
config.lograge.formatter = Lograge::Formatters::Json.new
config.lograge.custom_options = ->(event) {
  { user_id: event.payload[:user_id], request_id: event.payload[:request_id] }
}

6. Graceful Shutdown и деплой

Puma поддерживает graceful shutdown через SIGTERM — дожидается завершения in-flight запросов. Kubernetes должен отправлять SIGTERM и ждать terminationGracePeriodSeconds.

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

on_worker_shutdown do
  ActiveRecord::Base.connection_pool.disconnect!
end

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

  • После hot restart Puma старые воркеры доживают запросы, но новые уже обрабатывают трафик — миграции с удалением колонок должны быть в отдельном деплое.
  • ActiveRecord::Base.connection в потоке, не управляемом Rails, не возвращает соединение в пул — всегда оборачивайте в with_connection.
  • Devise по умолчанию хранит remember_me токен в виде SHA1 — обновляйте stretches и используйте Devise 4.9+.
  • Rack::Attack без лимитов на /api/v1/sessions позволяет брутфорс паролей — добавьте throttle по IP и email.
  • Rails.logger.info в hot path создаёт много строк и аллоцирует память — используйте уровень :warn в production для не-критичных событий.
  • Отсутствие health check endpoint с проверкой БД и Redis вызывает проблемы при rolling deploy — Kubernetes считает pod healthy пока не настроен readinessProbe.
  • N+1 в GraphQL или serializer незаметен в dev, но на production при 1000 записях убивает БД — используйте Bullet в staging и query count assertions в тестах.
  • Sidekiq без Redis Sentinel/Cluster — единая точка отказа: падение Redis останавливает всю обработку джобов.

What hurts your answer

  • Говорить только о запуске Ruby on Rails, но не об эксплуатации
  • Не упоминать observability, обновления, безопасность и rollback
  • Описывать риски абстрактно, без способов их снижать

What they're listening for

  • Видит production-риски Ruby on Rails
  • Говорит про monitoring, rollout, rollback и безопасность
  • Умеет ранжировать риски по вероятности и влиянию

Related topics