0

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

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

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

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

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

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

    • 我能做些什么来捕捉内存问题并在控制器中进行救援,所以如果有任何“坏”评论,它们不会破坏整个网站?

    • 您在代码中看到任何可能导致这种怪物内存分配的东西吗?

下面的代码:

评论#创建:

  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
4

2 回答 2

2

对我来说突出的一件事是你的草皮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
于 2018-10-29T16:46:36.643 回答
1

突出的问题是:

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

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

deliver顺便说一句,Rails 5 已弃用,支持deliver_nowand deliver_later。)

于 2018-10-29T16:16:28.120 回答