重构 用 Rails 写 API 服务,性能感觉不足,怎么办?

sefier · 2017年07月25日 · 最后由 zhu_jinlong 回复于 2017年08月06日 · 21726 次阅读
本帖已被管理员设置为精华贴

我有一个 Rails 5 的 Web 项目,同时有三个 API 接口,我就直接写到 rails controller 里面,返回 json 结果了。这个方案最简单,但是随着 api 调用达到 2 万的 rps 之后,性能急剧下降。

我没做仔细评估,考虑到数据库压力,所以首先是改写了接口的实现,全部数据都采取读写 redis 的方式,然后异步更新到数据库中。这个方案实现后,性能确实很大提升,但是速度依然难以接受。我查看 new relic,发现非数据库非 redis 的底层占用时间巨大,是服务器性能不足吗?服务器集群已经加到 150 台了,有点不理智了。

这种情况该怎么办?有没有剥离 api 的好办法?因为毕竟跟 rails web 的基础项目有一定的关联,是不是要换个框架专门写 api,还是换一种语言来做?

谢谢。

1 楼 已删除

2w rps 的话估计也不缺钱了,好好请个狗驾驶重写一遍吧。

默认的 controller 一路会有很多 api 不需要的中间件,比如 cookie 之类的。可以考虑删减掉不必要的中间件。

把 controller 的继承改成 ActionController::API,应该会有可观的提升

你确定是两万 rps?

你现在多少台服务器 (看到了 150 台),量高那个接口单次请求响应时间是多少 ms ? 量高的是写入请求么?

服务器配置是怎样的?什么应用服务器,三个 API 的响应时间,低峰高谷的响应时间

用 elixir 可能会好点?

回复下:有两万多个客户端,采用了愚蠢的一秒一次的轮询方式,所以 RPS 确实是 2 万。客户端那边的逻辑不受我的技术小组控制,暂时不好动,所以用 websocket 或者 tcp 长连接目前都实现不了,所以只能优化我们负责的服务端。

服务器配置是 4 核心 16GB,可以加到 150 台,还可以再加服务器,但我感觉我的方法应该有问题,按理说用不了这么多服务器的。

单次请求目前平均已经慢到 500ms 了,不可接受了,而且跟读写都没关系,直接用的 redis 存储,redis 是 256GB 的集群实例。

我现在考虑的是重写服务端,问题是 ruby 类的框架真的不行吗(比如迁移到 grape,rails-api 或者 sinatra 之类的)。如果不行的话,我考虑迁移到 java 或者 go 框架,这两个框架有没有推荐的?我们的需求很简单的,三个 API,操作 redis,对框架功能丰富度无要求,关键是并发性能高。

感谢各位的建言献策。

sefier 回复

那个接口读取的是什么数据,内容量有多大,既然是多次浪费的轮询,加缓存是否能缓解掉很多不必要的请求(因为很多时候可能都拉到的都是未变动的数据)

同时缓存还可以做几层 Rails 应用里面基于业务增加查询缓存,Nginx 或其他前端 Web 服务器上做缓存,减少请求到 App Server

这么一改动下来,估计大量的请求都被 Nginx 的缓存給扛下来了。

huacnlee 回复

都是纯粹的实时请求,没办法做缓存呢。这个业务蛮特殊的,瓶颈几乎不在 IO 上,因为基本的逻辑都是 redis 逻辑,瓶颈几乎全部在每次的请求上(又或者我的观察不准确,我的业务都是请求到达 => redis 读写 => 请求返回,看了 new relic 数据,redis 时间都在几毫秒之间)。我听说 node.js 和 go 改写 rails 接口的例子,都有 20 倍提升之类的,不知道是不是该换掉?感谢解答。

如果业务逻辑基本都在 Redis 那就没必要用 Rails… Rails 挂了太多不必要的东西,直接用个裸 ruby 都够了…速度绝对嗖嗖的

用的什么 web server?puma 还是 unicorn?这个不说清楚,光看框架一点用都没。另外看看 CPU 跑满了没,没跑满的话,应该还有优化空间

