网站的性能优化

目录

一、Web前端性能优化

1.1 浏览器访问优化

1.2 CDN 加速

1.3 反向代理

二、应用服务器性能优化

1.1 分布式缓存

1.2 异步操作

1.3 使用集群

1.4 代码优化

三、存储性能优化

1.1 机械硬盘 vs 固态硬盘

1.2 B+ 树 vs LSM 树

1.3 RAID vs HDFS


如果性能测试结果不满足我们预期的需求时,检查请求处理的各个环节的日志,分析哪个环节响应时间不合理,超出预期;然后检查监控数据分析影响性能的主要因素是内存磁盘网络、还是 CPU,是代码问题还是架构设计不合理,或者系统资源确实不足。根据网站分成架构,可分为 Web前端性能优化应用服务器性能优化存储服务器性能优化3大类。

一、Web前端性能优化


Web 前端指网站业务逻辑之前的部分,包括浏览器加载器、网站视图模型、图片服务、CDN 服务等,主要优化手段有优化浏览器访问、使用反向代理CDN等。

1.1 浏览器访问优化


【1】减少 HTTP请求:HTTP 协议是无状态的应用层协议,意味着每次 HTTP 请求都需要建立通信链路进行数据传输,在服务器端,每个 HTTP都需要启动独立的线程去处理。这些通信和服务的开销都很昂贵,减少HTTP 请求的数目可有效提高访问性能。减少 HTTP 的主要手段是将多次请求合并为一次。例如:合并CSS、JavaScript、图片等,将浏览器一次访问需要的 JavaScript、CSS 合并成一个文件,这样浏览器就只需要一次请求。

【2】使用浏览器缓存:对网站而言,静态资源文件更新的频率都较低(CSS、JavaScript、Logo、图标等),但这些文件几乎每次 HTTP 请求都需要有,如果将这些文件缓存在浏览器中可以极好地改善性能。通过设置 HTTP 头中 Cache-Control 和 Expires 的属性,可设定浏览器缓存,缓存时间可以是天或者月等。但静态文件的变化有时需要及时应用到客户端浏览器可以通过改变文件名实现。使用浏览器缓存策略的网站在更新静态资源时,应采用逐量更新静态资源,比如需要更新十个图标时,不宜把十个文件一次全部更新,而应一个一个文件更新,并在一定的时间间隔内,以免用户浏览器突然缓存失效(缓存雪崩),集中更新缓存,造成服务器负载骤增、网络堵塞的情况。

【3】启用压缩:在服务器端对文件进行压缩,在浏览器端对文件解压缩,可以有效减少通信传输的数据量。文本文件的压缩效率可达80%以上,因此HTML、CSS、JavaScript 文件启用 GZip 压缩可达到较好的效果。但是压缩对服务器和浏览器产生一定的压力,在通信带宽良好,而服务器资源不足的情况下要权衡考虑。

【4】CSS 放在页面最上面,JavaScript 放在页面最下面:浏览器会在下载完全部 CSS 之后才对整个页面进行渲染,因此最好的做法是将 CSS 放在页面最上面,让浏览器尽快下载。JavaScript 则相反,浏览器在加载 JavaScript 后立即执行,有可能会阻塞整个页面,造成页面显示缓慢,因此 JavaScript 最好放在页面最下面。但是如果解析时就需要用到 JavaScript,这是放在底部就不合适了。

【5】减少 Cookie 传输:一方面,每次请求和响应都包含Cookie,太大的 Cookie 会严重影响数据传输,因此哪些数据需要写入Cookie 需要慎重考虑,尽量减少 Cookie 中传输的数据量。另一方面,对于某些静态资源的访问,如CSS等,发送 Cookie 没有意义,可以考虑静态资源使用独立域名访问,避免请求静态资源时发送 Cookie,减少 Cookie 传输的次数。

1.2 CDN 加速


