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进程都被关闭掉。
1. 提示 Cookie 长度超限
报错信息:Request Request Header Or Cookie Too Large
- [问题起因] 服务新建之后,其他用户登录网页的时候,页面提示 400: Bad Request 错误,大致如下所示:
- [解决方法] 后来发现,是因为多个服务公用同一个域名导致在该域名下面存储的 Cookie 太多导致的,可以设置如下两个参数来缓解该问题的出现。
- 设置读取客户端请求头的缓冲区大小。对于大多数请求,1K 字节的缓冲区就足够了。但是,如果一个请求包含很长的 cookie,或者来自 WAP 客户端,那么它可能不适合 1K。如果请求行或请求头字段不适合这个缓冲区,那么将分配更大的缓冲区,由 large_client_header_buffers 指令配置。
- 设置用于读取大型的客户端请求头的缓冲区的最大数量和大小。请求行不能超过一个缓冲区的大小,否则会向客户端返回 414(Request-URI Too Large)错误。请求头字段也不能超过一个缓冲区的大小,否则会将 400(Bad Request)错误返回给客户端,缓冲区仅按需分配。默认情况下,缓冲区大小等于 8K 字节。 如果在请求处理结束后将连接转换为保持活动状态,则会释放这些缓冲区。
- 后来网上找到了,对 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 协议自动管理文件而设计的,该模块处理 HTTP 和 WebDAV 的请求方法 PUT、DELETE、MKCOL、COPY 和 MOVE。默认情况下,该模块不会构建,需要使用编译参数 -–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 支持 SSL 需要使用 --http_ssl_module 这个模块,如果使用的版本没有安装的话,则需要手动安装或者使用新版本。我们这里使用的 Docker 容器镜像,该版本默认是支持的。
# 手动编译安装SSL支持 $ ./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-openssl-opt="enable-tlsext" ... # 编译并安装 $ make && make install # 查看版本是否支持SSL功能 [email protected]:/# 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 这个自定义的错误码,我们刚好利用了这点来完成配置的。
- http 和 https 是 tcp 的上层协议,当 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.conf 配置文件中 location 加入如下参数,即可。
# 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 参数来解决。
- [解决方法] 之前介绍过什么是跨越问题,可以参考之前写的博文(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,所以服务器需要允许该方法
- [1] Access-Control-Allow-Origin
# 可以加到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
- 复杂请求
- 那种对服务器有特殊要求的请求
- 比如请求方法是 PUT、DELETE 或 OPTIONS 的等
- 或者 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
- [解决方法] 需要在 Nginx 配置里面新增如下 header 信息。
# 可以通过 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 负载均衡的排坑和思考
- [问题起因] Nginx 在 1.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 会关闭连接
- proxy_connect_timeout(60s) -> 后端服务器连接的超时时间 发起握手等候响应超时时间
# 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 "-"
- [解决方法] 很奇怪,并没有发现关于 60 的 Nginx 设置,所以必定是某些默认参数的默认值。有根据返回码 408 判断初始因为 client_header_timeout 和 client_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) -> 服务器将会在这个时间后关闭连接
- 这个头能够让一些浏览器主动关闭连接,这样服务器就不必要去关闭连接了
- client_header_timeout(60s) -> 指定等待 client 发送一个请求头的超时时间
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 端配置),但是如果真的是这样的话,那缓存的意义何在?应该找到一种好的方式来解决这个问题,而不是暴力的一刀切。
- 如果网页是通过 iframe 嵌入的话,有可能是因为 iframe 中有缓存导致,可以通过在 iframe 的 src url 后面加个时间戳,这样可以保证每次拿到的都是最新的。
- 如果是直接访问的话,可以通过对浏览器 header 设置针对 html 的缓存策略(在 Nginx 端配置),只对收的 html 页面不设置使用缓存。正常 html 拿了新的,那 css/js 的 hash 的路径都变了,会自动重新获取新的地址。
# 设置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; } }