放弃 rails 用 go 重写~

如果业务逻辑还不是很复杂,要不要试一试这个 😀https://github.com/goonr/go-on-rails

sefier 回复

你测试下单台服务器的性能 或者你负载均衡处理有点问题,150 台服务器是不是都在处理请求!!! 是在不行就换成 2w 台 一台对应一个客户端!!这样的话你估计要和你领导一起跑路啦!!以上纯属开玩笑。

hrz3424 回复

觉得这个帖子很有可能会变得很有营养,暂时先别带歪了。

adamshen 回复

我也是挺好奇的,这个 150 台服务器是怎么配置的!!

  • 150 台机器撑两万的 QPS, 平均单台 QPS 133
  • 机器配置是 4 核心 16 GB
  • 接口内部只读写 Redis, 无其他 IO, 接口响应时间超过 500 ms

我觉得这是个钓鱼贴... 或者你们需要请我,😅

招人吗?我就冲着这 150 台服务器来的

改客户端 用 websocket 或其他长链接方案 应该就不用 150 台吧

42thcoder 回复

1)我想知道这个能钓到什么样的鱼?

2)我单台开的是 4 进程,16 个线程的 puma,按响应速度 100ms 计算,RPS 也就 160,单台 133 的 RPS 在你看来很可笑?

3)我的 2 万并发,操作的是同一组数据,我是用 redis 队列分配的,生产者消费者模型,也就是 2 万个客户端同时进行读写,类似于秒杀,还带动态库存的。这里面没有任何缓存可用,也没有任何静态内容可用。

4)我感觉阁下应该没做过这么高并发的应用吧,不是说什么东西都可以水平扩展的。

不应该吧,这个用 rails 也不应该是这效果。应该有别的问题。

sefier 回复

哈哈,没想到是好朋友的朋友,看错啦~

你的问题有太多可能了,可以邮件交流下,42thcoder # gmail.com

既然有在用 Newrelic,那么:

  1. 在 Newrelic 里看 Transactions -> Slowest Average Response Time -> 右边线图里点进去看具体的 transaction trace,看瓶颈具体在哪里
  2. 如果是 Rails 本身太慢的话,去除 Rails —— 我如果要新写一个 API 服务的话,基本不太会选 Rails 或 Rails API,用纯 Ruby 写的话 overhead 小很多
  3. 如果没地方能优化了的话... 来弃暗投明加入 Elixir 的阵营吧(安利模式开启)😏
sefier 回复

我估计你卡在 Redis 的锁了,Redis 默认没有加连接池,为了线程安全,几乎所有的地方都会用锁 https://github.com/redis/redis-rb/blob/master/lib/redis.rb#L93 特别是像你这种并发这么高的情况下,线程极度繁忙,锁个几百毫秒应该很正常

用这个吧 https://github.com/mperham/connection_pool pool size 设成线程数量就好

纯 API,要不是特别依赖 rails 或者 ruby,考虑一下 OpenResty,配合 redis,性能不会是问题

fxg 回复

看了下 OpenResty,好像还真是有戏,我仔细研究下看行不行。确实依赖于现有的管理平台,也就是 Rails 框架,但是如果性能差异很大,多做一些剥离 API 的工作还是值得的。我就是太追求系统的统一性,导致这个压力越来越不可控了。

sefier 回复

聊技术嘛,就心平气和地聊,不要带情绪啦~ 也是我不该调侃的,你碰到这种问题肯定也着急,sorry~

回复下你的问题:

单台开的是 4 进程,16 个线程的 puma,按响应速度 100ms 计算,RPS 也就 160

按你给出的指标,TPS / RPS / QPS, 不管用哪个词儿了,应该更高吧

我的 2 万并发,操作的是同一组数据,我是用 redis 队列分配的,生产者消费者模型,也就是 2 万个客户端同时进行读写,类似于秒杀,还带动态库存的。这里面没有任何缓存可用,也没有任何静态内容可用。

这个可以展开说说,有伪代码就更好了,方便大家帮你找问题。#10 跟 #21 都说得有点模糊。

