Ubuntu部署python3-flask-nginx-uwsgi-supervisor完美

安装虚拟环境

$ pip install virtualenv
$ pip install virtualenvwrapper

把虚拟机环境添加环境变量中

这个最好find / -name virtualenvwrapper.sh 看下位置

$ vi .bashrc
if [ -f /usr/local/bin/virtualenvwrapper.sh ]; then
    export WORKON_HOME=$HOME/.virtualenvs
    source /usr/local/bin/virtualenvwrapper.sh
fi

为flask项目创建一个虚拟环境

$ mkvirtualenv --python=python3 flask  #flask这个名字可以按自己需求修改,我项目是需要python3。所以选择 --python=python3,如果需要python2可以不加这个。
$ deactivate  #安装完虚拟环境后,先退出这个虚拟环境。

安装mysql数据库,安装数据这个没什么好提的网上有很多详细教程

$ apt install mysql-server mysql-client
$ apt install libmysqld-dev

安装nginx

$ apt install nginx   #这个安装也比较简单

安装supervisor

需要是python2 暂时不支持python3,这里有时候会遇到坑。pip install –upgrade pip 看看现在pip是什么版本。

$ vi /usr/local/bin/pip #如果发现pip是python3,不要慌用这个命令把第一行的python3修改python2 即可,如果是python2就无视这步
$ pip install supervisor #安装supervisor

安装uwsgi

需要注意flask项目需要的环境 选择python3 还是python2.

这个我的项目是python3,如果是python2创建虚拟环境就用python2。具体可以看上面的为项目创建虚拟环境

$ workon flask  #进入虚拟环境
$ pip install uwsgi  #这个之前装到虚拟环境里面

如果出现Failed building wheel for uwsgi执行下面语句

apt-get install python3-dev

项目文件创建

这个按自己需要创建,也可以按我这个创建

$ mkdir /www  #根目录下创建一个www
$ mkdir /www/wwwroot  #这个项目文件全部放这个理
$ mkdir /www/log #日志文件

uwsgi配置

uwsgi配置好后,可以测试下

uwsgi配置路径:/www/wwwroot/uwsgi.ini

$ cd /www/wwwroot #可以放到项目,按自己需求都可以
$ vi uwsgi.ini   #创建一个uwsgi配置文件

[uwsgi]
# 当前这个项目的路径
chdir           = /www/wwwroot
# 模块
module          = manage   #启动文件名 个人理解
# python的虚拟环境
home            = /root/.virtualenvs/python3
# 是否启用mater模式
master          = true
# 进程数
processes       = 2
# socket文件地址
socket          = /www/wwwroot/uwsgi.sock
# wsgi文件
wsgi-file       = /www/wwwroot/manage.py  #启动文件
# wsgi文件中的app变量
callable        = app
# socket文件的权限
chmod-socket    = 666

配置好后可以运行起来测试是否成功

$ workon python3 #进入虚拟环境
$ uwsgi --uid www --gid www --ini /www/wwwroot/uwsgi.ini #这个可以指定用户和用户组权限,也可以不指定。测试没能正常打开项目就往下面步骤继续配置

nginx配置

$ cd /etc/nginx/sites-enabled/   #切换到nginx默认配置目录
$ mv default default.bak #修改配置前先备份下配置
$ vi default
server {
        listen 80;
        server_name www.xxoo.com;
        charset utf-8;
        client_max_body_size 75M;
        access_log /www/log/xxoo.access.log;
        error_log /www/log/xxoo.error.log;

        location / {
                include uwsgi_params;
                uwsgi_pass unix:/www/wwwroot/uwsgi.sock; #这个.sock文件一定要和uwsgi配置中一样
        }
}

修改nginx配置/etc/nginx/nginx.conf ,第一行user www www ; Nginx用户及组:用户 组 按自己需求配置。详细配置参数网上自己找

supervisor配置

supervisor配置路径:/www/wwwroot/supervisor.conf

$ vi supervisor.conf
[program:python]  #这个python可以按自己需求写
# supervisor执行的命令
command=/root/.virtualenvs/py3-zqcms/bin/uwsgi --uid www --gid www --ini /www/wwwroot/uwsgi.ini
# 项目的目录
directory = /www/wwwroot
# 开始的时候等待多少秒
startsecs= 5   #按自己需求写
# 停止的时候等待多少秒
stopwaitsecs= 5 #按自己需求写
# 自动开始
autostart=true
# 程序挂了后自动重启
autorestart=true
# 输出的log文件
stdout_logfile=/www/log/supervisord.log
# 输出的错误文件
stderr_logfile=/www/log/supervisorderr.log

[supervisord]
# log的级别
loglevel=info

配置好后就运行

$ supervisord -c /www/wwwroot/supervisor.conf  #执行的时候注意是在python2环境

如何终止多余 Supervisor 进程?

$ ps -ef | grep supervisor  #查看
$ kill 4012 #结束进程

注意:uwsgi nginx supervisor我只是简单配置了下,各位可以按需求配置。详细配置参数网上有很多资料。哪里写错,可以留言告诉我。

nginx+uwsgi完美配置文件,解决“upstream prematurely closed connection”报错

这段时间在折腾django,一开始用单一的uwsgi控制web访问,虽然说没有什么大问题,但是很多东西没法配置,比如超时时间,uwsgi虽然有个“harakiri”配置项,但并没有什么作用。

未分类

所以终究还是需要接口nginx来做前端代理,但是在代理的出现了一个问题,前端一直没有响应,nginx错误日志(/var/log/nginx/error.log)中出现如下报错

