一次 Chrome 缓存锁的有趣探索

昨天同事问我关于 Node Event Loop 的问题,代码如下:

const Koa = require('koa')
const app = new Koa()
const logger = require('koa-logger')

let index = 0

const sleep = delay => new Promise(resolve => setTimeout(resolve, delay))

app.use(logger())

app.use(async ctx => {
  await sleep(500)
  ctx.body = index++
})

app.listen(3000)

在 Chrome 下用 fetch 同时发起五个请求:

Promise
  .all([
    fetch('http://localhost:3000'),
    fetch('http://localhost:3000'),
    fetch('http://localhost:3000'),
    fetch('http://localhost:3000'),
    fetch('http://localhost:3000')
  ])
  .then(data => console.log(data))

结果会是一个个收到请求并一个个 response,看着不太对劲,毕竟从 Node.js 异步原理(或者用脚趾头想想)中我们已经知道 setTimeout 是不可能起到阻塞主线程的效果的:

这个时候考虑的重点只有:第一,我们遇到了一个神奇的 Bug,第二,浏览器端发生了什么延迟了请求的发送。

在浏览器打完 fetch,Network 长这样:

首先在用脚趾头思考之后排除了 Promise.all 会不会有毒的情况(因为我用 for 循环得到了相同的结论)。

之后看了下 Waterfall 中的详情:

Connection Start 中的 Stalled 占了很大的时间,在灰色的阶段请求根本没用被发出去,事情越发朝着一个神奇的方向展开,于是决定研究一下 Stalled 的原因:

Stalled/Blocking

请求等待发送所用的时间。 可以是等待 Queueing 中介绍的任何一个原因。 此外,此时间包含代理协商所用的任何时间。

Queuing
如果某个请求正在排队,则指示:

  • 请求已被渲染引擎推迟,因为该请求的优先级被视为低于关键资源(例如脚本/样式)的优先级。 图像经常发生这种情况。
  • 请求已被暂停,以等待将要释放的不可用 TCP 套接字。
  • 请求已被暂停,因为在 HTTP 1 上,浏览器仅允许每个源拥有六个 TCP 连接。
  • 生成磁盘缓存条目所用的时间(通常非常迅速)

关于以上内容可以在以下连接阅读和了解:

第一,我们的 XHR 并不涉及优先级问题,第二我们也没有满 6 个 TCP 连接——一脸懵逼之后看来只能考虑缓存的嫌疑了。

正巧我们查到了:关于请求被挂起页面加载缓慢问题的追查(01/13更),尽管原作者的原因非常复杂,但是我们的问题还是觉得是个缓存锁的问题,先来确认一下,做个小小的实验:

然后再次发送请求,发现这一次五个请求同时发送,没有被卡住,嫌疑人越来越倾向于是 Cache Lock 了。

然后我们学习了一波,通过 <chrome://net-internals/> 抓了波请求:

我们发现 HTTP_CACHE_ADD_TO_ENTRY 花了特别长的时间(dt),果然是缓存锁的锅:

实际上我们在上面那篇文章中也读到了一些信息:

https://codereview.chromium.org/345643003

Http cache: Implement a timeout for the cache lock.

The cache has a single writer / multiple reader lock to avoid downloading the
same resource n times. However, it is possible to block many tabs on the same
resource, for instance behind an auth dialog.

This CL implements a 20 seconds timeout so that the scenario described in the
bug results in multiple authentication dialogs (one per blocked tab) so the
user can know what to do. It will also help with other cases when the single
writer blocks for a long time.

The timeout is somewhat arbitrary but it should allow medium size resources
to be downloaded before starting another request for the same item. The general
solution of detecting progress and allow readers to start before the writer
finishes should be implemented on another CL.

大致感觉上去就是一个读者写者模型的锁,写者在写的时候为了避免重复下载,需要等到写者写完读者再读,但是由于我们默认没有设置缓存,这部分缓存又不能直接用,所以请的请求又准备申请写锁去缓存下一次请求获得的结果了。

要避免这个问题,一是像我们原来排查嫌疑人时的那样禁用缓存,二可以通过 query 时间戳解决(好复古的解决方案!)。

当然从中我们也学到了不少,比如更会看 Network 了(笑),以及昨天又成功的熬了个夜 =^=。

植入部分

如果您觉得文章不错,可以通过赞助支持我。

如果您不希望打赏,也可以通过关闭广告屏蔽插件的形式帮助网站运作。

标签: 知识, Chrome

已有 5 条评论

  1. Trash

    感觉越来越不会写代码了

  2. 鬼7

    嗯,有趣,以前还不知道chrome有缓存锁这回事,受教。
    不过一开始为啥会需要一次性对同一个资源请求5次呢?

  3. wozien

    大神姐姐,你用的typecho代码高亮插件是哪个?好好看

  4. kwok

    thumb up

添加新评论