性能调优

在本章中,我们将会讨论如下话题:

  • 使用最佳实践
  • 加速session处理
  • 使用缓存依赖和chains
  • profiling一个Yii应用
  • Leveraging HTTP缓存
  • 合并和最小化资源
  • 在HHVM上运行Yii2

Yii是现有最快的框架中的一个。然后,当开发和部署一个应用时,有一些免费的额外性能和遵守最佳实践是有好处的。在本章中,你将会看到如何配置Yii来获取额外的性能。此外,你将会了解到一些最佳实践,可用于开发你的应用,能顺畅的运行,知道你有非常高的负载。

使用最佳实践

在本小节中,你将会看到如何配置Yii2,得到最好的性能,以及额外的创建响应式应用的原则。这些原则既是常用的也是Yii相关的。因此,我们将能使用这些原则,甚至不使用Yii2时也可以。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

  1. 更新你的PHP到最新的稳定版本。PHP的主发布版可能会带来非常大的性能提升。关掉调试模式,并设置为prod环境。这可以通过编辑web/index.php
  1. defined('YII_DEBUG') or define('YII_DEBUG', false);
  2. defined('YII_ENV') or define('YII_ENV', 'prod');

注意:在yii2-app-advanced应用框架中,你可以使用shell命令php init,以及选择生产环境,用于加载优化的index.php和配置文件。

  1. 激活cache组件:
  1. 'components' => [
  2. 'cache' => [
  3. 'class' => 'yii\caching\FileCache',
  4. ],
  5. ],

你可以使用任何缓存存储,不只是FileCache。此外,你可以注册多个缓存应用组件,并使用Yii::$app->cacheYii::$app->cache2来获取不同的数据类型:

  1. 'components' => [
  2. 'cache' => [
  3. 'class' => 'yii\caching\MemCache',
  4. 'useMemcached' => true,
  5. ],
  6. 'cache2' => [
  7. 'class' => 'yii\caching\FileCache',
  8. ],
  9. ],

这个框架默认在它自己的类中使用cache组件。

  1. db组件激活表schema缓存:
  1. return [
  2. // ...
  3. 'components' => [
  4. // ...
  5. 'cache' => [
  6. 'class' => 'yii\caching\FileCache',
  7. ],
  8. 'db' => [
  9. 'class' => 'yii\db\Connection',
  10. 'dsn' => 'mysql:host=localhost;dbname=mydatabase',
  11. 'username' => 'root',
  12. 'password' => '',
  13. 'enableSchemaCache' => true,
  14. // Optional. Default value is 3600 seconds
  15. 'schemaCacheDuration' => 3600,
  16. // Optional. Default value is 'cache'
  17. 'schemaCache' => 'cache',
  18. ],
  19. ],
  20. ];
  1. 使用纯数据,而不是ActiveRecord对象来列出元素的集合。
  1. $categoriesArray = Categories::find()->asArray()->all();
  1. foreach中使用each()而不是all()来获取大量的结果:
  1. foreach (Post::find()->each() as $post) {
  2. // ...
  3. }
  1. 因为Composer的autoloader被用于包含大部分的第三方类文件,你应该考虑通过如下命令优化它:
  1. composer dump-autoload -o

工作原理…

YII_DEBUG被设置为false时,Yii关闭了所有trace级别的日志,并使用较少的错误处理代码。此外,当你设置YII_ENVprod时,你的应用不会加载Yii和Debug面板模块。

设置schemaCachingDuration为一个以秒为单位的数字,允许Yii的ActiveRecord缓存数据的schema。对于生产环境,我们非常建议这样做,它会大幅提高ActiveRecord的性能。为了使它能功能,你需要正确的配置cache

  1. 'cache' => [
  2. 'class' => 'yii\cache\FileCache',
  3. ],

激活缓存对其它Yii组件也有正面的影响。例如,Yii路由或者urlManager从cache路由开始。

当然,你可以进入到一种情况,先前的设置对于显著的提升性能没有帮助。在大部分情况下,这意味着这个应用本身是一个瓶颈,你需要更多的硬件。

  • 服务端性能只是重点中的一部分:服务端性能只是所有能影响全局性能中的一个点。通过优化客户端,例如CSS、图像和Javascript文件,正确的缓存和减少HTTP请求的数量,可以有一个很好的可见的性能提升,即使是没有优化PHP代码。
  • 不使用Yii做事:有些事情如果不使用Yii可以很好的完成。例如,实时修改图像大小在一个独立的PHP脚本中进行会更快,可以避免额外的负载。
  • Active Record和Query Builder以及SQL对比:在对性能比较敏感的应用部分使用Query Builder和SQL。一般情况下,AR对于添加和编辑记录非常有用,因为它添加一个很方便的校验层,但当查询记录时并没有什么用。
  • 经常检查慢查询:如果开发者意外忘记给一个表格添加索引,当数据库经常被读取时,数据库就会成为性能瓶颈;反之亦然,如果添加太多的索引,而又要经常写数据。同样的事情会发生在选择不必要的数据以及不需要的JOINs。
  • 缓存或者保存重型过程的结果:如果你可以在每一个页面加载过程中避免运行一个重型过程,这最好了。例如,保存或者缓存解析的markdown文本,净化一次后(这是一个非常耗资源的过程),以后就是可以直接用于展示的HTML了。
  • 处理太多的过程:有时有太多的过程需要立即处理。它可以创建复杂的报告,或者只是简单的发送电子邮件(如果你的项目加载的很重)。在这种情况下,最好将它放入到队列中,然后使用cron或者其它指定的工具来处理。

参考

欲了解更多关于性能调优和缓存的信息,参考如下地址:

加速session处理

在PHP中原生的session处理在大部分情况下已经非常好了。但至少有两个可能原因,你希望改变session的处理方式:

  • 当使用多个服务器时,需要有统一的session存储。
  • 默认的PHP session使用文件,所以最大的性能瓶颈在磁盘I/O上。
  • 默认的PHP session是阻塞并发的session存储。在这个小节中,我们将会看到如何使用Yii做高效的session存储。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。并安装Memcache服务器和memcache PHP扩展。

