不要使用 Go 默认的 HTTP 客户端(在生产环境中)

点击上方蓝色“Golang来啦”关注我哟

加个“星标”,天天 15 分钟,掌握 Go 语言

via:
https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
作者:Nathan Smith

四哥水平有限,如有翻译或理解错误,烦请帮忙指出,感谢!

原文是作者 2016 发表的,点赞已经有 3K+。文章原标题是《不要使用 Go 默认的 HTTP 客户端》,发表出来之后,评论就炸锅了,按现在话说,有点标题党。一票网友认为原标题很是误导人,容易让别人以为 Go 默认的 HTTP 客户端有什么 Bug。迫于“压力”,可能作者本身也觉得标题不合适,就将标题加上了 - in production。

回过头看,文章还是非常值得推荐的,毕竟点赞数在这摆着呢。

原文如下:


通过 Go 自带的 HTTP 包写服务既简单又非常有趣。我封装过很多 API 包,发现这是一项令人愉快的任务。但是我遇到了一个很容易掉进去的坑,这个坑很容易使程序崩溃:Go 默认的 HTTP 客户端。

总的一句话:Go 默认的 HTTP 客户端没有指定请求超时时间,允许服务劫持 goroutine。当请求外部服务时,请始终使用自定义的 http.Client。

场景复现

假设你想通过 API 接口来访问 spacely-sprockets.com,获取可用的 sprocket 列表。用 Go 语言实现的话,你可能会这样做:

// error checking omitted for brevity
var sprockets SprocketsResponse
response, _ := http.Get("spacely-sprockets.com/api/sprockets")
buf, _ := ioutil.ReadAll(response.Body)
json.Unmarshal(buf, &sprockets)

你将代码写好,各种 error 也都处理好,接着编译运行,一切都很美好。现在你将 API 包应用到 web 应用中,你的 app 中的某一个页面通过调用 API 向用户显示 Spacely Sprockets 的广告资源列表。

一切都运行的很平稳直到有一天你的 app 宕掉了。于是你查看日志,似乎也没找到导致宕机的原因。接着你打开了监控工具,但是 CPU、内存和 I/O 看起来都很合理,没有可能会导致宕机。最后你打开了沙箱,它工作也是正常的。那到底是什么导致宕机呢?

沮丧的是,你检查了推特,并注意到 Spacely Sprockets 开发人员团队的一条推文,说他们经历了短暂的停电,但是现在一切恢复正常。于是你查看了他们的 API 状态页,发现停电发生在 APP 宕机的前几分钟。这似乎是不太可能的巧合,但是你无法完全弄清它们之间的关系,因为你的 API 代码已经优雅地处理了各种错误。你依然无法弄清楚问题的根源在哪。

Go 的 HTTP 包

Go 的 HTTP 包使用 Client 结构体来管理 HTTP(S) 通信的内部过程。Clients 是并发安全的对象,包含配置、管理 TCP 状态、处理 cookies 等。当你使用 http.Get(url) 时,就会调用  http.DefaultClient,走的是 HTTP 默认配置,声明如下:

var DefaultClient = &Client{}

除其他配置项外,http.Client 会配置一个超时时间,当请求时间超过这个数值时,请求就会自动断开。该数值默认值是 0,即没有超时时间。该默认值对于 HTTP 包来说挺合理的,同时这的确也是一个容易让人掉进去的坑,在上面的例子中,导致了 app 崩溃。事实证明,Spacely Sprockets 的 API 中断导致我们的请求尝试挂起,进程一直 hang 住。只要发生故障的服务器没有恢复,进程就会一直挂着。因为进行 API 调用是为了服务用户请求,所以这也会导致服务用户请求的 goroutine 也挂起。一旦有足够的人点击 sprockets 页面,app 就崩溃了,很有可能是因为系统资源达到了极限。

下面是演示出现问题的 Go 示例:

package main
import (
  “fmt”
  “net/http”
  “net/http/httptest”
  “time”
)
func main() {
  svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    time.Sleep(time.Hour)
  }))
  defer svr.Close()
  fmt.Println(“making request”)
  http.Get(svr.URL)
  fmt.Println(“finished request”)
}

编译运行程序之后,将会向服务器发送一个请求,请求发出之后会睡眠,等待一个小时之后程序正常退出。

解决办法

解决这个问题的办法就是使用 http.Client 时定义一个合理的超时时间,比如下面这样的:

var netClient = &http.Client{
  Timeout: time.Second * 10,
}
response, _ := netClient.Get(url)

设置了 10s 的超时时间,如果超时 Get() 将会返回错误:

&httpError{
  err:     err.Error() + " (Client.Timeout exceeded while awaiting headers)",
  timeout: true,
}

如果需要对请求生命周期进行更细粒度的控制,还可以另外指定自定义 net.Transport 和 net.Dialer。Transport 结构体用来管理底层 TCP 连接,Dialer 是用来管理连接建立的结构体。Go 的 net 包使用默认的 Transport 和 Dialer。下面是一个自定义的例子:

var netTransport = &http.Transport{
  Dial: (&net.Dialer{
    Timeout: 5 * time.Second,
  }).Dial,
  TLSHandshakeTimeout: 5 * time.Second,
}
var netClient = &http.Client{
  Timeout: time.Second * 10,
  Transport: netTransport,
}
response, _ := netClient.Get(url)

上面代码设置了 TCP 拨号时间、TLS 握手时间和请求超时时间。如果有需要我们还可以设置其他选项,例如 keep-alive 超时时间。

总结

Go 语言的 net/http 包是经过深思熟虑的产物,可以非常便捷地用于 HTTP(S) 通信。然而,缺少请求超时时间是一个非常容易掉进去的坑,因为这个包提供了很多诸如 http.Get(url) 等便捷的方法。其他一些语言(例如 Java)也有类似的问题;有一些开发语言则没有,比如 Ruby 就提供了 60s 的超时时间。请求远程服务时不设置超时时间会使你的应用程序依赖于该服务,如果远程服务发生故障或者有恶意程序,我们的请求将会永远挂起,从而有可能使系统资源耗尽导致宕机。

推荐阅读:

Go 基础之 Goroutine

Golang 时间操作大全

如果我的文章对你有所帮助,点赞、转发都是一种支持!

给个[在看],是对四哥最大的支持
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Seekload

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值