本篇文章分享一个和 Nginx 以及 PHP 有关的“黑魔法”:NGX-PHP 模块。通过这个方式,我们可以低成本的实现高性能应用,以及适合在服务器资源有限的情况下,同时体验到 Nginx 的高效以及 PHP 的灵活。
如果你对 PHP 的印象还停留在“慢”,那么或许这篇文章可以帮助你打开新世界。
写在前面
提到 “NGX 和 PHP”,使用过 Nginx 和 PHP 的同学第一反映可能是 Nginx + PHP-FPM 这种架构。不过,这篇文章中,我们要提到的技术架构更简单高效一些:直接使用 Nginx 和三方模块(NGX-PHP),调用 PHP Embedded 库,来实现原本需要跨进程实现的功能,从而明显提升应用性能。
之所以能够这样玩,需要感谢下面两个项目的相关实现:
- PHP 提供了一种有趣的调用方式:让其他的程序能够通过支持 C Bindings 的符号绑定的方式来调用它的核心引擎,Zend。这种接口调用方式,被称作 PHP SAPI 或者 PHP-Embeded,项目地址:https://github.com/php/php-src/tree/master/sapi/embed
- 2016 年,有一位来自搜狐的工程师 rryqszq4,开始在 GitHub 上尝试开源一个项目,把 “Nginx” 和 “PHP-Embeded Library” 桥接到一起,这个项目经过多年发展,陆续支持了 PHP5、PHP7,以及最新的 PHP8。项目地址:https://github.com/rryqszq4/ngx-php
在 Techem Power 的测试中,自 2020 年开始,“NGX-PHP” 这个技术选型出现之后,便取得了不错的成绩,比如:2020 年的Round 19,以及 2022 年的Round 21。
在最近的 2022 年测试中,框架开销非常低,位于排行榜第五和第六名。
如果用我们熟悉的 Node.js + MongoDB 作为基准,那么这套方案开销比它少 300%:跑的快,吃草少。换个角度来看,这个方案非常贴合 “Nginx” 和 “PHP” 的特性:快糙猛。
好了,关于这个项目的概况就介绍到这里,我们先来使用 Docker 快速、实际的感受下它的性能。
快速体验
执行下面的命令:
docker run --rm -it -v `pwd`/data:/usr/share/nginx/html/data:rw -p 8090:80 soulteary/ngx-php:8-microblog
当 Docker 镜像下载完毕之后,我们将看到一个和普通 Nginx 镜像启动无异的日志输出:
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2022/10/05 10:25:39 [notice] 1#1: using the "epoll" event method
2022/10/05 10:25:39 [notice] 1#1: nginx/1.23.1
2022/10/05 10:25:39 [notice] 1#1: built by gcc 11.2.1 20220219 (Alpine 11.2.1_git20220219)
2022/10/05 10:25:39 [notice] 1#1: OS: Linux 5.10.76-linuxkit
...
打开浏览器,输入 http://localhost:8090
,就能够看到效果啦。
随手输入一些内容,能够看到程序“跑的”还是挺快的。
在不进行应用优化、Nginx 优化的前提下,我们能够看到处理一个请求不过 2ms 左右。
接下来,我们来聊聊如何使用 NGX-PHP,学习了解这种开源方案背后的一些细节。完整的应用代码,我上传到了 soulteary/ngx-php-micro-blog,有需要可以自取。
准备工作
想要愉快的阅读和跟着本文游玩,只需要 Docker 环境,可以参考《在笔记本上搭建高性价比的 Linux 学习环境:基础篇》文章完成基础环境的准备,就不过多赘述了。
实现简单的微博应用
我们来使用“最好的语言:PHP”,实现一个简单的“微博/推特”程序。
简单实现模版类
使用 PHP “画一个”页面出来,可以用的方式非常多,最具可维护性的方式是使用”“模版”。为了不过多引入复杂性,就不使用 PHP 包管理器来为项目添加“模版引擎”了,我们来实现一个简单的模版类(不到 30 行):
<?php
class Template
{
protected $dir = TEMPLATE_DIR . DIRECTORY_SEPARATOR;
protected $vars = array();
public function __construct($dir = null)
{
if ($dir !== null) {
$this->dir = $dir;
}
}
public function render($file)
{
if (file_exists($this->dir . $file)) {
include $this->dir . $file;
} else {
throw new Exception('no template file ' . $file . ' present in directory ' . $this->dir);
}
}
public function __set($name, $value)
{
$this->vars[$name] = $value;
}
public function __get($name)
{
return $this->vars[$name];
}
}
在完成简单的模版功能之后,我们就能够在应用中使用 new Template, template->render('template.name.html')
来进行页面结果的渲染了。
简单实现主要逻辑
接下来,我们来实现“微博”的主要流程逻辑,大概 130 行左右的代码就能够搞定:
<?php
class Whisper
{
public function __construct()
{
if (empty($_POST['content'])) {
$start_time = microtime(true);
$page = 1;
if (!empty($_GET['p'])) {
$page = (int) filter_var($_GET['p'], FILTER_SANITIZE_NUMBER_INT);
if ($page < 1) {
$page = 1;
}
}
$tpl = new Template();
$tpl->data = $this->loadData($page);
ob_start();
$tpl->render('main.html');
ob_end_flush();
$end_time = microtime(true);
echo "\n<!-- " . round($end_time - $start_time, 3) . "s -->";
} else {
$content = trim($_POST['content']);
if (strlen($content) == 0) {
echo ERROR_IS_EMPTY;
exit;
}
$content = (string) filter_var($content, FILTER_SANITIZE_SPECIAL_CHARS);
$this->postWhisper($content);
}
}
private function postWhisper($content)
{
$date = date('Y-m-d g:i:s A');
$filename = DATA_DIR . DIRECTORY_SEPARATOR . date('YmdHis') . ".txt";
$file = fopen($filename, "w+");
$content = $date . "\n" . $content . "\n";
fwrite($file, $content);
fclose($file);
header("location: /");
}
private function loadData($page)
{
$result = [
'whispers' => [],
'pagination' => ['hide' => true],
];
$files = [];
if ($handle = @opendir(DATA_DIR)) {
while ($file = readdir($handle)) {
if (!is_dir($file)) {
$files[] = $file;
}
}
}
rsort($files);
$total = sizeof($files);
if ($total == 0) {
return $result;
}
$page = $page - 1;
$start = $page * WHISPER_PER_PAGE;
if (($start + WHISPER_PER_PAGE) > $total) {
$last = $total;
} else {
$last = $start + WHISPER_PER_PAGE;
}
for ($i = $start; $i < $last; $i++) {
$raw = file(DATA_DIR . DIRECTORY_SEPARATOR . $files[$i]);
$date = trim($raw[0]);
unset($raw[0]);
$content = "";
foreach ($raw as $value) {
$content .= $value;
}
$data = array(
'date' => $date,
'content' => $content,
);
$result['whispers'][] = $data;
}
$result['pagination'] = $this->getPagination($start, $last, $page, $total);
return $result;
}
private function getPagination($start, $last, $page, $total)
{
if ($total <= WHISPER_PER_PAGE) {
return ['hide' => true];
}
$page = $page + 1;
$next = 0;
$prev = 0;
if ($start == 0) {
if ($last < $total) {
$next = $page + 1;
}
} else {
if ($last < $total) {
$next = $page + 1;
$prev = $page - 1;
} else {
$prev = $page - 1;
}
}
return [
'hide' => false,
'prev' => $prev,
'next' => $next,
'page' => $page,
'last' => ceil($total / 5),
];
}
}
new Whisper();
简单实现页面模版
完成主要程序实现之后,我们来实现页面模版,大概 120 行就能够搞定:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/style.css">
<title>Whisper</title>
</head>
<body>
<div id="root">
<div class="container" id="brand-container">
<div class="row">
<div class="py-4 text-center">
<h2>
<span>Whisper</span>
<img class="logo" src="assets/img/logo.svg" alt="" width="72" height="57" />
</h2>
<p class="lead">a simplest example.</p>
</div>
</div>
</div>
<div class="container" id="post-container">
<div class="row">
<div class="col-xs-12">
<h4 class="mb-3"># Post a Whisper</h4>
<form class="mb-3" action="/" method="post" novalidate >
<div class="row g-3">
<div class="col-12">
<textarea class="form-control mb-2" placeholder="enter content here..." name="content" rows="4" required></textarea>
<button class="w-100 btn btn-primary" type="submit">Post</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="container" id="whispers-container">
<?php if(!$this->data['pagination']['hide']):?>
<div class="row row-cols-sm-auto">
<div class="col-sm-9">
<hr class="my-3 w-100" />
</div>
<div class="col-sm-3">
<div aria-label="Page navigation" id="pagination">
<ul class="pagination justify-content-center pagination-sm">
<?php if($this->data['pagination']['prev']==0):?>
<li class="page-item disabled">
<a class="page-link">Previous</a>
</li>
<?php else:?>
<li class="page-item">
<a class="page-link" href="?p=<?=$this->data['pagination']['prev']?>">Previous</a>
</li>
<?php endif;?>
<?php if($this->data['pagination']['next']==0):?>
<li class="page-item disabled">
<a class="page-link">Next</a>
</li>
<?php else:?>
<li class="page-item">
<a class="page-link" href="?p=<?=$this->data['pagination']['next']?>">Next</a>
</li>
<?php endif;?>
</ul>
</div>
</div>
</div>
<?php endif;?>
<?php if(sizeof($this->data['whispers'])>0):?>
<div class="row row-cols-sm-auto">
<h4 class="col-sm-12">
<span># List</span>
<?php if(!$this->data['pagination']['hide']):?>
<span class="text-muted" id="page-info">Page #<?=$this->data['pagination']['page']?> / <?=$this->data['pagination']['last']?></span>
<?php endif;?>
</h4>
</div>
<div class="row">
<div class="col-xs-12">
<ul class="list-group w-auto">
<?php foreach ($this->data['whispers'] as $whisper): ?>
<li class="list-group-item d-flex gap-3 py-3" aria-current="true">
<img src="assets/img/icon.svg" width="32" height="32" class="rounded-circle flex-shrink-0">
<div class="d-flex gap-2 w-100 justify-content-between">
<div>
<h6 class="mb-0 whisper-content"><?=$whisper['content']?></h6>
<p class="mb-0 opacity-75"><?=$whisper['date']?></p>
</div>
<small class="opacity-50 text-nowrap timeago" data-value="<?=$whisper['date']?>"></small>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif;?>
</div>
<div class="container">
<div class="row">
<div class="my-3 pt-3 text-muted text-center text-small">
<ul class="list-inline mb-1">
<li class="list-inline-item">© 2007–<?=date('Y');?> @soulteary: <a href="https://soulteary.com/" target="_blank">Blog</a></li>
<li class="list-inline-item"><a href="https://www.zhihu.com/people/soulteary" target="_blank">Zhihu</a></li>
<li class="list-inline-item"><a href="https://weibo.com/u/1220149481" target="_blank">Weibo</a></li>
</ul>
</div>
</div>
</div>
</div>
<script src="assets/js/main.js"></script>
</body>
</html>
使用 PHP 官方镜像验证程序
为了方便后续的演示和性能对比,这里我们直接声明一些路径为 Nginx 容器的地址,所以当你看到后续 Apache 镜像中使用的路径,不必惊讶:
<?php
date_default_timezone_set('Asia/shanghai');
define('TEMPLATE_DIR', '/usr/share/nginx/html/templates');
define('DATA_DIR', '/usr/share/nginx/html/data');
define('WHISPER_PER_PAGE', 5);
...
在完成程序调整之后,我们简单编写一个 compose
配置,来使用 PHP 官方提供的 Docker 镜像来验证程序是否能够正常运行:
version: '3'
services:
talk:
image: php:8.1.10-apache-buster
restart: always
ports:
- 8090:80
volumes:
- ./app:/var/www/html/
- ./app/templates:/usr/share/nginx/html/templates:rw
- ./app/data:/usr/share/nginx/html/data:rw
使用 docker-compose up
启动程序之后,我们访问 http://localhost:8090
就能够看到文章一开头的界面了。随便输入点内容,然后点击“发布” 按钮,能够看到一切符合预期,功能可以正常工作。
确认程序能够正常工作后,我们来将程序迁移到 NGX-PHP 环境中。
这部分的代码,可以参考项目的提交记录:soulteary/ngx-php-micro-blog/commit/d97385b945c998385cbc7dee813529f05b4f15d3
构建 NGX PHP 容器镜像
这里,我们借助很早之前提到过的一个项目 https://github.com/nginx-with-docker/nginx-docker-playground 来快速完成 NGX PHP 这个 Nginx 模块的构建,关于“如何在容器时代高效使用 Nginx 三方模块”,可以参考这篇文章。
# Nginx Docker Playground (https://github.com/nginx-with-docker/nginx-docker-playground)
FROM soulteary/prebuilt-nginx-modules:base-1.23.1-alpine AS Builder
RUN sed -i -E "s/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g" /etc/apk/repositories
RUN apk update && apk --no-cache add curl gcc g++ make musl-dev linux-headers gd-dev geoip-dev libxml2-dev libxslt-dev openssl-dev pcre-dev perl-dev pkgconf zlib-dev libedit-dev ncurses-dev php8-dev php8-embed git unzip argon2-dev
ENV PHP_LIB=/usr/lib
WORKDIR /usr/src
# Nginx Development Kit (https://github.com/vision5/ngx_devel_kit)
ARG DEVEL_KIT_MODULE_CHECKSUM=e15316e13a7b19a3d2502becbb26043a464a135a
ARG DEVEL_KIT_VERSION=0.3.1
ARG DEVEL_KIT_NAME=ngx_devel_kit
RUN curl -L "https://github.com/vision5/ngx_devel_kit/archive/v${DEVEL_KIT_VERSION}.tar.gz" -o "v${DEVEL_KIT_VERSION}.tar.gz" && \
echo "${DEVEL_KIT_MODULE_CHECKSUM} v${DEVEL_KIT_VERSION}.tar.gz" | shasum -c && \
tar -zxC /usr/src -f v${DEVEL_KIT_VERSION}.tar.gz && \
mv ${DEVEL_KIT_NAME}-${DEVEL_KIT_VERSION}/ ${DEVEL_KIT_NAME}
# Nginx PHP Module (Mirror https://github.com/nginx-with-docker/ngx_http_php_module-src)
ARG MODULE_CHECKSUM=aeef775b2beb8378cb295a4da29b80d98274e1fa
ARG MODULE_VERSION=master
ARG MODULE_NAME=ngx_http_php_module-src
ARG MODULE_SOURCE=https://github.com/nginx-with-docker/ngx_http_php_module-src
RUN curl -L "${MODULE_SOURCE}/archive/refs/heads/${MODULE_VERSION}.zip" -o "v${MODULE_VERSION}.zip" && \
echo "${MODULE_CHECKSUM} v${MODULE_VERSION}.zip" | shasum -c && \
unzip "v${MODULE_VERSION}.zip" && \
mv "$MODULE_NAME-$MODULE_VERSION" "$MODULE_NAME"
# Nginx
RUN cd /usr/src/nginx && \
CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \
CONFARGS=${CONFARGS/-Os -fomit-frame-pointer -g/-Os} && \
echo $CONFARGS && \
./configure --with-compat $CONFARGS --with-ld-opt="-Wl,-rpath,${PHP_LIB}" --add-dynamic-module=../${DEVEL_KIT_NAME} --add-dynamic-module=../${MODULE_NAME} && \
make modules
完成构建之后,我们使用多阶段构建,制作最终的应用镜像就好了:
FROM nginx:1.23.1-alpine
LABEL MAINTAINER=soulteary@gmail.com
COPY --from=Builder /usr/lib/libphp.so /usr/lib/
COPY --from=Builder /usr/lib/libargon2.so.1 /usr/lib/
COPY --from=Builder /lib/libz.so.1 /lib/
COPY --from=Builder /etc/php8/php.ini /etc/php8/
COPY --from=Builder /usr/src/nginx/objs/ndk_http_module.so /etc/nginx/modules/
COPY --from=Builder /usr/src/nginx/objs/ngx_http_php_module.so /etc/nginx/modules/
ENV PHP_LIB=/usr/lib
COPY conf/nginx.conf /etc/nginx/
COPY app/index.php /usr/share/nginx/html/
COPY app/assets /usr/share/nginx/html/assets
COPY app/templates /usr/share/nginx/html/templates
RUN mkdir -p /usr/share/nginx/html/data && \
chown nginx:nginx /usr/share/nginx/html/data && \
chmod 777 /usr/share/nginx/html/data
这个小节的完整代码,在这里可以找到: soulteary/ngx-php-micro-blog/Dockerfile。使用 docker build -t soulteary/ngx-php:8-microblog .
,完成基础镜像构建,我们将得到一个 12MB 左右的小巧的、包含了 Nginx PHP 模块的镜像。
在完成了基础镜像构建之后,我们来进行程序的“改造”。
将 PHP 程序适配 NGX PHP 环境
如果我们不修改任何代码,通过调整 docker compose
配置文件,切换容器镜像和挂载的文件,是可以让程序在我们新构建的 NGX-PHP 镜像中运行的。
version: '3'
services:
talk:
image: soulteary/ngx-php:8-microblog
restart: always
ports:
- 8090:80
volumes:
- ./app/data:/usr/share/nginx/html/data:rw
- ./app/index.php:/usr/share/nginx/html/index.php:ro
- ./app/templates:/usr/share/nginx/html/templates:ro
- ./app/assets:/usr/share/nginx/html/assets:ro
但是我们会得到一些报错,导致程序不能正常运行。
解决变量、函数重复定义的问题
我们首先可能遇到的问题就是类似下面的报错,告诉我们重复声明了“某些内容”,比如常量:
Warning: Constant TEMPLATE_DIR already defined in /usr/share/nginx/html/index.php on line 4
或者重复声明了“某些类”:
Fatal error: Cannot declare class Template, because the name is already in use in /usr/share/nginx/html/index.php on line 18
出现这两个问题的原因,是因为 NGX PHP 模块中,“全局变量和静态变量”都是不安全的。
解决第一个问题,我们可以有两个方案,降低声明的作用域,或者加上一些防御性判断:
defined('TEMPLATE_DIR') or define('TEMPLATE_DIR', '/usr/share/nginx/html/templates');
解决第二个问题,我们只能够依赖添加判断来避免重复声明:
if (!class_exists('Template')) {
class Template
{
// ...
}
}
解决完毕上面两个问题,程序就能够正常展示界面了。
解决参数获取不到的问题
虽然解决了上面的问题,程序能够正常展示,但是我们会发现提交任何内容,程序都不会有“正确的反应”,而 Nginx 日志中也没有任何错误信息。
出现这个问题的原因是,在 NGX PHP 环境下,PHP 获取用户提交数据的方式由 $_GET
和 $_POST
改为了 ngx_query_args()
和 ngx_post_args()
。
为了解决这个问题,并且保持我们的程序依旧能够在官方 PHP 环境中运行、调试,可以实现一个简单的 getArgs
方法,让程序兼容不同的环境:
private function getArgs($key, $method)
{
$dataSource = null;
$isNginxEnv = false;
if ($method == 'GET') {
if (function_exists('ngx_query_args')) {
$dataSource = ngx_query_args();
$isNginxEnv = true;
} else {
$dataSource = $_GET;
}
} else {
if (function_exists('ngx_post_args')) {
$dataSource = ngx_post_args();
$isNginxEnv = true;
} else {
$dataSource = $_POST;
}
}
if (!isset($dataSource[$key])) {
return "";
}
return $isNginxEnv ? trim(urldecode($dataSource[$key])) : trim($dataSource[$key]);
}
对应的,调整上文中程序获取用户输入数据的方法,就能够让程序正常的在 NGX PHP 容器中运行啦。
最终应用程序
最终的应用程序,算上换行大概 220 行左右:
<?php
date_default_timezone_set('Asia/shanghai');
defined('TEMPLATE_DIR') or define('TEMPLATE_DIR', '/usr/share/nginx/html/templates');
defined('DATA_DIR') or define('DATA_DIR', '/usr/share/nginx/html/data');
defined('WHISPER_PER_PAGE') or define('WHISPER_PER_PAGE', 5);
defined('ERROR_IS_EMPTY') or define('ERROR_IS_EMPTY', '内容不能为空');
if (defined('DATA_DIR')) {
if (!file_exists(DATA_DIR)) {
mkdir(DATA_DIR);
}
} else {
echo "需要定义数据目录";
exit;
}
if (!class_exists('Template')) {
class Template
{
protected $dir = TEMPLATE_DIR . DIRECTORY_SEPARATOR;
protected $vars = array();
public function __construct($dir = null)
{
if ($dir !== null) {
$this->dir = $dir;
}
}
public function render($file)
{
if (file_exists($this->dir . $file)) {
include $this->dir . $file;
} else {
throw new Exception('no template file ' . $file . ' present in directory ' . $this->dir);
}
}
public function __set($name, $value)
{
$this->vars[$name] = $value;
}
public function __get($name)
{
return $this->vars[$name];
}
}
}
if (!class_exists('Whisper')) {
class Whisper
{
private function getArgs($key, $method)
{
$dataSource = null;
$isNginxEnv = false;
if ($method == 'GET') {
if (function_exists('ngx_query_args')) {
$dataSource = ngx_query_args();
$isNginxEnv = true;
} else {
$dataSource = $_GET;
}
} else {
if (function_exists('ngx_post_args')) {
$dataSource = ngx_post_args();
$isNginxEnv = true;
} else {
$dataSource = $_POST;
}
}
if (!isset($dataSource[$key])) {
return "";
}
return $isNginxEnv ? trim(urldecode($dataSource[$key])) : trim($dataSource[$key]);
}
private function redir($url)
{
if (function_exists('ngx_header_set')) {
ngx_header_set("Location", $url);
ngx_exit(NGX_HTTP_MOVED_TEMPORARILY);
} else {
header("Location: " . $url);
}
}
public function __construct()
{
$content = $this->getArgs('content', 'POST');
if (empty($content)) {
$start_time = microtime(true);
$page = 1;
$page = $this->getArgs('p', 'GET');
if (!empty($page)) {
$page = (int) filter_var($page, FILTER_SANITIZE_NUMBER_INT);
if ($page < 1) {
$page = 1;
}
} else {
$page = 1;
}
$tpl = new Template();
$tpl->data = $this->loadData($page);
ob_start();
$tpl->render('main.html');
ob_end_flush();
$end_time = microtime(true);
echo "\n<!-- program processing time: " . round($end_time - $start_time, 3) . "s -->";
} else {
$content = htmlentities((string) filter_var($content, FILTER_SANITIZE_SPECIAL_CHARS));
$this->postWhisper($content);
}
}
private function postWhisper($content)
{
$date = date('Y-m-d g:i:s A');
$filename = DATA_DIR . DIRECTORY_SEPARATOR . date('YmdHis') . ".txt";
$file = fopen($filename, "w+");
$content = $date . "\n" . $content . "\n";
fwrite($file, $content);
fclose($file);
$this->redir("/");
}
private function loadData($page)
{
$result = [
'whispers' => [],
'pagination' => ['hide' => true],
];
$files = [];
if ($handle = @opendir(DATA_DIR)) {
while ($file = readdir($handle)) {
if (!is_dir($file)) {
$files[] = $file;
}
}
}
rsort($files);
$total = sizeof($files);
if ($total == 0) {
return $result;
}
$page = $page - 1;
$start = $page * WHISPER_PER_PAGE;
if (($start + WHISPER_PER_PAGE) > $total) {
$last = $total;
} else {
$last = $start + WHISPER_PER_PAGE;
}
for ($i = $start; $i < $last; $i++) {
$raw = file(DATA_DIR . DIRECTORY_SEPARATOR . $files[$i]);
$date = trim($raw[0]);
unset($raw[0]);
$content = "";
foreach ($raw as $value) {
$content .= $value;
}
$data = array(
'date' => $date,
'content' => html_entity_decode($content),
);
$result['whispers'][] = $data;
}
$result['pagination'] = $this->getPagination($start, $last, $page, $total);
return $result;
}
private function getPagination($start, $last, $page, $total)
{
if ($total <= WHISPER_PER_PAGE) {
return ['hide' => true];
}
$page = $page + 1;
$next = 0;
$prev = 0;
if ($start == 0) {
if ($last < $total) {
$next = $page + 1;
}
} else {
if ($last < $total) {
$next = $page + 1;
$prev = $page - 1;
} else {
$prev = $page - 1;
}
}
return [
'hide' => false,
'prev' => $prev,
'next' => $next,
'page' => $page,
'last' => ceil($total / 5),
];
}
}
}
new Whisper();
简单的性能比较
除了相信相对中立的机构的测试结果之外,我们也可以自己进行应用性能测试,来验证 NGX-PHP 是否真的能够“降本增效”。
下面我们就用上面最终实现好的程序,分别在我们构建的 soulteary/ngx-php:8
镜像和 PHP 官方镜像 php:8.1.10-apache-buster
中进行简单的请求性能测试:
先使用开启 OPCACHE 之后的官方镜像(php:8.1.10-apache-buster
),完成30s 的压力测试:
wrk -t16 -c 100 -d 30s http://127.0.0.1:8090
Running 30s test @ http://127.0.0.1:8090
16 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 132.92ms 158.49ms 1.98s 86.67%
Req/Sec 54.38 56.83 670.00 94.88%
22603 requests in 30.08s, 49.40MB read
Socket errors: connect 0, read 0, write 0, timeout 112
Requests/sec: 751.53
Transfer/sec: 1.64MB
接着,使用我们构建好的 NGX PHP 镜像,在不开启缓存的情况下进行测试:
wrk -t16 -c 100 -d 30s http://127.0.0.1:8090
Running 30s test @ http://127.0.0.1:8090
16 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 94.01ms 15.94ms 431.01ms 81.62%
Req/Sec 64.02 11.33 148.00 74.26%
30715 requests in 30.09s, 65.03MB read
Requests/sec: 1020.65
Transfer/sec: 2.16MB
可以看到,性能提升还是比较明显的。
最后
好了,关于 NGX PHP 的第一篇文章就聊到这里吧。关于更多的细节,或许后面有机会,我会再写一两篇文章进行分享。
–EOF