我相信有些人在面試運維類崗位的時候會碰到對方問關於這方面的問題,我這里通過幾個實驗來復現這個情況,並做出相關分析,我希望大家看完后針對這種問題能有一個清晰思路。
服務器 | 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你去看看錯誤日志就知道了,就可以做一個歸類。面對這種問題你先不要想后端是怎么回事,大家的后端千差萬別。不要被別人代跑。