Ruby 阐述一下关于 Ruby Web Servers 的一些坑爹事实.

Saito · 2012年08月23日 · 最后由 bruceyue 回复于 2013年02月19日 · 7072 次阅读

今天 Hack 了 WEBrick Thin Unicorn 跟 Puma.

Hack 的主要目的是:想知道关于 large request body 各个 server 是怎么处理的。以及我需要在程序中获取 request 的 body.

先说说 WEBrick:

Webrick 会将 body 一股脑的塞到 @env["rack.input"] 里面去。所以等于是没做任何处理。当然作为原生自带 server. 这种假设可以说 能够接受。缺点就是别人塞超大对象。内存会爆。当然小对象可以直接获取到整个 body.

下面说说一些高端的 Server.

高端 Server 对于这方面的处理基本都遵循了一个原则,就是用 tempfile, 如果 body 大过某一个值,就会将对象写入一个临时文件里面去。如果不大于,那就直接写入 @env["rack.input"] .

先看 Thin:

Thin 里有一个的 MAX_BODY 常量 是:

MAX_BODY = 1024 * (80 + 32)

也就是说有 112k.

当 body 大于 112k , 就会生成一个 临时文件继续接收 body.

Thin 的 MAX_BODY 没有可配置的选项,是固定的。

Unicorn

Unicorn 也有一个 MAX_BODY 常量,Unicorn 的 MAX_BODY 是这样的:

MAX_BODY = 1024 * 112

当 body 大于 112k 的时候 也会生成一个临时文件继续接受。

并且有特殊的对象去处理这个 IO. Unicorn::TempIO 并且实现了 read 等方法。

并且 Unicorn 的 MAX_BODY 是可以配置的。

Puma

Puma 内部也有一个 MAX_BODY 的常量。

是这样定义的:

MAX_HEADER = 1024 * (80 + 32)
MAX_BODY = MAX_HEADER

Puma 也用了一个特殊的对象来处理这个 IO . 叫做 Puma::NullIO.

NullIO 把自己伪装成一个 File, 但是 read 方法只会返回 nil 或者 "". 也就是说你在 NullIO 里是拿不到 body 的。

有什么意义

其实是能说明一点,当我想在 程序里通过 @env['rack.input'] 或者 @request.body 获取 body 的时候:

当 body 小于 112k 的时候:

WEBrick Thin Unicorn Puma 都可以完美工作。

当 body 大于 112k 的时候:

就只有 WEBrick 跟 Unicorn 可以工作。

虽然大家都有 read 方法可以用,但是有的能读到,有的读不到的赶脚真的很差!!!

这里有一些 hack 过程的记录,有些文件的位置:https://gist.github.com/3433666

我咋觉得*sgi.input默认就是直接把 HTTP 连接的 socket file 直接映射过去,啥也别做,至于后面咋处理是应用的事情,*SGI Server不该干这件事,因为只有应用才知道这里应该是啥。

而且 CGI 就是直接把输入映射到标准输入的。

其实爆内存有难度的,HTTP Server 默认配置的上传大小是有限制的,实在不行自己设小一点嘛。当然,你不判断就把整个输入先读到内存里就是你的错了...

#2 楼 @bhuztez Python 系是这样的么?那后端岂不是得判断到底是 stream 还是 string. 然后用不同的方法处理。感觉还不如让 server 做了。

#3 楼 @Saito 反正我那个 Server 就是直接 env['wsgi.input'] = socket.makefile('r') 的,跑了几个礼拜也没碰到过问题。其实只有在应用层你才知道这里到底该出现啥。

应用层处理应该更好一些,对于不需要处理的请求,就不浪费时间在把它写到临时文件上面了。

Server 不知道到底是参数还是 stream。而且,文件上传会是个 multipart,你还是需要从里面提取 POST 参数的。在 Server 这层做判断,按你的说法,multipart 既可能是 stream 也可能是 string,等于没判断。再考虑到可以有一些恶意的客户端的存在,你不能根据 header 来区别 body 的内容。

#2 楼 @bhuztez 话说为什么大家的默认大小都是 80 + 32 这个有什么解释么?

#5 楼 @Saito 没发现有哪里定义过啊,我怀疑是互抄的

#2 楼 @bhuztez 作为一个功能完备的 HTTP server 这些都需要吧,即使像静态文件处理,文件压缩什么的实际上都是 web server 来处理的。

#7 楼 @hooopo 作为HTTP Server是需要,但是作为*SGI Server不需要,因为你前面有一个反向代理在干这事,你再干一遍也没意义吧。

#5 楼 @Saito 我猜可能跟 Ruby 1.8.7 之前的一个 io bug 有关。在一些系统上当文件大于 112k 时上传会失败。

#9 楼 @ashchan 有趣的原因。我宁愿相信这个答案。

http://unicorn.bogomips.org/KNOWN_ISSUES.html On Ruby 1.8 prior to Ruby 1.8.7-p248, *BSD platforms have a broken stdio that causes failure for file uploads larger than 112K. Upgrade your version of Ruby or continue using Unicorn 1.x/3.4.x.

#11 楼 @hooopo 亮了,112k 是可以解释了。话说 80 + 32 还是个谜。

不能用 body.read 么?

@request.body.read(@env['CONTENT_LENGTH'].to_i)

#13 楼 @quakewang Puma 的 NullIO 的 read 是这样实现的:

# Mimics IO#read with no data
#
def read(count=nil,buffer=nil)
  (count && count > 0) ? nil : ""
end

Thin 用的是 StringIO. 但是它没往里面写入。

只有 unicorn 实现了真正的 read.

def read(*args)
  @socket ? tee(super) : @tmp.read(*args)
end

#13 楼 @quakewang

@request.body 各个返回是不一样的。

Thin 是 StringIO

unicorn 是 Unicorn::TmpIO

Puma 是 Puma::NullIO

#15 楼 @Saito 我很怀疑 string 是因为历史原因,body 有 read 这一个接口应该就行了

#15 楼 @Saito Thin 我记得是超过上限就进入临时文件: https://github.com/macournoyer/thin/blob/master/lib/thin/request.rb#L150

看上去应该是支持 read 方法的亚,现在没有开发环境,明天我测试看看

#17 楼 @quakewang https://github.com/macournoyer/thin/issues/126 给 Thin 提了一个 issue. 等待有人回复。

读取 raw post data,我现在用 request.body.gets thin/unicorn都OK,用的是rack 1.5

sorry,gets 只是读一行,用 read 可以读取完整的 data

#20 楼 @robbin read 就会碰到我原文里面的问题。

有的 Server 可以读取,有的不行。

#21 楼 @Saito 我 fork 了你的 grack 库,里面记得就是直接 read body 来获取请求的,也有这个问题么?不过 git 的 http header 应该很小就是了。

#22 楼 @ranmocy 如果没记错的话,在 Thin 上是不行的。用 unicorn 部署是没问题的。

我的 grack 主要是在 gitlabhq 里面实现 valid? 方法的时候用到了,主要是为了实现 gitlabhq 的权限控制。

如果没有分支权限问题的话,是不需要获取头的。https://github.com/gitlabhq/gitlabhq/blob/master/lib/gitlab/backend/grack_auth.rb#L76-L85

#23 楼 @Saito 我一直是在 Thin 上跑测试的,目前没有捕获到问题。

我说的其实是这个 read_body

话说 MIT 协议可以进入闭源软件中么?

#24 楼 @ranmocy 嗯,是的。这里的 read 不论什么都不会出错的。

我想起来了,貌似是之前没有 rewind.. MIT 是可以进入闭源软件的.. 基本等同于没限制。

#25 楼 @Saito 有什么区别么?难道是两个 @req 类型不一样的原因?

rewind 是用来重置流的头的是吧?我记得有个通用函数来着……

#26 楼 @ranmocy 嗯,这两个是没区别的。

valid? 的时候先读了一次是因为要 解析 body 体,获取 git 的 push 信息.. 从而判断权限。

server.rb 内部的就不需要做这个 rewind 了。( 读过一次之后,头指针变化之后,重置头指针..

具体是什么还真有点忘记了。貌似是我最后解决了所有 Server 的 Grack 问题... 现在的版本都是没问题的。orz..

#27 楼 @Saito 当时我 hack grack 的时候各种莫名其妙的问题,git push 的时候,hooks 死活拿不到 ENV……我还把 IO.popen 改成了 Open3.popen3……

其实说实话我觉得你的 grack 写的蛮 hack 的,要一直揣摩。虽然让我写的话估计要 dirty 多了……

#28 楼 @ranmocy grack 本身是 Scott Chacon 写的,只不过没有认证体系。是完全开放的。

我把它封装成了一个 gem, 然后开了一个 valid? 的口子。大家就可以根据 valid? 来自己实现了。

揣摩 grack/server 其实还好。知道有那么几个 http 的接口就好了。核心是 upload-pack 跟 receive-pack. 当然这两个接口是 git 最重要的两个接口了。

如何让 Puma 支持 https 呢?

hooopo Rack 的一些坑爹事实 提及了此话题。 04月03日 10:57
需要 登录 后方可回复, 如果你还没有账号请 注册新账号