论坛首页 编程语言技术论坛

【原】Zend Framework Performance Guide (性能指南) 第一部分

浏览 5696 次
精华帖 (0) :: 良好帖 (2) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2010-01-31   最后修改:2010-01-31
PHP

 

最近正在进行针对Zend Framework(本文简称为ZF)的调研工作,本文翻译于ZF最新的官方手册附录E - Zend Framework Performance Guide。水平有限,多有缺点,请多指正。

原文链接:http://framework.zend.com/manual/en/performance.html

我的个人博客链接:http://becoder.cn/?p=119

E.2. Class Loading

任何对ZF应用进行性能分析都会意识到,由于Class loading所导致的性能问题是非常严重的。从大量的class文件载入到那些class-file不能一对一关联的plugin组件。各种各样的include_once和require_once充斥着我们的应用程序,从而导致了的大量的潜在问题,本章节我们提供了一些常见问题的具体解决方案。

E.2.1.  如何优化 Include_path ?

include_path的定义会Class loading的速度略有影响,通常我们可以通过以下四种方式来提高速度:

  1. 使用绝对路径引用(包括基于绝对路径的相对引用)
  2. 减少path的定义数量.
  3. 让ZF的include_path优先级越高越好
  4. 使当前目录在include_path中处于最后的位置,也就是’.’在最后(默认是最先)

E.2.1.1.  使用相对路径

绝对还是相对,这看上去并不像是一个大问题。它们之间的区别在于,如果我们使用相对引用,那么意味着程序无法使用PHP的realpath cache来快速访问文件目录,而绝对路径可以通过cache来提高我们的访问速度。

有两种非常方便的办法来实现绝对引用。第一种方式是我们在php.ini,httpd.conf, .htaccess或者其他的定义方式来通过硬编码的方式来使用绝对引用。其次我们可以在程序中通过PHP的realpath函数来帮助设置include_path为绝对引用。如下例:

$paths = array(
     realpath(dirname(__FILE__) . '/../library'),
     '.'
);
set_include_path(implode(PATH_SEPARATOR, $paths);

或者使用相对路径---当然,它还是相对于某个绝对路径(其实,它依然是绝对的)

define('APPLICATION_PATH', realpath(dirname(__FILE__)));

$paths = array(

    APPLICATION_PATH . '/../library'),

    '.',

);

