一个斜杠引发的CDN资源回源请求量飙升

一个,斜杠,引发,cdn,资源,请求,飙升 · 浏览次数 : 2389

小编点评

**源站301请求飙升原因分析** 由于测试代码带入之前的测试代码,导致了CDN 301请求的飙升。具体原因如下: 1. **重复拼接的path**:测试代码中添加了部分代码,导致了图片类CDN请求的路径重复拼接。当客户端301跳转到正确的URL请求后,其path会重复拼接,导致nginx返回404响应。 2. **301跳转顺序问题**:由于测试代码带入了之前的测试代码,导致了301跳转顺序问题。当客户端301跳转到正确的URL请求后,其path会重复拼接,导致nginx返回404响应。 3. **nginx版本问题**:测试代码中使用了版本较低的nginx,可能存在一些版本问题导致的301请求飙升。 4. **服务器带宽限制**:由于测试代码中使用了版本较低的nginx,可能存在一些服务器带宽限制导致的404响应。 5. **请求路径重复拼接**:测试代码中添加了部分代码,导致了图片类CDN请求的路径重复拼接。当客户端301跳转到正确的URL请求后,其path会重复拼接,导致nginx返回404响应。 **解决方案** 1. **修复path重复拼接问题**:仔细检查测试代码中添加的代码,修复path重复拼接问题。 2. **调整301跳转顺序**:尝试调整301跳转顺序,以使客户端301跳转到正确的URL请求后,其path不再重复拼接。 3. **更新nginx版本**:使用版本较高的nginx,确保其支持301合并功能。 4. **优化服务器带宽**:检查服务器带宽,优化服务器性能以确保请求能顺利完成。 5. **处理请求路径重复拼接**:仔细检查图片类CDN请求的路径,并确保其不重复拼接。

正文

背景

一个安静的晚上,突然接到小伙伴电话线上CDN回源异常,具体表现为请求量飙升,且伴有少量请求404,其中回源请求量飙升已经持续两天但一直未被发现,直到最近404请求触发了告警后分析log才同时发现回源量飙升这一问题。
触发问题的原因很快被发现并修复上线,这里分享一下跟进过程中进一步学习到的CDN回源策略、301触发机制原理等相关概念与知识。

多余斜杠(/)引发的流量飙升

大量301

通过查看源站nginx log,发现回源请求量比上周同一天飙升近1000倍,甚至开始怀疑是不是CDN厂商那边出问题了,进一步分析log后发现源站对于CDN的回源请求基本上都是301(Move Permanently)响应,此类响应占了95%以上,301表示永久重定向,这说明绝大部分回源请求都会被源站重定向到另一个地址。
咨询CDN厂商后确认,CDN节点对于源站的301请求默认是不跟随的,即CDN节点不会代为跳转到最终地址并缓存在本地后返回,而是直接返回给客户端301,最终由客户端自行进行301跳转。
那突然飙升的301请求是怎么来的?查看nginx 301请求的path后很快发现所有的CDN图片资源地址的path开头多了一个/,比如http://cdn.demo.com/image/test.jpg 会变成 http://cdn.demo.com//image/test.jpg ,此类请求源站会直接返回301 Location: http://cdn.demo.com/image/test.jpg ,即要求用户301跳转至去除多余/的版本。
这就产生了一个疑问,印象中源站的文件server是没有这种类似301的处理机制,那这个合并/的操作难道是发生在nginx?然而印象中nginx也没有进行过类似配置。

似是301黑手的nginx merge_slashes

再次check文件server代码确认没有合并/的301处理逻辑,并经过实测后好似能够确认是nginx做了这一步301的处理(注意这是初版的错误结论),nginx配置中有一个merge_slashes配置,官方文档说明如下:

Syntax:	merge_slashes on | off;
Default: merge_slashes on;
Context: http, server
Enables or disables compression of two or more adjacent slashes in a URI into a single slash.
Note that compression is essential for the correct matching of prefix string and regular expression locations. Without it, the “//scripts/one.php” request would not match
location /scripts/ {
    ...
}
and might be processed as a static file. So it gets converted to “/scripts/one.php”.
Turning the compression off can become necessary if a URI contains base64-encoded names, since base64 uses the “/” character internally. However, for security considerations, it is better to avoid turning the compression off.

如上所示merge_slashes默认是开启的,其作用是将URI中多于2个的连续/合并为一个单一的/,也就是说无论是cdn.demo.com//a.jpg 还是cdn.demo.com///a.jpg , 都会被合并为cdn.demo.com/a.jpg 。
探索到这里的时候已经下意识的认为就是nginx的这个merge_slashes功能导致的返回301响应了,本来以这个为结论已经把blog都发布出来了,结果两天后看到博友@唐大侠 灵魂拷问:“既然nginx默认开启合并/,为啥不起作用呢?”
陷入深思,回想之前尝试了好一会儿阅读nginx源码找寻merge_slashes具体触发301的代码逻辑,但是并没有在纷繁复杂的源码中捋清这个逻辑,于是在简单验证了直接通过HTTP调用后端server时并不会触发301后即直接将301的锅甩到了nginx头上,所以其实属于一个间接确认,并不能100%肯定。看似有必要重整旗鼓再探查一番301产生的迷雾,结果还真发现了迷雾背后另有真相。
先重新澄清一下merge_slashes的功能,merge_slashes在开启的情况下确实会合并path中的多余/,但是合并这块的逻辑仅发生在location match这一步,假设有以下配置

    location / {
        return 404;
    }
    location /scripts/  {
        proxy_pass http://127.0.0.1:6666;
    }

当merge_slashes开启时: //scripts/one.php 在匹配时会合并多余/视为/scripts/one.php 进行匹配,因此命中location /scripts/ 规则,但是命中后proxy_pass传递到upstream 服务的数据中其path依然会是原本的 //scripts/one.php ,而非合并版本。
而当merge_slashes关闭时: //scripts/one.php 直接和location /scripts/ 比较是不会match的,所以最终只能命中 location / 返回404。
所以merge_slashes只会在match location时合并/,而后将对应请求转入location后的处理流程,真正的301幕后黑手并不是它!!
任重道远,继续探寻,既然nginx上找不到301的真正黑手,又回过来来拷问文件server。

golang DefaultServeMux

源站的文件server是个golang 服务,之前只check了业务代码而没有考虑依赖的底层库,这次重整旗鼓研究一下net/http的相关源码。
在默认的ServerMutex实现中有一个Handler函数:

2322 func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
...
2335
2336     // All other requests have any port stripped and path cleaned
2337     // before passing to mux.handler.
2338     host := stripHostPort(r.Host)
2339     path := cleanPath(r.URL.Path)
2340
2341     // If the given path is /tree and its handler is not registered,
2342     // redirect for /tree/.
2343     if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
2344         return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
2345     }
2346
2347     if path != r.URL.Path {
2348         _, pattern = mux.handler(host, path)
2349         url := *r.URL
2350         url.Path = path
2351         return RedirectHandler(url.String(), StatusMovedPermanently), pattern
2352     }
2353
2354     return mux.handler(host, r.URL.Path)
2355 }

在上述代码的2339行会调用cleanPath, CleanPath又最终会调用path.Clean, path.Clean官方文档有介绍:

Clean returns the shortest path name equivalent to path by purely lexical processing. It applies the following rules iteratively until no further processing can be done:

1. Replace multiple slashes with a single slash.
2. Eliminate each . path name element (the current directory).
3. Eliminate each inner .. path name element (the parent directory) along with the non-.. element that precedes it.
4. Eliminate .. elements that begin a rooted path: that is, replace "/.." by "/" at the beginning of a path.

第一点就是明确说明会将多个slashes合并为单个slash,而在Handler代码2347行明确会判断原始的 r.URL.Path若与CleanPath处理过后的path不相等,则会通过 return RedirectHandler(url.String(), StatusMovedPermanently), pattern 直接返回301永久重定向。
文件server的HTTP启动代码是http.ListenAndServe(settings.HttpBind, nil), 即handler=nil,就会使用默认的DefaultServeMux,至此终于真相大白。
至于为什么之前为什么会有“于是在简单验证了直接通过HTTP调用后端server时并不会触发301后即直接将301的锅甩到了nginx头上” 这个情况,因为当时偷懒直接HTTP调用的不是golang的文件server而是golang版的log server,而log server中定义了自己的handler,因此并不会有默认handler的301逻辑--最终导致了错误的结论--偷懒早晚是要付出代价的==

多余的/到底哪来的

