本文首发于 vivo互联网技术 微信公众号 

链接:upload/201910211541494345.jpg" alt="" width="650" data-cke-saved-src="upload/201910211541505658.jpg" style="border: none; margin-top: 20px; margin-bottom: 20px; max-width: 650px; height: auto;" />

注意看红色标注部分,左边disable cache 代表关闭浏览器缓存,打开这个选项之后,每次访问页面都会重新拉取而不是使用缓存,右边的online可以下拉菜单选择弱网环境下访问此页面。在模拟弱网环境的时候此方法通常非常有效。例如我们正常访问的时候,耗时仅仅2s。

打开弱网(fast 3g)

时间膨胀到了26s.

这里说一下这2个选项的作用,Preserve log主要就是保存前一个页面的请求日志,比如我们在当前页面a点击了一个超链接访问了页面b,那么页面a的请求在控制台就看不到了,如果勾选此选项那么就可以看到这个前面一个页面的请求。另外这个Hide data Urls,选项额外说明一下,有些h5页面的某些资源会直接用base64转码以后嵌入到代码里面(比如截图中data: 开头的东西),这样做的好处是可以省略一些http请求,当然坏处就是开启此选项浏览器针对这个资源就没有缓存了,且base64转码以后的体积大小要比原大小大4/3。我们可以勾选此选项过滤掉这种我们不想看的东西。

再比如,我们只想看看同一个页面下某一个域名的请求(这在做竞品分析时对竞品使用域名数量的分析很有帮助),那我们也可以如下操作:

再比如说我们只想看一下这个页面的post请求,或者是get请求也可以。

再比如我们可以用 is:from-cache 查看我们当前页面哪些资源使用了缓存。large-than:(单位是字节)这个也很有用,通常我们利用这个过滤出超出大小的请求,看看有多少超大的图片资源(移动端排查h5页面速度慢的一个手段)。

我们也可以点击其中一个请求,按住shift,注意看蓝色的就是我们选定的请求,绿色的代表这个请求是蓝色请求的上游,也就是说只有当绿色的请求执行完毕以后才会发出蓝色的请求,而红色的请求就代表只有蓝色的请求执行完毕以后才会请求。这种看请求上下游关系的方法是很多时候h5优化的一个技巧。将用户最关心的资源请求前移,可以极大优化用户体验,虽然在某种程度上这种行为并不会在数据上有所提高(例如activity之间跳转用动画,application启动优化用特殊theme等等,本质上耗时都没有减少,但给用户的感觉就是页面和app速度很快)。

这个timing 可以显示一个请求的详细分段时间,比如排队时间,发出请求到第一个请求响应的字节时间,以及整个response都传输完毕的时间等等。有兴趣的可以自行搜索下相关资料。

二、关于Connection:Keep-Alive

在现代服务器架构中,客户端的长连接大部分时候并不是直接和源服务器打交道(所谓源服务器可以粗略理解为服务端开发兄弟实际代码部署的那台服务器),而是会经过很多代理服务器,这些代理服务器有的负责防火墙,有的负责负载均衡,还有的负责对消息进行路由分发(例如对客户端的请求根据客户端的版本号,ios还是Android等等分别将请求映射到不同的节点上)等等。

客户端的长连接仅仅意味着客户端发起的这条tcp连接是和第一层代理服务器保持连接关系。并不会直接命中到原始服务器。

再看一张图:

通常来讲,我们的请求客户端发出以后会经过若干个代理服务器才会到我们的源服务器。如果我们的源服务器想基于客户端的请求的ip地址来做一些操作,理论上就需要额外的http头部支持了。因为基于上述的架构图,我们的源服务器拿到的地址是跟源服务器建立tcp连接的代理服务器的地址,压根拿不到我们真正发起请求的客户端ip地址。

http RFC规范中,规定了X-Forwarded-For 用于传递真正的ip地址。当然了在实际应用中有些代理服务器并不遵循此规定,例如Nginx就是利用的X-Real-IP 这个头部来传递真正的ip地址(Nginx默认不开启此配置,需要手动更改配置项)。

在实际生产环境中,我们是可以在http response中将上述经过的代理服务器信息一一返回给客户端的。

看这个reponse的返回里面的头部信息有一个X-via 里面的信息就是代理服务器的信息了。

再比如说 我们打开淘宝的首页,找个请求。

这里的代理服务器信息就更多了,说明这条请求经过了多个代理服务器的转发。另外有时我们在技术会议上会听到正向代理和反向代理,其实这2种代理都是指的代理服务器,作用都差不多,只不过应用的场景有一些区别。

正向代理:比如我们kexue上网的时候,这种是我们明确知道我们想访问外网的网站比如facebook、谷歌等等,我们可以主动将请求转发到一个代理服务器上,由代理服务器来转发请求给facebook,然后facebook将请求返回给代理服务器,服务器再转发给我们。这种就叫正向代理了。

反向代理:这个其实我们每天都在用,我们访问的服务器,99%都是反向代理而来的,现代计算机系统中指的服务器往往都是指的服务器集群了,我们在使用一个功能的时候,根本不知道到底要请求到哪一台服务器,通常这种情况都是由Nginx来完成,我们访问一个网站,dns返回给我们的地址,通常都是一台Nginx的地址,然后由Nginx自己来负责将这个请求转发给他觉得应该转发的那台服务器。

