高性能Web应用的优化技术
前端优化
并发策略
缓存优化
数据库优化
如何构建一个高性能的Web应用, 其实是一个很大的话题。
之前在公司内部对此做过一个课题分享,将一些主流的优化技术罗列了一二,基于当时的PPT本文力图在广度方面延展开来,每个技术点若要深度挖掘需要大量的篇幅,待日后可系列写写。部分内容参考《构建高性能Web站点》
# 一个HTTP请求过程
一个完整HTTP的请求过程,大约可以抽象为下面一张图,相信比较容易看懂。
主要说下OSI七层网络那里有个第七层的DNS负载均衡,和第四层的TCP/UDP层负载均衡(通过第三层的IP及第四层的端口)
HTTP请求
经典问题《当你打开一个URL到底发生了什么?》
http://igoro.com/archive/what-really-happens-when-you-navigate-to-a-url/
OSI七层协议
# 前端相关优化技术
该章节的前端优化大部分时候不仅仅是前端,还包括后端服务的配合。故声明为前端“相关”优化技术。
1、浏览器缓存(强缓存 VS 协商缓存)
1.1、强缓存
强缓存是利用http的返回头中的Expires或者Cache-Control两个字段来控制的,用来表示资源的缓存时间。
Expires
该字段是http1.0时的规范,它的值为一个绝对时间的GMT格式的时间字符串,比如Expires:Mon,30 Jan 2017 23:59:59 GMT。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存。这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。
Cache-Control
该字段是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对时间,例如Cache-Control:max-age=3600,代表着资源的有效期是3600秒。cache-control除了该字段外,还有下面几个比较常用的设置值:
no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。
1.2、 协商缓存
协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问,这主要涉及到下面两组header字段,这两组搭档都是成对出现的,即第一次请求的响应头带上某个字段(Last-Modified或者Etag),则后续请求则会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modified或者Etag字段,则请求头也不会有对应的字段。
Last-Modify/If-Modify-Since
浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。
当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。
如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modify。
ETag/If-None-Match
与Last-Modify/If-Modify-Since不同的是,Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。
与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。
ETag VS Last-Modified
ETag实体标签(Entity Tag)的缩写,在HTTP 1.1协议中并没有规范如何计算ETag。ETag值可以是唯一标识资源的任何东西,如持久化存储中的某个资源关联的版本、一个或者多个文件属性,实体头信息和校验值、也可以计算实体信息的散列值。
ETag关心的是实体内容,Last-Modified 关心的是实体修改时间。
ETag的出现主要是为了解决几个Last-Modified比较难解决的问题:
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新请求;
某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
某些服务器不能精确的得到文件的最后修改时间。
Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
强缓存与协商缓存的区别可以用下表来表示:
缓存类型 | 获取资源形式 | 状态码 | 发送请求到服务器 |
---|---|---|---|
强缓存 | 从缓存取 | 200 (From Cache) | 否,直接从本地缓存取 |
协商缓存 | 从缓存取 | 304 (Not Modified) | 是,通过服务器响应来确定本地缓存是否仍有效 |
2、Keep-Alive 持久连接
Connection: Keep-AliveKeep-Alive: timeout=5, max=100
非KeepAlive模式时,每个请求/应答客户和服务器都要新建一个连接,完成之后立即断开连接;当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。
HTTP 1.0中默认是关闭的,需要在HTTP头加入"Connection: Keep-Alive",才能启用Keep-Alive;HTTP 1.1中默认启用Keep-Alive,如果加入"Connection: close "才关闭。
3、CDN缓存加速
CDN是什么,搞互联网应该都晓得。这里主要说下CDN的几个要点和注意事项:
CDN所缓存的内容类型包括web对象、可下载的对象(媒体文件、软件、文档)、应用程序和实时媒体流。
工作原理分为DNS解析方式和非DNS解析方式(嵌入SDK),其中前者可能会有DNS劫持问题,后者会有很多不兼容问题。很多视频播放软件用的就是非DNS方式
合理规划CDN的回源频率,既不对源站产生负担,也不会缓存失效
研究透彻CDN厂商的技术手册,比如某些响应header不缓存导致缓存失效等
4、GZip压缩
开启Gzip ,那么服务器端响应后,会将HTML/JS/CSS等文本文件或者其他文件通过高压缩算法将其压缩,然后传输到客户端,由客户端的浏览器负责解压缩、渲染及展现。
5、合并压缩多个 CSS/JS
将多个CSS 或者 JavaScript 文件合并压缩为一个小文件,减少HTTP请求次数,减轻网络流量提高响应速度。
以上,可以满足大部分的优化需求了,更多详情可参见Google & Yahoo的最佳实践。
更多详情可参见Google & Yahoo的最佳实践
https://developers.google.com/speed/docs/insights/rules
https://developer.yahoo.com/performance/rules.html
# IO 模型
计算机的重要工作之一便是负责各种设备的数据输入/输出,即Input/Output操作。
IO的类型根据设备的不同分为很多类型,比如内存IO,网络IO,磁盘IO。
内存IO大部分情况不是我们需要关注的瓶颈点,它的速度远远高于网络和磁盘IO,而后两者才是我们需要重点关注的。当然如果你的系统需要像LMAX那种需要把性能调优到极致的架构,对于内存甚至CPU缓存的使用都是需要优化的,比如Disruptor magic cache line padding。
我们所关注的IO操作主要是网络数据的接收和发送,以及磁盘文件的访问,这里我们归纳为多种IO模型,它们的本质区别就是CPU的参与方式:如何协调高速的CPU与慢速的IO设备。
PIO VS DMA
简单说下慢速IO设备和内存之间的数据传输方式。以磁盘为例,以前磁盘和内存之间的数据传输需要CPU控制,也就是如果读取磁盘文件到内存中,数据要经过CPU存储转发,需要占用大量的CPU时间来读取文件,造成文件访问时系统几乎停止响应,该模型当前已经基本没有了。
DMA(Direct Memory Access,直接内存访问)取代了PIO, 该模式下CPU只需要向DMA控制器下达指令,让DMA控制器通过系统总线来传送数据,传输完毕再通知CPU,大大降低了CPU占有率。
内存映射
内存映射是操作系统的提供的一种机制,可以减少这种不必要的数据拷贝,从而提高效率。它由mmap()将文件直接映射到用户空间,mmap()并没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,所以只进行了一次数据拷贝 ,比read进行两次数据拷贝(多经过一次内核缓冲区)要少一次,因此,内存映射的效率要比read/write效率高。
直接IO
Linux2.6中,内存映射和直接访问文件没有本质上的差异。
引入内核缓冲区的目的在于提高磁盘文件的访问性能。比如当需要读取磁盘文件,如果文件内容已经在内核缓冲区,那么就不需要再次访问磁盘;而写入磁盘文件,实际上用户进程只是写到了内核缓冲区便标志进程已经写成功,真正的写入磁盘是通过一定的策略延迟处理的。
Linux提供了绕过内核缓冲区的需求支持,在open()系统调用中增加参数选项 O_DIRECT,这样便避免了CPU和内存的多余开销。
同步 VS 异步
同步和异步关注的是消息通信机制。所谓同步,就是在发出一个 调用 时,在没有得到结果之前,该 调用 就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者 主动等待这个 调用 的结果。
而异步则是相反, 调用 在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在 调用 发出后,被调用者 通过状态、通知来通知调用者,或通过回调函数处理这个调用。
阻塞 VS 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
非阻塞IO一般指针对网络IO有效,比如接收网络数据的时候,如果网卡缓冲区中没有可接收的数据,函数就及时返回告诉进程没有数据可读了,而阻塞IO在这里就会阻塞住只到网卡缓冲区有数据可以处理。
对于 磁盘IO ,非阻塞IO无效果!!!
本文不详细分析常见的几种IO模型了(同步阻塞,同步非阻塞,多路IO复用,异步IO),大家可以自行了解。
同步阻塞IO(Blocking IO)
传统的IO模型。首先IO等待是不可避免的,那么既然有等待就会有阻塞。
请注意,我们说的阻塞指的是当前发起IO操作的进程被阻塞,并不是CPU被阻塞,事实上没有什么能让CPU阻塞,CPU只知道拼命的计算,对于阻塞一无所知。
# 负载均衡
对于Web应用的水平扩展能力,负载均衡是关键的一环。主要的技术有:
DNS负载均衡:OSI第七层(应用层),一个域名可以对应多个IP(但最终只能解析出唯一一个)。性能极佳,但不灵活,生效延迟。
反向代理负载均衡: OSI第七层(应用层),定制转发策略,分配流量权重等。
反向代理的功能远不仅于此:
HTTPS Offload / SSL Accelerator: SSL加解密处理,释放服务器处理
能力。
Session会话保持: Session Stikcy, Session Copy.
页面流量压缩: GZip
静态文件缓存: Nginx本地存储,减少服务器带宽负担
IP负载均衡: OSI第四层(传输层), 通过对IP数据包的IP地址和端口进行修改达到负载均衡目的。 常见应用LVS-NAT/DR
不能在第三层做负载: 因为软负载面向的对象应该是一个已经建 立连接的用户,而不是一个孤零零的IP包, 所以LVS是需要关心 「连接」级别的状态的。
反向代理:
# 服务器并发策略
所有到达服务器的请求本质上都是封装到IP包中(位于网卡接收缓冲区),Web服务器IO读取请求数据,进行CPU计算,然后IO写到发送缓冲区。服务器的并发策略就是让IO操作和CPU计算尽量重叠。
一个进程处理一个连接: prefork模式,安全独立进程,子进程崩溃不会影响服务器本身,但受系统进程数的限制,并发连接数相应的受限。
一个线程处理一个连接: worker 多线程+多进程混合方式,进程数较少则上下文切换消耗很大,此模式很少用
一个进程处理多个连接: 结合多路复用IO才能发挥威力。
进程数是不是越多越好?NO, 进程越多CPU的上下文切换成本越高
一个线程处理多个连接: 结合异步IO。线程读取的数据若不在缓冲区,则磁盘必须从物理设备读取数据,这时候整个进程都是阻塞的。那多线程也就没意义了。
# 缓存
缓存本质上就是避免重复计算,是一种牺牲数据的实时性以换取
效率的方式。更是互联网应用各个环节都避不开的技术手段,用好缓存技术可以让应用性能提高N个数量级,成为性能银弹大杀器,用不好会使性能急剧下降,成为巨坑。
缓存 Cache VS 缓冲 Buffer
二者都是临时存储,都具备改善系统 I/O 吞吐量的能力,但是这两个概念是有区别的。缓冲区,一个用于存储速度不同步的设备或优先级不同的设备之间传输数据的区域。通过缓冲区,可以使进程之间的相互等待变少,从而使从速度慢的设备读入数据时,速度快的设备的操作进程不发生间断。
业内经常会引用Phil Karlton的一句话:
There are only two hard things in Computer Science:
cache invalidation and naming things.
大神 Martin Fowler 在此基础上又加上了第三个难事
off-by-one errors(OBOE) 大小差一错误
关于缓存有如下几个关键指标或者说概念,而上面提到的缓存失效只是其一:
缓存命中(Cache Hits) : 该项指标的调优是个很大的课题,不管是数据库,还是CDN,亦或是Redis,都值得专门的章节来剖析。
没有万能的调优策略,只有符合场景的策略:
选择合理的缓存淘汰算法
缓存粒度越小,命中率越高
缓存有效时长,越长命中率越高
缓存容量(内存/磁盘),越大命中率越高
一致性哈希(Consistent Hashing):
缓存集群服务器节点的增减会影响缓存的命中率缓存预热
空节点查询问题(缓存穿透):
设计适当的空节点过滤机制,对于一张简单的记录映射表,不可能的数据直接过滤掉。
cache-hit-ratio
缓存淘汰/失效(Cache Eviction/Invalidation): 缓存淘汰算法无非是老套的
LRU(Least Recently Used)
LFU(Least Frequently Used)
FIFO(First In First Out)
缓存失效问题背后的根本问题则是异步系统的状态同步问题。
缓存穿透(Cache Penetration): 指查询一个一定不存在的数据(缓存或者数据库都不存在),这样第一次查询发现缓存不存在,就会穿透到数据库进行再次查询,数据库也查不到,那么缓存也不会记录任何返回结果;这样后续的对这个“不可能存在”数据的查询,永远会穿透缓存直接读数据库。
很多情况是来自于外部的恶意攻击,是安全方面不得不重视的一个指标。
缓存并发/雪崩(Cache Concurrency): 缓存失效时大量并发请求对源数据服务器造成极大压力,产生雪崩效应。可以结合业务场景适度的加锁来避免。
缓存一致性(Cache Coherence):
如何保证缓存数据与持久化数据的一致性?
缓存失效时间控制:数据实时容忍度
数据变化触发缓存更新:可以结合异步消息来处理
实时和体验只能二者取其一,不能兼得,完全一致性是不可能的!
# 异步消息(削峰填谷)
异步处理
重试机制
幂等性
消息顺序
投递/消费一次
# 数据库优化
索引优化 Index
索引设计优化、索引使用优化、最左前缀原则、索引填充因子、索引碎片
数据库连接线程池缓存 Pool
减少数据库不断的创建和销毁连接的开销,连接复用
分库/分区/分表 Sharding
基本思想就要把一个数据库切分成多个部分放到不同的数据库(server)上,
从而缓解单一数据库的性能问题。
缺点同样明显: 分布式事务、跨库Join/聚合
读写分离(主从备份,主主备份)
数据同步的时效性受到考验!
优化手段千千万,你偏爱哪一支呢?
如果觉得有用,欢迎打赏