后端问题排查记录分享-关于Nginx开启gzip导致响应为空的分析

在工作过程中遇到的疑难杂症排查记录分享,后续还会分享一些k8s,docker,jvm角度排查问题的记录。

关于Nginx开启gzip导致响应为空的分析

Nginx #gzip #okhttp #feign

前言

当开启nginx的gzip压缩后会有部分GET响应出现响应异常问题

基本概念

什么是gzip?

简单来说gzip是一种压缩算法,用来对请求后的响应进行压缩传输,这里涉及两个角色,“客户端”和"服务端"。那么首先需要搞清楚有哪些资源是支持压缩的?谁去进行压缩,谁去进行解压?怎么判断哪些资源需要压缩?

1.请求的数据是不进行压缩的,只有响应体数据会进行压缩,目前测试如果是通过nginx开启gzip压缩,只会对GET请求资源进行压缩,其他类型的请求不会进行压缩。额外提一点,springboot内置的tomcat也支持gzip压缩,是否支持GET以外的请求压缩暂时未测试

2.压缩的目的是减少网络传输中宽带使用量,那自然是"服务端"进行压缩,“客户端”进行解压

3.如何判断哪些资源需要进行压缩呢?第一必须请求的“客户端”支持解压,如果"客户端"都不支持解压,那么在"客户端"看来响应回来就是一串无用的乱码,所以在发起请的时候“客户端”需要主动告诉“服务端”我是否支持gzip,http协议中是通过携带Accept-Encoding:gzip请求头来告诉“服务端”我支持解析gzip的解压,同样的"服务端"在响应体中会携带Content-Encoding:gzip来告诉“客户端”这个是一个需要解压的响应体

分析

回到应用中使用FeignAddHeaderInterceptor将所有请求头都转发到远程微服务。集成环境无法复现场的异常,经过排查,原因在于部署方式的差异导致的。


集成环境:

请求:浏览器(我支持GZIP解压) -》业务应用-》其他微服务

响应:其他微服务(响应原始数据)-》业务应用(接受原始数据)-》浏览器


现场环境:

请求:浏览器 (我支持gzip解压)-》Nginx(检测到上游携带gzip请求头)-》业务应用(原样转发所有请求头) -》Nginx(检测到上游携带gzip请求头)-》其他微服务

响应:其他微服务(原始数据) -》Nginx(对响应进行gzip压缩)-》业务应用(不支持数据gzip解压抛出异常,流程终止Fallback对异常数据返回null) -》Nginx-》浏览器


可以看出来,现场环境无论是请求还是响应中间始终是隔了一层Nginx。在集成环境因为没有开启gzip,就算请求携带了Accept-Encoding:gzip,“服务器”也不会处理,也不会在响应头中携带Content-Encoding:gzip,就不会有问题了。

现场环境因为将所有的请求头都转发了其中包括Accept-Encoding:gzip,又因为经过了一层nginx,nginx检查到请求告诉他支持gzip压缩解压,所以nginx将原数据进行了gzip压缩后再响应给到业务应用,但是实际上业务应用是不支持gzip的,他只是原样转发浏览器携带的头而已,于是导致解析出现以下异常

feign.FeignException: Illegal character ((CTRL-CHAR, code 31)): only regular white space (\r, \n, \t) is allowed between tokens
 at [Source: (BufferedReader); line: 1, column: 2] reading GET http://127.0.0.1//xxx
	at feign.FeignException.errorReading(FeignException.java:47)
    at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:149)
	at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:77)
	at feign.hystrix.HystrixInvocationHandler$1.run(HystrixInvocationHandler.java:107)
	at com.netflix.hystrix.HystrixCommand$2.call(HystrixCommand.java:302)

而项目中又写了对应的Fallback逻辑处理异常就返回null,就导致了看起来返回了空数据。

处理方式

改用feign-okhttp作为请求的client,okhttp支持自动解析响应头中携带Content-Encoding:gzip的数据,以下是改造步骤

1.添加相关依赖

<dependency>
	<groupId>io.github.openfeign</groupId>
	<artifactId>feign-okhttp</artifactId>
</dependency>

2.修改FeignCreateBuilder

找到构造HystrixFeign.builder()的逻辑添加.client(new feign.okhttp.OkHttpClient()),以下是示例代码

3.修改FeignAddHeaderInterceptor

FeignAddHeaderInterceptor中需要将Accept-Encoding:gzip请求头排除掉,原因下面会讲到

原因如下:追踪OkHttpClient源码中的BridgeInterceptor类会发现如果我们手动添加Accept-Encoding请求头,OkHttpClient将不会帮我们对数据进行解压,所以我们还是需要按照以上步骤排除一下浏览器传递过来的Accept-Encoding请求头

QA问答

Q1: 改造后是否影响集成环境

A1:改造后不影响集成环境,因为判断是否需要解压除了transparentGzip为true以外,还必须要响应头中携带了Content-Encoding:gzip代表该数据是经过压缩的,集成环境通过直接请求响应的数据没经过压缩,也不会响应Content-Encoding:gzip这个请求头

注意事项

  1. 压缩是双刃剑,实际上就是用算力换流量宽带。启用了gzip压缩会增加nginx和服务端的压力。建议安排压测根据实际情况来决定是否启用gzip压缩
  2. 目前主流的浏览器支持的压缩算法除了gzip以外还有 deflate和br,okhttpclient内置了gzip的解压,如果需要支持deflate,br需要额外改造,当然需要用到的情况很少,因为使用哪种压缩算法还是根据“客户端”携带的Accept-Encoding请求头来告诉“服务端”
9 Likes

学习了,感谢分享

感谢大佬,我们也遇到这个问题了 :rose:

1 Like

感谢大佬,真让我学到东西了 :rofl:

From #dev to 开发调优

1 Like