Nginx疑难杂症汇总


这里主要是为了记录在使用 Nginx 的时候遇到的问题及其处理解决方法。

诡异的问题时常发生,如果正确的处理它们呢?将是一个值得思考和学习的问题!通过对错误和异常问题的排除和处理,能够增长我们的知识储备已经掌握处理问题最为有效和使用的方式。这里将会记录常见的集群使用的问题,以备不时之需。

Nginx疑难杂症汇总

# 获取帮助信息
$ nginx -h
nginx version: nginx/1.18.0 (Ubuntu)
Usage: nginx [-?hvVtTq] [-s signal] [-c filename] [-p prefix] [-g directives]
Options:
  -?,-h         : this help
  -v            : show version and exit
  -V            : show version and configure options then exit
  -t            : test configuration and exit
  -T            : test configuration, dump it and exit
  -q            : suppress non-error messages during configuration testing
  -s signal     : send signal to a master process: stop, quit, reopen, reload
  -p prefix     : set prefix path (default: /usr/share/nginx/)
  -c filename   : set configuration file (default: /etc/nginx/nginx.conf)
  -g directives : set global directives out of configuration file

# 值得一说的就是 -s(发送信号)/-g(从配置文件中设置全局指令) 参数
stop: 快速停止nginx服务
quit: 完整有序的停止nginx服务
reopen: 重新打开日志文件(日志文件mv等操作后会重新生成)
reload: 修改配置后重新加载生效

# 简述reload命令的执行步骤
1. master进程检查配置文件的正确性,若错误则返回错误信息并终止(采用原配置文件继续工作;因为worker并未受到影响),若正常则继续后续步骤。
2. 用新的配置文件,启动新的worker进程。
3. nginx将新的请求,分配新的worker进程。
4. 等待以前的worker进程的全部请求已经都返回后,关闭相关worker进程。
5. 重复上面过程,知道全部旧的worker进程都被关闭掉。

报错信息:Request Request Header Or Cookie Too Large

  • [问题起因] 服务新建之后,其他用户登录网页的时候,页面提示 400: Bad Request 错误,大致如下所示:

Nginx疑难杂症汇总 - 提示Cookie长度超限

  • [解决方法] 后来发现,是因为多个服务公用同一个域名导致在该域名下面存储的 Cookie 太多导致的,可以设置如下两个参数来缓解该问题的出现。

  • 设置读取客户端请求头的缓冲区大小。对于大多数请求,1K 字节的缓冲区就足够了。但是,如果一个请求包含很长的 cookie,或者来自 WAP 客户端,那么它可能不适合 1K。如果请求行或请求头字段不适合这个缓冲区,那么将分配更大的缓冲区,由 large_client_header_buffers 指令配置。

Nginx疑难杂症汇总 - client_header_buffer_size

  • 设置用于读取大型的客户端请求头的缓冲区的最大数量和大小。请求行不能超过一个缓冲区的大小,否则会向客户端返回 414Request-URI Too Large)错误。请求头字段也不能超过一个缓冲区的大小,否则会将 400Bad Request)错误返回给客户端,缓冲区仅按需分配。默认情况下,缓冲区大小等于 8K 字节。 如果在请求处理结束后将连接转换为保持活动状态,则会释放这些缓冲区。

Nginx疑难杂症汇总 - large_client_header_buffers

  • 后来网上找到了,对 nginx 处理 header 时的方法,大致如下所示:
    • 1.先处理请求的 request_line,之后才是 request_header
    • 2.这两者的 buffer 分配策略相同。
    • 3.先根据 client_header_buffer_size 配置的值分配一个 buffer,如果分配的 buffer 无法容纳 request_line/request_header,那么就会再次根据 large_client_header_buffers 配置的参数分配 large_buffer,如果 large_buffer 还是无法容纳,那么就会返回 414/400 错误。
  • 根据对手册的理解,这两个指令在配置 header_buffer 时的使用场景是不同的,个人理解如下:
    • 1.如果你的请求中的 header 都很大,那么应该使用 client_header_buffer_size,这样能减少一次内存分配。
    • 2.如果你的请求中只有少量请求 header 很大,那么应该使用 large_client_header_buffers,因为这样就仅需在处理大 header 时才会分配更多的空间,从而减少无谓的内存空间浪费。

2. 开启转发 Delete 方法