2017/12/21 10:36:48 [error] 9056#0: *18 upstream prematurely closed connection while reading response header from upstream, client: 180.160.72.9, server: localhost, request: "GET /favicon.ico HTTP/1.1", upstream: "uwsgi://127.0.0.1:8000", host: "127.0.0.1:8002"

nginx中access.log返回:

180.160.72.9 - - [21/Dec/2017:10:25:28 +0800] "GET / HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36"
180.160.72.9 - - [21/Dec/2017:10:25:53 +0800] "GET / HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36"
180.160.72.9 - - [21/Dec/2017:10:26:55 +0800] "GET / HTTP/1.1" 499 0 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36"
180.160.72.9 - - [21/Dec/2017:10:27:55 +0800] "GET / HTTP/1.1" 502 575 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36"

找了很多资料后,从v2ex某个评论中看到了某个点,就是代理的地址需要将端口换成sock进程文件,

最终完美的配置文件

新建一个conf配置文件,同时可解决大部分超时问题,内容如下:

fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
fastcgi_buffer_size 256k;         #以下四个参数已加大,如果设置太小也会出现timeout 504
fastcgi_buffers 16 256k;
fastcgi_busy_buffers_size 512k;
fastcgi_temp_file_write_size 512k;


server {
        listen  8002;
        server_name localhost;

        location / {
            include     uwsgi_params;
            uwsgi_pass   unix:/home/api/vuln_cn/log/uwsgi.sock;    #这里是关键,指向uwsgi.sock文件,不能用127.0.0.1:8000,否则会出现文中的报错
            uwsgi_param UWSGI_CHDIR  /home/api/vuln_cn;     #这里与uwsgi.ini中配置类似,指向django项目目录
            uwsgi_param UWSGI_SCRIPT vuln_cn.wsgi;     #同上,指向项目名称
            access_log /home/api/vuln_cn/log/access.log;     #定义访问日志
            uwsgi_read_timeout 1800;
            uwsgi_send_timeout 300;
            proxy_read_timeout 300;
            }
    }

以上,备忘

[Django笔记] uwsgi + nginx 配置

安装nginx

安装配置uwsgi

pip install uwsgi

回想php-fpm安装完直接启动就完事了,好像只要配置php的路径
uwsgi的启动需要一大堆参数,可以写好一个配置文件 uwsgi_conf.ini,下面是一个demo:

# uwsig使用配置文件启动
[uwsgi]
# 项目目录
chdir=/var/www/path/to/django/
# 指定项目的application(django 目录的 wsgi.py)
module=pics.wsgi:application
# 指定sock的文件路径       
socket=/var/www/path/to/django/script/uwsgi.sock
# 进程个数       
workers=5
pidfile=/var/www/path/to/django/script/uwsgi.pid
# 指定IP端口       
http=127.0.0.1:80
# 指定静态文件
static-map=/static=/var/www/path/to/django/static
# 启动uwsgi的用户名和用户组
uid=root
gid=root
# 启用主进程
master=true
# 当服务停止的时候自动移除unix Socket和pid文件
vacuum=true
# 序列化接受的内容,如果可能的话
thunder-lock=true
# 启用线程
enable-threads=true
# 设置自中断时间
harakiri=30
# 设置缓冲
post-buffering=4096
# 设置日志目录
daemonize=/var/www/path/to/django/script/uwsgi.log

配置文件解释:

  • 通过 uwsgi [xxx.ini]来启动服务

  • pid, sock, log 三个文件跟这个 ini 文件可以统一在 django 目录下面新建一个script 文件夹存放

  • nginx 是读取sock 来连接 uwsgi

  • pid 文件非常重要, uwsgi 通过 uwsgi –stop [xxx.pid]和uwsgi –reload [xxx.pid] 来停止和重启服务

  • 因为uwsgi 是给nginx 做桥接,ip 为127.0.0.1. 我的http端口不是80,因此这里可以使用80

启动后,可以通过http://127.0.0.1来测试访问 django. (我在linux直接curl 127.0.0.1测试)

nginx.conf 配置 uwsgi

本机配置了一个php服务,想通过子路径/pics来访问 django:

# 指定项目路径uwsgi
location /pics { 
    include uwsgi_params; # 导入一个Nginx模块他是用来和uWSGI进行通讯的
    uwsgi_connect_timeout 30; # 设置连接uWSGI超时时间
    uwsgi_pass unix:/var/www/path/to/django/script/uwsgi.sock; # 指定uwsgi的sock文件所有动态请求就会直接丢给     
}
# 指定静态文件     
location /pics/static/ {
    alias /var/www/path/to/django/static/;
    index index.html index.htm;
}

重启nginx nginx -s reload ,大功告成

未分类

使用Nginx 和Supervisor在Linux服务器上部署Tornado

Nginx 安装: sudo apt-get install nginx

Nginx 安装后用浏览器进入127.0.0.1就可以看到nginx的欢迎页了

nginx 常用命令

  1. sudo service nginx start 启动nginx
  2. sudo service nginx stop 停止nginx
  3. sudo service nginx restart 重启nginx
  4. sudo service nginx reload 重新加载配置文件

未分类

Supervisor 安装: sudo apt-get install supervisor

部署步骤:

  • Tornado项目路径 : /home/你的用户名/Tornado项目文件夹名称/main.py

  • 在/etc/nginx/下 创建nginx.conf配置文件
    这里我们使用8000-8003四个端口,进行端口转发 配置文件编写要注意main.py所在位置要写对,即下面配置文件中的中文

user root;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 768;
    multi_accept on;
    use epoll;
}
http {
    # Enumerate all the Tornado servers here
    upstream frontends {
        server 127.0.0.1:8000;
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
        server 127.0.0.1:8003;
    }

    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    access_log /var/log/nginx/access.log;

    keepalive_timeout 65;
    proxy_read_timeout 200;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    gzip on;
    gzip_min_length 1000;
    gzip_proxied any;
    gzip_types text/plain text/html text/css text/xml
               application/x-javascript application/xml
               application/atom+xml text/javascript;
    proxy_next_upstream error;

    server {
        listen 80;

        # Allow file uploads
        client_max_body_size 50M;

        location ^~ /static/ {
            root /home/用户名/项目文件夹名/;
            if ($query_string) {
                expires max;
            }
        }
        location = /favicon.ico {
            rewrite (.*) /static/favicon.ico;
        }
        location = /robots.txt {
            rewrite (.*) /static/robots.txt;
        }

        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_pass http://frontends;
        }
    }
}
  • 编写supervisor配置文件

  • 进入supervisor配置文件夹 cd /etc/supervisor/conf.d/

  • 创建tornados.conf

[group:tornadoes]
programs=tornado-8000,tornado-8001,tornado-8002,tornado-8003

[program:tornado-8000]
command=python /home/用户名/项目文件夹名/main.py --port=8000
directory=/home/用户名/项目文件夹名
user=root
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/tornado.log
loglevel=info

[program:tornado-8001]
command=python /home/用户名/项目文件夹名/main.py --port=8001
directory=/home/用户名/项目文件夹名
user=root
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/tornado.log
loglevel=info

[program:tornado-8002]
command=python /home/用户名/项目文件夹名/main.py --port=8002
directory=/home/用户名/项目文件夹名
user=root
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/tornado.log
loglevel=info

[program:tornado-8003]
command=python /home/用户名/项目文件夹名/main.py --port=8003
directory=/home/用户名/项目文件夹名
user=root
autorestart=true
redirect_stderr=true
stdout_logfile=/var/log/tornado.log
loglevel=info
  • 其中/var/log/tornado.log为日志文件目录
    然后先重载nginx的配置文件 sudo service nginx reload
    重启 nginx : sudo service nginx restart
    supervisor开启所有进程: sudo supervisorctrl restart all

再次打开127.0.0.1后可以看到项目已经成功部署。

使用 Nginx 和 Gunicorn 部署 Django 博客

我们博客的基础功能已经开发的基本差不多了,虽然还有很多地方可以完善,但我们还是希望早点把博客部署到服务器上,让他人可以通过外网访问。至于有待完善的地方,可以等部署完后一点点地迭代和改进。现在就让我们来把博客部署到服务器上吧!

注意:本文的每一个步骤都在真实环境下验证无误。除非你知道自己在做什么,否则我们建议每一步均严格按照教程的指导来,这样能保证你顺利完成部署。

部署前准备

我们将使用比较流行的 Nginx + Gunicorn 的方式将 Django 开发的博客部署到自己的服务器,让别人能够通过域名访问你的博客。至于 Nginx、Gunicorn 是什么暂时放到一边,读完本教程后你就会知道它们的作用和使用方法了。
为了部署我们的博客,需要满足以下两个条件:

  1. 有一台可以通过外网访问的服务器。
  2. 有一个域名。

如果你已经满足以上条件,可以直接跳到后面的搭建服务器部分。这里简单介绍一下我目前所知的以最低成本满足以上两个条件的方式。

购买服务器

如果你是学生,推荐购买阿里云服务器,学生优惠价是 9.9 元/月,而且服务器性能比较高。购买地址:阿里云服务器学生专区。具体的购买步骤这里就不赘述了,根据网站的指引相信你肯定能够购买成功。只是注意一点的是在选服务器类型的时候选择公共镜像,这样系统比较纯净。操作系统建议选 ubuntu 14.04 64位,这是本教程使用的服务器环境。

如果你不是学生,推荐购买搬瓦工 vps。目前最便宜的是 19.9美元/年,缺点是服务器性能没有阿里云高,但优点是顺带可以用它来搭梯子,从此访问 google、youtube 不是梦(基于 shadowsocks 只需简单几步就可以搭建起自己的梯子服务器)。同样购买的过程就不赘述了,搬瓦工 vps 中文网 有超级详细的指引。只是注意安装操作系统时建议选 ubuntu 14.04 64位,这是本教程使用的服务器环境。

如果你不差那点钱,随意选择一个云服务器提供商购买一个云服务器即可。

购买域名

域名服务商很多,我这里使用的是 阿里云域名注册系统。域名是网站的门牌,如果打算长期运营这个网站建议多考虑考虑,选一个适当的域名。如果只是为了测试,随便注册一个域名即可,一些非常见后缀的域名非常便宜,一般 10元/年就能搞定。但注意一点根据工信部规定,以下后缀的域名需要实名认证后才能使用:

.cn/.com/.net/.top/.xyz/.vip/.club/.ren/.wang/.shop/.xin/.中国/.信息/.公司/.网络/.广东/.佛山

如果你购买的是上述后缀的域名,意味着需要提交个人的身份资料实名认证后才能正常使用,这通常需要花费几天的时间。所以如果只为了测试和学习部署的话,最好避开上述后缀的域名。

搭建服务器

本教程使用的本地环境为 Windows 10,服务器环境为 ubuntu 14.04(64 位)。如果你的环境和我的有所差异导致一些命令无法执行,将这些命令转换为你所在环境的命令执行即可。

远程登录到服务器

服务器通常位于云端,需要使用远程登录工具登录后才能对服务器进行操作。我使用的是 Xshell,Windows 下百度 Xshell 下载安装即可,软件对学校和个人用户是免费的。
如何远程登录到服务器这里就不赘述了,相信你参考网上的一些教程肯定能够顺利登录。假如你和我一样使用 Xshell 的话,这里有一篇很详细的教程可以参考:教你怎么使用xshell远程连接linux服务器。

安装软件

顺利连接到远程服务器了。如果是一台全新服务器的话,通常我们是以 root 用户登录的。在 root 下部署代码不安全,最好是建一个新用户(如果你已经以非 root 用户登录的话可以跳过这一步)。下面的一些列命令将创建一个拥有超级权限的新用户:

# 在 root 用户下运行这条命令创建一个新用户,yangxg 是用户名
# 因为我叫杨学光,所以我取的用户名是 yangxg
# 选择一个你喜欢的用户名,不一定非得和我的相同
root@localhost:~# useradd -m -s /bin/bash yangxg

# 把新创建的用户加入超级权限组
root@localhost:~# usermod -a -G sudo yangxg

# 为新用户设置密码
# 注意在输密码的时候不会有字符显示,不要以为键盘坏了,正常输入即可
root@localhost:~# passwd yangxg

# 切换到创建的新用户
root@localhost:~# su - yangxg

# 切换成功,@符号前面已经是新用户名而不是 root 了
yangxg@localhost:~$

新用户创建并切换成功了。如果是新服务器的话,最好先更新一下系统,避免因为版本太旧而给后面安装软件带来麻烦。运行下面的两条命令:

yangxg@localhost:~$ sudo apt-get update
yangxg@localhost:~$ sudo apt-get upgrade

接下来就可以安装必要的软件了,这里我们需要用到的软件有 Nginx、Pytohn3、Git、pip 和 virtualenv。

yangxg@localhost:~$ sudo apt-get install nginx
yangxg@localhost:~$ sudo apt-get install git python3 python3-pip
yangxg@localhost:~$ sudo pip3 install virtualenv

解析域名到服务器的 IP 地址

将域名和服务器的 IP 地址绑定后,用户就可以通过在浏览器输入域名来访问服务器了。

各大域名服务商都提供了域名解析服务,但其配置界面各有差异,请依据其指引完成域名解析。下面是我使用的阿里云域名解析页面。

未分类

启动 Nginx 服务

Nginx 是用来处理静态文件请求的。比如当我们访问一个博客文章详情页面时,服务器会接收到下面两种请求:

  • 显示文章的详情信息,这些信息通常保存在数据库里,因此需要调用数据库获取数据。
  • 图片、css、js 等存在服务器某个文件夹下的静态文件。

对于前一种请求,博客文章的数据需要借助 Django 从数据库中获取,Nginx 处理不了,它就会把这个请求转发给 Django,让 Django 去处理。而对于后一种静态文件的请求,只需要去这些静态文件所在的文件夹获取,Nginx 就会代为处理,不再麻烦 Django。

用 Django 去获取静态文件是很耗时的,但 Nginx 可以很高效地处理,这就是我们要使用 Nginx 的原因(当然其功能远不止这些)。

通过前面的步骤我们已经安装了 Nginx,并且已经把域名和服务器 IP 绑定了。运行下面的命令启动 Nginx 服务:

yangxg@localhost:~$ sudo service nginx start

在浏览器输入域名,看到如下页面说明 Nginx 启动成功了。

未分类

部署代码

部署前的项目配置

Django 项目中会有一些 CSS、JavaScript 等静态文件,为了能够方便地让 Nginx 处理这些静态文件的请求,我们把项目中的全部静态文件收集到一个统一的目录下,这个目录通常位于 Django 项目的根目录,并且命名为 static。为了完成这些任务,需要在项目的配置文件里做一些必要的配置:

blogproject/settings.py

# 其他配置...

STATIC_URL = '/static/'
# 加入下面的配置

STATIC_ROOT = os.path.join(BASE_DIR, ‘static’)STATIC_ROOT 指明了静态文件的收集目录,即项目根目录(BASE_DIR)下的 static 文件夹。

为了安全起见,在生产环境下需要关闭 DEBUG 选项以及设置允许访问的域名。打开 settings.py 文件,找到 DEBUG 和 ALLOWED_HOSTS 这两个选项,将它们设置成如下的值:

blogproject/settings.py

DEBUG = False

ALLOWED_HOSTS = [‘127.0.0.1’, ‘localhost ‘, ‘.zmrenwu.com’]ALLOWED_HOSTS 是允许访问的域名列表,127.0.0.1 和 localhost 是本地访问的域名,.zmrenwu.com 是访问服务器的域名(换成你自己的域名)。域名前加一个点表示允许访问该域名下的子域名,比如 www.zmrenwu.com、test.zmrenwu.com 等二级域名同样允许访问。如果不加前面的点则只允许访问 zmrenwu.com。

项目还会依赖一些第三方 Python 库,为了方便在服务器上一次性安装,我们将全部依赖写入一个叫 requirements.txt 的文本文件中。激活本地的虚拟环境(如果你使用了虚拟环境的话),并进入项目的根目录,运行 pip freeze > requirements.txt 命令:

(blogproject_env) C:UsersyangxgWorkspaceblogproject>
pip freeze > requirements.txt

这时项目根目录下会生成了一个 requirements.txt 的文本文件,其内容记录了项目的全部依赖。

将代码上传到 GitHub

将代码上传到 GitHub 等代码托管平台,这样我们就可以方便地把代码拉取到服务器了。Git 和 GitHub 的使用相信你已经很熟悉了,这里就不赘述过程。如果不知道如何使用地话可以自行百度相关教程。
注意数据库文件不要上传!

设置服务器目录结构

接下来需要把代码上传到服务器了。我服务器上存放代码的目录结构一般是这样的:

/home/yangxg/
    sites/
        demo.zmrenwu.com/
            env/
            django-blog-tutorial/

一台服务器可能部署多个网站,所有网站代码都放在 sites/ 目录下。demo.zmrenwu.com/ 这个文件夹以网站的域名命名,便于区分。env/ 是 python 虚拟环境目录。django-blog-tutorial/ 是 Django 博客项目目录。

因此先来创建这个目录结构,注意目录名替换为你自己的域名,以后涉及到 demo.zmrenwu.com 的地方通常都要替换你自己的域名,后面就不再一一指出了,运行下面的命令,

yangxg@localhost:~$ mkdir -p ~/sites/demo.zmrenwu.com

这里 ~ 代表当前用户的 home 目录,即 /home/yangxg/。

接下来创建虚拟环境,先进入到 demo.zmrenwu.com 目录下,然后运行 virtualenv 命令创建虚拟环境:

yangxg@localhost:~$ cd ~/sites/demo.zmrenwu.com
yangxg@localhost:~/sites/demo.zmrenwu.com$ virtualenv --python=python3 env

注意这里使用 –python=python3 来指定克隆 Python3 的环境。因为 ubuntu 系统默认安装了 Python2,如果不特别指定的话 Virtualenv 默认克隆的是 Python2 的环境。

检查一下虚拟环境是否创建成功,运行 ls 命令列出当前目录下的文件和文件夹,看到 env 这个文件夹说明虚拟环境创建成功。

yangxg@localhost:~/sites/demo.zmrenwu.com$ ls
env

接着再从代码仓库把项目代码拉取过来,把 git clone 后的地址换成你自己的 GitHub 仓库地址!

yangxg@localhost:~/sites/demo.zmrenwu.com$ git clone https://github.com/zmrenwu/django-blog-tutorial.git

运行 ls 命令检查一下是否拉取成功:

yangxg@localhost:~/sites/demo.zmrenwu.com$ ls
django-blog-tutorial  env

多了 django-blog-tutorial 文件夹(文件夹名称由你的 GitHub 仓库名决定),说明拉取成功了。

安装项目依赖

激活虚拟环境,再进入到项目根目录,即 requirements.txt 所在的目录,安装项目的全部依赖:

yangxg@localhost:~/sites/demo.zmrenwu.com$ source env/bin/activate
(env) yangxg@localhost:~/sites/demo.zmrenwu.com$ cd django-blog-tutorial/
(env) yangxg@localhost:~/sites/demo.zmrenwu.com/django-blog-tutorial$ pip install -r requirements.txt

收集静态文件

虚拟环境下继续运行 python manage.py collectstatic 命令收集静态文件到 static 目录下:

(env) yangxg@localhost:~/sites/demo.zmrenwu.com/django-blog-tutorial$ python manage.py collectstatic

生成数据库

虚拟环境下继续运行 python manage.py migrate 命令创建数据库文件:

(env) yangxg@localhost:~/sites/demo.zmrenwu.com/django-blog-tutorial$ python manage.py migrate

创建超级用户

虚拟环境下继续运行 python manage.py createsuperuser 命令创建一个超级用户,方便我们进入 Django 管理后台。这和本地开发时是一样的,具体请参照:在 Django Admin 后台文章。

(env) yangxg@localhost:~/sites/demo.zmrenwu.com/django-blog-tutorial$ python manage.py createsuperuser

配置 Nginx

接下是配置 Nginx 来处理用户请求。

先在服务器的 /etc/nginx/sites-available/ 目录下新建一个配置文件,文件名我一般就设置为域名。写上下面的配置内容:
/etc/nginx/sites-available/demo.zmrenwu.com

server {
    charset utf-8;
    listen 80;
    server_name demo.zmrenwu.com; ①

    location /static { ②
        alias /home/yangxg/sites/demo.zmrenwu.com/django-blog-tutorial/static; 
    }

    location / { ③
        proxy_set_header Host $host;
        proxy_pass http://unix:/tmp/demo.zmrenwu.com.socket;
    }
}

① 服务的域名为 demo.zmrenwu.com。

② 所有URL 带有 /static 的请求均由 Nginx 处理,alias 指明了静态文件的存放目录。

③ 其它请求转发给 Django 处理。proxy_pass 后面使用了 unix 套接字,其作用是防止端口冲突,这里就不再详述。

至于怎么在服务器新建文件和写文件,请自行学习一点点 vi 编辑器的用法,这里也不一一讲解了。

我们在 /etc/nginx/sites-available/ 放置了配置文件,接下来需要创建一个符号链接,把这个配置文件加入到启用的网站列表中去,被启用网站的目录在 /etc/nginx/sites-enabled/,你可以理解为从 sites-available/ 目录下发送了一个配置文件的快捷方式到 sites-enabled/ 目录。具体命令如下:

