写点什么

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

  • 2023-06-28
    福建
  • 本文字数:4435 字

    阅读完需:约 15 分钟

背景

一个安静的晚上,突然接到小伙伴电话线上 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, serverEnables 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 matchlocation /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) {...23352336     // All other requests have any port stripped and path cleaned2337     // before passing to mux.handler.2338     host := stripHostPort(r.Host)2339     path := cleanPath(r.URL.Path)23402341     // 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.Path2345     }23462347     if path != r.URL.Path {2348         _, pattern = mux.handler(host, path)2349         url := *r.URL2350         url.Path = path2351         return RedirectHandler(url.String(), StatusMovedPermanently), pattern2352     }23532354     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.pngGET /demo/image/profile.png,/demo/image/profile.pngGET /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

发布于: 刚刚阅读数: 5
用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
一个斜杠引发的CDN资源回源请求量飙升_CDN_互联网工科生_InfoQ写作社区