Finding/killing rails 中特定控制器的内存问题

Finding/killing a memory problem in a specific controller in rails

我在 Heroku 上的 Rails 上建立了一个网站,该网站通常在大约 90% 的内存使用情况下运行良好。

通过 Scout,我在我的 Rails-app 中发现了一个问题,我的 comments#create-controller 有时会分配 860k 内存,这会导致我的应用在超时等情况下长时间关闭.大多数情况下,分配的内存只是其中的一小部分,因此问题是间歇性的。

评论功能本身不是特别重要,但我仍然需要它。我相信它的三个不同部分可能会导致此内存问题:

  1. 内容字符串(即评论本身)太长。例如,如果垃圾邮件发送者发布超长文本。我认为这不是问题,因为我上次的内存峰值是由普通用户引起的,他发表了一条非常简短的评论。

  2. My rakismet-gem (https://github.com/joshfrench/rakismet) 和垃圾邮件检查。我使用的是最新版本 (1.5.4)。这可能是个问题,因为我真的不知道在使用内存时加载到内存中的内容。

  3. 代码中我的通知程序调用。

    • 我能做些什么来捕捉内存问题并在控制器中进行救援,这样如果有任何 "bad" 评论,它们就不会破坏整个站点?

    • 您在代码中看到任何可能导致此怪物内存分配的内容吗?

代码如下:

评论#创建:

  def create    
    require 'memory_profiler'
    report = MemoryProfiler.report do

    @comment = Comment.new(comment_params)
    spam_features = %w(\xA cialis informative the that this buy href)
    unless @current_administrator.present?
      if spam_features.any? {|str| @comment.content.include? str}
        logger.info "L: Comment include spam features"
        redirect_to article_path(Article.find('din-kommentar-har-inte-sparats')) and return 
      elsif @comment.author.size > 40 || @comment.author_email.size > 40
        logger.info "L: Comment author name or email too long (suspicious)"        
        redirect_to article_path(Article.find('din-kommentar-har-inte-sparats')) and return       
      end
    end

    # This shouldn't be here (but don't know how to put it in the model)
    if !@comment.blog_post_id.blank? # This is a comment on a blog post
      return_to_path = blog_post_path(BlogPost.find(@comment.blog_post_id))
    elsif !@comment.gift_id.blank? # This is a comment on a gift
      return_to_path = gift_path(Gift.find(@comment.gift_id))      
    elsif !@comment.contest_id.blank? # This is a comment on a contest     
      return_to_path = contest_path(Contest.find(@comment.contest_id))   
    elsif !@comment.christmas_fair_id.blank? # This is a comment on a christmas fair     
      return_to_path = christmas_fair_path(ChristmasFair.find(@comment.christmas_fair_id))
    elsif @comment.tmp_julrim # This is a comment on a christmas fair     
      return_to_path = rhymes_path                   
    else
      raise ActionController::RoutingError.new('Not Found')
    end
    return_to_path << "#comments"
    @comment.status_id = 3

    @comment.user_ip = request.remote_ip
    @comment.user_agent = request.env['HTTP_USER_AGENT']
    @comment.marked_as_spam = @comment.spam? # Using rakismet to check for spam
    #if !@comment.marked_as_spam || @current_administrator.present?
    respond_to do |format|      
      #@comment.status_id = 1 if @comment.contest_id == 44          
      if @comment.save
        Notifier.new_comment(@comment).deliver if Rails.env == 'production' unless @comment.marked_as_spam
        format.html { redirect_to return_to_path, notice: 'Din kommentar har registrerats och kommer att ses över innan den godkänns.' }
        # format.json { render action: 'show', status: :created, location: @comment }
      else
        format.html { render action: 'new' }
        format.json { render json: @comment.errors, status: :unprocessable_entity }
      end      
    end

    end

突出的问题是:

Notifier.new_comment(@comment).deliver if Rails.env == 'production' unless @comment.marked_as_spam

我假设这是一个 ActionMailer 对象。 deliver 是一种阻塞方法,而不是您通常希望在请求-响应周期中在生产中使用的方法。如果您的邮件服务器响应缓慢,这可能会导致严重延迟,因此您应该将其替换为 deliver_later 并确保您有像 Sidekiq 这样的工具可以在后台完成请求。

deliver 自 Rails 5 顺便说一句,已弃用,取而代之的是 deliver_nowdeliver_later。)

