在工作过程中遇到的疑难杂症排查记录分享,后续还会分享一些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
这个请求头
注意事项
- 压缩是双刃剑,实际上就是用算力换流量宽带。启用了gzip压缩会增加nginx和服务端的压力。建议安排压测根据实际情况来决定是否启用gzip压缩。
- 目前主流的浏览器支持的压缩算法除了gzip以外还有 deflate和br,okhttpclient内置了gzip的解压,如果需要支持deflate,br需要额外改造,当然需要用到的情况很少,因为使用哪种压缩算法还是根据“客户端”携带的Accept-Encoding请求头来告诉“服务端”