nginx+swoole+yii2 vs nginx+php-fpm+yii2 简单的性能对比

1.本地环境的搭建

环境:

  • wsl,Ubuntu版本 18.04 LTS
  • 安装docker,安装docker-compose
  • 接口测试是在局域网环境测试

注:cpu是四核,Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz,空余内存12G. 但是由于本机运行了大量的服务,所以参考价值不大。
两种运行环境是在同一台电脑下做的对比,且运行其中一个环境会先停掉另外一个,不存在相互影响的情况。
php都开启了opcache。

1.1 docker+nginx+swoole +yii2

具体配置文件:https://github.com/10xjzheng/SwooleVsPhpfpm/tree/master/swoole

直接上docker-compose 配置

version: '3.4'
services:
    web:
        image: nginx:latest
        volumes:
            - ./nginx/config/nginx.conf:/etc/nginx/nginx.conf:ro
            - ./nginx/config/qmyx.conf:/etc/nginx/conf.d/qmyx.conf:ro
            #- "./nginx/ssl:/etc/ssl"
            - "/mnt/d/wamp/www/qmyx-base:/usr/share/nginx/html/qmyx-base" #需要修改为你项目的目录
        ports:
            - "80:80"  # api http
            - "443:443" # api https
        restart: always
        depends_on:
            - php
    php:
        image: 10xjzheng/swoole:latest
        volumes:
            - "/mnt/d/wamp/www/qmyx-base:/home/web" #需要修改为你项目的目录
        ports:
            - "8888:80"
        command: php /home/web/yii swoole-backend/start #需要修改为你项目的目录
        restart: always

swoole镜像是自己build的,dockerfile如下:

FROM php:7.4

ADD sources.list /etc/apt/sources.list

# install modules : GD iconv
RUN apt-get update && apt-get install -y \
        procps \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        openssl \
        libssh-dev \
        libpcre3 \
        libpcre3-dev \
        libnghttp2-dev \
        libhiredis-dev \
        libonig-dev \
        curl \
        wget \
        zip \
        unzip \
        git && \
        apt-get autoremove && apt-get clean
# install php pdo_mysql opcache
# WARNING: Disable opcache-cli if you run you php
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \
    docker-php-ext-install \
    iconv \
    gd \
    pdo_mysql \
    mysqli \
    iconv \
    mbstring \
    json \
    opcache \
    sockets \
    pcntl && \
    echo "opcache.enable_cli=1" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini

#install redis
RUN pecl install redis && docker-php-ext-enable redis

# install composer
ENV COMPOSER_ALLOW_SUPERUSER 1
RUN curl -sS https://getcomposer.org/installer | php && \
    mv composer.phar /usr/local/bin/composer && \
    composer self-update --clean-backups

