前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CDN故障案例content-encoding深入分析

CDN故障案例content-encoding深入分析

作者头像
richard.xia_志培
发布2022-06-14 14:20:44
2.1K0
发布2022-06-14 14:20:44
举报
文章被收录于专栏:PT运维技术PT运维技术

故障现象:

同事反映在AWS的s3增加自定义header: Content-Encoding:gzip后,通过AWS 的cdn(cloudfront)加速后,chrome浏览器发现无法打开。

于是一起查看,打开chrome浏览器的debug模式,发现chrome浏览器和cloudfront CDN节点是通过H2(HTTP2) over TLS 协议建连的,由于之前碰到多次HTTP2的故障(因为基于http2 over TLS要求的加密套件cipher中算法强度更高,会导致客户端,服务器协商失败,导致http2访问异常), 先让同事禁止掉cloudfront CDN的http2, 禁止后,再次使用chrome访问,确定使用的http1.1, 但仍然报错。 仔细查看报错的字符:content_decoded_fail, 初步判断是由于gzip压缩导致的问题,在S3 资源文件中去掉Content-Encoding:gzip自定义header后,cloudfront加速的静态资源可以正常访问。考虑到AWS技术询问流程过于复杂,没有国内便捷。

便将这个配置在国内公有云的CDN/公有桶上配置一次。chrome浏览器访问报出同样的错误,虽然不知道2家cdn底层的具体实现, 从这2个现象看,cdn的行为是一致的。(多家CDN的表现行为一致,说明此行为大概率就是按照RFC规定实现的), 咨询国内公有云技术支持(主要是询问国内技术团队的技术参数很快就会有结果),得知CDN节点开启了gzip,但是没有开启gzip_vary,公有桶也没有开启压缩策略。

既然两家CDN在配置相同配置情况下,都表现出相同的行为,主流版本nginx极有可能表现出同样行为(目前看像是共性问题)

二. 故障复现:

以nginx1.13.6作为实例例子,在开发环境搭建环境,复现,: 搭建2个节点,一个cdn节点,一个源站

一: CDN节点配置: 1.nginx版本:1.13.6 边缘nginx节点主配置: 开启gzip, 关闭gzip_vary【和公有云CDN保持一致】

代码语言:javascript
复制
# gzip upstream回源的时候,启用压缩请求头回源,即带上Accept-Encoding:gzip
upstream npsdk_shot_com_admin_gzip {
server 192.168.94.39:4000; 
check interval=3000 fall=5 rise=2 timeout=3000 default_down=true type=tcp;
keepalive 300;
}
#no gzip, upstream回源的时候,启用非压缩请求头回源
upstream npsdk_shot_com_admin_nogzip {
server 192.168.94.39:3000; 
check interval=3000 fall=5 rise=2 timeout=3000 default_down=true type=tcp;
keepalive 300;
}
#default, upstream回源的时候,客户端的请求头不作任何改变
upstream npsdk_shot_com_admin {
server 192.168.94.39:5000;
check interval=3000 fall=5 rise=2 timeout=3000 default_down=true type=tcp;
keepalive 300;
}
server {
listen 8000; 
server_name repo.ops.ot.ease.com localhost;
access_log /data/logs/openresty/repo.ops.ot.netease.com.log nginxjson;
set $upstream 'npsdk_shot_com_admin';
location /default {
##增加cache,缓存时间24小时
proxy_cache ops;
proxy_cache_valid 200 304 24h;
proxy_cache_key $host$uri$is_args$args;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for,$server_addr;
#默认行为,回源行为不改变客户端的请求头
#proxy_set_header Accept-Encoding "";
#proxy_ignore_headers Vary;
proxy_pass http://$upstream;
}
location /rs_noogzip {
##增加cache,缓存时间24小时
proxy_cache ops;
proxy_cache_valid 200 304 24h;
proxy_cache_key $host$uri$is_args$args;
set $upstream 'npsdk_shot_com_admin_nogzip';
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for,$server_addr;
##回源时候,不带上压缩请求头
proxy_set_header Accept-Encoding "";
proxy_ignore_headers Vary;
proxy_pass http://$upstream;
}
location /rs_gzip {
##增加cache,缓存时间24小时
proxy_cache ops;
proxy_cache_valid 200 304 24h;
proxy_cache_key $host$uri$is_args$args;
set $upstream 'npsdk_shot_com_admin_gzip';
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for,$server_addr;
##回源时候,带上压缩请求头
proxy_set_header Accept-Encoding "gzip";
proxy_ignore_headers Vary;
proxy_pass http://$upstream;
}
}

