背景描述
阿里云CDN提供了页面优化功能,当开启页面优化功能时,CDN可以自动清除HTML页面冗余的注释和重复的空白符,缩小文件体积,提升页面可阅读性。本案例遇到的一个问题是,按照文档开启了CDN的页面优化功能,但是访问HTML页面实际并没有优化效果。
Gzip压缩引发
在排查测试的过程中,发现直接用curl URL的方式查看响应结果,是已经被页面优化了,但是直接在浏览器上访问HTML却没有被优化。进一步对比curl请求以及浏览器Network下的Request Header以及Response Header,发现浏览器的Request Header带了Accept-Encoding: gzip, deflate,Response Header返回了Content-Encoding: gzip,如下图所示
我们知道HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术,大流量的WEB站点常常使用GZIP压缩技术来让用户感受更快的速度。从测试来看,浏览器请求头带了Accept-Encoding: gzip表示浏览器支持解码gzip压缩后的文件,如果服务器支持gzip压缩,那么在收到这个请求头以后,就会返回gzip压缩以后的文件,在Response Header里以Content-Encoding: gzip的形式体现。直接用如下curl命令带Accept-Encoding请求头测试,发现返回结果也是带了gzip的,且页面也是没有优化的。
curl -I 'http://测试URL' -H 'Accept-Encoding: gzip'
综上可以暂时得出结论,返回结果带了Accept-Encoding: gzip以后,CDN的页面压缩不生效,返回结果不带Accept-Encoding: gzip,CDN的页面压缩生效。
Gzip压缩是在哪里发生的
由于请求的链路是:客户端Client -->CDN -->源站
这个Gzip压缩,可能是CDN产生的,也可能是源站产生的。这个比较容易验证,直接用curl命令绑定到源站IP去测试即可得出结论
curl -I 'http://测试URL' -H 'Accept-Encoding: gzip' -x '源站IP:80'
测试来看,返回结果果然带了Accept-Encoding: gzip,说明是源站gzip压缩导致的。产生这个现象的原因逐渐浮出水面,整个过程如下:
当用户在浏览器发起请求时,浏览器默认带了Accept-Encoding: gzip这个请求头,CDN作为一个代理服务器,在回源请求源服务器的时候转发了这个来自真实客户端(浏览器)的请求头,源服务器由于开了Gzip压缩,因此在收到这个请求头以后返回了Gzip压缩以后的内容给CDN,由于CDN不具备Gunzip功能,因此无法对Gzip压缩以后的内容去做页面优化,因此导致了页面优化功能不生效。
Gzip配置参数
以上基本定位问题,但是为了更明确,我搭建了一个基于Nginx的Web服务器用做CDN的源站,并在Nginx的配置文件nginx.conf里开启了Gzip压缩,配置如下图
但是测试发现,通过CDN访问,并且发了Accept-Encoding: gzip请求头以后,CDN依然可以完成页面压缩,这就比较奇怪。为了定位问题,直接在Web服务器上抓了包,看一下CDN和Web服务器的交互请求,发现一个很奇怪的现象:CDN请求Web服务器的时候转发了Accept-Encoding: gzip,但是Web服务器并没有响应Content-Encoding: gzip,报文如下图:
根据这个现象,去查了一下Nginx官网对于ngx_http_gzip_module模块的配置说明,可以看到该模块有如下配置参数
其中有一个gzip_proxied参数引起了注意。这个参数的含义,可以解释如下
语法: gzip_proxied [off|expired|no-cache|no-store|private|no_last_modified|no_etag|auth|any] …
默认值: gzip_proxied off
作用域: http, server, location
Nginx作为反向代理的时候启用,开启或者关闭后端服务器返回的结果,匹配的前提是后端服务器必须要返回包含”Via”的 header头。
off – 对于所有来自代理服务器的请求,都关闭压缩
expired – 如果响应header头中包含 “Expires” 头信息则启用压缩
no-cache – 如果响应header头中包含 “Cache-Control:no-cache” 头信息则启用压缩
no-store – 如果响应header头中包含 “Cache-Control:no-store” 头信息则启用压缩
private – 如果响应header头中包含 “Cache-Control:private” 头信息则启用压缩
no_last_modified – 如果响应header头中不包含 “Last-Modified” 头信息则启用压缩
no_etag – 如果响应header头中不包含 “ETag” 头信息则启用压缩
auth – 如果响应header头中包含 “Authorization” 头信息则启用压缩
any – 无条件启用压缩,也就是对任何来自代理服务器的请求,都返回压缩的内容
由于Nginx配置里配置了 gzip_proxied expired no-cache no-store private auth
因此相当于启用了gzip_proxied参数,当Web服务器发现来自代理服务器的请求时(在这里就是来自CDN的请求),Web服务器会去校验gzip_proxied参数,当发现服务器的Response Header里没有返回Expires、"Cache-Control:no-cache"等类似响应头时,服务器就返回了不带Gzip压缩的数据。如果Gzip配置模块是按照如下配置的话,那么任何来自代理服务器的请求,服务器都会返回Gzip压缩的内容。
gzip_proxied any
如何判断请求是来自代理服务器的
那么问题来了,服务器是如何判断这个请求是来自代理服务器的,而不是真实客户端呢。这里就涉及到Via这个HTTP Header了。关于Via的介绍,可以参考HTTP协议关于Via的文档。Via 是一个通用首部,是由代理服务器添加的,适用于正向和反向代理,在请求和响应首部中均可出现。这个消息首部可以用来追踪消息转发情况,防止循环请求,以及识别在请求或响应传递链中消息发送者对于协议的支持能力。在这里,CDN作为代理服务器,去请求源服务器的时候,请求头里会带上Via头(这点在上面的抓包截图里也可以看到),而服务器就是根据请求头里的Via得知该请求是来自上游代理服务器的。
HTTP服务器的问题是知道代理本身是否能够处理压缩响应。传入请求中的接受编码头(也就是Accept-Encoding: gzip)很可能是由原始客户机请求提供的,但这并不能表明它所经过的代理或网关的能力,也就是说,服务器并不知道上游代理服务器能否处理Gzip压缩以后的内容。因此,在此场景中,服务器采用最安全的选项,并选择不压缩它发回的响应,这也是合理的。关于Via这个Header对于Gzip压缩的影响,可以参考这篇Akamai的文章,有详细的介绍。
一个新的想法
既然源站响应了gzip内容会导致CDN的页面优化不生效,那只能源站响应未压缩过的内容,但是这样的话,最终对于客户端来说,请求到的文件没有经过有效压缩,还是会消耗客户端带宽,从而影响Web页面的访问性能。而CDN除了提供页面优化的功能外,还提供了Gzip的功能,那能不能在CDN层面去做页面优化和Gzip压缩呢?
通过测试发现,在CDN开启页面优化的同时,无论是开启Gzip压缩还是Br压缩,只要请求头带了accept-encoding: gzip, deflate, br头,则页面优化不生效。由此可见,在CDN层面,智能压缩的优先级比较高,压缩以后的内容无法页面优化,看来这个方案暂时也不可行。当然,这部分的策略有待产品层面去改良。
总结和解决方案
综上,该问题可以总结如下
(1)如果源站响应了Gzip压缩的内容,CDN会因为无法Gunzip导致页面优化功能不生效
(2)如果希望与CDN去智能压缩和页面优化,因此CDN层面智能压缩的优先级比页面优化的优先级高,也会导致页面优化不生效
(3)如果期望使用CDN的页面优化,那么需要确保源站服务器关闭Gzip压缩。如果源站服务器是Nginx,通过修改Nginx配置文件里ngx_http_gzip_module模块的gzip_proxied参数,设定来自代理服务器的请求,不返回Gzip压缩的内容来实现。
(4)另外还有一种比较实现方案,是可以在CDN层面配置删除Accept-Encoding这个回源HTTP请求头。