Rails Grape on Rails + Puma 线程安全性的讨论?

dudu_zzzz · 2016年06月01日 · 最后由 psvr 回复于 2018年08月15日 · 3171 次阅读
本帖已被设为精华帖!

今天愉快的写 grape-api 时,忽然惊觉可能有线程安全问题!!我在 grape-issuepuma-issue 都发现了类似的提问。

根据grape-issue里的描述,他把服务器从 Unicorn 切换到 Puma 以后,发现 so much users get other users data from api。

他的代码是这样

base.rb

module V3
  class Base < Grape::API 
    helpers V3Helpers

    include V3::Photos
  end
end

v3_helpers.rb

def current_user
return @current_user if defined? @current_user

@current_user = User.find(params[:uid])
end

photos.rb

module V3
  module Photos
    extend ActiveSupport::Concern

    included do

      resources :photos do
        get do 
          photos = Photo.where(user_id: current_user.id)
          photos.to_json 
        end   
      end
    end
end
end

Base的实例变量 @current_user (我并不确定这个 @current_user 是否是 Base 的实例变量),似乎是被并发干扰了,很像 Java Servelet 里的线程安全的问题。

可我也正准备这么写呢,明明在 Rails 里这样用 @current_user 就 ok 啊。

我从 How Do I Know Whether My Rails App Is Thread-safe or Not? 里找到了为什么Rails里这样用是ok的。

In general, Rails creates a new controller object of every HTTP request, and everything else flows from there.

但是 Grape on Rails 里似乎是有线程安全问题的,那么如何解决呢?

我的想法是像下面这样避免使用 Instance Variable, 不知是否正确。如果是错误的,那么正确的方式又是什么呢?

v3_helpers.rb

def current_user
  User.find(params[:uid])
end

photos.rb

module V3
  module Photos
    extend ActiveSupport::Concern

    included do

      resources :photos do
        get do 
          user = current_user
          photos = Photo.where(user_id: user .id)
          photos.to_json 
        end   
      end
    end
end
end

PS: 实在是难以相信 Grape 和 Puma 一起用会有这么严重的问题啊,都不敢用 Grape 了。在生产环境使用过 Grape 的伙伴们有遇到过线程安全问题吗?

共收到 28 条回复

线程安全是切换puma后才有的吧

#1楼 @zouyu 根据那个人提的issue,的确是切换到puma后出现的问题,unicorn这种基于进程的肯定不会有这个问题了。

只能用回 RequestStore

这不能算是「问题」吧。Grape本来设计理念就和Rails不同,加上你主动选择了多线程的服务器方案,那你就得自己解决线程安全性问题了。

在用。还没发现问题。每次请求,关于current_user,没用helpers 方法,而是需要用户验证的地方,每次请求都拿token验证用户存在否。这样不可避免的查数据库和app端要较长时间保存token数据(有一定安全风险)

这个是有实际案例的,当初也是突然从 Unicorn 切到 Puma,然后测试的时候没发现,一上线就完蛋了,导致用户数据写乱了 @wxianfeng 来说说

#5楼 @pathbox 量少的时候很难出发,我们那个应用也是到了非常大量的时候才碰到,然后就晚了。结果 @wxianfeng 他们搞了很久...

#7楼 @huacnlee 我在github上看到的issue的确就是 @wxianfeng 提的...

#7楼 @huacnlee 原来如此。等@wxianfeng 讲解具体案例

Thread.current.thread_variable_get(:current_customer)

你是 Mounting 在 Rails 上,还是 Sinatra,还是自己创建了一个 Rack application。

#11楼 @victor 我是mounting on rails的

Grape mounting on Rails 4.x + Puma,纯 API 端日 PV 2000W,没遇到这个问题。

#13楼 @victor 意思是你们也写了类似的current_user方法,其中使用了实例变量,然而并没有遇到current_user混乱嘛?

那真是太好了蛤蛤蛤

#14楼 @dudu_zzzz 这帖子 问题是特指 current_user 出现问题么?

#15楼 @pathbox 应该是指由current_user引出的线程安全问题。对于我而言,current_user是眼前的问题。grape+puma是否可以在production下使用,怎样避开类似的线程安全问题,才是我更关心的。

这样用出现线程安全是必然,和是否用grape没一点关系。 你的解决方案还是可行。还有一个更好的方案是用 env['warden'].user 获取当前用户。 在warden文档里有。

@alex_l_zhang 那是不是就得规避实例变量的使用。 @dudu_zzzz 关注同样的问题,我们也在用,只是还没有大规模使用,如果真有问题,直接调整下实现方案

是的,实例变量多线程下都可以改变它,所以是不安全的。 #18楼 @wikimo

具体一点是不是可以这么理解。

  • helper 中禁止实例变量出现,helper垮 resource 可能引发问题;

  • 单个resource中,使用实例变量没有问题;

@alex_l_zhang

#20楼 @wikimo 也许是这一句

return @current_user if defined? @current_user

从别的线程中拿到了那个线程中存在的@current_user

#21楼 @pathbox 我们没细看最上面的帖子,不过这里貌似可以更简洁:

return @current_user if @current_user

grape你需要在before 和after 对user对象处理,比较安全的办法是在Thread[:current_user]上存取对象。

Usage

require 'thread_safe'

sa = ThreadSafe::Array.new # supports standard Array.new forms
sh = ThreadSafe::Hash.new # supports standard Hash.new forms
cache = ThreadSafe::Cache.new

Rails 4.0+ 默认是引入了 'thread_safe' 这个gem的。

https://github.com/ruby-concurrency/thread_safe

这个事情今天正好又有人来邮件问我,电话 @wxianfeng 聊了一下。

结果是后面他又分析了两周,最终发现不是 Grape 引起的,而是由其他 Gem (一个 Fork 版本的 ActiveModel::Serializer )引起的。 不是 current_user 那个点的问题。

@dudu_zzzz 描述下我遇到的问题,

事情发生在 2016年03月,我们 API 采用 Grape 开发,线上请求量非常大,nginx 阻塞严重, 于是把 unicorn 改成了 puma,以提高并发,上线后收到用户反馈,看到了别人数据。。

刚开始我们一度怀疑是 current_user 问题, 最后发现不是这里的问题

根源是当时对象的 json 序列化用了非 release 版本的 active_model_serializers,其使用了非线程安全的类变量导致,也就是说同时来了两个用户的请求,A 用户 write 的类变量, 有可能被 B read 到。。

当时的 Gemfile 配置: gem 'active_model_serializers', git: 'https://github.com/rails-api/active_model_serializers.git', ref: '2df8804'

可以直接把 active_model_serializers clone 下来, git checkout 到 2df8804 找到该处代码:

active_model_serializers/lib/active_model/serializer.rb

def read_attribute_for_serialization(attr)
  if self.class._serializer_instance_method_defined?(attr)
    send(attr)
  elsif self.class._fragmented
    self.class._fragmented.read_attribute_for_serialization(attr)
  else
    object.read_attribute_for_serialization(attr)
  end
end

测试代码没发现这个问题 ?
测试代码是单线程跑的,不会出现该问题。

怎么排查到这个问题的 ?
不同用户并发请求压测,最小原则一步一步注释代码,排除法找到是 active_model_serializers 问题,进一步发现用了类变量。

教训 ?
1、不要使用非正式版本的代码,隐患极大。
2、测试代码要测试多用户并发场景,验证返回的数据。

供参考,欢迎交流 ~ @dudu_zzzz

这个问题是我两年前刚毕业,自己创业的时候做技术选型遇到的(后来很快也倒闭了😂

虽然是很久以前的问题,依然能发挥余热,有点开心

依然很喜欢ruby,不过迫于生活,现在工作内容是go写中间件

huacnlee 将本帖设为了精华贴 07月03日 14:39

慎用 puma

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册