tourze/swoole-yii2

此包最新版本(v0.1.3)没有可用的许可信息。

v0.1.3 2016-11-10 09:11 UTC

This package is auto-updated.

Last update: 2024-09-19 05:20:11 UTC


README

项目说明

公司有一些项目使用Yii2进行开发,运行在php-fpm模式下,高并发下的性能问题很严重。安装了apc等opcode缓存后也无法很好地解决这些问题,于是尝试使用swoole来解决。

在这个项目之前,已经尝试过使用 https://github.com/bixuehujin/blink

Blink的优点在于简单,缺点也在于太简单。如果是新项目,并且需求清晰只用于接口,使用Blink会更好。

如果是继续使用Yii2这类全栈框架,并希望有明显的性能提升,可以尝试使用本项目来实现。

本项目目前还是在试验项目,请在正式环境中谨慎使用。

如果对项目有任何建议,或者使用过程中遇到问题,可以发issue给我。

项目的意义

对于现在的Web项目来说,特别是对PHP项目来说,遇到性能问题,第一时间想到的解决方案就是加机器。

不是说加机器不能解决问题,如果问题是多点部署可以解决的话,加机器也是一个很粗暴和容易实现的方案。

只是我认为,加机器这种途径,是属于运维范畴的解决方法。对于开发者来说,依然需要想办法来提升代码质量和效率。

适用人群

首先使用者应该对swoole有一定理解。建议使用本项目前,先阅读 http://wiki.swoole.com/ 中的说明。

其次,对于Yii2的核心概念和实现,也应该有一定掌握。

已经完成的工作

  • Http Server 的实现
  • Request 组件的兼容处理
  • Response 组件的兼容处理
  • Session 组件的兼容处理
  • 增加异步任务助手类
  • Debug 模块的兼容处理
  • Container 支持实例持久化
  • Db 组件的自动重连
  • 压力测试文档

进行中的工作

  • swoole 任务投递的优化
  • 增加单元测试
  • swoole 管理脚本的完善

使用方法

首先执行 composer require tourze/swoole-yii2

下面的配置描述,基本上就是基于 https://github.com/yiisoft/yii2-app-advanced 这个官方 DEMO 来说明的。建议在阅读前先大概了解下这个项目。

console配置

swoole 的实现,全部都是基于 CLI 的,项目的所有管理相关也是使用CLI实现的。

这里第一步我们应该先配置好一个 Yii2 的 console 服务。

console/config/main.php 中加入类似以下的代码

    'id' => 'app-console',
    'controllerNamespace' => 'console\controllers',
    'controllerMap' => [
        // 在下面指定定义command控制器
        'swoole' => \tourze\swoole\yii2\commands\SwooleController::className(),
    ],

此时执行 ./yii,应该可以在底部看到 swoole 相关命令。

frontend/backend配置

我们建议 frontend 部分使用 swoole 来运行,backend 部分依然使用已有的 php-fpm 模式来运行。

使用本项目,frontend 和 backend 的运行方式会有所变更。

在以前的方式中,我们会在入口文件 include 所有配置,然后 new Application 使系统运行起来。在现在的新方式中,我们的配置会在服务运行起来时就加载到内存,节省了上面加载配置的时间。

我们需要在 console/config/params.php 中加入类似以下的代码

<?php
return [
    'swooleHttp' => [
        'frontend' => [
            'host' => '127.0.0.1',
            'port' => '6677',
            'root' => realpath(__DIR__ . '/../../frontend/web'),
            // 在这里定义一些常用的可以常驻与内存的组件
            'persistClasses' => [
                'dmstr\web\AdminLteAsset',
                'dmstr\widgets\Alert',
                'kartik\grid\ActionColumn',
                'kartik\grid\ActionColumnAsset',
                'kartik\grid\BooleanColumn',
                'kartik\grid\CheckboxColumn',
                'kartik\grid\CheckboxColumnAsset',
                'kartik\grid\DataColumn',
                'kartik\grid\GridView',
                'kartik\grid\GridViewAsset',
                'kartik\grid\GridExportAsset',
                'kartik\grid\GridResizeColumnsAsset',
            ],
            // bootstrap文件, 只会引入一次
            'bootstrapFile' => [
                __DIR__ . '/../../common/config/aliases.php',
                __DIR__ . '/../../admin/config/aliases.php',
            ],
            // Yii的配置文件, 只会引入一次
            'configFile' => [
                __DIR__ . '/../../common/config/main.php',
                __DIR__ . '/../../frontend/config/main.php'
            ],
            // 有一些模块比较特殊, 无法实现Refreshable接口, 此时唯有在这里指定他的类名
            'bootstrapRefresh' => [
                'xxx\backend\Bootstrap',
            ],
            // 配置参考 https://www.kancloud.cn/admins/swoole/201155
            'server' => [
                'worker_num' => 20,
                'max_request' => 10000,
                'task_worker_num' => 50,  // 任务进程数
                'buffer_output_size' => 16 * 1024 * 1024, // 该参数可选, 如果你的业务需要输出大文件(如巨型html或导出大文件), 具体参考 http://wiki.swoole.com/wiki/page/440.html
            ],
        ],
    ],
];

