文章

Nginx

之前因为折腾小服务器,好好部署了一下nginx:

这次好好介绍一下nginx,尤其是配置。

  • http://nginx.org/en/docs/
  1. 基础操作
  2. 处理请求
    1. 监听什么
      1. listen
    2. 匹配哪些请求
      1. server_name
      2. default_server
    3. 请求路径怎么处理
      1. location
    4. 配置静态网站
      1. root
      2. alias
      3. try_files
      4. index
    5. 配置反向代理
      1. proxy_pass
      2. upstream负载均衡
  3. 分析配置
    1. 默认配置
    2. sites-enabled/netdata
    3. sites-enabled/netdata-subfolder
    4. 没有能匹配上的location时,请求到底去哪儿了

基础操作

  • install:http://nginx.org/en/docs/install.html
  • 新手教程:http://nginx.org/en/docs/beginners_guide.html

nginx的架构:nginx有两类进程,一个master进程,负责开关worker进程。一堆worker进程用来干活。每次配置改变,使用reload时,master就会加载配置,关闭旧worker,再使用新配置启动新worker。

nginx的启动不需要加任何参数/user/sbin/nginx。在启动之后,可以使用-s传入以下信号:

  • stop — fast shutdown
  • quit — graceful shutdown
  • reload — reloading the configuration file
  • reopen — reopening the log files

如果是debian,还可以用systemd开关nginx:systemctl restart nginx.service,更方便。

处理请求

  • http://nginx.org/en/docs/http/request_processing.html

nginx处理请求的流程:

  1. 监听网卡、端口;
  2. 收到请求,根据ip或者http的Host header判断要使用哪个server处理;
  3. 根据请求路径,决定返回的内容;

所以配置nginx,主要就是配置一个个server,每个server代表一个服务。每个server主要配置三个方面:

  1. 监听什么;
  2. 匹配哪些请求;
  3. 请求的路径要怎么处理;

所有这些都是在server命令里配置的:

  • http://nginx.org/en/docs/http/ngx_http_core_module.html#server

所有http的配置都放在http命令里

1
2
3
4
5
6
http {
    server {
        ...
    }
    ...
}
  • http://nginx.org/en/docs/http/ngx_http_core_module.html#http

所有tcp/udp的配置都放在stream命令里

1
2
3
4
5
6
stream {
    server {
        ...
    }
    ...
}
  • http://nginx.org/en/docs/stream/ngx_stream_core_module.html#stream

监听什么

一个服务首先要确定监听哪个网卡上的请求。主要通过listen来制定:

  • http://nginx.org/en/docs/http/ngx_http_core_module.html#listen

listen

listen设定服务监听的ip和端口,只有这个socket上面收到的请求能被这个server处理。其实这样理解有点儿误区,因为server配置的叫virtual server,所以并不是这些服务绑定在socket上,而是nginx会绑定到这些socket上,然后按照server的listen配置的socket来分发这些请求。这也是为什么多个server可以listen同一个socket,因为他们都是虚拟server。listen可以理解为是nginx用来分发所收到的请求的第一个过滤条件:按照ip和port过滤。

但是,还是说“该server监听某个socket”更顺嘴一些。

ip不一定非得是ipv4,还可以是ipv6。如果都不写,只写个port,那就是监听所有的ipv4和ipv6

1
2
3
4
5
server {
    listen      80;
    server_name example.org www.example.org;
    ...
}

listen也不止可以指定网络socket,还可以指定unix socket,直接写socket路径,比如:listen unix:/var/run/nginx.sock;

除了指定监听的地址,listen还可以指定一些行为:

  • default_server:指定哪一个server是这个socket上的请求的默认处理server。见后面的介绍;
  • ssl:这个socket上的链接要使用ssl默认。比如监听443端口时,一般都是使用https,所以要开启ssl:listen 443 ssl;
  • http2:只接受HTTP2请求;

等等,还有一些其他参数。

匹配哪些请求

server_name

server_name用来配置虚拟服务的名字: http://nginx.org/en/docs/http/ngx_http_core_module.html#server_name

其实它也是一个过滤条件:只有请求头里的Host匹配该虚拟服务的名字,该服务才会处理这个请求

比如这个server只处理Host为以下两个名称的请求:

1
2
3
server {
    server_name example.com www.example.com;
}

server_name还支持正则,还能使用正则表达式的捕获(匿名捕获、具名捕获)获取信息,在后面的命令中使用。

没有Host header的请求怎么处理?

给server_name添加一个空值server_name "",专门处理没有Host header的请求。

Since version 0.8.48, this is the default setting for the server name, so the server_name “” can be omitted. In earlier versions, the machine’s hostname was used as a default server name.

default_server

可以设置某个socket上的默认server。

比如以下三个server中,后两个都是各自监听socket上的默认server:

1
2
3
4
5
6
7
8
9
10
11
server {
    listen      192.168.1.1:80;
    server_name example.org www.example.org;
    ...
}

server {
    listen      192.168.1.1:80 default_server;
    server_name example.net www.example.net;
    ...
}

192.168.1.1:80上收到的请求的Host既不满足第一个server,也不满足第二个server,或者请求头里根本就没有Host时,后面那个server(default_server)就是默认处理该请求的server

如果第一个和第二个server都不写default_server,那第一个就是默认server:总有一个server要处理这个socket上的请求,不能没人处理它

  • http://nginx.org/en/docs/http/ngx_http_core_module.html#listen
  • http://nginx.org/en/docs/http/request_processing.html

请求路径怎么处理

location

  • http://nginx.org/en/docs/http/ngx_http_core_module.html#location

设置请求的不同路径的不同处理方式。请求在做完ip:port匹配、Host匹配后,就决定是由某个server来处理了。在这个server里还要做location匹配,根据请求路径的匹配结果,来决定要怎么处理这个请求。

location的语法:

1
2
Syntax:	location [ = | ~ | ~* | ^~ ] uri { ... }
        location @name { ... }

location有严格匹配、最长前缀匹配、正则匹配等方式。看定义比较费劲:

  1. =是严格匹配。如果和请求匹配了,直接终止。如果有大量请求都是同一个路径,可以给这个路径配置严格匹配,能够少很多前缀匹配、正则匹配,提升性能。
  2. 如果不明白为什么严格匹配会提升性能,看完最长前缀匹配就明白了:
    1. 最长前缀匹配是和所有普通匹配进行比较,找到最长匹配的那个。但是别急,这只是个备胎,只有正则匹配扑空了,才轮到备胎上位……
    2. 然后按照正则定义的顺序进行正则匹配,第一个匹配的正则表达式就是最终匹配到的内容;
    3. 如果一个正则都没有匹配到,刚刚最长前缀匹配找到的那个才算匹配。所以最长前缀匹配只是个备胎……还是要看正则。这样的话严格匹配当然快了,匹到直接就返回了。最长前缀匹配为了不匹配正则,可以以^~开头,代表“不正则”,直接按这个最长前缀匹配返回了;
  3. 正则匹配:~代表正则匹配(所以上述^~就代表不用正则了),~*代表正则匹配时忽略大小写;

直接看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
location = / {
    [ configuration A ]
}

location / {
    [ configuration B ]
}

location /documents/ {
    [ configuration C ]
}

location ^~ /images/ {
    [ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ {
    [ configuration E ]
}
  1. 请求路径是/,第一个严格匹配直接就返回了;
  2. 请求是/index.html,匹配到第二个。因为第一个和它不严格匹配,第二个和它有最长的共同前缀/,后面正则也没有和它匹配的;
  3. /documents/document.html,匹配到第三个,理由同二。不过//documents/都是它的前缀,后者更长;
  4. /images/1.gif,匹配第四个,和它有最长公共前缀/images/^~反正则;
  5. /documents/1.jpg,匹配第五个。虽然和第三个有最长的公共前缀,但是符合正则,所以正则优先了

配置静态网站

location确定处理什么样的请求之后,接下来就是怎么处理这个请求了。

静态网站是比较简单的处理:直接给请求返回静态文件。

root

  • http://nginx.org/en/docs/http/ngx_http_core_module.html#root
    1
    2
    
    Syntax:	root path;
    Default: root html;
    

    root代表静态文件所在的位置。默认是html

如果匹配上了下面这个location:

1
2
3
location /i/ {
    root /data/w3;
}

假设请求路径是/i/top.gif,返回的就是/data/w3/i/top.gif文件

root还可以设置在location外面,代表所有location默认的root。

alias

  • http://nginx.org/en/docs/http/ngx_http_core_module.html#alias

root是拼接root值和location里的uri,alias是用alias值替换location里的uri

1
2
3
location /i/ {
    alias /data/w3;
}

假设请求路径是/i/top.gif,返回的就是/data/w3/top.gif文件

如果location和alias值的最后一部分相同:

1
2
3
location /images/ {
    alias /data/w3/images/;
}

那不如使用root会更明了:

1
2
3
location /images/ {
    root /data/w3;
}

try_files

  • http://nginx.org/en/docs/http/ngx_http_core_module.html#try_files

在location里或者server里设定,把uri当做文件来查找,root + uri,找到就返回。如果都查不到,就内部重定向到最后一个参数。这个重定向是nginx内部做的,不是返回给client让client做的。

比如:

1
2
3
4
5
    location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to displaying a 404.
            try_files $uri $uri/ =404;
    }
  1. 先把uri当做文件来查找(root + uri),找到就返回;
  2. 再把uri当做文件夹来查找,找到文件就返回。默认查找的是root + uri/ + index[.html](配合下面要说的index指令);
  3. 如果都没找到,就重定向到最后一个参数,404;

index

  • http://nginx.org/en/docs/http/ngx_http_index_module.html
1
2
3
Syntax:	index file ...;
Default: index index.html;
Context: http, server, location

指定默认使用的文件。默认值是indexindex.html

比如上述try_files指令把uri当文件夹来找文件时,默认找的就是找root文件夹下的index文件。

配置反向代理

location里还可以配置把请求打给另一个服务,此时nginx就作为反向代理使用。

proxy_pass

  • http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass

把请求打给localhost:8080:

1
2
3
    location / {
        proxy_pass http://localhost:8080;
    }

还能同时配置静态服务和反向代理,不同路径的请求使用不同的服务方式:

1
2
3
4
5
6
7
8
9
server {
    location / {
        proxy_pass http://localhost:8080;
    }

    location /images/ {
        root /data;
    }
}

反向代理的时候,如果location配置的prefix带斜杠slash,会默认把和prefix相等,但不带斜杠的请求301重定向到带斜杠的请求。也就是下面配置的这个效果:

1
2
3
    location = /netdata {
        return 301 /netdata/;
    }

默认会把/netdata重定向为/netdata

如果不想做这种默认,需要在定义带slash的location的时候,再定义一个不带slash的版本,并显式设定他们的行为:

1
2
3
4
5
6
7
location /user/ {
    proxy_pass http://user.example.com;
}

location = /user {
    proxy_pass http://login.example.com;
}
  • http://nginx.org/en/docs/http/ngx_http_core_module.html#location

upstream负载均衡

  • http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream

反向代理的时候还可以使用upstream配置一组服务器:

1
2
3
4
5
6
7
upstream backend {
    server backend1.example.com weight=5;
    server 127.0.0.1:8080       max_fails=3 fail_timeout=30s;
    server unix:/tmp/backend3;

    server backup1.example.com  backup;
}

如果不指定策略就是轮询分发请求。上述策略指定了weight权重、备份服务器等。

此时,location就可以使用upstream配置的名称,把请求打给这组服务器:

1
2
3
    location / {
        proxy_pass http://backend;
    }

分析配置

学完了上面的内容,把之前装nginx使用的配置再详细分析一下。

默认配置

nginx的配置文件是nginx.conf,Debian把它放在/etc/nginx下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
        # multi_accept on;
}

http {

        ##
        # Basic Settings
        ##

        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;
        # server_tokens off;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        gzip on;

        # gzip_vary on;
        # gzip_proxied any;
        # gzip_comp_level 6;
        # gzip_buffers 16 8k;
        # gzip_http_version 1.1;
        # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}


#mail {
#       # See sample authentication script at:
#       # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
#       # auth_http localhost/auth.php;
#       # pop3_capabilities "TOP" "USER";
#       # imap_capabilities "IMAP4rev1" "UIDPLUS";
#
#       server {
#               listen     localhost:110;
#               protocol   pop3;
#               proxy      on;
#       }
#
#       server {
#               listen     localhost:143;
#               protocol   imap;
#               proxy      on;
#       }
#}

各个命令可以在core下找到:

  • https://nginx.org/en/docs/ngx_core_module.html

worker_processes指定worker的数量(进程数),auto代表“和cpu核数一致”:

  • http://nginx.org/en/docs/ngx_core_module.html#worker_processes

user指定了worker线程的执行者,这里配置的是用户www-data

  • http://nginx.org/en/docs/ngx_core_module.html#user
1
2
» id www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)

pid指定存放nginx的id的文件。

这个配置最引人注目的应该就是http指令了,占了很大一块空间。它内部除了开启access log、error log,很重要的就是两个include:

1
2
        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;

这两个include会默认把conf.d下的.conf文件和sites-enabled下的所有文件加载进来。所以这两个地方可以把不同配置拆开到不同文件里。但放的也只能是http相关的服务器配置,tcp的不行。

conf.d默认是空的,sites-enabled有个文件default

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

# Default server configuration
#
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        # SSL configuration
        #
        # listen 443 ssl default_server;
        # listen [::]:443 ssl default_server;
        #
        # Note: You should disable gzip for SSL traffic.
        # See: https://bugs.debian.org/773332
        #
        # Read up on ssl_ciphers to ensure a secure configuration.
        # See: https://bugs.debian.org/765782
        #
        # Self signed certs generated by the ssl-cert package
        # Don't use them in a production server!
        #
        # include snippets/snakeoil.conf;

        root /var/www/html;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }

        # pass PHP scripts to FastCGI server
        #
        #location ~ \.php$ {
        #       include snippets/fastcgi-php.conf;
        #
        #       # With php-fpm (or other unix sockets):
        #       fastcgi_pass unix:/run/php/php7.4-fpm.sock;
        #       # With php-cgi (or other tcp sockets):
        #       fastcgi_pass 127.0.0.1:9000;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #       deny all;
        #}
}


# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
#       listen 80;
#       listen [::]:80;
#
#       server_name example.com;
#
#       root /var/www/example.com;
#       index index.html;
#
#       location / {
#               try_files $uri $uri/ =404;
#       }
#}

因为是被include到http里,所以这里就直接配置server了。

主要配置就是一个server,作为所有80端口上请求的default server。

location指定了try files,即:所有的uri,都作为file去处理、再作为文件夹去处理,啥也找不到的话最后就返回404页面

  1. uri作为file,file寻找地址为root + uri,即/var/www/html文件夹下的以uri命名的文件;
  2. uri作为文件夹,file寻找地址为root + uri + index,即/var/www/html/<uri>/文件夹下的index.htmlindex.htmindex.nginx-debian.html中的一个;

这个location是一个非常标准的配置!因为最后这个location能匹配所有路径。

sites-enabled/netdata

再看看netdata的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
upstream backend {
    # the Netdata server
    server 127.0.0.1:19999;
    keepalive 64;
}

server {
    # nginx listens to this
    listen 80;
    listen 443 ssl;
    # uncomment the line if you want nginx to listen on IPv6 address
    listen [::]:80;
    listen [::]:443 ssl;

    # use password to access
    auth_basic "Protected";
    auth_basic_user_file passwords;

    # the virtual host name of this
    server_name netdata.puppylpg.xyz;
    ssl_certificate /etc/nginx/puppylpg-ssl/cert.pem;
    ssl_certificate_key /etc/nginx/puppylpg-ssl/key.pem;

    if ($scheme = http ) {
        return 307 https://$host$request_uri;
    }

    location / {
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_pass_request_headers on;
        proxy_set_header Connection "keep-alive";
        proxy_store off;
    }
}

这个请求也相对明了:

  1. 配置一组服务器,其实就一个,是netdata服务所在的地址;
  2. 配置所有80和443ssl端口上、Host为netdata.puppylpg.xyz的流量,都打给这个server;
  3. server开启basic认证;
  4. 指定server ssl使用的私钥、数字签名;
  5. 如果打过来的是80端口上的http,返回307跳转,让client使用https,访问443;

最后配置了一个location。默认匹配所有请求路径。这个location除了使用proxy_pass把请求转发到上面配置的netdata upstream之外,还配置了一些header设置。

proxy header设置很重要。否则client打过来的请求的一些header,nginx再转发给服务的时候就丢了。

默认nginx会设好两个header

1
2
3
4
Syntax:	proxy_set_header field value;
Default: proxy_set_header Host $proxy_host;
proxy_set_header Connection close;
Context: http, server, location

一个是把Host设为client请求里真正的Host,另一个是设置Connection。

  • http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header

这个配置则是设置了4个header:

1
2
3
4
5
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass_request_headers on;
        proxy_set_header Connection "keep-alive";

增加三个X-Forward-*,Connection设为keep alive,而不是默认的close,使得能够使用长连接。

最后还使用proxy_pass_request_header把client请求中的其他header也发给了服务。这个默认是开启的

1
2
3
Syntax:	proxy_pass_request_headers on | off;
Default: proxy_pass_request_headers on;
Context: http, server, location
  • http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass_request_headers

与之相对的,还有一个proxy_pass_header,开启之后可以给nginx的response header加一些默认关闭的header。比如Date、Server,否则nginx的返回的Server显示的就是nginx和自己的版本号。其实关闭会更安全,省得暴露后端实际用的什么服务。

  • http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass_header
  • http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_hide_header

sites-enabled/netdata-subfolder

最后还配置了一个使用路径访问netdata的方式puppylpg.xyz/netdata,在sites-enabled/netdata-subfolder里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
upstream netdata {
    server 127.0.0.1:19999;
    keepalive 64;
}

server {
    listen 80;
    # uncomment the line if you want nginx to listen on IPv6 address
    listen [::]:80;

    # the virtual host name of this subfolder should be exposed
    #server_name netdata.example.com;
    server_name puppylpg.xyz;

    # use password to access
    auth_basic "Protected";
    auth_basic_user_file passwords;

    location = /netdata {
        return 301 /netdata/;
    }

    location ~ /netdata/(?<ndpath>.*) {
        proxy_redirect off;
        proxy_set_header Host $host;

        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_http_version 1.1;
        proxy_pass_request_headers on;
        proxy_set_header Connection "keep-alive";
        proxy_store off;
        proxy_pass http://netdata/$ndpath$is_args$args;

        gzip on;
        gzip_proxied any;
        gzip_types *;
    }
}

和上面主要的不一样地方在于:

  1. 没使用https,只服务80的http;
  2. Host是puppylpg.xyz
  3. 启用了location,严格匹配/netdata,并重定向到/netdata/,其实添加slash是默认行为,不需要配置;
  4. 最后配置了一个正则匹配uri,匹配任意/netdata/开头的请求,并把/netdata/后面的路径和参数一起带上,转发给upstream

比如原来请求netdata的链接为:<ip:port>/#after=-600;before=0;=undefined;theme=slate;utc=Asia%2FHong_Kong

现在需要使用:puppylpg.xyz/netdata/#after=-600;before=0;=undefined;theme=slate;utc=Asia%2FHong_Kong

最后实际转发给netdata的是:/#after=-600;before=0;=undefined;theme=slate;utc=Asia%2FHong_Kong

is_argsargs是内置的变量:

  • http://nginx.org/en/docs/http/ngx_http_core_module.html#variables

注意这个upstream不能和上一个upstream名字一样!因为最终会被merge到一个nginx.conf文件里,相当于有两个重名的server,nginx是不允许的

没有能匹配上的location时,请求到底去哪儿了

当没有能匹配上的Host时,nginx会使用default_server处理请求。但是处理请求时没有能匹配上的location时,会发生什么情况呢?

按照上面的配置,一切都正常。然后我就踩到了这个坑里。有两个请求一时把我搞蒙了:

  1. http://www.puppylpg.xyz
  2. http://puppylpg.xyz

www.puppylpg.xyzpuppylpg.xyz的CNAME,也就是说他们指向同一个ip。

第一个请求还是比较好理解的:

  1. 请求达到80端口;
  2. 因为没有任何配置符合www.puppylpg.xyz这个Host,所以使用了sites-enabled/default配置里的server;
  3. 按照try files的指示,在root /var/www/html下寻找index指定的文件,第一个就找到了index.html
  4. 返回/var/www/html/index.html的内容。

/var/www/html/下有两个文件,index.html是我装apache的时候自动生成的,index.nginx-debian.html是后来装nginx的时候生成的:

1
2
3
4
 » ls -lhb /var/www/html
total 16K
-rw-r--r-- 1 root root 11K Dec  1 04:31 index.html
-rw-r--r-- 1 root root 612 Dec  1 12:10 index.nginx-debian.html

所以网页显示的是apache的页面,符合预期。

curl看header也正常:

1
2
3
4
5
6
7
8
9
10
11
$ curl -I http://www.puppylpg.xyz
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sun, 12 Dec 2021 11:17:32 GMT
Content-Type: text/html
Content-Length: 10701
Last-Modified: Wed, 01 Dec 2021 09:31:22 GMT
Connection: keep-alive
ETag: "61a740ea-29cd"
Accept-Ranges: bytes

第二个请求按理来说,会达到sites-enabled/netdata-subfolder这个配置的server里,因为它接收80上Host为puppylpg.xyz的请求。

实际上也就是它接收的,因为它配置了basic验证,所以如果不给出用户名和密码,会显示401 UnAuthorized。使用密码才可以访问:

1
2
3
4
5
6
7
8
9
10
11
$ curl --dump-header - http://puppylpg.xyz -u <username:password>
HTTP/1.1 200 OK
Server: nginx/1.18.0
Date: Sun, 12 Dec 2021 11:18:40 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Tue, 21 Apr 2020 14:09:01 GMT
Connection: keep-alive
ETag: "5e9efe7d-264"
Accept-Ranges: bytes

说明确实请求被这个server处理了。但疑惑的是它返回的是nginx的页面,也就是index.nginx-debian.html的内容。

这就很疑惑了,它为什么返回这个页面呢?实际上虽然请求交给了这个server处理,但是根本没有匹配请求的location。因为我只定义了/netdata,没定义/

一个未定义location的请求,会发生什么情况呢?至少不应该返回index.nginx-debian.html,因为之前的index指令定义的顺序是

1
index index.html index.htm index.nginx-debian.html;

index.htmlindex.nginx-debian.html,要返回也返回前者——apache的页面。

再者,这个index定义在sites-enabled/default配置的server里,和sites-enabled/netdata-subfolder这个server压根也没关系啊!

总之就是很迷,直到我随便敲了个http://puppylpg.xyz/hello,依然是由这个server处理,但是hello页面是肯定没有的,所以报错了404 not found。查一下nginx error log:

1
2021/12/12 06:32:37 [error] 1041325#1041325: *3107 open() "/usr/share/nginx/html/hello" failed (2: No such file or directory), client: 114.249.24.58, server: puppylpg.xyz, request: "GET /hello HTTP/1.1", host: "puppylpg.xyz"

纽约时间(西五区)。不然早上六点还不睡我已经疯了hhh

这下看出来了,访问的是/usr/share/nginx/html文件夹下的文件!这个文件夹又是哪来的?根本没配置啊。

在这里找到了答案:

  • https://serverfault.com/a/940174/801099

nginx编译时候默认的--prefix是这个文件夹:

1
2
3
4
5
» sudo nginx -V
nginx version: nginx/1.18.0
built with OpenSSL 1.1.1k  25 Mar 2021
TLS SNI support enabled
configure arguments: --with-cc-opt='-g -O2 -ffile-prefix-map=/build/nginx-q9LD4J/nginx-1.18.0=. -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module

当没有location能匹配到uri的时候,它就来这个文件夹找文件了

而这个文件夹下,有一个名为index.html的文件,内容和上面index.nginx-debian.html的文件一样:

1
2
3
>> ll /usr/share/nginx/html 
total 4.0K
-rw-r--r-- 1 root root 612 Apr 21  2020 index.html

所以这就是为什么第一个请求返回apache页面,第二个请求返回nginx页面了。

  • https://www.nginx.com/resources/wiki/start/topics/tutorials/installoptions/

正因为如此,我上面才会说,为什么配置一个location /很重要!因为这样所有的行为都是可预期的了,否则根本想不到还有这个默认找文件的方式……

或许给error log开启debug选项也是不错的找问题途径:

1
2
    access_log /var/log/nginx/access.log debug;
    error_log /var/log/nginx/error.log debug;
本文由作者按照 CC BY 4.0 进行授权