网站从 Linode 搬走了,颠沛流离,无依无靠。
有个叫 CloudEndure 的工具能从基于 Linux 的 VPS 上迁移整个系统到新的 VPS,结果试了半天都是各种 Error。
试图把网站从(据说)笨重的 Apache 迁移到 Nginx 的过程也是风波不断,真的怀疑我是不是 BUG 体质。

需求

  • 网站从 Apache 迁移到到 Nginx
  • 其中包括由 Python 写成的最基本的 CGI Script.
  • 从 Ubuntu 16.04 LTS 迁移到 CentOS 7(我也不知道为什么脑子抽了要换系统)

流程和坑

1. 安装 & 配置 Nginx

安装不能再简单。yum install nginx 解决。
配置 Nginx 也不算太困难,思路和配置 Apache 差不多。如果说 Apache 的配置文件是 xml 的话,Nginx 就有点像 JavaScript(虽然只有大括号像)

Nginx 的主配置文件在 /etc/nginx/nginx.conf,主站以外虽然也能写进主配置文件里,不过还是另外放到其它文件夹然后在主配置文件里 include 进去比较好。
默认情况下是包括了/etc/nginx/conf.d/ 里面的所有 .conf 文件。如果要自己 include,需要把配置写在 http 项下面,和 server 项平行。
(顺便,RedHat 系?下的 Apache 包名是 httpd,Debian 系下的则是 apache2,目录也因此不同,配置文件的设置方法甚至都不一样)

1.1 设置域名

server_name 属性,用逗号分隔。
要把 www 转到不带 www 的域名下建议另开一个 server 项做 301 跳转。跳转写法为 return <HTTP STATUS> <URL>,其中 URL 可以使用 Nginx 自带的变量。

1.2 设置监听端口

listen 属性。一行一个监听端口,分 ipv4、6、http(80) 和 https(443)。

1.3 设置根目录

root 属性,和 Apache 一样。

1.4 设置权限

Apache 是通过 <dictionary> 项设置的,Nginx 则是 location {}
例如要屏蔽所有的 ini 文件访问可以直接写成:

location ~ .ini$ {deny all;}

1.5 设置 https

Nginx 自带的默认配置文件里已经有了 https 的设置项。只需要把 ssl_certificatessl_certificate_key 项改成对应的密钥路径即可。

1.6 设置 http 跳转 https

简单的做法是做一个判断,发现访问 http 时自动转向到 https。这样 http 和 https 规则可以写在一个 server 项里面。比如:

if ($scheme = http){return 301 https://$server_name$request_uri;}

全部完成后,启动 Nginx 服务就可以了。在不添加 CGI 脚本的情况下运行起来和 Apache 没有太大的区别。
Nginx 的运行状况可以用 service nginx <command>nginx -s <command> 来控制,和 Apache 差不多。

由于 Nginx 默认配置文件带了指定 40x、50x 错误页的规则,要用默认的错误页记得删掉规则,否则 50x 错误也会返回 404(因为不存在错误页文件)导致干扰除错工作。

2. 添加 CGI Script 支持

重头戏来了。Nginx 的配置不算太难,但 Nginx 原生不支持 CGI 脚本。
我又不想为了一个简单的脚本去动用 Flask 这种大型框架,只能借助于 CGI 的力量了。

在 Nginx 下添加 Python CGI 脚本支持的目前似乎是 uWSGI 比较好。CGI 脚本的原理实际上就是前端服务器在访问脚本时把请求转给 CGI Server 来处理,最后返回脚本。所以 Nginx 只起到一个转发作用,实际的处理需要后台另外跑一个 CGI Server。

2.1 安装 Python

基于 Python,记得先安装 Python。
对于不喜欢自己折腾的(比如我)尽量用包管理器安装,不要让各种调用问题影响你的生产效率。
另外 Python2 和 3 只安装一个小版本。比如 Python2.7 和 Python3.6。
从不同的源安装多个版本会导致 pip 安装外部模块时路径不明确,进而导致uWSGI 无法调用外部库,然后就会出现明明安装了某个模块却

ModuleNotFoundError: No module named <module name here>

我折腾了半天都没解决,最后直接 sudo python 交互模式 import 模块发现ModuleNotFoundError,才发现模块调用完全乱套了,乖乖从软件源直接装了二进制。

Debian 系的软件源好像直接有二进制包可以用,然而 RedHat 系没有,shabi。谁给我安利 CentOS 的我要打死他。
记得自己另外找个 community 的软件源。
还有记得安装 pip。软件源应该也带了。

2.2 安装 uWSGI

可以从源码编译,也可以 pip 直接装(虽然也是编译,只是帮你放好路径)。
反正我是 pip 直装党:pip3 install uwsgi
CentOS 上装好了还得建立到 /usr/bin 的软链接,要不然 sudo 的时候就找不到了。

pip 安装的 uWSGI 自带 Python 支持,但并不带传统的 CGI 的支持,需要另外编译 CGI 插件。
我没搞懂怎么编译同时带 Python 和 CGI 支持的二进制,所以就直接编译插件好了。
下载 uWSGI 的源码,执行 python uwsgiconfig.py --plugin plugins/cgi 编译 CGI 模块,把生成的 cgi_plugin.so 随便放哪备用,以后执行时要调用。

2.3 配置 uWSGI

配置之前先做的第一件事是关掉 SELinux。否则 Nginx 传递给 uWSGI 时会报错 (13: Permission denied) while connecting to upstream 。安全?大家都关的也没见哪里不安全了啊(
※ 这个地方又卡了我好几个小时。垃圾 CentOS。
关掉 SELinux 执行setenforce 0,这个命令似乎是临时关闭,重启就会还原?

在方便调用的地方建立 uWSGI 的配置文件,内容如下:

[uwsgi]
plugins-dir =            // 插件目录
plugins = cgi            // 启用 CGI 插件
socket = 127.0.0.1:81    // 本地监听端口,Nginx 和 uWSGI 使用这个来通信。也可以使用 socket 文件。
chdir =                  //uWSGI 执行时的工作根目录
cgi=                     //CGI 目录,设置详情看官方文档
cgi-helper=.py=python3   // 设置 CGI 文件后缀和调用程序
master = true
http-modifier1 = 9
max-requests = 100

对于不同站点我目前目前还没发现写在一个配置文件里的方法,因此我两个站点使用了两个配置文件。

2.4 配置 Nginx

配置 Nginx 以将 py 文件转给 uWSGI 来处理。在配置文件里的 server 项下添加如下内容。
改了后缀名不是 py?自己改配置文件啊

location ~ .py$ {
    include uwsgi_params;
    uwsgi_modifier1 9;
    uwsgi_pass 127.0.0.1:81;     // 和 uWSGI 配置文件相同
}

2.5 测试

uwsgi --ini <config_file_path> 命令带配置文件启动 uWSGI。
Python 脚本的错误会显示在命令行中。Nginx 端的错误会记录在 Nginx 侧的 log 中。
像 Apache 时代一样注意权限基本就没问题了。只要不遇到什么 SELinux 呀 pip 依赖呀的沙皮问题。
配置文件中的 CGI 位置可能会有点问题。注意看访问时是不是返回 404。
如果返回的是 Nginx 风格的错误页面,说明问题出在 Nginx 那边。如果控制台有响应且报错,说明 Python 脚本没写对。如果是几个小字 Not Found,则说明 uWSGI 的 CGI 位置没有写对。多读文档。如果控制台什么响应都没有,说明 Nginx 没能把请求传递给 uWSGI,一般是监听地址写错了,或者是 SELinux 在作威作福。

2.6 配置为服务运行

将 uWSGI 配置为服务运行用到 systemctl,在 /etc/systemd/system 下新建个 service 比如emperor.uwsgi.service,编辑内容如下:

[Unit]
Description=uWSGI Emperor A
After=syslog.target

[Service]
ExecStart=/usr/bin/uwsgi --ini <config_file_path>
// 需要写 uWSGI 和配置文件的绝对地址
RuntimeDirectory=uwsgi
Restart=always
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

[Install]
WantedBy=multi-user.target

接着直接 systemctl start <servicename> 就行了。
配置为开机自启动则是enable

感想

配置了很多次 Apache,没想到在迁移到 Nginx 上栽了一大堆跟头。
这次正好又把系统给换了,我真是和自己过不去啊。

迁移完之后发现跑一个 Nginx+MySQL 的服务器好像也不怎么吃资源,以后干脆选择 512M 的 VPS 就行了吧……

至于用全新的 uWSGI 的协议写 CGI 脚本,这辈子都不可能的。
好吧,等我开始正式写一个大型的需要很多前后端交互的项目之后大概还是会用的。

顺手查了下 Python 的大型项目中经常有的 if __name__ == '__main__'含义,原来是区分直接执行和作为模块导入时用的。
__name__可以看作是一个类似于 $server_name 的系统变量,当直接执行 python foo.py 时,__name__ == "__main__"
我目前好像还用不到这东西……