最近线上 nginx 遇到了一些较难排查的 502 和 504 错误,顺便了解了一下 nginx 的相关配置。我发现网上很多介绍 nginx 超时配置只是列了这几个配置的含义和数值,并没有解释什么原因会触发哪个配置。因此趁这个机会演示一下,如何让 nginx 符合预期正确出现 502 和 504。
502 和 504 的解释
在 http status 的 定义 中:
- 502 Bad Gateway: The server was acting as a gateway or proxy and received an invalid response from the upstream server.
- 504: he server was acting as a gateway or proxy and did not receive a timely response from the upstream server.
502 的错误原因是 Bad Gateway,一般是由于上游服务的故障引起的;而 504 则是 nginx 访问上游服务超时,二者完全是两个意思。但在某些情况下,上游服务的超时(触发 tcp reset)也可能引发 502,我们会在之后详述。
演示环境
你需要 3 个逻辑组件:nginx 服务器,php-fpm,client 访问客户端。3 个组件可以在同一台机器中,我用的是 docker 来配置 PHP 和 nginx 环境,在宿主机上访问。如果你很熟悉这 3 个组件,这部分可以跳过。用 docker 来做各种测试和实验非常方便,这里就不展开了。docker-compose 的配置参考了这篇文章。我的 docker composer 文件如下:
version: '3'
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./code:/code
- ./nginx/site.conf:/etc/nginx/conf.d/site.conf
depends_on:
- php
php:
image: php:7.1-fpm-alpine
volumes:
- ./code:/code
- ./php/php-fpm.conf:/usr/local/etc/php-fpm.conf
使用的镜像都是基于 alpine 制作的,非常小巧:
REPOSITORY TAG SIZE
php 7.1-fpm-alpin 69.5MB
nginx alpine 18.6MB
nginx 的配置:
server {
index index.php index.html;
server_name php-docker.local;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /code;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_connect_timeout 5s;
fastcgi_read_timeout 8s;
fastcgi_send_timeout 10s;
}
}
php-fpm 的配置
[global]
include=etc/php-fpm.d/*.conf
request_terminate_timeout=3s
代码放在 github。
关键参数
在这个演示中,PHP 的关键参数有两个,一个是 PHP 脚本的 max_execution_time,这个配置在php.ini中;另一个是 php-fpm 的 request_terminate_timeout,在php-fpm.conf中。当以 php-fpm 提供服务时,request_terminate_timeout 设置会覆盖 max_execution_time 的设置,因此我们这里只测试 request_terminate_timeout。
request_terminate_timeout 的意思是 php-fpm 接受的请求的超时时间,超过这个时间 php-fpm 会 kill 掉执行脚本的 worker 进程。
nginx的关键参数是 fastcgi 相关的 timeout,即:fastcgi_connect_timeout,fastcgi_read_timeout,fastcgi_send_timeout。
这几个 nginx 参数的主语都是 nginx,所以 fastcgi_connect_timeout 的意思是 nginx 连接到 fastcgi 的超时时间,fastcgi_read_timeout 是 nginx 读取 fastcgi 的内容的超时时间,fastcgi_send_timeout 是 nginx 发送内容到 fastcgi 的超时时间。
演示过程
首先启动 nginx 和 PHP:
docker-compose up
在 code 文件夹下添加一个 index.php 文件:
<?php
sleep(70);
echo 'hello world';
上游服务主动 reset
访问 php-docker.local:8080/index.php,报错 502 bad gateway。而且是在 3s 之后报的错,说明触发了 request_terminate_timeout 设置,php-fpm 关闭了连接。
通过观察 ps aux | grep php 可以发现,php-fpm 是通过杀掉超时的进程来解决进程超时问题的(pid 每次有一个会变化,说明一个进程杀掉了,并启动了另一个进程。这和 php-fpm 的进程池设定有关,你的设定未必会重新启动一个新的进程)。
/var/www/html # ps aux | grep php
1 root 0:00 php-fpm: master process (/usr/local/etc/php-fpm.conf)
6 www-data 0:00 php-fpm: pool www
7 www-data 0:00 php-fpm: pool www
/var/www/html # ps aux | grep php
1 root 0:00 php-fpm: master process (/usr/local/etc/php-fpm.conf)
7 www-data 0:00 php-fpm: pool www
17 www-data 0:00 php-fpm: pool www
/var/www/html # ps aux | grep php
1 root 0:00 php-fpm: master process (/usr/local/etc/php-fpm.conf)
17 www-data 0:00 php-fpm: pool www
20 www-data 0:00 php-fpm: pool www
在这种情况下,nginx 日志中的错误是:
recv() failed (104: Connection reset by peer) while reading response header from upstream
即连接被服务端(PHP)reset 了,也就很好理解了。
注意,在这种情况下,php-fpm 的日志中也会记录的:
php_1 | [18-Jul-2018 16:33:42] WARNING: [pool www] child 5, script '/code/index.php' (request: "GET /index.php") execution timed out (3.040130 sec), terminating
php_1 | [18-Jul-2018 16:33:42] WARNING: [pool www] child 5 exited on signal 15 (SIGTERM) after 30.035736 seconds from start
php_1 | [18-Jul-2018 16:33:42] NOTICE: [pool www] child 8 started
这也是可以发现问题的一个地方。
nginx 读取上游服务超时
删掉 request_terminate_timeout 配置,重启应用:
docker-compose down && docker-compose up
此时,PHP 脚本将要执行 70s,肯定超过 nginx 设置的超时时间,get 一下发现确实如此,8s 之后抛出 504 Gateway Time-out 错误,nginx 日志是:
upstream timed out (110: Operation timed out) while reading response header from upstream
说明触发了 fastcgi_read_timeout 设置。
关闭上游服务
关掉 PHP 服务:
docker-composer stop php
PHP 服务停掉之后第一次访问,得到 504 错误,错误是:
upstream timed out (110: Operation timed out) while connecting to upstream
超时时间为 fastcgi_connect_timeout 的设置。说明这个时候 tcp 连接还在,但是尝试连接的时候失败了。
再次访问,得到 502 错误,错误是:
connect() failed (113: Host is unreachable) while connecting to upstream
502 的原因很容易理解,上游服务挂了,同时因为之前访问的时候发现连接不上就把连接断掉了,再次连接的时候便无法找到 host 了。
我曾怀疑第一次访问 504 是由于 keepalive。但我停掉 PHP 之后隔了好久才发第一个请求,仍然是这个结果。
如果将 nginx fastcgi_pass 配置为 127.0.0.1:9000(本地没有这个端口),则马上就会抛出 502 错误,错误为:
connect() failed (111: Connection refused) while connecting to upstream
登入 nginx 服务,使用 tcpdump 监控 9000 上的通信:
tcpdump -i eth0 -nnA tcp port 9000
# 如果你的 PHP 在本地,eth0 应该改成 lo
我们发现,当 PHP 关闭之后第一次访问,nginx 会尝试向 PHP 发起若干次 TCP SYN 请求,但 PHP 显然不会响应,这个时候 nginx 就返回了 504。第二次访问的时候 nginx 根本不会发起任何请求,直接 502 了1。如果我们这个时候执行nginx -t会发现,nginx 已经认为配置文件有问题了:nginx: configuration file /etc/nginx/nginx.conf test failed。
换一种配置
这篇文章 提到,我们之前的 nginx 配置并不合理2,我们重新设置 nginx:
server {
index index.php index.html;
server_name php-docker.local;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /code;
resolver 127.0.0.11; # here
location ~ \.php$ {
set $upstream php:9000; # here
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass $upstream; # here
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_connect_timeout 5s;
fastcgi_read_timeout 8s;
fastcgi_send_timeout 10s;
}
}
其中 127.0.0.11 是 docker 的内网 dns resolver。该配置动态指定 fastcgi pass,所以 nginx 不会检查该连接能否建立起来。
按照这个配置启动,先访问 index.php 建立连接,然后关闭 PHP,表现为:
在 keepalive 期间,抛出 504 错误,超时时间为 fastcgi_connect_timeout,错误是:
upstream timed out (110: Operation timed out) while connecting to upstream
keepalive 断线之后,抛出 502 错误,超时时间不定,错误是:
connect() failed (113: Host is unreachable) while connecting to upstream
按照这篇文章所说,这种配置 nginx 不会认为有问题,执行nginx -t确实如此。在 一段时间 内,每次请求 nginx 都会向 upstream 发送 SYN,这段时间的状态码都是 504,之后再访问就不再发 TCP 包,状态码也变成 502。
其他
除此之外,PHP 脚本还有一个超时时间的设置:max_execution_time。它是限制 PHP 脚本的执行时间,但这个时间不会计算系统调用(比如 sleep,io,等)。因为该原因导致 PHP 杀掉进程时,会抛出 fatal error,而 php-fpm 不会有 fatal error。
这里实验使用的是 PHP 的 fastcgi 工作方式,如果是 nginx 通过代理的方式连接上游服务的话,fastcgi_connect_timeout,fastcgi_read_timeout,fastcgi_send_timeout 都需要替换成对应的 proxy_connect_timeout,proxy_read_timeout,proxy_send_timeout。
结论
504 的原因比较简单,一般都是上游服务的执行时间超过了 nginx 的等待时间,这种情况是由于上游服务的业务太过耗时导致的,或者连接到上游服务器超时。从上面的实验来看,后者的原因比较难以追踪,因为这种情况下连接是存在的,但是却连不上,好在这种 504 一般都会在一段时间后转为 502。
502 的原因是由于上游服务器的故障,比如停机,进程被杀死,上游服务 reset 了连接,进程僵死等各种原因。在 nginx 的日志中我们能够发现 502 错误的具体原因,分别为:104: Connection reset by peer,113: Host is unreachable,111: Connection refused。
有一些细节上的差别和 nginx 的工作原理有关,这部分尚未深挖。