让我印象深刻的一件事是你的地盘 else 声明

raise ActionController::RoutingError.new('Not Found')

加薪。只需在此处渲染 401。您已经知道它是一个 401,可以避免通过堆栈加注。也可以将整个逻辑移至专用的受保护方法。以下是我将如何用评论重构你的方法。

# always do requires in the file before the class definition
# so this would go at the top of the file
require 'memory_profiler'

...

def create    
  report = MemoryProfiler.report do
    @comment = Comment.new(comment_params)
    check_admin?  

    # There is possibility to merge these with the comment params above 
    # during init above or just pass them to the model and act upon 
    # appropriately  there
    @comment.status_id = 3
    @comment.user_ip = request.remote_ip
    @comment.user_agent = request.env['HTTP_USER_AGENT']
    @comment.marked_as_spam = @comment.spam? # Using rakismet to check for spam

    #if !@comment.marked_as_spam || @current_administrator.present?
    respond_to do |format|      
      if @comment.save
        Notifier.new_comment(@comment).deliver if Rails.env.production? && !@comment.marked_as_spam
        format.html   { 
          if return_to_path == false
            render file: "public/401.html", status: :not_found # dump to 401 immediately
          else
            redirect_to return_to_path, notice: 'Din kommentar har registrerats och kommer att ses över innan den godkänns.' 
          end
        }
        # format.json { render action: 'show', status: :created, location: @comment }
      else
        format.html { render action: 'new' }
        format.json { render json: @comment.errors, status: :unprocessable_entity }
      end      
    end
  end
end

protected 

  def spam_features
    %w(\xA cialis informative the that this buy href)
  end

  def return_to_path 
    anchor = "comments" 
    if @comment.blog_post_id.present?
      blog_post_path(@comment.blog_post, anchor: anchor) # trust your associations vs. relookup and leverage the anchor option in url helpers
    elsif @comment.gift_id.present?
      gift_path(@comment.gift, anchor: anchor) # trust your associations vs. relookup and leverage the anchor option in url helpers
    elsif @comment.contest_id.present?
      contest_path(@comment.contest, anchor: anchor) # trust your associations vs. relookup and leverage the anchor option in url helpers
    elsif @comment.christmas_fair_id.present?
      christmas_fair_path(@comment.christmas_fair, anchor: anchor) # trust your associations vs. relookup and leverage the anchor option in url helpers
    elsif @comment.tmp_julrim
      rhymes_path(anchor: "comments") and leverage the anchor option in url helpers                   
    else
      false # give a testable exit condition and for short circut render
    end 
  end

  # if you were to check the comment_params vs an instantiated object, you could 
  # short circuit the controller method in a before_action 
  # Also check out known existing methods of spam prevention such as invisible_captcha or rack attack. Ideally 
  # once you hit your controller's method spam checking is done. 
  def check_admin? 
    # for clarity use positive logic check when possible, e.g. if blank? vs unless present? 
    # reduce your guard code to one the fewest levels necessary and break out into testable methods
    if has_spam? 
      logger.info {"L: Comment include spam features"} # use blocks for lazy evaluation of logger
      redirect_to article_path(Article.find('din-kommentar-har-inte-sparats')) and return 
    elsif has_suspicious_name? 
      logger.info {"L: Comment author name or email too long (suspicious)"} # use blocks for lazy evaluation of logger
      redirect_to article_path(Article.find('din-kommentar-har-inte-sparats')) and return       
    end
    # is there be an else condition here that we're not accounting for here? 
  end

  # this check is less than optimal, e.g. use of any? and include? has code smell
  def has_spam? 
    @current_administrator.blank? && spam_features.any? {|str| @comment.content.include? str } 
  end

  def has_suspicious_name?
    @current_administrator.blank? && @comment.author.size > 40 || @comment.author_email.size > 40
  end