PHP-FPM进程CPU 飙高的原因及解决方案

在最近开发中,发现打开网页越来越慢,所以用用top命令发现,php-fpm CPU 飙到了90%以上,所以需要紧急处理这个问题,

未分类

主要解决思路如下:

1. 设置控制php-fpm进程池进程数量。

修改pm.max_children的数量,根据内存来进行分配,系统开一个进程20-30M。比如系统内存1G,那就将差不多能开30个进程,所以可以设置pm.max_children 为30,以此类推,然后需要重启下php-fpm服务。

pm.max_children:静态方式下开启的php-fpm进程数量。
pm.start_servers:动态方式下的起始php-fpm进程数量。
pm.min_spare_servers:动态方式下的最小php-fpm进程数量。
pm.max_spare_servers:动态方式下的最大php-fpm进程数量。

2. 开启慢日志。

编辑php-fpm.conf文件找到request_slowlog_timeout = 0这一行,默认值为0,表示不开启slowlog,将其值改为3s,表示跟踪执行时间达到或超过3s的脚本。找到slowlog,它的值表示慢执行日志的路径。

3.内存分配。

内存分配太少,理论上开一个进程消耗CPU 20-30M,所以1G内存的可以开30个进程左右,如果是虚拟机的话可以分配2G内存。

4. 编辑php-fpm.conf配置文件。

php_admin_value[memory_limit] = 128M(我服务器上的配置文件在/etc/php5/fpm/pool.d/www.conf 这个文件是被包含在php-fpm.conf里的) 后边的数字可以随便更改:32M,64M,128M,256M,512M,这个设置可根据你的服务器内存大小和你的需求来写,修改后要加载一下php-fpm服务。

欢迎补充!

Php-fpm的配置和优化

Nginx本身不能处理 php请求,它是一个web服务器,接收到php请求后,发给php解释器处理,并把结果返回给客户端

nginx 一般是把请求发给fastcgi 管理进程处理,fascgi管理进程选择cgi 子进程处理结果,并返回给nginx

未分类

php-fpm.conf 配置文件

[www]

pm.max_children = 15 #最大子进程数 

pm.start_servers = 2 #启动时创建的子进程数  

pm.max_requests = 500 #每个子进程可以处理的请求数 

slowlog = log/$pool.log.slow #慢日志 

request_slowlog_timeout = 10s #慢日志记录时间,注意单位,超时的会被纪录到slowlog的path文件中

rlimit_core = 1024

listen = /run/php/php7.2-fpm.sock 

;listen.allowed_clients = 127.0.0.1 #限制访问ip为localhost any为所有主机 

listen.owner = www-data #启动进程的用户

listen.group = www-data #启动进程的用户组

#以上两个配置需要和Server 相同

pm.max_requests = 500 #设置每个子进程重生之前服务的请求数. 对于可能存在内存泄漏的第三方模块来说是非常有用的. 如果设置为 '0' 则一直接受请求. 等同于 PHP_FCGI_MAX_REQUESTS 环境变量. 默认值: 0.

;request_terminate_timeout = 0 #设置单个请求的超时时间,这个设置和 php.ini 中配置的max_execution_time 这个参数一样,当max_execution_time 失效时,request_terminate_timeout 会被使用。


[global]
8
pid = /run/php/php7.2-fpm.pid 

error_log = /var/log/php7.2-fpm.log #错误日志path

log_level = warning #默认为notice 

daemonize = yes #后台执行fpm,默认值为yes

Ubuntu+uwsgi+Nginx部署Flask应用

由于是第一次在Linux部署Python应用,过程中遇到很多坑,也找了很多部署博客的分享。再一次体会到好文章带你上天堂,坏文章带你瞎逼忙的道理。索性就记录这次部署的全过程,供以后参考。

介绍

首先先介绍下各个技术的功能,以及他们组合的大致流程。部署的是一个web应用,从用户打开浏览器访问网页开始,到浏览到网页内容,这个过程就是各个技术实现功能的过程。

整体结构

  • 用户浏览器(客户端)打开网页,向服务器发起请求;
  • 请求传给Nginx服务器,Nginx将请求发给uWSGI;
  • uWSGI服务器发来的请求翻译为应用程序理解的形式,发给应用;
  • Flask应用接收请求并处理,将响应结果发给uWSGI;
  • uWSGI与Nginx服务器通信,将结果传给他;
  • Nginx服务器收到响应结果,将其传给客户端;
  • 浏览器显示响应结果,并进行下一个请求。

安装Python环境

阿里云Ubuntu服务器自带的Python2.7和Python3.4,所以尽管我的应用是Python3程序,也不必重新装Python3。

更新apt-get

sudo apt-get update

获取应用源码

由于我的代码放在github仓库,直接通过git来安装。首先安装git:

sudo apt-get install git

安装完成后,在用户目录中新建project目录mkdir project,存放我们的应用程序。不知道是不是在用户目录可以输入指令pwd查看。我们转到project文件夹下,使用git克隆项目源码:

git clone https://github.com/Blackyukun/YuBlog.git

转到项目目录cd YuBlog

安装pip和virtualenv

sudo apt-get install python-pip
sudo apt-get install python-virtualenv

创建虚拟环境

这里需要注意的是,如果直接virtualenv venv命令,创建的将会是Python2的虚拟环境。如果想要创建Python3的环境,需要指定Python3的目录:

virtualenv -p /usr/bin/python3 venv

如果成功,项目目录下会生成一个venv目录,那里就是我们的python3虚拟环境了。接下来激活虚拟环境:

source /home/xyy/py2env/bin/activate

退出虚拟环境命令是:deactivate

安装依赖包

如果项目实在虚拟环境中完成的,那么通常我们会使用pip freeze >requirements.txt命令列出项目所有依赖。然后当我们安装这些依赖的时候只需要使用命令:

pip install -i http://pypi.douban.com/simple/ -r requirements.txt

如果全部安装完成,那么我们的程序依赖环境全都准备好了。

安装Mysql数据库

我的程序是使用Mysql数据库做存储的,安装它也很简单,但是这里会有一个阿里云服务器的大坑。

sudo apt-get install mysql-server mysql-client
sudo apt-get install libmysqlclient-dev

安装过程中会需要你输入用户以及密码,暂且就使用root和password吧。

sudo netstat -tap | grep mysql命令检查Mysql是否安装成功,如果mysql的socket处于listen状态则表示安装成功。

登录mysql数据库命令:mysql -u root -p这里的root就是之前安装是设置的用户名,接着输入密码password。在Linux上,我们需要修改mysql的默认编码为utf-8,以便正确地处理中文。

这里需要编辑MySQL的配置文件,把数据库默认的编码全部改为UTF-8。MySQL的配置文件默认存放在/etc/my.cnf或者/etc/mysql/my.cnf:

vim /etc/mysql/my.cnf

linux使用的是vim编辑器,不了解vim的可以自行了解。我们按i进去插入模式,将下面的指令粘贴到对应位置:

[client]
default-character-set = utf8
[mysqld]
default-storage-engine = INNODB
character-set-server = utf8
collation-server = utf8_general_ci

把队应指令放在对应地方就好了。配置完成后,在vim编辑器下,按ESC进入普通模式,键入:wq进行保存并退出。show variables like ‘%char%’;指令查看编码是否设置正确。如果看到utf8就表示正确。

未分类

接着重启数据库:

service mysql restart

重新登录mysql -u root -p创建我们的数据库,使用:create database mydb;创建名为mydb的库名(注意后面封号)。

中文乱码

这里我们会遇到一个坑,就是在后面程序启动保存数据的时候会出现中文乱码,但是我们明明已经编辑过默认编码了呀。这里我发现是阿里云服务器本身没有安装中文包,我们需要进行安装。

安装中文语言包

sudo apt-get -y install language-pack-zh-hans

修改语言环境设置

echo "LC_ALL=zh_CN.utf8" >> /etc/profile
echo "export LC_ALL" >> /etc/profile

查看语言

source /etc/profile
locale

看到zh_CN.UTF-8就成功了。接着需要重启服务器。

根本原因

虽然中文包安装成功了,但是这样就表示ok了吗?并没有,后来启动中网页中文显示依然乱码。我发现是保存数据库里的数据才会乱码,那么根本原因还是数据库编码问题。

我们需要在创建数据库时要同时定义他的默认编码:

删除数据库:mysql>delete database mydb;

创建数据库:mysql>create database mydb default character set utf8;

安装Nginx服务器

