Ruby 服务器性能讨论...

luikore · 2013年05月27日 · 最后由 zw963 回复于 2013年08月26日 · 9531 次阅读
本帖已被管理员设置为精华贴

先看一个最简单的超高性能 hello world http 服务器:

require "socket"

s = TCPServer.new '127.0.0.1', 3000
s.listen 10000

loop do
  io = s.accept
  content = 'hello world'
  io << "HTTP/1.1 200 OK
Content-Type: text/html; utf-8
Connection: close
Content-Length: #{content.size}

#{content}"
  io.close
end

我的机器上占用内存十多兆,ab 测下大约 10000 request/s, 和 C 写的差别不大,完爆各种 goroutine STM async***... c10k 解决了!... 那到底是什么东西拖慢了服务器的响应呢?

问题 1: 请求解析和响应组装

五花八门的应用服务器做的事情,说白了就是用来解析请求 header 的... rack 提供了这么多工具这么多层,说白了就是用来组装响应 header 的...

如果加上 ruby 写的 http 请求解析,直接就掉到 1000 req/s 的层次去了。所以这部分还是倾向于用 C 去做。这里有两个选择,一个是 thin / mongrel 用的基于状态机的 http parser, 另一个是 eventmachine / nodejs 用的基于事件的 http parser, 不管选哪个,都能比较好的改良性能,还能保持 5000 request/s 左右。

一般模板也有 50000/s 以上的渲染速度,对组装 response 的影响不大...

问题 2: 数据库和外部 service 调用

外部调用延迟都比较大,为了不让服务器被单请求阻塞,需要一个 multiplexing 方案,我们这些一辈子都碰不上 c10k 问题的屌丝,多线程甚至一个 fork 就够了...

改良版 hello world 服务器,unicorn 架构 ...

require "socket"

s = TCPServer.new '127.0.0.1', 3000
s.listen 10000

6.times do
  fork {
    loop do
      io = s.accept
      content = 'hello world'
      io << "HTTP/1.1 200 OK
Content-Type: text/html; utf-8
Connection: close
Content-Length: #{content.size}

#{content}"
      io.close
    end
  }
end
Process.waitall

10 几年前 aio_ 系列的函数一度被推崇,但现在大家都在踩 aio, 推 epoll / kqueue 了... 但是 kqueue 不能 share 给子进程,epoll 每次只取出 1 个事件,都有它们自己的问题,事件框架包装后又都显得比较慢...

另外顺带提下 prefork eventmachine 进程的简单解决方案: https://gist.github.com/rkh/1102809

问题 3: 框架和中间件

rails dispatch 很慢,解决方案就是页面静态化和 etag rails helper 很慢,解决方案就是页面片段缓存 active record 很慢,解决方案就是对象缓存 devise 很慢,解决方案就是不用 devise ...

毕竟加点缓存有 20 req/s 就已经能撑起巨大流量的应用了...

问题 4+: 操作系统,网络,浏览器,协议等等非应用服务器的因素...

igvita 刚整的 slides, 比这个帖子有价值多了... http://www.igvita.com/slides/2013/fluent-perfcourse.pdf

还有这本新书,趁免费赶紧看呀! http://chimera.labs.oreilly.com/books/1230000000545/index.html

  • linux 内核 3.2+ 有对付丢包的新算法 PRR (Proportional Rate Reduction)
  • linux 内核 3.5+ 用了 CoDel 算法可以降低 TCP 连接的延迟。
  • linux 内核 3.7+ 用了 TFO (TCP fast open, 在 SYN 里附带数据) 可以降低 TCP 握手的影响。
  • nginx 开了 SPDY / HTTP2.0 可以并发发送 header 和 data 降低延迟。
  • congression window
  • sysctl net.ipv4.tcp_window_scaling.
  • SSR sysctl net.ipv4.tcp_slow_start_after_idle
  • BDP (Bandwidth-delay product)
  • ss --options --extended --memory --processes --inf
  • 大韩民国光纤到户,带宽世界第一... ...

好文,深入潜出,但是感觉好像有点意犹未尽,貌似没有写完就收尾了?

问题 2 中的代码例子,跟 aio_ 系列函数,以及 epoll / kqueue 的关联?还有 prefork eventmachine 的性能情况如何?

确实啊,刚刚要到高潮的地方就没了

java 的 aio 不就是通过 epoll 实现的吗,难道其实是两码事?

吕核心这是连载么?前排占座,广告位招租。

我的机器上占用内存十多兆,和 C 写的差别不大

差别真不大...

... 随便写了点东西,主要集中在 hello world... 意犹未尽是因为展开就是各种大坑啊...

#3 楼 @goinaction aio 和 epoll 不是一回事,aio 是通过 callback 的方式做 read / write, 要用新开线程的,好处是对应函数在 posix 标准里。epoll 和 kqueue 是不开新线程,就没有 aio 复制栈/同步/切换 CPU 的开销。

我们知道 unix 的一大特色就是什么都是文件,文件描述符 (fd) 可以用一个整数来表示打开的文件,socket, 管道等等东西。进程中的文件描述符 fork 以后还能用,例如 working with unix process 里有用 ruby 启动 python 进程然后在 python 中用 ruby 打开的 fd 的例子。

block 的意思是在等待 read / write / accept 这些 io 操作的时候,进程是 sleep 的。如果用 nonblock 的文件描述符:fcntl(sock, F_SETFL, O_NONBLOCK), 你就可以:A. 写个无限循环去不断的问"是不是好了", 这样就会把 CPU 吃到 100% 还不如用 block 操作,B. 做别的事情,但可以做什么就需要一个调度机制,现在可以选择的有线程调度或者事件队列调度,线程调度就是 aio, 事件队列调度就是 epoll / kqueue.

附带说一下 ruby 的 io 的神奇的地方:ruby 的 io 正常来说是阻塞整个进程的,但一开新线程,它就自动变成非阻塞了...

#4 楼 @krazy 没用过,看它的实现应该是 libev 的包装?现在的事件框架太多了吧 libev, libevent, gevent, libuv ...

#6 楼 @bhuztez 那是几万个请求后的... 打开 7M 左右

#8 楼 @luikore 你说了,我觉得更垃圾了。即便打开 7M,几万个请求之后,8M 内总能控制住的吧...

#9 楼 @bhuztez 内存还这么多,不用白不用,没必要着急回收白白浪费时间吧... 我还想 GC.stop 然后 n 个请求后直接杀进程呢...

#10 楼 @luikore 一个 Hello, world 进程就要 10+M,一个跑应用的 PHP 进程也不过如此

#11 楼 @bhuztez 哦,你可以试试 java 的 hello world, 这么多内存不用好浪费啊...

#7 楼 @luikore gevent 和 libuv 都是用 libev 的其实

#7 楼 @luikore 短短的时间,10 人喜欢,说明需要你继续写下去,把坑挖了让大家好跳进去呀。

#13 楼 @reus 好吧看来 libev 值得一看。就怕 c++ 代码略多... 最近看了 eventmachine 觉得它写这么多 c++ 效率会很低... 因为:A: c++ 自动生成栈上对象的析构调用,如果在 c++ 里调用 ruby 但没用 rb_protect 包起来,ruby 的长跳转/异常就会飞到外面而跳过析构函数造成泄漏 (不过粗看去 eventmachine 好像基本都包了... 没包的地方也基本没什么好析构的). B: 栈上的 struct 会降低保守式 gc 的效率,有 patch 搞定修过 eventmachine 上这样的问题。

#14 楼 @lgn21st prefork em 略有提升但不明显,fork 3 个后简单的 hello world 有 +50% 左右吧,带数据库访问的没测... 但如果数据库客户端有自己的线程池能跳出 GVL , 单进程 em 基本够使了...

#13 楼 @reus libuv 早已经移除了对 libev 的依赖。

#15 楼 @luikore redis 用了几百行就实现了个事件库:https://github.com/antirez/redis/blob/unstable/src/ae.c 这个也值得一看

#18 楼 @skandhas 哦,之前在非 win 下还是包装 libev 的…

#20 楼 @reus 嗯 以前是用 libev 的。现在不用了。

伟大啊。每次都能耐心地给我们出好文。 没想到 ruby 光跑 http 也那么快 以前我自己光跑 eventmachine 的时候很快 10000req/s 后面发现解析 header 的字符串处理才是关键性能问。还看到有个人写了一个 C 的 header 处理的代码。

epoll 省内存这样说起来也没啥用了

好比让我取舍

进程消耗 1G 内存,但是很快,很容易些。 (容易,但是能解决问题,成本能接受) 进程消耗 100M,很快,但是不是很容易。 (不容易,但是解决问题,成本很低)

#15 楼 @luikore 作者有个分支,说要改写成 C。

精华帖干嘛放到“瞎扯淡”里呢

#24 楼 @hlxwell 我帮吕哥把帖子挪到 Ruby 版去了。

#22 楼 @hlxwell 这个阻塞的 hello world, 一调数据库,单个请求需要 100+ms 速度就掉下去了,但是 eventmachine 调数据库就不阻塞...

期待改成 C, 里面的 throw std::runtime_error(...) 看得我瞩目惊心... EM 里有几个新加方法忘记捕获异常,给个失效 fd 什么都没说就进程终止了...

#25 楼 @lgn21st 真是尽心尽责啊。顶@lgn21st

#26 楼 @luikore 给我们讲讲 C 与 C++ 的实现最大的区别吧。像我们这种小鸟都不理解啊,虽然也不关心。但是听到 lv 头头是道的解释我们又能出去忽悠了。 赐教啊赐教啊。

#10 楼 @luikore 你说的 GC.stop 然后 n 个请求后中止进程或许是个好办法! 这样请求期间就不会有突然 GC 了,如果前后端分离机器的,前端分发的时候弄个阈值,比如 100 个请求后,暂停向该 app server 分发。app server 自己也检测达到 100 请求后自动重启进程。

#28 楼 @hlxwell ... 见 15 楼...

C++ 的坑之一是 calling convention. calling convention 是类似于协议的东西,函数是怎么调用的,被调用方是怎么获取参数的,参数放在哪个寄存器,是顺序还是逆序推到栈上都有约定。编译器往往有扩展,可以用 __cdecl, __fastcall 等方式去指定一个函数的 calling convention.

C 没有函数重载,C++ 有函数重载,所以 C++ 不能用 C 的 calling convention 而是自己另外弄了一套。这就造成两个东西一模一样但是编译出来的东西却不一样... 下面是一个最简单的 ruby ext, 如果保存为 .c 就可以,保存成 .cxx 或者 .cpp 就编译不过去

#include <ruby.h>
VALUE helloworld(VALUE self) {
  printf("hello world\n");
  return Qnil;
}
void Init_helloworld() {
  rb_define_method(rb_cObject, "helloworld", helloworld, 0);
}

如果在 C++ 里,就要声明这个函数是 C call:

...
extern "C" VALUE helloworld(VALUE self) {
...

或者用一个宏转换类型:

rb_define_method(rb_cObject, "helloworld", RUBY_METHOD_FUNC(helloworld), 0);

坑之二是自动生成析构函数调用,例如

{
  std::vector<int> vec; // 一个 vector 对象, 会动态分配内存的
  ...
  // 这里会在编译期自动生成 vec 的析构函数调用, 把 vec 动态分配的内存释放掉
}

如果在中间插入了 ruby 方的调用,会出现一个跳出去的 long jump

{
  std::vector<int> vec;
  rb_eval_string("raise 'hello world'"); // 跳出去了, 下面的代码包括自动析构也不会执行了
  ...
}

所以要用 rb_protect 之类的包装起来

{
  std::vector<int> vec;
  int state;
  rb_eval_string_protect("raise 'hello world'", &state);
  if (state) ...
}

坑之三是异常,ruby 出现严重错误还会列出一个 segfault 的报告,报告中会包含当前的调用栈可以帮助排除问题。但如果在代码中扔一个 C++ 异常但忘记捕获

throw std::runtime_error("hello world");

程序就会报一个 "libc++ abi error" 退出,其它什么都不说了... 除非用 ulimit -c unlimited 再用 gdb / lldb 打开 image, 否则是看不到异常栈的...


坑之四是 std::iterator 等模板对象没法在 gdb / lldb 中查看,因为 C++ template 是编译期的,debugger 在运行时不能知道最终生成的是什么...

for (auto i = vec.begin(); i != vec.end(); i++) {
  // 你可以在这里插入一个断点, 但你看不到 i 是什么, 也不能对 i 做运算...
}

但往往这些坑必须去踩... C 并没有标准的数据结构/容器可以用,而 C++ 的标准库有各种实现,经常不得不去用,要么就是自己造一套或者是用一些让你不太放心的 ...


什么语言的语法最复杂变态?不是 C++, 是 Objective-C++ ...

坑之五... 就是 Objective-C++ 各种诱惑你去用...

Objective-C

id a = @"hello world";
id b = 3; // :(

Objective-C++

auto a = @"hello world";
auto b = 3; // (:

好多坑阿...

#32 楼 @luikore 看来怨恨不是三言两语啊。看来并不是高效不高效了,全部都是一些坑才让你这么恨之入骨啊。难怪从来没听谈起 C++。 你还写过 Objective-c++ 这个我真没怎么了解过。看到别人用,就马上跳过了。

从头一字不拉的看到尾,很有料呀,似乎明白了很多。

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