脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - 排查 Go 开发的 HttpClient 读取 Body 超时

排查 Go 开发的 HttpClient 读取 Body 超时

2022-01-11 01:31精益码农有态度的马甲 Golang

本人负责的主备集群,发出的 HttpClient 请求有 30%概率超时, 报context deadline exceeded (Client.Timeout or context cancellation while reading body) 异常

排查 Go 开发的 HttpClient 读取 Body 超时

记一次go httpclient [读取响应Body超时]的排查过程。

今年度解锁的第一个技能。

1故障现场

排查 Go 开发的 HttpClient 读取 Body 超时

本人负责的主备集群,发出的 HttpClient 请求有 30%概率超时, 报context deadline exceeded (Client.Timeout or context cancellation while reading body) 异常

Kibana 显示 Nginx 处理请求的耗时request_time在[5s-1min]区间波动, upstream_response_time在 2s 级别。

所以我们认定是 Nginx 向客户端回传 50M 的数据,发生了网络延迟。于是将 HttpClient Timeout 从 30s 调整到 60s, 上线之后明显改善。

But 昨天又出现了一次同样的超时异常

  1. time="2022-01-05T22:28:59+08:00" .... 
  2. time="2022-01-05T22:30:02+08:00" level=error msg="service Run error" error="region: sz,first load self allIns error:context deadline exceeded (Client.Timeout or context cancellation while reading body)" 

 2线下复盘

go的HttpClient Timeout包括连接、重定向(如果有)、从Response Body读取的时间,内置定时器会在Get,Head、Post、Do 方法之后继续运行,直到读取完Response.Body.

Timeout specifies a time limit for requests made by this Client. The timeout includes connection time, any redirects, and reading the response body. The timer remains running after Get, Head, Post, or Do return and will interrupt reading of the Response.Body.

Kibana 显示 Nginx 处理请求的耗时request_time才 32s, 远不到我们对于 Http 客户端设置的 60s 超时阈值。

排查 Go 开发的 HttpClient 读取 Body 超时

这里有必要穿插 Nginx Access Log 的几个背景点

1.Nginx 写日志的时间 根据nginx access log[1]官方,NGINX writes information about client requests in the access log right after the request is processed. 也就是说 Nginx 是在端到端请求被处理完之后才写入日志。

2.Nginx Request_Time upstream_response_time

  • $upstream_response_time – The time between establishing a connection and receiving the last byte of the response body from the upstream server

从 Nginx 向后端建立连接开始到接受完数据然后关闭连接为止的时间

  • $request_time– The total time spent processing a request

Nginx 从接受用户请求的第一个字节到发送完响应数据的时间

从目前的信息看,Nginx 从接受请求到发送完响应流,总共耗时 32s。那剩下的 28s,是在哪里消耗的?

3三省吾身