如何做…

我们使用apache的ab工具对网站做压力测试。它是和apache二进制文件一起发布,所以如果正在使用apache,你将会在bin文件夹中找到它。

  1. 运行如下命令,并将网址替换成你的正在使用的网站的网址:
  1. ab -n 1000 -c 5 http://yii-book.app/index.php?r=site/contact

这将会发送1000次请求,一次发送5个,并会得到如下输出统计:

  1. This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
  2. Copyright 1996 Adam Twiss, Zeus Technology Ltd,
  3. http://www.zeustech.net/
  4. Licensed to The Apache Software Foundation,
  5. http://www.apache.org/
  6. ...
  7. Server Software: nginx
  8. Server Hostname: yii-book.app
  9. Server Port: 80
  10. Document Path: /index.php?r=site/contact
  11. Document Length: 14866 bytes
  12. Concurrency Level: 5
  13. Time taken for tests: 10.961 seconds
  14. Complete requests: 1000
  15. Failed requests: 0
  16. Total transferred: 15442000 bytes
  17. HTML transferred: 14866000 bytes
  18. Requests per second: 91.24 [#/sec] (mean)
  19. Time per request: 54.803 [ms] (mean)
  20. Time per request: 10.961 [ms] (mean, across all
  21. concurrent requests)
  22. Transfer rate: 1375.84 [Kbytes/sec] received
  23. Connection Times (ms)
  24. min mean[+/-sd] median max
  25. Connect: 0 0 0.0 0 0
  26. Processing: 18 55 324.9 29 4702
  27. Waiting: 15 41 255.1 24 4695
  28. Total: 18 55 324.9 29 4702

我们对每秒请求次数指标(requests-per-second,简称QPS)感兴趣。这个值意味着这个网站在并发数为5的情况下,每秒可以处理91.24次请求。

注意:注意调试并没有关闭,因为我们对修改session处理速度感兴趣。

  1. 现在添加如下代码到/config/web.php组件部分:
  1. 'session' => array(
  2. 'class' => 'yii\web\CacheSession',
  3. 'cache' => 'sessionCache',
  4. ),
  5. 'sessionCache' => array(
  6. 'class' => 'yii\caching\MemCache',
  7. ),
  1. 再次以相同的设置运行ab。这次,你应该能得到更好的结果。在我的例子中,QPS是139.07。这意味着Memcache,作为一个session处理器,相对于基于文件的session处理器提升了52%的性能。

注意:不要依赖于这里提供的精确的结果。它依赖于软件版本、设置和使用的硬件。经常尝试在你即将部署应用的环境中,运行所有的测试。

  1. 通过选择正确的session处理后端,你可以得到一个显著的性能提升。Yii支持更多的缓存后端out-of-the-box,包括WinCache、XCache和Zend Data Cache,它来自于Zend Server。而且,你可以实施你自己的缓存后端,来使用快速的noSQL存储,例如Redis。

工作原理…

默认情况下,Yii使用原生PHP session;这意味着大部分情况下使用文件系统。文件系统并不能高效的处理高并发请求。

Memcache或者其它平台在如下情况下,能很好的执行:

  1. 'session' => array(
  2. 'class' => 'yii\web\CacheSession',
  3. 'cache' => 'sessionCache',
  4. ),
  5. 'sessionCache' => array(
  6. 'class' => 'yii\caching\MemCache',
  7. ),

在先前的配置部分,我们在Yii中使用CacheSession作为一个session处理器。使用这个组件,我们可以委托session处理器为cache中指定的缓存组件。这次我们使用MemCache

当使用一个memcached后端,你应该考虑到这个事实,当使用这些解决方案时,当缓存达到最大存储容量时,应用用户可能丢失session。

注意:注意到,当为一个session使用一个缓存后端时,你不能依赖于一个session作为一个临时数据存储,因为在memcached中将不会有更多内存来存储更多数据。在这个例子中,只需要清理所有的数据,并清除其中的一部分。

如果你在使用多个服务器,你不能使用文件存储。没有办法来分享多个服务器之间的session数据。在memcached的例子中,这非常容易,因为它可以被多个服务器访问。

此外,对于分享session数据,你可以使用DbSession

  1. return [
  2. // ...
  3. 'components' => [
  4. 'session' => [
  5. 'class' => 'yii\web\DbSession',
  6. ],
  7. ],
  8. ];

现在,在你的数据库中创建一个张新表:

  1. CREATE TABLE session (
  2. id CHAR(40) NOT NULL PRIMARY KEY,
  3. expire INTEGER,
  4. data BLOB
  5. )

更多…

尽可能关闭session是一个好主意。如果你不想在当前的请求中在session中存储任何数据,你甚至可以在你的控制器动作一开始就关闭它。这样,在你的应用中即使是使用文件作为存储也是没关系的。

使用如下命令:

  1. Yii:$app->session->close();

参考

欲了解更多关于性能和缓存的信息,参考如下地址:

使用缓存依赖和chains

Yii支持需要缓存后端,但是使Yii缓存灵活的是依赖和依赖chaining支持。有一些情况,你不能简单的只缓存1个小时的数据,因为信息随时可能会边。

在这个小节中,我们将会学习如何缓存整个页面,并能在有更新时获取最新的数据。这个页面是一个仪表盘类型的,将会展示5个最新添加的文章,以及总数。

注意:注意一个操作不能被编辑 as it is added,但是一个文章可以。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

  1. config/web.php中激活缓存组件:
  1. return [
  2. // ...
  3. 'components' => [
  4. 'cache' => ['class' => 'yii\caching\FileCache',
  5. ],
  6. ],
  7. ];
  1. 设置一个新的数据库,并将它配置到config/db.php中:
  2. 运行如下migration:
  1. <?php
  2. use yii\db\Schema;
  3. use yii\db\Migration;
  4. class m160308_093233_create_example_tables extends Migration
  5. {
  6. public function up()
  7. {
  8. $tableOptions = null;
  9. if ($this->db->driverName === 'mysql') {
  10. $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
  11. }
  12. $this->createTable('{{%account}}', [
  13. 'id' => Schema::TYPE_PK,
  14. 'amount' => Schema::TYPE_DECIMAL . '(10,2) NOT NULL',
  15. ], $tableOptions);
  16. $this->createTable('{{%article}}', [
  17. 'id' => Schema::TYPE_PK,
  18. 'title' => Schema::TYPE_STRING . ' NOT NULL',
  19. 'text' => Schema::TYPE_TEXT . ' NOT NULL',
  20. ], $tableOptions);
  21. }
  22. public function down()
  23. {
  24. $this->dropTable('{{%article}}');
  25. $this->dropTable('{{%account}}');
  26. }
  27. }
  1. 使用Yii为account和article表生成模型。
  2. 创建protected/controllers/DashboardController.php
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Account;
  4. use app\models\Article;
  5. use yii\web\Controller;
  6. class DashboardController extends Controller
  7. {
  8. public function actionIndex()
  9. {
  10. $total = Account::find()->sum('amount');
  11. $articles = Article::find()->orderBy('id DESC')->limit(5)->all();
  12. return $this->render('index', array(
  13. 'total' => $total,
  14. 'articles' => $articles,
  15. ));
  16. }
  17. public function actionRandomOperation()
  18. {
  19. $rec = new Account();
  20. $rec->amount = rand(-1000, 1000);
  21. $rec->save();
  22. echo 'OK';
  23. }
  24. public function actionRandomArticle()
  25. {
  26. $n = rand(0, 1000);
  27. $article = new Article();
  28. $article->title = "Title #".$n;
  29. $article->text = "Text #".$n;
  30. $article->save();
  31. echo 'OK';
  32. }
  33. }
  1. 创建views/dashboard/index.php
  1. <?php
  2. use yii\helpers\Html;
  3. /* @var $this yii\web\View */
  4. /* @var $total int */
  5. /* @var $articles app\models\Article[] */
  6. ?>
  7. <h1>Total: <?= $total ?></h1>
  8. <h2>5 latest articles:</h2>
  9. <?php foreach($articles as $article): ?>
  10. <h3><?= Html::encode($article->title) ?></h3>
  11. <div><?= Html::encode($article->text) ?></div>
  12. <?php endforeach ?>
  1. 运行dashboard/random-operationdashboard/random-article几次,然后,运行dashboard/index你将会看到如下所示的截图:

性能调优 - 图1

  1. 在页面的底部,点击调试面板上数据库查询的数量:

性能调优 - 图2

看到一个查询列表:

性能调优 - 图3

如何做…

执行如下步骤:

  1. 我们需要修改控制器的代码:
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Account;
  4. use app\models\Article;
  5. use yii\caching\DbDependency;
  6. use yii\caching\TagDependency;
  7. use yii\web\Controller;
  8. class DashboardController extends Controller
  9. {
  10. public function behaviors()
  11. {
  12. return [
  13. 'pageCache' => [
  14. 'class' => 'yii\filters\PageCache',
  15. 'only' => ['index'],
  16. 'duration' => 24 * 3600 * 365, // 1 year
  17. 'dependency' => [
  18. 'class' => 'yii\caching\ChainedDependency',
  19. 'dependencies' => [
  20. new TagDependency(['tags' =>
  21. ['articles']]),
  22. new DbDependency(['sql' => 'SELECT MAX(id) FROM ' . Account::tableName()])
  23. ]
  24. ],
  25. ],
  26. ];
  27. }
  28. public function actionIndex()
  29. {
  30. $total = Account::find()->sum('amount');
  31. $articles = Article::find()->orderBy('id DESC')->limit(5)->all();
  32. return $this->render('index', array(
  33. 'total' => $total,
  34. 'articles' => $articles,
  35. ));
  36. }
  37. public function actionRandomOperation()
  38. {
  39. $rec = new Account();
  40. $rec->amount = rand(-1000, 1000);
  41. $rec->save();
  42. echo 'OK';
  43. }
  44. public function actionRandomArticle()
  45. {
  46. $n = rand(0, 1000);
  47. $article = new Article();
  48. $article->title = "Title #".$n;
  49. $article->text = "Text #".$n;
  50. $article->save();
  51. TagDependency::invalidate(\Yii::$app->cache,
  52. 'articles');
  53. echo 'OK';
  54. }
  55. }
  1. 完成了。现在,在加载dashboard/index几次以后,你将会看到只有1个查询,如下所示:

性能调优 - 图4

此外,尝试运行dashboard/random-operation或者dashboard/randomarticle,并刷新dashboard/index。数据将会改变:

性能调优 - 图5

工作原理…

为了修改最少的代码,并达到最高的性能,我们使用一个过滤器来作全页缓存:

  1. public function behaviors()
  2. {
  3. return [
  4. 'pageCache' => [
  5. 'class' => 'yii\filters\PageCache',
  6. 'only' => ['index'],
  7. 'duration' => 24 * 3600 * 365, // 1 year
  8. 'dependency' => [
  9. 'class' => 'yii\caching\ChainedDependency',
  10. 'dependencies' => [
  11. new TagDependency(['tags' => ['articles']]),
  12. new DbDependency(['sql' => 'SELECT MAX(id) FROM account'])
  13. ]
  14. ],
  15. ],
  16. ];
  17. }

先前的代码意味着我们在index动作中应用一个全页缓存。这个页面将会缓存1年,并且如果数据改变了,这个缓存将会刷新。因此,一般情况下,依赖工作如下:

  • 按依赖中的描述,第一次运行会获取最新的数据,保存以备之后调用,并更新缓存
  • 按依赖中的描述,获取最新的数据,获取保存的数据,然后比较两者
  • 如果相等,使用缓存的数据
  • 如果不相等,更新缓存,使用最新的数据,并保存最新依赖数据,以备以后调用

在我们的例子中,使用了两个类型的依赖——标签和DB。一个标签依赖使用自定义字符串标签标记数据,并检查它,来决定缓存是否无效,一个DB依赖使用SQL查询结果来达到相同的目的。

现在你可能有的问题是,“为什么有时候使用DB而有时候使用标签?”这是一个好问题。

使用DB依赖的目的是,替换重的计算,并选择一个轻的查询,尽可能获取少的数据。关于这种依赖最好的事情是,我们不需要在已有的代码中嵌入任何额外的逻辑。在我们的例子中,我们可以使用这种类型的依赖用于账户操作,但是不能用于文章,因为文章的内容会改变。因此,对于文章,我们设置一个全局标签,名叫文章,它表示我们可以手动调用来刷新文章缓存:

  1. TagDependency::invalidate(\Yii::$app->cache, 'articles');

参考

欲了解更多关于缓存的信息,并使用缓存依赖,参考http://www.yiiframework.com/doc-2.0/guide-caching-overview.html

使用Yii profiling一个应用

如果在部署一个Yii应用时,你使用了所有的最佳实践,但是你仍然得不到你想要的性能,很有可能是应用本身存在一些性能瓶颈。在处理这些性能瓶颈时最主要的原则是你不应该假设任何事请,并在尝试优化它之前去测试和profile代码。

在本小节中,我们将会尝试找出Yii2最小应用的性能瓶颈。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

  1. 设置数据库连接,并应用如下migration:
  1. <?php
  2. use yii\db\Migration;
  3. class m160308_093233_create_example_tables extends Migration
  4. {
  5. public function up()
  6. {
  7. $tableOptions = null;
  8. if ($this->db->driverName === 'mysql') {
  9. $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
  10. }
  11. $this->createTable('{{%category}}', [
  12. 'id' => $this->primaryKey(),
  13. 'name' => $this->string()->notNull(),
  14. ], $tableOptions);
  15. $this->createTable('{{%article}}', [
  16. 'id' => $this->primaryKey(),
  17. 'category_id' => $this->integer()->notNull(),
  18. 'title' => $this->string()->notNull(),
  19. 'text' => $this->text()->notNull(),
  20. ], $tableOptions);
  21. $this->createIndex('idx-article-category_id',
  22. '{{%article}}', 'category_id');
  23. $this->addForeignKey('fk-article-category_id',
  24. '{{%article}}', 'category_id', '{{%category}}', 'id');
  25. }
  26. public function down()
  27. {
  28. $this->dropTable('{{%article}}');
  29. $this->dropTable('{{%category}}');
  30. }
  31. }
  1. 在Yii中为每一个表生成模型。
  2. 写如下控制台命令:
  1. <?php
  2. namespace app\commands;
  3. use app\models\Article;
  4. use app\models\Category;
  5. use Faker\Factory;
  6. use yii\console\Controller;
  7. class DataController extends Controller
  8. {
  9. public function actionInit()
  10. {
  11. $db = \Yii::$app->db;
  12. $faker = Factory::create();
  13. $transaction = $db->beginTransaction();
  14. try {
  15. $categories = [];
  16. for ($id = 1; $id <= 100; $id++) {
  17. $categories[] = [
  18. 'id' => $id,
  19. 'name' => $faker->name,
  20. ];
  21. }
  22. $db->createCommand()->batchInsert(Category::tableName(), ['id', 'name'], $categories)->execute();
  23. $articles = [];
  24. for ($id = 1; $id <= 100; $id++) {
  25. $articles[] = [
  26. 'id' => $id,
  27. 'category_id' => $faker->numberBetween(1, 100),
  28. 'title' => $faker->text($maxNbChars = 100),
  29. 'text' => $faker->text($maxNbChars = 200),
  30. ];
  31. }
  32. $db->createCommand()
  33. ->batchInsert(Article::tableName(), ['id', 'category_id', 'title', 'text'], $articles)->execute();
  34. $transaction->commit();
  35. } catch (\Exception $e) {
  36. $transaction->rollBack();
  37. throw $e;
  38. }
  39. }
  40. }

并执行它:

  1. ./yii data/init
  1. 添加ArticleController类:
  1. <?php
  2. namespace app\controllers;
  3. use Yii;
  4. use app\models\Article;
  5. use yii\data\ActiveDataProvider;
  6. use yii\web\Controller;
  7. class ArticleController extends Controller
  8. {
  9. public function actionIndex()
  10. {
  11. $query = Article::find();
  12. $dataProvider = new ActiveDataProvider([
  13. 'query' => $query,
  14. ]);
  15. return $this->render('index', [
  16. 'dataProvider' => $dataProvider,
  17. ]);
  18. }
  19. }
  1. 添加views/article/index.php视图:
  1. <?php
  2. use yii\helpers\Html;
  3. use yii\widgets\ListView;
  4. /* @var $this yii\web\View */
  5. /* @var $dataProvider yii\data\ActiveDataProvider */
  6. $this->title = 'Articles';
  7. $this->params['breadcrumbs'][] = $this->title;
  8. ?>
  9. <div class="article-index">
  10. <h1><?= Html::encode($this->title) ?></h1>
  11. <?= ListView::widget([
  12. 'dataProvider' => $dataProvider,
  13. 'itemOptions' => ['class' => 'item'],
  14. 'itemView' => '_item',
  15. ]) ?>
  16. </div>

然后添加views/article/_item.php

  1. <?php
  2. use yii\helpers\Html;
  3. /* @var $this yii\web\View */
  4. /* @var $model app\models\Article */
  5. ?>
  6. <div class="panel panel-default">
  7. <div class="panel-heading"><?= Html::encode($model->title);
  8. ?></div>
  9. <div class="panel-body">
  10. Category: <?= Html::encode($model->category->name) ?>
  11. </div>
  12. </div>

如何做…

跟随如下步骤,profile基于Yii的应用:

  1. 打开文章页面:

性能调优 - 图6

  1. 打开views/article/index.php并在ListView小部件之前和之后添加profiler调用:
  1. <div class="article-index">
  2. <h1><?= Html::encode($this->title) ?></h1>
  3. <?php Yii::beginProfile('articles') ?>
  4. <?= ListView::widget([
  5. 'dataProvider' => $dataProvider,
  6. 'itemOptions' => ['class' => 'item'],
  7. 'itemView' => '_item',
  8. ]) ?>
  9. <?php Yii::endProfile('articles') ?>
  10. </div>

现在刷新这个页面。

  1. 展开页面底部的调试面板,点击timing badge(在我们的例子中是73ms):

性能调优 - 图7

现在检查Profiling报告:

性能调优 - 图8

我们可以看到我们的文章块花费了将近40ms。

  1. 打开我们的控制器,并为文章的category关系添加主动加载:
  1. class ArticleController extends Controller
  2. {
  3. public function actionIndex()
  4. {
  5. $query = Article::find()->with('category');
  6. $dataProvider = new ActiveDataProvider([
  7. 'query' => $query,
  8. ]);
  9. return $this->render('index', [
  10. 'dataProvider' => $dataProvider,
  11. ]);
  12. }
  13. }
  1. 回到网站,刷新页面,再次打开Profiling报告:

性能调优 - 图9

现在这个文章列表花费了将近25ms,因为应用使用主动加载做了更少的SQL查询。

工作原理…

你可以使用Yii::beginProfileYii::endProfile查看源代码的任何片段:

  1. Yii::beginProfile('articles');
  2. // ...
  3. Yii::endProfile('articles');

在执行过页面以后,你可以在调试模块的Profiling页面看到这个报告,有所有的执行时间。

此外,你可以使用嵌套profiling调用:

  1. Yii::beginProfile('outer');
  2. Yii::beginProfile('inner');
  3. // ...
  4. Yii::endProfile('inner');
  5. Yii::endProfile('outer');

注意:注意要正确的打开和关闭调用,以及正确命名block名称。如果你忘记调用Yii::endProfile,或者颠倒了Yii::endProfile('inner')Yii::endProfile('outer')的嵌套顺序,性能Profiling将不会工作。

参考

Leveraging HTTP缓存

不只是服务端的缓存,你通过设置HTTP头可以使用客户端缓存。

在这个小节中,我们将会讨论基于Last-ModifiedETag头的全页缓存。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

  1. 创建并运行migration:
  1. <?php
  2. use yii\db\Migration;
  3. class m160308_093233_create_example_tables extends Migration
  4. {
  5. public function up()
  6. {
  7. $this->createTable('{{%article}}', [
  8. 'id' => $this->primaryKey(),
  9. 'created_at' => $this->integer()->unsigned()->notNull(),
  10. 'updated_at' =>
  11. $this->integer()->unsigned()->notNull(),
  12. 'title' => $this->string()->notNull(),
  13. 'text' => $this->text()->notNull(),
  14. ]);
  15. }
  16. public function down()
  17. {
  18. $this->dropTable('{{%article}}');
  19. }
  20. }
  1. 创建Article模型:
  1. <?php
  2. namespace app\models;
  3. use Yii;
  4. use yii\behaviors\TimestampBehavior;
  5. use yii\db\ActiveRecord;
  6. class Article extends ActiveRecord
  7. {
  8. public static function tableName()
  9. {
  10. return '{{%article}}';
  11. }
  12. public function behaviors()
  13. {
  14. return [
  15. TimestampBehavior::className(),
  16. ];
  17. }
  18. }
  1. 创建带有如下动作的博客控制器:
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Article;
  4. use yii\web\Controller;
  5. use yii\web\NotFoundHttpException;
  6. class BlogController extends Controller
  7. {
  8. public function actionIndex()
  9. {
  10. $articles = Article::find()->orderBy(['id' =>
  11. SORT_DESC])->all();
  12. return $this->render('index', array(
  13. 'articles' => $articles,
  14. ));
  15. }
  16. public function actionView($id)
  17. {
  18. $article = $this->findModel($id);
  19. return $this->render('view', array(
  20. 'article' => $article,
  21. ));
  22. }
  23. public function actionCreate()
  24. {
  25. $n = rand(0, 1000);
  26. $article = new Article();
  27. $article->title = 'Title #' . $n;
  28. $article->text = 'Text #' . $n;
  29. $article->save();
  30. echo 'OK';
  31. }
  32. public function actionUpdate($id)
  33. {
  34. $article = $this->findModel($id);
  35. $n = rand(0, 1000);
  36. $article->title = 'Title #' . $n;
  37. $article->text = 'Text #' . $n;
  38. $article->save();
  39. echo 'OK';
  40. }
  41. private function findModel($id)
  42. {
  43. if (($model = Article::findOne($id)) !== null) {
  44. return $model;
  45. } else {
  46. throw new NotFoundHttpException('The requested page does not exist.');
  47. }
  48. }
  49. }
  1. 添加views/blog/index.php视图:
  1. <?php
  2. use yii\helpers\Html;
  3. $this->title = 'Articles';;
  4. $this->params['breadcrumbs'][] = $this->title;
  5. ?>
  6. <?php foreach($articles as $article): ?>
  7. <h3><?= Html::a(Html::encode($article->title), ['view',
  8. 'id' => $article->id]) ?></h3>
  9. <div>Created <?= Yii::$app->formatter->asDatetime($article->created_at) ?></div>
  10. <div>Updated <?= Yii::$app->formatter->asDatetime($article->updated_at) ?></div>
  11. <?php endforeach ?>
  1. 添加视图views/blog/view.php
  1. <?php
  2. use yii\helpers\Html;
  3. $this->title = $article->title;
  4. $this->params['breadcrumbs'][] = ['label' => 'Articles', 'url' => ['index']];
  5. $this->params['breadcrumbs'][] = $this->title;
  6. ?>
  7. <h1><?= Html::encode($article->title) ?></h1>
  8. <div>Created <?= Yii::$app->formatter->asDatetime($article->created_at) ?></div>
  9. <div>Updated <?= Yii::$app->formatter->asDatetime($article->updated_at) ?></div>
  10. <hr />
  11. <p><?= Yii::$app->formatter->asNtext($article->text) ?></p>

如何做…

执行如下步骤来leverage HTTP缓存:

  1. 访问http://yii-book.app/index.php?r=blog/create三次,来创建三个文章。
  2. 打开如下博客地址:

性能调优 - 图10

  1. 在你的浏览器中打开开发者控制台,每次刷新页面都可以看到200 OK的响应状态:

性能调优 - 图11

  1. 打开BlogController并附加如下行为:
  1. <?php
  2. class BlogController extends Controller
  3. {
  4. public function behaviors()
  5. {
  6. return [
  7. [
  8. 'class' => 'yii\filters\HttpCache',
  9. 'only' => ['index'],
  10. 'lastModified' => function ($action, $params) {
  11. return Article::find()->max('updated_at');
  12. },
  13. ],
  14. [
  15. 'class' => 'yii\filters\HttpCache',
  16. 'only' => ['view'],
  17. 'etagSeed' => function ($action, $params) {
  18. $article = $this->findModel(\Yii::$app->request->get('id'));
  19. return serialize([$article->title, $article->text]);
  20. },
  21. ],
  22. ];
  23. }
  24. // ...
  25. }
  1. 接下来,刷新页面几次,并检查服务器返回的是304 Not Modified,而不是200 OK

性能调优 - 图12

  1. 使用如下URL打开相关页面,更新相关文章:http://yiibook.app/index.php?r=blog/update
  2. 更新过博客页面以后,检查服务器首次返回的是200 OK,接着就是304 Not Modified,并确认你在页面上看到了新的更新时间:

性能调优 - 图13

  1. 从我们的页面上打开任何页面:

性能调优 - 图14

确认服务器首次返回的是200 OK,以及接下来的请求是304 Not Modified

工作原理…

在HTTP头的帮助下,你的浏览器用基于时间和基于内容的方法,来检查缓存响应内容的可用性。

上次修改时间

这个方法建议服务端必须返回每个文章的上次修改时间。在存储这个这个日期之后,我们的浏览器可以在接下来的每次请求中,在If-Modified-Since头设置这个值。

我们必须附加这个action过滤器到我们的控制器中,并指定lastModified回到:

  1. <?php
  2. class BlogController extends Controller
  3. {
  4. public function behaviors()
  5. {
  6. return [
  7. [
  8. 'class' => 'yii\filters\HttpCache',
  9. 'only' => ['index'],
  10. 'lastModified' => function ($action, $params) {
  11. return Article::find()->max('updated_at');
  12. },
  13. ],
  14. // ...
  15. ];
  16. }
  17. // ...
  18. }

\yii\filters\HttpCache类调用这个回调,并将返回值和$_SERVER['HTTP_IF_MODIFIED_SINCE']系统变量进行比较。如果这个文章没有改变,HttpCache将会发送一个轻量级的304响应头,而且不需要运行这个动作。

但是,如果文档更新了,这个缓存将会失效,服务端将会返回一个完整的响应。

性能调优 - 图15

作为Last-Modified的备选或者补充,你可以使用ETag

Entity标签

如果我们没有存储上次修改时间,我们可以使用自定义hash,它可以基于文章的内容生成。

例如,我们可以为我们的文档使用一个内容标题,来做了一个hash值:

  1. class BlogController extends Controller
  2. {
  3. public function behaviors()
  4. {
  5. return [
  6. [
  7. 'class' => 'yii\filters\HttpCache',
  8. 'only' => ['view'],
  9. 'etagSeed' => function ($action, $params) {
  10. $article =
  11. $this->findModel(\Yii::$app->request->get('id'));
  12. return serialize([$article->title,
  13. $article->text]);
  14. },
  15. ],
  16. ];
  17. }
  18. // ...
  19. }

这个HttpCache过滤器会将这个tag附加到服务器响应的ETag头变量上。

在存储了ETag之后,我们的浏览器会为接下来的每次请求附加它在If-None-Match头上。

如果这个文档仍然为改变,HttpCache将会发送一个轻量级的304响应头,并且不需要运行这个动作。

性能调优 - 图16

性能调优 - 图17

当这个缓存是合法的,我们的应用将会发送304 Not Modified响应头,而不是页面内容,而且不会重复运行控制器和动作。

参考

和并和最小化assets

如果你的页面包含很多CSS和/或Javascript文件,这个页面将会打开的比较慢,因为浏览器发送了大量的HTTP请求来下载每一个文件。为了减少请求和连接的数量,我们可以在生产模式下合并和压缩多个CSS/Javascript文件到一个或者非常少的几个文件,然后将这些压缩的文件包含在页面上。

准备

如何做…

跟随如下步骤,来和并和最小化资源:

  1. 打开你的应用index页面的源HTML代码。检查是否和如下结构比较相似:
  1. <!DOCTYPE html>
  2. <html lang="en-US">
  3. <head>
  4. ...
  5. <title>My Yii Application</title>
  6. <link href="/assets/9b3b2888/css/bootstrap.css"
  7. rel="stylesheet">
  8. <link href="/css/site.css" rel="stylesheet">
  9. </head>
  10. <body>
  11. ...
  12. <script src="/assets/25f82b8a/jquery.js"></script>
  13. <script src="/assets/f4307424/yii.js"></script>
  14. <script src="/assets/9b3b2888/js/bootstrap.js"></script>
  15. </body>
  16. </html>

这个页面包含三个Javascript文件。

  1. 打开config/console.php文件,并添加@webroot@web alias定义:
  1. <?php
  2. Yii::setAlias('@webroot', __DIR__ . '/../web');
  3. Yii::setAlias('@web', '/');
  1. 打开一个控制台,并运行如下命令:
  1. yii asset/template assets.php
  1. 打开生成的assets.php文件,并按如下配置:
  1. <?php
  2. return [
  3. 'jsCompressor' => 'java -jar compiler.jar --js {from}
  4. --js_output_file {to}',
  5. 'cssCompressor' => 'java -jar yuicompressor.jar --type css
  6. {from} -o {to}',
  7. 'bundles' => [
  8. 'app\assets\AppAsset',
  9. 'yii\bootstrap\BootstrapPluginAsset',
  10. ],
  11. 'targets' => [
  12. 'all' => [
  13. 'class' => 'yii\web\AssetBundle',
  14. 'basePath' => '@webroot/assets',
  15. 'baseUrl' => '@web/assets',
  16. 'js' => 'all-{hash}.js',
  17. 'css' => 'all-{hash}.css',
  18. ],
  19. ],
  20. 'assetManager' => [
  21. 'basePath' => '@webroot/assets',
  22. 'baseUrl' => '@web/assets',
  23. ],
  24. ];
  1. 运行合并命令yii asset assets.php config/assets-prod.php。如果成功,你就能得到带有如下配置的config/assets-prod.php文件:
  1. <?php
  2. return [
  3. 'all' => [
  4. 'class' => 'yii\\web\\AssetBundle',
  5. 'basePath' => '@webroot/assets',
  6. 'baseUrl' => '@web/assets',
  7. 'js' => [
  8. 'all-fe792d4766bead53e7a9d851adfc6ec2.js',
  9. ],
  10. 'css' => [
  11. 'all-37cfb42649f74eb0a4bfe0d0e715c420.css',
  12. ],
  13. ],
  14. 'yii\\web\\JqueryAsset' => [
  15. 'sourcePath' => null,
  16. 'js' => [],
  17. 'css' => [],
  18. 'depends' => [
  19. 'all',
  20. ],
  21. ],
  22. 'yii\\web\\YiiAsset' => [
  23. 'sourcePath' => null,
  24. 'js' => [],
  25. 'css' => [],
  26. 'depends' => [
  27. 'yii\\web\\JqueryAsset',
  28. 'all',
  29. ],
  30. ],
  31. 'yii\\bootstrap\\BootstrapAsset' => [
  32. 'sourcePath' => null,
  33. 'js' => [],
  34. 'css' => [],
  35. 'depends' => [
  36. 'all',
  37. ],
  38. ],
  39. 'app\\assets\\AppAsset' => [
  40. 'sourcePath' => null,
  41. 'js' => [],
  42. 'css' => [],
  43. 'depends' => [
  44. 'yii\\web\\YiiAsset',
  45. 'yii\\bootstrap\\BootstrapAsset',
  46. 'all',
  47. ],
  48. ],
  49. 'yii\\bootstrap\\BootstrapPluginAsset' => [
  50. 'sourcePath' => null,
  51. 'js' => [],
  52. 'css' => [],
  53. 'depends' => [
  54. 'yii\\web\\JqueryAsset',
  55. 'yii\\bootstrap\\BootstrapAsset',
  56. 'all',
  57. ],
  58. ],
  59. ];
  1. config/web.php文件中为assetManager组件添加配置:
  1. 'components' => [
  2. // ...
  3. 'assetManager' => [
  4. 'bundles' => YII_ENV_PROD ? require(__DIR__ . '/assets-prod.php') : [],
  5. ],
  6. ],
  1. web/index.php打开生产模式:
  1. defined('YII_ENV') or define('YII_ENV', 'prod');
  1. 在你的浏览器中刷新这个页面,就能看到HTML代码。现在应该有一条包含我们压缩文件的一行:
  1. <!DOCTYPE html>
  2. <html lang="en-US">
  3. <head>
  4. ...
  5. <title>My Yii Application</title>
  6. <link href="/assets/
  7. all-37cfb42649f74eb0a4bfe0d0e715c420.css" rel="stylesheet">
  8. </head>
  9. <body>
  10. ...
  11. <script src="/assets/
  12. all-fe792d4766bead53e7a9d851adfc6ec2.js"></script>
  13. </body>
  14. </html>

工作原理…

首先,我们的页面有包含文件的集合:

  1. <link href="/assets/9b3b2888/css/bootstrap.css" rel="stylesheet">
  2. <link href="/css/site.css" rel="stylesheet">
  3. ...
  4. <script src="/assets/25f82b8a/jquery.js"></script>
  5. <script src="/assets/f4307424/yii.js"></script>
  6. <script src="/assets/9b3b2888/js/bootstrap.js"></script>

接下来,我们生成assets.php配置文件,并制定需要压缩的东西:

  1. 'bundles' => [
  2. 'app\assets\AppAsset',
  3. 'yii\bootstrap\BootstrapPluginAsset',
  4. ],

注意:我们可以指定所有中间资源包,例如yii\web\JqueryAssetyii\web\YiiAsset,但是这些资源已经作为AppAssetBootstrapPluginAsset的依赖被指定了,这个压缩命令会自动解析所有的依赖。

AssetManager发布所有的资源到web/assets经典子文件夹中,在发布过以后,它会运行压缩器,将所有的CSS和JS文件压缩到all-{hash}.jsall-{hash}.css文件中。

检查这个CSS文件是否包含其它带有相对路径的资源,例如bootstrap.css文件中:

  1. @font-face {
  2. font-family: 'Glyphicons Halflings';
  3. src: url('../fonts/glyphicons-halflings-regular.eot');
  4. }

如果是这样的话,在和并的文件中,我们的压缩器会修改所有的相对路径:

  1. @font-face{
  2. font-family: 'Glyphicons Halflings';
  3. src: url('9b3b2888/fonts/glyphicons-halflings-regular.eot');
  4. }

处理过以后,我们得到了assets-prod.php文件,里边有assetManager组件的配置。它定义了新的virtual资源作为原始包的干净拷贝:

  1. return [
  2. 'all' => [
  3. 'class' => 'yii\\web\\AssetBundle',
  4. 'basePath' => '@webroot/assets',
  5. 'baseUrl' => '@web/assets',
  6. 'js' => [
  7. 'all-fe792d4766bead53e7a9d851adfc6ec2.js',
  8. ],
  9. 'css' => [
  10. 'all-37cfb42649f74eb0a4bfe0d0e715c420.css',
  11. ],
  12. ],
  13. 'yii\\web\\JqueryAsset' => [
  14. 'sourcePath' => null,
  15. 'js' => [],
  16. 'css' => [],
  17. 'depends' => [
  18. 'all',
  19. ],
  20. ],
  21. // ...
  22. ]

现在,我们可以require这个配置到config/web.php文件中:

  1. 'components' => [
  2. // ...
  3. 'assetManager' => [
  4. 'bundles' => require(__DIR__ . '/assets-prod.php'),
  5. ],
  6. ],

或者,我们可以只在生产环境中require这个文件:

  1. 'components' => [
  2. // ...
  3. 'assetManager' => [
  4. 'bundles' => YII_ENV_PROD ? require(__DIR__ . '/assets-prod.php') : [],
  5. ],
  6. ],

注意:不要忘记在更新了原始资源后重新生成所有的压缩和合并文件。

参考

在HHVM上运行Yii2

HipHop Virtual Machine (HHVM)是一个处理虚拟机器,来自Facebook,基于just-in-time(JIT)编译。HHVM将PHP代码翻译成功中间的HipHop bytecode (HHBC),并动态翻译PHP代码为机器码,它可以被优化并原生的执行。

准备

按照官方指南http://www.yiiframework.com/doc-2.0/guide-start-installation.html的描述,使用Composer包管理器创建一个新的yii2-app-basic应用。

如何做…

根据如下步骤,在HHVM上运行Yii:

  1. 安装Apache2或者Nginx web服务器:
  2. 跟随这个指南https://docs.hhvm.com/hhvm/installation/introduction,在Linux或者Mac上安装HHVM。例如在Ubuntu上,你需要运行如下命令:
  1. sudo apt-get install software-properties-common
  2. sudo apt-key adv --recv-keys --keyserver
  3. hkp://keyserver.ubuntu.com:80 0x5a16e7281be7a449
  4. sudo add-apt-repository "deb http://dl.hhvm.com/ubuntu
  5. $(lsb_release -sc) main"
  6. sudo apt-get update
  7. sudo apt-get install hhvm
  8. After installing, you will see the following tips in your
  9. terminal:
  10. ****************************************************************
  11. ****
  12. * HHVM is installed.
  13. *
  14. * Running PHP web scripts with HHVM is done by having your
  15. * webserver talk to HHVM over FastCGI. Install nginx or Apache,
  16. * and then:
  17. * $ sudo /usr/share/hhvm/install_fastcgi.sh
  18. * $ sudo /etc/init.d/hhvm restart
  19. * (if using nginx) $ sudo /etc/init.d/nginx restart
  20. * (if using apache) $ sudo /etc/init.d/apache restart
  21. *
  22. * Detailed FastCGI directions are online at:
  23. * https://github.com/facebook/hhvm/wiki/FastCGI
  24. *
  25. * If you're using HHVM to run web scripts, you probably want it
  26. * to start at boot:
  27. * $ sudo update-rc.d hhvm defaults
  28. *
  29. * Running command-line scripts with HHVM requires no special
  30. setup:
  31. * $ hhvm whatever.php
  32. *
  33. * You can use HHVM for /usr/bin/php even if you have php-cli
  34. * installed:
  35. * $ sudo /usr/bin/update-alternatives \
  36. * --install /usr/bin/php php /usr/bin/hhvm 60
  37. ****************************************************************
  38. ****
  1. 尝试为你的网站手动启动内置服务器:
  1. cd web
  2. hhvm -m server -p 8080

在你的浏览器中打开localhost:8080

性能调优 - 图18

现在你就可以使用HHVM来开发你的项目了。

  1. 如果你使用Nginx或者Apache2服务器,HHVM会在/etc/nginx/etc/apache2目录中自动创建它自己的配置文件。在Nginx的例子中,它会创建/etc/nginx/hhvm.conf模板,来包含你的项目的配置。例如,我们来创建一个新的虚拟托管名叫yii-book-hhvm.app
  1. server {
  2. listen 127.0.0.1:80;
  3. server_name .yii-book-hhvm.app;
  4. root /var/www/yii-book-hhvm.app/web;
  5. charset utf-8;
  6. index index.php index.html index.htm;
  7. include /etc/nginx/hhvm.conf;
  8. }

添加hostname到你的/etc/hosts

  1. 127.0.0.1 yii-book-hhvm.app

现在重启这个Nginx服务器:

  1. sudo service nginx restart

最后,在浏览器中打开这个新的host:

性能调优 - 图19

你的服务器被成功设置。

工作原理…

你可以在fastcgi模式下使用HHVM作为PHP处理的备选项。默认情况下,它会监听9000端口。你可以在/etc/hhvm/server.ini文件中修改fastcgi进程的这个默认端口:

  1. hhvm.server.port = 9000

/etc/hhvm/php.ini文件中配置这个指定的PHP选项:

参考

欲了解更多关于安装HHVM的信息,参考如下地址:

欲了解更多关于HHVM使用方法的信息,参考https://docs.hhvm.com/hhvm/