本文来自作者
大闲人柴毛毛
在
GitChat
上分享「
轻松几招你也可以架构高性能网站
」,
「
阅读原文
」查看交流实录
「
文末高能
」
编辑 | 莫兰迪
1. 了解衡量网站性能的指标
1.1 服务器如何发送数据?
-
服务器程序将需要发送的数据写入该程序的内存空间中;
-
服务器程序通过操作系统的接口向内核发出系统调用;
-
系统内核将用户态内存空间中的数据复制到内核缓冲区中去,然后通知网卡过来取;此后 CPU 转而做其他处理;
-
网卡到CPU指定的内核缓冲区中将数据复制到网卡缓冲区中;
-
网卡将字节转换成二进制位,再以电信号的形式输出至网络。
注意:
数据在计算机内部的复制是按照总线的宽度来复制的。比如在32位的操作系统中,数据每次都复制32位。
总线就像是一条32/64车道的马路,数据在计算机中是以0/1的形式存储,每次复制每条车道只能走一个0/1,因此每次只能同时复制32个0/1.
1.2 数据在网线中的速度
网络传输介质有光缆和铜缆,在光缆中电信号的传输速度为 2.3x10^8m/s,在铜缆中传输速度为 2.0x10^8m/s。
光的传播速度为 3.0x10^8m/s,但由于光缆采用反射机制传播,并不是直射,因此电信号实际走的路程要比直线长很多,因此在光缆中的传播速度只有2.0x10^8m/s。
1.3 什么是带宽?
带宽的定义
带宽的定义:数据的发送速率。
带宽的单位
100Mbps = 100M bit per second,平时所说的100M带宽指的是100M比特每秒,100Mbps = 12.5MBps。
注意
:我们平时所说的“100M”指的是100MB,而带宽的单位是Mb,而1MB = 8Mb。因此,运营商所说的“百兆宽带”其实是“12.5兆宽带”,呵呵。
什么影响了数据发送速度(带宽)?
-
数据的发送速度由接收方的接收速度决定。在数据链路层中,为了确保数据在接收过程中不发生丢失,因此接收方要告诉发送方目前发送速度是否合理。
若接收方来不及收,就会告诉发送方,让它慢点发。
因此,数据的发送速度(即带宽)由接收方的接收速度决定。
-
与传播介质的并行度有关。传输介质可以看成是多车道马路,数据由0/1组成,每股车道每次只能容纳一个0/1。
因此,如果马路的车道增多,那么每次发送的0/1也就增多,从而提高了发送速度(即带宽).
运营商为什么要限制带宽?
我们的服务器会通过一个交换机连入互联网,互联网由无数个路由器和主机构成,路由器负责数据包的存储转发,将数据包根据目的地址途径一个个路由器,最终投递到目的主机中。
由于一个交换机往往有多个服务器接入,服务器们都会将需要发送的数据首先发给交换机,再由交换机发给路由器,这些数据先存储在路由器的缓存中,然后路由器根据先后顺序逐个转发。
所以,如果服务器发送数据的速度过快,路由器缓存满了,那接下来的数据就会丢失,因此需要限制服务器向路由器发送数据的速度,即限制服务器的带宽。
而这个限制由接入服务器的交换机完成。通过上文可知,交换机只要控制接收速度,就能限制服务器的发送速度。
什么是共享带宽?什么是独享带宽?
-
独享带宽
如果一个路由器的出口带宽为 100Mbps,并且同一个广播域内有10台主机,交换机只要将每台主机的最大出口带宽限制为10Mbps,那么不管在任何情况下每台主机的最大出口带宽为10Mbps。
这就是独享带宽。独享带宽不会受到同一个广播域内其他主机的影响,任何时候最大出口带宽均为10Mbps。
-
共享带宽
假设一个路由器的出口带宽仍为100Mbps,但运营商为了挣更多钱,使同一个广播域内有多于10个主机接入,那么每台主机的平均最大带宽就小于10Mbps。
此时即使交换机仍然将每台主机的最大出口带宽限制为10Mbps,但当主机都有较大的网络通信时,就无法保证每台主机都有10Mbps的最大带宽,此时就会相互竞争带宽。
综上所述,独享10M带宽能保证服务器的最大出口带宽在任何情况下都为10Mbps,不会受到同一广播域内的其他主机影响;
而共享 10M 带宽只能保证在同一广播域内的其他主机通信空闲时,才能达到最大 10Mbps 的出口带宽。
1.4 什么是响应时间?
响应时间是指从数据包的第一个0/1离开服务器开始,到最后一个0/1被客户端接收为止的这段时间。
响应时间 = 发送时间+传输时间+处理时间
-
发送时间:从发送数据包的第一个0/1开始,到发完最后一个0/1为止的这段时间。
发送时间=数据包比特数/带宽
-
传输时间:数据在通信线路中的传输时间。
传输时间=传输距离/传输速度
(传输速度近似为 2x10^8m/s)
-
处理时间:数据在各个路由器中存储转发的时间。
处理时间比较难以计算。
响应时间=(数据包比特数/带宽)+(传输距离/传输速度)+处理时间。下载速度=数据的字节数/响应时间。
1.5 什么是吞吐率?
-
吞吐率:服务器单位时间内处理请求的个数。
-
单位:reqs/s
吞吐率用来衡量服务器处理请求的能力。
当请求非常少的时候吞吐率并不高,因为此时服务器的性能还没有体现出来。那么随着请求的不断增多,吞吐率会随之上升。
但当并发请求数上升到某一个临界点时,吞吐率不升反降。那个临界点就是服务器吞吐率的最大值,也叫最大吞吐率。
若我们的网站有促销活动前,可以通过上述方法来估计服务器的最大吞吐率,从而能判断服务器能否顶住促销带来的压力。
1.6 什么是并发数?什么是并发用户数?
要搞清楚并发数和并发用户数的区别,首先需要了解 HTTP 协议。
HTTP 协议是一种应用层协议,它本身是无连接的,也就是客户端与服务器每完成一次数据交互就需要断开连接,下次通信时重新建立连接。
但是 HTTP1.1 中有一个 keep-alive 字段,它使得通信双方在完成一次通信后仍然保持一定时长的连接。
若该时间内客户端又想与服务器通信,那么无需再创建新的连接,只需重用刚才的连接即可,这样能提高通信的效率,减少额外的开销。
注意
:现在的浏览器支持多线程,可以同时与服务器建立多个 TCP 连接,因此一个用户可能会导致多个并发用户数。所以“并发用户数”和“用户数”不能完全画等号,这点需要注意!
1.7 平均请求等待时间 和 服务器平均请求处理时间
平均请求等待时间:
用户从点击一个按钮,到新的页面加载完毕所需的时间。
服务器平均请求处理时间:
服务器从等待队列中取出一个请求开始,到处理完该请求所需的时间。
综上所述:
平均请求处理时间是站在用户角度,是用来衡量用户体验的好坏的指标。
而服务器平均请求处理时间是衡量服务器性能好坏的指标,其实就是吞吐率的倒数。
注意:
平均请求等待时间 和 服务器平均请求处理时间不成正比关系!
由此可知,在请求数很少的情况下,浏览器发来的请求无需等待,直接被服务器处理,那么请求等待时间和服务器请求处理时间成正比关系;
但在请求异常多的时候,请求到来速度远远大于服务器处理请求的速度,那么很多请求将会在等待队列中挤压,此时即使服务器处理请求的能力很强(即服务器平均请求处理时间很短),但用户的等待时间依然很长,此时用户等待时间与服务器请求处理时间不成正比。
1.8 使用Apache Bench进行压力测试
我们使用 Apache 服务器的 Apache Bench (简称ab)对网站进行压力测试。
ab简单易用,关键可以直接在服务器本地发起测试,这样我们可以获取不包括传输时间的服务器处理时间。通过服务器处理时间就可以知道服务器的性能。
1. 压力测试命令
ab -n100 -c10 http://www.acmcoder.com/index.php
2. 测试结果解析
Server Software: openresty #服务器软件
Server Hostname: www.acmcoder.com #测试的网址
Server Port: 80 #访问的端口号
Document Path: /index.php #测试的网页
Document Length: 162 bytes #HTTP响应信息的正文长度
Concurrency Level: 10 #并发用户数
Time taken for tests: 1.497209 seconds #测试所花费的时间
Complete requests: 100 #总请求数
Failed requests: 0 #失败的请求数(响应码非2xx的请求由Non-2xx responses记录)
Write errors: 0
Non-2xx responses: 100 #HTTP响应头中状态码非2xx的响应个数
Total transferred: 32400 bytes #总的响应数据长度,包括HTTP响应的头和正文数据,但不包括请求数据。
HTML transferred: 16200 bytes #HTTP响应中正文数据的长度。
Requests per second: 66.79 [#/sec] (mean) #吞吐率
Time per request: 149.721 [ms] (mean) #用户平均请求等待时间
Time per request: 14.972 [ms] (mean, across all concurrent requests) #服务器平均请求处理时间
Transfer rate: 20.71 [Kbytes/sec] received #服务器的数据传输速度(在极限情况下该数据即为服务器出口带宽)
Connection Times (ms)
min mean[+/-sd] median max
Connect: 40 46 4.8 46 58
Processing: 41 46 5.0 46 58
Waiting: 40 46 4.9 45 58
Total: 81 92 9.7 92 116
Percentage of the requests served within a certain time (ms)
50% 92 #50%的请求在92毫秒内完成
66% 98
75% 99
80% 101
90% 107
95% 114
98% 115
99% 116
100% 116 (longest request)
2. 第一招——动态内容缓存
2.1 什么是动态内容缓存?
浏览器向服务器发送请求后,服务器会根据浏览器的要求做相应的处理(如:数据库操作),然后将处理后的结果注入 JSP 页面生成 HTML,最后将生成的 HTML 返回给浏览器显示。
我们知道,数据库读取操作是非常耗时的,如果能将每次请求中的数据库处理时间去掉,那服务器的相应速度将会大幅提升。
要实现这一点,我们就需要将常用的 HTML 页面事先生成好,当用户发出请求时,服务器只需从缓存中取出即可,无需再做数据库处理操作。
综上所述:事先生成 HTML 页面的技术称为动态内容缓存。
2.2 什么是“缓存命中率”?
缓存命中率 = 访问缓存的请求数/请求总数
缓存命中率是衡量缓存是否有效的重要指标。如果将所有需要访问的数据均缓存起来,那么缓存命中率是100%。
但一半情况下,为了节约存储空间,只将访问频率较高的数据缓存起来,那么这样就会造成有一些请求能够通过缓存访问数据,而有一些请求需要查询数据库访问数据。
我们没有办法确保缓存的命中率100%,但我们需要使用一些算法确保缓冲命中率较高。也只有当缓冲命中率较高的情况下,缓冲才能发挥它真正的价值。
PS:缓存与缓冲的区别?
这里顺便提一下“缓存”和“缓冲”的区别。
-
缓冲:缓冲是一块临时的存储空间,为了解决不同存储设备之间读写速率不匹配而产生的。
比如:内存的读写速度远远大于硬盘的读写速度。当内存中的数据向硬盘中写入时,内存会先将数据写入内存缓冲区,再由内存缓冲区向硬盘写入数据。这样内存快速腾空之后就可以做其他事了,提高了效率。
-
缓存:缓存是在一些为了避免重复计算,而被暂时性存储的数据。
综上所述:缓冲=缓冲区,它是指内存中的一片区域;而缓存指的是数据,一些味了避免重复计算而被临时性存储的数据。
它们都是为了提高处理效率而存在的,都采用了“牺牲空间换取时间”的思想。
2.3 缓存的三种存储方式
缓存可以存储在三种不同的地方:
-
存储在内存
-
存储在IO设备
-
存储在内存和IO设备上
-
存储在缓存服务器上
对于小型网站,缓存首选的存储位置就是内存。内存相对于IO设备有较快的访问速度,能大大缩短缓存的读取时间。
但内存资源比较宝贵,如果没钱买大内存的服务器的话,那就只好考虑第二种办法,将缓存存在 IO 设备中。
对于一些小型网站,如果需要存储的缓存数据量比较大,而且买不起大内存的服务器的话,那存储在 IO 设备是不错的选择。
但 IO 设备相对于内存来说有个致命的缺陷,那就是慢!因此,对于资金不充裕的小型网站来说,第三种方式最为合适。
如果既想获得内存的读写速度,又想拥有IO设备的巨大存储空间,那就可以选择两者结合的方式。
内存中开辟一块固定大小的缓冲区,用于存储使用频率较高的缓存数据,而将使用频率较低的缓存数据存储至IO设备。
当缓冲区存满时,你需要使用 LRU 算法将最近最旧未使用的缓存从缓冲区中扔出去,而将最近一段时间 IO 设备中访问频率最高的缓存放进来,从而保证了当前缓冲区中数据的命中可能性是最大的。
最后,对于大型网站来说,可以使用专门的缓存服务器来存储缓存。这主要有两点好处:
-
节约服务器的内存资源,让服务器的内存作更多数据处理的工作,而把数据存储的工作交给缓存服务器;
-
缓存服务器具有更好的可扩展性。如果网站需要定期举办一些营销活动,为了应付急剧增加的并发量,缓存服务器是不二之选。
但是,由于 Web 服务器与缓存服务器采用 TCP 通信,而建立和释放TCP连接时间开销比较大,因此对于小型网站来说,直接将缓存存在内存无疑是种首选办法。
2.4 如何实现缓存?
这里就简单地介绍下基于内存的缓存机制,近期我会单独写一篇“基于内存+IO+LRU 算法的缓存机制”的博客,敬请关注。
服务器在接收客户端发来的请求后,首先根据请求的URL判断内存中是否有对应的缓存;若有的话先判断该缓存是否过期,若没过期就直接返回缓存数据;
若过期了,或者内存中根本就没有该请求的缓存的话,就调用业务逻辑层的相关函数,处理客户端发来的请求,生成客户端需要的数据。然后给生成的数据设置一个有效期,并存入缓存,供下次使用,最后将数据返回给用户。
2.5 静态化页面
之前我们介绍的缓存机制仍然需要服务器程序去判断存储空间中是否有缓存,而服务器程序判断的过程也需要时间,如果缓存很多的话,服务器程序需要找半天。
那么我们能否把静态页面事先都生成好,让浏览器直接访问静态页面,而无需再通过服务器程序访问缓存。
要知道,浏览器直接访问静态页面的速度比通过服务器程序访问静态页面的速度要快很多很多很多!
要实现这样的想法,我们可以这么做:
-
服务器程序启动的时候将生成所有需要访问的 HTML 页面,存储至服务器外设中;
-
浏览器所有 a 标签中的 href 都填写这些静态 HTML 页面 URL;
-
在服务器中开启一条定时线程,每隔一段时间检查动态数据发生修改的 HTML 页面,并生成新的 HTML 页面;
-
当数据库发生修改时,立即更新与该数据相关的 HTML 页面。
3. 第二招——浏览器缓存
3.1 什么是浏览器缓存?
在上一节中我们知道,当不同用户请求相同数据时,动态内容缓存能够避免服务器的重复计算,从而降低用户的等待时间。
但如果是同一个用户请求相同的数据,即使服务器能避免重复计算,但仍需将重复的数据传递给浏览器。
若使用浏览器缓存,则同一个用户请求相同的数据时,浏览器只需从自己本地读取,无需从服务器上获取。从而大大降低用户的等待时间,减少了服务器的压力,可谓一箭双雕!
3.2 浏览器是如何处理缓存的?
是否启用浏览器缓存是通过 HTTP 协议控制的。
浏览器在接收服务器传来的页面后,会将页面存入本地缓存。如果响应头中包含了页面的过期时间,那么当用户请求相同的页面时,浏览器会询问服务器是否可以使用缓存页面。
若服务器同意使用缓存,则返回 304 状态码;若服务器不同意,则将新的页面返回给浏览器,并携带 200 状态码。
此外,如果响应头中未包含启用浏览器缓存属性的话,浏览器仍然会缓存当前页,但下次请求相同页面时浏览器会直接向服务器请求新的页面,而不会询问是否使用缓存页面。
3.3 如何实现浏览器缓存?
1. last-modified
如果服务器向浏览器返回的响应头中包含 last-modified 属性,那么浏览器会将该属性与页面一起存入本地缓存。
当用户请求相同的页面时,浏览器发送的请求头中会携带属性:
If-Modified-Since:XXXXX
服务器会根据该值判断是否可以使用缓存页面,若可以使用缓存,则返回304状态码,若不可以使用缓存,则返回最新的页面,并携带200状态码。
注意:如果服务器的响应头中未包含 last-modified 属性,那么用户在请求相同页面时,浏览器中也不会包含 If-Modified-Since:XXXXX 属性,从而服务器也不会判断是否使用浏览器缓存,而是直接返回页面。
2. 采用ETag属性
ETag 属性和 Last-Modified 属性类似。ETag 属性值是一串字符串。
若服务器的响应头中包含了 ETag 属性,那么浏览器会将页面和 ETage 属性值一起缓存;
当用户请求相同的页面时,浏览器会读区缓存的 ETag 值,并作为请求头的 If-None-Match:”xxxxx” 属性发送给服务器;
服务器收到该属性后,判断是否允许浏览器使用缓存页面,若允许则返回304状态码,若不允许直接返回新的页面,并携带200状态码。
3.4 如何彻底实现浏览器缓存?
刚才的浏览器缓存还会涉及到浏览器与服务器的通信,因为浏览器需要向服务器询问是否使用本地缓存,而这些通信仍然需要消耗较多的用户等待时间。能否避免浏览器的这种询问呢?
1. 采用 Expires 属性
Expires 属性表示过期时间。
服务器只需在响应头中添加 Expires 属性,浏览器会将该属性与页面一起缓存。
当用户再次请求相同的页面时,浏览器会将该页面的 Expires 与当前系统时间进行比较,判断是否过期;若尚未过期,则直接使用本地页面。
但是,如果用户的本地时间是错误的,那么Expires属性无法发挥它的作用,此时需要使用 Cache-Control 属性。
2. 采用 Cache-Control 属性
Cache-Control:max-age=3600 表示页面从当前时间开始3600秒后过期。从而能解决用户本地时间错误的问题。
3.5 浏览器缓存的优点
-
浏览器缓存能够大大降低(甚至消除)服务器的网络IO,从而服务器能够租用更廉价的带宽。
-
浏览器缓存能减少(甚至消除)服务器查询缓存的操作、数据库操作,从而减小服务器压力,提高并发数。
4. 第三招——反向代理缓存
4.1 什么是反向代理?
在介绍“反向代理”之前,首先要介绍一下“正向代理”的概念。
1. 什么是正向代理?
在 NAT 技术(Network Address Translation)出现之前,所有主机无法直接与外网相连,要想上网,需要连接到一台能够访问外网的 Web 服务器,再通过这台服务器访问外网。而这台 Web 服务器就叫做“正向代理服务器”。
现在的“翻墙”技术也是如何,我们把请求发给一台可以连接外面世界的 Web 服务器,由它转发我们的请求,再将结果返回给我们。这台 Web 服务器就是“正向代理服务器”。
综上所述:正向代理服务器是客户端和目的服务器之间的一个中介,客户端通过正向代理服务器访问客户端原本无法访问的目标服务器。
2. 什么是反向代理?
客户端向一个服务器A提交请求后,服务器A偷偷地去服务器B上获取资源,并返回给客户端。
客户端天真地以为数据是服务器A给他的。在这过程中,服务器A称为“反向代理服务器”,服务器B称为反向代理服务器的“后端服务器”。
3. “正向代理”与“反向代理”的区别?
两者最直观的区别是在用户的角度。“正向代理”是用户使用的技术。用户首先是知道自己要访问的目标服务器是谁,但由于某种原因无法直接访问该目标服务器,因此选择使用正向代理服务器帮忙转发请求。
而“反向代理”是服务器使用的技术。用户向服务器发送请求后,服务器在用户不知情的情况下去其他服务器上获取资源并返回给用户。
4.2 什么是反向代理服务器?
反向代理服务器用于存储静态数据和缓存数据,它处于Web服务器之前。当用户发起请求时,请求首先被反向代理服务器截获,若请求的是静态数据或缓存数据,则反向代理服务器直接将数据返回;
若请求的是动态数据,且缓存中不存在,则反向代理服务器将请求转发给后端的Web服务器,在获取后端服务器的数据后再返回给用户。
4.3 反向代理服务器有何作用?
反向代理服务器能够分担后端服务器的压力。在请求数很高的情况下,即使服务器使用了缓存,但仍然无法应对巨大的并发数,因此需要反向代理服务器的帮忙。
反向代理服务器收到请求后,如果请求的是缓存数据或静态数据,则直接返回给用户,而无需再劳驾后端服务器了,从而缓解后端服务器的压力。
4.4 如何使用反向代理缓存?
1. 如何选择反向代理服务器?
反向代理服务器有多种选择,可以使用 Nginx 的反向代理模块,但它毕竟是 Nginx 的一个插件,功能不够全面。
Squid 是一个缓存服务器,除提供反向代理外还拥有其他功能,但过于重量级,历史也比较悠久,性能不咋地。
Varnish 是一款专门用于反向代理的服务器,相对于 Squid 较为轻量,由于使用内存缓存,因此性能较好,但也收到了内存的存储容量的限制。
究竟哪一个反向代理服务器适合你,可以参考
:
varnish / squid / nginx cache 有什么不同?
。
这里我们以 Varnish 为例。
2. 搭建Varnish服务器
-
下载安装 Varnish
首先你需要去 Varnish 官网下载并安装 Varnish,这里不做详细介绍了。
-
配置 Varnish 的后端服务器的IP和端口号
backend default{
.host = "127.0.0.1";
.port = "80";
}
-
启动 Varnish
在启动时,你需要指定如下参数:
-
Varnish 对外的端口
-
Varnish 命令行操作的端口
-
缓存空间的大小
PS:Varnish 会采用一种类似于 Mysql Innodb 的存储引擎来存储缓存数据。
3. 自定义Varnish的缓存规则
Varnish 采用 VCL(Varnish Configuration Language) 来让我们自定义缓存规则。
Varnish 将反向代理的处理过程分为多个阶段,每个阶段都会触发对应的函数,我们可以在这些函数中配置具体的缓存处理策略。下面对常用规则的配置进行介绍:
(1)vcl_revc函数
sub vcl_revc{
if(req.request!="GET" &&
req.request!="POST" &&
req.request!="PUT" &&
req.request!="HEAD" &&
req.request!="OPTIONS" &&
req.request!="DELETE" &&
req.request!="TRACE"){
return(pipe);
}
if(req.request!="GET" &&
req.request!="HEAD"){
return(pass);
}
if(req.http.Authorization || req.http.Cookie){
return(pass);
}
return (lookup);
}