update books
set author = 'David'
where title LIKE '%Rails%'
Another example is iteration over a large dataset. Sometimes you need only the data. No typecasting, no updates. This snippet just runs the query and avoids ActiveRecord altogether:
result = ActiveRecord::Base.execute 'select * from books'
result.each do |row|
# do something with row.values_at('col1', 'col2')
前两种方式能够很好的运行,但是第三种不可以。为什么呢?因为执行 Rails 回调需要存储执行上下文(变量,常量,全局实例等等)就是在回调的时候。如果你的应用很大,你最终在内存里复制了大量的数据。因为回调在任何时候都可以执行,内存在你程序结束之前不可以回收。
有象征,回调在每个请求为我节省了 0.6 秒。
2.2 写更少的 Ruby
这是我最喜欢的一步。我的大学计算机科学类教授喜欢说,最好的代码是不存在的。有时候做好手头的任务需要其它的工具。最常用的是数据库。为什么呢?因为 Ruby 不善于处理大数据集。非常非常的糟糕。记住,Ruby 占用非常大的内存。所以举个例子,处理 1G 的数据你可能需要 3G 的或者更多的内存。它将要花费几十秒的时间去垃圾回收这 3G。好的数据库可以一秒处理这些数据。让我来举一些例子。
2.2.1 属性预加载
有时候反规范化模型的属性从另外一个数据库获取。比如,想象我们正在构建一个 TODO 列表,包括任务。每个任务可以有一个或者几个标签标记。规范化数据模型是这样的:
Tasks
Tasks_Tags
tag_id
task_id
加载任务以及它们的 Rails 标签,你会这样做:
tasks = Task.find(:all, :include => :tags)
> 0.058 sec
这段代码有问题,它为每个标签创建了对象,花费很多内存。可选择的解决方案,将标签在数据库预加载。
tasks = Task.select <<-END
array(
select tags.name from tags inner join tasks_tags on (tags.id = tasks_tags.tag_id)
where tasks_tags.task_id=tasks.id
) as tag_names
> 0.018 sec
这只需要内存存储额外一列,有一个数组标签。难怪快 3 倍。
2.2.2 数据集合
我所说的数据集合任何代码去总结或者分析数据。这些操作可以简单的总结,或者一些更复杂的。以小组排名为例。假设我们有一个员工,部门,工资的数据集,我们要计算员工的工资在一个部门的排名。
SELECT * FROM empsalary;
depname | empno | salary
-----------+-------+-------
develop | 6 | 6000
develop | 7 | 4500
develop | 5 | 4200
personnel | 2 | 3900
personnel | 4 | 3500
sales | 1 | 5000
sales | 3 | 4800
你可以用 Ruby 计算排名:
salaries = Empsalary.all
salaries.sort_by! { |s| [s.depname, s.salary] }
key, counter = nil, nil
salaries.each do |s|
if s.depname != key
key, counter = s.depname, 0
counter += 1
s.rank = counter
end
Empsalary 表里 100K 的数据程序在 4.02 秒内完成。替代 Postgres 查询,使用 window 函数做同样的工作在 1.1 秒内超过 4 倍。
SELECT depname, empno, salary, rank()
OVER (PARTITION BY depname ORDER BY salary DESC)
FROM empsalary;
depname | empno | salary | rank
-----------+-------+--------+------
develop | 6 | 6000 | 1
develop | 7 | 4500 | 2
develop | 5 | 4200 | 3
personnel | 2 | 3900 | 1
personnel | 4 | 3500 | 2
sales | 1 | 5000 | 1
sales | 3 | 4800 | 2
4 倍加速已经令人印象深刻,有时候你得到更多,到 20 倍。从我自己经验举个例子。我有一个三维 OLAP 多维数据集与 600k 数据行。我的程序做了切片和聚合。在 Ruby 中,它花费了 1G 的内存大约 90 秒完成。等价的 SQL 查询在 5 内完成。
Unicorn 可以在创建新的 worker 进程前,预载入 Rails 应用。这样有两个好处。第一,主线程可以通过写入时复制的友好GC机制(Ruby 2.0以上),共享内存的数据。操作系统会透明的复制这些数据,以防被worker修改。第二,预载入减少了worker进程启动的时间。Rails worker进程重启是很常见的(稍后将进一步阐述),所以worker重启的速度越快,我们就可以得到更好的性能。
若需要开启应用的预载入,只需要在unicorn的配置文件中添加一行:
preload_app true
2.3.2 在 Request 请求间的 GC
请谨记,GC 的处理时间最大会占到应用时间的50%。这个还不是唯一的问题。GC 通常是不可预知的,并且会在你不想它运行的时候触发运行。那么,你该怎么处理?
首先我们会想到,如果完全禁用 GC 会怎么样?这个似乎是个很糟糕的想法。你的应用很可能很快就占满 1G 的内存,而你还未能及时发现。如果你服务器还同时运行着几个 worker,那么你的应用将很快会出现内存不足,即使你的应用是在自托管的服务器。更不用说只有 512M 内存限制的 Heroku。
其实我们有更好的办法。那么如果我们无法回避GC,我们可以尝试让GC运行的时间点尽量的确定,并且在闲时运行。例如,在两个request之间,运行GC。这个很容易通过配置Unicorn实现。
对于Ruby 2.1以前的版本,有一个unicorn模块叫做OobGC:
require 'unicorn/oob_gc'
use(Unicorn::OobGC, 1) # "1" 表示"强制GC在1个request后运行"
对于Ruby 2.1及以后的版本,最好使用gctools(https://github.com/tmm1/gctools):
require 'gctools/oobgc'
use(GC::OOB::UnicornMiddleware)
但在request之间运行GC也有一些注意事项。最重要的是,这种优化技术是可感知的。也就是说,用户会明显感觉到性能的提升。但是服务器需要做更多的工作。不同于在需要时才运行GC,这种技术需要服务器频繁的运行GC. 所以,你要确定你的服务器有足够的资源来运行GC,并且在其他worker正在运行GC的过程中,有足够的worker来处理用户的请求。
2.4 有限的增长
我已经给你展示了一些应用会占用1G内存的例子。如果你的内存是足够的,那么占用这么一大块内存并不是个大问题。但是Ruby可能不会把这块内存返还给操作系统。接下来让我来阐述一下为什么。
Ruby通过两个堆来分配内存。所有Ruby的对象在存储在Ruby自己的堆当中。每个对象占用40字节(64位操作系统中)。当对象需要更多内存的时候,它就会在操作系统的堆中分配内存。当对象被垃圾回收并释放后,被占用的操作系统中的堆的内存将会返还给操作系统,但是Ruby自有的堆当中占用的内存只会简单的标记为free可用,并不会返还给操作系统。
这意味着,Ruby的堆只会增加不会减少。想象一下,如果你从数据库读取了1百万行记录,每行10个列。那么你需要至少分配1千万个对象来存储这些数据。通常Ruby worker在启动后占用100M内存。为了适应这么多数据,worker需要额外增加400M的内存(1千万个对象,每个对象占用40个字节)。即使这些对象最后被收回,这个worker仍然使用着500M的内存。
这里需要声明, Ruby GC可以减少这个堆的大小。但是我在实战中还没发现有这个功能。因为在生产环境中,触发堆减少的条件很少会出现。
如果你的worker只能增长,最明显的解决办法就是每当它的内存占用太多的时候,就重启该worker。某些托管的服务会这么做,例如Heroku。让我们来看看其他方法来实现这个功能。
2.4.1 内部内存控制
Trust in God, but lock your car 相信上帝,但别忘了锁车。(寓意:大部分外国人都有宗教信仰,相信上帝是万能的,但是日常生活中,谁能指望上帝能帮助自己呢。信仰是信仰,但是有困难的时候 还是要靠自己。)。有两个途径可以让你的应用实现自我内存限制。我管他们做,Kind(友好)和hard(强制).
Kind 友好内存限制是在每个请求后强制内存大小。如果worker占用的内存过大,那么该worker就会结束,并且unicorn会创建一个新的worker。这就是为什么我管它做“kind”。它不会导致你的应用中断。
获取进程的内存大小,使用 RSS 度量在 Linux 和 MacOS 或者 OS gem 在 windows 上。我来展示下在 Unicorn 配置文件里怎么实现这个限制:
class Unicorn::HttpServer
KIND_MEMORY_LIMIT_RSS = 150 #MB
alias process_client_orig process_client
undef_method :process_client
def process_client(client)
process_client_orig(client)
rss = `ps -o rss= -p #{Process.pid}`.chomp.to_i / 1024
exit if rss > KIND_MEMORY_LIMIT_RSS
end
硬盘内存限制是通过询问操作系统去杀你的工作进程,如果它增长很多。在 Unix 上你可以叫 setrlimit 去设置 RSSx 限制。据我所知,这种只在 Linux 上有效。MacOS 实现被打破了。我会感激任何新的信息。
这个片段来自 Unicorn 硬盘限制的配置文件:
after_fork do |server, worker|
worker.set_memory_limits
class Unicorn::Worker
HARD_MEMORY_LIMIT_RSS = 600 #MB
def set_memory_limits
Process.setrlimit(Process::RLIMIT_AS, HARD_MEMORY_LIMIT * 1024 * 1024)
end
2.4.2 外部内存控制
自动控制没有从偶尔的 OMM(内存不足)拯救你。通常你应该设置一些外部工具。在 Heroku 上,没有必要因为它们有自己的监控。但是如果你是自托管,使用 monit,god 是一个很好的主意,或者其它的监视解决方案。
2.5 优化 Ruby GC
在某些情况下,你可以调整 Ruby GC 来改善其性能。我想说,这些 GC 调优变得越来越不重要,Ruby 2.1 的默认设置,后来已经对大多数人有利。
GC 好的调优你需要知道它是怎么工作的。这是一个独立的主题,不属于这编文章。要了解更多,彻底读读 Sam Saffron 的 揭秘 Ruby GC 这篇文章。在我即将到来的 Ruby 性能的一书,我挖到更深的 Ruby GC 细节。订阅这个,当我完成这本书的 beta 版本会给你发送一份邮件。
我的建议是最好不要改变 GC 的设置,除非你明确知道你想要做什么,而且有足够的理论知识知道如何提高性能。对于使用 Ruby 2.1 或之后的版本的用户,这点尤为重要。
我知道只有一种场合 GC 优化确实能带来性能的提升。那就是,当你要一次过载入大量的数据。你可以通过改变如下的环境变量来达到减少GC运行的频率:RUBY_GC_HEAP_GROWTH_FACTOR,RUBY_GC_MALLOC_LIMIT,RUBY_GC_MALLOC_LIMIT_MAX,RUBY_GC_OLDMALLOC_LIMIT,和 RUBY_GC_OLDMALLOC_LIMIT。
请注意,这些变量只适用于 Ruby 2.1 及之后的版本。对于 2.1 之前的版本,可能缺少某一个变量,或者变量不是使用这个名字。
RUBY_GC_HEAP_GROWTH_FACTOR 默认值 1.8,它用于当 Ruby 的堆没有足够的空间来分配内存的时候,每次应该增加多少。当你需要使用大量的对象的时候,你希望堆的内存空间增长的快一点。在这种场合,你需要增加该因子的大小。
内存限制是用于定义当你需要向操作系统的堆申请空间的时候,GC 被触发的频率。Ruby 2.1 及之后的版本,默认的限额为:
New generation malloc limit RUBY_GC_MALLOC_LIMIT 16M
Maximum new generation malloc limit RUBY_GC_MALLOC_LIMIT_MAX 32M
Old generation malloc limit RUBY_GC_OLDMALLOC_LIMIT 16M
Maximum old generation malloc limit RUBY_GC_OLDMALLOC_LIMIT_MAX 128M
让我简要的说明一下这些值的意义。通过设置以上的值,每次新对象分配 16M 到 32M 之间,并且旧对象每占用 16M 到 128M 之间的时候 ("旧对象" 的意思是,该对象至少被垃圾回收调用过一次), Ruby 将运行 GC。Ruby 会根据你的内存模式,动态的调整当前的限额值。
所以,当你只有少数几个对象,却占用了大量的内存(例如读取一个很大的文件到字符串对象中),你可以增加该限额,以减少 GC 被触发的频率。请记住,要同时增加 4 个限额值,而且最好是该默认值的倍数。
我的建议是可能和其他人的建议不一样。对我可能合适,但对于你却未必。这些文章将介绍,哪些对 Twitter 适用,而哪些对 Discourse 适用。
很简单,换 PHP7!:)
拍黄片的撸棒性超强,毕竟是FastCGI运行模式,所有量(变量/常量/静态/全局)只存在于每一次请求的生命周期里.
而且PHP7速度翻倍,占用内存一半,给应用带来的性能提升将十分明显.
几句代码就能实现MVC:
比如浏览器访问页面控制器,控制器处理输入,调用模型获取数据,载入视图输出数据.
/post.php?id=1024 //页面控制器(输入ID,输出文章)
<?php
if(!defined('APP_ROOT')) define('APP_ROOT', './'); //定义入口
require APP_ROOT.'include/common.php'; //加载模型(比如functions.php)
$data = get_post(intval($_GET['id'])); //处理输入,调用模型获取数据
//载入视图输出数据
ob_start();
require APP_ROOT.'themes/'.$app['theme'].'/post.php';
$view = ob_get_contents();
ob_end_clean();
echo $view;
另外,Active Record和ORM这些东西就不要越俎代庖了,SQL和PHP关联数组已经足够易用高效了.