Ruby Web 服务器的并发模型与性能

2019 年 12 月 05 日

Ruby Web 服务器的并发模型与性能


这是整个 Rack 系列文章的最后一篇了,在之前其实也尝试写过很多系列文章,但是到最后都因为各种原因放弃了,最近由于自己对 Ruby 的 webserver 非常感兴趣,所以看了下社区中常见 webserver 的实现原理,包括 WEBrick、Thin、Unicorn 和 Puma,虽然在 Ruby 社区中也有一些其他的 webserver 有着比较优异的性能,但是在这有限的文章中也没有办法全都介绍一遍。



在这篇文章中,作者想对 Ruby 社区中不同 webserver 的实现原理和并发模型进行简单的介绍,总结一下前面几篇文章中的内容。


文中所有的压力测试都是在内存 16GB、8 CPU、2.6 GHz Intel Core i7 的 macOS 上运行的,如果你想要复现这里的测试可能不会得到完全相同的结果。


WEBrick


WEBrick 是 Ruby 社区中非常古老的 Web 服务器,从 2000 年到现在已经有了将近 20 年的历史了,虽然 WEBrick 有着非常多的问题,但是迄今为止 WEBrick 也是开发环境中最常用的 Ruby 服务器;它使用了最为简单、直接的并发模型,运行一个 WEBrick 服务器只会在后台启动一个进程,默认监听来自 9292 端口的请求。



当 WEBrick 通过 .select 方法监听到来自客户端的请求之后,会为每一个请求创建一个单独 Thread 并在新的线程中处理 HTTP 请求。


Ruby


run Proc.new { |env| ['200', {'Content-Type' => 'text/plain'}, ['get rack\'d']] }
复制代码


如果我们如果创建一个最简单的 Rack 应用,直接返回所有的 HTTP 响应,那么使用下面的命令对 WEBrick 的服务器进行测试会得到如下的结果:


Concurrency Level:      100Time taken for tests:   22.519 secondsComplete requests:      10000Failed requests:        0Total transferred:      2160000 bytesHTML transferred:       200000 bytesRequests per second:    444.07 [#/sec] (mean)Time per request:       225.189 [ms] (mean)Time per request:       2.252 [ms] (mean, across all concurrent requests)Transfer rate:          93.67 [Kbytes/sec] received
复制代码


在处理 ApacheBench 发出的 10000 个 HTTP 请求时,WEBrick 对于每个请求平均消耗了 225.189ms,每秒处理了 444.07 个请求;除此之外,在处理请求的过程中 WEBrick 进程的 CPU 占用率很快达到了 100%,通过这个测试我们就可以看出为什么不应该在生产环境中使用 WEBrick 作为 Ruby 的应用服务器,在业务逻辑和代码更加复杂的情况下,WEBrick 的性能想必也不会达到期望。


Thin


在 2006 和 2007 两年,Ruby 社区中发布了两个至今都非常重要的开源项目,其中一个是 Mongrel,它提供了标准的 HTTP 接口,同时多语言的支持也使得 Mongrel 在当时非常流行,另一个项目就是 Rack 了,它在 Web 应用和 Web 服务器之间建立了一套统一的 标准,规定了两者的协作方式,所有的应用只要遵循 Rack 协议就能够随时替换底层的应用服务器。



随后,在 2009 年出现的 Thin 就站在了巨人的肩膀上,同时遵循了 Rack 协议并使用了 Mongrel 中的解析器,而它也是 Ruby 社区中第一个使用 Reactor 模型的 Web 服务器。



Thin 使用 Reactor 模型处理客户端的 HTTP 请求,每一个请求都会交由 EventMachine,通过内部对事件的分发,最终执行相应的回调,这种事件驱动的 IO 模型与 node.js 非常相似,使用单进程单线程的并发模型却能够快速处理 HTTP 请求;在这里,我们仍然使用 ApacheBench 以及同样的负载对 Thin 的性能进行简单的测试。


Concurrency Level:      100Time taken for tests:   4.221 secondsComplete requests:      10000Failed requests:        0Total transferred:      880000 bytesHTML transferred:       100000 bytesRequests per second:    2368.90 [#/sec] (mean)Time per request:       42.214 [ms] (mean)Time per request:       0.422 [ms] (mean, across all concurrent requests)Transfer rate:          203.58 [Kbytes/sec] received
复制代码


对于一个相同的 HTTP 请求,Thin 的吞吐量大约是 WEBrick 的四倍,每秒能够处理 2368.90 个请求,同时处理的速度也大幅降低到了 42.214ms;在压力测试的过程中虽然 CPU 占用率有所上升但是在处理的过程中完全没有超过 90%,可以说 Thin 的性能碾压了 WEBrick,这可能也是开发者都不会在生产环境中使用 WEBrick 的最重要原因。


但是同样作为单进程运行的 Thin,由于没有 master 进程的存在,哪怕当前进程由于各种各样奇怪的原因被操作系统杀掉,我们也不会收到任何的通知,只能手动重启应用服务器。


Unicorn


与 Thin 同年发布的 Unicorn 虽然也是 Mongrel 项目的一个 fork,但是使用了完全不同的并发模型,每 Unicorn 内部通过多次 fork 创建多个 worker 进程,所有的 worker 进程也都由一个 master 进程管理和控制:



由于 master 进程的存在,当 worker 进程被意外杀掉后会被 master 进程重启,能够保证持续对外界提供服务,多个进程的 worker 也能够很好地压榨多核 CPU 的性能,尽可能地提高请求的处理速度。



一组由 master 管理的 Unicorn worker 会监听绑定的两个 Socket,所有来自客户端的请求都会通过操作系统内部的负载均衡进行调度,将请求分配到不同的 worker 进程上进行处理。


不过由于 Unicorn 虽然使用了多进程的并发模型,但是每个 worker 进程在处理请求时都是用了阻塞 I/O 的方式,所以如果客户端非常慢就会大大影响 Unicorn 的性能,不过这个问题就可以通过反向代理来 nginx 解决。



在配置 Unicorn 的 worker 数时,为了最大化的利用 CPU 资源,往往会将进程数设置为 CPU 的数量,同样我们使用 ApacheBench 以及相同的负载测试一个使用 8 核 CPU 的 Unicorn 服务的处理效率:


Concurrency Level:      100Time taken for tests:   2.401 secondsComplete requests:      10000Failed requests:        0Total transferred:      1110000 bytesHTML transferred:       100000 bytesRequests per second:    4164.31 [#/sec] (mean)Time per request:       24.014 [ms] (mean)Time per request:       0.240 [ms] (mean, across all concurrent requests)Transfer rate:          451.41 [Kbytes/sec] received
复制代码


经过简单的压力测试,当前的一组 Unicorn 服务每秒能够处理 4000 多个请求,每个请求也只消耗了 24ms 的时间,比起使用单进程的 Thin 确实有着比较多的提升,但是并没有数量级的差距。


除此之外,Unicorn 由于其多进程的实现方式会占用大量的内存,在并行的处理大量请求时你可以看到内存的使用量有比较明显的上升。


Puma


距离 Ruby 社区的第一个 webserver WEBrick 发布的 11 年之后的 2011 年,Puma 正式发布了,它与 Thin 和 Unicorn 一样都从 Mongrel 中继承了 HTTP 协议的解析器,不仅如此它还基于 Rack 协议重新对底层进行了实现。



与 Unicorn 不同的是,Puma 是用了多进程加多线程模型,它可以同时在 fork 出来的多个 worker 中创建多个线程来处理请求;不仅如此 Puma 还实现了用于提高并发速度的 Reactor 模块和线程池能够在提升吞吐量的同时,降低内存的消耗。



但是由于 MRI 的存在,往往都需要使用 JRuby 才能最大化 Puma 服务器的性能,但是即便如此,使用 MRI 的 Puma 的吞吐量也能够轻松达到 Unicorn 的两倍。


Concurrency Level:      100Time taken for tests:   1.057 secondsComplete requests:      10000Failed requests:        0Total transferred:      750000 bytesHTML transferred:       100000 bytesRequests per second:    9458.08 [#/sec] (mean)Time per request:       10.573 [ms] (mean)Time per request:       0.106 [ms] (mean, across all concurrent requests)Transfer rate:          692.73 [Kbytes/sec] received
复制代码


在这里我们创建了 8 个 Puma 的 worker,每个 worker 中都包含 16~32 个用于处理用户请求的线程,每秒中处理的请求数接近 10000,处理时间也仅为 10.573ms,多进程、多线程以及 Reactor 模式的协作确实能够非常明显的增加 Web 服务器的工作性能和吞吐量。


在 Puma 的 官方网站 中,有一张不同 Web 服务器内存消耗的对比图:



我们可以看到,与 Unicorn 相比 Puma 的内存使用量几乎可以忽略不计,它明显解决了多个 worker 占用大量内存的问题;不过使用了多线程模型的 Puma 需要开发者在应用中保证不同的线程不会出现竞争条件的问题,Unicorn 的多进程模型就不需要开发者思考这样的事情。


对比


上述四种不同的 Web 服务器其实有着比较明显的性能差异,在使用同一个最简单的 Web 应用时,不同的服务器表现出了差异巨大的吞吐量:



Puma 和 Unicorn 两者之间可能还没有明显的数量级差距,1 倍的吞吐量差距也可能很容易被环境因素抹平了,但是 WEBrick 可以说是绝对无法与其他三者匹敌的。


上述的不同服务器其实有着截然不同的 I/O 并发模型,因为 MRI 中 GIL 的存在我们很难利用多核 CPU 的计算资源,所以大多数多线程模型在 MRI 上的性能可能只比单线程略好,达不到完全碾压的效果,但是 JRuby 或者 Rubinius 的使用确实能够利用多核 CPU 的计算资源,从而增加多线程模型的并发效率。



传统的 I/O 模型就是在每次接收到客户端的请求时 fork 出一个新的进程来处理当前的请求或者在服务器启动时就启动多个进程,每一个进程在同一时间只能处理一个请求,所以这种并发模型的吞吐量有限,在今天已经几乎看不到使用 accept & fork 这种方式处理请求的服务器了。


目前最为流行的方式还是混合多种 I/O 模型,同时使用多进程和多线程压榨 CPU 计算资源,例如 Phusion Passenger 或者 Puma 都支持在单进程和多进程、单线程和多线程之前来回切换,配置的不同会创建不同的并发模型,可以说是 Web 服务器中最好的选择了。


最后要说的 Thin 其实使用了非常不同的 I/O 模型,也就是事件驱动模型,这种模型在 Ruby 社区其实并没有那么热门,主要是因为 Rails 框架以及 Ruby 社区中的大部分项目并没有按照 Reactor 模型的方式进行设计,默认的文件 I/O 也都是阻塞的,而 Ruby 本身也可以利用多进程和多线程的计算资源,没有必要使用事件驱动的方式最大化并发量。



Node.js 就完全不同了。Javascript 作为一个所有操作都会阻塞主线程的语言,更加需要事件驱动模型让主线程只负责接受 HTTP 请求,其余的脏活累活都交给线程池来做了,结果的返回都通过回调的形式通知主线程,这样才能提高吞吐量。


总结


在这个系列的文章中,我们先后介绍了 Rack 的实现原理以及 Rack 协议,还有四种 webserver 包括 WEBrick、Thin、Unicorn 和 Puma 的实现,除了这四种应用服务器之外,Ruby 社区中还有其他的应用服务器,例如:Rainbows 和 Phusion Passenger,它们都有各自的实现以及优缺点。


从当前的情况来看,还是更推荐开发者使用 Puma 或者 Phusion Passenger 作为应用的服务器,这样能获得最佳的效果。


相关文章



Reference



本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/ruby-webserver


2019 年 12 月 05 日 18:15109

评论

发布
暂无评论
发现更多内容

【智简联接,万物互联】华为云·云享专家董昕:Serverless和微服务下, IoT的变革蓄势待发

华为云开发者社区

Serverless 物联网 IoT

全球最火的程序员学习路线!2020年GitHub上那些优秀Android开源库总结,吊打面试官系列!

欢喜学安卓

android 程序员 面试 移动开发

网易区块链打造可信数字身份认证应用新场景,赋能科技峰会

CECBC区块链专委会

数字身份

工业区块链正在改变什么?

CECBC区块链专委会

环保

完美!凭借这份阿里大佬分享的4170页Java高手真经笔记!offer拿到手软

马士兵老师

Java 程序员 编程语言 电子书 架构资料

闲聊个人服务:革「to C」的命

欧雷

产品 去中心化

架构师训练营第 1 期 week13 总结

张建亮

极客大学架构师训练营

ES6中的Promise和Generator详解

程序那些事

新特性 ES6 Promise 程序那些事 Generator

甲方日常 70

句子

工作 随笔杂谈 日常

闭嘴,别再问什么是锁了

程序员老猫

乐观锁 悲观锁 分布式锁 java锁 公平锁

TeamLeader不可不知的三种团队建设形式

Alan

团队管理 个人成长 28天写作营

Superset 助力企业级大数据 Ad-hoc 查询

麻婆豆腐没麻婆

数据分析 Apache Superset BI数美

阿里技术官面鹅厂,被高并发问蒙,含泪整理全网最全线程并发文档

周老师

Java 编程 程序员 架构 面试

利用Python进行数据分析(原书第2版)免费下载

计算机与AI

Python 数据分析 数据科学

re:Invent 重磅回顾 | AWS 重塑机器学习的四大亮点,触及每一位 AI 工作者

亚马逊AWS官方博客

云计算 AWS

架构师训练营第 1 期第 13 周学习总结

好吃不贵

极客大学架构师训练营

程序员如何解决中年危机?我的阿里春招之路分享,顺利通过阿里Android岗面试

欢喜学安卓

android 程序员 面试 移动开发

生产环境全链路压测建设历程12:通过生产压测发现的问题摘录

数列科技杨德华

全链路压测

直播报名 | 携程技术沙龙——前端测试技术创新与实践

携程技术中心

AI 数据分析

Rancher开源Harvester:基于K8S的超融合基础架构软件

RancherLabs

Kubernetes rancher

QoS简介

网络技术平台

星环科技自动特征工程论文被ICA3PP2020接收

星环科技

AI 数据集

架构之书:我们从何处来?我们是谁?我们向何处去?

lidaobing

架构 编程的未来

架构师 3 期 3 班 -week4- 总结

zbest

总结 week4

Kafka实战宝典:Kafka的控制器controller详解

数据社

kafka 七日更

架构师训练营第 1 期 week13

张建亮

极客大学架构师训练营

游戏服务器多钱一个月呢?

德胜网络-阳

技术选型背后的国家利益:区块链自主化道路的交锋

CECBC区块链专委会

科技

阅站无数!不过我只推荐下面这些

cxuan

推荐 网站

波场链智能合约软件系统开发|波场链智能合约APP开发

开發I852946OIIO

系统开发

图解Janusgraph系列-并发安全:锁机制(本地锁+分布式锁)分析

洋仔聊编程

janusgraph 图数据库

Ruby Web 服务器的并发模型与性能-InfoQ