配置好后,我们执行 ./yii swoole/http frontend,就可以启动 swoole 服务器了。

兼容思路

在确定使用swoole来优化Yii2性能前,我们先确定下目标

  1. Yii2项目能平滑从php-fpm环境迁移到swoole上
  2. 代码尽量少侵入性,最好做好零侵入性
  3. 性能要有明显的提升(5倍以上)

逐点分析

  • 对于第1点,如果项目之前运行在 php-fpm 模式中,现在只要在 swoole 中模拟实现一次 php-fpm 的转发,是不是就可以解决平滑迁移的问题?
  • 对于第2点,Yii2 的组件系统,已经给我们提供了一个很好的解决方案。但是有一个问题需要考虑,就是Yii中多少使用到了全局变量,部分类使用了 static private 属性,在组件兼容时,为了避免踩坑,可能需要将组件实现都review一次,这里的时间成本不好控制
  • 对于第3点,我们可以先分析Yii2在执行时,性能卡在哪里。直观上来说,Yii2的项目,性能应该只卡在文件IO和DI部分。但从实际业务来说,也可能会卡在网络IO或其他复杂的业务逻辑上。所以也需要为这些问题提供一定解决方法才可以真实提升性能。

就上面的分析之后,我尝试了三种方式来实现Yii2在swoole上的兼容

模拟PHP-FPM,直接执行PHP文件

这种实现思路最简单,流程大概是

  1. 获取header信息,将header信息和环境变量直接写入 $_SERVER 等全局变量中去
  2. 直接include当前请求的文件

这种方式有一个很明显的优点,那就是实现简单。只需要跑起一个HttpServer,然后就可以直接使用了,而且通用性较强,大部分PHP项目都可以直接跑。

但是缺点也十分明显,这种方法只可以节省部分文件IO(如类的自动加载部分),但是对于具体业务逻辑的实现,没有节省到任何时间。

以Yii2为例,按照这种模式,每次有请求进入,打包好 $_SERVER 信息,直接加载 index.php 即可,然后在入口文件里面,依然会继续初始化 Application,再执行一大堆组件初始化操作。而这些大部分可以复用的组件,在当前请求结束后(也就是 swoole 的当前 Request 请求结束后)会自动销毁。

这种方案,有反复的创建/销毁对象带来的开销,而且每次请求都需要重新加载缺省文件,带来一个没必要的文件读操作,所以不一定是好方案。

唯一 Application 实例方式

上面的第一个方案,是最早时尝试的,目标有点贪心,希望能在兼容 Yii2 时还能直接兼容其他 PHP 项目。不过实践后发现目标定得有点大了。

第二种方式的目标是只兼容 Yii2,思路是:在 swoole 的 workerStart 时就创建一个 Yii2 应用对象,然后每次请求进入后,直接 handleRequest。

这种方式有什么问题?直观上来看没什么大问题,减少了缺省文件的载入IO,减少了重复创建对象和销毁对象的开销。

但是有一个很严重的问题,就是 Yii2 的上下文混乱问题。

举个例子,按照这种思路,Application 在 worker 内是唯一的,那么 Response 组件也应该是唯一的。

假如第一个请求输出头部信息

Content-Type: application/json
Access-Control-Allow-Origin: *

第二个请求在执行时,期望输出

Content-Type:text/html; charset=utf-8

但实际输出的header信息却是

Content-Type:text/html; charset=utf-8
Access-Control-Allow-Origin: *

因为 Response 组件是唯一的,所以上一个返回的 headers 会一直存在在内存中,第二个请求只会覆盖上一次请求的同名头部信息,然后将常驻的头部信息全部输出!

类似的上下文问题还有 \yii\base\Widget::$counter\yii\base\Widget::$stack,等等。

所以这种方式,效率提升比第一种方式要明显,但是带来的问题也更多。

实例复制方式(最终选择)

这种方式是在第二种方式的基础上实现的。主要思路是

  1. 每个worker进程都创建一个Application对象
  2. 每个请求处理时,克隆一个Application对象
  3. 将一些不需要重复构建的组件,同样克隆到刚复制出来的Application对象
  4. 然后在这个复制出来的对象中运行原有逻辑

这种方案的优点是减少了主要组件的构建开销,同时整个应用的执行流程也清晰很多了。组件的上下文问题变成了可以控制的复制与否问题。

现在本项目就是使用这种方式来实现的。

上面的思路,很多并不只是针对Yii2,使用同样的思路,同样可以顺利改造其他现代化框架,如Symfony。

关于上面几种实现方式,如果你有更好的方法,我们可以一起沟通。

压力测试结果