我感觉阁下应该没做过这么高并发的应用吧,不是说什么东西都可以水平扩展的。

确实没做过 2w QPS 的应用,也就做过单机三五百 QPS 吧,😅 . 如果真碰到这种场景 (2w QPS), 我会尽量在设计阶段规避。

多说两句

  • 在这种高并发的情况下,New Relic 的 Transaction Trace 非常不准,远不如在本地做 benchmark
  • 立个 flag, 没必要换语言

能否把 newrelic 性能部分的图形贴上来。 直觉感觉瓶颈在 redis 上,而不是在 rails 上,你加再多的应用服务器没有用。

另外,你一共就三个 api,业务逻辑简单,直接读取 redis,又是 rails5,可以无痛迁到 rails-api 试一下效果,没有任何成本。

hooooopo 回复

炮哥分享下高并发经验呗,太期待了

newrelic 在生产环境监控效果比本地 bm 效果好很多,可以涵盖各种环境因素。

监控效果确实挺好的,看趋势,看平均响应时间都很好,但是具体到某个特定慢请求的 Transaction Trace 就很不准了,各种环境因素太多了,请求之间也相互影响,NewRelic 的埋点机制感觉也不是百分百靠谱

解决思路还是着重两点:

  1. 优化单次请求时间
  2. 扩大并发数

感觉优化 1 的空间比较大,可以压测,给数据,试试不同的框架,语言等,首先应该把每台机器的性能压到极致,系统参数也要适当调优。

btw: 没有压测,没有火焰图,怎么谈优化呢

Redis 没加 connection pool 的话,肯定有问题啊,其他啥都不用看,先把这个改了看看再说。而且楼主说了 "非数据库非 redis 的底层占用时间巨大",应该就是 "Ruby" 的时间,这个很大概率就是锁了

能达到那个级别了,直接转 Java go,除非创业公司一分钱都没有

一个请求要 500ms 就和 rails 框架没有关系了,因为正常的 rails 返回也是几十 ms

可以在一两台机器上采样看下时间消耗在哪个方法上。

如果在 redis(看你程序的流程好像也没有别的地方可以出问题了)

你如果是用云的 redis 集群,他上面应该有监控,看下负载情况如何

redis 的 slowlog 里面都是什么?(slowlog 没有开开一下,2w rqs, 0.1ms 对你来说都是慢查询了,这还没有计算你一个请求有多个 redis command)

比较好解决的情况就是有人误用 keys 这样的操作,去掉就好了,正常就不应该这么用

这问题可能一眼就能看出来

比较麻烦的是 rang 这种 O(S+N) 的命令,在 slowlog 里比较难找出来

这种就考虑尽量改成 O(1) get set

建议使用 datadog 这种 statsd 采样一下每个命令的执行时间

39 楼 已删除
41 楼 已删除

好的,我先检查业务逻辑,先优化这里然后再考虑语言与框架的问题。

简单算一下 2w qps 分到 150 台机器上 也就是单机 133 qps。怎么可能怪在 ruby / rails 头上?

再分析一下其他的系统瓶颈吧

压一压 redis 集群的带宽、qps。检查下 redis 指令 什么的

又或者你们在 redis 层是不是有啥同步机制呢?

如果楼主不能提供更多信息 基本是解答不了了

😏 我只能说你们的前端一定很厉害。。。比 150 台服务器还值钱。。。

…………换成 websocket 一台机器就能解决了……