# install swoole
#TIP: it always get last stable version of swoole coroutine.
RUN cd /root && \
    curl -o /tmp/swoole-releases https://github.com/swoole/swoole-src/releases -L && \
    cat /tmp/swoole-releases | grep 'href=".*archive.*.tar.gz"' | head -1 | \
    awk -F '"' ' {print "curl -o /tmp/swoole.tar.gz https://github.com"$2" -L" > "/tmp/swoole.download"}' && \
    sh /tmp/swoole.download && \
    tar zxvf /tmp/swoole.tar.gz && cd swoole-src* && \
    phpize && \
    ./configure \
    --enable-coroutine \
    --enable-openssl  \
    --enable-http2  \
    --enable-async-redis \
    --enable-mysqlnd && \
    make && make install && \
    docker-php-ext-enable swoole && \
    echo "swoole.fast_serialize=On" >> /usr/local/etc/php/conf.d/docker-php-ext-swoole-serialize.ini && \
    rm -rf /tmp/*

# set China timezone
RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo 'Asia/Shanghai' > /etc/timezone && \
    echo "[Date]\ndate.timezone=Asia/Shanghai" > /usr/local/etc/php/conf.d/timezone.ini

nginx配置如下:

server {
    proxy_ignore_client_abort on;
    set $web /usr/share/nginx/html/qmyx-base/backend/web;
    root $web;
    server_name my.swoole.com;

    location ~* .(ico|gif|bmp|jpg|jpeg|png|swf|js|css|mp3)$ {
        root  $web;
    }

    location / {
        proxy_http_version 1.1;
        proxy_set_header Connection "keep-alive";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host http://my.swoole.com;
        proxy_pass http://php:80;
    }
 }

1.2 docker+nginx+php-fpm+yii2

具体配置文件:https://github.com/10xjzheng/SwooleVsPhpfpm/tree/master/php-fpm
直接上docker-compose 配置

version: '3.4'
services:
    web:
        image: nginx:latest
        volumes:
            - ./nginx/config/nginx.conf:/etc/nginx/nginx.conf:ro
            - ./nginx/config/qmyx.conf:/etc/nginx/conf.d/qmyx.conf:ro
            #- "./nginx/ssl:/etc/ssl"
            - /mnt/d/wamp/www/qmyx-base:/usr/share/nginx/html/qmyx-base:rw #需要修改为你项目的目录
        ports:
            - "80:80"
        restart: always
        depends_on:
            - php
    php:
        image: 10xjzheng/php-fpm:latest
        volumes:
            - /mnt/d/wamp/www/qmyx-base:/usr/share/nginx/html/qmyx-base:rw #需要修改为你项目的目录
        ports:
            - "9000:9000"
        restart: always

php-fpm镜像也是自己build的,跟swoole镜像区别只在于镜像源swoole用的是php7.4,php-fpm用的是php-fpm7.4,然后swoole镜像装了swoole后者没有。
dockerfile:

FROM php:7.4-fpm

ADD sources.list /etc/apt/sources.list

# install modules : GD iconv
RUN apt-get update && apt-get install -y \
        procps \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        openssl \
        libssh-dev \
        libpcre3 \
        libpcre3-dev \
        libnghttp2-dev \
        libhiredis-dev \
        libonig-dev \
        curl \
        wget \
        zip \
        unzip \
        git && \
        apt-get autoremove && apt-get clean
# install php pdo_mysql opcache
# WARNING: Disable opcache-cli if you run you php
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && \
    docker-php-ext-install \
    iconv \
    gd \
    pdo_mysql \
    mysqli \
    iconv \
    mbstring \
    json \
    opcache \
    sockets \
    pcntl && \
    echo "opcache.enable_cli=1" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini

#install redis
RUN pecl install redis && docker-php-ext-enable redis

# install composer
ENV COMPOSER_ALLOW_SUPERUSER 1
RUN curl -sS https://getcomposer.org/installer | php && \
    mv composer.phar /usr/local/bin/composer && \
    composer self-update --clean-backups

# set China timezone
RUN /bin/cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo 'Asia/Shanghai' > /etc/timezone && \
    echo "[Date]\ndate.timezone=Asia/Shanghai" > /usr/local/etc/php/conf.d/timezone.ini

1.3 压测工具wrk的安装

  • 安装:依次执行下列命令:
git clone https://github.com/wg/wrk.git
cd wrk
make
cp wrk /usr/local/bin
  • 使用方法: wrk <选项> <被测HTTP服务的URL>
  Options:                                            
    -c, --connections <N>  跟服务器建立并保持的TCP连接数量  
    -d, --duration    <T>  压测时间           
    -t, --threads     <N>  使用多少个线程进行压测   

    -s, --script      <S>  指定Lua脚本路径       
    -H, --header      <H>  为每一个HTTP请求添加HTTP头      
        --latency          在压测结束后,打印延迟统计信息   
        --timeout     <T>  超时时间     
    -v, --version          打印正在使用的wrk的详细版本信息

  <N>代表数字参数,支持国际单位 (1k, 1M, 1G)
  <T>代表时间参数,支持时间单位 (2s, 2m, 2h)
  • 示例
wrk -t 8 -c 200 -d 30s  --latency http://www.5lmh.com
Running 30s test @ http://www.5lmh.com (压测时间30s)
  8 threads and 200 connections (共8个测试线程,200个连接)
  Thread Stats   Avg      Stdev     Max   +/- Stdev
              (平均值) (标准差)(最大值)(正负一个标准差所占比例)
    Latency    46.67ms  215.38ms   1.67s    95.59%
    (延迟)
    Req/Sec     7.91k     1.15k   10.26k    70.77%
    (处理中的请求数)
  Latency Distribution (延迟分布)
     50%    2.93ms
     75%    3.78ms
     90%    4.73ms
     99%    1.35s (99分位的延迟)
  1790465 requests in 30.01s, 684.08MB read (30.01秒内共处理完成了1790465个请求,读取了684.08MB数据)
Requests/sec:  59658.29 (平均每秒处理完成59658.29个请求)
Transfer/sec:     22.79MB (平均每秒读取数据22.79MB)

2.压测实验

2.1 YII2测试代码

yii2框架使用高级版本。执行下面命令:

php composer.phar create-project yiisoft/yii2-app-advanced advanced

或者直接下载再解压:
https://github.com/yiisoft/yii2/releases/download/2.0.32/yii-advanced-app-2.0.32.tgz

然后安装yii2-swoole:

composer require "feehi/yii2-swoole"

具体说明文档见:https://github.com/liufee/yii2-swoole

根据上述文档修改配置:

'controllerMap'=>[
        'swoole-backend' => [
            'class' => feehi\console\SwooleController::class,
            'rootDir' => str_replace('console/config', '', __DIR__ ),//yii2项目根路径
            'app' => 'backend',
            'host' => '0.0.0.0',
            'port' => 80,
            'web' => 'web',//默认为web。rootDir app web目的是拼接yii2的根目录,如果你的应用为basic,那么app为空即可。
            'debug' => true,//默认开启debug,上线应置为false
            'env' => 'dev',//默认为dev,上线应置为prod
            'swooleConfig' => [
               'reactor_num' => 4, //4核
                'worker_num' => 10, //10个进程
                'daemonize' => false,
                'log_file' => __DIR__ . '/../../backend/runtime/logs/swoole.log',
                'log_level' => 0,
                'pid_file' => __DIR__ . '/../../backend/runtime/server.pid',
            ],
        ]
    ],

测试代码分为两种:

  • 1.没有db操作的,直接返回信息
  • 2.有db操作

2.2 我们先来测试没有db操作的

  • 代码如下:
class BrokerController extends Controller
{

    /**
     * Displays homepage.
     * @return array
     */
    public function actionIndex()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;
        return ['code' => 0, 'message' => 'success'];
    }
}
  • 先运行swoole环境