老版本 Nginx 默认不转发 put/delete 等方法

  • [问题起因] 使用 Nginx 代理网盘服务,但是操作删除文件的时候,提示有问题。

  • [解决方法] ngx_http_dav_module 模块是为通过 WebDAV 协议自动管理文件而设计的,该模块处理 HTTPWebDAV 的请求方法 PUTDELETEMKCOLCOPYMOVE。默认情况下,该模块不会构建,需要使用编译参数 -–with-http_dav_module 开启。需要额外 WebDAV 方法的 WebDAV 客户端操作,在该模块中不会起作用。

# 示例配置
location / {
    root                    /data/www;

    client_body_temp_path   /data/client_temp;

    dav_methods PUT DELETE  MKCOL COPY MOVE;

    create_full_put_path    on;
    dav_access              group:rw  all:r;

    limit_except GET {
        allow 192.168.1.0/32;
        deny  all;
    }
}

3. 配置多个 HTTPS 主机

在 Nginx 服务上如何在同一个 IP 上配置多个 HTTPS 主机呢?

  • [问题起因] 最近有个需求,就是需要保证新旧域名同时运行。那么,这就需要对于 https 的域名,在同一个 IP 上如何同时存在多个虚拟主机了。

Nginx疑难杂症汇总 - 配置多个HTTPS主机

  • [解决方法] Nginx 支持 SSL 需要使用 --http_ssl_module 这个模块,如果使用的版本没有安装的话,则需要手动安装或者使用新版本。我们这里使用的 Docker 容器镜像,该版本默认是支持的。
# 手动编译安装SSL支持
$ ./configure --prefix=/usr/local/nginx \
    --with-http_ssl_module \
    --with-openssl-opt="enable-tlsext" ...

# 编译并安装
$ make && make install

# 查看版本是否支持SSL功能
root@7e201233b8e1:/# nginx -V | grep ssl
nginx version: nginx/1.19.10
built by gcc 8.3.0 (Debian 8.3.0-6)
built with OpenSSL 1.1.1d  10 Sep 2019
TLS SNI support enabled
server {
    listen       443 ssl default_server;
    server_name  www.escapelife.site;

    ssl_certificate            "/xxx/escapelife_site.crt";
    ssl_certificate_key        "/xxx/escapelife_site.key";

    location / {
        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_pass http://172.16.123.100:8000;
    }
}

server {
    listen       443 ssl;
    server_name  www.escapelife.com;

    ssl_certificate            "/xxx/escapelife_com.crt";
    ssl_certificate_key        "/xxx/escapelife_com.key";

    location / {
        proxy_http_version 1.1;
        proxy_set_header Host $http_host;
        proxy_pass http://172.16.123.101:8000;
    }
}

4. Nginx 实现协议自跳转

利用 Nginx 的 497 状态码实现 http 跳转 https

  • [问题起因] 有一个需求,就是将 http 的请求转发到 https 上面,我们需要怎么做呢?
# Nginx的497状态码定义
497 - normal request was sent to HTTPS
  • [解决方法] 当部署的 Nginx 网站只允许 https 协议访问的时候,我们可以利用 error_page 命令将 497 状态码的链接重定向到 https 的地址上面。因为使用 http 协议访问的时候,Nginx 会报出 497 这个自定义的错误码,我们刚好利用了这点来完成配置的。
    • httphttpstcp 的上层协议,当 Nginx 服务器建立 tcp 连接后,根据收到的第一份数据来确定客户端是希望建立 tls 还是 http
    • Nginx 会判断 tcp 请求的首写节内容以进行区分,如果是 0x80 或者 0x16 就可能是 ssl 或者 tls,然后尝试 https 握手。如果端口开启了 https,但请求过来的并不是,会抛出一个 http 级别的错误。
    • 这个错误的状态码是 NGX_HTTP_TO_HTTPS,错误代码 497,然后在返回 response 中会抛出一个 400 错误(因为 497 不是标准状态码,丢给浏览器也没有用),这时浏览器会显示 “400 Bad Request,The plain HTTP request wes sent to HTTPS port”。这样,可以对 497 进行路由处理,做 302 重定向
# 示例配置
server {
    listen       443;
    server_name  www.escapelife.com;

    ssl                  on;
    ssl_certificate      cert/test.pem;
    ssl_certificate_key  cert/test.key;

    # 让http请求重定向到https请求
    error_page 497  https://$host$uri?$args;
}
  • 当然,我们这里还可以使用其他方式完成同样的目的,但是使用 497 这个状态码,可以不用开放 80 端口,直接将其转发到 https 协议上面。
