本文翻译自:Rails from Request to Response 系列;个人选择了自己感兴趣的部分进行翻译,需要阅读原文的同学请戳前面的链接。

第一部分 导言(Introduction)

服务器

在讲 Rails 调用栈之前,先简单介绍一下不同服务器应用的作用,其中并不会涉及到各个服务器应用(比如 Thin 和 Unicorn 或 Nginx)的细节,因为文章的重点是讲 Rails 端的一些东西。

这里举一个 Unicorn 的简单例子,管窥整个 Rails 应用。

Unicorn架构

Unicorn 是一个实现了Rack接口的服务器应用,通过多个 worker 并行处理请求(request)。启动时,主进程会将 Rails App 代码加在到内存中,随后以加载进来内存为原料进行复制,生成一定数量的 worker,并对他们进行监控和信号捕获(比如被用作关闭和重启的 QUIT,TERM,USR1 信号等等)。这些 worker 负责处理的一个个真实的 web 请求(request)。

下图是 Unicorn 的架构(这幅图片来自Github一篇很棒的文章):

这些 worker 从共享的 socket 中读取 HTTP 请求(request),并将它们发给 Rails 应用。随后得到响应(response),写回到共享的 socket 中。这个过程中的大部份都发生在Unicorn HttpServer 类的 #process_client 方法中,下面是相关部分的代码:

  1. # unicorn/lib/unicorn/http_server.rb
  2. def process_client(client)
  3. status, headers, body = @app.call(env = @request.read(client))
  4. ...
  5. http_response_write(client, status, headers, body,
  6. @request.response_start_sent)
  7. client.shutdown
  8. client.close
  9. rescue => e
  10. handle_error(client, e)
  11. end

其中省略了一些关于HTTP 100状态处理和Rack socket hijacking相关的代码,感兴趣的话可以阅读完整版本

我们可以看到,这个方法的核心逻辑相当的简明!

第一行是Rack specification:Rack App 其实就是一个 Ruby Object,我们只需要为它写一个可以接受 hash 参数 env 的 #call 方法,让它的返回 [status, headers, body](译者:还不是很明白 rack 是什么鬼的同学可以去看一下这个视频,亲测好评)。

这个就是 Rails,Sinatra,Padrino 等那些兼容了Rake接口的框架的核心。回到 #process_client 方法,可以看到,我们向 @app 的 #call 方法传递 env 参数,并在 client 关闭之前,将响应(response)写回。

你没猜错,这个 @app 就是我们的 Rails 项目,我们来看他的声明:

  1. # blog/config/application.rb
  2. module Blog
  3. class Application < Rails::Application
  4. ...
  5. end
  6. end

这就是 Rails 调用栈的入口,但是如果你仔细观察,你会发现 #call 并没有定义在 Blog::Application,而是被声明在了父类 Rails::Application 中。

现在开始,我们需要了解 Rails 应用的继承机制,以及一个请求(request)是如何在 Rails 内部被处理的。

Rails Application and Engines

我们之前提到,整个 Rails 应用的入口 #call 被定义在了 Rails::Application 中,我们通过继承来使用它。这里是它的定义(源码):

  1. # rails/railties/lib/rails/application.rb
  2. module Rails
  3. class Application < Engine
  4. # Implements call according to the Rack API. It simply
  5. # dispatches the request to the underlying middleware stack.
  6. def call(env)
  7. env["ORIGINAL_FULLPATH"] = build_original_fullpath(env)
  8. env["ORIGINAL_SCRIPT_NAME"] = env["SCRIPT_NAME"]
  9. super(env)
  10. end
  11. ...
  12. end
  13. end

这里并没有什么东西,大部分功能通过调用 super 来执行。如果我们跟着代码看,可以发现 Rails::Application 类继承于 Rails::Engine 类。如果你熟悉Rails engines,你会惊喜地发现,Rails::Application 就是一个超级 engine!

我们来看看 Engine 类中的 #call 方法:

  1. # rails/railties/lib/rails/engine.rb
  2. module Rails
  3. class Engine < Railtie
  4. def call(env)
  5. env.merge!(env_config)
  6. if env['SCRIPT_NAME']
  7. env.merge! "ROUTES_#{routes.object_id}_SCRIPT_NAME" => env['SCRIPT_NAME'].dup
  8. end
  9. app.call(env)
  10. end
  11. ...
  12. end
  13. end

所以,Engine 类继承于 Rails::Railtie 。通过观察源码,我们可以发现Railtie0是 Rails 框架的核心,他为 initializers,config keys,generators 和 rack tasks 等提供钩子方法。

所有Rails的主要部件(ActionMailer,ActionView,ActionController 和 ActiveRecord)都是一个 Railtie,这也就是为什么你可以随意拆装组合这些部件。

在 Engine 的 #call 方法中我们看到了 #call 的另一个代理方法(delegation),这里的 app 代表的是什么?在同一个文件中,我们发现了他的定义:

  1. # rails/railties/lib/rails/engine.rb
  2. # Returns the underlying rack application for this engine.
  3. def app
  4. @app ||= begin
  5. config.middleware = config.middleware.merge_into(default_middleware_stack)
  6. config.middleware.build(endpoint)
  7. end
  8. end

现在我们到了一个牛逼的位置。这个engine构造了一个类似Rack application的中间件,并将 #call 方法代理(delegate)给它,他的终点是我们应用的 routes,ActionDispatch::Routing::RouteSet 类的一个实例。

Rack middleware可以用来「过滤」请求(request)和响应(response),并且可以将一个请求(request)处理过程分解成多个步骤,并视为一个「管道」进行处理,比如:处理权限认证,缓存等等。

你可以通过执行 rake middleware 来列出一个应用所使用的全部中间件。这里是我对 Blog 应用执行这条指令所得到的结果:

  1. $ RAILS_ENV=production rake middleware
  2. use Rack::Sendfile
  3. use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007f7ffb206f20>
  4. use Rack::Runtime
  5. use Rack::MethodOverride
  6. use ActionDispatch::RequestId
  7. use Rails::Rack::Logger
  8. use ActionDispatch::ShowExceptions
  9. use ActionDispatch::DebugExceptions
  10. use ActionDispatch::RemoteIp
  11. use ActionDispatch::Callbacks
  12. use ActiveRecord::ConnectionAdapters::ConnectionManagement
  13. use ActiveRecord::QueryCache
  14. use ActionDispatch::Cookies
  15. use ActionDispatch::Session::CookieStore
  16. use ActionDispatch::Flash
  17. use ActionDispatch::ParamsParser
  18. use Rack::Head
  19. use Rack::ConditionalGet
  20. use Rack::ETag
  21. run Blog::Application.routes

其中的大部分都不会讲,因为没有必要去了解全部这些中间件,即使一个请求(request),在到达 Blog::Application.routes 之前,会途径列表中从上到下所有的中间件。

到这里,我们已经完成了 App server / Rails application stack 的导言部分的介绍,接下来会着重介绍 Rails routing / dispatch stack。

译者总结

我们可以将 rack app 简化地表示为一个函数:

ƒ: env_set{ [status, headers, body] }

Rails app 其实就是若干个函数的嵌套,逐层对输入的 env 和返回 [status, headers, body] 进行加工。整个Rails的执行过程,不过如此。

  1. env(1) env(2) ... env(i) ... env(n)

令:s: [status, headers, body]

  1. s(n) s(n-1) ... s(i) ... s(1)

看完这篇文章后,终于理解了《Ruby社区应该去Rails化了》中这段话的意思:

Rails为何不适合做Web Service?

我发现了一个有意思的现象,最早的一批用Ruby开发Web Service服务的网站,都选择了用Rails开发,而在最近几年又不约而同抛弃Rails重写Web服务框架。当初用Rails的原因很简单,因为产品早期起步,不确定性很高,使用Rails快速开发,可以最大限度节约开发成本和时间。但为何当请求量变大以后,Rails不再适合了呢?

这主要是因为Rails本身是一个full-stack的Web框架,所有的设计目标就是为了开发Website,所以Rails框架封装过于厚重,对于需要更高性能更轻薄的Web Service应用场景来说,暴露出来了很多缺陷:

Rails调用堆栈过深,URL请求处理性能很差

Rails的设计目标是提供Web开发的 最佳实践 ,所以无论你需要不需要,Rails默认提供了开发Website所有可能的组件,但其中绝大部分你可能一辈子都用不上。例如Rails项目默认添加了20个middleware,但其中10个都是可以去掉的,我们自己的项目当中手工删除了这些middleware:

  1. config.middleware.delete 'Rack::Cache' # 整页缓存,用不上
  2. config.middleware.delete 'Rack::Lock' # 多线程加锁,多进程模式下无意义
  3. config.middleware.delete 'Rack::Runtime' # 记录X-Runtime(方便客户端查看执行时间)
  4. config.middleware.delete 'ActionDispatch::RequestId' # 记录X-Request-Id(方便查看请求在群集中的哪台执行)
  5. config.middleware.delete 'ActionDispatch::RemoteIp' # IP SpoofAttack
  6. config.middleware.delete 'ActionDispatch::Callbacks' # 在请求前后设置callback
  7. config.middleware.delete 'ActionDispatch::Head' # 如果是HEAD请求,按照GET请求执行,但是不返回body
  8. config.middleware.delete 'Rack::ConditionalGet' # HTTP客户端缓存才会使用
  9. config.middleware.delete 'Rack::ETag' # HTTP客户端缓存才会使用
  10. config.middleware.delete 'ActionDispatch::BestStandardsSupport' # 设置X-UA-Compatible, 在nginx上设置

其中最夸张的是ActionDispatch::RequestIdmiddleware,只有在大型应用部署在群集环境下进行线上调试才可能用到的功能,有什么必要做成默认的功能呢? Rails的哲学是:提供最全的功能集给你,如果你用不到,你自己手工一个一个关闭掉 ,但是这样带来的结果就是默认带了太多不必要的冗余功能,造成性能损耗极大。

我们看一个Ruby web框架请求处理性能评测 ,这个评测不访问数据库,也不测试并发性能,主要是测试框架处理URL请求路由,渲染文本,返回结果的处理速度。

