Google Chrome中的高性能网络
Google Chrome 的历史和指导原则 【译注】这部分不再详细翻译,只列出核心意思。
驱动 Chrome 继续前进的核心原则包括:
Speed: 做最快的(fastest)的浏览器. Security:为用户提供最为安全的(most secure)的上网环境。
Stability: 提供一个健壮且稳定的(resilient and stable)的 Web 应用平台。
Simplicity: 以简练的用户体验(simple user experience)封装精益求精的技术(sophisticated technology)。
本文关将注于第一点,速度。
关于性能的方方面面 一个现代浏览器就是一个和操作系统一样的平台。在 Chrome 之前的浏览器都是单进程的应用,所有页面共享相同的地址空间和资源。引入多进程架构这是 Chrome 最为著名的改进【译注:省略一些反复谈论的细节】。
一个进程内,Web 应用主要需要执行三个任务:获取资源,页面排版及渲染,和运行 JavaScript。渲染和脚本都是在运行中交替以单线程的方式运行的,其原因是为了保持 DOM 的一致性,而 JavaScript 本身也是一个单线程的语言。所以优化渲染和脚本运行无论对于页面开发者还是浏览器开发者都是极为重要的。
Chrome 的渲染引擎是 WebKit, JavaScript Engine 则使用深入优论的 V8 (“V8″ JavaScript runtime)。但是,如果网络不畅,无论优化 V8 的 JavaScript 执行,还是优化 WebKit 的解析和渲染,作用其实很有限。巧妇难为无米之炊,数据没来就得等着!
相对于用户体验,作用最为明显的就是如何优化网络资源的加载顺序、优先级及每一个资源的延迟时间(latency)。也许你察觉不 到,Chrome 网络模块每天都在进步,逐步降低每个资源的加载成本:向 DNS lookups 学习,记住页面拓扑结构(topology of the web), 预先连接可能的目标网址,等等,还有很多。从外面来看就是一个简单的资源加载的机制,但在内部却是一个精彩的世界。
关于 Web 应用 开始正题前,还是先来了解一下现在网页或者 Web 应用在网络上的需求。
HTTP Archive 项目一直在追踪网页构建。除了页面内容外,它还会分析流行页面使用的资源数量,类型,头信息以及不同目标地址的元数据(metadata)。下面是 2013 年 1 月的统计资料,由 300,000 目标页面得出的平均数据:
1280 KB 包含 88 个资源(Images,JavaScript,CSS …)
连接 15 个以上的不同主机(distinct hosts)。
这些数字在过去几年中一直持续增长( steadily increasing ),没有停下的迹象。这说明我们正不断地建构一个更加庞大的、野心勃勃的网络应用。还要注意,平均来看每个资源不过 12KB, 表明绝大多数的网络传输都是短促(short and bursty)的。这和 TCP 针对大数据、流式(streaming)下载的方向不一致,正因为如此,而引入了一些并发症。下面就用一个例子来抽丝剥茧,一窥究竟……
一个 Resource Request 的一生 W3C 的 Navigation Timing specification 定义了一组 API,可以观察到浏览器的每一个请求(request)的时序和性能数据。下面了解一些细节:
给定一个网页资源地址后,浏览器就会检查本地缓存和应用缓存。如果之前获取过并且有相应的缓存信息(appropriate cache headers)(如 Expires, Cache-Control, etc.), 就会用缓存数据填充这个请求,毕竟最快的请求就是没有请求(the fastest request is a request not made)。否则,我们重新验证资源,如果已经失效(expired),或者根本就没见过,一个耗费网络的请求就无法避免地发送了。
给定了一个主机名和资源路径后,Chrome 先是检查现有已建立的连接(existing open connections)是否可以复用, 即 sockets 指定了以(scheme、host 和 port)定义的连接池(pool)。但如果配置了一个代理,或者指定了 proxy auto-config (PAC)脚本,Chrome 就会检查与 proxy 的连接。PAC 脚本基于 URL 提供不同的代理,或者为此指定了特定的规则。与每一个代理间都可以有自己的 socket pool。最后,上述情况都不存在,这个请求就会从 DNS 查询(DNS lookup)开始了,以便获得它的 IP 地址。
幸运的话,这个主机名已经被缓存过。否则,必须先发起一个 DNS Query。这个过程所需的时间和 ISP,页面的知名度,主机名在中间缓存(intermediate caches)的可能性,以及 authoritative servers 的响应时间这些因素有关。也就是说这里变量很多,不过一般还不致于到几百毫秒那么夸张。
拿到解析出的 IP 后,Chrome 就会在目标地址间打开一个新 TCP 连接,我们就要执行一个 3 度握手(“three-way handshake”): SYN > SYN-ACK > ACK。这个操作每个新的 TCP 连接都必须完成,没有捷径。根据远近,路由路径的选择,这个过程可能要耗时几百毫秒,甚至几秒。而到现在,我们连一个有效的字节都还没收到。
当 TCP 握手完成了,如果我们连接的是一个 HTTPS 地址,还有一个 SSL 握手过程,同时又要增加最多两轮的延迟等待。如果 SSL 会话被缓存了,就只需一次。
最后,Chrome 终于要发送 HTTP 请求了 (如上面图示中的 requestStart)。 服务器收到请求后,就会传送响应数据(response data)回到客户端。这里包含最少的往返延迟和服务的处理时间。然后一个请求就完成了。但是,如果是一个 HTTP 重定向(redirect)的话?我们又要从头开始这个过程。如果你的页面里有些冗余的重定向,最好三思一下!
你得出所有的延迟时间了吗? 我们假设一个典型的宽带环境:没有本地缓存,相对较快的 DNS lookup (50ms), TCP 握手,SSL 协商,以及一个较快服务器响应时间(100ms)和一次延迟(80ms,在美国国内的平均值): 50ms for DNS 80ms for TCP handshake (one RTT)
160ms for SSL handshake (two RTT’s)
40ms (发送请求到服务器)
100ms (服务器处理)
40ms (服务器回传响应数据)
一个请求花了 470 毫秒, 其中 80% 的时间被网络延迟占去了。看到了吧,我们真得有很多事情要做!事实上,470 毫秒已经很乐观了:
如果服务器没有达到到初始 TCP 的拥塞窗口(congestion window),即4-15KB,就会引入更多的往返延迟。 SSL 延迟也可能变得更糟。如果需要获取一个没有的认证(certificate)或者执行 online certificate status check (OCSP), 都会让我们需要一个新的 TCP 连接,又增加了数百至上千毫秒的延迟。
怎样才算”够快”? 前面可以看到服务器响应时间仅是总延迟时间的 20%,其它都被 DNS,握手等操作占用了。过去用户体验研究( user experience research )表明用户对延迟时间的不同反应:
延迟及用户反应 0 �C 100ms 迅速 100 �C 300ms 有点慢 300 �C 1000ms 机器还在运行 1s+ 想想别的事…… 10s+ 我一会再来看看吧…
上表同样适用于页面的性能表现: 渲染页面,至少要在 250ms 内给个回应来吸引住用户。这就是简单地针对速度。从 Google, Amazon, Microsoft,以及其它数千个站点来看,额外的延迟直接影响页面表现:流畅的页面会吸引更多的浏览、以及更强的用户吸引力(engagement) 和页面转换率(conversion rates).
现在我们知道了理想的延迟时间是 250ms,而前面的示例告诉我们,DNS Lookup, TCP 和 SSL 握手,以及 request 的准备时间花去了 370ms, 即便不考虑服务器处理时间,我们也超出了 50%。
对于绝大多数的用户和网页开发者来说,DNS, TCP,以及 SSL 延迟都是透明,很少有人会想到它。这也就是为什么 Chrome 的网络模块那么的复杂。
我们已经识别出了问题,下面让我们深入一下实现的细节…
深入 Chrome 的网络模块 多进程架构
Chrome 的多进程架构为浏览器的网络请求处理带来了重要意义,它目前支持四种不同的执行模式( four different execution models ).
默认情况下,桌面的 Chrome 浏览器使用 process- per-site 模式, 将不同的网站页面隔离起来, 相同网站的页面组织在一起。举个简单的例子: 每个 tab 独立一个进程。从网络性能的角度上说,并没什么本质上的不同,只是 process-per- tabl 模式更易于理解。
每一个 tab 有一个渲染进程(render process),其中包括了用于解析页面(interpreting)和排版(layout out)的 WebKit 的排版引擎(layout engine), 即上图中的 HTML Render。还有 V8 引擎和两者之间的 DOM Bindings,如果你对这部分很好奇,可以看这里( great introduction to the plumbing )。
每一个这样的渲染进程被运行在一个沙箱环境中,只会对用户的电脑环境做极有限的访问�C包括网络。而使用这些资源,每一个渲染进程必须和浏览内核 进程(browser[kernel] process)沟通,以管理每个渲染进程的安全性和访问策略(access policies)。
进程间通讯(IPC)和多进程资源加载 渲染进程和内核进程之间的通讯是通过 IPC 完成的。在 Linux 和 Mac OS 上,使用了一个提供异步命名管道通讯方式的 socketpair ()。每一个渲染进程的消息会被序列化地到一个专用的I/O线程中,然后再由它发到内核进程。在接收端,内核进程提供一个过滤接口(filter interface)用于解析资源相关的 IPC 请求( ResourceMessageFilter ), 这部分就是网络模块负责的。
这样做其中一个好处是所有的资源请求都由I/O进程处理,无论是 UI 产生的活动,或者网络事件触发的交互。在内核进程(browser/kernel process)的I/O线程解析资源请求消息,将转发到一个 ResourceDispatcherHost 的单例(singleton)对象中处理。
这个单例接口允许浏览器控制每个渲染进程对网络的访问,也能达到有效和一致的资源共享:
Socket pool 和 connection limits: 浏览器可以限定每一个 profile 打开 256 个 sockets, 每个 proxy 打开 32 个 sockets, 而每一组{scheme, host, port}可以打开 6 个。注意同时针对一组{host,port}最多允计打开 6 个 HTTP 和 6 个 HTTPS 连接。 Socket reuse: 在 Socket Pool 中提供持久可用的 TCP connections,以供复用。这样可以为新的连接避免额外建立 DNS、TCP 和 SSL (如果需要的话)所花费的时间。
Socket late-binding (延迟绑定): 网络请求总是当 Scoket 准备好发送数据时才与一个 TCP 连接关连起来,所以首先有机会做到对请求有效分级(prioritization),比如,在 socket 连接过程中可能会到达到一个更高优先级的请求。同时也可以有更好的吞吐率(throughput),比如,在连接打开过程中,去复用一个刚好可用的 socket, 就可以使用到一个完全可用的 TCP 连接。其实传统的 TCP pre-connect (预连接)及其它大量的优化方法也是这个效果。
Consistent session state (一致的会话状态): 授权、cookies 及缓存数据会在所有渲染进程间共享。
Global resource and network optimizations (全局资源和网络优化): 浏览器能够在所有渲染进程和未处理的请求间做更优的决策。比如给当前 tab 对应的请求以更好的优先级。
Predictive optimizations (预测优化): 通过监控网络活动,Chrome 会建立并持续改善预测模型来提升性能。
… 项目还在增加中.
单就一个渲染进程而言, 透过 IPC 发送资源请求很容易,只要告诉浏览器内核进程一个唯一 ID, 后面就交给内核进程处理了。
跨平台的资源加载 跨平台也是 Chrome 网络模块的一个主要考量,包括 Linux, Windows, OS X, Chrome OS, Android, 和 iOS。 为此,网络模块尽量实现成了单进程模式(只分出了独立的 cache 和 proxy 进程)的跨平台函数库, 这样就可以在平台间共用基础组件(infrastructure)并分享相同的性能优化,更有机会做到同时为所有平台进行优化。
相关的代码可以在这里找到 the “src/net” subdirectory)。本文不会详细展开每个组件,不过了解一下代码结构可以帮助我们理解它的能力结构。 比如:
net/android 绑定到 Android 运行时(runtime) [译注(Horky):运行时真是一个很烂的术语,翻和没翻一样。] net/base 公共的网络工具函数。比如,主机解析, cookies, 网络转换侦测(network change detection),以及 SSL 认证管理 net/cookies 实现了 Cookie 的存储、管理及获取 net/disk_cache 磁盘和内存缓存的实现 net/dns 实现了一个异步的 DNS 解析器(DNS resolver) net/http 实现了 HTTP 协议 net/proxy 代理(SOCKS 和 HTTP)配置、解析(resolution) 、脚本抓取(script fetching), … net/socket TCP sockets,SSL streams 和 socket pools 的跨平台实现 net/spdy 实现了 SPDY 协议 net/url_request URLRequest, URLRequestContext 和 URLRequestJob 的实现 net/websockets 实现了 WebSockets 协议上面每一项都值得好好读读,代码组织的很好,你还会发现大量的单元测试。
Mobile 平台上的架构和性能 移动浏览器正在大发展,Chrome 团队也视优化移动端的体验为最高优先级。先要说明的是移动版的 Chrome 的并不是其桌面版本的直接移植,因为那样根本不会带来好的用户体验。移动端的先天特性就决定了它是一个资源严重受限的环境,在运行参数有一些基本的不同:
桌面用户使用鼠标操作,可以有重叠的窗口,大的屏幕,也不用担心电池。网络也非常稳定,有大量的存储空间和内存。 移动端的用户则是触摸和手势操作,屏幕小,电池电量有限,通过只能用龟速且昂贵的网络,存储空间和内存也是相当受限。
再者,不但没有典型的样板移动设备,反而是有一大批各色硬件的设备。Chrome 要做的,只能是设法兼容这些设备。好在 Chrome 有不同的运行模式(execution models),面对这些问题,游刃有余!
在 Android 版本上,Chrome 同样运用了桌面版本的多进程架构- 一个浏览器内核进程,以及一个或多个渲染进程。但因为内存的限制,移动版的 Chrome 无法为每一个 tabl 运行一个特定的渲染进程,而是根据内存情况等条件决定一个最佳的渲染进程个数,然后就会在多个 tab 间共享这些渲染进程。
如果内存实在不足,或其它原因导致 Chrome 无法运行多进程,它就会切到单进程、多线程的模式。比如在 iOS 设备上,因为其沙箱机制的限制,Chrome 只能运行在这种模式下。
关于网络性能,首先 Chrome 在 Android 和 iOS 使用的是各其它平台相同的网络模块。这可以做到跨平台的网络优化,这也是 Chrome 明显领先的优势之一。所不同的是需要经常根据网络情况和设备能力进行些调整, 包括推测优化(speculative optimization)的优先级、socket 的超时设置和管理逻辑、缓存大小等。
比如,为了延长电池寿命,移动端的 Chrome 会倾向于延迟关闭空闲的 sockets (lazy closing of idle sockets), 通常是为了减少信号(radio)的使用而在打开新的 socket 时关闭旧的。另外因为预渲染(pre-rendering,稍后会介绍)会使用一定的网络和处理资源,它通常只在 WiFi 才会使用。
关于移动浏览体验会独立一章,也许就在 POSA 系列的下一期。
Chrome Predictor 的预测功能优化 Chrome 会随着使用变得更快。 它这个特性是通过一个单例对象 Predictor 来实现的。这个对象在浏览器内核进程(Browser Kernel Process)中实例化,它唯一的职责就是观察和学习当前网络活动方式,提前预估用户下一步的操作。下面是一个示例:
用户将鼠标停留在一个链接上,就预示着一个用户的偏好以及下一步的浏览行为。这时 Chrome 就可以提前进行 DNS Lookup 及 TCP 握手。用户的点击操作平均需要将近 200ms,在这个时间就可能处理完 DNS 和 TCP 相关的操作, 也就是省去几百毫秒的延迟时间。 当在地址栏(Omnibox/URL bar) 触发高可能性选项时,就同样会触发一个 DNS lookup 和 TCP 预连接(pre-connect),甚至在一个不可见的页签中进行预渲染(pre-render)!
我们每个人都一串天天会访问的网站, Chrome 会研究在这些页面上的子资源, 并且尝试进行预解析(pre-resolve), 甚至可能会进行预加载(pre-fetch)以优化浏览体验。
除了上面三项,还有很多..
Chrome 会在你使用过程中学习 Web 的拓扑结构,而不单单是你的浏览模式。理想的话,它将为你省去数百毫秒的延迟, 更接近于即时页面加载的状态. 正是为了这个目标,Chrome 投入了以下的核心优化技术:
DNS 预解析(pre-resolve) 提前解析主机地址,以减少 DNS 延迟 TCP 预连接(pre-connect) 提前连接到目标服务器,以减少 TCP 握手延迟资源预加载(prefetching) 提前加载页面的核心资源,以加载页面显示页面预渲染(prerendering)
提前获取整个页面和相关子资源,这样可以做到及时显示
每一个决策都包含着一个或多个的优化, 用来克服大量的限制因素. 不过毕竟都只是预测性的优化策略,如果效果不理想,就会引入多余的处理和网络传输。甚至可能会带来一些加载时间上的负体验。
Chrome 如何处理这些问题呢? Predictor 会尽量收集各种信息,诸如用户操作,历史浏览数据,以及来自渲染引擎(render)和网络模块自身的信息。
和 Chrome 中负责网络事务调度的 ResourceDispatcherHost 不同,Predictor 对象会针对用户和网络事务创建一组过滤器(filter):
IPC channel filter 用来监控来自 render 进程的事务。 每个请求上都会加一个 ConnectInterceptor 对象,这样就可以跟踪网络传输的模式以及每一个请求的度量数据。
渲染进程(render process)会在一系列的事件下发送消息到浏览器进程(browser process), 这些事件被定义在一个枚举(ResolutionMotivation)中以便于使用 (url_info.h):
enum ResolutionMotivation { MOUSE_OVER_MOTIVATED, // 鼠标悬停. OMNIBOX_MOTIVATED, // Omni-box 建议进行解析. STARTUP_LIST_MOTIVATED, // 这是在前 10 个启动项中的资源. EARLY_LOAD_MOTIVATED, // 有时需要使用 prefetched 来提前建立连接.
// 下面定义了预加载评估的方式,会由一个 navigation 变量指定. // referring_url_也需要同时指定. STATIC_REFERAL_MOTIVATED, // 外部数据库(External Database)建议进行解析。 LEARNED_REFERAL_MOTIVATED, // 前一次浏览(prior navigation 建议进行解析. SELF_REFERAL_MOTIVATED, // 猜测下一个连接是不是需要进行解析.
// <略> … }; 通过这些给定的事件,Predictor 的目标就可以评估它成功的可能性, 然后再适时触发操作。每一项事件都有其成功的机率、优先级以及时间戳,这些可以在内部维护一个用优先级管理的队列,也是优化的一个手段。最终,对于这个队 列中发出的每一个请求的成功率,都可以被 Predictor 追踪到。基于这些数据,Predictor 就可以进一步优化它的决策。