Nginx是一款轻量级的Web服务器/反向代理服务器及电子邮件IMAP/POP3代理服务器。其特点是占有内存少,并发能力强。用于接收HTTP请求并返回响应。安装:

sudo apt-get install nginx

启动:sudo /etc/init.d/nginx start

看到OK表示成功。接着需要配置Nginx。Nginx的配置文件在/etc/nginx/sites-available目录的default文件中,将其删除rm default。新的default创建并打开vim default,在里面写入:

server { 
  listen 80; # 80端口需要打开
  server_name X.X.X.X; #阿里云公网ip
  location / { 
  include uwsgi_params;
  uwsgi_pass 127.0.0.1:5000; # 指向uwsgi 所应用的内部地址
  uwsgi_param UWSGI_PYHOME /home/root/project/YuBlog/venv; # 虚拟环境目录
  uwsgi_param UWSGI_CHDIR /home/root/project/YuBlog; # 应用根目录
  uwsgi_param UWSGI_SCRIPT manage:app; # 启动程序
  uwsgi_read_timeout 100; 
 }  
}

重启Nginx:sudo service nginx restart

看到OK表示成功。如果失败,可以输入指令sudo nginx -t查找错误,进行处理。

安装uWSGI

uWSGI虽然也可以起到Web服务器的作用,那么为什么有了uWSGI还需要Nginx呢。具体的优势大家自行了解。在Nginx+uWSGI的结构中,它充当中间件的程序,是Web的通信协议。

安装:sudo pip install uwsgi

注意实在虚拟环境。安装成功后需要配置。我们在项目的根目录下也就是/home/root/project/YuBlog下,创建配置文件config.ini,添加内容:

[uwsgi]
master = true
home = venv
wsgi-file = manage.py
callable = app
socket = :5000
processes = 4
threads = 2

配置完成后,就可以启动uWSGI了。但在这之前,我们先启动应用程序,并添加程序必须的环境变量。

添加linux系统环境变量:export CONFIG=production

先创建迁移仓库:python manage.py db init

创建迁移脚本,migrate子命令用来自动创建:python manage.py db migrate -m “v1.0”

更新数据库操作:python manage.py db upgrade

创建管理员信息:python manage.py addAdmin

ctrl+c终止程序。

启动uWSGI: uwsgi config.ini

会看到很多信息,只要没有报错,就表明启动成功。

部署成功

如果Nginx和uWSGI全部启动成功,就说明部署已经成功了。打开外部浏览器,访问公网ip地址,就可以看到我们的程序已经跑起来了。

CentOS下nginx的编译安装与配置

一、安装编译工具以及库文件

[root@zfs src]# yum -y install make zlib zlib-devel gcc-c++ libtool  openssl openssl-devel

二、安装nginx依赖关系

nginx所需的依赖关系,一般我们都需要先装pcre, zlib, 前者为了重写rewrite,后者为了gzip压缩。如果系统已经yum安装了这些库也没关系,无需卸载。
直接编译安装最新的就可以了。为了一次性完成编译,先准备编译下面的依赖关系。

1. 安装PCRE库

[root@zfs src]# wget https://sourceforge.net/projects/pcre/files/pcre/8.40/pcre-8.40.tar.gz
[root@zfs src]# tar zxvf pcre-8.40.tar.gz 
[root@zfs src]# cd pcre-8.40
[root@zfs pcre-8.40]# ./configure 
[root@zfs pcre-8.40]# make && make install
[root@zfs pcre-8.40]# cd ../

2. 安装zlib库

[root@zfs src]# wget http://prdownloads.sourceforge.net/libpng/zlib-1.2.11.tar.gz
[root@zfs src]# tar zxvf zlib-1.2.11.tar.gz 
[root@zfs src]# cd zlib-1.2.11
[root@zfs zlib-1.2.11]# ./configure 
[root@zfs zlib-1.2.11]# make && make install
[root@zfs zlib-1.2.11]# cd ../

3. 安装openssl

[root@zfs src]# wget http://www.openssl.org/source/openssl-1.0.1g.tar.gz
[root@zfs src]# tar zxvf openssl-1.0.1g.tar.gz 
[root@zfs src]# cd openssl-1.0.1g
[root@zfs openssl-1.0.1g]# ./config 
[root@zfs openssl-1.0.1g]# make && make install
[root@zfs openssl-1.0.1g]# cd ../

三、安装nginx

准备工作完成以后,现在开始安装nginx

[root@zfs src]# wget http://nginx.org/download/nginx-1.12.2.tar.gz
[root@zfs src]# tar zxvf nginx-1.12.2.tar.gz 
[root@zfs src]# cd nginx-1.12.2
[root@zfs
 nginx-1.12.2]# ./configure --prefix=/usr/local/nginx 
--with-http_stub_status_module --with-http_ssl_module 
--with-pcre=/usr/local/src/pcre-8.40 
--with-zlib=/usr/local/src/zlib-1.2.11 
--with-openssl=/usr/local/src/openssl-1.0.1g
[root@zfs nginx-1.12.2]# make && make install

查看nginx版本

[root@zfs nginx-1.12.2]# /usr/local/nginx/sbin/nginx -v
nginx version: nginx/1.12.2

至此,nginx安装完成

四、nginx配置

创建nginx运行时用户nginx

groupadd nginx
useradd -g nginx nginx

然后修改nginx配置文件

vim /usr/local/nginx/conf/nginx.conf

将 #user nobody改成 user nginx;
检查nginx配置文件

[root@zfs nginx-1.12.2]# /usr/local/nginx/sbin/nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful

配置正确无误

[root@zfs nginx-1.12.2]# /usr/local/nginx/sbin/nginx

在浏览器中输入http://127.0.0.1 如果出现如下欢迎界面,表示nginx启动成功

未分类

Linux 下 Nginx + PHP 环境的配置

从我开始折腾 WordPress、Typecho 博客至今,我折腾了无数次 Nginx 的安装、配置与 PHP 环境的搭建,看过各种各样的教程,它们往往都有一个共同点,就是仅仅是给你一些现成的命令复制粘贴,它们大多从操作的角度出发,并没有太多原理上的阐述。就像之前我看到 火丁笔记博客的一篇文章 所说:“如果大家不求甚解,一味的拷贝粘贴,早晚有一天会为此付出代价。”所以我希望通过这篇文章,能够在一个不一样的角度去描述这个过程,希望能对看到这篇文章的你有所帮助。

本文假定你对 Linux 的命令、程序的文件IO、HTTP 协议、PHP 有一些大致的了解。

服务器后端做的事情

首先我们应该对网站的结构有一个基本的认识,通常来说一个网站的分为前端与后端两部分。它们相互依存,前端在用户的浏览器中,负责内容的显示和交互。后端负责处理浏览器发来的请求,根据不同的请求生成对应的响应返回给前端展示给用户。

浏览器与服务器之间需要通信,必然得遵循一些标准,一个必备的标准就是 HTTP 协议啦。(可能有的网站会用WebSocket等协议,但这篇文章只讨论HTTP的内容)

从本质的角度来看,后端的主要工作就是处理浏览器发来的 HTTP 请求,并根据服务器程序的业务逻辑返回这些请求对应的 HTTP 响应给浏览器。后端返回的响应,可以是一段 HTML/CSS/JavaScript 代码、也可以是一张图片、一段音频、或者是一个个不同类型文件。

我们可以这么说,无论这个后端是什么程序编写的,只要它能依照 HTTP 协议返回符合协议规定格式的响应就可以满足要求。因此,与前端只能用 HTML/CSS/JavaScript 三件套不同,后端的语言,平台的选择可以有很多,例如 PHP, Python, Ruby, JavaScript(Node.js), Java, Go, C++, C 等等。PHP 只是其中的一种选择,由于 PHP 的部署简单,性能也不错,所以也十分流行。

Web服务器

后端处理 HTTP 请求,主要还是通过 Web 服务器来实现,Web 服务器返回响应的过程主要有两条途径:

  1. 返回请求的对应的磁盘文件
  2. 把请求分发给其他的程序处理,并把该程序处理的结果返回。

如图:

未分类

其中,这里的请求分发的过程,也叫作反向代理(Reverse Proxy)。

一般来说用的比较多的 Web 服务器软件有 Nginx、Apache 和 Lighttpd,它们都是 C 语言编写的,其中因为 Nginx 的配置较为简单,高性能且资源占用较少,在业界备受欢迎,这里我也选择了 Nginx 作为网站的 Web 服务器。

PHP的执行机制

PHP脚本的执行

PHP的全称是,PHP: Hypertext Preprocessor,中文名为“超文本预处理器”。也就是说,PHP主要还是为了处理文本而产生的,这从它的代码中也有体现,我们来尝试一个简单的例子:

新建一个文本文件,命名为 temp.php ,里面输入以下内容:

这是php标签外的内容
---------华丽丽的分割线---------
<?php
$a = 2333 + 6666;
echo "这是PHP标签内部的内容,将会返回一个运算结果n";
echo "2333+6666=" . $a;
?> 
---------又是一条华丽丽的分割线---------
这是php标签外部的内容
---------下一条华丽丽的分割线---------
<?php
$b = 12345;
echo "变量$b的值为" . $b;
?> 
---------最后一条华丽丽的分割线---------
php标签外部的内容

把 temp.php 交给PHP解释器执行,这里我以Linux命令行为例,下面是这段脚本运行后的结果:

未分类

也就是说,PHP 解释器在处理文本的时候,把 之外的内容直接输出到了标准输出流中,当遇到 之间的代码时,它运行了代码,再把代码中 echo 语句的结果输出到标准输出流中。

我们可以通过重定向的操作,把 PHP 解释器的标准输出流重定向到别的地方,例如,我这里把它的输出结果重定向到与代码同目录的 result.txt 里面。

这时候我们试一下把 result.txt cat出来,发现里面的内容也和之前的输出一样。

未分类

ps: 如果你对数据流的概念不熟悉,可以参考《鸟哥的 Linux 私房菜》关于流的描述

通过Web服务器运行PHP脚本

我们知道,PHP 这门语言主要应用在 Web 的领域中,所以一般 PHP 文件都是通过 Web 服务器来触发运行的。

为了方便测试,我们可以用下面的命令可以直接在 temp.php 所在的文件夹下启动一个临时的 PHP cli 开发服务器,就不用配环境那么麻烦了:

php -S 0.0.0.0:8888

未分类

在浏览器访问 http://0.0.0.0:8888/temp.php 或者 http://127.0.0.1:8888/temp.php ,查看源代码,我们可以看到如图所示的结果:

未分类

我们可以发现,浏览器中看到的内容,和我们之前用 PHP 运行这个脚本生成的输出结果是一样的。

忽略细节的话,在某种意义上我们也许也可以这么说,PHP 服务器程序在收到浏览器发过来的请求之后,运行脚本,把脚本的标准输出流重定向到了浏览器中,就像之前把命令行运行的结果重定向到了 result.txt 一般。

相比通过文件存储的静态网页,类似PHP每次接到请求后通过解释器执行,执行的结果来返回数据的页面,因为数据会根据实际情况而变化,通常也被称为“动态网页”。

CGI协议

随着动态网页的流行,这种服务器接收请求,执行程序,把执行程序的输出返回给浏览器的过程也逐渐成型。规范化这个过程也变得有必要起来,由此,CGI协议便诞生了。

CGI (Common Gateway Interface),中文名是“通用网关接口”,它定义了Web服务器与处理请求的程序之间传输数据需要遵循的标准。

一般来说,程序运行时,它与外界交互的途径是标准输入(stdin)、标准输出(stdout)和环境变量,有的可能涉及到其它的文件IO,CGI协议定义了HTTP请求、HTTP响应与程序运行的环境变量、输入流、输出流的对应关系,从而实现了通过后端程序来处理HTTP请求的功能,这个程序也被称为CGI中的“网关”(Gateway)。关于CGI协议的具体规定,我们可以参阅 RFC3875 的文档描述。 https://tools.ietf.org/html/rfc3875

CGI协议规定了,每一个HTTP请求交给一个网关程序的进程来处理。这个HTTP请求的请求头(Header),QueryString,以及其它关于客户端的信息,作为网关程序运行的环境变量,这个HTTP中的请求体(Body),作为网关程序运行的标准输入(stdin);网关程序执行过程中的标准输出(stdout),则作为这个HTTP请求的响应数据返回给Web服务器。

参考 http://www.php-internals.com/book/?p=chapt02/02-02-03-fastcgi 的介绍,引用一下:

CGI 的运行原理

  1. 客户端访问某个 URL 地址之后,通过 GET/POST/PUT 等方式提交数据,并通过 HTTP 协议向 Web 服务器发出请求。

  2. 服务器端的 HTTP Daemon(守护进程)启动一个子进程。然后在子进程中,将 HTTP 请求里描述的信息通过标准输入 stdin 和环境变量传递给 URL 指定的 CGI 程序,并启动此应用程序进行处理,处理结果通过标准输出 stdout 返回给 HTTP Daemon 子进程。

  3. 再由 HTTP Daemon 子进程通过 HTTP 协议返回给客户端。

上面的这段话理解可能还是比较抽象,下面我们就通过一次 GET 请求为例进行详细说明。

未分类

如图所示,本次请求的流程如下:

  1. 客户端访问 http://127.0.0.1:9003/cgi-bin/user?id=1
  2. 127.0.0.1 上监听 9003 端口的守护进程接受到该请求
  3. 通过解析 HTTP 头信息,得知是 GET 请求,并且请求的是 /cgi-bin/ 目录下的 user 文件。
  4. 将 uri 里的 id=1 通过存入 QUERY_STRING 环境变量。
  5. Web 守护进程 fork 一个子进程,然后在子进程中执行 user 程序,通过环境变量获取到id。
  6. 执行完毕之后,将结果通过标准输出返回到子进程。
  7. 子进程将结果返回给客户端。

基于PHP语言的Web程序,它的工作机制也类似于CGI的模型,但根据实际的情况,PHP 的具体实现会有些不一样。在PHP中,CGI协议所用到的程序,是通过它自带的 php-cgi 来支持的,这个后文会继续介绍。

SAPI 抽象层

刚刚我们可以注意到,我们可以通过浏览器发送HTTP请求的方式执行PHP,也可以在命令行通过运行文件的方式执行PHP,每种运行方式看起来很不一样,但是实际上它们内部的工作流程是一样的。这得益于PHP代码中的SAPI中间层。

那么,什么是SAPI呢?

首先我们来看看PHP的架构图(图片来自鸟哥的博客 ps: PHP的鸟哥和写 Linux 私房菜的鸟哥不是同一个人哦)

未分类

从图片中可以看出,PHP内部从下到上分为4层:

  • 负责 PHP 的执行的 Zend 引擎
  • Extensions 扩展层,各种基础的库和扩展都在这一层实现
  • SAPI:Server Application Programming Interface,服务端应用编程接口,它通过一些钩子函数,定义 PHP 与外部应用的交互,通过它可以实现 PHP 与上层应用的隔离,我们可以基于SAPI编写不同的应用适应不同的环境。
  • Application 层,这里代表了PHP应用的部分,如命令行下的脚本的执行,web服务器的脚本的执行等等

在这篇文章中,我们的关注点主要是在 Application 层与 SAPI 层的部分,理解了这些的话具体的部署自然就是水到渠成了。

对PHP的架构有了一些印象之后,我们可以知道,SAPI是一种不同的应用与PHP内核的交互方式,上层的应用通过SAPI定义的接口把代码和执行需要的环境变量,输入输出等数据交给PHP内核解析。

下面是一些常见的 SAPI 的应用实现:

  • Shell CLI: 通过命令行执行 PHP 程序用到的 SAPI。
  • Apache 2.0 Handler: 通过 Apache 服务器的 mod_php 模块部署 PHP 服务的运行方式
  • PHP 自带的 CGI/FastCGI 接口: PHP 本身实现了一个名为 php-cgi 的程序,它有 CGI、FastCGI 两种工作模式,专门处理 CGI/FastCGI 的请求
  • PHP-FPM: 这是一个 PHP 专用的 fastcgi 管理器,克服了 php-cgi 本身的一些问题,并且附加了许多适合大流量高并发网站的功能

早期的 PHP 为了适配多种多样的Web服务器环境,内置了许许多多的 SAPI ,到PHP 7以后,只保留了一部分重要的 SAPI,其它的都已经移除,下面是 PHP 7 以后移除的SAPI列表:(来自菜鸟教程)

aolserver, apache, apache_hooks, apache2filter, caudium, continuity, isapi, milter, nsapi, phttpd, pi3web, roxen, thttpd, tux, webjames

通过PHP的 php_sapi_name() 函数 我们可以获得当前PHP运行所使用的sapi的名字,实际的 PHP 代码编写中,我们可以根据这个函数的值判断程序所处的运行环境。

