关于Nginx499、502和504的分析

我相信有些人在面试运维类岗位的时候会碰到对方问关于这方面的问题,我这里通过几个实验来复现这个情况,并做出相关分析,我希望大家看完后针对这种问题能有一个清晰思路。

服务器 IP
Nginx 192.168.10.40
后端Web 192.168.10.50

我们使用一个flask制作一个小的后端程序,程序里加了sleep,为的是有时间你可以进行其他操作,比如终止进程:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask
import random
import time

app = Flask(__name__)

@app.route('/')
def hello_world():
    time.sleep(10)
    html = "Hello World!"
    return html


if __name__ == '__main__':
    app.run(host="0.0.0.0", port="5555")

下面是Nginx配置:

server {
    listen       80;
    server_name  www.test.com;

    #charset koi8-r;
    access_log  /var/log/nginx/www.test.com/access.log  detailed;
    error_log  /var/log/nginx/www.test.com/error.log  error;

    location / {
        proxy_pass   http://192.168.10.50:5555;
        proxy_set_header host www.test.com;
        proxy_connect_timeout 60s;
        proxy_read_timeout 60s;
        proxy_send_timeout 60s;
        # proxy_ignore_client_abort on;
    }

    #error_page  404              /404.html;

    #要保证下面的root目录中有50x.html,否则你只会收到404的代码
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

Nginx使用的日志格式

log_format  detailed  '$remote_addr - $remote_user [$time_local] '
                      '$server_protocol $scheme $ssl_protocol $http_host $request_method "$content_type" "$request" '
                      '$status $body_bytes_sent "$http_referer" $request_time '
                      '"$http_user_agent" "$http_x_forwarded_for" ' 
                      '[$upstream_addr] [$upstream_response_time] [$upstream_status] [$upstream_response_length]';

实验1:状态码499

其实499很好模拟,运行命令启动flask:

python3 ./myweb.py

确保程序已经开始监听

打开浏览器访问www.test.com由于后端10秒返回(Python代码中有一个sleep函数)数据,所以当你访问这个URL之后,马上按终止或者关闭浏览器。

这时候Nginx的日志就是499

这个就是客户端在服务器还没有返回数据之前就终止了,也就是不再等待了。对于浏览器来说可能就是用户主动终止了。另外还有一种情况就是远程调用,对端设置的超时时长如果是5秒,那么由于我们程序设置的10秒后返回,对端到达5秒后会中断连接,那么我们的Nginx上就会产生499,其实这个和终止浏览器是一个意思。

proxy_ignore_client_abort on另外网上有说加上这个参数就没有499了,其实这个参数是忽略了客户端终止连接的这种情况,所以它在日志中就没有499了。

实验2:状态码502(Bad Gateway)

情况一:

使用上面的配置不做任何改变。然后运行命令:

python3 ./myweb.py

打开浏览器访问www.test.com,由于处理程序我设置的是10秒,所以我们有时间做其他操作,当请求进来时程序所在终端会显示如下信息:

在还没有返回的时候你直接按下CTRL+C来终止Flask程序,Nginx访问日志马上就是502错误。

查看Nginx访问日志和错误日志:

下面是抓包情况,最后一个箭头,我终止了flask程序,发送了fin给对方,所以Nginx的错误日志显示permaturely closed connection。

这种情况下后端处理程序因为某些原因被终止,导致Nginx到后端程序这条socket连接断开那么就会报这个错误。但是从抓包可以看到四次断开是正常完成的,也就是说你按CTRL+C或者kill -9 PID 这个程序,它内部还是会正常断开连接,对于Nginx来说相对于在等待数据返回的时候却收到了一个FIN。

情况二:

修改一下Nginx的配置:

server {
    ......

    location / {
        proxy_pass   http://192.168.10.50:5556; # 修改一个不存在的端口
        proxy_connect_timeout 60s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;
    }
    ......
}

我们这里把后端指向了一个后端服务器不存在的端口,然后通过浏览器访问,你会马上得到502状态码,错误日志信息为“connect() failed (111: Connection refused)”。

情况三:

这种情况不好模拟,这时候就不能用之前的flask框架的程序了,需要自己写一个套接字程序,代码如下:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
import os, time, sys
import signal


def echoStr(connFd, sleep_time):
    print("新连接:", connFd.getpeername())
    sleep_time = sleep_time
    print("睡眠 %s 秒后返回数据" % sleep_time)
    time.sleep(sleep_time)
    try:
        with open("./index.html", "r") as f1:
            html_data = f1.read()
        connFd.send(html_data.encode(encoding="utf-8"))
    except Exception as err:
        print(err)


def main():
    sockFd = socket.socket()
    sockFd.bind(("", 5555))
    sockFd.listen(5)
    signal.signal(signal.SIGCHLD, sigChld)
    print("等待客户端连接......")
    while True:
        connFd, remAddr = sockFd.accept()
        try:
            # 10 是传递进去的睡眠时间
            echoStr(connFd, 10)
            connFd.close()
        except Exception as err:
            print(err)


if __name__ == '__main__':
    main()

启动程序python3 ./mysocket.py

然后使用浏览器访问,依然是通过Nginx做代理,Nginx无需修改,在10秒内CTRL+C来终止程序,这时候相当于是向套接字发送RES:

查看Nginx日志:

看看抓包情况

前面部分都和情况一相同,但是你注意最后一个箭头,它是[R.]而且方向是从50到40方向,也就是后端给Nginx发的,这就是我在程序收到请求以后按了CTRL+C终止了程序,这就等于向双方建立的套接字发送了RST,当Nginx尝试去读一个已经收到RST的套接字的时候就会得到ECONNRESET错误,当然如果是写操作也会得到这个ECONNRESET。

我这里使用CTRL+C和情况一中不同,同样的操作但是对于套接字的影响不同,这是因为你在套接字编程的时候一定需要考虑进程崩溃怎么办、网络临时抖动怎么办,flask框架里面的web服务是有这些机制的,当它收到终止操作信号的时候会做哪些后续处理,我们看到它是通过正常机制来完成了四次端口,而反观我自己写的这个简易Socket程序没有任何其他考虑,收到终止信号后直接就发送了RST。

小结

针对Nginx在什么情况下产生502我们找到3种:

  • 代理到一个不存在的端口,记住这里是端口,不是地址。 在error_log中显示 connect() failed (111: Connection refused) 访问日志出现502,

  • 后端终止了程序,但正常完成了四次断开,error_log中显示 permaturely closed connection。

  • 后端终止了程序,但没有完成四次端开而是发送了RST,error_log中显示 recv() failed (104: Connection reset by peer)

实验3:状态码504(Gateway Timeout)

情况一:

这个就是Nginx超时导致的,我们修改一下Nginx配置:

server {
    ......
    location / {
        proxy_pass   http://192.168.10.50:5555;
        proxy_set_header host www.test.com;
        proxy_connect_timeout 60s;
        proxy_read_timeout 5s;  # 修改为5秒
        proxy_send_timeout 60s;
        # proxy_ignore_client_abort on;
    }
    ......
}

我们把proxy_read_timeout改成了5秒,这样Nginx等待后端回传数据只能5秒,因为我们后端是10秒后响应。然后重新reload配置。之后启动我们的Flask程序进行访问测试:

通过浏览器访问,并观察Nginx日志

这个原因就是Nginx的proxy_read_timeout超时时长小于,后端处理时长。但其根本原因是当Nginx的read或者send到达超时时长后端还没有返回响应那么Nginx就会主动断开和后端的,也就是主动发送FIN,从而产生了504。如下图:

我这个例子是把read改为5秒,造成读超时,其实对于proxy_send_timeout也一样,如果写超时也是也是504。

情况二:

这个就需要修改的地方会多一些,我们一点一点来

修改Nginx主机的/etc/hosts文件,添加一条,如下:

192.168.10.50   www.myweb.com

修改Nginx的配置,如下:

server {
    ......
    location / {
        proxy_pass   http://www.myweb.com:5555; # 改成域名
        proxy_connect_timeout 60s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;  # 还原为60秒
    }
    ......
}

启动后端程序,然后用浏览器访问看看可以正常访问页面,如果愿意还可以修改Python程序中的sleep时间,改成0,我们这个测试对于后端响应时间没有要求。

这时候你把/etc/hosts中添加的那个解析记录删除,你再次访问,会发现还可以访问到页面内容,而且Nginx日志里都是正常的。

下面则是最关键的,你把运行flask程序的主机也就是192.168.10.50这个主机IP修改为60(总之就是和之前不一样):

然后再次用浏览器访问,你觉得会发生什么?肯定还是访问不到,看看日志:

你可以看到错误日志和情况一是一样的,但是场景不同了。有人说我把HOST注释了,其实你就是把解析记录修改为新的IP地址其结果也是一样的。这是因为Nginx自己有解析记录的缓存,由于我们在proxy_pass中使用的是域名,这种场景在实际上是存在的,面对这种场景你有2个办法:

  • reload nginx,这种办法对于域名解析变动非常不频繁的情况

  • 使用变量,这种办法对于域名解析比较频繁的情况

如何使用变量呢,如下所示:

server {
    listen       80;
    server_name  www.test.com;
    # 新增
    resolver 127.0.0.1:5353 [::1]:5353 valid=30s;

    access_log  /var/log/nginx/www.test.com/access.log  detailed;
    error_log  /var/log/nginx/www.test.com/error.log  error;

    location / {
        # 新增
        set $backendname http://www.myweb.com;
        # proxy_pass   http://www.myweb.com:5555;
        proxy_pass   $backendname:5555;
        proxy_connect_timeout 60s;
        proxy_send_timeout    60s;
        proxy_read_timeout    60s;
    }
    ......
}

先说一下变量名引用和直接代理有什么不同:

  • proxy_pass http://www.myweb.com:5555 这种方式将tengine在启动的时候使用系统的域名服务器把www.myweb.com解析成ip。 如果系统的域名服务器不能解析出ip,tengine将不能启动。

  • 变量方式,tengine在运行的时候动态解析域名,也就是在访问的时候才进行解析,使用Tengine的 reslover 指令配置的域名服务器。 tengine会保存域名解析的TTL时间,在TTL时间内直接使用这个ip,过期之后重新域名解析。

两者的利弊: 第一种不会感知到域名的ip变动,启动之后就不依赖域名服务器; 第二种配置会感知到域名的ip变动,但运行时依赖域名服务器,如果域名服务器挂了会返回502,而并不会尝试使用本机配置的其他DNS服务器来解析。

注意:通过变量的形式配置如果你不resolver的话,Nginx是不会用本地的hosts或者resolv文件去解析的。

我这里就在我Nginx这个服务器上安装了一个dnsmasq,通过yum安装yum -y install dnsmasq,编辑配置文件/etc/dnsmasq.conf

port=5353
listen-address=127.0.0.1
resolv-file=/etc/resolv.conf
addn-hosts=/etc/hosts

最关键的就是最后一句,因为对www.myweb.com的解析就在本地hosts文件中。配置好后启动服务。然后再次访问页面就可以了。

小结

针对504的我们模拟了2种情况,你从error日志里看其实根本原因都一样就是连接超时,只不多第一种是属于主动超时,第二种属于被动超时,但不管怎么样都是在一定时间内得不到响应,这种响应可以是连接层面的响应也可以是后端数据返回的响应,但重点不在响应本身而是在于一定时间

总结

499无需多说,这个在Nginx代码的定义就是NGX_HTTP_CLIENT_CLOSED_REQUEST,客户端关闭请求,这就是Nginx产生499的原因,这句定义你可以解读出2个信息,第一客户端和Nginx已经建立了连接,第二在Nginx还没有完成响应动作的时候客户端关闭了连接,至于客户端为什么关闭请求那是另外一回事,这种原因可就非常多了。

502:Bad Gateway,意为网关错误,这就是说你访问的Web服务器充当的是一个网关或者代理角色(比如我们例子中的Nginx反代),当Nginx从上游服务器收到无效响应的时候会产生该状态码。最后一句“从上游服务器收到无效响应”,可以看出一切的首先是得先能找到上游服务器,如果都找不到服务器那么响应的有效与否就无从谈起,其次是无效响应。在我们的例子中,Nginx可以通过IP找到后端,在后端进行响应的时候后端发生问题,这种情况就包括端口无效或者端口有效建立了连接但是后端进程处理时候发生问题导致中断了连接。在这些情况下Nginx收到的响应都不是后端应用发给它的,都是后端服务器发给的,进一步说就是后端服务器内核中的TCP/IP协议栈发给Nginx的。所以它的关注点不在时间维度上而是无效响应这个点。

504:Gateway Timeout,意为网关超时,扮演网关和代理的服务器无法在规定时间内得到有效响应。这里你就可以看出区别,502是无效响应,504是有效响应。但504的定义中有一个时间概念,所以它关注的点是时间,如果在规定时间内得到响应,无论是有效的还是无效的,那么其状态码一定不是504,如果是有效数据则状态码是2XX、3XX或者是4XX,如果是无效数据那么就是502。

其实Nginx为什么产生499、502或者504你去看看错误日志就知道了,就可以做一个归类。面对这种问题你先不要想后端是怎么回事,大家的后端千差万别。不要被别人代跑。

HTTP状态码查询