Drupal 的内核架构非常简洁并且非常灵活。然而,这种灵活性是有代价的。当启用的模块增加时,处理一个请求的复杂度也会增加。这意味着将耗费更多的服务器资源,必 须实现一些策略,在一个站点日渐流行时,来保证 Drupal 的极其著名的灵活性。通过适当的配置,Drupal 可以很容易的满足用户的需求。在本章中,我们将讨论性能(performance)和可升级性(scalability)。性能指的是你的站点响应一个请 求所用的时间。可升级性指的是你的系统可以同时处理多少个并发请求,通常用“请求数/秒”来度量。
找出瓶颈
如果你的网站运行性能达不到预期,第一步要做的就是分析问题所在。可能的原因包括 web 服务器、操作系统、数据库和网络。
逐步追踪
知道如何估算一个系统的性能和可升级性,即便是在出现大的问题时,它也能使得你能保持自信,从而快速的隔离并找到系统的瓶颈。你可以借助于一些简单 的工具,通过询问一些问题,来发现瓶颈。这里有一个方式用来分析一个存在严重性能问题的服务器。首先,我们需要知道性能有哪些因素决定了,CPU、 RAM、I/O 或者带宽都是影响性能的因素。那么你需要询问以下问题:
CPU 占用率达到最大了吗?检查 CPU 的使用情况,在 Unix 上你可以使用 top,在 Windows 上可以使用任务管理器,如果 CPU 占用率达到了 100%,那么你的任务就是找出是什么程序占用了 CPU。查看进程列表,可以让你了解到是不是 web 服务器或者数据库占用了大部分处理器计算资源。这两者都是可以解决的。
服务器的 RAM 用完了没有?在 Unix 上你可以使用 top,在 Windows 上可以使用任务管理器,可以很容易的检查这一点。如果服务器还有大量的空闲内存,继续下一个问题。如果服务器用完了内存,你必须找出原因。
磁盘空间不足了吗?检查磁盘子系统,在 Unix 使用工具 vmstat,在 Windows 下使用性能监测器,如果可用磁盘不能满足需求,同时又有大量空闲内存剩余,那么这就是一个 I/O 问题。可能的原因包括记录大量的冗余的日志,对数据库不恰当的配置使得在磁盘上创建了许多临时表,后台脚本的执行,对于一个需要大量写操作的系统使用了一 个不恰当的 RAID 级别,等等。
网络带宽用完了吗?如果网络带宽用完了,那么只有两种解决方案。一个是增加带宽。另一个是对发送的信息进行恰当的压缩,从而发送更少的信息。
Web 服务器用完了 CPU
如果你的 CPU 占用率达到了100%,而进程列表显示,是 web 服务器消耗了大量的资源而不是数据库(后面介绍),那么你应该去减少 web 服务器处理一个请求所耗费的资源。通常 PHP 代码的执行是罪魁祸首。
PHP 最优化措施
在 Drupal 中,由于 PHP 代码执行在处理一个请求中占了一大块,所以我们需要知道采取哪些措施才能加快这一进程,这一点非常重要。对编译后的 PHP 操作代码(opcodes)进行缓存,和剖析应用层来找出低效算法,能够带来重要的性能提升。
缓存操作代码
有两种方式可以减少执行 PHP 代码所耗费的资源。很明显,一种是减少代码总量,可以通过禁用不必要的 Drupal 模块和编写高效的代码来达到这一点。另一种方式就是使用一个 opcode 缓存。PHP 对于每个请求,都会将所有代码解析并编译成一种中间形态,这种形态里包含了一系列的操作代码。添加一个 opcode 缓存可以让 PHP 能够重用前面编译过的代码,这样就会跳过解析和编译。常见的 opcode 缓存有 Alternative PHP Cache (http://pecl.php.net/package/APC)、eAccelerator(http://eaccelerator.net)、 XCache(http://trac.lighttpd.net/xcache/)和 Zend Platform(http://zend.com)。Zend 是一个商业产品,而其它几个则是免费的。
由于 Drupal 是一个需要大量数据库操作的程序,所以一个 opcode 缓存不能作为一个单独的解决方案,但它一个整体方案中的一部份。只需要最小的努力,他人仍然可以代码明显的性能提升。
图22-1 Alternative PHP Cache(APC)带有有一个接口,它能够展示内存分配情况和当前缓存中的文件。
剖析应用
通常定制的代码和模块对于小规模的站点能够很好的工作,如果将它放到上线的站点上那么就可能成为站点的瓶颈。耗费 CPU 的代码循环,占用内存的算法,还有大量的数据库读取,这些都可以通过剖析你的代码来决定 PHP 在哪里花费了大量时间,因此,找到的关键点也就是你需要花费大量时间进行调试的地方。更多关于 PHP 调试器和 profiler 的信息,参看第21章。
如果,即便是添加了 opcode 并且优化了你的代码以后,你的 web 服务器仍然不能处理这么大的负载,那么现在你就应该换一个带有更多或者更快的 CPU 的更强大的服务器,或者更换应用架构,采用多个 web 服务器。
Web 服务器用完了 RAM
Web 服务器进程处理一个请求时,用到 RAM 的地方包括,web 服务器加载所有的模块(比如 Apache 的 mime_module、rewrite_module 等等),还有 PHP 解释器使用的内存。启用的 web 服务器和 Drupal 模块越多,处理单个请求耗费的 RAM 就越多。
注意:分配给 PHP 解释器的最大内存,可通过 PHP 的 php.ini 文件中的 memory_limit 进行设置。它的默认值为 8MB,为了留给 Drupal 足够的空间,至少需要将其翻倍。只有编译你的 PHP 时使用了 Denable-memory-limit,指令 memory_limit 才会生效。
因为 RAM 是个有限的资源,你应该决定对于每个请求分配多少内存,还有你的 web 服务器能够能够同时处理多少个请求。为了知道平均为每个请求使用多少内存,可使用一个向 Unix 中的 top 一样的程序来查看你的进程列表。在 Apache 中,可以使用指令 MaxClients 来设置能够处理的最大并发请求数。一个常见的错误认为,解决满负荷的 web 服务器的方案是增加 MaxClients 的值。这只会让问题变得更加复杂,因为你将同时收到太多的请求。这意味着 RAM 将被耗尽,而你的服务器将开始进行磁盘交换并变得不能响应请求。让我们假定,你的 web 服务器拥有 2GB 的内存,而每个 Apache 请求大约使用 20MB(你可以使用 top 来检查实际值)。通过下面的公式,你可以为 MaxClients 计算出一个合适的值;你应该记住你将需要为你的操作系统和其它进程保留一定的内存:
2GB RAM / 20MB per process = 100 MaxClients
如果在禁用了不需要的 web 服务器模块,并且优化了定制的模块或者代码以后,你的服务器仍然耗尽内存,那么接下来你要做的是确保数据库和操作系统不是产生瓶颈的原因。如果是由它们引 起的,那么就添加更多的内存。如果不是,那么问题就很简单了,你收到的请求比你能够处理的请求要多;解决方案就是添加更多的 web 服务器。
提示:由于 Apache 进程用到的内存,随着它的子进程处理更加耗费内存的页面,有逐步增加的趋势,通过将 MaxRequestsPerChild 的值设的小一点比如300(实际的值依赖于你所处的环境)可以收回不少的内存。Apache 将会更努力一点的工作来生成新的子进程,而新的子进程比替换掉的旧的子进程所需的内存要少一些,这样你就可以是要较少的内存处理更多的请求。 MaxRequestsPerChild 的默认值为0,这意味着进程永不过期。(译者注:我不知道这里讲的什么意思,没学过)。
其它 web 服务器最优化
为了让你的 web 服务器更有效的运行,这里有一些其它的措施。
Apache 最优化
Apache 是 Drupal 最常用的 web 服务器,通过对它进行调整可以获得更高的性能。下面的部分将建议一些可以尝试的方式。
mod_expires
这个 Apache 模块将让 Drupal 发出过期的 HTTP 头部,在用户的浏览器中对静态文件缓存两周,或者直到存在一个文件的新版本为止。这是用于所有的图片、CSS 和 JavaScript 文件和其它静态文件。最终的结果是减少带宽,而服务器需要发送的信息将会更少一些。Drupal 对于和 mod_expires 一同工作预先进行了配置,一旦 mod_expires 可用,Drupal 就会使用它。mod_expires 的设置可以在 Drupal 的 .htaccess 文件中找到。
# Requires mod_expires to be enabled.
# Enable expirations.
ExpiresActive On
# Cache all files for 2 weeks after access (A).
ExpiresDefault A1209600
# Do not cache dynamically generated pages.
ExpiresByType text/html A1
我们不能让 mod_expires 缓存 HTML 内容,这是由于 Drupal 输出的 HTML 内容不总是静态的。这也是 Drupal 拥有自己的内部缓存系统的原因,内置缓存系统可对它的 HTML 输出进行缓存(比如页面缓存)。
迁移 .htaccess 文件
Drupal 自带了两个 .htaccess 文件:一个位于 Drupal 根路径,而另一个将被自动创建,在你创建了存储上传文件的目录以后,访问 Administer/File system 设置目录的位置,此时系统将为你自动创建一个 .htaccess 文件。在处理每个请求时,任何 .htaccess 文件都将被搜索、读取和解析。相反,httpd.conf 只在 Apache 启动时才被读取。Apache 指令可以放在这两种文件中。如果你能够控制你自己的服务器,你应该将. htaccess 文件中的内容转移到 Apache 主配置文件(httpd.conf)中,并通过将 AllowOverride 设置为 None 来禁用在你的 web 服务器根路径下查找 .htaccess 文件:
AllowOverride None
…
对于每个请求,这将阻止 Apache 穿过目录树来查找要执行的 .htaccess 文件。这样对于每个请求,Apache 要做的工作就会有所减少,这就使得它能够处理更多请求。
其它的 Web 服务器
另一种选项是使用另一个服务器来代替 Apache。Benchmarks 已经说明了这一点,例如,LightTPD web 服务器一般情况下每秒能够为 Drupal 处理更多的请求。更多详细比较,参看 http://buytaert.net/drupal-webserver-configurations-compared
Database Bottlenecks
数据库瓶颈
Drupal 需要进行大量的数据库操作,特别是对于验证的用户和定制的模块。数据库常常会成为产生瓶颈的原因。这里有一些基本的策略用于优化 Drupal 中数据库的使用。
启用 MySQL 的查询语句缓存
MySQL 是 Drupal 最常用的数据库。它具有在内存中缓存常用查询语句的能力,这样一个给定的查询语句再次被调用时,MySQL 将立即从缓存中将其返回。然而,在大多数 MySQL 中,这一特性默认是被禁用的。为了启用它,向你的 MySQL 配置选项文件添加下列代码,该文件的名称为 my.cnf,它用来声明变量和你的 MySQL 服务器的行为(参看http://dev.mysql.com/doc/refman/5.1/en/option-files.html)。在这里,我 们将查询语句缓存设为 64MB:
# The MySQL server
[mysqld]
query_cache_size=64M
当前的查询语句缓存的大小可以通过 MySQL 的 SHOW VARIABLES 命令的输入来查看:
mysql>SHOW VARIABLES;
…
| query_cache_size | 67108864
| query_cache_type | ON
…
不断的试验查询语句缓存的大小通常是有用的。缓存太小就意味着缓存了的查询语句很快就会过期。缓存太大就意味着搜索一个缓存可能需要花费相对较长的时间;还有就是使用内存进行缓存比使用其它一些方式要好,比如更多的 web 服务器处理或者操作系统的文件缓存。
提示:访问 Administer/Logs/Status report,点击 MySQL 版本号,来快速浏览一些更重要的 MySQL 变量的值。你也可以从该页检查查询语句缓存是否被启用了。
识别耗费资源查询
如果你想了解在生成一个给定页面时都发生了什么,那么 devel.module 就会非常有用。它拥有一个选项,用来展示生成页面所用到的所有的查询语句,以及每个查询所用的时间。关于如何使用 devel.module,通过 EXPLAIN 语法来识别和优化数据库查询的更详细的讨论,参看第21章。
找出执行时间过长的查询的另一种方式是,在 MySQL 中启用缓慢查询日志。为了启用它,需要在 MySQL 的配置选项文件(my.cnf)中这样设置:
# The MySQL server
[mysqld]
log-slow-queries
这会将超过10秒的查询记录到 MySQL 数据目录中的日志文件 example.com-slow.log 中去。你可以修改秒数以及日志的位置,如下面的代码所示,这里我们将缓慢查询的最小值设为5秒:
# The MySQL server
[mysqld]
long_query_time = 5
log-slow-queries = /var/log/mysql/example-slow.log
识别耗费资源的页面
为了找出哪些页面是最耗费资源的,需要启用 Drupal 自带的统计模块。尽管统计模块增加了你的服务器的负担(由于它将你的站点的访问统计记录到了数据库中),但它能够帮助我们找出哪些页面的访问量最大,对这 些页面我们要进行更多的优化。它也可以用来追踪一个时期内的生成页面的总的时间,你可以在 Administer/Logs/Access log settings 中进行声明。这对于识别耗费系统资源的不能控制的网络爬虫非常有用,通过访问 Administer/Logs/Top visitors 并点击“ban”(“禁用”)来当场禁止该网络爬虫的访问。你还需要小心一点——你有时候会很容易的禁用掉一个好的网络爬虫,它能够给你的站点带来访问 量。在禁止网络爬虫以前,你要确保调查了它的出处。
优化查询
看一下下面耗费资源的代码:
// Very expensive, silly way to get node titles. First we get the node IDs.
$sql = “SELECT n.nid FROM {node} n WHERE n.status = 1″;
// We wrap our node query in db_rewrite_sql() so that node access is espected.
$result = db_rewrite_sql(db_query($sql));
// Now we do a node_load() on each individual node.
while ($data = db_fetch_object($result)) {
$node = node_load($data->nid);
$titles[$node->nid] = $node->title;
}
完全加载一个节点是一个耗费资源的操作:运行钩子函数,模块处理数据库查询来添加或者修改节点,使用内存来在 node_load() 内部缓存中缓存节点。如果你不依赖于某一模块对节点的修改,直接对节点表进行你自己的查询,速度将会更快。当然这仅仅是一个人为的例子,但是常常会遇到同 样的模式,也就是,常常数据是通过多个查询来取回的,而实际上可以将多个查询合并成一个简单的查询,或者不需要加载整个节点。
提示:Drupal 拥有一个内部的缓存机制(使用静态变量),当在一个请求中多次记载同一节点时使用这一机制。例如,如果调用了 node_load(1)。节点1将被完全加载并被缓存。当在同一个 web 请求中再次调用 node_load(1) 时,Drupal 将会返回前面使用相同节点 ID 加载节点的缓存过结果。
作为一个实际的例子,假定你的站点拥有一个很大的分类,而你又想为每个词语(term)展示一列节点。回想一下在第14章“使用一个定制查询根据词 语对内容分类”部分中的例子的代码,它使用一个单独的查询(尽管复杂)来取得所有的东西。将其与使用分类(taxonomy)模块函数 taxonomy_select_nodes() 的代码进行比较,其中 taxonomy_select_nodes() 为每个词语进行以下查询($tids 是词语 ID 列表):
foreach ($tids as $index => $tid) {
// taxonomy_get_term() executes a database query.
$term = taxonomy_get_term($tid);
// taxonomy_get_tree() executes a database query.
$tree = taxonomy_get_tree($term->vid, $tid, -1, $depth);
$descendant_tids[] = array_merge(array($tid),array_map(”_taxonomy_get_tid_from_term”, $tree));
}
如果你的分类词语的数量巨大,那么单个查询和上百个查询的区别就会非常明显了。
优化表结构
另外,SQL 的缓慢可能是由第3方模块中的 SQL 表的不良实现引起的。例如,没有索引的列会引起查询缓慢。一个快速查看 MySQL 如何执行查询的方式,是从你的缓慢查询日志中取出一个查询,在其前面加上单词 EXPLAIN,然后将这个查询发给 MySQL。那么结果就是得到了一个显示使用了哪些索引的表。更多信息可参看 MySQL 的相关书籍。
手工缓存查询语句
如果你必须要进行非常昂贵的查询时,那么你可以在你的模块中亲自将结果缓存起来。关于更多 Drupal 缓存 API 的详细信息,参看第15章。
将表类型从 MyISAM 改为 InnoDB
MySQL 有两种常见的存储引擎,通常也称为表类型,它们是 MyISAM 和 InnoDB。Drupal 默认使用 MyISAM。
MyISAM 使用表级别的锁机制,而 InnoDB 使用行级别的锁机制。锁对于数据库的完整性非常重要;它可以阻止两个数据库进程试图同时对同一数据进行更新操作。在实际中,锁机制策略的不同意味着当你对 MyISAM 表进行写操作时,整个表将被锁住。因此,在一个繁忙的 Drupal 站点上,当正在添加多个评论时,在插入一个新评论的同时所有的评论都不能被读取。而在 InnoDB 上,这就不是一个问题,因为只有正被插入的那一行被锁住了,从而允许其它的服务器线程对其它各行继续进行操作。然而,在 MyISAM 中,表的读取更快,而数据维护和恢复工具更加完善。MySQL 表的存储架构的更多信息,参看 http://dev.mysql.com/tech-resources/articles/storage-engine/part_1.html。
为了测试表级锁是不是引起性能缓慢的原因,你可以通过在 MySQL 中检查状态变量 Table_locks_immediate 和 Table_locks_waited 的值来分析对锁竞争的程度。
mysql> SHOW STATUS LIKE ”Table%”;
+———————–+———+
| Variable_name | Value |
+———————–+———+
| Table_locks_immediate | 1151552 |
| Table_locks_waited | 15324 |
+———————–+———+
Table_locks_immediate 指的是能够立即获得表级锁的次数,而 Table_locks_waited 指的是不能立即获取表级锁而需要等待的次数。如果 Table_locks_waited 的值比较大的话,并且你遇到了性能问题,你可能希望将大表切分成小表;例如,你可以为一个定制模块创建一个专有的缓存(cache)表,或者通过其它方式 来减小表的大小,或者减小表级锁命令的频率。对于一些表,比如 cache_*、Watchdog 和 accesslog 表,减少表的大小的一种方式是减少数据的生命周期。使用 Drupal 的后台管理接口可以设置数据的生命周期。还有,确保每小时能够运行一次 cron,从而能够不断的清理这些表中的过期数据。
因为 Drupal 可以运行在不同的应用下,所以不可能一刀切的具体到某个表就应该使用某个表引擎。然而,一般情况下,适合转变为 InnoDB 的表有 cache、watchdog、sessions 和 accesslog 表。幸运的是,转变到 InnoDB 上非常简单:
ALTER TABLE accesslog TYPE=”InnoDB”;
当然,这一转变应该在站点下线并且已经为你的数据做好备份时进行,而且你也应该首先知道 InnoDB 表的不同的特性。
注意:由于 Drupal 对于 InnoDB 表仍然使用 LOCK TABLE 命令,你应该确保禁用了 MySQL 的自动提交模式,否则 MySQL 和 InnoDB 都将采用表级锁。更多信息参看 http://dev.mysql.com/doc/refman/5.1/en/lock-tables.html。
关于 MySQL 的性能调优,参看 http://www.day32.com/MySQL/ 的性能调优脚本,这里提供了调整 MySQL 服务器变量的建议。
Memcached(内存对象缓存)
当数据必须使用缓慢的设备比如一个硬盘进行交互时,系统通常会遇到一个性能问题。如果你能够为数据绕过这一操作,而且你能够承受的起数据的丢失(比 如 session 数据),那会怎么样呢?此时我们可以使用 memcached,这个系统将读写操作都放到内存中进行。与本章中介绍了其它解决方案相比,Memcached 更加复杂,而且更难设定。但是当你的系统需要在可升级性方面有所提高时还是值得考虑这一方案的。
Drupal 有一个内置的数据库缓存用来缓存页面、菜单和其它 Drupal 数据,而 MySQL 数据库也能够缓存常用查询,但是当你的数据库不堪重负时那会怎样?你可以再买一台数据库服务器,或者你也可以直接将数据存放在内存中而不是数据库中从而完 全减轻数据库的重负。Memcached 库(http://www.danga.com/memcached/)和 PECL Memcache PHP 扩展 (http://pecl.php.net/package/memcache)都是专门为你做这件事的工具。
Memcached 将任意数据都保存在随即存取的内存中,而且能够迅速的从中读取数据。使用这种方式比任何使用磁盘的方式在性能上都要好一些。Memcached 存储对象并使用唯一的键来引用对象。哪些对象应该被放到 memcached 中,这由程序员决定。对放到 Memcached 中的对象,Memcached 不知对象的类型和本质;在它眼中,一切都是一堆比特,带有键并且等待取回。
系统的简单性是它的优点。当为 Drupal 编写支持 memcached 的代码时,开发者可以决定对任何引起瓶颈的主要因素进行缓存。这可能是运行过于频繁的数据库查询的结果,比如路径查找,或者甚至更复杂的构造比如完全加载 的节点和分类词汇,这些都需要许多数据库查询和大量的 PHP 处理才能得到。
Memcached 的不足之处是它仅对于一小部分 Drupal 用户适用 — 他们的站点非常的流行以至于普通的硬件不能满足其需求 — 因此用于决定缓存什么和什么时候缓存的逻辑一直没有直接放到 Drupal(内核)中去。替代的是,任何想要构建一个异常快速的 Drupal 站点的人,必须要在 Drupal 内核打上一系列的补丁。这些补丁,以及 Drupal 的 memcache 模块和使用 PECL Memcache 接口的 Drupal 专有 API 可在 Drupal 的 Memcache 工程中找到(参看 http://drupal.org/project/memcache)。