CDN(Content Delivery Network,内容分发网络)其本质仍然是一个缓存,而且将数据缓存在离用户最近的地方,使用户以最快的速度获取。由于 CDN 部署在网络运营的机房,这些运营商又是终端用户的网络服务提供商,因此用户请求路由的第一层就到达CDN 服务器,当CDN 中存在浏览器请求的资源时,将从CDN直接返回给浏览器,加快用户访问速度,减少数据中心负载压力CDN 服务器一般能够缓存静态资源(图片、CSS、Script 脚本、静态网页等)但是这些文件访问频度很高,将其缓存在CDN可极大改善网页的打开速度。

1.3 反向代理


浏览器将 HTTP 请求发送到互联网上,反向代理服务器代理网站 Web 服务器接收 HTTP请求。和传统代理服务器一样代理服务器也可以保护网站安全,互联网的访问请求必须经过代理服务器。代理服务器可以通过配置缓存功能加速 Web请求。当用户第一次访问静态内容的时候,静态内容就被缓存在反向代理服务器中,当其他用户访问该静态内容的时候,就可以直接从反向代理中获取,加速 Web 请求的响应,减轻 Web 服务器负载压力。事实上,有些网站会把动态内容也缓存在代理服务器上,比如维基百科以及某些博客论坛网站,把热门词条、帖子、博客缓存在反向代理服务器上加速用户访问速度,当这些内容有变化时,通过内部通知机制通知反向代理缓存失效,反向代理会重新加载最新的动态内容再次缓存起来。此外,反向代理也可以实现负载均衡的功能而通过负载均衡的应用集群可以提高系统总体处理能力,进而改善网站高并发情况下的性能。

二、应用服务器性能优化


应用服务器就是处理网站业务的服务器,网站业务代码都部署在此,是网站开发最复杂,变化最多的地方,优化手段主要有缓存集群异步等。优先考虑使用缓存优化性能

1.1 分布式缓存


网站架构演化历程中,当网站遇到性能瓶颈的时候,第一个想到的解决方案就是使用缓存。在整个网站应用中,缓存几乎无所不在,既存在于浏览器中,也存在于应用服务器和数据库服务器中;既可以对数据缓存,也可以对文件缓存,还可以对页面片段缓存。合理使用缓存,对网站性能优化意义重大

【1】缓存的基本原理:缓存指将数据存储在相对较高访问速度的存储介质中,以供系统处理。一方面缓存访问速度快,可以减少数据访问的时间,另一方面如果缓存的数据是经过计算处理得到的,那么被缓存的数据无需重复计算可直接使用,因此缓存还起到减少计算时间的作用缓存的本质是一个内存 Hash 表,Hash 表数据读写的时间复杂度为O(1),计算KV对中 Key 的 HashCode 对应的 Hash 表索引,可快速访问 Hash 表中的数据。【HashMap源码博客链接

缓存主要用来存放那些读写比很高,很少变化的数据。网站数据访问通常遵循二八定律,即 80%的访问落在 20%的数据上,因此利用Hash 表和内存的高速访问特性,将这 20%的数据缓存起来,可很好地改善系统性能,提高数据读取速度,降低存储访问压力。

