Ruby 服务器性能讨论...

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

先看一个最简单的超高性能 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++ 这个我真没怎么了解过。看到别人用,就马上跳过了。

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

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