docker-compose up  -d

监控一下进程的cpu和内存:

docker stats
image.png

swoole启用了10个worker进程,使用了59M内存,cpu基本没占用。

执行压测命令,即3个线程1000个链接,持续3s。

wrk -t 3 -c 1000 -d 3s --latency http://my.swoole.com/broker/index

cpu瞬间飙升,内存也飙升:


image.png

image.png

看看测试报告:


image.png

qps是每秒80个左右,还有不少timeout,不知道是什么原因。
延迟分布的话,居然要1.9s左右,这只是个返回json数据的接口,晕死。

  • php-fpm环境
    为了保持和swoole一样数量的worker进程,我把配置改成以下配置:


    image.png

    同swoole环境,先启动:

docker-compose up  -d

监控一下进程的cpu和内存:

docker stats

启动后,占用的内存和CPU如下图:


image.png

看似内存占用会少一点。

然后还是执行一下压测命令,和swoole保持一致:

wrk -t 3 -c 1000 -d 3s --latency http://my.swoole.com/broker/index

可以看到CPU同样暴涨,但内存的情况却好很多:


image.png

image.png

再看看测试报告:


image.png

从测试报告来看,从延迟分布来看swoole相当好一点,只是没那么明显,但是从qps来看,swoole要好很多,swoole是80 requests/sec, 而php-fpm只有60 requests/sec。

  • 结果分析
  1. 关于swoole在高并发情况下,所占内存会高出很多,甚至达到1g以上,还伴随压测有一些timeout的情况的问题的探索。
    我写了压测过程中php进程占用内存的日志,grep查询了一下,如下图:


    image.png

    代码是:

SwooleLogger::flushLog(posix_getpid(). '--' . ' after mem:'.$m2.'MB');

即记录了当前pid的进程占用的php内存。
可以看到swoole进程在高并发情况下,内存会暴涨到120M以上。
随后用docker logs CID(容器ID) 也可以验证我的想法:

image.png

这是导致有一定数量的timeout的原因。

为什么会这样子呢?因为swoole是常驻进程,不断的handle请求,然后用的又是协程,即没个请求占用2M内存,那么同时处理100个请求就是200M,如果压测时间缩短,有可能10个worker进程有可能同时达到顶峰,2G。当然在我们这里有些请求处理完内存会被释放,所以并没直接去到2g,但我们能捕获到达到1g以上的情况。而php-fpm是每一个接口一个进程跑完就释放,所以内存不会这么暴涨。

2.关于php-fpm的3s的时间段内处理请求数量少并有那么多timeout的问题。
docker logs CID(容器ID) 看看nginx的容器,可以看到在大量200请求里面还夹杂着不少499:

image.png

499 / ClientClosed Request
An Nginx HTTP server extension. This codeis introduced to log the case when the connection is closed by
client whileHTTP server is processing its request, making server unable to send the HTTP header back

即php-fpm响应太慢,nginx直接closed了请求。
这是为什么php-fpm 在3s内完成的请求比swoole慢的原因,swoole使用了协程,对于网络IO是很有效率的,同一时间段内能接受更多请求也合理。

  1. 结论:
    swoole在并发情况下的qps要比php-fpm好很多,但在内存方面需要优化。

2.3 我们再来测试有db操作的

需要自己先配置mysql的链接。
yii 测试代码如下:

<?php
namespace backend\controllers;

use backend\entities\UserEntity;
use yii\web\Controller;
use Yii;
use yii\web\Response;

/**
 * Site controller
 */
class BrokerController extends Controller
{

    /**
     * Displays homepage.
     *
     * @return string
     */
    public function actionIndex()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;
        /** @var UserEntity  $broker */
        $broker = UserEntity::find()
            ->where(['mobile_tel' => '15220165155'])
            ->select('user_id, user_name,mobile_tel')
            ->limit(1)->asArray()->one();

        $bank = UserEntity::getDb()->createCommand("select * from user_bank where user_id = '{$broker['b_regbrokerId']}'")
            ->queryOne();
        return array_merge($broker, $bank);
    }
}

运行了2个SQL语句。

  • 直接压测swoole,先把并发请求降下来
 wrk -t 5 -c 500 -d 5s --latency http://my.swoole.com/broker/index
image.png

5s内能处理65个requests, qps是12.5+ Requests/sec。

  • 在压测php-fpm
 wrk -t 5 -c 500 -d 5s --latency http://my.swoole.com/broker/index
image.png

5s内能处理60个requests, qps是12+ Requests/sec。

多次压测结果,swoole的qps和单位时间内处理的请求还是相对好一点,从延迟发布来看,swoole的表现也相对好一点。

3. 结论

  1. swoole在并发情况下性能还是要好一点,但在我这里测试并不明显。可能是因为yii2框架的原因,也可能是环境的原因。毕竟不是独立的服务器;
  2. swoole并发情况下的内存问题需要有好的解决方式,一个是可以调大php的限制内存,另外一个是对yii框架运行时占用的内存做优化。