























又拖更了很久!最近要打的游戏有点多……
在一个多月以前开始倒霉的我加入了我们的性能排查专项,主要解决我们平台站点网络反应慢的问题。(再不更新就要忘光了)。
为了以防各位难以理解文章的脑回路,首先先来简单描述一下我们遇到的场景:
也因此处理内部网络问题更加困难,如果说在 C 端可以用「部分用户网络问题」来搪塞的话,内部系统一个老板 Case 就要一查到底了。
首先作为服务端我们先内部自查了一波,排除了服务本身的问题,剩下的可能性还有 Nginx 造成的问题、端到端网络问题、和前端的不当使用几个点可能会导致问题。
首先我们来看一个极端的 Case:

Chrome 中有针对不同的阶段进行解释说明:
fetchpriority guide.因此对于 Waiting For Server Response 来说,最简单的可能性当然还是 Server 的问题,但是我们打了 Server 的点,非常短的耗时。
然后回到我们的背景中,我们的访问位置是中国大陆,而我们的服务(页面)都是部署在美国的,中美跨洋算 300ms 一个 RTT,服务内部耗时 4ms(特地挑了一个没有业务逻辑的接口),那么剩下的时间在哪里呢。
结合之前的可能性,首先得先排除 Nginx 带来的影响,因为我们是双七层架构,链路本身就比较复杂,不过后续我们姑且排除了这个的可能性。
剩下的就是排查这一现象可能的原因了,当然,首先我们有个思考方向,那就是,如果差值在 300ms,那么有没有可能是因为多了一次 RTT 呢?
为此我们要用到一个祖传工具:chrome://net-export/,在 2017 年的时候,我们就有文章对此做过介绍:https://www.codesky.me/archives/something-about-chrome-cache-lock.wind
它的优点是,你可以得到一些 Chrome 网络通信过程中的细节,去协助你排查和理解对应请求到底发生了什么。
这是我们抓包得到的结果:

其中:
t= 表示事件发生的时间,st= 表示相对于请求开始的时间偏移量(以毫秒为单位),dt= 表示该事件持续的时间(以毫秒为单位)。HTTP2_STREAM_UPDATE_RECV_WINDOW:更新接收窗口与之相对的还有一个事件,让我们来看下面一个 Case:

这里有一个和 RECV_WINDOW 相对的事件:HTTP2_STREAM_UPDATE_SEND_WINDOW
HTTP/2 使用流控制来避免发送方发送超过接收方处理能力的流量。每个流和整个连接都有一个发送窗口和接收窗口:
关于两者的差别我们可以看本表格:
| 特性 | HTTP2_STREAM_UPDATE_SEND_WINDOW | HTTP2_STREAM_UPDATE_RECV_WINDOW |
|---|---|---|
| 方向 | 本地发送能力的变化 | 本地接收能力的变化 |
| Negative Delta | 本地发送数据 | 远端发送数据,本地接收 |
| Positive Delta | 远端增加本地发送窗口 | 本地增加远端发送窗口 (本地接收窗口增加) |
| 目的 | 变小:控制本地发送速率,避免远端过载 变大:本地发送的更快 |
变小:控制远端发送速率,避免本地过载 变大:本地下载的更快 |
对于上一张截图来说,窗口调整带来的正好是一次 RTT,其实是符合预期的。
也就是说,理论情况下,我们可以简化成:
Client Server
| |
|------- HEADERS (GET) ----------->| (Stream 1)
| |
|<------ HEADERS (200) ------------| (Stream 1)
|<------ DATA (64KB) --------------| (Stream 1, 耗尽初始窗口)
| |
|------- WINDOW_UPDATE (64KB) ---->| (Stream 1)
| |
|<------ DATA (剩余数据) -----------| (Stream 1)
| |
在跨洋链路中,窗口调整耗时是不可避免却无法被忽视的。
那么下一个问题,这么小的接口,为什么也会涉及到窗口调整呢?因为 HTTP2 多路复用,本质上复用了同一个链接,那么即使你自己是一个小接口,但是由于同一个 TCP 连接内的其他链接耗尽了资源,还是会有相同的问题。
常见的更新窗口时机有:
WINDOW_UPDATE。
WINDOW_UPDATE 以避免阻塞。
WINDOW_UPDATE 帧的往返: 如果服务器知道即将发送大量数据,提前将窗口开大可以减少客户端后续发送 WINDOW_UPDATE 帧的次数,从而减少了这些控制帧的网络往返延迟此外,HTTP2 虽然进行了多路复用,但这也并不意味着可以同时发起无限个请求,一个连接中分配的流是有限个,在网络分析工具的 HTTP2 面板中,我们可以看到最大流数以及一些其他的信息:

另外,下一个问题是,我们发现 HTTP2 中 Content Download 的时间有时候非常短,短到你会觉得同机房调用也没有这么快。对此我们可以看最开始的截图中URL_REQUEST_DELEGATE_RESPONSE_STARTED dt=0,URL_REQUEST_DELEGATE_RESPONSE_STARTED 意味着 URL 请求的代理(delegate)开始处理接收到的 HTTP 响应。换成人话就是「响应头已经收到了,数据要来了,你准备一下接收和处理吧!」
如果是串行传输的,那么很明显不可能有这种效果,这也是得益于多路复用,因此不必等待,响应体的数据可能已经在响应头发送的过程中或紧随其后就开始传输了。
在 Chromium 源代码中有详细记录了每个打点的事件和对应的位置,如果有需要欢迎大家翻找:https://source.chromium.org/chromium/chromium/src/+/main:net/url_request/url_request.cc?q=URL_REQUEST_DELEGATE_RESPONSE_STARTED

排查了第一轮过后,我们发现在加载时还有不可名状的解释空间,后来发现是因为 Service worker 异步获取 HTML 带来的问题:
导致很小的接口也需要很长时间才能下载完成。
首先我们上面提到 HTTP2 中同一个连接最多可以有 N 条流,而这 N 条流也不是同一优先级的,依旧要服从优先级算法。
在 Netlog Viewer 中,我们也能寻找到 Priority 分配的影子,比如:
t=70537 [st= 0] +REQUEST_ALIVE [dt=704]
--> priority = "MEDIUM"
--> traffic_annotation = 101845102
Chrome 会根据不同的资源分配不同的权重,不同权重间拿到的带宽资源是不等的,网络质量优、包体积普遍较小的情况下这一现象可能并不明显,而包体大、网络差的情况就变得很明显。
对此,让我们来看下 Chromium 是怎么处理的:
1enum ResourceLoadPriority {
2 // The unresolved priority is here for the convenience of the clients. It
3 // should not be passed to the ResourceLoadScheduler.
4 ResourceLoadPriorityUnresolved = -1,
5 ResourceLoadPriorityVeryLow = 0,
6 ResourceLoadPriorityLow,
7 ResourceLoadPriorityMedium,
8 ResourceLoadPriorityHigh,
9 ResourceLoadPriorityVeryHigh,
10 ResourceLoadPriorityLowest = ResourceLoadPriorityVeryLow,
11 ResourceLoadPriorityHighest = ResourceLoadPriorityVeryHigh,
12};
13
首先先定义了优先级,其次在根据类型分配优先级:
1static ResourceLoadPriority typeToPriority(Resource::Type type)
2{
3 switch (type) {
4 case Resource::MainResource:
5 return ResourceLoadPriorityVeryHigh;
6 case Resource::XSLStyleSheet:
7 ASSERT(RuntimeEnabledFeatures::xsltEnabled());
8 case Resource::CSSStyleSheet:
9 return ResourceLoadPriorityHigh;
10 case Resource::Raw:
11 case Resource::Script:
12 case Resource::Font:
13 case Resource::ImportResource:
14 return ResourceLoadPriorityMedium;
15 case Resource::LinkSubresource:
16 case Resource::TextTrack:
17 case Resource::Media:
18 case Resource::SVGDocument:
19 return ResourceLoadPriorityLow;
20 case Resource::Image:
21 case Resource::LinkPrefetch:
22 case Resource::LinkPreload:
23 return ResourceLoadPriorityVeryLow;
24 }
25 ASSERT_NOT_REACHED();
26 return ResourceLoadPriorityUnresolved;
27}
28
还有一些耳熟能详的八股调度策略:
1// If enabled, drop the priority of all resources in a subframe.
2if (frame()->settings()->lowPriorityIframes() && !frame()->isMainFrame())
3 return ResourceLoadPriorityVeryLow;
4// Async/Defer scripts.
5if (type == Resource::Script && FetchRequest::LazyLoad == request.defer())
6 return frame()->settings()->fetchIncreaseAsyncScriptPriority() ? ResourceLoadPriorityMedium : ResourceLoadPriorityLow;
7
现在,和前端的八股文就高度一致了,只是以后人家问你优先级的时候,你甚至可以告诉他怎么算的
1spdy::SpdyPriority ConvertRequestPriorityToSpdyPriority(
2 const RequestPriority priority) {
3 DCHECK_GE(priority, MINIMUM_PRIORITY);
4 DCHECK_LE(priority, MAXIMUM_PRIORITY);
5 return static_cast<spdy::SpdyPriority>(MAXIMUM_PRIORITY - priority +
6 spdy::kV3HighestPriority);
7}
8
9int Spdy3PriorityToHttp2Weight(SpdyPriority priority) {
10 priority = ClampSpdy3Priority(priority);
11 const float kSteps = 255.9f / 7.f;
12 return static_cast<int>(kSteps * (7.f - priority)) + 1;
13}
14
将 HTTP/2 的最大权重值 256 除以 7,并乘以 SpdyPriority 作为系数。
最终我们得到的优先级调度结论为:

当然,还有一些原因也可能会导致问题,我们都知道如果在一个 HTTP/2 连接中发生丢包,整个 TCP 连接上的所有流都会受到影响,直到丢失的包被重传并按顺序到达。这可能会抵消多路复用带来的好处,尤其是在高丢包率的网络环境下(例如,不稳定的移动网络或远距离连接)。跨洋链路稳定性差也是一个很重要的问题,尤其是国内的出口网络。
以及我们早在 2017 年就写过的 Chrome 缓存锁导致 Stall 的问题,这些都是大家可能不注意就会触发的问题。
此外,在 300ms RTT 下,我们有必要注意跨域请求带来的影响。
本文只从原理的角度做一些排查方法和排查思路的总结,剩下的优化思路其实还是各位熟悉的八股文时间,能优化优化,不能优化拉倒,毕竟很大困难是由跨洋链路带来的物理耗时,我们只能避免自己程序所带来的问题。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。