二 源站节点配置 源站nginx版本:1.13.6 源站主配置:关闭gzip压缩,关闭gzip_vary 测试Server配置增加自定义头:add_header Content-Encoding gzip;

代码语言:javascript
复制
server {
listen 3000;
##no zip
server_name _;
access_log /data/logs/openresty/3000.log nginxjson;
set $upstream 'rpgmkt_nodejs';

location / {
#return 200 "test 3000 ok";
root /opt/nginxtest;
}
}
server {
listen 4000;
## gzip
server_name _;
access_log /data/logs/openresty/4000.log nginxjson;
set $upstream 'rpgmkt_nodejs';

location / {
#return 200 "test 4000 ok";
root /opt/nginxtest;
}
} 
server {
listen 5000;
##default
server_name _;
access_log /data/logs/openresty/5000.log nginxjson;

location / {
add_header Content-Encoding gzip;
root /opt/nginxtest;
#return 200 "test 4000 ok";
}
}

通过带压缩头请求

代码语言:javascript
复制
#curl 'http://192.168.94.21:8000/default/test.html' -H 'Host:repo.ops.ot.ease.com' -H 'Accept-Encoding: gzip,deflate' 
出现故障(命令行请求居然出现文本内容,应该出现gzip压缩的内容才是正常)

#不带压缩头请求

代码语言:javascript
复制
#curl 'http://192.168.94.21:8000/default/test.html' -H 'Host:repo.ops.ot.ease.com' 
正常

通过chrome 浏览器访问,故障可以重现,故障现象和AWS cloudfront , 公有云CDN报的错误一样:CONTENT_DECODED_FAIL, 完成了重现环境的搭建。

三. 故障定位

由于先前有一定的nginx基础,所以很快就找到相应的代码文件,代码段。由于gzip的压缩处理,全部都是由http_gzip_filter_module处理,那么查看http/modules/ngx_http_gzip_filter_module.c 的大体逻辑即可。

代码语言:javascript
复制
if (!conf->enable
|| (r->headers_out.status != NGX_HTTP_OK
&& r->headers_out.status != NGX_HTTP_FORBIDDEN
&& r->headers_out.status != NGX_HTTP_NOT_FOUND)
|| (r->headers_out.content_encoding
&& r->headers_out.content_encoding->value.len)
|| (r->headers_out.content_length_n != -1
&& r->headers_out.content_length_n < conf->min_length)
|| ngx_http_test_content_type(r, &conf->types) == NULL
|| r->header_only)
{
return ngx_http_next_header_filter(r);
}
r->gzip_vary = 1;
/* http/modules/ngx_http_gzip_filter_module.c: 261 */
if (!r->gzip_tested) {
if (ngx_http_gzip_ok(r) != NGX_OK) {
return ngx_http_next_header_filter(r);
}
} else if (!r->gzip_ok) {
return ngx_http_next_header_filter(r);
}
/* http/ngx_http_core_module.c: 1915 */
if (r->http_version < clcf->gzip_http_version) {
return NGX_DECLINED;
}

由于upstream response 处理完成后会处理一系列的filter handle, 其中gzip是其中的一个filter handle,其中upstream的响应包头读入 ngx_http_request_t::headers_out结构体中, 即r->headers中。如果upstream返回的数据的包 头字段中必定含有: "Content-Encoding: gzip" 字段, 那么上面的

r→headers_out.content_encoding判断为真,nginx直接跳过gzip_module的处理, 也就是说在源站自定义header: Content-Encoding: gzip 会导致cdn节点不进行压缩,直接跳过return ngx_http_next_header_filter(r); 另外的2个条件分支(本质就是2个条件) 也会导致http_gzip_filter_module不压缩,1. http_version 版本小于 gzip_http_version; 2. 已经存在gzip压缩过的资源(gzip_ok/gzip_testd的值由ngx_http_core_module.c处理,该模块会处理cache文件的内容,读取cache文件中的response header,读取cache文件中的response header, 如果已经存在了gzip的cache赋值r->gzip_ok=1) .

