核心知识点:前端
网站性能优化第必定律:优先使用缓存。算法
1.分布式缓存数据库
(1)缓存原理编程
a.什么是缓存?(将数据存储在相对较高访问速度的介质中,以供系统处理)设计模式
b.缓存的优势:访问速度快,若是须要计算能够减小计算时间浏览器
c.缓存的本质是一张以键值对存储的内存hash表缓存
d.主要用来存储:读写比例高,不多变化的数据安全
e.网站的访问遵循28定律性能优化
(2)合理使用缓存应该注意如下问题服务器
a.频繁更新的数据(数据尚未读就已经失效,通常要求读写比在2:1以上才有意义)
b.没有热点数据(无疑浪费资源)
c.数据不一致与脏读(缓存被加载的过程当中可能会产生数据不一致,有效时间过程数据在内存中就会变成脏数据)
d.缓存的可用性(若是太过依赖缓存,容易产生雪崩。使用缓存热备并不能提升缓存的可用性,使用集群能够提升可用性)
e.缓存预热(LRU计算时间过长,有的数据须要提早加载)
f.缓存穿透(解决部分数据没法命中,而加剧数据库压力的问题,通常设置空值)
(3)分布式缓存架构
a.JBoss Cache:数据相同
b.Memcached:数据不一样互不通讯
c.Memcached5大优势:协议简单、通用性强(支持各类语言)、Libevent网络通讯、内存管理高效、互不通讯
2.异步操做
a.数据直接写入数据库会形成巨大的压力
b.异步能够减缓响应时间,还能提高网站性能
c.对于大型网站来讲,异步能够实现削峰
3.使用集群:使单台服务处于最佳性能区间
4.代码优化
a.多线程
(1)线程优于进程的缘由:更轻量级、资源消耗更少、切换更容易
(2)使用多线程的缘由:IO阻塞和多CPU
(3)保证线程安全的手段:无状态对象、局部对象、锁
b.数据结构
(1)Hash表数据结构:依赖于HashCode,越散列,冲突越少,读写性能越高。
(2)优化手段:Time33和信息指纹
c.资源复用:单例和线程池
d.垃圾回收:理解JVM垃圾回收机制
上一章阐述了如何在Web前端进行性能优化,本章讲阐述如何在应用服务器进行性能优化。
应用服务器就是处理网站业务的服务器,网站的业务代码都部署在这里,
是网站开发最复杂,变化最多的地方,优化手段主要有缓存、集群、异步等
1.分布式缓存
回顾网站架构演化历程,当网站遇到性能瓶颈时,第一个想到的解决方案就是使用缓存。
在整个网站应用中,缓存几乎无处不在,既存在于浏览器,也存在于应用服务器和数据库服务器;
便可以对数据缓存,也能够对文件缓存,还能够对页面片断缓存,对网站性能优化意义重大。
网站性能优化第必定律:优先考虑使用缓存优化性能。
1)缓存基本原理
缓存指将数据存储在相对较高访问速度的存储介质中,以供系统处理。
一方面缓存访问速度快,能够减小数据访问的时间,另外一方面若是缓存的数据是通过计算处理获得的,
那么被缓存的数据无需重复计算便可直接使用,所以缓存还起到减小计算时间的做用。
缓存的本质是一个内存Hash表,网站应用中,数据缓存以一对Key、Value的形式存储在内存Hash表中。
Hash表数据读写的时间复杂度为O(1)。下图为一对KV在Hash表中的存储。
缓存主要用来存放那些读写比很高、不多变化的数据,如商品的类目信息,热门词的搜索信息,热门商品信息等。
应用程序读取数据时,先到缓存中读取,若是读取不到或数据已失效,再访问数据库,并将数据库写入缓存。
网站数据访问一般遵循二八定律,即80%的访问落在20%的数据上,所以利用Hash和内存的高速访问特性,
将这20%的数据缓存起来,可很好的改善系统性能,提升数据读取的速度,下降存储访问压力。
2)合理使用缓存
使用缓存对提升系统性能有不少好处,可是不合理使用缓存非但不能提升系统的性能,还会成为系统的累赘,甚至风险。
实践中,缓存滥用的情景家常便饭——过度依赖低可用的缓存系统、不恰当地使用缓存的数据访问特性等。
(1)频繁修改的数据
若是缓存中保存的数据是 频繁修改的数据,就会出现数据写入缓存后,应用还来不及读取缓存,数据就已失效的情形,徒增系统负担。
通常来讲,数据的读写比在2:1以上,即写入一次缓存,在数据更新前至少读取两次,缓存才有意义。
实践中,这个读写比一般很是高,好比新浪微博的热门博客,缓存之后可能会被读取数百万次。
(2)没有热点的访问
缓存使用内存做为存储,内存资源宝贵而有限,不可能将全部数据都缓存起来,
只能将最新的访问数据缓存起来,而将历史数据清理出缓存。
若是系统访问数据没有热点,不遵循二八定律,即大部分数据访问没有集中在小部分数据上,
那么缓存就没有意义,由于大部分数据尚未被再次访问就已经被挤出缓存了。
(3)数据不一致与脏读
通常对缓存的数据设置失效时间,一旦超过失效时间,就要从数据库中从新加载。
所以数据要容忍一时的数据不一致,如卖家已经编辑来商品属性,可是须要过一段时间才能被卖家看到。
在互联网应用中,这种延迟一般是能够接受的,可是具体 应用仍需慎重对待。
还有一种策略是数据更新时当即更新缓存,不过这也会带来更多系统开销和事务一致性问题。
(4)缓存可用性
缓存是为提升数据读取性能的,缓存数据丢失或者缓存不可用不会影响到应用程序的处理——它能够从数据库直接获取数据。
可是随着业务的发展,缓存会承担大部分数据访问的压力,数据库已经习惯了有缓存的日子,
因此当缓存服务崩溃时,数据库会由于彻底不能承受如此大的压力而当宕机,进而致使整个网站不可用。
这种状况被称为缓存雪崩,发生这种故障,甚至不能简单地重启缓存服务器和数据库服务器来恢复网站访问。
实践中,有的网站经过缓存热备等手段提升缓存可用性:将某台缓存服务器宕机时,将缓存访问切换到热备服务器上。
可是这种设计显然有违缓存的初衷,缓存根本就不该该被看成一个可靠的数据源来使用。
经过分布式缓存服务器集群,将缓存数据分布到集群多台服务器上可在必定程度上改善缓存的可能性。
当一台缓存服务器宕机 时,只有部分缓存数据丢失,从新从数据库加载这部分数据不会对数据库产生很大影响。
产品在设计之初就须要一个明确的定位:什么是产品要实现的功能,什么不是产品提供的特性。
在产品漫长的生命周期中,会有形形色色的困难和诱惑来改变产品的发展方向,
左右摇摆、什么都想作的产品,最后有可能成为一个失去生命力的四不像。
(5)缓存预热
缓存中存放的是热点数据,热点数据又是缓存系统利用LRU(最近最久未用算法)对不断访问的数据筛选出来的, 这个过程须要花费较长的时间。
新启动的缓存系统若是没有任何数据,在重建缓存数据的过程当中,系统的性能和数据库负载都不太好,
那么最好在缓存系统启动时就把热点数据加载好,这个缓存预加载手段叫做预热。
对于一些元数据如城市地名列表、类目信息,能够在启动时加载数据库中所有数据到缓存进行预热。
(6)缓存穿透
若是由于不恰当的业务、或者恶意攻击持续高并发地请求某个不存在的数据,因为缓存没有保存该数据,
全部的请求都会落在数据库上,会对数据库形成很大压力,甚至崩溃。
一个简单的对策是将不存在的数据也缓存起来(其value值为null)。
3)分布式缓存架构
分布式缓存指缓存部署在多个服务器组成的集群中,以集群方式提供缓存服务,其架构方式有两种,
一种是以JBoss Cache为表明的须要更新同步的分布式缓存,一种是以Memcached为表明的互不通讯的分布式缓存。
JBoss Cache的分布式缓存在集群中全部服务器中保存相同的缓存数据,当某台服务器有缓存数据更新的时候,
会通知集群中其余机器更新缓存数据或清除缓存数据。
JBoss Cache一般将应用程序和缓存部署在同一台服务器上,应用程序可从本地缓存读取数据,
可是这种方式带来的问题是缓存数据的数量受限于单一服务器的内存空间,
并且当集群规模较大的时候,缓存更新信息须要同步到集群全部机器,其代价惊人。
于是这种方案更多见于企业应用系统中,而不多在大型网站使用。
大型网站须要缓存的数据量通常都很庞大,可能须要数TB的内存作缓存,这时候就须要另外一种分布式缓存。
Memcached采用一种集中式的缓存集群管理,也被称做互不通讯的分布式架构方式。
缓存与应用分离部署,缓存系统部署在一种专门的服务器上,应用程序经过一致性hash等路由算法选择缓存服务器远程访问缓存数据,
缓存服务器之间不通讯,缓存服务器的规模能够很容易的实现扩容,具备良好的可伸缩性。
2.异步操做
使用消息队列将调用异步化,可改善网站的扩展性,事实上。使用消息队列还能够改善网站系统的性能。
在不使用消息队列的状况下,用户的请求数据直接写入数据库,在高并发的状况下,会对数据库形成巨大的压力,同时也使响应延迟加重。
在使用消息队列后,用户请求的数据发送给消息队列以后当即返回,
再由消息队列的消费者进程(一般状况下该进程独立部署在专门的服务器集群上)从消息队列中获取数据,异步写入数据库。
因为消息队列服务器处理速度远快于数据库(消息队列服务器比数据库具备更好的伸缩性),所以用户的响应延迟能够获得有效改善。
消息队列具备很好的削峰做用——即经过异步处理,将短期高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。
在电子商务网站促销活动中,合理使用消息队列,能够抵御促销活动刚开始大量涌入的订单对系统的形成的冲击。
须要注意的是,因为数据写入消息队列以后当即返回给用户数据在后续的业务校验、写数据库操做可能失败,
所以在使用消息队列进行业务的异步处理后,须要适当修改业务流程进行配合,
如提交订单后,订单数据写入消息队列,不能当即返回用户订单数据提交成功,
须要在消息队列的订单消费者进程真正处理完该订单,甚至商品出库后,在经过电子邮件或SMS消息通知用户订单成功,以避免交易纠纷。
3.使用集群
在网站高并发的场景下,使用负载均衡技术为一个应用构建一个由多台服务器组成的一个集群,
将并发访问请求分发到多台服务器上处理,避免单一服务器因负载压力过大而响应缓慢,使用户请求具备更好的响应延迟特性。
三台Web服务器共同处理来自用户浏览器的访问请求,这样每台Web服务器须要处理的http请求只有并发请求总数的三分之一,
根据性能曲线测试,使服务器的并发请求数目控制在最佳的运行区间,获取最佳的访问请求延迟。
4.代码优化
网站的业务逻辑实现代码主要部署在应用服务器上,须要处理复杂的并发事务。
合理优化业务代码,能够很好地改善网站的性能。不一样编程语言的代码优化手段有不少,下面主要讲几个重要的方面。
1)多线程
多用户并发访问是网站的基本需求,大型网站的并发用户数会达到数万,单台服务器的并发用户数也会达到数百。
CGI编程时代,每一个用户请求都会建立一个独立的系统进程去处理。因为线程比进程更轻量,更少占用系统资源,
切换代价更小,因此目前主要的Web应用服务器都采用多线程的方式响应并发用户请求,所以网站开发自然就是多线程编程。
从资源利用的角度看,使用多线程的缘由主要有两个:IO阻塞和多CPU。
当前线程进行IO处理的时候,会被阻塞释放CPU以等待IO操做完成,
因为IO操做(无论是磁盘IO仍是网络IO)一般都须要较长的时间,这时CPU能够调度其它的线程进行处理。
前面咱们提到,理想的系统Load是既没有进程(线程)等待也没有CPU空闲,利用多线程IO阻塞与执行交替进行,能够最大限度地利用CPU资源。
使用多线程的另外一个缘由是服务器有多个CPU,在这个连手机都有四核CPU的时代,除了最低配置的虚拟机,
通常数据中兴的服务器至少16核CPU,要想最大限度地使用这些CPU,必须启动多线程。
网站的应用程序通常被Web服务器容器管理,用户请求的多线程也一般被Web服务器Web服务器容器管理,
但无论是Web容器管理的线程,仍是应用程序本身建立的线程,一台服务器上启动多少线程合适呢?
假设服务器上执行的都是相同类型的任务,针对该任务启动的线程数有个简化的估算公式可供参考:
启动线程数=[任务执行时间/(任务执行时间-IO等待时间)] x CPU内核数
最佳启动线程数和CPU内核数量成正比,和IO等待时间成正比。
若是任务都是CPU计算型任务,那么线程数最多不超过CPU内核数,由于启动再多线程,CPU也来不及调度;
相反若是是任务须要等待磁盘操做,网络响应,那么多启动线程有助于提升任务并发度,提升系统吞吐能力,改善系统性能。
多线程编程一个须要注意的问题是线程安全问题,即多线程并发对某个资源进行修改,致使数据混乱。
这也是缺少经验的网站工程师最容易犯错的地方,而线程安全Bug又难以测试和重现,
网站故障中,许多所谓偶然发生的“灵异事件“都和多线程并发问题有关。
对网站而言,无论有没有进行多线程编程,工程师写的每一行代码都会被多线程执行,由于用户请求是并发提交的,
也就是说,全部的资源——对象、内存、文件、数据库,乃至另外一个线程均可能被多线程并发访问。
编程上,解决线程安全的主要手段有以下几点:
(1)将对象设计为无状态对象
所谓无状态对象是指对象自己不存储状态信息(对象无成员变,或者成员变量也是无状态对象),
这样多线程并发访问的时候就不会出现状态不一致,Java Web开发中经常使用的Servlet对象就设计为无状态对象,
能够被应用程序多线程并发调用处理用户请求。而Web开发中经常使用的贫血模型对象都是些无状态对象。
不过从面向对象设计的角度看,无状态对象是一种不良设计。
(2)使用局部对象
即在方法内部建立对象,这些对象会被每一个进入该方法的线程建立,
除非程序有意识地将这些对象传递给其它程序,不然不会出现对象被多线程访问地情形。
(3)并发访问资源时使用锁
即多线程访问资源的时候,一般锁的方式使多线程并发操做转化为顺序操做,从而避免资源被并发修改。
随着操做系统和编程语言的进步,出现各类轻量级锁,使得运行期线程获取锁和释放锁的代价都变得更小,
可是锁致使线程同步顺序执行,可能会对系统性能产生严重影响。
2)资源复用
系统运行时,要尽可能减小那些开销很大的系统资源的建立和销毁,好比数据库链接、网络通讯链接、线程、复杂对象等。
从编程角度,资源复用主要的两种模式:单例(Singleton)和对象池(Object Pool)。
单例虽然是GoF经典设计模式中较多被诟病的一个模式,但因为目前Web开发中主要使用贫血模式,
从Service到Dao都是些无状态对象,无需重复建立,使用单例模式也就天然而然了。
事实上,Java开发经常使用的对象容器Spring默认构造的对象都是单例(须要注意的是Spring的单例是Spring容器管理的单例而不是用单例模式构造的单例)。
对象池模式经过复用对象实例,减小对象建立和资源消耗。
对于数据库链接对象,每次建立链接,数据库服务端都须要建立专门的资源以应对,所以频繁建立关闭数据库链接,
对数据库服务器而言是灾难性的,同时频繁建立关闭链接也须要花费较长时间。
所以在实践中,应用程序的数据库链接基本都使用链接池(Connection Pool)的方式。
数据库链接对象建立好之后,将链接对象放入对象池容器中,应用程序要链接的时候,
就从链接池中取一个空闲的链接使用,使用完毕再将对象归还到对象池中便可,不须要在建立新的链接。
对于每一个Web请求(HTTP Request),Web应用程序都须要建立一个独立的线程去处理,这方面,应用服务器也采用线程池的方式。
这些所谓的链接池、线程池,本质上都是对象池,即链接、线程都是对象,池管理方式也基本相同。
3)数据结构
早期关于程序的一个定义是,程序就是数据结构+算法,数据结构对于编程的重要性不言而喻。
在不一样场景中合理使用恰当的数据结构,灵活组合各类数据结构读写和计算性能可极大优化程序的性能。
前面缓存部分已经描述过Hash表的基本原理,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等具备垃圾回收功能的环境中,那么垃圾回收可能对系统的性能特性产生极大影响。
理解垃圾回收机制有助于程序优化和参数调优,以及编写内存安全的代码。
以JVM为例,其内存主要可划分为堆(heap)和堆栈(stack)。
堆栈用于存储线程上下文信息,如方法参数、局部变量等。
堆则是存储对象的内存空间,对象的建立和释放、垃圾回收就在这里进行。
经过对对象生命周期的观察,发现大部分对象的生命周期都极其短暂,
这部分对象产生的垃圾应该被更快的收集,以释放内存,这就是JVM分代垃圾回收。
在JVM分代垃圾回收机制中,将应用程序可用的堆栈空间分为年轻代(Young Generation)和年老代(Old Generation),
又将年轻带分为Eden区(Eden Space)、From区和To区,新建对象老是在Eden区被建立,
当Eden区空间已满,就触发一次Young GC(Garbage Collection,垃圾回收),将还被使用的对象复制到From区,
这样整个Eden区都是未被使用的空间,能够继续建立对象,当Eden区再次用完,再触发一次Young GC,
将Eden区和From区还在被使用的对象复制到To区,下一次Young GC则是将Eden区和To区还未被使用的对象复制到From区。
所以通过屡次的young GC,某些对象会在From区和To区屡次复制,若是超过某个阀值,对象还未被释放,则将该对象复制到Old Generation。
若是Old generation空间也已用完,那么就会触发Full GC,即所谓的全变量回收,全变量回收会对系统性能产生较大的影响,
所以应根据系统的业务特色和对象的生命周期合理设置Young generation和Old generation的大小,尽可能减小Full GC。
事实上,某些Web应用在整个运行期间能够作到从不进行Full GC。