(env) yangxg@localhost:~/sites/demo.zmrenwu.com/django-blog-tutorial$ sudo ln -s /etc/nginx/sites-available/demo.zmrenwu.com /etc/nginx/sites-enabled/demo.zmrenwu.com

使用 Gunicorn

Gunicorn 一般用来管理多个进程,有进程挂了Gunicorn 可以把它拉起来,防止服务器长时间停止服务,还可以动态调整 worker 的数量,请求多的时候增加 worker 的数量,请求少的时候减少。
在虚拟环境下,安装 Gunicorn:

(env) yangxg@localhost:~/sites/demo.zmrenwu.com/django-blog-tutorial$ pip install gunicorn

用 Gunicorn 启动服务器进程:

(env) yangxg@localhost:~/sites/demo.zmrenwu.com/django-blog-tutorial$ gunicorn --bind unix:/tmp/demo.zmrenwu.com.socket blogproject.wsgi:application

浏览器输入域名,可以看到访问成功了!

自动启动 Gunicorn

现在 Gunicorn 是我们手工启动的,万一哪天服务器崩溃重启了又得重新手工启动。为此我们写一个自动启动脚本,这样当服务器重新启动后,脚本会帮我们重启 Gunicorn。先按 Ctrl + c 停止刚才启动的服务器进程。

写一个启动脚本,这样当服务器重启后能自动引导 Gunicorn 的启动。脚本位于 /etc/init/ 目录下,且脚本文件名必须以 .conf 结尾:

/etc/init/gunicorn-demo.zmrenwu.com.conf

start on net-device-up ①
stop on shutdown

respawn ②

setuid yangxg ③
chdir /home/yangxg/sites/demo.zmrenwu.com/django-blog-tutorial ④

exec ../env/bin/gunicorn --bind unix:/tmp/demo.zmrenwu.com.socket blogproject.wsgi:application ⑤

① start on net-device-up 确保只在服务器联网时才启动 Gunicorn。

② 如果进程崩溃了(比如服务器重启或者进程因为某些以外情况被 kill),respawn 将自动重启 Gunicorn。

③ setuid 确保以 yangxg 用户的身份(换成你自己的用户名)运行 Gunicorn 进程。

④ chdir 进入到指定目录,这里进入项目的根目录。

⑤ exec 执行进程,即开启服务器进程。

现在可以用 start 命令启动 Gunicorn 了:

sudo start gunicorn-demo.zmrenwu.com

以后如果更新了代码,只要运行下面的命令重启一下 Nginx 和 Gunicorn 就可以使新的代码生效了:

sudo service nginx reload
sudo restart gunicorn-demo.zmrenwu.com

使用 CDN 加快 Bootstrap 和 jQuery 的加载速度

我们的项目使用了 Bootstrap 和 jQuery,这两个文件我们是从本地加载的。如果服务器性能比较差的话,加载需要耗费很长的时间,网站打开的速度就变得无法忍受。我们使用 CDN 来加快加载速度。具体来说,替换 base.html 的几个静态文件的加载标签:

base.html

- <link rel="stylesheet" href="{% static 'blog/css/bootstrap.min.css' %}">
- <script src="{% static 'blog/js/jquery-2.1.3.min.js' %}"></script>
- <script src="{% static 'blog/js/bootstrap.min.js' %}"></script>
+ <link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
+ <script src="https://cdn.bootcss.com/jquery/2.1.3/jquery.min.js"></script>
+ <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

这样网站访问的速度将大大提升!

部署过程自动化

在整个部署过程中我们运行了十几条命令,手动输入了 N 个字符。如果每次更新代码都要远程连接到服务器执行这些命令的话将变得非常麻烦。接下来的教程我们将介绍使用 Fabric 自动化整个部署过程。写好部署脚本后,只需要执行一条命令,就可以非常方便地自动完成整个部署。

如何在CentOS 7上安装Nginx

本教程中的步骤要求用户拥有root权限

第一步 – 添加Nginx存储库

要添加CentOS 7 EPEL仓库,请打开终端并使用以下命令:

sudo yum install epel-release

第二步 – 安装Nginx

现在Nginx存储库已经安装在您的服务器上,使用以下yum命令安装Nginx :

sudo yum install nginx

在对提示回答yes后,Nginx将在服务器上完成安装。

第三步 – 启动Nginx

Nginx不会自行启动。要运行Nginx,请输入:

sudo systemctl start nginx

如果您正在运行防火墙,请运行以下命令以允许HTTP和HTTPS通信:

sudo firewall-cmd --permanent --zone=public --add-service=http 
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload

您将会看到默认的CentOS 7 Nginx网页,这是为了提供信息和测试目的。它应该看起来像这样:

未分类

如果看到这个页面,那么你的Web服务器现在已经正确安装了。

如果想在系统启动时启用Nginx。请输入以下命令:

sudo systemctl enable nginx

恭喜!Nginx现在已经安装并运行了!

docker搭建nginx+php-fpm开发环境

一、创建目录

mkdir -p /data1/www/app;
mkdir -p /data1/www/logs;
mkdir -p /data1/www/php;
mkdir -p /data1/www/nginx;
mkdir -p /data1/www/php/fpm.d;
mkdir -p /data1/www/nginx/vhost;
chmod 777 -R /data1/www;

添加项目:

vim /data1/www/app/www.mydemo.com/index.php
<?php
    echo 22;
    phpinfo();

二、启动容器

docker run -it --name=web_container --net=host -v /data1:/data1 centos /bin/bash;
yum -y install wget;
yum -y install initscripts;

三、安装nginx