【2】合理使用缓存:缓存可以提高性能,但是不合理使用缓存非但不能提高系统性能,还会称为系统累赘,甚至风险。例如:
   ■  频繁修改的数据:会出现数据写入缓存后,应用还没有来得及读取缓存,数据就发生了变化,缓存就失效了,这样就徒增系统负担。一般说来,数据的读写比在2:1以上,即写入一次缓存,在数据更新前至少读取两次,缓存才有意义。实践中,这个读写比通常非常高。
   ■  没有热点的访问缓存使用内存作为存储,内存资源宝贵且有限,不可能将所有数据缓存起来,只能将最新访问的数据缓存起来,而将历史数据清理。如果应用系统访问数据没有热点,不遵循二八定律,那么缓存就没有意义,因为大部分数据还没有别再次访问就被挤出缓存了。
   ■  数据不一致与脏读:一般会对缓存数据设置失效时间,一旦超过时间,就要从数据库中重新加载。因此应用要容忍一定时间的数据不一致。还有一种策略是数据更新时立即更新缓存,不过这会带来更多系统开销和事务不一致性问题。
   ■  缓存可用性:缓存时提高数据读取性能的,缓存数据丢失或者缓存不可用不会影响到应用程序的处理。随着业务的发展,缓存会承担大部分数据访问的压力,数据库已经习惯了有缓存的日子,所以当缓存服务崩溃时,数据库可能会因为不能承受如此大的压力而宕机,进而导致整个网站不可用。这种情况被称作缓存雪崩,发生这种故障,甚至不能简单地重启缓存服务和数据库服务器来恢复网站访问。所以在设置缓存时,对每个key设置一个随机的失效时间,而不是统一的失效时间。实践中,可以通过缓存热备等手段提高缓存可用性。通过分布式缓存集群,将缓存数据分布到集群多台服务器上可在一定程度上改善缓存的可用性。当一台缓存服务器宕机时,只有部分缓存数据丢失,重新从数据库加载这部分数据不会对数据库产生很大影响。
   ■  缓存预热:缓存中存放的是热点数据,热点数据又是缓存系统利用 LRU(最近最少使用)算法对不断访问的数据筛选淘汰出来的。这个过程需要花费较长时间。新启动的缓存系统没有任何数据,在重建缓存数据的过程中,系统的性能和数据库负载都不太好,最好在缓存系统启动时就把热点数据加载好,这个缓存预加载手段叫做缓存预热(warmup)。
   ■  缓存穿透:如果因为不恰当的业务、或者恶意攻击持续高并发的请求不存在的数据,由于缓存中没有此数据,所有的请求都会落到数据库上,会对数据库造成很大压力,甚至崩溃。一个简单的策略就是将不存在的数据也缓存起来,value值为null。

【3】分布式缓存架构:Redis、Memcached 使用比较广泛的两个分布式框架系统。Redis 可以实现持久化功能,Memcached 具有简单的设计、优异的性能、互不通信的服务器集群、海量数据可伸缩的架构得到广泛使用。Redis系列可以参考【链接】,进行单独的博客说明,这里就省略了。

1.2 异步操作


使用消息队列将调用异步化,可改善网站的扩展性和系统的性能。不适用消息队列的情况下,用户的请求数据直接写入数据库,在高并发的情况下,会对数据库造成巨大的压力,同时也使得响应延迟加剧。使用消息队列后,用户请求的数据发送给消息队列后立即返回。再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度远快于数据库,因此用户的响应延迟可得到有效改善。消息队列具有很好的削锋作用:通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。在电子商务网站促销活动中,合理使用消息队列,可有效低于促销活动刚开始大量涌入的订单对系统造成的冲击。需要注意的是,由于数据写入消息队列后立即返回给用户,数据在后续的业务校验、写数据库等操作可能失败,因此在使用消息队列进行业务异步处理后,需要适当修改业务流程进行配合,如果订单提交后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单,甚至商品出库后,再通过电子邮件等通知用户订单成功,以免交易纠纷。

1.3 使用集群


在网站高并发访问的场景下,使用负载均衡技术为应用构建一个由多个服务器组成的服务器集群,将并发访问请求分发到多台服务器上处理,避免单一服务器因负载压力过大而响应缓慢。如下三台服务器共同处理用户请求,每台 Web 服务器需要处理的 http 请求只有总并发请求的三分之一,根据性能测试曲线,使服务器的并发请求数目控制在最佳运行区间。

1.4 代码优化


网站的业务逻辑实现代码主要部署在应用服务器上,需要处理复杂的并发事务。合理优化业务代码,可以很好地改善网站性能。不同编译语言的代码优化手段有很多,如下:

【1】多线程:多用胡并发访问时网站的基本需求,大型网站的并发用户数会达到数万,单台服务器的并发用户会达到数百。由于线程比进程更轻量,更少占有系统资源,切换代价更小,所以目前 Web 应用服务器都采用多线程的方式响应并发用户请求,因此网站开发天然就是多线程编程。从资源利用的角度看,使用多线程的原因有两个:IO阻塞多CPU。当线程进行 IO处理的时候,会被阻塞释放CPU以等待 IO操作完成,由于IO操作通常都需要较长的时间,这时CPU可以调度其他的线程进行处理。理想的系统 Load 既没有进程(线程)等待,也没有CPU空闲。利用多线程 IO阻塞与执行交替执行,可最大限度地利用CPU资源。使用多线程的另一个原因是服务器是多个CPU,在这个连手机都是四核 CPU的时代,处理低配置的虚拟机,一般数据中心的服务器至少16核 CPU,要想最大限度使用这些 CPU,必须启动多线程。网站的应用程序一般都被Web 服务器容器管理,用户请求的多线程也通常被 Web服务器容器管理,但不管是 Web 容器管理的线程,还是应用程序自己创建的线程,服务器的线程数启动多少合适呢?有个简化的估算公式可供参考:

启动线程数 = [任务执行时间/(任务执行时间-IO等待时间)] * CPU 内核数;

最佳启动线程数和 CPU内核数量成正比,和IO等待时间成正比。如果任务都是 CPU计算型任务,那么线程数最多不超过 CPU内核数,因为启动再多线程,CPU 也来不及调度;相反如果是任务需要等待磁盘操作,网络响应,那么多启动线程有助于提高任务并发度,提高系统吞吐能力,改善系统性能。

多线程编程需要注意的是线程安全问题,即多线程并发对某个资源进行修改,导致数据混乱。网络故障中,许多所谓偶然发生的“灵异事件” 都和多线程并发问题有关。对网站而言,不管有没有进行多线程编程,工程师写的每一行代码都会被多线程执行,因为用户请求是并发提交的,也就是说,所有的资源(对象、内存、文件、数据库)乃至另一个线程都可能被多线程并发访问。解决线程安全的主要手段如下几点:
   ■ 将对象设计为无状态对象:指对象本身不存储状态信息,这样多线程并发访问的时候就不会出现状态不一致。
   ■ 使用局部对象:在方法内部创建对象,这些对象会被每个进入该方法的线程创建,除非程序有意识地将这些对象传递给其他线程,否则不会出现对象被多线程并发访问的情形。
   ■ 并发访问资源时使用锁:即多线程访问资源的时候,通过的方式使多线程并发操作转化为顺序操作,从而避免资源被开发修改。随着操作系统和编程语言的进步,出现各种轻量级锁,使得运行期线程获取锁和释放锁的代价都变得更小,但是锁导致线程同步顺序执行,可能会对系统性能产生严重影响。

【2】资源复用:系统运行时,要尽量减少那些开销很大的系统资源的创建销毁,比如数据库连接、网络通信连接、线程、复杂对象等。从编程角度,资源复用主要有两种模式:单例Singleton)和对象池Object Pool)。对象池模式通过复用对象实例,减少对象创建和资源消耗。对于数据库连接对象,每次创建连接,数据库服务端都需要创建专门的资源应用,因此频繁创建关闭数据库连接,对数据库服务器而言是灾难性的,同时频繁创建关闭连接池也需要花费较长时间。因此在实践中,应用程序的数据库连接基本都使用连接池(Connection Pool)的方式。数据库来接对象创建好以后,将连接对象放入对象池容器中,应用程序要连接的时候,就从对象池中获取一个空闲的连接使用,使用完毕再将该对象归还到对象池中即可,不需要创建新的连接。

【3】数据结构:程序=数据结构+算法,在不同场景中合理使用恰当的数据结构,灵活组合各种数据结构改善数据读写和计算特性可极大优化程序的性能。Hash 表的读写性能在很大程度上依赖 HashCode 的随机性,即 HashCode 越随机散列,Hash 表的冲突就越少,读写性能也就越高,目前比较好的字符串 Hash 散列算法有 Time33 算法,即对字符串逐字符迭代乘以 33,求得 Hash 值,算法原型为:

hash(i) = hash(i-1) * 33 + str[i]

 Time33 虽然可以较好地解决冲突,但是有可能相似字符串的 HashCode 也比较接近,如字符串“AA”的 HashCode 是 2210,字符串“AB”的HashCode 是2211。这在某些应用场景是不能接受的,这种情况下,一个可行的方案是对字符串取信息指纹,再对信息指纹求 HashCode,由于字符串微小的变化就可以引起信息指纹的巨大不同,因此可以获得较好的随机散列。

【4】垃圾回收:如果Web应用运行在 JVM等具有垃圾回收功能的环境中,那么垃圾回收可能会对系统的性能特性产生巨大影响。理解垃圾回收机制有助于优化和参数调优,以及编写内存安全的代码。内存主要可划分为堆heap)和stack)。栈用于存储线程上下文信息,如方法参数、局部变量等。堆则是存储对象的内存空间,对象的创建和释放、垃圾回收就在这里进行。通过对对象生命周期的观察,发现大部分对象的声明周期都极其短暂,这部分对象产生的垃圾应该被更快地收集,以释放内存,这就是 JVM 分代垃圾回收。将应用程序可用的堆空间分为年轻代(Young Generation)和老年代(Old Genenration),又将年轻代分为 Eden区、From区和 To区,新建对象总是在 Eden区中被创建,当Eden 区空间已满,就触发一次 Young GC,将还被使用的对象复制到 From区,这样整个 Eden区都是未被使用的空间,可供继续创建对象。当Eden区再次用完,再触发一次 Young GC,将 Eden 区和 From 区还在被使用的对象复制到 To区,下一次 Young GC则是将 Eden区和 To区还被使用的对象复制到 From区。因此,经过多次 Young GC,某些对象会在 From 和 To区多次复制,如果超过某个阈值对象还没被释放,则将该对象复制到 Old Generation。如果老年代已用完,那么就会触发 FullGC,即所谓的全量回收,全量回收会对系统性能产生较大影响,因此应根据系统业务特点和对象生命周期,合理设置 Young Generation 和 Old Generation 大小,尽量减少 FullGC。事实上,某些 Web 应用在整个运行期间可以做到从不进行 FullGC。

三、存储性能优化


在网站应用中,海量的数据读写对磁盘访问造成巨大压力,虽然可以通过 Cache 解决一部分数据读压力,但是很多时候,磁盘仍然是系统最严重的瓶颈。而且磁盘中存储的数据是网站最重要的资产,磁盘的可用性和容错性也至关重要。

1.1 机械硬盘 vs 固态硬盘


机械硬盘是目前最常用的一种硬盘,通过马达驱动磁头臂,带动磁头到指定的磁盘位置访问数据,由于每次访问数据都需要移动磁头臂,当随机访问时,性能表现会比连续访问相差很大。固态硬盘又称为 SSD或Flash 硬盘,这种硬盘没有机械装置,数据存储在可持久记忆的硅晶体上,因此可以像内存一样快速随机访问。而且SSD具有更小的功能消耗和更少的磁盘震动与噪音。在网站中,大部分应用访问数据都是随机的,这种情况下 SSD具有更好的性能。但是目前 SSD硬盘技术还不太成熟,可靠性、性价比有待提升,因此SSD的使用还在摸索阶段。但是相信随着 SSD工艺水平的提高,逐步替代传统机械磁盘时迟早的事。

1.2 B+ 树 vs LSM 树


由于传统的机械磁盘具有快速顺序读写、慢速随机读写的访问特性,这个特性对磁盘存储结构和算法的选择影响甚大。为了改善数据访问特性,文件系统或数据库系统通常会对数据排序后存储,加快数据检索速度,这就需要保证数据在不断更新、插入、删除后依然有序,传统关系数据库的做法是使用 B+树。如下:

B+ 树时一种专门针对磁盘存储而优化的 N叉排序树,以树节点为单位存储在磁盘中,从根开始查找所需数据所在的节点编号和磁盘位置,将其加载到内存中然后继续查找,知道找到所需的数据。目前数据库多采用两级索引的 B+树,输的层次最多三层。因此可能需要 5次磁盘访问才能更新一条记录(三次磁盘访问获得数据索引及行 ID,然后再进行一次数据文件读操作及一次数据文件写操作)

目前许多 NoSQL产品采用 LSM树作为主要数据结构,LSM树可以看作是一个 N阶合并树。数据写操作(增删改)都在内存中进行当数据量超过设定的内存阈值后,会将这颗排序树和磁盘上最新的排序树合并。当这颗树的数据量也超过设定阈值后,和磁盘上下一级的排序树合并。合并过程中,会用最新更新的数据覆盖旧的数据。在需要读操作时,总是从内存中的排序树开始搜索,如果没有找到,就从磁盘上的排序树顺序查找