Rack: 1570.43 request/s

Campig: 1166.16 request/s

Sinatra: 912.81 request/s

Padrino: 648.68 request/s

Rails: 291.27 request/s

Sinatra至少是Rails速度的3倍以上。

[Rails] 从 Request 到 Response(1)的更多相关文章

  1. [Rails] 从 Request 到 Response(2)

    本文翻译自:Rails from Request to Response 系列:个人选择了自己感兴趣的部分进行翻译,需要阅读原文的同学请戳前面的链接. 第二部分 路由(Routing) Blog::A ...

  2. Request 和 Response 原理

    * Request 和 Response 原理:     * request对象和response对象由服务器创建,我们只需要在service方法中使用这两个对象即可        * 继承体系结构: ...

  3. Request 、Response 与Server的使用

    纯属记录总结,以下图片都是来自 ASP.NET笔记之 Request .Response 与Server的使用 Request Response Server 关于Server.MapPath 方法看 ...

  4. request 和response

    当今web程序的开发技术真是百家争鸣,ASP.NET, PHP, JSP,Perl, AJAX 等等. 无论Web技术在未来如何发展,理解Web程序之间通信的基本协议相当重要, 因为它让我们理解了We ...

  5. Request和Response对象

    Request 和 Response 对象起到了服务器与客户机之间的信息传递作用.Request 对象用于接收客户端浏览器提交的数据,而 Response 对象的功能则是将服务器端的数据发送到客户端浏 ...

  6. Java 中的 request 和response 理解

    request和response(请求和响应)  1.当Web容器收到客户端的发送过来http请求,会针对每一次请求,分别创建一个用于代表此次请求的HttpServletRequest对象(reque ...

  7. 【转】request和response的页面跳转传参

    下面是一位园友的文章: jsp或Servlet都会用到页面跳转,可以用 request.getRequestDispatcher("p3.jsp").forward(request ...

  8. LoadRunner中取Request、Response

    LoadRunner中取Request.Response LoadRunner两个“内置变量”: 1.REQUEST,用于提取完整的请求头信息. 2.RESPONSE,用于提取完整的响应头信息. 响应 ...

  9. Spring mvc中使用request和response

    @ResponseBody @RequestMapping(value="/ip", method=RequestMethod.GET) public String getIP(H ...

随机推荐

  1. 在gem5的full system下运行 x86编译的测试程序 running gem5 on ubuntu in full system mode in x86

    背景 上篇博客写了如何在gem5的full system模式运行alpha的指令编译的程序,这篇博客讲述如何在gem5的full system模式运行x86指令集编译的程序,这两种方式非常类似. 首先 ...

  2. 微信内嵌H5网页 解决js倒计时失效

    项目要求:将H5商城页面嵌套到公司微信公众号里 项目本身的开发跟移动端网页并无太多差异,只是这昨天遇到一个问题,说是棘手,到也简单. 用户下单后,在选择支付方式页面,有个倒计时的逻辑(从下单时开始计算 ...

  3. Jquery源码分析与简单模拟实现

    前言 最近学习了一下jQuery源码,顺便总结一下,版本:v2.0.3 主要是通过简单模拟实现jQuery的封装/调用.选择器.类级别扩展等.加深对js/Jquery的理解. 正文 先来说问题: 1. ...

  4. 【JS学习笔记】提取行间事件

    行间提取事件第一种方法: function 名字() { ... } oBtn.onclick=名字: 第二种方法: oBtn.onclick=function () { ... } 其实在JS当中, ...

  5. 【JS学习笔记】关于function函数

    函数的基本格式 function 函数名() { 代码: } 函数的定义和调用 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transit ...

  6. Linux控制脚本:信号捕捉,作业控制,调整谦让度,以及计划任务

    1.关于信号以及信号捕捉 (1) $ ps  -au可以用来查看所有作业,包括暂停的和停止的,当然还有正在运行的. 在STAT这一列表示各个作业的状态,S表示Stop,R表示Run,T表示被追踪的或停 ...

  7. mongo数据库时间存储的问题

    题记:项目中要加的内容,可以实现对设备的预定,被某个用户预定后的设备就不能再被其他用户所使用了,用户预定的时候就需要输入预定时间,web前端用到了boostrap的date的一个插件,非常好用,接下来 ...

  8. html+css基础篇

    2016年11月19号,计划把基础在看一下,听大神说好的东西就要多看几遍,知识是学来用的解决问题的,加油 接下来的是我在自学中的小笔记吧,每天都在保持几个小时的学习思考状态,由于要升本所以学前端的时间 ...

  9. react重学

    知识点一:react解析中 return {__html:rawMarkup}; 这里的html前边用的是双下划线(谢谢学妹的指点)

  10. 关于WIN7 家庭版 iis 部署问题

    预装Win7家庭普通版系统的iis部署 必先升级为win7  预计10分钟因个人电脑而异 Win7家庭普通版系统的机器可免费升级为旗舰版.(WIN7任何低版本的系统 都可以升级到旗舰版) 开始的步骤: ...