# nginx的rewrite方法
# 将所有http请求通过rewrite重写到https上
sever{
    listen          80;
    server_name     www.escapelife.com;
    rewrite ^(.*)$  https://$host$uri permanentl;
}

server {
    listen       443;
    server_name  www.escapelife.com;

    ssl                  on;
    ssl_certificate      cert/test.pem;
    ssl_certificate_key  cert/test.key;

    ......
}

5. 用 Nginx 伪装 SSH 端口

利用 Nginx Stream 模块把 SSH 藏在 443 端口

  • [问题起因] 虽然可以使用 fail2ban 防火墙工具护体(每天 auth.log 里一堆垃圾试探),但是使用 Stream 模块加固 SSH 服务器的连接更上一层楼。

  • [解决方法] 这样做的好处在于,当我们连接服务(:ssh)的时候,需要经过 443 端口进入(可以屏蔽默认的 22 端口或自定义端口),使服务更加安全一些且不要再头痛烦人的嗅探工具了。

stream {
    map $ssl_preread_server_name $name {
        escapelife        xtls;
        www.escapelife    http;
        default           ssh;
    }

    upstream xtls {
        server localhost:8081;
    }

    upstream http {
        server localhost:8080;
    }

    upstream ssh {
        # 默认ssh连接会回落到default然后到本机22端口
        server localhost:22;
        # 你甚至可以在这里挂个openvpn实现自用梯子
    }

    server {
        listen 443 reuseport;
        listen [::]443 reuseport;
        proxy_pass $name;
        proxy_protocol on;
        ssl_preread on;
    }
}

6. 解决 WS 链接 400 问题

内部环境发现页面 websocket 连接错误

  • [问题起因] 因为服务前后端之前使用 WebSocket 进行通信,所以遇到了如下问题:
    • WebSocket 可以减小客户端与服务器端建立连接的次数,减小系统资源开销,只需要一次 HTTP 握手,整个通讯过程是建立在一次连接/状态中,也就避免了 HTTP 的非状态性,服务端会一直与客户端保持连接,直到你关闭请求,同时由原本的客户端主动询问,转换为服务器有信息的时候推送。
    • 客户端的话,支持就很多了,比如 Chrome/Firefox/Safari 等浏览器内置了 JS 语言的 WebSocket 客户端、微信小程序开发框架内置的 WebSocket 客户端等等
# 报错信息大致如下所示
WebSocket connection to 'ws://xxx' failed: Error during WebSocket handshake: Unexpected response code: 400

Nginx疑难杂症汇总 - 解决WS链接400问题

# Nginx从1.3版本开始支持WebSocket的
location /chat/ {
    # 告诉Nginx使用HTTP/1.1通信协议,这是WebSocket必须要使用的协议
    proxy_http_version 1.1;
    # 告诉Nginx当它想要使用WebSocket时,升级响应HTTP请求
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_pass http://backend;
}

7. 用 Nginx 解决跨域问题

利用 Nginx Stream 模块把 SSH 藏在 443 端口

  • [问题起因] 当发现请求出现 403 跨域错误的时候,需要给 Nginx 服务器配置响应的 header 参数来解决。

Nginx疑难杂症汇总 - 用Nginx解决跨域问题 - 网络上采集而来

  • [解决方法] 之前介绍过什么是跨越问题,可以参考之前写的博文(HTTPS 跨越问题处理方法)。
    • [1] Access-Control-Allow-Origin
      • 服务器默认是不被允许跨域的
      • 添加上述配置表示服务器可以接受的请求源
      • 星号(*)表示接受所有跨域的请求
    • [2] Access-Control-Allow-Methods
      • 设置允许的方法类型
    • [3] Access-Control-Allow-Headers
      • 设置当前请求 Content-Type 的值被支持,一般包括 Content-Type/Cache-Control
      • 因为 application/json 格式的请求不属于 MIME 类型,需要先发送预检请求
      • 预检请求会带上头部信息 Access-Control-Request-Headers: Content-Type:
      • 服务器返回的头部信息如果不包含则表示不接受非默认的 Content-Type 会报错
    • [4] 给 OPTIONS 添加 204 的返回
      • 按需添加,不涉及的话,可以不用配置,主要是为了让预检请求通过
      • 为了处理在发送 POST 请求时 Nginx 依然拒绝访问的错误
      • 发送预检请求时,需要用到方法 OPTIONS,所以服务器需要允许该方法