set_include_path(implode(PATH_SEPARATOR, $paths);
 

E.2.1.2.  减少引用目录数量

在应用程序的解释过程中,扫描引用目录的顺序是由目录在include_path中定义的顺序所决定的。这样就意味着如果所需要引入文件的目录在include_path中定义的越优先,那么扫描引用目录的损耗就会越低。因此,我们可以通过减少引用目录的数量来提高速度。只需要谨慎得校对定义的include_path内容,去除那些无用的目录。

另一个优化措施是合并引用目录,例如,ZF采用了PEAR的命名规范,所以,当系统中需要引用PEAR类库或者其他采用PEAR命名规范的类库时,可以尝试着将这些类库放到同一个引用目录中。

E.2.1.3.  尽量提前引用 ZF 类库所在的 path

从第二点我们可以推断出第三点优化方案,把ZF所在目录定义在include_path最先位置,从而保证应用程序可以在第一次目录扫描中获取到ZF的相应文件。

E.2.1.4.  降低或排除当前目录引用

在很多出现include_path定义的例子中,都会在include_path中定义当前目录(‘.’),而且往往出现在include_path中的第一位(php.ini的默认值)。这的确为引用同一目录下文件提供了极大便利。但是这导致了应用程序会首先遍历当前目录来寻找文件,而在遵循ZF标准的应用程序中,大多数情况下我们根本不需要在当前目录下进行遍历。所以,请将’.’放到include_path的最后一位。

E.2.2.  如何去除不必要的 require 引入 ?

为了优化文件引入过程,PHP提供了延迟加载的优化措施,使用延迟加载,应用程序可以只在需要使用类的时候再进行类的加载,比如对象的初始化,静态方法调用以及静态变量或常量的引用时。PHP通过自动加载机制来实现延迟加载,我们可以通过定义一个或者多个回调函数来实现类名到文件名之间的映射。

然而,在我们的类库的代码中,依然存在并大量使用require_once调用(ZF目前正是如此)。所以现在我们需要停止使用这些require_once从而在最大限度上发挥autoloader所能带来的性能提升。

E.2.2.1.  通过 find sed 命令来停用 require_once

一个简单的办法是,我们可以通过UNIX的sed工具和find命令来注释掉每个require_once的调用。可以试着运行下面的命令来实现(%代表了shell提示符):

% cd path/to/ZendFramework/library

% find . -name '*.php' -not -wholename '*/Loader/Autoloader.php' \

  -not -wholename '*/Application.php' -print0 | \

  xargs -0 sed --regexp-extended --in-place 's/(require_once)/\/\/ \1/g'
 

仅仅一行命令就遍历所有的php文件同时将每个’require_once’替代为’//require_once’。(我们必须保持在Zend_Application 和Zend_Loader_Autoloader文件中的require_once调用,不然会导致程序错误)

我们可以把这个处理命令加到自动编译或者部署流程中,从而提高我们的生产环境下的产品性能。同时需要注意的是,我们必需要将自动加载的过程加到public/index.php文件中,如下例:

require_once 'Zend/Loader/Autoloader.php';

Zend_Loader_Autoloader::getInstance();
 

E.2.3.  如何优化插件加载 ?

大部分组件都支持插件扩展,开发者可以通过自己创建类来和组件协同工作,或者重写已经存在的标准ZF自身插件。插件可以提高框架的灵活性,但代价是,插件载入也会导致性能问题。

插件加载器允许我们注册类前缀与路径的映射,以及在非标准路径下定义类。每个类前缀都可以对应多个路径,运行时,插件加载器会遍历所有的类前缀以及其所对应的所有的路径,检查路径下是否存在可供加载的文件。加载后系统会判断是否包含需要引用的类。我们可以想象一下,这个过程会导致多少次针对物理文件系统的I/O操作。

将你的结论乘以使用的PluginLoader的插件数量,我们可以得出关于插件性能问题的最终结论。在本文发布之时,使用PluginLoader包括以下组件:

  • Zend_Controller_Action_HelperBroker: helpers
  • Zend_Dojo: view helpers, form elements and decorators
  • Zend_File_Transfer: adapters
  • Zend_Filter_Inflector: filters (used by the ViewRenderer action helper and Zend_Layout)
  • Zend_Filter_Input: filters and validators
  • Zend_Form: elements, validators, filters, decorators, captcha and file transfer adapters
  • Zend_Paginator: adapters
  • Zend_View: helpers, filters

现在,我们应该如何来减少如此多的调用次数呢?

E.2.3.1.  使用 PluginLoader 引用文件缓存

ZF 1.7版本中为PluginLoader增加了引用文件缓存机制,这个功能将include_once调用保存到一个文件中,并在bootstrap过程中引用这个文件。虽然这样在我们代码中增加了多余的include_once调用,但同时能保证PluginLoader能够尽早的返回结果,减少引入过程的资源消耗。

E.3. Zend_Db 性能相关

作为数据库抽象层,Zend_DB类库封装了常见SQL操作;Zend_Db_Table则是数据表访问接口,为表级数据库操作提供常用抽象。在这些抽象和所带来的便利背后,有时也会导致额外的性能开销

E.3.1. 如何降低由于获取元数据所导致的性能开销?

为了简便起见,同时在开发过程中可以和库表结构保持变更同步,Zend_Db_Table在内部通过以下方式进行操作,即首次使用库表元数据时,从数据库获得元数据同时保存在成员变量以备之后操作使用。这种方式对性能存在较大影响,无论您底层使用何种数据库,都会导致系统瓶颈的出现。

好消息是,我们有一些方法来解决这个问题:

E.3.1.1. 使用元数据缓存

Zend_Db_Table可以通过使用Zend_Cache来缓存库表元数据。显然这要比每次从数据库中加载元数据要更快更实惠。

可以通过这个链接获得更多关于元数据缓存的内容 Zend_Db_Table documentation includes information on metadata caching .

E.3.1.2. 将元数据硬编码

从ZF 1.7开始,Zend_Db_Table支持可以将库表元数据硬编码到代码中。这属于一种”高级”选项:它的优缺点都是显而易见的,首先可以对性能带来毋庸置疑的提升,同时也意味着提高维护成本,你必须保证你的库表更改与代码中的设定同步。

E.3.2.  Zend_Db_Select所生成的SQL语句总是无法准确得命中库表索引,如何提高?

总体来说,Zend_Db_Select很好得完成了它的设计初衷,但是,当面对一些复杂的SQL查询时,如多表关联或者子查询时,它往往会体现得力不从心。

E.3.2.1. 直接使用自定义优化过的SQL语句

这个问题的正解是自己来写SQL。我们并不需要Zend_Db_Select为我们完成所有的事情。自己设计并且调优出的SQL语句是最正确最值得信赖的选择。

通过执行EXPLAIN命令来分析你的查询(Mysql only?),同时进行反复的测试直到你可以确认语句可以最大限度得发挥你的索引优势,然后将语句定义为成员变量或者类常量来使用它。

如果语句需要传递多个占位符参数,可以通过使用vsprintf() and array_walk()函数来将参数注入到SQL语句中。如下例:

// $adapter is the DB adapter. In Zend_Db_Table, retrieve
// it using $this->getAdapter().
$sql = vsprintf(
    self::SELECT_FOO,
    array_walk($values, array($adapter, 'quoteInto'))
);
   发表时间:2010-01-31  

为了方便大家阅读,一楼附上官方原文内容:

E.2. Class Loading

E.2. Class Loading

Anyone who ever performs profiling of a Zend Framework application will immediately recognize that class loading is relatively expensive in Zend Framework. Between the sheer number of class files that need to be loaded for many components, to the use of plugins that do not have a 1:1 relationship between their class name and the file system, the various calls to include_once() and require_once() can be problematic. This chapter intends to provide some concrete solutions to these issues.

E.2.1. How can I optimize my include_path?

One trivial optimization you can do to increase the speed of class loading is to pay careful attention to your include_path. In particular, you should do four things: use absolute paths (or paths relative to absolute paths), reduce the number of include paths you define, have your Zend Framework include_path as early as possible, and only include the current directory path at the end of your include_path.

E.2.1.1. Use absolute paths

While this may seem a micro-optimization, the fact is that if you don't, you'll get very little benefit from PHP's realpath cache, and as a result, opcode caching will not perform nearly as you may expect.

There are two easy ways to ensure this. First, you can hardcode the paths in your php.ini, httpd.conf, or .htaccess. Second, you can use PHP's realpath() function when setting your include_path:

$paths = array(
    realpath(dirname(__FILE__) . '/../library'),
    '.',
);
set_include_path(implode(PATH_SEPARATOR, $paths);

You can use relative paths -- so long as they are relative to an absolute path:

define('APPLICATION_PATH', realpath(dirname(__FILE__)));
$paths = array(
    APPLICATION_PATH . '/../library'),
    '.',
);
set_include_path(implode(PATH_SEPARATOR, $paths);

However, even so, it's typically a trivial task to simply pass the path to realpath().

E.2.1.2. Reduce the number of include paths you define

Include paths are scanned in the order in which they appear in the include_path. Obviously, this means that you'll get a result faster if the file is found on the first scan rather than the last. Thus, a rather obvious enhancement is to simply reduce the number of paths in your include_path to only what you need. Look through each include_path you've defined, and determine if you actually have any functionality in that path that is used in your application; if not, remove it.

Another optimization is to combine paths. For instance, Zend Framework follows PEAR naming conventions; thus, if you are using PEAR libraries (or libraries from another framework or component library that follows PEAR CS), try to put all of these libraries on the same include_path. This can often be achieved by something as simple as symlinking one or more libraries into a common directory.

E.2.1.3. Define your Zend Framework include_path as early as possible

Continuing from the previous suggestion, another obvious optimization is to define your Zend Framework include_path as early as possible in your include_path. In most cases, it should be the first path in the list. This ensures that files included from Zend Framework are found on the first scan.

E.2.1.4. Define the current directory last, or not at all

Most include_path examples show using the current directory, or '.'. This is convenient for ensuring that scripts in the same directory as the file requiring them can be loaded. However, these same examples typically show this path item as the first item in the include_path -- which means that the current directory tree is always scanned first. In most cases, with Zend Framework applications, this is not desired, and the path may be safely pushed to the last item in the list.

Example E.1. Example: Optimized include_path

Let's put all of these suggestions together. Our assumption will be that you are using one or more PEAR libraries in conjunction with Zend Framework -- perhaps the PHPUnit and Archive_Tar libraries -- and that you occasionally need to include files relative to the current file.

First, we'll create a library directory in our project. Inside that directory, we'll symlink our Zend Framework's library/Zend directory, as well as the necessary directories from our PEAR installation:

library
    Archive/
    PEAR/
    PHPUnit/
    Zend/

This allows us to add our own library code if necessary, while keeping shared libraries intact.

Next, we'll opt to create our include_path programmatically within our public/index.php file. This allows us to move our code around on the file system, without needing to edit the include_path every time.

We'll borrow ideas from each of the suggestions above: we'll use absolute paths, as determined using realpath(); we'll include Zend Framework's include path early; we've already consolidated include_paths; and we'll put the current directory as the last path. In fact, we're doing really well here -- we're going to end up with only two paths.

$paths = array(
    realpath(dirname(__FILE__) . '/../library'),
    '.'
);
set_include_path(implode(PATH_SEPARATOR, $paths));

E.2.2. How can I eliminate unnecessary require_once statements?

Lazy loading is an optimization technique designed to push the expensive operation of loading a class file until the last possible moment -- i.e., when instantiating an object of that class, calling a static class method, or referencing a class constant or static property. PHP supports this via autoloading, which allows you to define one or more callbacks to execute in order to map a class name to a file.

However, most benefits you may reap from autoloading are negated if your library code is still performing require_once() calls -- which is precisely the case with Zend Framework. So, the question is: how can you eliminate those require_once() calls in order to maximize autoloader performance?

E.2.2.1. Strip require_once calls with find and sed

An easy way to strip require_once() calls is to use the UNIX utilities 'find' and 'sed' in conjunction to comment out each call. Try executing the following statements (where '%' indicates the shell prompt):

% cd path/to/ZendFramework/library
% find . -name '*.php' -not -wholename '*/Loader/Autoloader.php' \
  -not -wholename '*/Application.php' -print0 | \
  xargs -0 sed --regexp-extended --in-place 's/(require_once)/\/\/ \1/g'

This one-liner (broken into two lines for readability) iterates through each PHP file and tells it to replace each instance of 'require_once' with '// require_once', effectively commenting out each such statement. (It selectively keeps require_once() calls within Zend_Application and Zend_Loader_Autoloader, as these classes will fail without them.)

This command could be added to an automated build or release process trivially, helping boost performance in your production application. It should be noted, however, that if you use this technique, you must utilize autoloading; you can do that from your "public/index.php" file with the following code:

require_once 'Zend/Loader/Autoloader.php';
Zend_Loader_Autoloader::getInstance();

E.2.3. How can I speed up plugin loading?

Many components have plugins, which allow you to create your own classes to utilize with the component, as well as to override existing, standard plugins shipped with Zend Framework. This provides important flexibility to the framework, but at a price: plugin loading is a fairly expensive task.

The plugin loader allows you to register class prefix / path pairs, allowing you to specify class files in non-standard paths. Each prefix can have multiple paths associated with it. Internally, the plugin loader loops through each prefix, and then through each path attached to it, testing to see if the file exists and is readable on that path. It then loads it, and tests to see that the class it is looking for is available. As you might imagine, this can lead to many stat calls on the file system.

Multiply this by the number of components that use the PluginLoader, and you get an idea of the scope of this issue. At the time of this writing, the following components made use of the PluginLoader:

  • Zend_Controller_Action_HelperBroker: helpers

  • Zend_Dojo: view helpers, form elements and decorators

  • Zend_File_Transfer: adapters

  • Zend_Filter_Inflector: filters (used by the ViewRenderer action helper and Zend_Layout)

  • Zend_Filter_Input: filters and validators

  • Zend_Form: elements, validators, filters, decorators, captcha and file transfer adapters

  • Zend_Paginator: adapters

  • Zend_View: helpers, filters

How can you reduce the number of such calls made?

E.2.3.1. Use the PluginLoader include file cache

Zend Framework 1.7.0 adds an include file cache to the PluginLoader. This functionality writes "include_once()" calls to a file, which you can then include in your bootstrap. While this introduces extra include_once() calls to your code, it also ensures that the PluginLoader returns as early as possible.

The PluginLoader documentation includes a complete example of its use.

 

E.3. Zend_Db Performance

E.3. Zend_Db Performance

Zend_Db is a database abstraction layer, and is intended to provide a common API for SQL operations. Zend_Db_Table is a Table Data Gateway, intended to abstract common table-level database operations. Due to their abstract nature and the "magic" they do under the hood to perform their operations, they can sometimes introduce performance overhead.

E.3.1. How can I reduce overhead introduced by Zend_Db_Table for retrieving table metadata?

In order to keep usage as simple as possible, and also to support constantly changing schemas during development, Zend_Db_Table does some magic under the hood: on first use, it fetches the table schema and stores it within object members. This operation is typically expensive, regardless of the database -- which can contribute to bottlenecks in production.

Fortunately, there are techniques for improving the situation.

E.3.1.1. Use the metadata cache

Zend_Db_Table can optionally utilize Zend_Cache to cache table metadata. This is typically faster to access and less expensive than fetching the metadata from the database itself.

The Zend_Db_Table documentation includes information on metadata caching.

E.3.1.2. Hardcode your metadata in the table definition

As of 1.7.0, Zend_Db_Table also provides support for hardcoding metadata in the table definition. This is an advanced use case, and should only be used when you know the table schema is unlikely to change, or that you're able to keep the definitions up-to-date.

E.3.2. SQL generated with Zend_Db_Select s not hitting my indexes; how can I make it better?

Zend_Db_Select is relatively good at its job. However, if you are performing complex queries requiring joins or sub-selects, it can often be fairly naive.

E.3.2.1. Write your own tuned SQL

The only real answer is to write your own SQL; Zend_Db does not require the usage of Zend_Db_Select, so providing your own, tuned SQL select statements is a perfectly legitimate approach,

Run EXPLAIN on your queries, and test a variety of approaches until you can reliably hit your indices in the most performant way -- and then hardcode the SQL as a class property or constant.

If the SQL requires variable arguments, provide placeholders in the SQL, and utilize a combination of vsprintf() and array_walk() to inject the values into the SQL:

// $adapter is the DB adapter. In Zend_Db_Table, retrieve
// it using $this->getAdapter().
$sql = vsprintf(
    self::SELECT_FOO,
    array_walk($values, array($adapter, 'quoteInto'))
);
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics