运行环境:CentOS release 6.9 (Final) ,Amazon Linux AMI 2017.09

1、问题

使用 yum 安装的Nginx,会在系统 service 中添加 rc.d 启动脚本,以便在系统重启时,随系统自动启动Nginx。

但随系统自动启动的Nginx,其Max open files limit 是系统默认的1024/4096(并不是我们在 /etc/security/limits.conf 中配置的数字):

所以在访问量不大的情况下,你都会在nginx error log 中,发现"worker_connections exceed open file resource limit: 1024"的报警:

2017/12/28 11:53:20 [warn] 5765#5765: 8192 worker_connections exceed open file resource limit: 1024

你可能会觉得很奇怪,我明明在 /etc/security/limits.conf  中设置了nofile  131072 参数啊,而且这个设置也一直都生效的(ulimit -n 检查OK),之前手动启动Nginx也都是应用这个nofile 设置的。

$ cat /etc/security/limits.conf 
# /etc/security/limits.conf
#
#<domain>      <type>  <item>         <value>
#
*  -  nofile  131072

其实,这个问题也存在于随系统启动的其他服务,如Apache。到底怎么回事呢?

2、解惑

问题出在Linux系统启动过程中,也就是说Nginx主进程启动时,上面的限制配置没有生效;实际上系统启动后执行用户登录login时才会使 limits.conf 配置生效。

简单描述一下,Linux内核启动后的系统启动过程如下:

  1. 运行/sbin/init 程序,执行系统的1号进程(此后系统的控制权就交给 /sbin/init 进程了)

  2. 读取 /etc/inittab 来确定系统启动级别(例如 读取到的默认级别是3)

  3. 执行初始化系统脚本 /etc/rc.d/rc.sysinit 来进行系统配置初始化

  4. 执行 /etc/rc.d/rc 脚本,根据启动级别,执行/etc/rc.d/rc*.d/ 下的各种服务启动脚本(比如 rc3.d下的文件)

  5. 执行 /etc/rc.d/rc.local 脚本(其实是/etc/rc.d/rc*.d/ 下的最后一个脚本,即S99local)

  6. 完成了所有的启动任务后,Linux会启动终端或X-Window来等待用户登录。

至此系统启动过程完成,当用户登录时,才会执行/etc/profile,~/.bash_profile和~/.bashrc 环境配置文件等;

关键一点是用户登陆时才会使 /etc/security/limits.conf 配置文件生效,这个比Nginx进程启动晚,此时的ulimit -n查到的值不是Nginx进程启动时的值。

这里简单扩展一下Linux是如何加载这个 limits.conf 文件的:

$ cat /etc/pam.d/login
#%PAM-1.0
...
account    include      system-auth
password   include      system-auth
session    include      system-auth
...

$ cat /etc/pam.d/system-auth
#%PAM-1.0
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
...
session     required      pam_limits.so
...

$ grep -a "/etc/security/limits.conf" /lib64/security/pam_limits.so
#结果见下图

3、解决

1)登录系统,手动运行 service nginx restart 重启Nginx服务

可以解决上述问题,因为此时 limits.conf 配置已经生效。

但是这个方法每次系统重启都要手动执行,有时忘了就麻烦了,甚至机器多的时候更糟糕。

2)在 /etc/rc.local 设置Nginx重启命令,并在 Nginx 启动前执行 ulimit 命令

$ tail /etc/rc.local 
# for nginx
ulimit -SHn 131072
service nginx restart

可以解决上述问题。

注:仅仅在 /etc/rc.local 中添加 service nginx restart 命令是不行的。

3)有人说 Nginx 有个配置文件参数 worker_rlimit_nofile

经实测,无法解决上述 Nginx 随系统 service 自动启动时ulimit -n 不生效的问题。



在访问带目录的URL时,如果末尾不加斜杠(“/”),Nginx默认会自动加上,其实是返回了一个301跳转,在新的Location中加了斜杠。

但这个默认行为在Nginx前端有LB负载均衡器、且LB的端口与Nginx Server监听的端口不同时,可能会导致访问出错。

比如域名所指向的LB对外监听端口80,转发到后端Nginx 8080端口,当Nginx进行上述自动重定向时,导致重定向到了域名的8080端口,如:


$ curl -I http://www.mydomain.com/app

HTTP/1.1 301 Moved Permanently
Date: Tue, 21 Nov 2017 08:27:50 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Server: nginx
Location: http://www.mydomain.com:8080/app/

(这里由于LB没有监听8080端口,导致访问出错)


此时,解决方法有两种。


1)新版本nginx(≥1.11.8)可以通过设置 absolute_redirect off; 来解决:


server {
    listen       8080;
    server_name  www.mydomain.com;
    
    absolute_redirect off;    #取消绝对路径的重定向
    
    root   html;
    ...
}


即使用相对路径的重定向,结果如下:


$ curl -I http://www.mydomain.com/app

HTTP/1.1 301 Moved Permanently
Date: Tue, 21 Nov 2017 07:45:21 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Server: nginx
Location: /app/    (这里变成了相对路径)


2)LB是80端口的,旧版本nginx(<1.11.8)可以增加 port_in_redirect off; 参数来解决:


server {
    listen       8080;
    server_name  www.mydomain.com;
    
    #absolute_redirect off;     # appeared in version 1.11.8
    port_in_redirect off;
    
    ...
}


即去掉重定向后的Location中的端口,效果如下:


$ curl -I http://www.mydomain.com/app

HTTP/1.1 301 Moved Permanently
Date: Tue, 21 Nov 2017 13:03:34 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Server: nginx
Location: http://www.mydomain.com/app/


如果LB监听非80端口,那建议升级Nginx到1.11.8版本以上了。



Nginx proxy timed out with dynamic upstreams

当使用Nginx 转发请求到AWS ELB(即把ELB的域名作为proxy_pass的upstream)时,由于ELB域名的IP经常变化,会出现Nginx转发请求失败(连接upstream超时)的情况。Nginx 配置样例如下:

location / {
    proxy_pass http://service-1234567890.us-east-1.elb.amazonaws.com;
}

原因在于Nginx解析域名的时机:

Nginx仅在启动的时候,对配置文件中的upstream域名进行解析,并将得到A记录IP地址全部作为upstream,直到下一次加载配置文件时,才会更新域名解析结果。而AWS ELB域名的IP是经常变化的,所以很容易出现转发错误的情况。

解决方案:

1、使用 Nginx Plus(付费方案)

其中一个解决方案是付费购买Nginx Plus版本,然后在配置文件 upstream 组配置中,在 server 指令后添加 resolve 标记。

Example:

upstream backend {
    server service-1234567890.us-east-1.elb.amazonaws.com  resolve;
}

这样Nginx 就会遵循域名的TTL过期机制,当TTL过期时重新进行域名解析,获得server (AWS ELB)域名更新后IP地址。

2、将upstream server配置为变量(取巧免费方案)

另一个免费的方案是按照如下方法进行配置:

resolver 172.16.0.23;
set $upstream_endpoint http://service-1234567890.us-east-1.elb.amazonaws.com;
location / {
    proxy_pass $upstream_endpoint;
}

即为Nginx 配置一个域名解析器 resolver ,并将 upstream server 配置为一个变量。

这样Nginx 也会遵循域名的TTL过期机制,并在收到请求一个过期的upstream server域名时重新进行域名解析。

这个方法奏效的原因可以在Nginx 官方文档中对 proxy_pass 指令的描述末尾中可以找到:

A server name, its port and the passed URI can also be specified using variables:

proxy_pass http://$host$uri;

or even like this:

proxy_pass $request;

In this case, the server name is searched among the described server groups, and, if not found, is determined using a resolver.


关于 proxy_pass 的 forwarded URI:

如果 location 不是 / ,proxy_pass 后的URI不同的写法会有不同的结果。

1、通常情况(upstream 未使用变量)

1)假设我们有如下配置:

location /foo/ {
    proxy_pass http://127.0.0.1:8080;
}

当我们请求 /foo/bar/baz 时,Nginx 会转发请求到 http://127.0.0.1:8080/foo/bar/baz 。

2)但如果我们这样配置:

location /foo/ {
    # Note the trailing slash       ↓
    proxy_pass http://127.0.0.1:8080/;
}

Nginx 会去除在 loation 中指定的路径,再将URI余下的部分转发。则请求 /foo/bar/baz 时,会被转发到 http://127.0.0.1:8080/bar/baz 。

2、upstream 使用变量的情况

1)假设我们按刚才的解决方案如下进行配置

resolver 172.16.0.23;
set $upstream_endpoint http://service-1234567890.us-east-1.elb.amazonaws.com/;
location /foo/ {
    proxy_pass $upstream_endpoint;
}

当我们请求 /foo/bar/baz 时,Nginx 会转发请求到 / ,而不是 /bar/baz 。

如果想实现通常情况下Nginx 的转发行为,可以在上述配置中去掉upstream末尾的 / ,手工写rewrite 实现:

resolver 172.16.0.23;
set $upstream_endpoint http://service-1234567890.us-east-1.elb.amazonaws.com;
location /foo/ {
    rewrite ^/foo/(.*) /$1 break;
    proxy_pass $upstream_endpoint;
}

这样,当我们请求 /foo/bar/baz 时,后端 upstream 会收到 /bar/baz 的请求。


参考:Nginx with dynamic upstreams


伪静态

在WordPress中,固定链接可以采用Rewrite(伪静态),默认使用动态链接。在Typecho中,同样的设置叫做“永久链接”,然而Typecho默认使用了Pathinfo(也就是 http://域名/index.php/文章地址)的链接方式。如果服务器不支持Pathinfo则Typecho可以浏览首页却无法查看文章或者进入后台。如果开启了Typecho的Rewrite,则Typecho的管理后台默认支持自定义伪静态。如图:

新记(2016-02-19):

nginx官方早就推荐使用 try_files 来替代 rewrite 了,贴一个新的推荐设置方法(with cgi.fix_pathinfo=0):

    location / {
        # one line for typecho staticize and with cgi.fix_pathinfo=0 in php.ini more secure
        try_files $uri $uri/ @typecho;
    }
    
    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    location ~ \.php$ {
        #try_files $uri @typecho;
        fastcgi_pass 127.0.0.1:9000;
        #include from snippets/fastcgi-php.conf:
        # regex to split $uri to $fastcgi_script_name and $fastcgi_path
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        # Check that the PHP script exists before passing it
        try_files $fastcgi_script_name =404;
        # Bypass the fact that try_files resets $fastcgi_path_info
        # see: http://trac.nginx.org/nginx/ticket/321
        set $path_info $fastcgi_path_info;
        fastcgi_param PATH_INFO $path_info;
        fastcgi_index index.php;
        include fastcgi.conf;
    }
    
    location @typecho {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param  SCRIPT_FILENAME  $document_root/index.php;
        include fastcgi_params;
    }


---- 旧文:-----------------------------------------

运行环境:nginx/1.6.0 + PHP/5.4.31

1、php.ini中 cgi.fix_pathinfo=1 时的nginx 配置:

    location / {
            #rewrite rule for typecho staticize
             if (!-e $request_filename) {
                 rewrite ^/(.*)$ /index.php/$1 last;
             }
    }
    location ~ ^.+\.php {
            fastcgi_pass unix:/var/run/php5-fpm.sock;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $request_filename;
            #fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;     //也可以,但不推荐
            include fastcgi_params;
    }


2、php.ini中 cgi.fix_pathinfo=0 时的nginx 配置:

为了避免PHP CGI 中 fix_pathinfo 引起的安全隐患,现在一般关闭上述php.ini选项。此时nginx的配置:

    location / {
            #rewrite rule for typecho staticize
             if (!-e $request_filename) {
                 rewrite ^/(.*)$ /index.php/$1 last;
             }
    }
    location ~ ^.+\.php {
            fastcgi_pass unix:/var/run/php5-fpm.sock;
            fastcgi_index index.php;
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 
            include fastcgi_params;
    }


参考资料:



2014/07/02 21:53:02 [error] 4952#0: *8 FastCGI sent in stderr: "Primary script unknown" while reading response header from upstr eam, client: 180.180.180.180, server: localhost, request: "GET /info.php HTTP/1.1", upstream: "fastcgi://unix:/var/run/php5-fpm.sock:", host: "100.100.100.100"

nginx+PHP,访问PHP页面显示空白并提示“No input file specified”,nginx错误日志中显示"Primary script unknown" 。这实际上是PHP在display_errors设置为off时,PHP报出的错误。在PHP默认安装下,会将错误输出到stderr或stdout、然后被nginx捕获并记录在error.log中。其根本原因是PHP无法找到nginx告诉它的相应文件,有一些原因可以导致这个问题,如下。

1、给PHP传递了错误的路径

大多数原因是初学者参考网上google的文章来配置导致的。Nginx通过 SCRIPT_FILENAME 这个 fastcgi_param 的值来告诉PHP需要执行的文件。网上大多数的文章将这个参数的值设置为 $document_root$fastcgi_script_name ,有时这样配置还是为了解决"No input file specified" 问题的。但是这会导致路径被“写死”,突然有一天你发现问题出现了。

原因在于nginx有三级配置继承关系:三个层次的区块分别为http、server 和 location,是父子的关系。nginx配置里的命令向下继承,但不会向上继承或交叉。比如你在一个 location中定义了一些东西,是不会应用到其他location中的。

典型的一些初学者,在 location / 中定义 index 和 root ,因为网上的文章是这么教的。当再使用 $document_root 来定义SCRIPT_FILENAME 时,root 并没有真正被定义,因此 SCRIPT_FILENAME 的值仅仅变成了URI,导致PHP找不到该文件。

简单的解决办法是在 server 或 http 区块中定义 root 。

2、不正确的文件权限

当我告诉他你的权限设置不正确时,大多数人都不相信我。他们盯着脚本文件的权限设置,PHP用户有读取权限啊!?很遗憾,这表现出他对Unix用户权限缺乏理解。PHP的启动用户不仅需要脚本文件的读权限,还需要能进入到脚本文件所在的目录(及上层目录),这就需要PHP用户拥有这些目录的执行权限。举一个例子:

说你在 /var/www 目录下有一个 index.php 的脚本文件,需要PHP用户对 /var/www/index.php 有读权限,且需要对 /var 和 /var/www 目录有执行权限。

我就遇到过一次新装的Debian nginx+php环境,在将PHP用户加入到 $document_root 目录拥有者的用户组后,因为没有重启 php-fpm 而导致的权限没有生效问题,查了半天原因。

- 阅读剩余部分 -