简介

Nginx Unit 是由 Nginx 团队开发的又一力作,可能没有人比他们更了解 Nginx 已经 Unit 如何平衡性能和可用性。

没错,Unit 最重要的特性就是 Flexibility ,你可以使用 RESTful API 动态的来修改 Unit 的运行配置。在更新配置时,只要你清楚的知道自己在做什么的话,可以保证服务的零中断。

最主要的是它实现了 SAPI,可以取代 PHP-FPM 在 PHP 中重要的位置,在网上看到几年前的测评文章,对比 PHP-FPM 有不少性能优势。

所以我打算使用 Nginx Unit + PHP 8.1,在做一组性能测试对比。

构建运行环境

我这里采用 Docker 来构建和部署 Unit 服务,在 Dockerfile 中我安装了 MongoDB 扩展和 PHPRedis 扩展,均采用最新版本。

Dockerfile

FROM php:8.1-cli as BUILDER

LABEL maintainer="NGINX Docker Maintainers <docker-maint@nginx.com>"

RUN set -ex \
    && apt-get update \
    && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates mercurial build-essential libssl-dev libpcre2-dev \
    && mkdir -p /usr/lib/unit/modules /usr/lib/unit/debug-modules \
    && hg clone https://hg.nginx.org/unit \
    && cd unit \
    && hg up 1.26.1 \
    && NCPU="$(getconf _NPROCESSORS_ONLN)" \
    && DEB_HOST_MULTIARCH="$(dpkg-architecture -q DEB_HOST_MULTIARCH)" \
    && CC_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_CFLAGS_MAINT_APPEND="-Wp,-D_FORTIFY_SOURCE=2 -fPIC" dpkg-buildflags --get CFLAGS)" \
    && LD_OPT="$(DEB_BUILD_MAINT_OPTIONS="hardening=+all,-pie" DEB_LDFLAGS_MAINT_APPEND="-Wl,--as-needed -pie" dpkg-buildflags --get LDFLAGS)" \
    && CONFIGURE_ARGS="--prefix=/usr \
                --state=/var/lib/unit \
                --control=unix:/var/run/control.unit.sock \
                --pid=/var/run/unit.pid \
                --log=/var/log/unit.log \
                --tmp=/var/tmp \
                --user=unit \
                --group=unit \
                --openssl \
                --libdir=/usr/lib/$DEB_HOST_MULTIARCH" \
    && ./configure $CONFIGURE_ARGS --cc-opt="$CC_OPT" --ld-opt="$LD_OPT" --modules=/usr/lib/unit/debug-modules --debug \
    && make -j $NCPU unitd \
    && install -pm755 build/unitd /usr/sbin/unitd-debug \
    && make clean \
    && ./configure $CONFIGURE_ARGS --cc-opt="$CC_OPT" --ld-opt="$LD_OPT" --modules=/usr/lib/unit/modules \
    && make -j $NCPU unitd \
    && install -pm755 build/unitd /usr/sbin/unitd \
    && make clean \
    && ./configure $CONFIGURE_ARGS --cc-opt="$CC_OPT" --modules=/usr/lib/unit/debug-modules --debug \
    && ./configure php \
    && make -j $NCPU php-install \
    && make clean \
    && ./configure $CONFIGURE_ARGS --cc-opt="$CC_OPT" --modules=/usr/lib/unit/modules \
    && ./configure php \
    && make -j $NCPU php-install \
    && ldd /usr/sbin/unitd | awk '/=>/{print $(NF-1)}' | while read n; do dpkg-query -S $n; done | sed 's/^\([^:]\+\):.*$/\1/' | sort | uniq > /requirements.apt

FROM php:8.1-cli
COPY docker-entrypoint.sh /usr/local/bin/
COPY --from=BUILDER /usr/sbin/unitd /usr/sbin/unitd
COPY --from=BUILDER /usr/sbin/unitd-debug /usr/sbin/unitd-debug
COPY --from=BUILDER /usr/lib/unit/ /usr/lib/unit/
COPY --from=BUILDER /requirements.apt /requirements.apt
RUN ldconfig
RUN set -x \
    && mkdir -p /var/lib/unit/ \
    && mkdir /docker-entrypoint.d/ \
    && addgroup --system unit \
    && adduser \
         --system \
         --disabled-login \
         --ingroup unit \
         --no-create-home \
         --home /nonexistent \
         --gecos "unit user" \
         --shell /bin/false \
         unit \
    && apt-get update \
    && apt-get upgrade -y \
    && apt-get --no-install-recommends --no-install-suggests -y install git libzip-dev zip openssl libssl-dev libcurl4-openssl-dev curl $(cat /requirements.apt) \
    && docker-php-ext-install zip \
    && pecl install lzf \
    && pecl install igbinary \
    && pecl install redis \
    && pecl install mongodb \
    && docker-php-ext-enable lzf igbinary redis mongodb \
    && docker-php-ext-install pcntl \
    && docker-php-ext-install pdo_mysql \
    && docker-php-ext-install opcache \
    && docker-php-ext-install exif \
    && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
    && rm -f /requirements.apt \
    && rm /var/log/lastlog /var/log/faillog \
    && ln -sf /dev/stdout /var/log/unit.log