依赖说明:

  • zlib: Nginx提供gzip模块,需要zlib库支持。
  • openssl: Nginx提供SSL功能
  • pcre: 支持地址重写rewrite功能

依赖安装:

yum -y install zlib zlib-devel openssl openssl-devel pcre-devel;

依赖检测:

rpm -qa  zlib;
rpm -qa  openssl;
rmp -qa  pcre;

nginx安装

wget http://nginx.org/download/nginx-1.10.3.tar.gz
tar -zxvf nginx-1.10.3.tar.gz;
cd nginx-1.10.3;
./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_flv_module;
make && make install;

创建用户

groupadd  www
useradd -r -g www  www

在宿主机上修改添加配置文件: vim /data1/www/nginx/mime.types

types {
    text/html                             html htm shtml;
    text/css                              css;
    text/xml                              xml;
    image/gif                             gif;
    image/jpeg                            jpeg jpg;
    application/javascript                js;
    application/atom+xml                  atom;
    application/rss+xml                   rss;

    text/mathml                           mml;
    text/plain                            txt;
    text/vnd.sun.j2me.app-descriptor      jad;
    text/vnd.wap.wml                      wml;
    text/x-component                      htc;

    image/png                             png;
    image/tiff                            tif tiff;
    image/vnd.wap.wbmp                    wbmp;
    image/x-icon                          ico;
    image/x-jng                           jng;
    image/x-ms-bmp                        bmp;
    image/svg+xml                         svg svgz;
    image/webp                            webp;

    application/font-woff                 woff;
    application/java-archive              jar war ear;
    application/json                      json;
    application/mac-binhex40              hqx;
    application/msword                    doc;
    application/pdf                       pdf;
    application/postscript                ps eps ai;
    application/rtf                       rtf;
    application/vnd.apple.mpegurl         m3u8;
    application/vnd.ms-excel              xls;
    application/vnd.ms-fontobject         eot;
    application/vnd.ms-powerpoint         ppt;
    application/vnd.wap.wmlc              wmlc;
    application/vnd.google-earth.kml+xml  kml;
    application/vnd.google-earth.kmz      kmz;
    application/x-7z-compressed           7z;
    application/x-cocoa                   cco;
    application/x-java-archive-diff       jardiff;
    application/x-java-jnlp-file          jnlp;
    application/x-makeself                run;
    application/x-perl                    pl pm;
    application/x-pilot                   prc pdb;
    application/x-rar-compressed          rar;
    application/x-redhat-package-manager  rpm;
    application/x-sea                     sea;
    application/x-shockwave-flash         swf;
    application/x-stuffit                 sit;
    application/x-tcl                     tcl tk;
    application/x-x509-ca-cert            der pem crt;
    application/x-xpinstall               xpi;
    application/xhtml+xml                 xhtml;
    application/xspf+xml                  xspf;
    application/zip                       zip;

    application/octet-stream              bin exe dll;
    application/octet-stream              deb;
    application/octet-stream              dmg;
    application/octet-stream              iso img;
    application/octet-stream              msi msp msm;

    application/vnd.openxmlformats-officedocument.wordprocessingml.document    docx;
    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet          xlsx;
    application/vnd.openxmlformats-officedocument.presentationml.presentation  pptx;

    audio/midi                            mid midi kar;
    audio/mpeg                            mp3;
    audio/ogg                             ogg;
    audio/x-m4a                           m4a;
    audio/x-realaudio                     ra;

    video/3gpp                            3gpp 3gp;
    video/mp2t                            ts;
    video/mp4                             mp4;
    video/mpeg                            mpeg mpg;
    video/quicktime                       mov;
    video/webm                            webm;
    video/x-flv                           flv;
    video/x-m4v                           m4v;
    video/x-mng                           mng;
    video/x-ms-asf                        asx asf;
    video/x-ms-wmv                        wmv;
    video/x-msvideo                       avi;
}

新增fast_cig: vim /data1/www/nginx/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;

vim /data1/www/nginx/nginx.conf

user  www;
worker_processes  5;

error_log  logs/nginx-error.log;
pid        nginx/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    client_body_temp_path /tmp/client_body_temp_path;
    fastcgi_temp_path /tmp/fastcgi_temp_path;
    proxy_temp_path /tmp/proxy_temp_path;
    scgi_temp_path /tmp/scgi_temp;
    uwsgi_temp_path /tmp/uwsgi_temp_path;

    sendfile        on;
    keepalive_timeout  65;
    gzip  on;
    include vhost/*.conf;
}

配置文件检测

/usr/local/nginx/sbin/nginx -c /data1/www/nginx/nginx.conf -p /data1/www -t

新增项目配置: vim /data1/www/nginx/vhost/mydemo.com.conf

server {
         listen       80 ;
         root /data1/www/app/www.mydemo.com/;
         server_name  www.mydemo.com mydemo.com;

         access_log   logs/nginx_www.mydemo.com-access_log  main;
         error_log    logs/nginx_www.mydemo.com-error_log;
         rewrite  "^/(.*)" /index.php/$1 last;

         location  / {
                   proxy_ignore_client_abort on;
                   fastcgi_pass 127.0.0.1:9023;
                   fastcgi_index index.php;
                   fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                   #fastcgi_param  SCRIPT_URL         $script_uri;
                   #fastcgi_param  REQUEST_ID           $request_uid;
                   include fastcgi_params;
         }
}

启动nginx:

/usr/local/nginx/sbin/nginx -c /data1/www/nginx/nginx.conf -p /data1/www

平滑重启

Kill -HUP  ${nginx_pid}

四、安装php-fpm

yum -y install gd-devel zlib-devel libjpeg-devel libpng-devel libiconv-devel freetype-devel libxml2 libxml2-devel openssl openssl-devel curl-devel libxslt-devel libmcrypt-devel mhash mcrypt
wget http://am1.php.net/get/php-7.2.0.tar.bz2/from/this/mirror ;
tar -jxvf mirror;
cd php-7.2.0;
./configure  --prefix=/usr/local/php --enable-fpm --enable-cli --enable-pcntl --with-curl;
make && make install; 

新增配置文件: vim /data1/www/php/php-fpm.conf

[global]
pid = php/php-fpm.pid
error_log = logs/php-fpm.log
emergency_restart_threshold = 20
emergency_restart_interval = 60s
process_control_timeout = 0
process.max = 2048
daemonize = yes
rlimit_files = 65535
rlimit_core = 67108864
events.mechanism = epoll

; auto include phpfpm configure
include = php/fpm.d/*.conf

vim /data1/www/php/php.ini

[PHP]
engine = On
short_open_tag = Off
precision = 14
output_buffering = 4096
zlib.output_compression = Off
implicit_flush = Off
unserialize_callback_func =
serialize_precision = -1
disable_functions =
disable_classes =
zend.enable_gc = On
expose_php = On
max_execution_time = 30
max_input_time = 60
memory_limit = 128M
error_reporting = E_ALL
display_errors = On
display_startup_errors = On
log_errors = On
log_errors_max_len = 1024
ignore_repeated_errors = Off
ignore_repeated_source = Off
report_memleaks = On
html_errors = On
variables_order = "GPCS"
request_order = "GP"
register_argc_argv = Off
auto_globals_jit = On
post_max_size = 8M
auto_prepend_file =
auto_append_file =
default_mimetype = "text/html"
default_charset = "UTF-8"
doc_root =
user_dir =
enable_dl = Off
file_uploads = On
upload_max_filesize = 2M
max_file_uploads = 20
allow_url_fopen = On
allow_url_include = Off
default_socket_timeout = 60
[CLI Server]
cli_server.color = On
[Date]
[filter]
[iconv]
[intl]
[sqlite3]
[Pcre]
[Pdo]
[Pdo_mysql]
pdo_mysql.cache_size = 2000
pdo_mysql.default_socket=
[Phar]
[mail function]
SMTP = localhost
smtp_port = 25
mail.add_x_header = On
[ODBC]
odbc.allow_persistent = On
odbc.check_persistent = On
odbc.max_persistent = -1
odbc.max_links = -1
odbc.defaultlrl = 4096
odbc.defaultbinmode = 1
[Interbase]
ibase.allow_persistent = 1
ibase.max_persistent = -1
ibase.max_links = -1
ibase.timestampformat = "%Y-%m-%d %H:%M:%S"
ibase.dateformat = "%Y-%m-%d"
ibase.timeformat = "%H:%M:%S"
[MySQLi]
mysqli.max_persistent = -1
mysqli.allow_persistent = On
mysqli.max_links = -1
mysqli.cache_size = 2000
mysqli.default_port = 3306
mysqli.default_socket =
mysqli.default_host =
mysqli.default_user =
mysqli.default_pw =
mysqli.reconnect = Off
[mysqlnd]
mysqlnd.collect_statistics = On
mysqlnd.collect_memory_statistics = On
[OCI8]
[PostgreSQL]
pgsql.allow_persistent = On
pgsql.auto_reset_persistent = Off
pgsql.max_persistent = -1
pgsql.max_links = -1
pgsql.ignore_notice = 0
pgsql.log_notice = 0
[bcmath]
bcmath.scale = 0
[browscap]
[Session]
session.save_handler = files
session.use_strict_mode = 0
session.use_cookies = 1
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 0
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly =
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 1440
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.sid_length = 26
session.trans_sid_tags = "a=href,area=href,frame=src,form="
session.sid_bits_per_character = 5
[Assertion]
zend.assertions = 1
[COM]
[mbstring]
[gd]
[exif]
[Tidy]
tidy.clean_output = Off
[soap]
soap.wsdl_cache_enabled=1
soap.wsdl_cache_dir="/tmp"
soap.wsdl_cache_ttl=86400
soap.wsdl_cache_limit = 5
[sysvshm]
[ldap]
ldap.max_links = -1
[dba]
[opcache]
[curl]
[openssl]

新增项目配置:

vim /data1/www/php/fpm.d/mydemo.com.conf

 [mydemo.com]
user = www
group = www
listen = 127.0.0.1:9023
listen.allowed_clients = 127.0.0.1
pm = dynamic
pm.max_children = 512
pm.start_servers = 5
pm.min_spare_servers = 4
pm.max_spare_servers = 64
pm.max_requests = 1500

;pm.status_path = /dpool_monitor


slowlog = logs/$pool-slow_log
request_slowlog_timeout = 2

request_terminate_timeout = 30
catch_workers_output = no
security.limit_extensions = ""

access.log=logs/php-fpm_$pool.access.log
access.format = "%R - %u %t "%m %r%Q%q" %s %f %{mili}d %{kilo}M %C%%"

启动命令:

/usr/local/php/sbin/php-fpm -c /data1/www/php/php.ini  -y /data1/www/php/php-fpm.conf  -p /data1/www

平滑重启:

kill -USR2  fpm-pid

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 配置,还得多参考一下官方的文档、源代码和一些高质量的博客文章。