在 LSM 树上进行一次数据更新不需要磁盘访问,在内存即可完成,速度远快于 B+树。当数据访问以写操作为主,而读操作则集中在最近写入的数据上时,使用 LSM树可极大程度地减少磁盘访问次数,加快访问速度。

作为存储结果,B+树不是关系数据库所独有的。NoSQL 数据库也可以使用 B+树。同理,关系数据库也可以使用 LSM,而且随着 SSD硬盘的日趋成熟及大容量持久存储的内存技术的出现,相信 B+ 树这一“古老”的存储结构会焕发第二春。

1.3 RAID vs HDFS


RAID 磁盘阵列(Redundant Arrays of Independent DisksRAID)技术主要是为了改善磁盘的访问延迟,增加磁盘的可用性和容错性能力。目前服务器级别的计算机都支持插入多块磁盘(8块或者更多),通过使用 RAID 技术,实现数据在多块磁盘上的并发读写和数据备份。
   ■ RAID0数据从内存缓冲区写入磁盘时,根据磁盘数量将数据分成 N份,这些数据同时并发写入N块磁盘,使得数据整体写入速度是一块磁盘的 N倍。读取也是一样,因此 RAID0 具有极好的数据读写速度,但是 RAID0 不做数据备份,N块磁盘中只要有一块损坏,数据完整性就被破坏,所有磁盘的数据都会损坏
   ■ RAID1:数据在写入磁盘时,将一份数据同时写入两块磁盘,这样任何一块磁盘损坏都不会导致数据丢失,插入一块新磁盘就可以通过复制数据的方式自动修复,具有极高的可靠性。
   ■ RAID10:结合 RAID0RAID1 两种方案,将所有磁盘平均分成两份,数据同时在两份磁盘写入,相当于 RAID1,但是每一份磁盘里面的 N/2 块磁盘上,利用 RAID0 技术并发读写,既提高可靠性又改善性能,不过 RAID10 的磁盘利用率较低,有一半的磁盘用来写备份数据。
   ■ RAID3:一般一台服务器上不会出现同时损坏两块磁盘的情况,在只损坏一块磁盘的情况下,如果能利用其它磁盘的数据恢复损坏磁盘的数据,这样在保证可靠性和性能的同时,磁盘利用率也得到大幅提升。在数据写入磁盘的时候,将数据分成N-1份,并发写入 N-1 块磁盘,并在第N块磁盘记录校验数据,任何一块磁盘损坏,都可以利用其它 N-1 块磁盘大金额数据恢复。
   ■ RAID5:相比 RAID3,RAID5被更多使用。两者比较相似,RAID5 校验数据不是写入第 N 块磁盘,而是螺旋式地写入所有磁盘中。这样校验数据的修改也被平均到所有磁盘上,避免 RAID3 频繁写坏一块磁盘的情况。
   ■ RAID6:如果数据需要很高的可靠性,在出现同时损坏两块磁盘的情况下,仍然需要修复数据时,就使用 RAID6。RAID6与RAID5相似,但是数据只写入 N-2 块磁盘,并螺旋式地在两块磁盘中写入校验信息。

RAID 技术在传统关系数据库及文件系统中应用比较广泛。

HDFS(Hadoop 分布式文件系统)中,系统在整个存储集群的多台服务器上进行数据并发读写和备份,可以看做在服务器集群规模上实现了类似 RAID 的功能,因此不需要磁盘 RAID。HDFS 以块(Block)为单位管理文件内容,一个文件被分割成若干个 Block,当应用程序写文件时,每写完一个 Block,HDFS 就将其自动复制到另外两台机器上,保证每个 Block 有三个副本,即使有两台服务器宕机,数据依然可以访问,相当于实现了 RAID1的数据复制功能。当对文件进行处理计算时,通过 MapReduce 并发计算任务框架,可以启动多个计算子任务(MapReduce Task),同时读取文件的多个 Block,相当于实现了 RAID0 的并发访问能力。

----如果喜欢,点个  红心♡  支持以下,谢谢----

发布了229 篇原创文章 · 获赞 317 · 访问量 22万+

猜你喜欢

转载自blog.csdn.net/zhengzhaoyang122/article/details/104381675