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

dudu_zzzz · 2016年06月01日 · 最后由 n5ken 回复于 2020年11月30日 · 5358 次阅读
本帖已被管理员设置为精华贴

今天愉快的写 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 的伙伴们有遇到过线程安全问题吗?

线程安全是切换 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

我近来也遇到类似问题,没在 Rails 上,最后发现是 to_json 的问题,这个方法线程不安全

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