# Install Composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \
    EXPECTED_CHECKSUM="$(php -r 'copy("https://composer.github.io/installer.sig", "php://stdout");')" && \
    php -r "if (hash_file('sha384', 'composer-setup.php') === '$EXPECTED_CHECKSUM') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" && \
    php composer-setup.php && \
    php -r "unlink('composer-setup.php');" && \
    mv composer.phar /usr/local/bin/composer && \
    chmod +x /usr/local/bin/composer

# Configure locale.
ARG LOCALE=POSIX
ENV LC_ALL ${LOCALE}

STOPSIGNAL SIGTERM

ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]

CMD ["unitd", "--no-daemon", "--control", "unix:/var/run/control.unit.sock"]

docker-compose.yml

version: "3.8"

services:
  unit:
  image: unit/php:laravel
  build:
    context: .
    dockerfile: Dockerfile
  working_dir: /usr/wwwroot/projects
  restart: always
  hostname: unit
  container_name: unit
  ports:
    - 80:80
    - 443:443
  networks:
    - services
  volumes:
    - ./projects:/usr/wwwroot/projects:rw
    - ./services/php/8.1/etc:/usr/local/etc
networks:
  services:
    name: services
    ipam:
      driver: default
      config:
        - subnet: 10.0.8.0/24

projects/phpinfo/index.php

<?php
phpinfo();

打包镜像

完成配置文件后的目录结构如下:

$ tree -L 3
.
├── Dockerfile
├── docker-compose.yml
├── docker-entrypoint.sh
├── php
│   └── etc
│       ├── pear.conf
│       └── php
└── projects
    └── phpinfo
        └── index.php

15 directories, 12 files

我已经将项目放到了 Github 上,需要的可以自行克隆。

$ docker-compose up -d

配置 Unit

{
    "listeners": {
        "*:80": {
            "pass": "routes"
        }
    },
    "routes": [
        {
            "match": {
                "uri": "!/index.php"
            },
            "action": {
                "share": "/usr/wwwroot/projects/phpinfo$uri",
                "fallback": {
                    "pass": "applications/laravel"
                }
            }
        }
    ],
    "applications": {
        "laravel": {
            "type": "php",
            "processes": {
                "max": 120,
                "spare": 5,
                "idle_timeout": 20
            },
            "root": "/usr/wwwroot/projects/phpinfo/",
            "script": "index.php"
        }
    }
}

将上面的 JSON 对象压缩为字符串,然后执行下面命令将配置文件发送给 Unit。

$ docker exec -it laravel curl -X PUT -d '{"listeners":{"*:80":{"pass":"routes"}},"routes":[{"match":{"uri":"!/index.php"},"action":{"share":"/usr/wwwroot/projects/phpinfo$uri","fallback":{"pass":"applications/laravel"}}}],"applications":{"laravel":{"type":"php","processes":{"max":120,"spare":5,"idle_timeout":20},"root":"/usr/wwwroot/projects/phpinfo/","script":"index.php"}}}' --unix-socket /var/run/control.unit.sock http://localhost/config/

执行成功的话会返回:

{
  "success": "Reconfiguration done."
}

然后访问 http://localhost 就可以打开 phpinfo 的页面了。

Unit phpinfo

性能对比

硬件配置

4核心/8Gb 网络 100Mbps 的带宽峰值,磁盘是 ESSD 40G(2280 IOPS)。

服务配置

压测 Unit + PHP 8.1.1

ab -n 10000 -c 20 "http://47.100.202.147:8080/"

Server Software:        Unit/1.26.1
Server Hostname:        47.100.202.147
Server Port:            8080

Document Path:          /
Document Length:        17592 bytes

Concurrency Level:      20
Time taken for tests:   73.888 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      187050000 bytes
HTML transferred:       175920000 bytes
Requests per second:    135.34 [#/sec] (mean)
Time per request:       147.775 [ms] (mean)
Time per request:       7.389 [ms] (mean, across all concurrent requests)
Transfer rate:          2472.22 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        6    9   1.3      9      24
Processing:    17  136 716.7     34    5985
Waiting:       10  127 716.7     25    5977
Total:         24  145 716.7     43    5992

Percentage of the requests served within a certain time (ms)
  50%     43
  66%     48
  75%     51
  80%     53
  90%     57
  95%     62
  98%     90
  99%   5230
 100%   5992 (longest request)
ab -n 10000 -c 50 "http://47.100.202.147:8080/"

Server Software:        Unit/1.26.1
Server Hostname:        47.100.202.147
Server Port:            8080

Document Path:          /
Document Length:        17592 bytes

Concurrency Level:      50
Time taken for tests:   61.516 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      187050000 bytes
HTML transferred:       175920000 bytes
Requests per second:    162.56 [#/sec] (mean)
Time per request:       307.580 [ms] (mean)
Time per request:       6.152 [ms] (mean, across all concurrent requests)
Transfer rate:          2969.41 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        6   10   4.4      9     151
Processing:    20  285 1350.1     99   11801
Waiting:       12  272 1350.4     85   11792
Total:         28  295 1350.1    109   11810

Percentage of the requests served within a certain time (ms)
  50%    109
  66%    119
  75%    127
  80%    132
  90%    145
  95%    156
  98%    198
  99%  10018
 100%  11810 (longest request)
ab -n 10000 -c 100 "http://47.100.202.147:8080/"

Server Software:        Unit/1.26.1
Server Hostname:        47.100.202.147
Server Port:            8080

Document Path:          /
Document Length:        17592 bytes

Concurrency Level:      100
Time taken for tests:   57.325 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      187050000 bytes
HTML transferred:       175920000 bytes
Requests per second:    174.45 [#/sec] (mean)
Time per request:       573.247 [ms] (mean)
Time per request:       5.732 [ms] (mean, across all concurrent requests)
Transfer rate:          3186.52 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        6   14  10.7     10      79
Processing:    22  515 2178.1    210   18381
Waiting:       13  493 2179.3    190   18364
Total:         31  528 2177.9    223   18394

Percentage of the requests served within a certain time (ms)
  50%    223
  66%    254
  75%    266
  80%    273
  90%    293
  95%    305
  98%    365
  99%  16907
 100%  18394 (longest request)

压测 Nginx + PHP-FPM + PHP 8.1.1

ab -n 10000 -c 20 "http://47.100.202.147:8081/"

Server Software:        nginx
Server Hostname:        47.100.202.147
Server Port:            8081

Document Path:          /
Document Length:        17592 bytes

Concurrency Level:      20
Time taken for tests:   76.832 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      187160000 bytes
HTML transferred:       175920000 bytes
Requests per second:    130.15 [#/sec] (mean)
Time per request:       153.663 [ms] (mean)
Time per request:       7.683 [ms] (mean, across all concurrent requests)
Transfer rate:          2378.88 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        6    9   1.2      9      27
Processing:    18  141 724.5     39    6025
Waiting:       11  132 724.5     30    6016
Total:         24  150 724.5     48    6035

Percentage of the requests served within a certain time (ms)
  50%     48
  66%     52
  75%     54
  80%     56
  90%     61
  95%     66
  98%     95
  99%   5314
 100%   6035 (longest request)
ab -n 10000 -c 50 "http://47.100.202.147:8081/"

Server Software:        nginx
Server Hostname:        47.100.202.147
Server Port:            8081

Document Path:          /
Document Length:        17592 bytes

Concurrency Level:      50
Time taken for tests:   80.285 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      187160000 bytes
HTML transferred:       175920000 bytes
Requests per second:    124.56 [#/sec] (mean)
Time per request:       401.426 [ms] (mean)
Time per request:       8.029 [ms] (mean, across all concurrent requests)
Transfer rate:          2276.55 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        6    9   3.9      9     177
Processing:    19  375 1973.8    100   16940
Waiting:       11  362 1974.2     88   16931
Total:         26  384 1973.8    109   16950

Percentage of the requests served within a certain time (ms)
  50%    109
  66%    124
  75%    134
  80%    139
  90%    150
  95%    162
  98%    331
  99%  15107
 100%  16950 (longest request)
ab -n 10000 -c 100 "http://47.100.202.147:8081/"

Server Software:        nginx
Server Hostname:        47.100.202.147
Server Port:            8081

Document Path:          /
Document Length:        17592 bytes

Concurrency Level:      100
Time taken for tests:   94.577 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      187160000 bytes
HTML transferred:       175920000 bytes
Requests per second:    105.73 [#/sec] (mean)
Time per request:       945.768 [ms] (mean)
Time per request:       9.458 [ms] (mean, across all concurrent requests)
Transfer rate:          1932.54 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        6   13   9.7      9     114
Processing:    22  862 4350.7    226   35655
Waiting:       13  842 4352.0    207   35643
Total:         30  875 4350.6    239   35663

Percentage of the requests served within a certain time (ms)
  50%    239
  66%    277
  75%    297
  80%    307
  90%    329
  95%    346
  98%  19222
  99%  32654
 100%  35663 (longest request)
服务 并发数 Requests per second Time per request
Unit+PHP 8.1 20 135.34 7.389
Unit+PHP 8.1 50 162.56 6.152
Unit+PHP 8.1 100 174.45 5.732
Nginx+PHP-FPM 20 130.15 7.683
Nginx+PHP-FPM 40 124.56 8.029
Nginx+PHP-FPM 100 105.73 9.458

从上面的数据来看,性能并没有想象中差距那么大,但是 Unit 却随着并发数的增加,性能呈现上升趋势,这个就有点意思了,难道这点并发不足以喂饱 Unit。

本地测试

考虑到肯能是我本地网络原因,所以我又直接在服务器上进行本地测试:

ab -n 10000 -c 100 "http://127.0.0.1:8080/"

Server Software:        Unit/1.26.1
Server Hostname:        127.0.0.1
Server Port:            8080

Document Path:          /
Document Length:        17592 bytes

Concurrency Level:      100
Time taken for tests:   99.609 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      187050000 bytes
HTML transferred:       175920000 bytes
Requests per second:    100.39 [#/sec] (mean)
Time per request:       996.091 [ms] (mean)
Time per request:       9.961 [ms] (mean, across all concurrent requests)
Transfer rate:          1833.83 [Kbytes/sec] received

Connection Times (ms)
             min  mean[+/-sd] median   max
Connect:        0    0   0.1      0       2
Processing:     4  914 5118.6    240   43107
Waiting:        4  912 5118.5    238   43107
Total:          4  914 5118.6    240   43108

Percentage of the requests served within a certain time (ms)
 50%    240
 66%    274
 75%    295
 80%    306
 90%    335
 95%    369
 98%    460
 99%  39589
100%  43108 (longest request)
ab -n 10000 -c 100 "http://127.0.0.1:8081/"

Server Software:        nginx
Server Hostname:        127.0.0.1
Server Port:            8081

Document Path:          /
Document Length:        17592 bytes

Concurrency Level:      100
Time taken for tests:   92.971 seconds
Complete requests:      10000
Failed requests:        0
Total transferred:      187160000 bytes
HTML transferred:       175920000 bytes
Requests per second:    107.56 [#/sec] (mean)
Time per request:       929.709 [ms] (mean)
Time per request:       9.297 [ms] (mean, across all concurrent requests)
Transfer rate:          1965.92 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.2      0       3
Processing:     3  854 4601.4    236   38351
Waiting:        3  854 4601.4    236   38351
Total:          3  854 4601.4    236   38351

Percentage of the requests served within a certain time (ms)
  50%    236
  66%    282
  75%    301
  80%    313
  90%    336
  95%    358
  98%    427
  99%  35636
 100%  38351 (longest request)

本地 WRK 压测

wrk -t4 -c50 http://localhost:8080

Running 10s test @ http://localhost:8080
 4 threads and 50 connections
 Thread Stats   Avg      Stdev     Max   +/- Stdev
   Latency    81.71ms   43.75ms 234.97ms   66.18%
   Req/Sec    57.10     52.38   252.00     82.73%
 2052 requests in 10.01s, 36.61MB read
Requests/sec:    205.03
Transfer/sec:      3.66MB
wrk -t4 -c50 http://localhost:8081

Running 10s test @ http://localhost:8081
 4 threads and 50 connections
 Thread Stats   Avg      Stdev     Max   +/- Stdev
   Latency    98.90ms   97.57ms 786.41ms   95.98%
   Req/Sec    55.98     52.61   300.00     83.20%
 2099 requests in 10.01s, 37.48MB read
Requests/sec:    209.69
Transfer/sec:      3.74MB

总结

经过几组测试发现,Unit 内嵌 PHP 8.1 的性能似乎和 NginX + PHP 8.1 + PHP-FPM 差距不大,不知道是 JIT 的功劳还是 Unit 内嵌 PHP 8.1 开启 JIT.CLI 没有生效。不可否认的一点是,PHP 这两年来的升级,性能确实提升了不少。

等后面有空再做一下 Swoole 和 RoadRunner 的测试。

I hope this is helpful, Happy hacking…