由于 SAPI 的多样,所以就有了许多不同的 PHP 部署方式,下面我会介绍一些常见的部署方式。

通过加载 Module 方式部署 PHP

Web 服务器除了可以通过 CGI 执行动态脚本外,还可以通过加载模块的方式来运行动态脚本,例如 Apache 的环境中是通过 mod_php 模块来实现运行PHP的,它利用了 Apache 2.0 Handler 这个 SAPI 与 PHP 解释器内核通信。

通过 Apache + mod_php 来部署 PHP 具有开箱即用,稳定成熟的特点,同时也有一些缺点:

  1. Web 服务器与 PHP 解释器之间是耦合的,程序出问题的时候不好定位是 Apache 的问题还是 PHP 这一层的问题
  2. 由于PHP的执行用户是与 Apache 相同的,这某些情况下可能有安全隐患
  3. 这种方式对于高并发大流量的场景下的性能消耗较大

所以我个人不太推荐通过这种方式在实际生产环境中部署PHP,当然,本地的开发环境还是挺适合的,尤其是Windows环境下。

FastCGI

上文提到了 FastCGI,它究竟是何方神圣呢?

首先我们回顾一下刚刚提到的CGI,它每次接到请求的时候,都需要根据请求的信息设置好参数,创建一个新的进程处理这个请求,处理完毕后退出程序,又叫 fork-and-execute 模式。进程的创建和销毁是一个耗费资源的过程,当系统的并发量一大,基于CGI的程序就撑不住了。

顾名思义,FastCGI = Fast + CGI,它是一种为了提高 CGI 程序性能的协议,是早期的 CGI 协议的改进版本。

参考 http://www.php-internals.com/book/?p=chapt02/02-02-03-fastcgi 的介绍,引用如下:

FastCGI是Web服务器和处理程序之间通信的一种协议, 是CGI的一种改进方案,FastCGI像是一个常驻(long-lived)型的CGI, 它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理(这是CGI最为人诟病的fork-and-execute模式)。 正是因为他只是一个通信协议,它还支持分布式的运算,所以 FastCGI 程序可以在网站服务器以外的主机上执行,并且可以接受来自其它网站服务器的请求。

FastCGI 是与语言无关的、可伸缩架构的 CGI 开放扩展,将 CGI 解释器进程保持在内存中,以此获得较高的性能。 CGI 程序反复加载是 CGI 性能低下的主要原因,如果 CGI 程序保持在内存中并接受 FastCGI 进程管理器调度, 则可以提供良好的性能、伸缩性、Fail-Over 特性等。

FastCGI 工作流程如下:

  1. FastCGI 进程管理器自身初始化,启动多个 CGI 解释器进程,并等待来自 Web Server 的连接。
  2. Web 服务器与 FastCGI 进程管理器进行 Socket 通信,通过 FastCGI 协议发送 CGI 环境变量和标准输入数据给 CGI 解释器进程。
  3. CGI 解释器进程完成处理后将标准输出和错误信息从同一连接返回 Web Server。
  4. CGI 解释器进程接着等待并处理来自 Web Server 的下一个连接。

未分类

FastCGI 与传统 CGI 模式的区别之一则是 Web 服务器不是直接执行 CGI 程序了,而是通过 Socket 与 FastCGI 响应器(FastCGI 进程管理器)进行交互,也正是由于 FastCGI 进程管理器是基于 Socket 通信的,所以也是分布式的,Web 服务器可以和 CGI 响应器服务器分开部署。Web 服务器需要将数据 CGI/1.1 的规范封装在遵循 FastCGI 协议包中发送给 FastCGI 响应器程序。

相比 CGI,这里又多了个需要我们考虑的程序 —— FastCGI 进程管理器。还有一个通信协议——FastCGI 协议。同时,HTTP 请求也不是 Web 服务器自己处理了,而是封装成 FastCGI 数据包发送到负责 FastCGI 的服务器。

PHP 本身实现了一个名为 php-cgi 的程序,它有 CGI、FastCGI 两种工作模式,专门处理 CGI/FastCGI 的请求。

PHP-FPM

刚刚我们有提到,PHP 可以通过内置的 php-cgi 程序的 FastCGI 模式实现 FastCGI 进程管理器的功能,解决了 CGI 的资源占用和并发的性能问题,同时也支持了分布式计算。然而,在需求日益增长的时候,php-cgi也暴露出了一些问题。最大的问题是,php-cgi 的配置不够人性化,主要体现在其修改 php.ini 后,不支持平滑重启,每次都要先停止服务再启动才能更新配置,这在某些场景下显然是很致命的。

为了解决上面的问题,一位名叫 Andrei Nigmatulin 的大神开发了 PHP-FPM,打破了这个尴尬的局面。

PHP-FPM 是一个为 PHP 量身打造的专用 FastCGI 进程管理器,它解决了上面的问题,并且在许多方面表现十分出色,因此在 PHP 5.3 版本以后,PHP-FPM 已经正式内置在 PHP 中了。官网关于 PHP-FPM 的介绍

综上,需要部署 PHP 环境的话,Apache/Nginx + PHP-FPM 是优于CGI 和 Module 加载的一个很好的选择,下面我就以 Nginx 为例,介绍一下 Nginx 和 PHP-FPM 的配置方法。

Nginx与PHP-FPM的配置

首先我们得装好 Nginx,PHP 和 PHP-FPM,具体安装过程可以参考其它的教程。

由于不同的发行版的安装后的文件路径不太一样,所以这里只会提到一些比较关键的配置部分。

我们需要明确 Nginx、PHP-FPM 各自的角色,Nginx 本身可以是一个提供静态文件分发的Web服务器、也可以是一个反向代理服务器,它的工作模式十分灵活,取决于我们怎么配置Nginx。在这里我的预期是,当 Nginx 收到请求以后,如果请求的是静态文件,那么将这个静态文件返回;如果它是一个要执行 PHP 程序的请求,Nginx 需要将其转发到 PHP-FPM 处理,PHP-FPM 收到请求以后,调用 PHP 内核执行 PHP 脚本,把脚本的输出返回给 Nginx,Nginx 再把响应通过 HTTP 响应的方式返回给用户。

上文我们提到了,FastCGI 的请求、响应是遵循 FastCGI 协议的,而 Web 服务器处理的是 HTTP 协议,所以 Nginx 与 PHP-FPM 一起工作时,就涉及到了 HTTP 协议的请求、响应与 FastCGI 协议的请求、响应的互相转换。这个转换的工作,是通过 Nginx 内置的 fastcgi 模块来实现的。所以,我们需要解决的问题是,如何配置 Nginx,调用 fastcgi 模块来让需要执行PHP的请求正确地转发到 PHP-FPM 中运行呢?

这里我们需要关注两个配置文件,一个是 Nginx 的 nginx.conf ,另一个是 PHP-FPM 的 php-fpm.conf

PHP-FPM 的配置文件

首先是 php-fpm.conf,这是 PHP-FPM 主要的配置文件,不同的安装方式的路径可能不一样,用 Ubuntu 16.04 的 apt 安装的 PHP-FPM,路径位于 /etc/php/7.0/fpm/php-fpm.conf ,如果是手动编译安装的话,假设 prefix 是 /usr/local/php,它一般会位于 /usr/local/php/etc/php-fpm.conf 。

php-fpm.conf 里面默认是 PHP-FPM 的基本配置,这里一般没有多少需要改动的地方,我们的目光放在最后一行的 include=xxxxx ,apt 安装的 PHP-FPM 默认是 include=/etc/php/7.0/fpm/pool.d/.conf ,手动编译安装的是 include=/usr/local/php/etc/php-fpm.d/.conf ,顾名思义,这是一个 include 操作,就是引入 pool.d 或者是 php-fpm.d 目录下所有的 .conf 文件的意思。这个目录下的文件主要的功能是定义了 php-fpm 监听端口等的信息。

定位到 pool.d (也可能是 php-fpm.d ) 目录下可以发现,它里面一般只有一个 www.conf,打开里面的内容,我们可以看到,里面每个配置项前面,都有一大堆详细的注释。这个文件是我们要配置 PHP-FPM 如何处理 PHP 的关键,它定义了 PHP-FPM 监听哪个端口或是 unix socket 的 FastCGI 请求,脚本执行环境的用户,用户组,权限等等。

一般如果对权限没有特殊要求的话我们不需要对它进行修改。这里我们需要记下里面 listen 选项的内容,listen 的值可以分为两种,一种是 TCP socket 地址,另一种是 unix socket 地址。如果我们这里看到的 listen 的值可能是 127.0.0.1:9000,它是 TCP socket,如果它是一个具体的文件路径类似 /run/php/php7.0-fpm.sock 的值,那么它是一个 unix socket,unix socket 是一种进程间的通信方式,它的操作类似网络套接字,但它实际的数据传输是不用经过网络层的,具体的内容可以查找相关的资料。

Nginx.conf 的配置

找到了 PHP-FPM 监听的 socket 之后,我们下一个目标就是配置 Nginx 让 .php 的请求转发到这个 socket 上了。

关于 Nginx 配置,推荐阅读官方文档 https://www.nginx.com/resources/admin-guide/nginx-web-server/#virtual-server 下面解释几个关键的部分

一般来说 Nginx 配置的基本结构是这样的,把 Nginx 用作 Web 服务器,则需要配置 nginx.conf 中的 http 块:

http {
    server {
        # Server configuration
        location / {

        }
        location ~ .php$ {

        }
    }
}

一个 http 块中可以有多个 server 块,每个 server 块代表了一个 Web 服务器,负责不同的域名、端口的请求的处理。在 server 块中,我们还可以配置不同的 location 块,每个 location 块都设置了它所匹配的 request URI 规则,符合规则的 request URI 将会跳到相应的 location 块中来处理。

location 语句块

利用 location 的强大功能,我们可以把所有 request URI 后缀为 .php 的请求交给一个 location 来处理,查询 Nginx 文档中关于 location 的描述,我们可以发现,location 的匹配 request URI 有两种方式:前缀匹配(prefix)和正则表达式匹配。.php 是后缀,所以需要正则表达式来处理,这里的正则表达式是 .php$ location 后的~ 代表匹配规则是一个正则表达式。

# 匹配 .php 后缀的 location
location ~ .php$ {

}

fastcgi 模块

启用 fastcgi 转发,需要用到 fastcgi 模块,在 location 块中配置与 fastcgi 相关的指令就可以了,官方文档对 fastcgi 模块的各个配置指令有很详细的介绍 http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html

一般我们使用的时候,我们只需用到 fastcgi 模块的其中两条指令,分别是 fastcgi_pass, fastcgi_param.

  • fastcgi_pass 这个参数是最最关键的,设置请求通过 fastcgi 协议转发的地址,需要设置为我们刚刚找到的 PHP-FPM 监听的地址。具体用法参考官网文档
  • fastcgi_param 这条指令描述了请求的一些关键参数的设定,比如要执行的脚本文件路径,请求的 QueryString,请求方法(GET还是POST),请求是否为 HTTPS 等等。详情继续参考官网文档

fastcgi_params

fastcgi_param 一般的用法如下

fastcgi_param parameter value [if_not_empty];

parameter 为当前 fastcgi 协议请求的参数名,value 为 Nginx 要设定此参数的参数值。这个value可以是一个固定的值,也可以是一个变量。我们可以根据实际的需要,来设置不同的 paramter 的参数值 http://nginx.org/en/docs/varindex.html

parameter 中有一个参数是最关键的,它就是 SCRIPT_FILENAME,这个参数定义了这个请求让 PHP-FPM 运行的 php 文件的完整路径,如果没有它,PHP-FPM 就不知道该运行什么脚本,将会返回一个内容为空白的 200 响应(https://nginx.org/en/docs/beginners_guide.html#fastcgi)。(实测在 PHP 7 环境中缺少 REQUEST_METHOD 也会有这种情况)

这个最关键的参数一般是这么设置的,意思是把 SCRIPT_FILENAME 设置为之前用 root 或 alias 设定的路径 + 脚本的相对路径:

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

举个例子,假如当前的网站根路径设置为 /var/www/html,我们访问的 .php 文件的地址是 http://example.com/test/test.php ,那么,这时候的 $document_root 的值为 /var/www/html ,$fastcgi_script_name 的值就是 /test/test.php 了。此时的 SCRIPT_FILENAME 将会被设置为 /var/www/html/test/test.php ,PHP-FPM 就会按照这个路径读取 php 代码了。

特殊情况下,我们可以直接把一个确定的路径代替 $document_root:

fastcgi_param  SCRIPT_FILENAME    /var/www/html$fastcgi_script_name;

除了最关键的参数外还有一系列的参数需要设置,参考 Nginx 的文档 PHP FastCGI Example | NGINX

大部分时候,fastcgi_param 的设置是固定的,由于太多人瞎粘贴配置,各种混乱,所以 Nginx 后来为我们提供了一个现成的 fastcgi_param 配置的集合,位于 nginx.conf 同目录的 fastcgi_params 和 fastcgi.conf 中,fastcgi_params 的内容如下:

fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  REQUEST_SCHEME     $scheme;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

所以,我们在配置 location 时,在 fastcgi_param 的设定上,我们设置完最关键的 SCRIPT_FILENAME 以后,只需要直接 include fastcgi_params; 就能完成任务了,下面是一个 location 配置 PHP 的例子。

location ~ .php$ {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include fastcgi_params;
}

这时候又衍生出了一个问题,fastcgi.conf 是做什么的呢?

fastcgi.conf 和 fastcgi_params 的内容相比,多了刚刚我们提到的最最关键的一行,其它完全一致。

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;

因为我们平常配置 PHP 环境的时候,直接指出文件的根路径反而会造成许多麻烦,配置文件也不灵活,所以Nginx 为了缓解这种情况,引入了一个 fastcgi.conf 来作为最一般的配置。

有了 fastcgi.conf ,我们的配置又可以更简单一些了~

location ~ .php$ {
    fastcgi_pass 127.0.0.1:9000;
    include fastcgi.conf;
}

关于 fastcgi_params 和 fastcgi.conf 的关系,可以参考这篇文章 FASTCGI_PARAMS VERSUS FASTCGI.CONF – NGINX CONFIG HISTORY

一些安全的因素

参考 https://huoding.com/2013/10/23/290 火丁笔记 的描述,我们还需要在 nginx 这一层判断一下访问的 PHP 文件是否存在,避免出现因为 php.ini 开启了 cgi.fix_pathinfo=1 导致任意后缀文件都能通过 PHP 解释器解析产生的可能引发的安全问题,虽然这个漏洞在高版本 PHP (>=5.3.9) 已经被补上,但是还是需要注意。这时候我们可以在负责 php 的 location 块增加一个 try_files 来解决。修改后的配置如下:

location ~ .php$ {
    try_files $uri =404;
    fastcgi_pass 127.0.0.1:9000;
    include fastcgi.conf;
}

PATH_INFO 的配置

许多著名的 PHP 程序、框架如 WordPress、Typecho、Moodle、ThinkPHP 等,会支持形如 /xxxx.php/archives/2333 的 request URI,比如说,Moodle许多文件的路径就是 http://xxx.com/lib/javascript.php/1512059879/lib/requirejs/jquery-private.js 这种类型。这样的URL看起来比较神奇,仿佛 php 文件就是一个文件夹一样,看起来也更加友好一些。

一般 PHP 程序要处理这样的 request URI ,是通过超全局变量 $_SERVER 的一个参数 $_SERVER[‘PATH_INFO’] 来实现的,这是一个很实用的参数,PHP 文档对它的描述如下:

包含由客户端提供的、跟在真实脚本名称之后并且在查询语句(query string)之前的路径信息,如果存在的话。例如,如果当前脚本是通过 URL http://www.example.com/php/path_info.php/some/stuff?foo=bar 被访问,那么 $_SERVER[‘PATH_INFO’] 将包含 /some/stuff。

我们可以看见,PATH_INFO 的信息必须是“客户端提供的”,也就是说需要由 Web 服务器提供,并且它的内容是跟在脚本名称后,在查询语句(QueryString)前的路径信息。Nginx 默认不会提供 PHP_INFO,因此,如果需要这个功能,我们需要为 Nginx 的 fastcgi_param 设置关于 PATH_INFO 的信息。

首先第一步我们要知道,面对 /xxx.php/xxxx 这样的链接,其实 Nginx 会把它当做一个文件夹来解析,而我们之前的配置是必须保证 request URI 是 .php 结尾的,所以我们第一个目标,就是把 /xxx.php/xxxx 能交给处理 PHP 的 location 块。我们变通一下,增加一种匹配 xxx.php/ 的情况:

至于为什么 .php 后一定要有 / 或者是 $(代表文件结尾),主要是考虑到一种风险,有的网站会有上传功能,若用户上传了一个 xxx.php.jpg ,配置不当的时候可能导致这个 xxx.php.jpg 传入 PHP 解释器,产生挂马的可能性。

location ~ .php(/|$) {

}

这时候请求已经可以转到这个 location 来处理了,但是,这时候,对于 /xxx.php/xxx,Nginx 转发给 PHP-FPM 的 fastcgi 请求中,SCRIPT_FILENAME 就成了 /xxx.php/xxx 。PHP 实际执行脚本的路径就需要依靠 PHP 内部去解析了,而且 PHP 并不知道 PATH_INFO 是什么。所以我们需要进一步操作,把正确的 SCRIPT_FILENAME 和 PATH_INFO 交给 PHP ,这里就用到了 Nginx 的 fastcgi_split_path_info 了。

fastcgi_split_path_info 在 Nginx 官方文档的描述是这样的 : http://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_split_path_info

Defines a regular expression that captures a value for the $fastcgi_path_info variable. The regular expression should have two captures: the first becomes a value of the $fastcgi_script_name variable, the second becomes a value of the $fastcgi_path_info variable. For example, with these settings
location ~ ^(.+.php)(.*)$ {
    fastcgi_split_path_info       ^(.+.php)(.*)$;
    fastcgi_param SCRIPT_FILENAME /path/to/php$fastcgi_script_name;
    fastcgi_param PATH_INFO       $fastcgi_path_info;

and the “/show.php/article/0001” request, the SCRIPT_FILENAME parameter will be equal to “/path/to/php/show.php”, and the PATH_INFO parameter will be equal to “/article/0001”.

使用 fastcgi_split_path_info ,需要设置一个含有两对括号正则表达式,第一个括号匹配的值将放入 $fastcgi_script_name 变量中,第二个括号的值是 $fastcgi_path_info 变量中,结合之前提到的 fastcgi_param 设置,SCRIPT_FILE_NAME 就已经正确设置啦,不用劳烦 PHP 去解析了,同时,我们再设置一个 fastcgi_param PATH_INFO 就能把 PATHINFO 设置好了。

这种情况的 location 配置如下:

location ~ .php(/|$) {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_split_path_info ^(.+?.php)(.*)$; #加入 ? 是为了避免类似 /test.php/t.php 匹配出错
    fastcgi_param PATH_INFO $fastcgi_path_info;
    include fastcgi.conf;
}

但这个配置还有一些问题,它没有考虑当访问请求的PHP不存在的时候的情况,访问文件不存在的 PHP 请求还是交给了 PHP-FPM 处理,仍然存在一些风险,一般是可以忽略不计的,但是为了完美还是要改进改进。

之前的配置我们用了 try_files $uri =404; 由于这时候的 request URI 并没有一个文件与之对应,所以使用 try_files $uri =404; 的话,肯定是直接返回 404 Not Found 。或许我们会想,request URI 没有对应的文件,那我们改成 try_files $fastcgi_script_name =404; 不就好啦!

这时候的配置类似这样,值得注意的是,Nginx 的配置并不会像一门编程语言一样有先后执行顺序,经过测试,fastcgi_split_path_info 有着更高的优先级,所以 try_files 不管加在哪一个位置,都能得到正确的结果,这里放在 fastcgi_split_path_info 后面是为了符合我们的直觉。

location ~ .php(/|$) {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_split_path_info ^(.+?.php)(.*)$;
    try_files $fastcgi_script_name =404;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    include fastcgi.conf;
}

但这样的配置又衍生出了新的问题,使用这个配置的时候,虽然 PHP 可以正常执行,但PHP脚本是获取不到 PATH_INFO 信息,这是为什么呢?

我们需要理解一下 try_files 指令的执行流程。根据 Nginx 关于 try_files 的文档 :

Checks the existence of files in the specified order and uses the first found file for request processing; the processing is performed in the current context. The path to a file is constructed from the *file* parameter according to the root and alias directives. It is possible to check directory’s existence by specifying a slash at the end of a name, e.g. “$uri/”. If none of the files were found, an internal redirect to the *uri* specified in the last parameter is made.

try_files 会依次检测传入的参数对应的文件路径是否存在对应的文件,若存在,则按参数所在 location 对应的 request URI 来处理,正如我标注的第一句加粗字体所述,try_files 的处理是取决于这个语句所在的上下文的,当这个 location 设置了 fastcgi_pass ,则这个请求会交给 fastcgi 模块发到后端 PHP-FPM 处理,如果这个 location 内部什么都没有,则会按照默认的文件读取返回来处理;如果前面所有的参数都不匹配的话,则会跳转到最后一个参数描述的内部重定向请求,跳到其他的 location 或返回状态码。

参考科大LUG老板踩过的 https://servers.ustclug.org/2014/09/nginx-try_files-fallacy/

回到刚刚的 try_files $fastcgi_script_name =404; 这里首先尝试的是 $fastcgi_script_name ,也就是说,跳到这一步以后,交回给这个 location 处理的 request URI 变成了 $fastcgi_script_name 这个变量所代表的,到了 .php 以后就结束。原本的 PATH_INFO 已经丢失了,这时候再 fastcgi_split_path_info 显然,$fastcgi_pathinfo 的值也变成了空白。

但是,我们第一步我们已经拿到了正确的 PATH_INFO 了,我们有没有什么办法可以把这个变量临时保存下来而不是在下一次匹配的时候就被覆盖掉呢?答案当然是有的~

这里就需要用到 Nginx 的 rewrite 模块的变量机制了,关于变量,我找到一篇讲解十分详细的文章 Nginx 变量漫谈(一)agentzh新浪博客http://blog.sina.com.cn/s/blog_6d579ff40100wi7p.html

这里用到的指令是 set ,变量需要注意的一点是,变量设置以后,变量名的可见范围是整个 Nginx 配置,在不同的请求中,变量值是独立的。所以,我们可以定义一个变量取名为 $real_path_info ,用来暂时存放这个值,增加了 set 指令以后的配置如下:

location ~ .php(/|$) {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_split_path_info ^(.+?.php)(.*)$;
    set $real_path_info $fastcgi_path_info;
    try_files $fastcgi_script_name =404;
    fastcgi_param PATH_INFO $real_path_info;
    include fastcgi.conf;
}

有些通用的程序为了兼容各种各样的服务器主机环境,它会使用多种不同手段来获取 .php 后面还存在的内容,所以它们也不一定用到 PATH_INFO 的方式,可能不需要配置得那么详细,但不代表我们不需要知道这些。只有在了解这些的情况下,遇到故障,我们也可以更容易更好地排查问题所在。

针对单入口程序的设置

许多 PHP 框架如 Laravel 采用了统一入口的方式,即它是通过 index.php 作为程序的入口,当我把 index 设置为 index.php 以后, /archives/2333 和 /index.php/archives/2333 就是等价的了,显然前者更加美观一些,这样还能实现“URI路由”操作,让不同 controller 来处理不同的 request URI,也就是我们常说的“伪静态”啦。

那么,“伪静态”该怎么实现呢,这里也用到了 Nginx 的 try_files 指令,方法是加一个 location / 的块:

location / {
  try_files $uri $uri/ /index.php$is_args$args
}
location ~ .php$ {
  fastcgi_pass 127.0.0.1:9000;
  include fastcgi.conf;
}

当访问的文件或目录不存在时,程序将重定向到 /index.php 处理,后面的 $is_args$args 是因为重定向以后 QueryString 丢失了,需要加回来。

我们可以发现,按照这样的配置重定向以后,request URI 变成了 /index.php?xxxx 了, 传给 PHP 的 PATHINFO 信息也丢失了 (https://trac.nginx.org/nginx/ticket/321) ,所以一般这种单入口的程序或框架会有许多判断,一般这些程序会通过 REQUEST_URI 参数来获得 index.php 后的路径信息,实现路由。

需要把路径信息传递到 PATH_INFO 的话,还有一种方案,就是通过 rewrite 来实现,假若文件不存在,则在 request URI 之前加上 /index.php 再继续解析。

参考 Typecho 的源码 (https://github.com/typecho/typecho/blob/master/tools/default_site) :

server {
    listen 80 default_server;
    server_name _;

    root /www;
    index index.html index.php;

    if (!-e $request_filename) {
        rewrite ^(.*)$ /index.php$1 last;
    }

    location ~ .php(/|$) {
      fastcgi_pass 127.0.0.1:9000;
      fastcgi_split_path_info ^(.+?.php)(.*)$;
      fastcgi_param PATH_INFO $fastcgi_path_info;
      include fastcgi.conf;
    }

}

这时候也不需要 try_files 了。

可以复制粘贴的 server 配置总结

注意把 fastcgi_pass 设定为服务器中 PHP-FPM 监听的连接。

如果只是想单纯地配一个 .php 的解析

server {
    listen 80 default_server;
    server_name _;

    root /www;
    index index.html index.php;

    location ~ .php$ {
        try_files $uri =404;
        fastcgi_pass 127.0.0.1:9000;
        include fastcgi.conf;
    }

}

如果想让脚本支持 PATH_INFO

类似 Moodle 的文件下载那种

如果程序是通过 REQUEST_URI 来获取路径而不需要 PATH_INFO 的话:

server {
    listen 80 default_server;
    server_name _;

    root /www;
    index index.html index.php;

    location ~ .php(/|$) {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_split_path_info ^(.+?.php)(.*)$;
        try_files $fastcgi_script_name =404;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        include fastcgi.conf;
    }
}

如果一定要完整获取 PATH_INFO 可以用这个完美支持的版本:

server {
    listen 80 default_server;
    server_name _;

    root /www;
    index index.html index.php;

    location ~ .php(/|$) {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_split_path_info ^(.+?.php)(.*)$;
        set $real_path_info $fastcgi_path_info;
        try_files $fastcgi_script_name =404;
        fastcgi_param PATH_INFO $real_path_info;
        include fastcgi.conf;
    }
}

如果部署的是一个单入口程序,并且对 PATH_INFO 没有要求

server {
    listen 80 default_server;
    server_name _;

    root /www;
    index index.html index.php;

    location / {
          try_files $uri $uri/ /index.php$is_args$args
    }

    location ~ .php$ {
        fastcgi_pass 127.0.0.1:9000;
        include fastcgi.conf;
    }

}

如果部署的是一个单入口程序,需要支持 PATH_INFO 的话

server {
    listen 80 default_server;
    server_name _;

    root /www;
    index index.html index.php;

    if (!-e $request_filename) {
        rewrite ^(.*)$ /index.php$1 last;
    }

    location ~ .php(/|$) {
      fastcgi_pass 127.0.0.1:9000;
      fastcgi_split_path_info ^(.+?.php)(.*)$;
      fastcgi_param PATH_INFO $fastcgi_path_info;
      include fastcgi.conf;
    }

}

关于 Nginx 配置文件的内部执行机理,博主目前还不懂=_=,有些问题也难以解释,现在只能是从外部的表现,官网文档一点点推敲和纠正,待博主有时间有能力搞清楚它的时候,博主会不断地修正这里的描述的,也希望能与各位高手多多交流完善。

以上是博主的一些理解与实践的经验,由于博主的水平有限,可能有一些地方的描述不太妥当,若你发现了本文有不妥甚至错误之处,希望可以尽快在评论区中指出。要深入地理解 Nginx + PHP 配置,还得多参考一下官方的文档、源代码和一些高质量的博客文章。

docker 制作自己的 php-fpm镜像

php-fpm的镜像官方已经有了,但是直接拿过来用或许不行,不同的项目所需要的扩展不一定一样。所以这里我们以官方的php-fpm 5.6版本为基础镜像,在这个镜像上制作我们自己的镜像。开发环境安装尽可能多的扩展,线上环境则安装所需要的扩展。

为了构建我们的镜像,首先需要一个名为Dockerfile的文件,docker会根据这个Dockerfile来构建镜像。

首先使用FROM指令,表示当前构建的镜像的基础镜像

FROM php:5.6-fpm

docker会从他自己的镜像库中拉取php-fpm5.6的镜像。

然后我们使用RUN指令来在这个镜像中执行一些指令。实际上RUN后面接着的就是linux的命令。比如apt-get,mkdir等等。

我们需要的是在这个镜像中安装一些php的扩展。可以使用RUN phpize,然后RUN make等一系列的命令来编译安装。不过docker里面内置了一个脚本,名为docker-php-ext-install,这个脚本会在/usr/src/php/ext的目录寻找扩展,并且编译安装,比我们自己编译然后写入配置要方便一些,所以我们就使用docker-php-ext-install来安装扩展啦。

比如我们需要安装redis

# install redis
RUN curl -L -o /tmp/redis.tar.gz http://pecl.php.net/get/redis-3.1.4.tgz 
&& tar xvf /tmp/redis.tar.gz 
&& rm -r /tmp/redis.tar.gz 
&& mkdir -pv /usr/src/php/ext 
&& mv redis-3.1.4 /usr/src/php/ext/redis 
&& docker-php-ext-install redis

上面的步骤就是首先下载redis,解压,移动到/usr/src/php/ext/里面,最后使用docker-php-ext-install编译和安装扩展就可以。同样,安装任何php的扩展都可以使用这个步骤来安装。

最后就是使用docker build命令来构建镜像,命令如下

docker build -t [镜像名称] [Dockerfile所在文件夹]

至此,一个我们自己的php-fpm镜像就构建完成,使用docker images看看镜像是不是已经存在啦!

Nginx配置禁止IP直接HTTP/HTTPS访问

这样配置可以禁止IP直接HTTP/HTTPS访问,也使未绑定的域名无法访问。

#http
server 
{
  listen 80 default_server;
  server_name _;
  return 500;
}

#https
server 
{
  listen 443 ssl default_server;
  server_name _;
  ssl_certificate      /yourpath/ssl.crt;
  ssl_certificate_key  /yourpath/ssl.key;
  return 500;
}

需要配置秘钥 否则会到时全部ssl配置失效 不知道为啥

yum install -y openssl
openssl genrsa -des3 -passout pass:x -out ssl.pass.key 2048
openssl rsa -passin pass:x -in ssl.pass.key -out ssl.key
openssl req -new -key ssl.key -out ssl.csr
…… 一路回车 ……
openssl x509 -req -days 3650 -in ssl.csr -signkey ssl.key -out ssl.crt

如何实现Nginx的防盗链功能?

未分类

1、前言

为了避免博客的图片等资源被盗链而增加网络开销,笔者需要实现防盗链功能。

2、实践部分

2.1 基础环境搭建

请参阅下文搭建http与https的LNMP环境,如果你已经有此环境,请直接跳过。
https://www.cmdschool.org/archives/1

2.2 配置防盗链

2.2.1 创建防盗链规则

mkdir /etc/nginx/global/
vim /etc/nginx/global/anti-theft-chain.conf

加入如下配置:

location ~* .(gif|jpg|png|webp)$ {
    root /var/www/www.cmdschool.org;
    valid_referers none blocked server_names
                                *.cmdschool.org cmdschool.*
                                ~.google. ~.baidu. ~.sogou.;
    if ($invalid_referer) {
        return 403;
        #rewrite ^/ http://www.cmdschool.org/403.jpg;
    }
}

注:留意”root”指令的配置(由于server{}标签没有定义root)

2.2.2 引用防盗链规则

vim /etc/nginx/conf.d/www.cmdschool.org_80.conf

将80端口的http服务配置文件修改如下:

server {
    listen       80;
    server_name  www.cmdschool.org;

    location / {
        root   /var/www/www.cmdschool.org;
        index  index.php;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    #引用防盗链规则
    include global/anti-theft-chain.conf;
}

注:综合之前的配置,配置文件的三个location优先级别请参考下表,

“=”精确匹配
“^~”不做模式匹配
“~”正则表达式的模式匹配
“~*”正则表达式的模式匹配
“” 无符号匹配模式

2.2.3 重载或重启服务

systemctl reload nginx

2.2.4 测试防盗链

2.2.4.1 向服务发送头Referer头模拟从百度引用图片链接

curl -I https://www.cmdschool.org/wp-content/uploads/2017/12/Nginx.png -H 'Referer:http://www.baidu.com'

信息显示如下:

HTTP/1.1 200 OK
Server: nginx/1.12.1
Date: Tue, 12 Dec 2017 05:18:40 GMT
Content-Type: image/png
Content-Length: 33308
Last-Modified: Sun, 10 Dec 2017 03:35:31 GMT
Connection: keep-alive
ETag: "5a2cab83-821c"
Accept-Ranges: bytes

2.2.4.2 向服务发送头Referer头模拟从QQ引用图片链接

curl -I https://www.cmdschool.org/wp-content/uploads/2017/12/Nginx.png -H 'Referer:http://www.qq.com'

信息显示如下:

HTTP/1.1 403 Forbidden
Server: nginx/1.12.1
Date: Tue, 12 Dec 2017 05:18:57 GMT
Content-Type: text/html
Content-Length: 169
Connection: keep-alive

Caddy、 SSLDocker、Nginx 性能比较及使用体验

Caddy、 SSLDocker、Nginx 都是可以用来做前端代理的服务,前两者是用go来写,部署比较简单。

Nginx 在部署HTTPS 时比较麻烦(相对其它两者来说),Caddy、 SSLDocker 都是自动配置并且更新HTTPS,这对我这样的懒人来说很有用。个人一直用Nginx (openresty) 1,后来在go 的世界发现了Caddy 4名器,然后在解决多域名反向代理时发现了SSLDocker 10小神器。

以下是在一台128MB单核的VPS 上部署一个应用,然后分别用Caddy、 SSLDocker、Nginx做前端,反向代理到该应用端口, 在另外一台VPS 做并发请求。开启ssl、gzip,使用hey 1 做并发请求:

# ./hey -n=20000 -c=5 https://mydomain.com/

Caddy、 SSLDocker、Nginx 并发测试结果

Caddy 并发请求测试结果

Summary:
  Total:    64.9214 secs
  Slowest:  0.7156 secs
  Fastest:  0.0031 secs
  Average:  0.0161 secs
  Requests/sec: 308.0650

Response time histogram:
  0.003 [1] |
  0.074 [19888] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  0.146 [2] |
  0.217 [2] |
  0.288 [2] |
  0.359 [90]    |
  0.431 [1] |
  0.502 [11]    |
  0.573 [1] |
  0.644 [1] |
  0.716 [1] |

SSLDocker 并发请求测试结果

Summary: 
  Total:    63.0618 secs 
  Slowest:  0.4883 secs 
  Fastest:  0.0030 secs 
  Average:  0.0156 secs 
  Requests/sec: 317.1490 

Response time histogram: 
  0.003 [1] | 
  0.052 [19865] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎ 
  0.100 [1] | 
  0.149 [2] | 
  0.197 [1] | 
  0.246 [0] | 
  0.294 [15]    | 
  0.343 [95]    | 
  0.391 [0] | 
  0.440 [2] | 
  0.488 [18]    | 

Nginx (openresty)并发请求测试结果

Summary:
  Total:    57.8501 secs
  Slowest:  0.0523 secs
  Fastest:  0.0029 secs
  Average:  0.0144 secs
  Requests/sec: 345.7212

Response time histogram:
  0.003 [1] |
  0.008 [539]   |∎∎
  0.013 [4327]  |∎∎∎∎∎∎∎∎∎∎∎∎∎
  0.018 [13150] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  0.023 [1397]  |∎∎∎∎
  0.028 [404]   |∎
  0.033 [120]   |
  0.037 [35]    |
  0.042 [18]    |
  0.047 [4] |
  0.052 [5] |

都很稳定,没有失败请求,从 Requests per second (RPS)看:

Caddy 308 < SSLDocker 317 < Nginx 345

三者相差不大,Nginx 和SSLDocker 稍有优势。

顺便测一下应用裸跑的结果,ssl、gzip 为on,RPS为367

Summary:
  Total:    54.3886 secs
  Slowest:  0.4766 secs
  Fastest:  0.0026 secs
  Average:  0.0136 secs
  Requests/sec: 367.7241

Response time histogram:
  0.003 [1] |
  0.050 [19995] |∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎∎
  0.097 [2] |
  0.145 [0] |
  0.192 [0] |
  0.240 [0] |
  0.287 [1] |
  0.334 [0] |
  0.382 [0] |
  0.429 [0] |
  0.477 [1] |

再测ssl、gzip 为off 情况,具体数据就不贴了,总对比如下:

# 数值为每秒请求数 RPS 
# ssl、gzip on 
Caddy 308 < SSLDocker 317 < Nginx 345 < 裸跑 367
# ssl、gzip off
Nginx 376 < 裸跑 424

拿nginx 反向代理和裸跑对比:

开启ssl、gzip 时性能损失 float(367-345)/367 = 5.99%,
关闭ssl、gzip 时性能损失 float(424-376)/424 = 11.32%。

配置文件

Caddy 配置

mydomain.com {
    gzip
    proxy  / 127.0.0.1:8999
}
mydomain2.com {
    proxy  / 127.0.0.1:8888
}

多个服务

SSLDocker 配置

{
  "Email": "[email protected]",
  "GzipOn": true,
  "Http2https": true,
  "MaxHeader": 10,
  "Certs": "certs",
  "ProxyItems": [
    {"Host": "mydomain.com", "Target": "http://localhost:8999"},
    {"Host": "mydomain2.com", "Target": "http://localhost:8888"}
  ]
}

也是两个服务

Nginx 配置

主配置nginx.conf 使用默认,使用epoll 模式

server {
    listen 443;
    server_name mydomain.com;

    ssl on;
    ssl_certificate /root/ssl/chained.pem;
    ssl_certificate_key /root/ssl/domain.key;

    ssl_session_timeout 5m;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA;
    ssl_prefer_server_ciphers on;

    location ^~ /.well-known/acme-challenge/ {
        alias /var/www/myapp/;
        try_files $uri =404;
    }

    location / {
        proxy_pass_header Server;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Scheme $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:8999;
    }
}

总结

上面的比较是针对个人的使用场景:在一个小VPS 上多挂几个网站而且都使用HTTPS,嫌配置HTTPS 麻烦。以前使用Nginx 则需要注册、配置、验证、添加自动更新任务到Cronjob,如果用Caddy 或SSLDocker,对HTTPS 不用做任何配置,一切都自动完成,其实过程是一样,只是Caddy 和SSLDocker 把这些任务都集成了(go 协程管理真方便)。

Nginx 功能多,成熟稳定;Caddy 功能也在慢慢增多,试着以不同方式实现Nginx 的功能和新鲜的功能;SSLDocker 专注于HTTPS管理和反向代理。如果说Nginx 是成功中年人士,则Caddy 是年轻高富帅,SSLDocker 是嫩屌丝。

相关项目

  • Nginx (Openresty) https://openresty.org/en/

  • Caddy https://caddyserver.com/

  • SSLDocker https://ssldocker.com/

解决Nginx服务返回500状态码问题

最近鼓捣博客服务器,不经意间误操作改了一些目录的用户组及权限,导致博客文件上传功能失败,周末检查了一天依然没有解决问题,在不经意间意识到了问题所在,其实有时候一些小问题会花费一大波时间去解决,于是博主记录本篇,总结一下发现问题,调试,解决问题的过程,希望以后解决问题能直指根本。

发现问题

首先是在12月9号发行博客文件上传功能失败,查看前端请求,发现是服务端返回500状态码,表明发生服务端内部异常:

未分类

如上图接口返回详情所示,nginx服务异常,服务暂时转移了,当前不可访问。

分析并解决问题

其实问题比较简单,但是奈何博主当时短路了,因为博主在浏览器直接访问该异常接口时,出现如下提示:

未分类

于是搜索了关于wordpress关于Are you sure相关问题,结果跑偏了,跑去分析wordpress源码,分析上传文件php代码,分析php内存使用是否达到上限,结果分析完一下午过去了,发现其实nginx没有转发该上传请求至php-fpm服务,我应该首先去看nginx输出日志。

于是跑去查看nginx日志:

vi /var/log/nginx/error.log

哎,发现是空的,并没有异常日志,怎么回事?确认日志路径无误后就可以考虑是因为文件权限的原因了,执行ll或ls -al指令:

未分类

哎,果然,nginx文件目录所属用户是root,而nginx执行用户配置了nginx,于是将其修改成nginx服务配置文件内配置的用户:

chown -R nginx /var/log/nginx
  • -R代表迭代设置目录及其子目录;
  • 第二个参数为为目录指定的所属用户名;

另外设置nginx用户对于/var/log/nginx目录的权限等级:

chmod -R 755 /var/log/nginx

再次执行ll查看目录信息:

未分类

nginx输出日志文件权限没问题了,再次上传文件,发现依然返回500状态码,这时查看nginx异常日志文件:

vi /var/log/nginx/error.log

发现终于有日志了:

未分类

看到是权限问题Permission Denied,打开/var/lib/nginx/tmp/client_body/0000001失败,说明启动转接服务失败,那就需要修改/var/lib/nginx目录的权限,使用ll查看该目录信息,发现,果然所属用户被改为root了,于是再次设置:

chown -R nginx /var/lib/nginx

然后重启nginx服务:

systemctl restart nginx

再次上传文件,上传成功,问题得到圆满解决。