构建高性能web站点--读书大纲
用户输入你的站点网址,等了半天。。还没打开,裤衩一下就给关了。好了,流失了一个用户。为什么会有这样的问题呢。怎么解决自己站点“慢”,体验差的问题呢。
在这段等待的时间里,到底发生了什么?事实上这并不简单,大概经历了以下几部分时间:
数据在网络上传输的时间
站点服务器处理请求并生成回应数据的时间
浏览器本地计算和渲染的时间
数据在网络上传输的时间总的来说包括两部分,即浏览器端主机发出的请求数据经过网络到达服务器的时间,以及服务器的回应数据经过网络回到浏览器端主机的时间。这两部分时间都可以视为某一大小的数据从某主机开始发送一直到另一端主机全部接收所消耗的总时间,我们称它为响应时间,它的决定因素主要包括发送的数据量和网络带宽。数据量容易计算,但是究竟什么是带宽呢?我们将在后续章节中详细介绍带宽的本质。
站点服务器处理请求并生成回应数据的时间主要消耗在服务器端,包括非常多的环节,我们一般用另一个指标来衡量这部分时间,即每秒处理请求数,也称吞吐率,注意这里的吞吐率不是指单位时间处理的数据量,而是请求数。影响服务器吞吐率的因素非常多,比如服务器的并发策略、I/O模型、I/O性能、CPU核数等,当然也包括应用程序本身的逻辑复杂度等。
浏览器本地计算和渲染的时间自然消耗在浏览器端,它依赖的因素包括浏览器采用的并发策略、样式渲染方式、脚本解释器的性能、页面大小、页面组件的数量、页面组件缓存状况、页面组件域名分布以及域名DNS解析等,并且其中一些因素随着各厂商浏览器版本的不同而略有变化。
可见,一个页面包含了若干个请求,每个请求都或多或少地涉及以上这些过程,假如有一处关键环节稍加拖延,整体的速度便可想而知。
1.增加带宽
“带宽”也许是计算机科学中最幽默的一个词,当我向一些开发者询问到底什么是带宽的时候,他们的回答总是让我联想到类似高速公路的宽度,而当我继续询问“我们的带宽有多宽”时,他们就会不知所措。
另一部分开发者显然知道带宽的单位是“bit/s”,也就是单位时间的比特数,所以他们将带宽解释为数据在线路中的移动速度,也就是将带宽的高低视为线路能力的强弱,那么很显然他们认为光纤对数据的传播能力大于铜线,但很可惜,事实上这是错误的。顺便说一下,我们一般常说的比如100M带宽,全称应该是100Mbit/s,或者100Mbps,后边的“bit/s”经常省略。
说到数据的发送,也就是数据从主机进入线路的这段旅程,一般需要经过以下几个环节:
1.应用程序首先得将要发送的数据写入该进程的内存地址空间中,熟悉网络编程的开发者对这个环节一定非常熟悉,通常在程序开发中这只需要一般的运行时变量赋值即可。
2.应用程序通过系统函数库接口(比如send函数)向内核发出系统调用,由系统内核来进行随后的操作,它将这些数据从用户态内存区复制到由内核维护的一段称为内核缓冲区的内存地址空间。这块地址空间的大小通常是有限的,所有要发
送的数据将以队列的形式进入这里,这些数据可能来自于多个进程,每块数据都有一定的额外记号来标记它们的去向。如果要发送的数据比较多,那么该系统调用需要多次进行,每次复制一定的数据大小,这个大小取决于网络数据包的大小以及内核缓冲区的承载能力。重复的系统调用体现在应用编程层面重复调用send函数。
3.当数据写入内核缓冲区后,内核会通知网卡控制器前来取数据,同时CPU转而处理其他进程。网卡控制器接到通知后,便根据网卡驱动信息得知对应内核缓冲区的地址,将要发送的数据复制到网卡的缓冲区中。注意在以上一系列的数据复制中,数据始终按照连接两端设备的内部总线宽度来复制,也就是字节的整数倍,比如在32位总线的主机系统中,采用PCI-X总线接口的网卡一般使用32位总线宽度,那么从内核缓冲区到网卡缓冲区的数据复制过程中,任何时刻只能复制32位的比特信息。
4.网卡缓冲区中的数据需要发送到线路中,同时释放缓冲区来获取更多要发送的数据。但是我们知道,只有二进制的数字信号才可以在线路中传输,所以这时候需要对数据进行字节到位的转换,这种转换不难想象,就是将数据的每个位按照顺序依次发出。
5.发送时,网卡会使用内部特定的物理装置来生成可以传播的各种信号,比如在使用铜线线路时,网卡会根据“0”和“1”的变化产生不同的电信号;而使用光纤线路时,网卡会产生不同的光信号。
在大多数情况下,我们都将Web站点服务器托管在IDC,通过将其连接到某个交换机,从而接入互联网。这时候,我们的服务器拥有自己的IP地址,当站点用户通过互联网向这台服务器请求数据后,数据从服务器流经交换机到达指定的路由器,这个过程需要交换机的存储转发机制,也就是交换机从连接服务器的端口接收数据,存储到交换机内部的高速缓冲区队列中,然后将其从连接路由器的端口发送出去,再经过路由器的转发,进入另一个网络,接下来依次重复这些过程,直到进入站点用户的PC。
如果全世界只有你的服务器和你的用户在传输数据,那么这部分数据流经的每个交换节点都会全心全意地做好转发工作,此时你的数据在各节点转发的发送速度都可以达到理论上设备所能达到的最大值。但实际上每处交换节点都有可能同时转
发来自其他主机的数据,包括你的数据在内的所有数据都汇集进入路由器的转发队列,路由器按照转发队列中的顺序来交错地发送这些来自不同主机的数据,所以单从来自不同主机的数据而言,其转发时的发送速度必定小于所有从路由器转发出去的数据的发送速度(即该交换节点的出口带宽)。
因为带宽是有限的,它毫无疑问是个抢手资源,而且互联网运营商也不会白白搭建网络,所以运营商在所有的基础交换节点上设置关卡,也就是限制数据从你的主机流入路由器转发队列的速度,而只要流入路由器转发队列的数据,都会按照路由器的出口带宽,流入其他网络。
这种关卡设置实际上等于限制了你的主机发送数据的速度,也就是限制了主机的出口带宽,而至于这种限制的实现机制,我想你已经很清楚了,那就是通过限制交换机对于你主机的数据接收速度,来将你的发送速度牢牢控制在手,因为数据链路层的流量控制是通过控制接收方来实现的。对于交换机的限速设置,IDC的网管非常擅长。
一切都清楚了,下面我们来看看两个被交换机限制了带宽的主机,它们都安装了MRTG,可以生成网卡流量报告单,不过我们在这里关心的不是流量,而是报告单顶部的一段信息,请注意这里的Max Speed属性值,它便是从交换机接收端口获得的最大接收速度,同时也是该主机的最大数据发送速度,但并不一定是此刻的实时发送速度,因为每时每刻的发送速度都是传输协议根据接收方的接收能力不断调整的,比如通过数据链路层或者传输层的滑动窗口技术等流量控制机制进行速度的调整。
如表2-1所示,这台主机使用了独享10M带宽,也就是10Mbit/s的数据发送速度,换算成字节的话,正好就是上面的1250.0kBytes/s。在这种情况下,如果路由器的出口带宽为100M,交换机的设置应该保证来自广播域内其他主机的数据发送速度总和不超过90Mbit/s,以保证该主机任何时刻都可以以10Mbit/s的速度发送数据,这才叫独享带宽,它独享的是路由器的一部分出口带宽,而不是交换机的带宽,因为交换机本来就是各个端口独享带宽而互不影响。如果这台交换机还为其他主机提供独享10M带宽的接入,并且路由器的出口带宽为100M,那么理论上总共只能接入10台主机,这样才可以保证每台主机的实际带宽总是10M。假设带宽运营商为了多赚钱,给该交换机上接入了20台主机,然后对每台交换机仍然都限制了10M带宽,这时候从MRTG的Max Speed属性上仍然看到的是1250.0 kBytes/s,但是你可以观察MRTG流量图或者使用Nmon实时流量监控来分析自己的10M带宽是否真的有所保证。
不论是大文件下载,还是网页、图片、样式表的下载,其下载速度都是站点用户最关心的指标,也是用户唯一能体验到的站点性能,所以如果能估算出各地用户的下载速度,并根据它来决策服务器的位置和带宽,是非常有意义的。在通常情况下,我们很清楚也很容易计算传输的数据量大小,所以只有搞清楚响应时间的计算方法,才可以计算出下载速度。
通过前面的介绍,我们了解了互联网上两台主机之间数据发送和传输的整个过程,事实上,数据的响应时间不难得出:响应时间 = 发送时间 + 传播时间 + 处理时间
发送时间很容易计算,即“数据量/带宽”。比如要发送100Mbit的数据,而且发送速度为100Mbit/s,也就是带宽为100M,那么发送时间便为1s。值得注意的是,在两台主机之间往往存在多个交换节点,每次的数据转发,都需要花费发送时间,那么总的发送时间也包括这些转发时所花费的发送时间。
传播时间主要依赖于传播距离,因为传播速度我们可以近似认为约等于2.0×108m/s,那么传播时间便等于传播距离除以传播速度。比如两个交换节点之间线路
比如两个交换节点之间线路的长度为1 000km,相当于北京到上海的直线距离,那么一个比特信号从线路的一端到另一端的传播时间为0.005s。
处理时间是什么意思呢?其实在之前的介绍中,虽然没有提出这个概念,但是已经包含了对其本质的介绍。简单地说,处理时间就是指数据在交换节点中为存储转发而进行一些必要的处理所花费的时间,其中的重要组成部分就是数据在缓冲区队列中排队所花费的时间,注意,准确地说应该是“你的数据”在队列中排队所花费的时间,因为在队伍中还有其他与你毫不相干的数据。此时,如果你想起在介绍带宽的时候我们提到的共享带宽,那就对了。
如果全世界只有你的服务器和你的用户在传输数据,那么用于排队的处理时间可以忽略。可见,处理时间的多少,取决于数据流经各交换节点所在网络的数据通信量,它往往是不可预测的,所以它的计算比较复杂,往往没有一个简单的数学计算公式,而是依赖于多变的外部因素,必须结合实际情况具体分析。
那么,我们可以将响应时间的计算公式整理为:
响应时间 = (数据量比特数 / 带宽) + (传播距离 / 传播速度) + 处理时间另外,下载速度的计算公式如下:
下载速度 = 数据量字节数 / 响应时间
有了以上的计算方法,下面我们还是在具体场景中试着来计算响应时间,注意,这里为了计算,我们暂时先忽略处理时间。
我们的站点用户绝不可能处于同一个互联网运营商的网络中,而事实上即使国内的互联网可以高速互联,如果我们的站点用户覆盖全球,并且要保证高速服务,那么跨国运营商互联和国际出口带宽依然是残酷存在的问题,幸运的是这些问题都可以抽象为同类问题来考虑。
归根结底,希望通过本节的介绍,可以让大家清晰地认识到响应时间和下载速度的本质和计算方法,而至于究竟将服务器部署在哪里的问题完全要通过大家自己的考察得出结论,比如选择IDC的时候要考察出口带宽以及与骨干网络是否直连,如果要同时为多个互联网运营商网络的用户提供服务,则需要考察出口节点与运营商互联节点的带宽,而如果要面向全球用户提供服务,则需要考察国际网络结构和各个国家的国际出口带宽等。另一方面,带宽作为稀缺资源,其价格严格服从市场供求规律,比如同样的独享带宽在北京就比沈阳贵很多,所以我们在选择的时候价格因素也相当重要。
2.减少网页中的http请求
将多个图片合并为一个文件,利用CSS背景图片的偏移技术呈现在网页中,避免了多个图片的下载。
合并JavaScript脚本或者CSS样式表。
充分利用HTTP中的浏览器端Cache策略,减少重复下载。
3.加快服务器脚本计算速度
我们知道,用脚本语言编写的程序文件需要通过相应的脚本解释器进行解释后生成中间代码,然后依托在解释器的运行环境中运行。所以生成中间代码的这部分时间又成为大家为获取性能提升而瞄准的一个目标,对于一些拥有较强商业支持的脚本语言,比如ASP.NET和JSP,均有内置的优化方案,比如解释器对某个脚本程序第一次解释的时候,将中间代码缓存起来,以供下次直接使用。
对于开源类的脚本语言,也有很多第三方组件来提供此类功能,比如PHP的APC组件等。使用这些组件进行脚本优化真的那么有用吗?不同的应用效果是否有所不同呢?
4.使用动态类容缓存
动态内容技术就像Web开发领域的一场工业革命,它带来了产业升级和Web开发者的地位提升,在过去相当长一段时间里,大家普遍认为一个站点的技术含量主要体现在后台的动态程序上,所以很多工程师都会带着虚荣心警告你:“请叫我后台开发工程师。”事实上这种概念和偏见已经开始逐渐被历史抛弃,但这不是我们此刻讨论的重点。
自动态内容技术产生后,聪明的工程师们为了减少动态内容的重复计算,想到了截取动态内容的胜利果实,将动态内容的HTML输出结果缓存起来,在随后的一段时间内当有用户访问时便跳过重复的动态内容计算而直接输出。
在实际应用中,动态内容缓存可能是大家使用得最多的技术,但是并不见得所有的动态内容都适合使用网页缓存,缓存带来的性能提升恰恰与有些动态数据实时交互的需求形成矛盾,这是非常尴尬的,而解决该问题的唯一途径不是技术本身,而是你如何权衡。
另一方面,缓存的实现还涉及了一系列非常现实的问题,即成千上万的缓存文件如何存储?缓存的命中率如何?缓存的过期策略如何设计?在拥有多台Web服务器的分布式站点上应用动态内容缓存需要考虑什么呢?
5.使用数据缓存
动态内容缓存是将数据和表现整体打包,一步到位,但就像快餐店里的组合套餐一样,有时候未必完全合乎我们的口味。当我们意识到在自己的站点中,某些动态内容的计算时间其实主要消耗在一些烦人的特殊数据上,这些数据或者更新过于频繁,或者消耗大量的I/O等待时间,比如对关系数据库中某字段的频繁更新和读取,这时我们为了提高缓存的灵活性和命中率,以及性能的要求,便开始考虑数据缓存。
更加细粒度的数据缓存避免了过期时大量相关网页的整体更新,比如很多动态内容都包含了一段公用的数据,如果我们将整个页面全部缓存,那么假如这段数据频繁更新导致频繁过期,无疑会使得所有网页都要频繁地重建缓存,这对网页的其他部分内容似乎很不公平。此时如何协调网页缓存和数据缓存呢?是否能够将它们一起使用并各显其能呢?
另外,将数据缓存存储在哪里呢?这需要考虑多方面的因素。速度是一方面,如果无法提供高速的读写访问,那么这部分数据缓存可能不久便成为新的系统瓶颈。另外,数据缓存的共享也至关重要,如同一主机上不同进程间的共享、网络上不同主机间的共享等,一旦设计不当,将对站点未来的规模扩展带来致命的威胁。
6.动态内容静态化
在动态内容缓存技术的实现机制中,虽然避免了可观的重复计算,但是每次还都需要调用动态脚本解释器来判断缓存是否过期以及读取缓存,这似乎有些多余,而且关键是消耗了不少时间。直接让浏览器访问这些动态内容的缓存不是更好吗?在这种情况下缓存成为直接暴露给前端的HTML网页,而整个缓存控制机制也发生了根本的变化,我们普遍称它为静态化,静态网页独立了,当家做主了,再也不用被脚本解释器呼来唤去。
7.更快的web服务器软件
从20世纪末开始影响全球经济的开源软件,不可否认给我们的生活带来了更多丰富的体验和选择,但是更多的选择也代表着更多的结局,不论结局是好是坏,我们都需要为此承担责任。
在Web服务器软件的选择问题上,很多架构师依然困惑,大量的压力测试对比数据蛊惑着激进的开发者和运维工程师,人们只关注所谓的并发量冠军,却忽视了更加本质性的东西,甚至不了解眼前测试数据的潜在前提。社会总是这样的,象牙塔式的精英教育和残酷的淘汰机制断送了无数人才的未来。而这一次,错误的选择将要付出沉痛的代价。有人拿着所谓的测试数据说Apache已经过时,你相信吗?
也许下此结论为时尚早,尽管放弃它的人比比皆是,但是它的成功不是空穴来风,毕竟它已经活了很久了。
另一方面,你正在使用的Web服务器软件也许让你无比自豪,可是你知道那些复杂的参数配置背后的本质吗?你知道为什么它仅仅在处理你的站点请求时如此出色吗?如果让你自己编写Web服务器软件,你可以让它更快一些吗?
我们必须停止盲目的选择,停止对表面现象的崇拜,我们需要学习一些稍显底层的知识来武装自己。
8.页面组件分离
从某种角度看,中学校园里的快慢分班似乎合乎逻辑,虽然不一定合乎情理。快班的学生学习能力强,理解知识快,那么课程安排的节奏可以加快一些;慢班的学生则可以放缓课程安排的节奏,这样既互不影响,学校的升学率又可以得到保证,当然假设的前提是学生之间互相帮助效果不大。
在Web站点中,网页和各种各样的组件是否也需要“分班”呢?显然它们的下载量和对服务器的能力要求不尽相同,如果由同一台物理服务器或者同一种并发策略的Web服务器软件来统一提供服务,那势必造成计算资源的浪费以及并发策略的低效。所以,分离带来的好处是显而易见的,那就是可以根据不同组件的需求,比如下载量、文件大小、对服务器各种资源的需求等,有针对性地采用不同的并发策略,并且提供最佳的物理资源。当然,如果你的站点基本无人问津,而且服务器的各种资源大量闲置,那么自然不存在什么性能问题,也不需要什么组件分离。但是如果你的站点负载已经让你意识到组件分离是大势所趋,那还是趁早动手。
9.合理部署服务器
真让人发疯,互联网为什么不是只有一个,你也许会说难道不是只有一个Internet吗?是的,但是Internet所特指的“互联网”是某种文化意义上的名词,同一个世界,同一个梦想,同一个互联网。而我这里说的互联网,则是指由某互联网运营商负责搭建的一系列网络节点,它覆盖的地域有大有小,接入这些网络节点的局域网也可以相互通信,同时这些互联网之间也能够通过骨干线路互联互通。
世界上很多国家都有不止一个互联网运营商,中国的互联网运营商想必大家都非常熟悉,当你在家中安装宽带或者需要托管服务器的时候,都面临着运营商选择的问题,包括电信、网通、铁通、移动在内的几大国内运营商让你很头疼。特别是在部署Web站点各类服务器的时候,是否能够找到合理的位置部署服务器至关重要。
我们都知道,在基于IP寻址的互联网中,IP地址相近的主机之间通信,数据经过少数的路由器即可到达,比如同一局域网内通信或者接入同一个城市交换节点的局域网之间通信,在这种情况下数据到达时间相对较短。
而如果通信的两端主机位于不同运营商的互联网中,那么数据必须流经两个互联网运营商的顶级交换节点和骨干线路,在这个过程中可想而知数据要经过更多次的存储转发,而且各互联网顶级交换节点之间又存在出口带宽的限制,如果互联网之间数据通信量比较大的话,那么这个顶级交换节点,也就是“出口”,将会是瓶颈所在,就像连接两座城市之间的高速公路,当大量汽车需要频繁地往返于两座城市时,高速公路出现车流缓慢,那么汽车从一个城市到另一个城市的总体时间加长了。
显而易见,我们当然希望Web站点的用户和服务器位于同一个互联网运营商的网络内,但如何实现呢?
10.负载均衡
到此为止,我们已经最大程度地发挥了单台Web服务器的处理能力,但是,当它所承受的压力达到极限时,就需要有更多的服务器来分担工作,我们需要想办法将流量合理转移到更多的服务器上。
为此,我们需要通过各种不同的方法来实现Web负载均衡,可能是简单的HTTP重定向,或者是基于DNS的轮询解析,或者通过反向代理服务器来实现负载均衡调度,还可以通过LVS来组建服务器集群,它们有什么区别呢?无论如何,透过这些具体的实现方法,我们更加关心的是能否真正地均衡调度请求,以及是否具备高可用性,还有影响规模扩展的制约因素.
11.优化数据库
对于使用数据库的Web站点来说,你是否在性能优化时或多或少地忽视了数据库的存在?往往一些性能问题可能都发生在表现不佳的数据访问层面,这来源于不合理的应用程序数据访问组件设计、不合理的数据库表结构设计以及对于数据库内部构造缺乏深入的了解。毫不夸张地说,也许你之前的优化全都白干了。
Web服务器与数据库服务器的数据通信一般基于标准的TCP,即便它们位于同一台物理主机也是如此。其通信连接的建立和释放涉及代表一段内核高速缓冲区的文件描述符的创建和销毁,这需要不少的时间开销,包括系统调用导致的内核态切换以及某些异步阻塞I/O模型采用的文件描述符队列扫描机制。所以,频繁的数据库连接和释放无疑将导致数据访问等待时间的加长,这段时间浪费得毫无意义。
使用数据库持久连接有效地解决了这一难题,它包括不同程度上的持久化,本质的区别在于持久连接的应用范围和生命周期,比如某个进程内部的全局数据库连接,供进程内所有计算任务共享,在这个进程终止后便被释放;或者在某个动态内容的执行周期内,代码层面的持久连接对象,在动态内容计算结束后便不复存在;还有跨进程的数据库连接池,保存多个持久连接供应用程序重复使用。在这些采用数据库持久连接的应用设计中,同时还要注意保证数据访问的线程安全性。
与此同时,在设计关系数据库的表结构时,你是否合理使用了各种类型的索引呢?要做到这一点,你必须了解索引的有关知识,然而更重要的是如何根据Web站点变幻莫测的数据访问特点来有针对性地设计每个表的索引,这往往也是最有难度的,索引的合理使用对于依赖数据库访问的Web应用至关重要。
另外,你了解数据库存储引擎的特性吗?其实这并不困难,因为所有的主流数据库文档中都有详细介绍,但是究竟你的Web站点应该选择什么存储引擎呢?当然,没有绝对完美的方案,我们在这个世界上要做的唯一的事情就是不断进行取舍,像考虑索引一样去弄清楚存储引擎的本质,是绝对不会让你失望的。
随着时间的推移,你的Web站点可能逐渐被数据库绑架,单台数据库服务器再也无法应付整个站点的需要,这包括存储空间以及查询时间,人们开始抱怨数据库模型的不良设计制约了横向扩展以及负载均衡,这不是我们希望看到的结果。为此,我们将数据散列在多台主机,包括必要的冗余数据,以此来合理地分散数据库的密集访问,数据库扩展便成为我们考虑的方案。
12.考虑可扩展性
对于Web站点的可扩展性讨论已经屡见不鲜了,不论是代码层面的扩展,还是架构层面的扩展,涉及的内容非常多,究竟我们应该从何谈起呢?这是一个值得深思的问题。缺乏良好的可扩展性设计就像慢性自杀或者等待死亡,这甚至比Web站点所能遇到的其他一切困难更让人头疼,因为扩展对于我们来说,就像在山穷水尽的时候被指引了一条星光大道,一旦扩展都无法进行,那真是死路一条。
的确,可扩展性并不是性能和速度的概念,它是指当系统负载增大时,通过增加资源来提高性能的能力。高性能往往需要通过这种能力来实现快速扩展,几乎没有多少团队可以在一个星期内通过增加服务器马上让服务能力扩容100倍。另一方面,可扩展性的目的在于适应负载的变化,从扩展的技术实现上来看,又包含了很多对局部性能的思考,以及了解何时需要扩展,这离不开对站点性能的把握。
然而,就像“人的病都是吃出来的”一样,Web站点在成长的道路上不断吸收新的技术,然而每一次技术的应用不当,都可能引入一定程度上的不可扩展。但现实往往是矛盾的,我们不得不使用一些技术来构建Web站点,同时又使用一些技术来提升站点性能,这些技术构成了我们理想架构的一部分,关键在于在这些技术和架构的应用中,我们是否意识到可扩展性,并且能否正确评估可扩展性的需求。
13.减少视觉等待
实在不行就给用户一些提示吧!最后我只能这么说了,事实上,这不是什么大不了的事情,即使认识到架构的瓶颈并投入大量人力来改善,也不是一天两天就可以完成的,要意识到用户也许只是希望你不要不理他而已。
这部分显然已经超出了本书的讨论范围,它涉及人机交互的相关知识,并且充满着人文情怀,要真正做好它,恐怕要比本书中所有的问题都更有难度和挑战性,这毫不夸张,我们要承认这个现实,因为世界上最难的学问就是研究人,你觉得呢?
引用自《构建高性能web站点》