关于Ruby 2.0的内存使用Unicorn和Heroku

81 阅读4分钟

在类似UNIX的操作系统(例如Linux)中,有一个通过fork() 系统调用的进程分叉概念。当人们从父进程中调用fork时,它将产生一个新的子进程--它是父进程的副本,然后两个进程都从fork返回。子进程在一个独立的地址空间中拥有父进程内存的精确拷贝。由于这显然是不高效的,UNIX实现了写时复制的语义(即CoW):这将内存的实际复制推迟到需要写入时进行,简单而优雅。

昔日的Ruby 1.8.7和REE

历史上,Ruby在分叉方面一直很糟糕,我指的不是语言本身,而是它的虚拟机实现:特别是MRI和YARV。问题在于这些版本的垃圾收集工作方式,不谈细节,当GC运行以清除内存中一些未使用的引用时,它会改变每个对象,使它们变得 "脏",即CoW很快失败,因为整个内存变得脏。为什么这很重要?最好的Rack兼容服务器使用预分叉来最大限度地利用所有的CPU(例如Unicorn, Phusion Passenger )

当你启动Unicorn服务器时,它会通过fork() 系统调用创建一些事先配置好的工作者。在64位的Linux上,一个Rails 3.x,几乎是普通的应用程序需要大约70M的内存,而一个相当复杂的应用程序需要大约200M的内存,现在把它乘以你系统上的可用核心数,比如说8个,我们就有一个从560M到惊人的1.6G的内存使用量。使用Ruby 1.8.7可以得到一个很好的奖励:有一个CoW友好的版本,有一个更新的垃圾收集算法,由Phusion的人编写--以Ruby企业版的形式--REE 。

这不是一个解决方案,因为1.8.7即将在2013年6月到期,而REE是下一个 ,尽管如此,REE过去/现在是一个伟大的软件。

最近发布的Ruby 2.0

Narihiro Nakamura实现了新的GC算法--位图标记:简而言之--Ruby虚拟机可以在不实际修改对象的情况下进行清扫,也就是说,CoW现在可以如期工作。关于更多的细节,你可以查看关于Ruby的GC的视频,以及Pat Shaughnessy最近的一个非常好的高级解释

当你把Ruby 2.0和Unicorn结合起来时,你可以得到一些非常令人印象深刻的结果。使用这个简单的脚本memstats.rb我检查了八个工作者的内存使用情况:

 Memory Summary:
 private_clean                   0 kB
 private_dirty               1,584 kB
 pss                        12,884 kB
 rss                        80,152 kB
 shared_clean                1,984 kB
 shared_dirty               76,584 kB
 size                      275,704 kB
 swap                            0 kB

根据这个gist: "rss代表实际使用的物理内存,它由private_clean + private_dirty + shared_clean + shared_dirty组成",所以实际上~76M是共享的,~1.5M是私有的。这与YARV 1.9.3相比是一个巨大的进步,在那里没有CoW友好的GC,即没有任何内存共享。这些结果令人印象深刻,当然,我的应用程序是相当微不足道的(它只是运行这个博客),所以在真实的应用程序中,比例不会那么高,但仍然足够大,以节省大量内存。

Heroku怎么样?

一个Heroku Dyno被限制在512M的内存,之后它开始交换内存,直到它崩溃和/或Heroku将发送一个SIGKILL并重新启动你的应用程序 -更多信息 。由于最近的路由错误,我们都知道Rack兼容应用的最佳服务器是Unicorn(实际上,这应该是一个常识):

# config/unicorn.rb
worker_processes 8

当你升级到Ruby 2.0时会发生什么?你可以在不影响硬配额的情况下,将工作者的数量提高一倍。

总结

显然,这并不只对Heroku用户有利,升级到Ruby 2.0是不费吹灰之力的,因为在大多数情况下,Ruby 2.0可以直接替换,而且还能降低内存使用。最大的问题是运行1.8.7或REE的应用程序,因为从1.8.7升级到1.9.3和2.0会产生一些兼容性问题。