到此,自定义header "Content-Encoding: gzip" 导致chrome报错的原因算是定位到了:由于源站没有开启gzip, cdn回源的时候返回的是非压缩的数据,但是添加了自定义了content-enconding: gzip的头, 该响应头也一并被cdn节点cache到文件, 等chrome浏览器发起压缩请求的时候, cdn节点发现cache文件中response的header中已经存在content-encoding:gzip了,就跳过了gzip压缩过程, chrome浏览器接收到了非压缩的数据(但带上了content-encoding:gzip的头), 于是使用gzip去解压未压缩的内容,报: CONTENT_DECODED_FAIL。

四. 问题扩展以及修复方案

需要彻底fix这个问题,需要了解http vary这个机制了,http vary的机制简单来说: 服务端根据客户端发送的请求头中某些字段自动发送最合适的版本, 例如: 以gzip_vary为例,客户端发起压缩请求(带Accept-Encoding:gzip,br,deflate),客户端发起的非压缩请求(不带该header), 服务器端根据请求不同分发给客户端gzip压缩内容,非gzip压缩版本的内容。 由于我们只是看了部分的源代码,不排除有其他的入口,所以不排除有其他的入口, 所以仍然需要调试和小心求证.

观察以下技术指标点:

  1. CDN缓存的文件名,大小,cache缓存文件的个数
  2. CDN缓存文件中的response header变化
  3. 非压缩请求response的Etag,content-encoding变化
  4. 压缩请求response的Etag,content-encoding变化

CDN的缓存规则: proxy_cache_key hosturiis_argsargs, CDN完全透传请求头到源站(不会proxy_set_header修改客户端请求头)

以下是考察的场景:

1. 源站和cdn节点都不开启gzip_vary, 源站和cdn节点都开启gzip 2. 源站和cdn节点开启gzip_vary,源站和cdn节点都开启gzip 3. cdn节点开启gzip_vary,源站不开启gzip_vary, cdn节点开启gzip, 源站开启gzip, 4. cdn节点不开启gzip_vary,cdn节点开启gzip, 源站不开启gzip 5. cdn节点不开启压缩,源站也不开启压缩,前后两者都不开启gzip_vary

场景1中: 1.1 如果客户端是第一次发起非压缩请求,那么cdn会透传请求头到源站,以非压缩请求发送到源站,源站返回非压缩的内容给cdn节点, cdn节点缓存非压缩内容,接着客户端第二次发起带压缩的请求, 由于cdn没有开启gzip_vary, 带压缩头的请求和非带压缩头的请求都会命中proxy_cache_key hosturiis_argsargs, 所以CDN节点将非压缩的内容发送给客户端,刚才在http_gzip_filter_module代码中观察到,cdn节点读取cache文件的response头部,此时的response头部没有content-encoding:gzip, 所以r->gzip_ok非真,仍然要走压缩过程,所以cdn节点取出非压缩版本内容然后压缩再发送给用户端。

1.2 如果客户端是第一次发起压缩请求,那么cdn会透传请求头到源站,以压缩请求发送到源站,源站返回压缩的内容给cdn节点, cdn节点缓存压缩内容,接着客户端第二次发起带压缩的请求,由于cdn没有开启gzip_vary, 带压缩头的请求和非带压缩头的请求都会命中proxy_cache_key hosturiis_argsargs, 所以CDN节点将压缩的内容发送给客户端,刚才在http_gzip_filter_module代码中观察到,cdn节点读取cache文件的response头部,此时的response头部存在content-encoding:gzip, 所以r->gzip_ok真,不需要走压缩过程,所以cdn节点取出压缩版本内容然后压缩再发送给用户端。再接着用户端发起一个非压缩的请求,那就出现灾难了,因为带压缩的请求和非带压缩的请求都命中同一个cache文件,cdn直接将cache文件发送给客户端,结果就出现问题,客户端请求非压缩内容,结果得到了压缩的内容。一般这种情况下,服务器端会出现,浏览器类型的客户端很少出现,因为一般的浏览器都是发送要求的请求头。

场景2,3,4,5类似

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PT运维技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 四. 问题扩展以及修复方案
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档