1.1.2 基于context的超时控制
由1.1.1小节的介绍可知,Go服务超时(请求处理时间超过WriteTimeout配置)会导致502状态码。幸运的是,这种情况比较容易排查。但是Go服务超时返回502状态码合理吗?
假设网关是基于Nginx搭建的,需要说明的是Nginx有一个功能叫作被动健康检查。这是什么意思呢?就是当Nginx向上游服务转发请求时,如果出现一些异常情况,比如超时、上游节点返回502/504等错误,或者Nginx与上游节点的TCP连接异常等情况,Nginx会标记该上游节点为异常。当某个上游节点在一段时间内的失败次数达到配置阈值时,Nginx会将该节点临时摘除,也就是说后续不会再向该节点转发请求。思考一下:是否会出现所有上游服务节点都被临时摘除的情况呢?
图1-1 WriteTimeout超时功能的实现逻辑
我们可以测试一下上面描述的场景。被动健康检查要求上游服务包含多个节点,所以再添加一个上游服务节点,配置如下:
注意,如果你在同一台机器上启动这两个Go服务,还需要修改另一个Go服务的监听端口号,同时记得重新启动Nginx容器。接下来将使用ab压测工具来模拟大量并发请求。ab工具的使用方法如下:
查看Nginx访问日志,你会发现存在大量502状态码的请求,对应的Nginx错误日志如下:
从上面的日志可以清楚地看到,该502状态码产生的原因是,当Nginx选择上游节点建立连接时,发现没有“存活”节点,也就是说所有的上游节点都被临时“摘除”了。这时候,Nginx不会再向任何节点转发请求,会直接向客户端返回502状态码。
笔者曾经遇到过这样的线上问题:由于一个非核心接口处理超时,短时间内出现了大量502状态码,网关Nginx因此认为所有的上游节点都不可用,导致其他核心接口也出现了大量502状态码,甚至影响到了业务功能。那有什么办法能避免呢?办法是当Go服务超时时,依然向客户端返回200状态码,超时错误信息可以通过数据标识返回给客户端。
Go语言为我们提供了一个非常有用的标准库——context(上下文),它可用于在整个请求上下文传递数据以及辅助实现超时控制逻辑。那么如何通过context改造Go服务,既能实现超时控制逻辑,又能保证超时后依然向客户端返回200状态码?可参考下面的代码:
参考上面的代码,context.WithTimeout返回一个带有超时功能的上下文,这里我们设置的超时时间是3s,也就是说3s后ctx.Done()方法返回的管道数据可读。请求处理逻辑由子协程实现,处理完成之后将返回结果写入管道c。select是Go语言关键字,可以监听多个管道的读写事件。如果子协程首先处理完成请求,管道c可读,便会向客户端正常返回数据。如果上下文先超时,ctx.Done()方法返回的管道数据可读,便会向客户端返回超时错误。编译并运行上面的Go程序,同时通过curl命令发起HTTP请求,结果如下:
可以看到,虽然处理该HTTP请求需要耗时5s,但是当上下文3s超时后,Go服务就立即向客户端返回数据,并且状态码是200。也就是说,基于context实现超时控制逻辑,一方面可以快速地向客户端返回数据(即使是超时错误),另一方面可以避免502状态码引起的一些隐患。