Go底层原理与工程化实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.1.1 服务超时为什么导致502状态码

根据HTTP状态码的定义,服务超时的状态码不应该是504吗?为什么我们却说Go服务超时会导致502状态码呢?很简单,我们可以模拟Go服务超时的情况,测试一下这时的状态码到底是502还是504。另外,为了保证测试结果的准确性,服务的访问链路尽量与线上环境保持一致,这里假设访问链路是客户端→网关→Go服务,也就是说除了Go服务之外还需要搭建网关服务。

目前大部分企业都是基于Nginx搭建网关,而Nginx的编译安装过程比较复杂,所以我们选择通过Docker方式部署并启动Nginx服务。部署方式如下:

参考上面的说明,docker run命令用于运行容器。容器中Nginx服务的默认配置文件位于/etc/nginx目录下,因此我们使用-v参数将本地配置文件映射到容器中的指定目录。其中,nginx.conf是Nginx的主配置文件(参考Nginx的默认配置模板);conf.d目录下包含所有虚拟服务的配置;/var/log/nginx是Nginx服务日志文件的存放目录。

我们的目的是测试Go服务超时情况,其对应的Nginx虚拟服务配置如下所示:

另外,别忘了在主配置文件nginx.conf中引入conf.d目录下所有的虚拟服务配置,引入方式如下:

参考上面的配置文件,网关Nginx会将所有请求转发到x.x.x.x:8080,即Go服务监听端口必须是8080。Go语言的标准库还是比较完善的,基于net/http库只需要几行代码就能创建并启动一个HTTP服务,代码如下:

参考上面的代码,WriteTimeout用于设置HTTP请求的超时时间,函数http.HandleFunc用于注册请求处理方法。注意,我们通过函数time.Sleep使协程休眠5s,模拟请求处理超时情况。编译并运行上面的Go程序,通过curl命令发起HTTP请求,结果如下:

参考上面的运行结果,客户端收到的是502状态码,并且请求处理耗时为5.049s。这里有两个问题。

1)为什么Go服务超时返回的是502状态码?

2)明明我们设置的请求超时时间为3s,为什么实际的请求处理耗时却是5.049s?

502状态码的含义是Bad Gateway,看上去好像是网关错误,其实不是。实际上,502状态码通常是由网关Nginx与上游服务之间的TCP连接异常导致的,或者是上游服务直接返回了502状态码。怎么判断是哪种情况呢?只需查看Nginx的错误日志。如果是网关Nginx与上游服务之间的TCP连接异常导致的502状态码,Nginx一定会记录错误日志。比如针对上面的502状态码问题,查看Nginx的错误日志,如下所示:

从上面的日志可以清楚地看到,该502状态码产生的原因是,当网关等待从上游服务读取响应头时,上游服务过早地关闭了连接。也就是说,Go服务在检测到请求超时后,直接关闭了TCP连接,所以才导致网关返回了502状态码。如果你对此还有疑惑的话,可以通过tcpdump工具抓包验证一下,抓包结果如下所示:

参考上面的抓包结果,20点48分56秒,网关Nginx与Go服务建立了TCP连接,然后将HTTP请求转发到Go服务。20点49分01秒,Go服务关闭了该TCP连接。也就是说,Go服务在5s超时后才关闭了连接。抓包结果与模拟验证的结果一致。

还有一个问题,请求超时时间为3s,为什么5s后Go服务才关闭连接呢?这就需要了解WriteTimeout超时功能的实现逻辑了。

参考图1-1,当Go服务接收到客户端请求时,会根据WriteTimeout添加定时器。当处理时间超时后,定时器会设置Go服务与客户端的连接状态为已超时(注意只是设置一个标志位),所以即使请求处理时间已超过WriteTimeout,Go服务依然还在默默地处理请求。当Go服务处理完该HTTP请求,准备向客户端返回数据时,检测到与客户端的连接为已超时状态,于是便关闭了与客户端的TCP连接,从而导致网关返回了502状态码。