0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Renderでのメモリ不足をRails8+Cloudinaryで回避する手順

Posted at

1. はじめに

Rails8 アプリを Render にデプロイ中、画像処理を多用したところ、メモリ不足でインスタンスが落ちるエラーが発生。
原因と対応方法を忘れないために、まとめました。

開発環境
・Ruby 3.3.6
・Rails 8.0.2.1
・Docker(開発環境)
・DB: PostgreSQL
・画像管理: Cloudinary
・デプロイ: Render / Neon

2. エラーメッセージ

Instance failed: c2g7h
Ran out of memory (used over 512MB) while running your code.
  • Render のインスタンスが 512MB 以上のメモリを消費
  • メモリ制限を超えたためプロセスが強制終了
    つまり サーバー側のメモリ不足 によるクラッシュ

3. 原因

  • ActiveStorage 側でバリアント処理(サーバーで画像加工)してた
  • 店舗一覧で複数画像を扱う処理でメモリを大量に消費していた

4. 対応方法

サーバーで画像加工せず、バリアント処理を Cloudinary に移行

Cloudinary の公式ドキュメント

4-1. Helperに画像URL生成

app/helpers/spots_helper.rb
module SpotsHelper
  # 1枚目の画像URL取得(一覧用)
  def display_image_url(record, size: :thumb, image_attribute: :images)
    # Cloudinaryでの画像URL生成を試行
    cloudinary_url = cloudinary_image_url(record, size: size, image_attribute: image_attribute)
    return cloudinary_url if cloudinary_url.present?

    # Cloudinary失敗時はActiveStorageでフォールバック
    fallback_image_url(record, size, image_attribute)
  rescue => e
    Rails.logger.error "画像URL生成エラー: #{e.message}"
    nil
  end

  # 全画像のURL配列取得(詳細用)
  def display_all_image_urls(record, size: :detail, image_attribute: :images)
    return [] unless record.has_images?(image_attribute: image_attribute)

    # Cloudinaryで全画像処理を試す
    cloudinary_urls = all_cloudinary_image_urls(record, size: size, image_attribute: image_attribute)
    return cloudinary_urls if cloudinary_urls.present?

    # フォールバック:ActiveStorageで全画像処理
    all_fallback_image_urls(record, size, image_attribute)
  rescue => e
    Rails.logger.error "全画像URL生成エラー: #{e.message}"
    []
  end

  private

  # Cloudinary:1枚目のURL
  def cloudinary_image_url(record, size: :thumb, image_attribute: :images)
    return nil unless record.has_images?(image_attribute: image_attribute)

    image_key = record.image_key(image_attribute: image_attribute)
    return nil unless image_key.present?

    # 画像サイズ
    dimensions = image_dimensions(size)
    cl_image_path(image_key,
      width: dimensions[:width],
      height: dimensions[:height],
      crop: :fill,
      quality: :auto
    )
  rescue => e
    Rails.logger.error "Cloudinary画像URL生成エラー: #{e.message}"
    nil
  end

  # Cloudinary:全画像のURL配列
  def all_cloudinary_image_urls(record, size: :detail, image_attribute: :images)
    return [] unless record.has_images?(image_attribute: image_attribute)

    image_keys = record.all_image_keys(image_attribute: image_attribute)
    return [] unless image_keys.present?

    dimensions = image_dimensions(size)
    
    image_keys.map do |key|
      cl_image_path(key,
        width: dimensions[:width],
        height: dimensions[:height],
        crop: :fill,
        quality: :auto
      )
    end
  rescue => e
    Rails.logger.error "Cloudinary全画像処理エラー: #{e.message}"
    []
  end

  # ActiveStorage:1枚目のURL
  def fallback_image_url(record, size, image_attribute)
    return nil unless record.has_images?(image_attribute: image_attribute)

    first_image = record.first_image(image_attribute: image_attribute)
    return nil unless first_image&.blob.present?

    rails_representation_url(first_image.variant(size))
  rescue => e
    Rails.logger.error "ActiveStorage処理エラー: #{e.message}"
    nil
  end

  # ActiveStorage:全画像のURL配列
  def all_fallback_image_urls(record, size, image_attribute)
    return [] unless record.has_images?(image_attribute: image_attribute)

    all_images = record.all_images(image_attribute: image_attribute)
    
    all_images.map do |image|
      next unless image&.blob.present?
      rails_representation_url(image.variant(size))
    end.compact
  rescue => e
    Rails.logger.error "ActiveStorage全画像処理エラー: #{e.message}"
    []
  end

  # 画像サイズ設定
  def image_dimensions(size)
    case size
    # 一覧(サムネ)用サイズ
    when :thumb
      { width: 200, height: 200 }
    # 詳細表示用サイズ
    when :detail
      { width: 600, height: 600 }
    else
      { width: 200, height: 200 }
    end
  end
end

なぜHelperに書いたか

画像URL生成は表示用の処理なので、Railsの設計思想では「どう見せるか」に関わる処理はビュー寄りの役割を持つHelperにまとめるのが適切。

Railsでのざっくり分け方

  • モデル:データを管理・保存する場所
  • ヘルパー:ビューで使うための便利な処理を書く場所

cl_image_tag メソッドを使わなかった理由

# cl_image_tag の場合
= cl_image_tag(image_key, width: 200, height: 200)
# Cloudinary専用の<img>タグが直接生成される
# でも、Cloudinaryでエラーが起きたら画像が表示されない

# cl_image_path の場合  
= image_tag cl_image_path(image_key, width: 200, height: 200)
# CloudinaryのURLだけ取得して、通常のimage_tagで表示
# エラーの時はActiveStorageで対応できる
  • 保険が作れる: フォールバック機能
  • どんな画像でも同じ書き方: 統一インターフェース

4-2.モジュールにモデル共通機能を追加

app/models/concerns/image_processable.rb
# 画像処理
module ImageProcessable
  extend ActiveSupport::Concern

  # 画像が存在するかの確認
  def has_images?(image_attribute: :images)
    send(image_attribute).attached?
  end

  # 最初の画像を取得
  def first_image(image_attribute: :images)
    images_collection = send(image_attribute)
    return nil unless images_collection.attached?
    images_collection.first
  end

  # 全画像を取得
  def all_images(image_attribute: :images)
    images_collection = send(image_attribute)
    return [] unless images_collection.attached?
    images_collection.to_a
  end

  # 画像のキーを取得(Cloudinary用)
  def image_key(image_attribute: :images)
    first_img = first_image(image_attribute: image_attribute)
    first_img&.blob&.key
  end

  # 全画像のキーを取得(Cloudinary用)
  def all_image_keys(image_attribute: :images)
    all_images(image_attribute: image_attribute).map do |image|
      image&.blob&.key
    end.compact
  end

  # 画像のblobを取得
  def image_blob(image_attribute: :images)
    first_img = first_image(image_attribute: image_attribute)
    first_img&.blob
  end
end

モジュールって何?

  • モジュールは「共通で使える処理をまとめた箱」みたいなもの
  • モデル(UserとかSpotとか)の中に直接書くのではなく、別ファイルにまとめて 必要なモデルに「include」して使う

4-3.views

app/views/maps/search.html.slim
/ 修正前 */
- thumbnail_url = spot.display_image_url(size: :thumb, image_attribute: :images)
/ 修正後 */
- thumbnail_url = display_image_url(spot, size: :thumb, image_attribute: :images)

なぜ修正が必要だったか?

メソッドをモデルから Helper に移動したため、spot.display_image_url ではなく display_image_url(spot) を使用

5. 結果

サーバー側での重い画像処理がなくなったため、Render のメモリ消費が減少
インスタンスが落ちることなく、安定してデプロイ可能になりました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?