業(yè)務(wù)方反應(yīng)調(diào)用接口超時(shí),但是在服務(wù)端監(jiān)控并沒有看到5xx異常, 于是我們模擬一下請求超時(shí)時(shí)發(fā)生了什么?
1.openresty模擬長耗時(shí)服務(wù)端
延遲5s響應(yīng)
| error_log logs/error.log; |
|
|
| http { |
| server { |
| listen 80; |
| charset utf-8; |
|
|
| location /reqtimeout { |
| default_type text/html; |
| content_by_lua ' |
| local start = os.clock() |
| while os.clock() - start < 5 do end |
| ngx.say("delay success!!") |
| '; |
| } |
| } |
| } |
2.golang和.net默認(rèn)的httpclient對外都只有一個(gè)timeout設(shè)置
用于控制請求、響應(yīng)的整體時(shí)間
.net httpclient 默認(rèn)timeout= 100s;
golang net/http 無默認(rèn)值設(shè)置,強(qiáng)烈推薦設(shè)置timeout,以避免服務(wù)端慢響應(yīng)拖垮客戶端。
| static void Main(string[] args) |
| { |
| Console.WriteLine("Hello, World!"); |
| var a = HttpReqTimeout(); |
| Console.WriteLine(a.Result); |
| } |
|
|
| static async Task<string> HttpReqTimeout() |
| { |
| var handler = new SocketsHttpHandler |
| { |
| PooledConnectionLifetime = TimeSpan.FromMinutes(1) |
| }; |
| using (var hc = new HttpClient(handler)) |
| { |
| hc.Timeout = TimeSpan.FromSeconds(3); |
| return await hc.GetStringAsync("http://localhost/reqtimeout"); |
| } |
| } |
dotnet run ./ 顯示客戶端請求3s超時(shí),爆出異常
| Hello, World! |
| Unhandled exception. System.AggregateException: One or more errors occurred. (A task was canceled.) |
| |
| at System.Threading.Tasks.Task.GetExceptions(Boolean includeTaskCanceledExceptions) |
| at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) |
| at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification) |
| at ConsoleApp1.Program.Main(String[] args) in /Users/admin/RiderProjects/TestHttpClientFactory/ConsoleApp1/Program.cs:line 9 |
| |
|
|
| |
| at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) |
| at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification) |
| at ConsoleApp1.Program.Main(String[] args) in /Users/admin/RiderProjects/TestHttpClientFactory/ConsoleApp1/Program.cs:line 9 |
openresty服務(wù)端日志,顯示執(zhí)行完成,返回200ok:
| 127.0.0.1 - - [04/Dec/2024:15:17:50 +0800] "GET /reqtimeout HTTP/1.1" 200 28 "-" "-" |
這也正是對應(yīng)上了業(yè)務(wù)方的反饋和服務(wù)端的監(jiān)控現(xiàn)象(無5xx報(bào)錯(cuò))。
3.wireshark抓包看實(shí)質(zhì)
tcp.port == 80 && ip.addr ==127.0.0.1 && ip.dst ==127.0.0.1
從tcp抓包過程看,分為三階段:
1>. httpclient請求, 正常tcp三次握手+ 請求確認(rèn);
2>. 客戶端3s之后超時(shí), 客戶端發(fā)送FIN+ACK 數(shù)據(jù)包(客戶端標(biāo)記連接已經(jīng)被關(guān)閉), 服務(wù)端確認(rèn)收到客戶端的FIN包;
3>. 服務(wù)端5s嘗試響應(yīng)給客戶端,最終會檢測到客戶端已經(jīng)關(guān)閉而釋放資源。
也就是說客戶端請求超時(shí),只會影響客戶端, 服務(wù)端還會繼續(xù)處理并響應(yīng), 這也是我們在服務(wù)端監(jiān)控上看不到5xx報(bào)錯(cuò)的原因,可以通過在服務(wù)端設(shè)置: request_time between (-xx, 3s) 監(jiān)測請求耗時(shí)占比。
正常的請求/響應(yīng)讀者可以參考下圖:
4. 服務(wù)端能感知到客戶端請求超時(shí)嗎 ?
客戶端請求超時(shí), 默認(rèn)情況下服務(wù)端都是繼續(xù)執(zhí)行之后響應(yīng);
服務(wù)器是具備感知客戶端請求取消的能力的。
C# 是通過CancellationToken
,感知客戶端取消,之后服務(wù)端可以做一些邏輯,比如記錄客戶端請求超時(shí)(常規(guī)實(shí)踐是記錄408響應(yīng)碼)
| |
| var cancellationToken = httpContext.RequestAborted; |
| await LongLoop(cancellationToken); |
|
|
|
|
| public Task LongLoop(CancellationToken token) |
| { |
| while(true) |
| { |
| if (token.IsCancellationRequested == true) |
| { |
| break; |
| } |
| |
| } |
|
|
| return Task.CompletedTask; |
| } |
golang 是通過request.Context獲取客戶端取消信號,內(nèi)核類似于C#
| func getHello(w http.ResponseWriter, r *http.Request) { |
| ctx := r.Context() |
| select { |
| case <-ctx.Done(): |
| |
| err := ctx.Err() |
| fmt.Println("Request cancelled:", err) |
| return |
| case <-time.After(5 * time.Second): |
| io.WriteString(w, "Hello, HTTP!\n") |
| return |
| } |
| } |
本文記錄了httpclient客戶端超時(shí)在雙端的現(xiàn)象, 服務(wù)端會繼續(xù)響應(yīng),在服務(wù)端可能檢測不到客戶端認(rèn)定的報(bào)錯(cuò), 經(jīng)驗(yàn)無他,唯手熟爾。
轉(zhuǎn)自https://www.cnblogs.com/JulianHuang/p/18586745
該文章在 2024/12/5 10:13:27 編輯過