这是我抽离的 HttpClient 的实践, 常规的不能再常规。

  1. package main 
  2.  
  3. import ( 
  4.     "bytes" 
  5.     "encoding/json" 
  6.     "io/ioutil" 
  7.     "log" 
  8.     "net/http" 
  9.     "time" 
  10.  
  11. func main() { 
  12.       c := &http.Client{Timeout: 10 * time.Second
  13.       body := sendRequest(c, http.MethodPost) 
  14.       log.Println("response body length:", len(body)) 
  15.  
  16. func sendRequest(client *http.Client, method string) []byte { 
  17.       endpoint := "http://mdb.qa.17usoft.com/table/instance?method=batch_query" 
  18.       expr := "idc in (logicidc_hd1,logicidc_hd2,officeidc_hd1)" 
  19.       jsonData, err := json.Marshal([]string{expr}) 
  20.       response, err := client.Post(endpoint, "application/json", bytes.NewBuffer(jsonData)) 
  21.       if err != nil { 
  22.           log.Fatalf("Error sending request to api endpoint, %+v", err) 
  23.       } 
  24.       defer response.Body.Close() 
  25.       body, err := ioutil.ReadAll(response.Body) 
  26.       if err != nil { 
  27.           log.Fatalf("Couldn't parse response body, %+v", err) 
  28.       } 
  29.       return body 

核心就两个动作

  • 调用Get、Post、Do方法发起 Http 请求, 如果无报错,则表示服务端已经处理了请求
  • iotil.ReadAll表示客户端准备从网卡读取 Response Body (流式数据), 超时异常正是从这里爆出来的

报错内容:"Client.Timeout or context cancellation while reading body" 读取 Response Body 超时,

潜台词是:服务器已经处理了请求,并且开始向客户端网卡写入数据。

根据我有限的网络原理/计算机原理,与此同时,客户端会异步从网卡读取 Response Body。

写入和读取互不干扰,但是时空有重叠。

排查 Go 开发的 HttpClient 读取 Body 超时

所以[读取 Body 超时]位于图中的红框区域,这就有点意思了。

  • 之前我们有 30%概率[读取 Body 超时],确实是因为 Nginx 回传 50M 数据超时,这在 Nginx request_time 上能体现。
  • 本次 Nginx 显示 request_time=32s, 却再次超时,推断 Nginx 已经写完数据,而客户端还没有读取完 Body。

至于为什么没读取完,这就得吐槽iotil.ReadAll的性能了。客户端使用 iotil.ReadAll 读取大的响应体,会不断申请内存(源码显示会从 512B->50M),耗时较长,性能较差、并且有内存泄漏的风险。客户端机器稍有差池,可能就会导致读大Body超时, 下一篇我会讲解针对大的响应体替换iotil.ReadAll的方案[2]。

为了模拟这个偶发的情况,我们可在Post、iotil.ReadAll前后加入时间日志。

  1. $ go run main.go 
  2. 2022/01/07 20:21:46 开始请求: 2022-01-07 20:21:46.010 
  3. 2022/01/07 20:21:47 服务端处理结束: 2022-01-07 20:21:47.010 
  4. 2022/01/07 20:21:52 客户端读取结束: 2022-01-07 20:21:52.010 
  5. 2022/01/07 20:21:52 response body length: 50575756 

可以看出,当读取大的响应体时候,客户端iotil.ReadAll的耗时并不算小,这块需要开发人员重视。

我们甚至可以iotil.ReadAll之前time.Sleep(xxx), 就能轻松模拟出生产环境的读取 Body 超时。

4我的收获

1.Nginx Access Log 的时间含义

2.go 的 HttpClient Timeout 包含了连接、请求、读取 Body 的耗时

3.通过对[读取 Body 超时异常]的分析,我梳理了端到端的请求耗时、客户端的行为耗时的时空关系, 这个至关重要。

引用链接

[1] nginx access log: https://docs.nginx.com/nginx/admin-guide/monitoring/logging/

[2] 替换iotil.ReadAll的方案: https://stackoverflow.com/questions/52539695/alternative-to-ioutil-readall-in-go

原文链接:https://mp.weixin.qq.com/s/gWjnMd26qSbd4vMtEnKSew

延伸 · 阅读

精彩推荐
  • GolangGo语言基础单元测试与性能测试示例详解

    Go语言基础单元测试与性能测试示例详解

    这篇文章主要为大家介绍了Go语言基础单元测试与性能测试示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助祝大家多多进步...

    枫少文7812021-12-05
  • GolangGolang实现四种负载均衡的算法(随机,轮询等)

    Golang实现四种负载均衡的算法(随机,轮询等)

    本文介绍了示例介绍了Golang 负载均衡的四种实现,主要包括了随机,轮询,加权轮询负载,一致性hash,感兴趣的小伙伴们可以参考一下...

    Gundy_8442021-08-09
  • GolangGo语言range关键字循环时的坑

    Go语言range关键字循环时的坑

    今天小编就为大家分享一篇关于Go语言range关键字循环时的坑,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来...

    benben_20154202020-05-23
  • GolangGO语言字符串处理Strings包的函数使用示例讲解

    GO语言字符串处理Strings包的函数使用示例讲解

    这篇文章主要为大家介绍了GO语言字符串处理Strings包的函数使用示例讲解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加...

    Jeff的技术栈6882022-04-14
  • Golang深入浅析Go中三个点(...)用法

    深入浅析Go中三个点(...)用法

    这篇文章主要介绍了深入浅析Go中三个点(...)用法,需要的朋友可以参考下...

    踏雪无痕SS6472021-11-17
  • GolangGolang 语言极简类型转换库cast的使用详解

    Golang 语言极简类型转换库cast的使用详解

    本文我们通过 cast.ToString() 函数的使用,简单介绍了cast 的使用方法,除此之外,它还支持很多其他类型,在这没有多多介绍,对Golang 类型转换库 cast相关知...

    Golang语言开发栈6112021-12-02
  • GolangGo语言实现自动填写古诗词实例代码

    Go语言实现自动填写古诗词实例代码

    这篇文章主要给大家介绍了关于Go语言实现自动填写古诗词的相关资料,这是最近在项目中遇到的一个需求,文中通过示例代码介绍的非常详细,需要的朋...

    FengY5862020-05-14
  • Golanggo语言获取系统盘符的方法

    go语言获取系统盘符的方法

    这篇文章主要介绍了go语言获取系统盘符的方法,涉及Go语言调用winapi获取系统硬件信息的技巧,具有一定参考借鉴价值,需要的朋友可以参考下 ...

    无尽海3862020-04-24