日薪一万五那位我觉得就很靠谱,(至少开价高🤐

不优化读写逻辑肯定不行,同一组数据每秒两万读写,瓶颈可能出现在网络延迟上

kabie 回复

这个也许不一定。还是要看项目的具体瓶颈。我司一个项目用 go 语言版的 socket io(其实和 socket io 没什么关系,只是前端兼容了 socket io 框架),16g 内存 8 核,后端用的 MongoDB 现在撑 2w 个连接 妥妥的,我觉得可以撑到 5w 连接,不过后面瓶颈就是 MongoDB 的并发了

quakewang 回复

请他!

我默默的提升了下我的 hour rate。

很有营养的话题,希望能看到最后的解决方案。 我一直以来处理并发都是堆服务器,反正服务器便宜,最便宜的几十块一个月,曾经一个项目堆到 2000 台阿里云最低配服务器,活动期结束,服务器释放掉,也没花多少钱,感觉非常划算。

rfei 回复

会不会不太环保 😂

我们小公司,也没技术大牛,但有时候活动参数人数能引爆到百万级,我唯一能想到的办法就是堆服务器了。负载均衡,数据库,对象存储,redis,消息队列都用阿里云的,磕磕绊绊几年,问题最后都能完美解决。

57 楼 已删除
rfei 回复

哇。两千台服务器用什么工具管理的?

jasl 将本帖设为了精华贴。 07月28日 13:21

设置个精华,方便更多人看到进来讨论吧

根据各位的建议,我准备先将 redis 的连接池加上,然后再看目前的性能表现,谢谢。

sefier 回复

我查看 new relic,发现非数据库非 redis 的底层占用时间巨大

不是非 redis 嘛?估计是因为 newrelic 没有把 redis 和 ruby 分开,把 redis 也算在 ruby 里了:https://ruby-china.org/topics/20219#Instrumentation%20Redis

hooopo 回复

好的,多谢,我加上看看。

hooopo 回复

加连接池不是为了解决 Redis 慢的问题,而是为了解决竞争的问题吧。Newrelic 比较新的版本,Redis 应该不会把太多 Ruby 的时间算进去

tony612 回复

不是的,连接池解决的是 app 和 db 直接创建连接的开销问题。带来额外的问题是,如果池里每个连接都被占用,其他线程就会等待。

newrelic agent 3.13.0 以上不需要安装了:https://docs.newrelic.com/docs/agents/ruby-agent/frameworks/redis-instrumentation#third-party-gems

hooopo 回复

我说的是他这个场景下的问题。那也比 Redis 不加连接池,一上来就等着好吧,而且连接池比线程数多就可以规避这个问题了。

67 楼 已删除

楼主可以尝试下:1.转换成 api 模式,去掉不必要的中间件,2.redis 加上连接池,3.使用物理服务器压测下,看看真实的数据,感觉楼主的服务器应该都是云服务器,云服务器同等配置下在大并发下的表现是不如真实服务器的,4.使用下类似 oneAPM 之类的工具在线看看是哪条语句拖时间,5.把代码拉出来一个方法一个方法作分析,然后优化,以上都做了那么可以考虑做分布式的负载均衡,拆掉 redis 的集群,和应用跑同一台服务器最大化的减小网络带来的延时,然后前端 Nginx 做好分发工作,参考下现在 git.oschina.net 使用的分布式方案。

我看了半天也没找到有价值的信息,以后这种问题是不是可以先看看性能埋点的分析? 另外,这句话有点用——

我的2万并发,操作的是同一组数据,我是用redis队列分配的

如果楼主有兴趣,不妨解释一下这是什么意思?性能瓶颈的问题绝大多数就是某个点出现了(广义的)锁,导致无用的等待,所以楼主说的越详细越有用

当然,楼上几位说得很对,这个问题中,语言和框架肯定不重要,单机 1000 以下并发、rt 时间超过 100ms 的业务基本不用怀疑到这些地方,虽然这种怀疑很廉价

现在不是 serverless 挺流行的吗?上 aws lambda 如何。

sefier 回复

既然是轮询模式,猜测基本是几个单一的 api 进行轮询,需要分析数据多久更新一次,对数据实时更新的需求;如果数据更新没有那么频繁,可以考虑在 rails 前面加一个 go 写的代理进行缓存,然后再建立合适的缓存失效机制; 这样能够大大缓解 rails 端的压力,同时 go 的多并发能力应该能很好的处理客户端的大量轮询。

其实写入那么频繁,对于返回值的正确性要求没有那么高吧,完全可以写入后马上返回。一点猜测。

@rfei 不扶墙只服你,能解决问题就是好办法

fsword 回复

我看了半天也没找到有价值的信息

同意这句话,要解决性能问题,首先要做的就是找到性能瓶颈,然后针对瓶颈的地方进行分析,看看这个瓶颈的问题是否可以解决,而不是在这里猜测是不是没有用连接池?还是 rails 性能差?或者服务器的问题等等。找瓶颈的思路也不难,对于程序级别就是埋点分析每一步的执行时间,看看哪一步占的时间长。

请大家研究性能问题之前一定好好的读几遍性能分析的圣经级别的论文《Thinking Clearly about Performance》,由 Oracle 大牛 Cary Millsap 在 10 年左右写的通俗易懂的关于分析性能问题思路的。相信我,看完这论文之后肯定对于性能分析有一个更清晰的思路和认知。

各类工具的性能分析都能看到我楼上说的这篇论文中提到的解决问题的思路,Oracle 的自带的性能分析工具自然不必说;Openresty 作者经常会发的火焰图也是一个例子;Percona Toolkit 里面的pt-query-digest出来的结果也是这种思路;甚至于最简单的strace工具加上-c的参数也会做一个花费时间从高到低的统计结果给你。大家可以先看下上面提到的论文,然后在看下我上面提到的这几个工具,自己去试用一下,然后你就知道我在说什么了。

下面给个strace -cp <pid>做 nginx 分析的输出给大家感受下:

~# strace -cp 1926
Process 1926 attached - interrupt to quit
^CProcess 1926 detached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 89.10    0.000711           0      2989           epoll_wait
  5.89    0.000047           0       802           close
  2.63    0.000021           0       552           writev
  1.38    0.000011           0      3014      1305 read
  1.00    0.000008           0      2203           epoll_ctl
  0.00    0.000000           0      2036           write
  0.00    0.000000           0       100         2 open
  0.00    0.000000           0         6           stat
  0.00    0.000000           0        98           fstat
  0.00    0.000000           0         3           mmap
  0.00    0.000000           0         3           munmap
  0.00    0.000000           0       394           ioctl
  0.00    0.000000           0        65           pread
  0.00    0.000000           0       414           readv
  0.00    0.000000           0        12           sendfile
  0.00    0.000000           0       394           socket
  0.00    0.000000           0       394       394 connect
  0.00    0.000000           0       829        21 recvfrom
  0.00    0.000000           0         1           shutdown
  0.00    0.000000           0       272           setsockopt
  0.00    0.000000           0       397           getsockopt
  0.00    0.000000           0      2989           gettimeofday
  0.00    0.000000           0         1         1 futex
  0.00    0.000000           0       298           accept4
------ ----------- ----------- --------- --------- ----------------
100.00    0.000798                 18266      1723 total

@sefier 特别想知道你的业务模型到底是什么样子的,读写分布是什么样样子的,是 io 密集型还是 cpu 密集型。可以交流下,我这面压力和你差不多大。

如果业务逻辑简单的话,OpenResty 的收益很好。

ruby 做不到 nodejs 的 3-4k 的请求量,优化一下起码也有 1k,用不到 150 太。10-20 台应该可以。

1,重构客户端论询方式。 2,用 java 或 erlang/elixr 重构

跟楼上很多人一样,我看完所有回复没有看性能瓶颈到底在哪里。。。不过就楼主对于 qps 和机器数量的描述来说,问题在语言上的可能性较小。 @fsword 说的卡在某个锁的可能性大

不用换个语言,直接上 jruby,用 vert.x 做,性能包你满意

jimrokliu 回复

可以,用 vert.x,吊打 node 没啥问题,我们用 ruby 都是一万多的并发量,轻轻松松,只不过是 jruby

看看是不是 同一个 key 的异步写 Redis 让大量 Redis 读被挂起了导致的 Redis 时间很长。

真的很想知道是哪个公司一个 150 台服务器的项目在上线后遇到了这种匪夷所思需要在线和网友讨论解决/profile 方案的?

sefier 关闭了讨论。 08月06日 17:59
需要 登录 后方可回复, 如果你还没有账号请 注册新账号