没错,这是个低级错误--几天前刚修改了图片文件CDN URL组装的模块代码,赶紧回去diff一看发现图片资源model生成代码中有一行本地测试代码给手抖提交了(尴尬==!),其最终效果就是导致所有CDN图片资源的URL path开头都会多出一个/,这多出的一个/只会导致图片加载变慢一些而非加载失败,所以大家使用app的时候也没有发现什么问题。
立刻把导致多余/的测试代码注释掉上线,并清理了一波线上相关缓存后,再回过头来观察源站的nginx log请求量肉眼可见的急速下降,后续少量404请求也逐渐消失了。

少量资源404怎么来的

虽然404请求也消失了,但是404出现的原因却依然没有定位到,整体来说404请求数量只有301请求的1%左右,而通过分析log发现404请求开始出现与消失的时机基本上与大量301请求的出现与消失是保持一致的,看上去两者直接是有相关性的。
仔细check 404请求的log,发现其path也很有规律,如:

GET /demo/image/icon.png,/demo/image/icon.png
GET /demo/image/profile.png,/demo/image/profile.png
GET /demo/image/head.png,/demo/image/head.jpg

拿这些path拼装完整URL如http://cdn.demo.com/demo/image/icon.png 其实是能成功获取到资源的,也就是说这些资源肯定是存在的,只是莫名原因客户端请求时其path部分重复拼接了一次,对应请求http://cdn.demo.com/demo/image/icon.png,/demo/image/icon.png 自然就是404了。
从整个CDN文件请求的流程上来说,存在四种可能:

  1. 首先怀疑的是服务端在拼接CDN URL时出错导致path重复拼接了,但是代码上确实没有找到值得怀疑的地方。
  2. CDN节点在转发源站的301请求到客户端时响应的Location给拼接错了,考虑到CDN作为一个广泛使用的产品提供给这么多用户使用,CDN节点出错的可能性很低。
  3. nginx在merge /后给出的301响应中的 Location 给错了,考虑到nginx这么千锤百炼的产品这种可能性极低。
  4. 不由得回忆起千奇百怪的android机型可能存在千奇百怪的各种情况(踩过的各种坑...),是不是某些小众机型在301跳转时其跳转机制存在问题?可惜源站并直接不能收到客户端的原始请求,因此无法分析此类404请求具体的客户端相关参数,暂时无法进一步分析。

因此目前只能从出现时机上推断404与大量请求的301出现有关联性,但是具体是哪一步有问题还没有办法定位--欢迎大家不吝赐教给出新的可能思路--在301问题修复后404请求已消失,暂且只可遗留至未来有缘再见了。

请求飙升而流量未飙升之谜

问题修复后突然疑惑为什么请求量飙升*1000+的数天,源站带宽没有被打爆呢?源站购买的带宽可是扛不住这*1000的流量呀!
仔细一思考后了然:因为这新增的的请求都是nginx直接返回的301而非实际目标文件,后续客户端301跳转到正确的URL请求后就进入正常的CDN回源->缓存->返回流程了,并不会给源站带来额外消耗。一个完整的301响应也就几百字节,所以实际对于带宽的消耗很小,1000个301响应所占的带宽也就相当于一张几百KB的图片占用带宽而已,所以万幸这一步带宽并没有出现问题。

总结

回顾本次事故,手抖真是万恶之源,本来是新增部分代码,结果不小心把之前的测试代码带上导致CDN 301请求飙升,具体对用户的影响:

  1. 所有图片类CDN请求均会多一次301跳转,根据用户所属地理位置与源站的距离加载耗时新增几到几百ms不等。
  2. 大约有1%左右的图片请求会因为重复拼接的path而被响应404。

行走码湖这么多年,终究还是踩下了这个测试代码提到线上导致事故的程序员经典大坑之一,引以为戒,引以为戒!

转载请注明出处,原文地址:https://www.cnblogs.com/AcAc-t/p/cdn_nginx_301_by_merge_slashes.html

参考

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/301
https://cloud.tencent.com/document/product/228/7183
https://www.cnblogs.com/AcAc-t/p/cdn_nginx_301_by_merge_slashes.html
http://nginx.org/en/docs/http/ngx_http_core_module.html#merge_slashes
https://juejin.cn/post/6844903850625761288
https://pkg.go.dev/path#Clean
https://weakyon.com/2017/04/25/redirect-of-golang-serverMux.html
https://stackoverflow.com/questions/47475802/golang-301-moved-permanently-if-request-path-contains-additional-slash

