ruby服务器并发模型(译)
并发
通常情况下会有很多用户同时使用你的应用,并且你也希望应用能够及时响应用户的请求。那么你就需要使用某种方法去处理并发问题,大部分的 web服务器已经默认实现了。但是当你要扩大规模的时候,你就要尽可能的使用高效的并发模型。
不同的并发类型
处理并发问题,有多种途径:多线程,多线程和事件循环,其中每一种都有自己的使用场景,优点及缺点。通过本文你会了解到它们的不同点和使用场景。
多进程(Unicorn)
是用多进程模型是比较容易实现并发的,主进程自身forks出多个工作进程,工作进程是用于实际处理请求的,而主进程用于管理工作进程。
每个工作进程在内存中有完整的代码基。这导致多进程模型很容易内存吃紧并且很难扩充到大规模架构上。
多进程小结
场景
一个非 ruby 的例子就是众所周知的Chrome 浏览器.
它使用多进程并发给每个标签页分配一个进程,这样就不会因为一个标签页挂掉的导致整个浏览器停止工作。并且可以隔离开发单个标签页。
优点
非常容易实现,避免了多线程的安全问题,每个工作进程挂掉并不影响整个系统的其他部分。
缺点
每个进程会加载整个代码库到内存当中去。这会使得内存变得吃紧,因此不适合大规模并发请求。
多线程(Puma)
多线程模型,是通过在一个进程中运行多个线程来让一个进程可以同时去处理多个请求。
与多进程不同, 所有的线程运行在一个进程中,这代表着它们中间是可以共享例如全局变量这样的数据的。因此每个线程只会额外使用少量的内存。
全局解释锁(Global Interpreter Lock)
在 MRI中使用线程会遇到全局解释锁(GIL)。GIL被是用于锁定所有 ruby 代码的执行的,尽管我们的线程看上去是并行执行的,但实际上一个时间内只有一个线程是激活的。
IO 操作不受 GIL的限制,当你等待执行数据库的返回结果时,是不会被锁定的,其他的线程也是可以同时执行同样的 IO操作的,如果你在线程中使用哈希或者数组去处理大量的数学计算的话,那么你只能利用一个核心。如果你使用 MRI 在大部分场景下还行需要使用多进程模型,去充分利用机器资源,或者你也可以使用 Rubinius或 jRuby,
这种没有 GIL 的 ruby 实现。
线程安全
如果你使用多线程模型,那么就必须十分小心的操作线程中的共享数据。你可以通过使用 互斥锁 在操作共享数据前锁定它,这样就可以确保其他线程不会在你已经修改了数据的情况下,还使用过期的数据。
多线程小总结
场景
这是一种折衷方案,被用于很多请求量少的普通 web 应用中
优点
相比于多进程模型,可以使用较少的内存。
缺点
你要确保你的代码是线程安全的,如果线程出现了崩溃,那么它很有可能让你的整个进程一起崩溃掉。GIL会锁定除 IO之外的所有操作
事件循环(Thin)
事件循环是用于,当你需要执行很多并发 IO操作的场景。这个模型本身不会强制的让多个请求在同时执行,而是通过高效的方式处理并发。
下面 你会看到非常简单的使用 Ruby写成的事件循环示例,循环从事件队列中取出事件处理,如果队列为空,他会休眠然后继续查看队列中是否有新的事件
loop do
if event_queue.any?
handle_event(event_queue.pop)
else
sleep 0.1
end
end
在图中,我们能够清晰的了解到事件循环是如何与操作系统队列和内存交互的
步骤
- 操作系统一直监听网络和磁盘何时可用。
- 当操作系统看到 I/O 可用后, 它就会将事件添加到队列中去。
- 队列中存储这若干,待事件循环处理的事件。
- 事件循环用于处理事件,并使用在内存中存储与连接相关的元数据,也可以再次直接发送新的事件到事件队列中去。
- 如果想要进行 I/O 操作,事件循环会告诉操作系统,它想继续那个特定的 I/O操作。操作系统继续监听网络和磁盘,并且当I/O可用后再次添加事件
事件循环小结
用例
有很多并发连接的场景,例如 Slack, Chrome notifications
优点
每个连接几乎没有多少内存开销,可处理大量的并行连接
缺点
事件循环不是一个容易理解的模型。为防止队列爆增,处理批次的大小必须小并且可预测。
究竟该使用哪一种模型?
希望这篇文章能够更好的让你理解不同的并发模型,虽然其中有些主题难以理解,但是掌握这些会让你有工具去实验并且能用正确的组织构建你的应用。
总结
大部分应用适合使用多线程模型,Ruby/Rails 生态圈 看起来正在(缓慢)向这个方面发展。
如果你的应用是高并发长连接的,那么事件循环很容你帮你实现。
如果你的应用不是高访问量的网站,那么使用多进程的方式就以及足够好了。
当然,还有一种可能是在多进程嵌套多线程,在多线程中再使用事件循环。