这里我们多次提到了Nginx服务器和代理服务器的概念,考虑到很多前端开发可能不太了解后端开发的工作,暂且在这里简单介绍一下。通常而言我们认为的服务器开发工程师每天大部分的工作都是在应用服务器上开发,所谓http的应用服务器就是指可以动态生成内容的http服务器。比如 java工程师写完代码以后打出包交给Tomcat,Tomcat本身就是一个应用服务器。再比如Go语言编译生成好的可执行文件,也是一个http的应用服务器,还有Python的simpleServer等等。而Nginx或者Apache更像是一个单纯的http server,这个单纯的http server 几乎没有动态生成http response的能力,他们只能返回静态的内容,或者做一次转发,是一个很单纯的http server。严格意义上说,不管是Tomcat还是Go语言编译出来的可执行文件还是Python等等,本质上他们也是http server,也可以拿来做代理服务器的,只是通常情况下没有人这么干,因为术业有专攻,这种工作通常而言都是交给Nginx来做。

下图是Nginx的简要介绍:用一个Master进程来管理n个worker进程,每个worker进程仅有一个线程在执行。

在Nginx之前,多数服务器都是开启多线程或者多进程的工作模式,然而进程或者线程的切换是有成本的,如果访问量过高,那么cpu就会消耗大量的资源在创建进程或者创建线程,还有线程和进程之前的切换上,而Nginx则没有使用类似的方案,而是采用了“进程池单线程”的工作模式,Nginx服务器在启动的时候会创建好固定数量的进程,然后在之后的运行中不会再额外创建进程,而且可以将这些进程和cpu绑定起来,完美的使用现代cpu中的多核心能力。

此外,web服务器有io密集型的特点(注意是io密集不是cpu密集),大部分的耗时都在网络开销而非cpu计算上,所以Nginx使用了io多路复用的技术,Nginx会将到来的http请求一一打散成一个个碎片,将这些碎片安排到单一的线程上,这样只要发现这个线程上的某个碎片进入io等待了就立即切换出去处理其他请求,等确定可读可写以后再切回来。这样就可以最大限度的将cpu的能力利用到极致。注意再次强调这里的切换不是线程切换,你可以把他理解为这个线程中要执行的程序里面有很多go to 的锚点,一旦发现某个执行碎片进入了io等待,就马上利用go to能力跳转到其他碎片(这里的碎片就是指的http请求了)上继续执行。

其实这个地方Nginx的工作模式有一点点类似于Go语言的协程机制,只不过Go语言中的若干个协程下面并不是只有一个线程,也可能有多个。但是思路都是一样的,就是降低线程切换的开销,尽量用少的线程来执行业务上的“高并发”需求。

然而Nginx再优秀,也抵不过岁月的侵蚀,说起来距离今天也有15年的时间了。还是有一些天生缺陷的,比如Nginx只要你修改了配置就必须手动将Nginx进程重启(master进程),如果你的业务非常庞大,一旦遇到要修改配置的情况,几百台甚至几千台Nginx手动修改配置重启不但容易出错而且重复劳动意义也不大。此外Nginx可扩展性一般,因为Nginx是c语言写的,我们都知道c语言其实还是挺难掌握的,尤其是想要掌握的好更加难。不是每个人都有信心用C语言写出良好可维护的代码。尤其你的代码还要跑在Nginx这种每天都要用的基础服务上。

基于上述缺陷,阿里有一个绰号为“春哥”的程序员章亦春,在Nginx的基础上开发了更为优秀的OpenResty开源项目,也是老罗锤子发布会上说要赞助的那个开源项目。此项目可以对外暴露Lua脚本的接口,80后玩过魔兽世界的同学一定对Lua语言不陌生,大名鼎鼎的魔兽世界的插件机制就是用Lua来完成的。OpenResty出现以后终于可以用Lua脚本语言来操作我们的Nginx服务器了,这里Lua也是用“协程”的概念来完成并发能力,与Go语言也是保持一致的。此外OpenResty对服务器配置的修改也可以及时生效,不需要再重启服务器。大大提高运维的效率。等等等等。。。

三、http协议中“队头拥塞”的真相

前文我们数次提到了服务器,高并发等关键字。我们印象中的服务器都是与高并发这3个字强关联的。那么所谓 http中的“队头拥塞”到底指的是什么呢?我们先来看一张图:

这张互联网中流传许久的图,到底应该怎么理解?有的同学认为http所谓的拥塞是因为传输协议是tcp导致的,因为tcp天生有拥塞的缺点。其实这句话并不全对。考虑如下场景:

  1. 网络情况很好。

  2. 客户端先用socket发送一组数据a,2s以后发送数据b。

  3. 服务端收到数据a 然后开始处理数据a,然后收到数据b,开始处理数据b(这里当然是开线程做)

  4. 此时服务端处理数据b的线程将数据b处理完毕以后开始将b的 responseb发到客户端,过了一段时间以后数据a的线程终于把数据处理完毕也将responsea发给客户端。

  5. 在发送responseb的时候,客户端甚至还可以同时发送数据c,d,e。

上述的通信场景就是完美诠释tcp作为全双工传输的能力了。相当于客户端和服务端是有2条传输信道在工作。所以从这个角度上来看,tcp不是导致http 协议 “队头拥塞”的根本原因。因为大家都知道http使用的传输层协议是tcp. 只有在网络