与一个斜杠引发的CDN资源回源请求量飙升相似的内容:

一个斜杠引发的CDN资源回源请求量飙升

背景 一个安静的晚上,突然接到小伙伴电话线上CDN回源异常,具体表现为请求量飙升,且伴有少量请求404,其中回源请求量飙升已经持续两天但一直未被发现,直到最近404请求触发了告警后分析log才同时发现回源量飙升这一问题。 触发问题的原因很快被发现并修复上线,这里分享一下跟进过程中进一步学习到的CDN

基于 Traefik 如何实现 path 末尾自动加斜杠?

前言 Traefik 是一个现代的 HTTP 反向代理和负载均衡器,使部署微服务变得容易。 Traefik 可以与现有的多种基础设施组件(Docker、Swarm 模式、Kubernetes、Marathon、Consul、Etcd、Rancher、Amazon ECS...)集成,并自动和动态地配

如何在 Windows Server 2022 阿里云服务器上搭建自己的 MQTT 服务器之二Mosquitto服务器

一、介绍 最近几天都在搭建MQTT服务器,几天前搭建好了一个 Apache-Apollo的 MQTT 服务器,当我们在管理我们的主题的时候,发现主题的名称的斜杠(/)变成了点号(.),正好我在调试程序,在调用的时候出现了一些问题,各种解决办法都想了,还是没有解决,于是就向重新搭建一个 MQTT 服务

umich cv-2-2

UMICH CV Linear Classifiers 在上一篇博文中,我们讨论了利用损失函数来判断一个权重矩阵的好坏,在这节中我们将讨论如何去找到最优的权重矩阵 想象我们要下到一个峡谷的底部,我们自然会选择下降最快的斜坡,换成我们这个问题就是要求权重矩阵相对于损失函数的梯度函数,最简单的方法就是使

B+树要点梳理

B+树重要操作 中间节点 中间节点的key,与其对应的指针的原则是,小于key的元素在其指针指向的节点中 中间节点的key可以看成是右斜着排放的,即小于等于key的节点由key对应的指针指定,最有一个指针指向大于最右侧key的节点 分裂 当中间节点数量满了时,进行分裂,新生成一个相邻的中间节点rig

使用位运算优化 N 皇后问题

使用位运算优化 N 皇后问题 作者:Grey 原文地址: 博客园:使用位运算优化 N 皇后问题 CSDN:使用位运算优化 N 皇后问题 问题描述 N 皇后问题是指在 n * n 的棋盘上要摆 n 个皇后, 要求:任何两个皇后不同行,不同列也不在同一条斜线上, 求给一个整数 n ,返回 n 皇后的摆法

JavaScript快速入门(一)

JavaScript快速入门(二) 语句 只需简单地把各条语句放在不同的行上就可以分隔它们 var a = 1 var b = 2 如果想把多条语句放在同一行上,就需要用分号隔开 var a = 1; var b = 2 注释 用两个斜线作为一行的开始,这一行就会被当成一条注释 //记得写注释 多行

【matplotlib 实战】--折线图

折线图是一种用于可视化数据变化趋势的图表,它可以用于表示任何数值随着时间或类别的变化。 折线图由折线段和折线交点组成,折线段表示数值随时间或类别的变化趋势,折线交点表示数据的转折点。 折线图的方向表示数据的变化方向,即正变化还是负变化,折线的斜率表示数据的变化程度。 1. 主要元素 折线图主要由以下

开源字体整理

开源字体整理,主要是为了使用方便。一般来说,开源字体大多都是可以免费商用的,具体使用请查看对应的开源协议,禁止售卖 开源字体 1. 得意黑 得意黑是一款在人文观感和几何特征中寻找平衡的中文黑体。整体字身窄而斜,细节融入了取法手绘美术字的特殊造型。字体支持简体中文常用字(覆盖 GB 2312 编码字符

一个难忘的json反序列化问题

前言 最近我在做知识星球中的商品秒杀系统,昨天遇到了一个诡异的json反序列化问题,感觉挺有意思的,现在拿出来跟大家一起分享一下,希望对你会有所帮助。 案发现场 我最近在做知识星球中的商品秒杀系统,写了一个filter,获取用户请求的header中获取JWT的token信息。 然后根据token信息