# 可以加到server段中
location / {
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    # 复杂请求时需配置;让预检请求通过;在location中才能添加
    if ($request_method = 'OPTIONS') {
        return 204;
    }
}
# 指定接受那个来源的网址
# http://www.ruanyifeng.com/blog/2016/04/cors.html
add_header Access-Control-Allow-Origin http://sh.xxx.com;

# 要把Cookie发到服务器要服务器同意
# 设置此配置Origin就不能设为星号,必须指定明确与请求网页一致的域名
add_header Access-Control-Allow-Credentials 'true';
  • [补充知识] CORS 是一个 W3C 标准,全称是跨域资源共享(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。简单来说就是跨域的目标服务器要返回一系列的 Headers,通过这些 Headers 来控制是否同意跨域。CORS 提供的 Headers,在 Request 包和 Response 包中都有一部分。
    • 简单请求
      • 请求方法是这三种方法(HEAD/GET/POST)之一
      • HTTP 的头信息不超出这几种字段(Accept/Accept-Language/Content-Language/Content-Type)
      • Content-Type 限于 application/x-www-form-urlencoded/multipart/form-data/text/plain
    • 复杂请求
      • 那种对服务器有特殊要求的请求
      • 比如请求方法是 PUTDELETEOPTIONS 的等
      • 或者 Content-Type 字段的类型是 application/json
      • 非简单请求的 CORS 请求会在正式通信之前增加一次 HTTP 查询请求,称为预检请求(用OPTIONS方法)
# HTTP Response Header
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Expose-Headers
Access-Control-Max-Age

# HTTP Request Header
Access-Control-Request-Method
Access-Control-Request-Headers

8. 为浏览器创建安全环境

新的跨域策略:使用 COOP、COEP 为浏览器创建更安全的环境

  • [问题起因] 前端界面报错,提示:
# browser network
Uncaught (in promise) ReferenceError: ShareArrayBuffer is not defined
# 可以通过 COOP、COEP 来创建隔离环境
add_header 'Cross-Origin-Opener-Policy' 'same-origin';
add_header 'Cross-Origin-Embedder-Policy' 'require-corp';

# 需要注意的是如果提示如下信息,则需要使用https协议
The Cross-Origin-Opener-Policy header has been ignored, because the URL is origin was untrustworthy.
Is was defined either in the final response or redirect.
Please deliver the response using the HTTPS protocol.
You can also use the 'localhost' origin install.

# 所有跨域相关的名词列出来
COEP: Cross Origin Embedder Policy:跨源嵌入程序策略
COOP: Cross Origin Opener Policy:跨源开放者政策
CORP: Cross Origin Resource Policy:跨源资源策略
CORS: Cross Origin Resource Sharing:跨源资源共享
CORB: Cross Origin Read Blocking:跨源读取阻止

9. 负载均衡的排坑和思考

来自 - nginx grpc streaming 负载均衡的排坑和思考

  • [问题起因] Nginx1.13 版本之后就可以支持 grpc 的负载均衡了,使用方法类似 proxy_pass 的语法,但是使用的过程中遇到短连接的问题。
# project stack
"grpc_message":"Received RST_STREAM with error code 1", "grpc_status":13
  • [解决方法] grpc 是基于 http2 的,而 http2 就是长连接的设计,即单个连接可以多路复用(http1 不支持)。知道在使用 nginx proxy_pass upstream 的时候,需要配置 keepalive 参数,不然 nginx 做负载均衡转发一律会按照短连接处理,没想到 grpc upstream 也要配置 keepalive 参数才行。需要注意的是 keepalive 是单个 worker 的连接池,毕竟 nginx 是多进程的,在 nginx 的架构模型下是不能连接共享的。
server {
    listen       9000 http2;
    server_name  _;

    location / {
        grpc_pass grpc://grpc_servers;
    }
}

upstream grpc_servers {
    keepalive 2000;
    server 100.100.100.100:8090;
    server 100.100.100.101:8090;
}

10. 请求客户端响应超时

服务端请求之后,客户端响应超时,并非必现场景!

  • [问题起因] 客户端请求某个服务的时候,当 POST 请求上传数据量打的时候,就会概率发生响应超时的情况。如下所示,在第三个请求的时候,请求超过 60s 之后服务就失败了。虽然可以通过加重试来间接解决这个问题,但是并没有找到对应的问题原因。
    • proxy_connect_timeout(60s) -> 后端服务器连接的超时时间 发起握手等候响应超时时间
      • 该指令设置与 upstream server 的连接超时时间,有必要记住这个超时不能超过 75
      • 这个不是等待后端返回页面的时间,那是由 proxy_read_timeout 声明的
      • 如果你的 upstream 服务器起来了,但是 hanging 住了,那么这个声明是没有用的
    • proxy_read_timeout(60s) -> 该指令设置与代理服务器的读超时时间
      • 它决定了 nginx 会等待多长时间来获得请求的响应
      • 这个时间不是获得整个 response 的时间,而是两次 reading 操作的时间
    • proxy_send_timeout(60s) -> 这个指定设置了发送请求给 upstream 服务器的超时时间
      • 超时设置不是为了整个发送期间,而是在两次 write 操作期间
      • 如果超时后,upstream 没有收到新的数据,nginx 会关闭连接
# project stack
122.100.11.123 - "POST /api/v1/xxx/png/60895/xxx HTTP/1.1" 200 426 2003840 0.513 0.096 "-"
122.100.11.124 - "POST /api/v1/xxx/png/60896/xxx HTTP/1.1" 200 413 1868160 0.484 0.076 "-"
122.100.11.125 - "POST /api/v1/xxx/png/60897/xxx HTTP/1.1" 200 383 11265848 60.114 0.132 "-"
229.207.88.100 - "POST /api/v1/xxx/png/60875/xxx HTTP/1.1" 200 374 11350466 1.214 0.196 "-"
229.207.88.100 - "POST /api/v1/xxx/png/60876/xxx HTTP/1.1" 200 383 16885939 1.148 0.224 "-"
229.207.88.100 - "POST /api/v1/xxx/png/60877/xxx HTTP/1.1" 200 378 16439591 36.457 0.124 "-"
  • [解决方法] 很奇怪,并没有发现关于 60Nginx 设置,所以必定是某些默认参数的默认值。有根据返回码 408 判断初始因为 client_header_timeoutclient_body_timeout 这两个参数导致客户端请求超时了。我们可以看到,正常情况下响应都是非常快的,基本在 60s 之内完全可以返回的。后来发现,是因为上了防火墙,其拿到请求之后需要进行流量分析导致拿到请求半天不给你传 header。有可能需要充值信仰了。。。。
    • client_header_timeout(60s) -> 指定等待 client 发送一个请求头的超时时间
      • 仅当在一次 read 中,没有收到请求头,才会算成超时
      • 如果在超时时间内,client 没发送任何东西,nginx 返回 HTTP 状态码 408
    • client_body_timeout(60s) -> 该指令设置请求体的读超时时间
      • 仅当在一次 read step 中,没有得到请求体就会设为超时,超时后 nginx 返回 HTTP 状态码 408
    • keepalive_timeout(75s) -> 服务器将会在这个时间后关闭连接
      • 这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了

Nginx疑难杂症汇总 - 请求客户端响应超时

Nginx疑难杂症汇总 - 请求客户端响应超时

server {
    sendfile                  on;
    client_header_timeout    240;
    client_body_timeout      240;
}

11. 平台网络上传限制

有时内部为了安全,会设置一些网络限制,比如上传文件的大小之类的!

  • [问题起因] 发现某个服务之前测试上传没有问题,但是后续其他人时候的时候却发现无法上传文件,排除之后提示如下报错信息:
# 报错信息
Request URL: http://localhost:8080/api/v1/xxx/60897/file
Request Method: POST
Status Code: 413 Request Entity Too Large
  • [解决方法] 根据经验是服务器限制了上传文件的大小,对应 1MB 以下的文件上传放行,大于则阻止。但是,排除服务本身的设置并没有相关的设置(1MB),所以排除是部署服务的网络平台进行了设置导致的。
# 修改配置
$ sudo vim /etc/nginx/nginx.conf
client_max_body_size  200m;

# 重启服务
$ sudo nginx -t
$ sudo nginx -s reload

12. 浏览器的缓存配置

有时内部为了安全,会设置一些网络限制,比如上传文件的大小之类的!

  • [问题起因] 我们已经会遇到,某个服务更新完成之后,通过浏览器并没有看到对应新功能或新特性。反馈之后,对应的解决方法就是三板斧(清理浏览器缓存)。如果只是内部使用的话,那到没有什么大问题,但是如果该系统是面向用户的话,就非常影响用户体验。

Nginx疑难杂症汇总 - 浏览器的缓存配置

  • [解决方法] 解决办法可以很简单,就是直接全部禁止缓存(在 Nginx 端配置),但是如果真的是这样的话,那缓存的意义何在?应该找到一种好的方式来解决这个问题,而不是暴力的一刀切。
    • 如果网页是通过 iframe 嵌入的话,有可能是因为 iframe 中有缓存导致,可以通过在 iframesrc url 后面加个时间戳,这样可以保证每次拿到的都是最新的。
    • 如果是直接访问的话,可以通过对浏览器 header 设置针对 html 的缓存策略(在 Nginx 端配置),只对收的 html 页面不设置使用缓存。正常 html 拿了新的,那 css/jshash 的路径都变了,会自动重新获取新的地址。
# 设置iframe缓存(加个时间戳)
<iframe src="https://www.escapelife.site?date=new Date()">
# 自定义的变量设置

map $sent_http_content_type $cache_control_type {
    default        "private";
    ~text/html     "no-store, no-transform";
}

add_header Cache-Control $cache_control_type;
# 官方给出的缓存策略设置
# http://nginx.org/en/docs/http/ngx_http_headers_module.html

map $sent_http_content_type $expires {
    default                    off;
    text/html                  epoch;
    text/css                   max;
    application/javascript     max;
    application/pdf            42d;
    ~image/                    max;
}

server {
    listen          80;
    server_name     your-domain;

    gzip            on;
    gzip_types      text/plain application/xml text/css application/javascript;

    location / {
        expires $expires;

        proxy_set_header Host               $host;
        proxy_set_header X-Real-IP          $remote_addr;
        proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto  $scheme;
        proxy_pass                          http://127.0.0.1:3000;
    }
}

13. 文件路径相关的匹配

很多细小的需要注意的点,在用到的时候起到了关键作用!

  • [问题起因] 在配置 Nginx 文件目录映射的时候(主要是前端代码、图片资源等),需要将用户请求的文件映射到对应目录下面。但是却发现,配置的代码并没有在合适的地方找到,就很诡异。

  • [解决方法] 最后发现是因为 rootalias 指令用法不对导致的,这里简单记录下相关指令,以备后续查看和使用。

【1】root path;

  • 作用域
    • httpserverlocationif in location
  • 含义解释
    • 设置Web资源路径,用于指定请求的根文档目录
  • 默认值
    • root html;
# /index.html将由/www/htdocs/index.html文件来响应
location / {
    root /www/htdocs;
}

# /images/b.html将由/data/w3/images/b.html文件来响应
location /images/ {
    root /data/w3;
}

【2】alias path;

  • 作用域
    • location
  • 含义解释
    • 只能用于location中,用于路径别名
# /i/top.gif将由/data/w3/images/top.gif文件来响应
location /i/ {
    alias /data/w3/images/;
}

# /images/b.html将由/data/w3/b.html文件来响应
location /images/ {
    alias /data/w3/;
}

# /images/b.html将由/data/w3/images/b.html文件来响应
location /images/ {
    alias /data/w3/images/;
}

location ~ ^/users/(.+\.(?:gif|jpe?g|png))$ {
    alias /data/w3/images/$1;
}

【3】index file ...;

  • 作用域
    • httpserverlocation
  • 含义解释
    • 定义默认页面,可参跟多个值,列表中的最后一个元素可以是一个带有绝对路径的文件
    • 模块ngx_http_index_module处理以斜线字符/结尾的请求
  • 默认值
  • index index.html;
location / {
    index index.$geo.html index.0.html /index.html;
}
# 需要注意的是,index文件会引发内部重定向,请求可能会被其它location处理
# 如下面这个例子中,请求“/”实际上将会在第二个location中作为“/index.html”被处理
location = / {
    index index.html;
}

location / {
    ...
}

【4】error_page code ... [=[response]] uri

  • 作用域
    • httpserverlocationif in location
  • 含义解释
    • 定义错误页面重定向
    • 当对于某个请求返回错误时,如果匹配上了error_page指令中设定的code,则重定向到新的URI
# 一般的设置方式
error_page 404             /404.html;
error_page 500 502 503 504 /50x.html;

# 可以使用=response语法改变响应状态码,响应的如果是404的话返回给用户200页面
error_page 404 =200 /empty.gif;

# 也可以使用本指令对错误处理进行重定向
error_page 403      http://example.com/forbidden.html;
error_page 404 =301 http://example.com/notfound.html;

# 如果内部跳转时无需改变URI,可以将错误处理转到一个命名路径
location / {
    error_page 404 = @fallback;
}

location @fallback {
    proxy_pass http://backend;
}

【5】try_files file ... uri;

  • 作用域
    • serverlocation
  • 含义解释
    • 还有一种表达方式:try_files file ... =code;
    • 自左至右尝试读取指定的路径,在第一次找到即停止并返回,如果所有路径均不存在,则返回最后一个URI
    • 文件路径是根据root指令和alias指令,将file参数拼接而成
    • 可以在名字尾部添加斜线以检查目录是否存在,比如$uri/
# $uri表示请求的路径
location /images/ {
    try_files $uri /images/default.gif;
}
location = /images/default.gif {
    expires 30s;
}
# /documents/a.html请求,先查找/docu/a.html,在查找/temp.html
location ~* ^/documents/(.*)$ {
    root /www/htdocs;
    try_files $uri /docu/$1 /temp.html;
}

# 从0.7.51版本开始,最后一个参数也可以是code
location / {
    try_files $uri $uri/index.html $uri.html =404;
}
# 下面是代理Mongrel的例子
location / {
    try_files /system/maintenance.html
              $uri $uri/index.html $uri.html
              @mongrel;
}

location @mongrel {
    proxy_pass http://mongrel;
}

# 下面是Drupal用FastCGI的例子
location / {
    try_files $uri $uri/ @drupal;
}

location ~ \.php$ {
    try_files $uri @drupal;
    fastcgi_pass ...;
    fastcgi_param SCRIPT_FILENAME /path/to$fastcgi_script_name;
    fastcgi_param SCRIPT_NAME     $fastcgi_script_name;
    fastcgi_param QUERY_STRING    $args;
    ... other fastcgi_param's
}

location @drupal {
    fastcgi_pass ...;
    fastcgi_param SCRIPT_FILENAME /path/to/index.php;
    fastcgi_param SCRIPT_NAME     /index.php;
    fastcgi_param QUERY_STRING    q=$uri&$args;
    ... other fastcgi_param's
}

14. 协议转换导致 Post 请求丢失

http 转发成 https 导致 post 请求变为 get 请求!

  • [问题起因] 越来越多的网站都从 http 迁移到了 https 协议。但迁移到 https 之前分发出去的链接都是 http 的,为了兼容以前的链接,就需要做一个 redirect,将 http 的请求 redirecthttps 中。

Nginx疑难杂症汇总

  • [解决方法] [方法一] 使用 301 Moved Permanentlyhttp 重定向到 https 中。
    • 对于普通的 GET 请求,这种方式是完全可以的。
server {
    listen       80;
    server_name  *.xxx.com;
    return 301   https://*.xxx.com;$request_uri;
}

server {
    listen       443 ssl default_server;
    server_name  *.xxx.com;;
    ......
}
  • [解决方法] [方法二] 所有 Post 请求通过 307 再转发就不会修改成 get 请求。
    • 但当请求是 POST 的时候,由于一些历史原因,一些浏览器和库收到 301/302 时会将原本的 POST 请求转为 GET 请求发送。此时 POSTbody 会丢失,导致请求出问题。这时就需要,使用 307 Temporary Redirect 来进行解决。
# nginx 307
# This status code is similar to 302 (Found), except that it does not allow changing the request method from POST to GET.

sever {
    listen 80;
    server_name *.xxx.com;

    if ($request_method ~* POST) {
        return 307 https://$server_name$request_uri;
    }
    rewrite ^(.*) https://$host$1;
}

server {
    listen       443 ssl default_server;
    server_name  *.xxx.com;;
    ......
}

15. 前置 F5 设备过滤请求

F5 可以通过配置或者默认(未识别)过滤服务请求!

  • [问题起因] 部署服务反馈页面访问均正常,但是反馈无法新建和其他特定操作,请求的页面显示如下所示。

Nginx疑难杂症汇总

  • [解决方法] 登录服务器查看对应日志发现,服务并没有收到任何的 POST 请求,判断请求就没有到该服务上,有可能被前置服务拦截或者过滤了。后来发现,服务是进行数据加密传输的,可能会因为格式等问题,会被 F5(不认识)拦截。放开加密配置,服务即正常可用。

16. 配置浏览器版本选择支持

使用 map 指令配置服务只支持>=93 版本的 chrome 浏览器

  • [问题起因] 典型的示例 - 配置服务只支持 >=93 版本的 chrome 浏览器

    • 获取变量 A 的内容进行匹配
    • 如果匹配到固定浏览器版本则将其置为后面的值
    • 如果没有匹配到的话,默认值为 0
  • [解决方法] map 指令简单使用示例。

#   A(原变量)         B(自定义变量)
map $http_user_agent $unsupport_browser {
    default                                 1;
    "~Chrome/[0-8][0-9]?\."                 1;
    "~Chrome/9[0-2]?\."                     1;
    "~Chrome"                               0;
    "~Chromium/[0-6][0-9]?\."               1;
    "~Chromium/7[0-5]?\."                   1;
    "~Chromium"                             0;
}

server {
    listen       8080;
    server_name  _;

    if ($unsupport_browser = 1) {
        rewrite .*\.html /unsupported_browser.html last;
    }

    ......
}

17. Nginx 日志写错文件

具体问题值得深究一下

通过 logrotate 工具触发 Nginx 的日志轮转,因为是 Ubuntu 系统,所以对应的配置如下所示:

  1. 创建 /etc/logrotate.d/nginx 的文件,用于指定 Nginx 日志轮转。
# ubuntu
$ sudo vim /etc/logrotate.d/nginx
  1. 添加配置文件内容。
    1. *.log -> 指定要轮转的 Nginx 日志文件
    2. daily -> 设置日志轮转的时间间隔为一天(weekly/monthly/yearly)
    3. missingok -> 如果日志文件不存在则忽略它并继续运行
    4. rotate 60 -> 保留 60 个轮转后的日志文件
    5. delaycompress -> 在轮转压缩之前保持一个旧的未压缩的文件
    6. notifempty -> 在日志文件为空时不轮转该文件
    7. create -> 创建新的日志文件使用的访问权限和用户组
    8. sharedscripts -> 在运行指定命令之前和之后只运行一次 postrotate 命令
    9. prerotate -> 执行轮命令之前的预处理操作
    10. postrotate -> 重新打开 Nginx 日志文件进行日志轮转
# 配置文件格式
/var/log/nginx/*.log {
    daily
    missingok
    rotate 60
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    prerotate
        if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
            run-parts /etc/logrotate.d/httpd-prerotate; \
        fi \
    endscript
    postrotate
        invoke-rc.d nginx rotate >/dev/null 2>&1
    endscript
}

# 手动强制轮转
$ sudo invoke-rc.d nginx rotate

我们会在其他系统上面看到,postrotate 字段可能是 kill -USR1 的形式,两个命令实现的功能是相同的,都用于手动触发 Nginx 日志轮转。不同的是,前者是使用 Debian 工具来执行轮转,而后者是直接使用 Linux shell 命令进行操作。

  1. invoke-rc.d nginx rotate
    1. invoke-rc.d -> 用于执行 Debian 系统服务控制脚本的工具
    2. nginx -> 要执行操作的服务名称
    3. rotate -> 轮转操作的指令参数
    4. >/dev/null 2>&1 -> 不输出任何日志内容
  2. kill -USR1 $(cat /var/run/nginx.pid)
    1. [ -f /var/run/nginx.pid ] -> 检查 Nginx 进程是否在运行
    2. kill -USR1 -> 向 Nginx 进程发送 USR1 信号以触发日志轮转操作
    3. USR1 -> 是一个自定义的信号,表示 Nginx 收到这个就会触发轮转

18. Nginx 配置 WS 请求

分享一个把 ws 和 http 放到一个 location 的方法

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen   80;

    location / {
        proxy_http_version 1.1;
        proxy_pass_header Server;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Scheme $http_scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_pass http://127.0.0.1:8000;
    }

}

文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !
  目录