文章译自Don't trust default timeouts[1],作者为 Roberto Vitillo。
译者注:
作者解释了涉及到网络调用的应用中,如果不假思索地依赖语言或库默认的超时敲定,可能会引发不少问题,如资源泄漏、程序卡死等。读者读完此文后应仔细检查自己程序中的网络调用只否用心考虑过该问题。另外,如果你也要编写对外提供的网络库,一个好的实践是,一定要提供超时设定的选项,不要写死,默认时间也不要是无限长。
现代应用程序不会崩溃,而是假死(Modern applications don’t crash; they hang)。其中一个主要原因,是它假设网络是可靠的。其实不然。
当你在没有设置超时的情况下进行网络请求,你就是在告诉你的代码,你有100%的信心,这个请求会成功。谁给你的勇气?
如果你做的是同步的网络调用(synchronous network call),且该调用永远不会返回,那么,说得轻一点,调用线程永远会被占用着。不会返回的异步网络调用也不是免费的。它虽然不会阻塞线程,却泄漏了sockets。任何值得信赖的HTTP客户端库都会使用sockets池来避免重新创建连接。而这些池的容量是有限的。就像任何其他资源泄漏一样,把池中的sockets耗光,只是时间问题。当这种情况发生时,你的应用程序就会僵死,它要一直等着连接释放。
如果网络是不可靠的,为什么我们还要不断创造出默认超时为无穷大的API?有些API甚至没有设置超时的选项! 一个好的API应该设计成想让你犯错都难。当默认超时是无穷大的时候,等于是让用户自乱阵脚。
如果你能从这篇文章中记住一件事,那么就让它是:永远不要使用 "无穷大" 作为默认超时时间。
我们看一些具体的例子吧。
JavaScript中的XMLHttpRequest
是从服务器上异步获取数据的Web API。它的默认超时时间为零[2],这意味着没有超时!
var xhr = new XMLHttpRequest();
xhr.open('GET', '/api', true);
// No timeout by default!
xhr.timeout = 10000;
xhr.onload = function () {
// Request finished
};
xhr.ontimeout = function (e) {
// Request timed out
};
xhr.send(null);
客户端超时和服务器端超时一样关键。浏览器对某台主机可以打开的socket数量是有上限的[3]。如果你发出的网络请求永远不会返回,你就会耗尽socket 池。当池子用完后,想再连接到主机就难了。
fetch
Web API是XMLHttpRequest
API的现代替代品,后者使用了Promises
。当API最初被引入时,根本没有办法设置超时! 不过最近浏览器添加了实验性的Abort API
,以支持超时。
const controller = new AbortController();
const signal = controller.signal;
const fetchPromise = fetch(url, {signal});
// No timeout by default!
setTimeout(() => controller.abort(), 10000);
fetchPromise.then(response => {
// Request finished
});
在 Python 的世界中,情况也不乐观。requests 库使用的默认超时时间为无穷大[4]。
# No timeout by default!
response = requests.get('https://github.com/', timeout=10)
那Go呢?Go的HTTP包默认也不使用超时[5]。
var client = &http.Client{
// No timeout by default!
Timeout: time.Second * 10,
}
response, _ := client .Get(url)
现代的 Java 和 .NET 的HTTP客户端做得更好,通常都有默认的超时。例如,.Net Core 的HttpClient
默认超时是100秒[6]。这很宽松,但比没有超时要好得多。这并不奇怪,因为这两个语言是用来构建大规模的分布式系统的,这些系统需要对网络故障具有健壮性。没有定定超时的网络请求是分布式系统中顶级的无声杀手[7]。
作为一个经验法则,在进行网络调用时,一定要设置超时。而如果你是自建的库,一定要设置合理的默认超时,并使其可配。
关于网络话题,之前也写或译过一些:
欢迎留言交流。
Don't trust default timeouts: https://robertovitillo.com/default-timeouts
[2]XMLHttpRequest的默认没有超时: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout
[3]浏览器对某台主机可以打开的socket数量是有上限: https://hpbn.co/primer-on-browser-networking/#connection-management-and-optimization
[4]Python的requests库的默认超时时间是无穷大: https://requests.readthedocs.io/en/master/user/quickstart/#timeouts
[5]Go的HTTP包不使用超时: https://github.com/golang/go/issues/24138
[6].Net HttpClient的默认超时: https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.timeout?view=netcore-3.1#remarks
[7]Top silent killer: https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing