|
本篇文章分享一个和 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[1],以及 2022 年的Round 21[2]。

2020 年和 2022 年的两轮框架评分测试
在最近的 2022 年测试中,框架开销[3]非常低,位于排行榜第五和第六名。

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 ,就能够看到效果啦。

2022 年测试中,框架开销排行
随手输入一些内容,能够看到程序“跑的”还是挺快的。

发一些只有自己看的到的“微博”
在不进行应用优化、Nginx 优化的前提下,我们能够看到处理一个请求不过 2ms 左右。

单次请求服务端处理时间2ms左右
接下来,我们来聊聊如何使用 NGX-PHP,学习了解这种开源方案背后的一些细节。完整的应用代码,我上传到了 soulteary/ngx-php-micro-blog[4],有需要可以自取。
准备工作
想要愉快的阅读和跟着本文游玩,只需要 Docker 环境,可以参考《在笔记本上搭建高性价比的 Linux 学习环境:基础篇[5]》文章完成基础环境的准备,就不过多赘述了。
实现简单的微博应用
我们来使用“最好的语言: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(&#39;no template file &#39; . $file . &#39; present in directory &#39; . $this->dir);
}
}
public function __set($name, $value)
{
$this->vars[$name] = $value;
}
public function __get($name)
{
return $this->vars[$name];
}
}
在完成简单的模版功能之后,我们就能够在应用中使用 new Template, template->render(&#39;template.name.html&#39;) 来进行页面结果的渲染了。
简单实现主要逻辑
接下来,我们来实现“微博”的主要流程逻辑,大概 130 行左右的代码就能够搞定:
<?php
class Whisper
{
public function __construct()
{
if (empty($_POST[&#39;content&#39;])) {
$start_time = microtime(true);
$page = 1;
if (!empty($_GET[&#39;p&#39;])) {
$page = (int) filter_var($_GET[&#39;p&#39;], FILTER_SANITIZE_NUMBER_INT);
if ($page < 1) {
$page = 1;
}
}
$tpl = new Template();
$tpl->data = $this->loadData($page);
ob_start();
$tpl->render(&#39;main.html&#39;);
ob_end_flush();
$end_time = microtime(true);
echo &#34;\n<!-- &#34; . round($end_time - $start_time, 3) . &#34;s -->&#34;;
} else {
$content = trim($_POST[&#39;content&#39;]);
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(&#39;Y-m-d g:i:s A&#39;);
$filename = DATA_DIR . DIRECTORY_SEPARATOR . date(&#39;YmdHis&#39;) . &#34;.txt&#34;;
$file = fopen($filename, &#34;w+&#34;);
$content = $date . &#34;\n&#34; . $content . &#34;\n&#34;;
fwrite($file, $content);
fclose($file);
header(&#34;location: /&#34;);
}
private function loadData($page)
{
$result = [
&#39;whispers&#39; => [],
&#39;pagination&#39; => [&#39;hide&#39; => 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 = &#34;&#34;;
foreach ($raw as $value) {
$content .= $value;
}
$data = array(
&#39;date&#39; => $date,
&#39;content&#39; => $content,
);
$result[&#39;whispers&#39;][] = $data;
}
$result[&#39;pagination&#39;] = $this->getPagination($start, $last, $page, $total);
return $result;
}
private function getPagination($start, $last, $page, $total)
{
if ($total <= WHISPER_PER_PAGE) {
return [&#39;hide&#39; => 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 [
&#39;hide&#39; => false,
&#39;prev&#39; => $prev,
&#39;next&#39; => $next,
&#39;page&#39; => $page,
&#39;last&#39; => ceil($total / 5),
];
}
}
new Whisper();
简单实现页面模版
完成主要程序实现之后,我们来实现页面模版,大概 120 行就能够搞定:
<!DOCTYPE html>
<html lang=&#34;en&#34;>
<head>
<meta charset=&#34;UTF-8&#34;>
<meta http-equiv=&#34;X-UA-Compatible&#34; content=&#34;IE=edge&#34;>
<meta name=&#34;viewport&#34; content=&#34;width=device-width, initial-scale=1.0&#34;>
<link rel=&#34;stylesheet&#34; href=&#34;assets/css/bootstrap.min.css&#34;>
<link rel=&#34;stylesheet&#34; href=&#34;assets/css/style.css&#34;>
<title>Whisper</title>
</head>
<body>
<div id=&#34;root&#34;>
<div class=&#34;container&#34; id=&#34;brand-container&#34;>
<div class=&#34;row&#34;>
<div class=&#34;py-4 text-center&#34;>
<h2>
<span>Whisper</span>
<img class=&#34;logo&#34; src=&#34;assets/img/logo.svg&#34; alt=&#34;&#34; width=&#34;72&#34; height=&#34;57&#34; />
</h2>
<p class=&#34;lead&#34;>a simplest example.</p>
</div>
</div>
</div>
<div class=&#34;container&#34; id=&#34;post-container&#34;>
<div class=&#34;row&#34;>
<div class=&#34;col-xs-12&#34;>
<h4 class=&#34;mb-3&#34;># Post a Whisper</h4>
<form class=&#34;mb-3&#34; action=&#34;/&#34; method=&#34;post&#34; novalidate >
<div class=&#34;row g-3&#34;>
<div class=&#34;col-12&#34;>
<textarea class=&#34;form-control mb-2&#34; placeholder=&#34;enter content here...&#34; name=&#34;content&#34; rows=&#34;4&#34; required></textarea>
<button class=&#34;w-100 btn btn-primary&#34; type=&#34;submit&#34;>Post</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class=&#34;container&#34; id=&#34;whispers-container&#34;>
<?php if(!$this->data[&#39;pagination&#39;][&#39;hide&#39;]):?>
<div class=&#34;row row-cols-sm-auto&#34;>
<div class=&#34;col-sm-9&#34;>
<hr class=&#34;my-3 w-100&#34; />
</div>
<div class=&#34;col-sm-3&#34;>
<div aria-label=&#34;Page navigation&#34; id=&#34;pagination&#34;>
<ul class=&#34;pagination justify-content-center pagination-sm&#34;>
<?php if($this->data[&#39;pagination&#39;][&#39;prev&#39;]==0):?>
<li class=&#34;page-item disabled&#34;>
<a class=&#34;page-link&#34;>Previous</a>
</li>
<?php else:?>
<li class=&#34;page-item&#34;>
<a class=&#34;page-link&#34; href=&#34;?p=<?=$this->data[&#39;pagination&#39;][&#39;prev&#39;]?>&#34;>Previous</a>
</li>
<?php endif;?>
<?php if($this->data[&#39;pagination&#39;][&#39;next&#39;]==0):?>
<li class=&#34;page-item disabled&#34;>
<a class=&#34;page-link&#34;>Next</a>
</li>
<?php else:?>
<li class=&#34;page-item&#34;>
<a class=&#34;page-link&#34; href=&#34;?p=<?=$this->data[&#39;pagination&#39;][&#39;next&#39;]?>&#34;>Next</a>
</li>
<?php endif;?>
</ul>
</div>
</div>
</div>
<?php endif;?>
<?php if(sizeof($this->data[&#39;whispers&#39;])>0):?>
<div class=&#34;row row-cols-sm-auto&#34;>
<h4 class=&#34;col-sm-12&#34;>
<span># List</span>
<?php if(!$this->data[&#39;pagination&#39;][&#39;hide&#39;]):?>
<span class=&#34;text-muted&#34; id=&#34;page-info&#34;>Page #<?=$this->data[&#39;pagination&#39;][&#39;page&#39;]?> / <?=$this->data[&#39;pagination&#39;][&#39;last&#39;]?></span>
<?php endif;?>
</h4>
</div>
<div class=&#34;row&#34;>
<div class=&#34;col-xs-12&#34;>
<ul class=&#34;list-group w-auto&#34;>
<?php foreach ($this->data[&#39;whispers&#39;] as $whisper): ?>
<li class=&#34;list-group-item d-flex gap-3 py-3&#34; aria-current=&#34;true&#34;>
<img src=&#34;assets/img/icon.svg&#34; width=&#34;32&#34; height=&#34;32&#34; class=&#34;rounded-circle flex-shrink-0&#34;>
<div class=&#34;d-flex gap-2 w-100 justify-content-between&#34;>
<div>
<h6 class=&#34;mb-0 whisper-content&#34;><?=$whisper[&#39;content&#39;]?></h6>
<p class=&#34;mb-0 opacity-75&#34;><?=$whisper[&#39;date&#39;]?></p>
</div>
<small class=&#34;opacity-50 text-nowrap timeago&#34; data-value=&#34;<?=$whisper[&#39;date&#39;]?>&#34;></small>
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif;?>
</div>
<div class=&#34;container&#34;>
<div class=&#34;row&#34;>
<div class=&#34;my-3 pt-3 text-muted text-center text-small&#34;>
<ul class=&#34;list-inline mb-1&#34;>
<li class=&#34;list-inline-item&#34;>&copy; 2007–<?=date(&#39;Y&#39;);?> @soulteary: <a href=&#34;https://soulteary.com/&#34; target=&#34;_blank&#34;>Blog</a></li>
<li class=&#34;list-inline-item&#34;><a href=&#34;https://www.zhihu.com/people/soulteary&#34; target=&#34;_blank&#34;>Zhihu</a></li>
<li class=&#34;list-inline-item&#34;><a href=&#34;https://weibo.com/u/1220149481&#34; target=&#34;_blank&#34;>Weibo</a></li>
</ul>
</div>
</div>
</div>
</div>
<script src=&#34;assets/js/main.js&#34;></script>
</body>
</html>使用 PHP 官方镜像验证程序
为了方便后续的演示和性能对比,这里我们直接声明一些路径为 Nginx 容器的地址,所以当你看到后续 Apache 镜像中使用的路径,不必惊讶:
<?php
date_default_timezone_set(&#39;Asia/shanghai&#39;);
define(&#39;TEMPLATE_DIR&#39;, &#39;/usr/share/nginx/html/templates&#39;);
define(&#39;DATA_DIR&#39;, &#39;/usr/share/nginx/html/data&#39;);
define(&#39;WHISPER_PER_PAGE&#39;, 5);
...
在完成程序调整之后,我们简单编写一个 compose 配置,来使用 PHP 官方提供的 Docker 镜像来验证程序是否能够正常运行:
version: &#39;3&#39;
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[6]
构建 NGX PHP 容器镜像
这里,我们借助很早之前提到过的一个项目 https://github.com/nginx-with-docker/nginx-docker-playground 来快速完成 NGX PHP 这个 Nginx 模块的构建,关于“如何在容器时代高效使用 Nginx 三方模块”,可以参考这篇文章[7]。
# 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 &#34;s/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g&#34; /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 &#34;https://github.com/vision5/ngx_devel_kit/archive/v${DEVEL_KIT_VERSION}.tar.gz&#34; -o &#34;v${DEVEL_KIT_VERSION}.tar.gz&#34; && \
echo &#34;${DEVEL_KIT_MODULE_CHECKSUM} v${DEVEL_KIT_VERSION}.tar.gz&#34; | 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 &#34;${MODULE_SOURCE}/archive/refs/heads/${MODULE_VERSION}.zip&#34; -o &#34;v${MODULE_VERSION}.zip&#34; && \
echo &#34;${MODULE_CHECKSUM} v${MODULE_VERSION}.zip&#34; | shasum -c && \
unzip &#34;v${MODULE_VERSION}.zip&#34; && \
mv &#34;$MODULE_NAME-$MODULE_VERSION&#34; &#34;$MODULE_NAME&#34;
# Nginx
RUN cd /usr/src/nginx && \
CONFARGS=$(nginx -V 2>&1 | sed -n -e &#39;s/^.*arguments: //p&#39;) \
CONFARGS=${CONFARGS/-Os -fomit-frame-pointer -g/-Os} && \
echo $CONFARGS && \
./configure --with-compat $CONFARGS --with-ld-opt=&#34;-Wl,-rpath,${PHP_LIB}&#34; --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[8]。使用 docker build -t soulteary/ngx-php:8-microblog .,完成基础镜像构建,我们将得到一个 12MB 左右的小巧的、包含了 Nginx PHP 模块的镜像。

小巧可爱的容器镜像
在完成了基础镜像构建之后,我们来进行程序的“改造”。
将 PHP 程序适配 NGX PHP 环境
如果我们不修改任何代码,通过调整 docker compose 配置文件,切换容器镜像和挂载的文件,是可以让程序在我们新构建的 NGX-PHP 镜像中运行的。
version: &#39;3&#39;
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(&#39;TEMPLATE_DIR&#39;) or define(&#39;TEMPLATE_DIR&#39;, &#39;/usr/share/nginx/html/templates&#39;);
解决第二个问题,我们只能够依赖添加判断来避免重复声明:
if (!class_exists(&#39;Template&#39;)) {
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 == &#39;GET&#39;) {
if (function_exists(&#39;ngx_query_args&#39;)) {
$dataSource = ngx_query_args();
$isNginxEnv = true;
} else {
$dataSource = $_GET;
}
} else {
if (function_exists(&#39;ngx_post_args&#39;)) {
$dataSource = ngx_post_args();
$isNginxEnv = true;
} else {
$dataSource = $_POST;
}
}
if (!isset($dataSource[$key])) {
return &#34;&#34;;
}
return $isNginxEnv ? trim(urldecode($dataSource[$key])) : trim($dataSource[$key]);
}
对应的,调整上文中程序获取用户输入数据的方法,就能够让程序正常的在 NGX PHP 容器中运行啦。
最终应用程序
最终的应用程序,算上换行大概 220 行左右:
<?php
date_default_timezone_set(&#39;Asia/shanghai&#39;);
defined(&#39;TEMPLATE_DIR&#39;) or define(&#39;TEMPLATE_DIR&#39;, &#39;/usr/share/nginx/html/templates&#39;);
defined(&#39;DATA_DIR&#39;) or define(&#39;DATA_DIR&#39;, &#39;/usr/share/nginx/html/data&#39;);
defined(&#39;WHISPER_PER_PAGE&#39;) or define(&#39;WHISPER_PER_PAGE&#39;, 5);
defined(&#39;ERROR_IS_EMPTY&#39;) or define(&#39;ERROR_IS_EMPTY&#39;, &#39;内容不能为空&#39;);
if (defined(&#39;DATA_DIR&#39;)) {
if (!file_exists(DATA_DIR)) {
mkdir(DATA_DIR);
}
} else {
echo &#34;需要定义数据目录&#34;;
exit;
}
if (!class_exists(&#39;Template&#39;)) {
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(&#39;no template file &#39; . $file . &#39; present in directory &#39; . $this->dir);
}
}
public function __set($name, $value)
{
$this->vars[$name] = $value;
}
public function __get($name)
{
return $this->vars[$name];
}
}
}
if (!class_exists(&#39;Whisper&#39;)) {
class Whisper
{
private function getArgs($key, $method)
{
$dataSource = null;
$isNginxEnv = false;
if ($method == &#39;GET&#39;) {
if (function_exists(&#39;ngx_query_args&#39;)) {
$dataSource = ngx_query_args();
$isNginxEnv = true;
} else {
$dataSource = $_GET;
}
} else {
if (function_exists(&#39;ngx_post_args&#39;)) {
$dataSource = ngx_post_args();
$isNginxEnv = true;
} else {
$dataSource = $_POST;
}
}
if (!isset($dataSource[$key])) {
return &#34;&#34;;
}
return $isNginxEnv ? trim(urldecode($dataSource[$key])) : trim($dataSource[$key]);
}
private function redir($url)
{
if (function_exists(&#39;ngx_header_set&#39;)) {
ngx_header_set(&#34;Location&#34;, $url);
ngx_exit(NGX_HTTP_MOVED_TEMPORARILY);
} else {
header(&#34;Location: &#34; . $url);
}
}
public function __construct()
{
$content = $this->getArgs(&#39;content&#39;, &#39;POST&#39;);
if (empty($content)) {
$start_time = microtime(true);
$page = 1;
$page = $this->getArgs(&#39;p&#39;, &#39;GET&#39;);
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(&#39;main.html&#39;);
ob_end_flush();
$end_time = microtime(true);
echo &#34;\n<!-- program processing time: &#34; . round($end_time - $start_time, 3) . &#34;s -->&#34;;
} else {
$content = htmlentities((string) filter_var($content, FILTER_SANITIZE_SPECIAL_CHARS));
$this->postWhisper($content);
}
}
private function postWhisper($content)
{
$date = date(&#39;Y-m-d g:i:s A&#39;);
$filename = DATA_DIR . DIRECTORY_SEPARATOR . date(&#39;YmdHis&#39;) . &#34;.txt&#34;;
$file = fopen($filename, &#34;w+&#34;);
$content = $date . &#34;\n&#34; . $content . &#34;\n&#34;;
fwrite($file, $content);
fclose($file);
$this->redir(&#34;/&#34;);
}
private function loadData($page)
{
$result = [
&#39;whispers&#39; => [],
&#39;pagination&#39; => [&#39;hide&#39; => 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 = &#34;&#34;;
foreach ($raw as $value) {
$content .= $value;
}
$data = array(
&#39;date&#39; => $date,
&#39;content&#39; => html_entity_decode($content),
);
$result[&#39;whispers&#39;][] = $data;
}
$result[&#39;pagination&#39;] = $this->getPagination($start, $last, $page, $total);
return $result;
}
private function getPagination($start, $last, $page, $total)
{
if ($total <= WHISPER_PER_PAGE) {
return [&#39;hide&#39; => 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 [
&#39;hide&#39; => false,
&#39;prev&#39; => $prev,
&#39;next&#39; => $next,
&#39;page&#39; => $page,
&#39;last&#39; => 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
<hr/>引用链接
[1] Round 19: https://www.techempower.com/benchmarks/#section=data-r19&test=composite
[2] Round 21: https://www.techempower.com/benchmarks/#section=data-r21&test=composite
[3] 框架开销: https://www.techempower.com/benchmarks/#section=data-r21&test=db
[4] soulteary/ngx-php-micro-blog: https://github.com/soulteary/ngx-php-micro-blog
[5] 在笔记本上搭建高性价比的 Linux 学习环境:基础篇: https://soulteary.com/2022/06/21/building-a-cost-effective-linux-learning-environment-on-a-laptop-the-basics.html
[6] soulteary/ngx-php-micro-blog/commit/d97385b945c998385cbc7dee813529f05b4f15d3: https://github.com/soulteary/ngx-php-micro-blog/commit/d97385b945c998385cbc7dee813529f05b4f15d3
[7] 这篇文章: https://soulteary.com/2021/03/22/how-to-use-nginx-third-party-modules-efficiently-in-the-container-era.html
[8] soulteary/ngx-php-micro-blog/Dockerfile: https://github.com/soulteary/ngx-php-micro-blog/blob/main/Dockerfile
<hr/>我们有一个小小的折腾群,里面聚集了一些喜欢折腾的小伙伴。
在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术沙龙的资料。
喜欢折腾的小伙伴,欢迎阅读下面的内容,扫码添加好友。
添加好友,请备注实名和公司或学校、注明来源和目的,否则不会通过审核。
<hr/>如果你觉得内容还算实用,欢迎点赞分享给你的朋友,在此谢过。
如果你想更快的看到后续内容的更新,请戳 “点赞”、“分享”、“喜欢” ,这些免费的鼓励将会影响后续有关内容的更新速度。
<hr/>本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)
本文作者: 苏洋
创建时间: 2022年10月05日 统计字数: 16668字 阅读时间: 34分钟阅读 本文链接: https://soulteary.com/2022/10/05/nginx-black-magic-low-cost-high-performance-applications-using-ngx-php-modules.html |
|