zoukankan      html  css  js  c++  java
  • 一次ETag不规范引发的下载错误

    问题背景与现象

    之前我们在CDN源站为部分项目做过一个优化,也就是安卓多渠道安装包在CDN场景下的差分打包、存储、分发,具体项目内容在这里不做过多解释,随着优化方案的上线,陆陆续续有几个运营同学找过来说有些安装包无法正常下载,具体现象为 用浏览器下载到百分之九十九之后,会提示发生网络错误,点击重试或者继续按钮后,会重新开始下载

    PS: 我们CDN节点的关键进程可以参考下图:

    Nginx用于处理业务相关的HTTP请求,ATS用于文件的缓存与回源。

    排查过程

    1. 利用curl将请求强制解析到有问题的CDN节点,下载对应的安装包,发现确实是会卡在最后的一个分片,但是如果构造range请求,发现可以正确下载到文件的最后一个分片,并且文件分片正确。

    2. 由于在CDN节点上,我们做了文件的分片存储,每个分片大小为1MB,所以可以利用二分法,定位有问题的分片内容,最终找到是中间的某个分片存在异常,其异常点主要表现为 HTTP的响应头content-length 并不是正常的1048576,而是0 , 但是content-range却是正确的,如下:

      HTTP/1.1 206 Partial Content
      Server: xxxxx
      Date: Fri, 20 Aug 2021 11:33:39 GMT
      Content-Type: application/vnd.android.package-archive
      Content-Disposition: attachment; filename="parent.apk"
      X-Split-Point: 2118123520
      Last-Modified: Fri, 20 Aug 2021 06:30:59 GMT
      ETag: 5a62842782890237774b5f54e801edca
      Access-Control-Allow-Origin: *
      X-Parent-File: parent.apk
      X-Real-Length: 1476358
      Expires: Sat, 20 Aug 2022 11:02:56 GMT
      Cache-Control: max-age=31536000
      X-Cache: hit-fresh
      Accept-Ranges: bytes
      Content-Range: bytes 1819279360-1820327935/2119599878
      Content-Length: 0
      Age: 231253
      Connection: keep-alive
      
      • 由于该分片在ATS的错误缓存,如果直接对ATS进行Range请求的话,结果如上面所示,但是如果从用户的角度,通过Nginx向上游ATS请求该分片的话,由于ATS响应了正确的Content-Range,Nginx忽略ATS响应头的Content-Length,并根据Content-Range重新计算Content-Length,并且把这个正确的Content-Length响应给客户端,最终就导致了文件的蹿位缺失,客户会一直等待Nginx给返回body,造成背景中提到的现象,直到连接超时。
      • 这里其实还有一个非常不容易被发现的问题,即ETag的值不符合规范,根据 RFC7232 中对ETag的格式规范,无论是强ETag还是弱ETag,ETag的值都需要被放在引号中。而这里的值没有引号。
    3. 下面通过ATS的回源逻辑来排查定位为什么会造成这种现象。我们 ATS 分片回源插件是根据 https://github.com/oxwangfeng/ats_slice_range 进行二次开发的,其关键逻辑可以参考这里: http://blog.chinaunix.net/uid-13776576-id-5749765.html

    4. 既然是ATS的回源问题,那么顺着CDN回源链路往上游的CDN二级节点找,发现了当时故障现场的一条日志,如下(去掉了部分敏感信息):

      12.xxx.xx.xx - - [20/Aug/2021:19:28:49 +0800] "GET https://foo.com/bar.apk HTTP/1.1" 200 2119600580 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36" 56.914 1048576 "1049344" "bytes=1819279360-1820327935" 206 hit-fresh 
      

      CDN节点的ATS在回源二级节点时,做了分片回源,请求的是1819279360-1820327935范围大小的文件,但是二级节点却给边缘节点的nginx ATS响应了整个包体,即响应http 状态码200,并且是完整的2119600580字节。

    5. 看样子问题点出在二级节点的nginx中两个关于处理range请求的模块,分别为ngx_http_slice_filter_modulengx_http_range_filter_module,最终确认是如果客户端在下载某个文件时,如果中间发生了意外的网络问题,某些浏览器如chrome在重试时会携带If-Range头部,If-Range的值包含两种( https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range ),一种是last-modified,另外一种是etag,nginx的range模块如果对If-Range的值校验不通过,则会响应200,否则响应206。所以正是由于客户端携带了该头部,并且源站给响应的分片的HTTP响应头中,ETag格式是不符合rfc规范的,导致二级节点nginx处理请求时作为非range请求处理返回200。问题原因找到。

      以下为nginx ngx_http_range_filter_module 相关的处理逻辑:

      if (r->headers_in.if_range) {
      
          if_range = &r->headers_in.if_range->value;
      
          if (if_range->len >= 2 && if_range->data[if_range->len - 1] == '"') {
      
              if (r->headers_out.etag == NULL) {
                  goto next_filter;
              }
      
              etag = &r->headers_out.etag->value;
      
              ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                              "http ir:%V etag:%V", if_range, etag);
      
              if (if_range->len != etag->len
                  || ngx_strncmp(if_range->data, etag->data, etag->len) != 0)
              {
                  goto next_filter;
              }
      
              goto parse;
          }
      
          if (r->headers_out.last_modified_time == (time_t) -1) {
              goto next_filter;
          }
      
          if_range_time = ngx_parse_http_time(if_range->data, if_range->len);
      
          ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                          "http ir:%T lm:%T",
                          if_range_time, r->headers_out.last_modified_time);
      
          if (if_range_time != r->headers_out.last_modified_time) {
              goto next_filter;
          }
      }
      

      对于客户端带了If-Range的http请求头的处理,nginx的行为如下:

      1. 如果If-Range的值的最后一个字符为 双引号,则认为其为ETag,会把该值当作ETag进行校验,如果成功,返回206
      2. 如果If-Range的值的最后一个字符不是 双引号,则认为其为Last-Modified,与http上游响应头的Last-Modified进行对比,如果成功,返回206,否则返回200 。

    扩展学习

    HTTP conditional requests

    在http中,有一个条件请求的概念,它主要用于客户端和服务端对http请求/响应的缓存内容的校验,以保证内容的完整性。例如,在客户端恢复下载时,或者在上传、修复服务器上的文档时,防止丢失更新。

    HTTP条件请求定义了如下的一系列相关头部,这些头部会作为一种前提条件,最终能否匹配会影响到服务端给客户端的http响应结果。HTTP规范中,认为GET请求是安全的,客户端/服务端可以附带上条件请求头,从而可以节省客户端、服务端的传输带宽。而对于PUT请求,是非安全的,条件请求头只可以用在上传内容到服务端的情况,并且修改的是服务端已存在的内容。

    • If-Match: 属于强校验,用于Range请求的GET/HEAD方法中,值为一个ETag或者多个ETag的列表,服务端只有在强ETag匹配到时(不校验弱ETag),才会响应206,否则响应416 。
    • If-None-Match:属于强校验,值同样为一个ETag或者多个ETag的列表,对于 GET/HEAD 方法来说,当验证失败的时候,服务器端必须返回响应码 304 (Not Modified)。对于能够引发服务器状态改变的方法,则返回 412 (Precondition Failed)。需要注意的是,服务器端在生成状态码为 304 的响应的时候,必须同时生成以下会存在于对应的 200 响应中的首部:Cache-Control、Content-Location、Date、ETag、Expires 和 Vary 。当与If-Modified-Since一起使用时,优先级更高。
    • If-Modified-Since: 值为一个GMT的日期时间,代表只有当请求的内容在给定的日期之后发生了修改,才会响应200状态,否则响应304(Not Modified)。If-Modified-Since只能与GET或HEAD一起使用。
    • If-Unmodified-Since: 值为一个GMT的日期时间,只有当资源在指定的时间之后没有进行过修改的情况下,服务器才会返回请求的资源,或是接受 POST 或其他 非安全 方法的请求。如果所请求的资源在指定的时间之后发生了修改,那么会返回 412 (Precondition Failed) 。除了应用在非安全的POST请求中,还可以与含有 If-Range 消息头的范围请求搭配使用,用来确保新的请求片段来自于未经修改的文档。
    • If-Range:与 If-Match 和 If-Unmodified-Since 类似,它的值可以是ETag,也可以是一个GMT的日期时间,它主要应用于GET/HEAD的Range请求中,如果服务端匹配的条件失败,则响应HTTP 200,并将完整的内容发送给客户端,如果匹配成功,则响应206(Partial Content)

    参考

  • 相关阅读:
    k8s系列---service
    算法
    golang-练习ATM --面向对象实现
    golang-练习ATM
    k8s系列---pod介绍
    12.20 一组v-if/v-else-if/v-else 的元素类型相同,应该使用 key
    12.20 await 操作符的学习(await后跟非promsie、promsie(成功/失败)的几种情况测试)
    12.20 async关键字的学习
    12.20 falsy变量
    12.19 js中递归优化(递归爆栈)
  • 原文地址:https://www.cnblogs.com/webber1992/p/15617121.html
Copyright © 2011-2022 走看看