四、瞬时响应:网站的高性能架构(大型网站技术架构原理)

4.1 网站性能测试

4.2 Web前端性能优化

4.3 应用服务器性能优化

4.4 存储性能优化

4.1 网站性能测试

  性能测试是性能优化的前提和基础,也是性能优化结果的检查和度量标准。不同视角下的网站性能有不同的标准,也有不同的优化手段。

4.1.1 不同视角下的网站性能

  1.用户视角

  从用户视角,网站性能就是用户在浏览器上直观感受到的网站响应速度快还是慢。

  用户感受到的时间,包括用户计算机和网站服务器通信的时间、网站服务器处理的时间、用户计算机浏览器构造请求解析响应数据的时间。

  不同计算机的性能差异,不同浏览器解析HTML速度的差异,不同网络运营商提供的互联网贷款服务的差异,这些差异最终导致用户感受到的响应延迟可能会远远大于网站服务器处理请求需要的时间。

  实践中,使用一些前端架构优化手段,通过优化页面 HTML 、利用浏览器端的并发和异步特性、调整浏览器缓存策略、使用CDN服务、反向代理等手段,使浏览器尽快地显示用户感兴趣的内容、尽可能近地获取页面内容,即使不优化应用程序和架构也可以很大程度地改善用户视角下地网站性能。

  2.开发人员视角的网站性能

  开发人员关注的主要是应用程序本身及其相关子系统的性能,包括响应延迟、系统吞吐量、并发处理能力、系统稳定性等技术指标。主要的优化手段有使用缓存加速数据读取,使用集群提高吞吐能力,使用异步消息加快请求响应及实现削峰,使用代码优化手段改善程序性能。

  3.运维人员视角的网站性能

  运维人员更关注基础设置性能的资源利用率,如网络运营商的带宽能力、服务器硬件的配置、数据中心网络架构、服务器和网络贷款的资源利用率等。主要优化手段有建设优化骨干网、使用高性价比定制服务器、利用虚拟化技术优化资源利用率。

4.1.2 性能测试指标

  不同视角下有不同的性能测试标准,不同的标准有不同的性能测试指标,从开发和测试人员的视角,网站性能测试的主要指标有响应时间、并发数、吞吐量、性能计数器等。

  1.响应时间

    响应时间指应用执行一个操作需要的时间,包括从发出请求开始到收到最后响应时间数据所需要的时间。

    响应时间是系统最重要的性能指标,直观地反映了系统地“快慢”

    如下表:列出了一些常用的系统操作需要的响应时间

    

  实践中采用重复请求测系统的响应时间,如一个请求操作重复执行一万次,测试一万次执行需要的总响应时间之和,然后除以一万,得到单次请求的响应时间。

  2.并发数

  指系统能够同时处理请求的数目,这个数字也反映了系统的负载特性。对于网站而言,并发数即网站并发用户数,指同时提交请求的用户数目。

   测试程序通过多线程模拟并发用户的办法来测试系统的并发能力,为了真实模拟用户行为,测试程序并不是启动多线程然后不停的发送请求,而是在两次请求之间加入一个随机等待时间,这个时间被称为思考时间。

  3.吞吐量

  指单位时间内系统处理的请求数量,体现系统的整体处理能力。对于网站,可以用 “请求数/秒”或是“页面数/秒”来衡量,也可以用“访问人数/天”或是“处理的业务数/小时”等来衡量。

  TPS(每秒事务数)也吞吐量的一个常用量化指标

  HPS(每秒HTTP请求数)

  QPS(每秒查询数)

  在系统并发数由小逐渐增大的过程中(这个过程也伴随着服务器系统资源消耗逐渐增大),系统吞吐量显示逐渐增加,达到一个极限后,随着并发数的增加反而下降,达到系统崩溃后,系统资源耗尽,吞吐量为0。

  这个过程中,响应时间则是先保持小幅上升,到达吞吐量极限后,快速上升,到达系统崩溃点后,系统失去响应。

  4.性能计数器

  性能计数器是描述服务器或操作系统性能的一些数据指标。包括 System Load、对象与线程数、内存使用、CPU使用、磁盘与网络I/O等指标。

  这些指标也是系统监控的重要参数,对这些指标设置报警阈值,当监控系统发现性能计数器超过阈值时,就向运维和开发人员报警,及时发现处理系统异常

  System Load 系统负载,指当前正在被CPU执行和等待被CPU执行的进程数目总和,时反应系统忙闲程度的重要指标。

  多核 CPU 情况下,完美情况是所有 CPU 都在使用,没有进程在等待处理,所以 Load 的理想值是 CPU 的数目。当 Load 值低于 CPU数目的时候,表示 CPU 有空闲,资源存在浪费;

  当 Load 值高于 CPU 数目的时候,表示进程在排队等待 CPU 调度,表示系统资源不足,影响应用程序的执行性能。

  在 Liunx 系统中使用 top 命令查看,该值是三个浮点数,表示最近 1 分钟, 10 分钟,15 分钟的运行队列平均进程数。

4.1.3 性能测试方法 

  性能测试是一个总称,具体可系分为性能测试、负载测试、压力测试、稳定性测试

  性能测试:以系统设计初期规划的性能指标为预期目标,对系统不断施加压力,验证系统在资源可接受范围内,是否能达到性能预期。

  负载测试:对系统不断地增加并发请求以增加系统压力,直到系统的某项或多项性能指标达到安全临界值,如某种资源已经呈饱和状态,这时继续对系统施加压力,系统的处理能力不但不能提高,反而会下降。

  压力测试:超过安全负载的情况下,对系统继续施加压力,直到系统崩溃或不能再处理任何请求,以此获得系统最大压力承受能力。

  稳定性测试:被测试系统在特定硬件、软件、网络环境条件下,给系统加载一定业务压力,使系统运行一段较长时间,以此检测系统是否稳定。在不同生产环境、不同时间点的请求压力是不均匀的,因此为了更好的模拟生产环境,稳定性测试也应不均匀地对系统施加压力。

 

  性能测试是一个不断对系统增加访问压力,以获得系统性能指标、最大负载能力、最大压力承受能力地过程。

  增加访问压力,就是在系统测试环境中,不断增加测试程序的并发请求数,一般来说,性能测试遵循如图4.3所示的抛物线规律。

  如上图,横坐标表示消耗的系统资源,纵坐标表示系统处理能力(吞吐量)。

  在开始阶段随着并发请求数目的增加,系统使用较少的资源就能达到较好的处理能力(a~b段),这一段是网站的日常运行区间,网站的绝大部分访问负载压力都集中在这一段区间,被称为性能测试,测试的目标是评估系统性能是否符合需求及设计目标;

  随着压力的持续增加,系统处理能力增加变缓,知道达到一个最大值(c点),这是系统的最大负载点,这一段被称作负载测试。测试目标是评估当系统因为突发事件超出日常访问压力的情况下,保证系统正常运行情况下能够承受的最大访问负载压力;

  超过这个点后,再增加压力,系统的处理能力反而下降,而资源消耗却更多,直到资源消耗达到极限(d点),这个点可以看作是系统的崩溃点,超过这个点继续加大并发请求数目,系统不能再处理任何请求,这一段被称作压力测试,测试目标是评估可能导致系统崩溃的最大访问负载压力。

4.1.4 性能测试报告

  测试结果报告应能反映上述性能测试曲线的规律,可以得到系统性能是否满足设计目标和业务要求、系统最大负载能力、系统最大压力承受能力等重要信息。

4.1.5 性能优化策略

  性能测试结果不能满足设计或业务需求,就需要寻找系统瓶颈,分而治之,逐步优化。

  1.性能分析

    排查一个网站的性能瓶颈和排查一个程序的性能瓶颈的手法基本相同:检查请求处理的各个环节的日志,分析哪个环节响应时间不合理、超过预期;然后检查监控数据,分析影响性能的主要因素是 内存、磁盘、网络、还是 CPU,

是代码问题还是架构设计不合理,或者系统资源确实不足。

  2.性能优化

    定位产生性能问题的具体原因后,需要进行性能优化,根据网站分层架构,可分为 Web 前端性能优化、应用服务器性能优化、存储服务器性能优化 3 大类。

 

4.2 Web前端性能优化

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

4.2.1 浏览器访问优化

  1.减少 http 请求

    HTTP协议是无状态的应用层协议,意味着每次 HTTP请求都需要建立通信链路、进行数据传输,而在服务器端,每个HTTP都需要启动独立的线程去处理。这些通信和服务的开销都很昂贵,减少 HTTP请求的数目可有效提高访问性能。

    较少HTTP的主要手段是合并 CSS、合并JavaScript、合并图片。将浏览器一次访问需要的 JavaScript、CSS合并成一个文件,这样浏览器就只需要一次请求。图片也可以合并,多张图片合并成一张,如果每张图片都有不同超链接,可通过 CSS 偏移响应鼠标点击操作,构造不同的URL   

  2.使用浏览器缓存

    对于一个网站而言,CSS、JavaScript、Logo、图标这些静态资源文件更新的频率都比较低,而这些文件又几乎是每次 HTTP 请求都需要的,如果将这些文件缓存在浏览器中,可以极好地改善性能。通过设置 HTTP 头中 Cache-Control 和 Expires 的属性,可设定浏览器缓存,缓存时间可以是数天,甚至是几个月。

  在某些时候,静态资源文件变化需要及时应用到客户端浏览器,这种情况,可通过改变文件名实现,即更新 JavaScript 文件并不是更新 JavaScript 文件内容,而是生成一个新的 JS 文件并更新 HTML 文件中的引用。

  使用浏览器缓存策略的网站在更新静态资源时,应采用批量更新的方法,比如需要更新 10 个图标文案,不宜把 10 个文件一次全部更新,而是应一个文件一个文件逐步更新,并有一定的间隔时间,以免用户浏览器突然大量缓存失效,集中更新缓存,造成服务器负载骤增、网络堵塞的情况。

  3.启用压缩

  在服务器端对文件进行压缩,在浏览器端对文件解压缩,可有效减少通信传输的数据量。文本文件的压缩效率可达 80%以上,因此 HTML、CSS、JavaScript 文件启用 GZip压缩可达到较好的效果。

  但是压缩对服务器和浏览器产生一定的压力,在通信带宽良好,而服务器资源不足的情况下要权衡考虑,

  4.CSS放在页面最上面、JavaScript 放在页面最下面

  浏览器会在下载全部 CSS之后才对整个页面进行渲染,因此最好的做法是将 CSS 放在页面的最上面,让浏览器尽快下载 CSS。

  JavaScript 则相反,浏览器在加载 JavaScript 后立即执行,有可能会阻塞整个页面,造成页面显示缓慢,因此  JavaScript 最好放在页面最下面。但如果页面解析时就需要用到JavaScript,这时放在底部就不合适了。

  5.减少 Cookie 传输

  Cookie 包含在每次请求和响应中,太大的 Cookie会严重影响数据传输,因此哪些数据需要写入 Cookie 需要慎重考虑,尽量减少 Cookie中传输的数据量。

  另一方面,对于某些静态资源的访问,如CSS、Script等,发送 Cookie 没有意义,可以考虑静态资源使用独立域名访问,避免请求静态资源时发送 Cookie,减少 Cookie传输的次数。

4.2.2  CDN加速

  CDN(Content Distribute Network 内容分发网络)的本质仍然是一个缓存,而且将数据缓存在离用户最近的地方,使用户以最快速度获取数据,即所谓网络访问第一跳。

  

  CDN部署在网络运营商的机房,这些运营商又是终端用户的网络服务提供商,因此用户请求路由的第一跳就到达了 CDN 服务器,当 CDN 中存在浏览器请求的资源时,从 CDN 直接返回给浏览器,最短路径返回响应,加快用户访问速度,减少数据中心负载。

  CDN 能够缓存的一般是静态资源,如图片、文件、CSS、Script脚本、静态网页等,但是这些文件访问频度很高,将其缓存在 CDN 可极大改善网页的打开速度。

4.2.3 反向代理

  正向代理服务器位于浏览器一侧,代理浏览器将 HTTP 请求发送到互联网上,而反向代理服务器位于网站机房一侧,代理网站 Web 服务器接收 HTTP请求。

  反向代理服务器具有保护网站安全的作用,来自互联网的访问请求必须经过代理服务器,相当于在 Web服务器和可能的网络攻击之间建立了一个屏障。

  除了安全功能,代理服务器也可以通过配置缓存功能加速 Web请求。当用户第一次访问静态内容的时候,静态内容就被缓存在反向代理服务器上,这样当其他用户访问该静态内容的时候,就可以直接从反向代理服务器返回,加速 Web请求响应速度,减轻 Web服务器负载压力。

  事实上,这些网站会把动态内容也缓存在代理服务器上,比如维基百科及某博客论坛网站,把热门词条、帖子、博客缓存在反向代理服务器上加速用户访问速度,当这些动态内容有变化时,通过内部通知机制通知反向代理缓存失效,反向代理会重新加载最新的动态内容再次缓存起来。

  此外,反向代理也可以实现负载均衡的功能,而通过负载均衡构建的应用集群可以提高系统总体处理能力,进而改善网站高并发情况下的性能。

4.3 应用服务器性能优化

  应用服务器就是处理网站业务的服务器,网站的业务代码都部署在这里,是网站开发最复杂,变化最多的地方,优化手段主要有 分布式缓存、异步操作、集群

  4.3.1 分布式缓存

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

    1.缓存的基本原理

      缓存指将数据存储在相对较高访问速度的存储介质中,以供系统处理。一方面缓存访问速度快,可以减少数据访问的时间,另一方面如果缓存的数据是经过计算处理得到的,那么被缓存的数据无需重复计算即可以直接使用,因此缓存还起到减少计算时间的作用。

      缓存的本质是一个内存 Hash表,网站应用中,数据缓存以一对 key、value的形式存储在内存 Hash 表中。Hash 表数据读写的时间复杂度O(I),如下图为一对 KV 在Hash表中的存储。

      

  计算 KV 对中 Key 的 HashCode 对应的 Hash表索引,可快速访问 Hash 表中的数据。许多语言支持获得任意对象的 HashCode,可以把 HashCode 理解为对象的唯一标示符,Java语言中 Hashcode 方法包含在根对象 Object中,其返回值是一个 Int。

  然后通过 Hashcode 计算 Hash表的索引下标,最简单的余数法,使用 Hash表数组长度对 Hashcode求余,余数即为 Hash表索引,使用该索引可直接访问得到 Hash 表中存储的 KV 对。Hash 表是软件开发中常用到的一种数据结构,其设计思想在很多场景下都可以应用。

  缓存主要用来存放那些读写比很高、很少变化的数据,如商品的类目信息,热门词的搜索列表信息,热门商品信息等。应用程序读取数据时,先到缓存中读取,如果读取不到或数据已失效,在访问数据库,并将数据写入缓存。

    

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

 2.合理使用缓存

  使用缓存对提高系统性能有很多好处,但是不合理使用缓存非但不能提高系统地性能,还会成为系统地累赘,甚至风险。实践中,缓存滥用地情景屡见不鲜----过分依赖低可用的缓存系统,不恰当地使用缓存地数据访问特性等。

  频繁修改的数据

  如果缓存中保存的是频繁修改的数据,就会出现数据写入缓存中,应用还来不及读取缓存,数据就已失效的情形,徒增系统负担。一般来说,数据的读写比在2:1以上,即写入一次缓存,在数据更新前至少读取两次,缓存才有意义。实践中,这个读写比通常非常高,比如新浪微博的热门微博,缓存以后可能被读取数百万次。

  没有热点的访问

  缓存使用内存作为存储,内存资源宝贵而有限,不可能将所有数据都缓存起来,只能将最新访问的数据缓存起来,而将历史数据清理出缓存。如果应用系统访问数据没有热点,不遵循二八定律,即大部分数据访问并没有集中在小部分数据上,那么缓存就没有意义,因为大部分数据还没有被再次访问就已经被基础缓存了。

  数据不一致与脏读

  一般会对缓存的数据设置失效时间,一旦超过失效时间,就要从数据库中重新加载。因此应用要容忍一定时间的数据不一致,如卖家已经编辑了商品属性,但需要过段时间才能被买家看到。在互联网应用中,这种延迟通常是可以接受的,但是具体应用仍要慎重对待。还有一种策略是数据更新时立即更新缓存,不过这也会带来更多系统开销和事务一致性的问题。

  缓存雪崩

  缓存时为了提高数据读取性能的,缓存数据丢失或缓存不可用不会影响应用程序的处理--它可以从数据库中直接获取数据。但是随着业务发展,缓存会承担大部分数据访问的压力,数据库已经习惯了有缓存的日子,所以当缓存服务崩溃时,数据库会因为完全不能承受如此大的压力而宕机,进而导致整个网站不可用。这种情况被称作缓存雪崩,发生这种故障,甚至不能简单地重启缓存服务器和数据库服务器来恢复网站访问。

  实践中,有的网站通过缓存热备等手段提高缓存可用性:当某台缓存服务器宕机时,将缓存访问切换到热备服务器上。但时这种设计显然有违缓存地初衷,缓存根本就不应该被当做一个可靠地数据源来使用。

  通过分布式缓存服务器集群,将缓存数据分布到集群多台服务器上可在一定程度上改善缓存地可用性。当一台缓存服务器宕机地时候,只有部分缓存数据丢失,重新从数据库加载这部分数据不会对数据库产生很大影响。

  缓存预热

  缓存中存放的是热点数据,热点数据又是缓存系统利用 LRU(最近最久未用算法)对不断访问的数据筛选淘汰出来的,这个过程需要花费较长的时间。新启动的缓存系统如果没有任何数据,在重建缓存数据的过程中,系统的性能和数据库负载都不太好,那么最好在缓存系统启动时就把热点数据加载好,这个缓存预加载手段叫作缓存预热。对于一些元数据如城市地名表、类目信息,可以在启动时加载数据库中全部数据到缓存进行预热。

  缓存穿透

  因为不恰当的业务或者恶意攻击持续高并发地请求某个不存在的数据,由于缓存没有保存该数据,所有的请求都会落在数据库上,会对数据库造成很大压力,甚至崩溃。这叫缓存穿透,一个简单的对策是将不存在的数据也缓存起来(value值为null)

 3.分布式缓存架构

  分布式缓存是指缓存部署在多台服务器组成的集群中,以集群方式提供缓存服务,其架构方式有两种,一种是以 JBoss Cache 为代表的需要更新同步的分布式缓存,一种是以Memcached为代表的不互相通信的分布式缓存。

   JBoss Cache 的分布式缓存在集群中所有服务器中保存相同的缓存数据,当某台服务器有缓存数据更新的时候,会通知集群中其他机器更新缓存数据或清除缓存数据,如图所示, JBoss Cache通常将应用程序和缓存部署在用一台服务器上,应用程序可从本地快速获取缓存数据,但是这种方式带来的问题是缓存数据的数量受限于单一服务器的内存空间,而且当集群规模较大的时候,缓存更新信息需要同步到集群所有机器,其代价惊人。因而这种方案更多见于企业应用系统中,很少在大型网站使用。

  

  大型网站需要缓存的数据量一般都很庞大,可能会需要数 TB的内存做缓存,这时候就需要另一种分布式缓存,如下如,Memcached 采用一种集中式的缓存集群管理,也被称为互不通信的分布式架构方式。缓存与应用分离部署,缓存系统部署在一组专门的服务器上,应用程序通过一致性Hash等路由算法选择缓存服务器远程访问缓存数据,缓存服务器之间不通信,缓存集群的规模可以很容易地实现扩容,具有良好地可伸缩性。

  4.Mamcached

    Mamcached 曾一度是网站分布式缓存地代名词,被大量网站使用。其简单的设计、优异的性能、互不通信的服务器集群、海量数据可伸缩的架构令网站架构师趋之若鹜。

 

简单的通信协议

  远程通信设计需要考虑两个方面的因素,一种是通信协议,即选择 TCP 协议还是 UDP协议,或是HTTP协议;一是通信序列化协议,数据传输的两端,必须使用彼此可识别的数据序列化方式才能使通信完成,如XML、JSON等文本序列化协议,或者 Google Protobuffer等二进制序列化协议。

  Memcached  使用 TCP 协议通信,其序列化协议则是一套基于文本的自定义协议,非常简单,以一个命令关键字开头,后面是一组命令操作数。例如读取一个数据的命令协议是 get<key>.Memcached以后,许多 NoSQL产品都借鉴了或直接支持这套协议

丰富的客户端程序

  Memcached通信协议非常简单,只要支持该协议的客户端都可以和 Memcached 服务器通信,因此 Memcached 发展出非常丰富的客户端程序,几乎支持所有主流的网站编程语言。

高性能的网络通信

  Memcached 服务端通信模块基于 Libevent,一个支持事件触发的网络通信程序库。 Libevent 的设计和实现有许多值得改善的地方,但它在稳定的长连接方面的表现却正是 Memcached 需要的。

高效的内存管理

  内存管理中一个令人头痛的问题就是内存碎片管理。操作系统,虚拟机垃圾回收在这方面想了许多办法:压缩、复制等。Memcached使用了一个非常简单的办法---固定空间分配。Memcached 将内存空间分为一组 slab,每个 slab里又包含一组 chunk,同一个 slab 里的每个 chunk 的大小是固定的,拥有相同大小 chunk 的 slab 被组织在一起,叫作 slab_class,如下图。存储数据时根据数据的 Size大小,寻找一个大于 Size 的最小 chunk 将数据写入。这种内存管理方式避免了内存碎片管理的问题,内存的分配和释放都时以 chunk 为单位的。和其他缓存一样,Memcached采用 LRU 算法释放最近最久未被访问的数据占用的空间,释放的 chunk 被标记为未用,等待下一个合适大小数据的写入。

  当然这种方式也会带来内存浪费的问题。数据只能存入一个比它大的chunk里,而一个chunk只能存一个数据,其他空间被浪费了。如果启动参数配置不合理,浪费会更加惊人,发现没有缓存多少数据,内存空间就用尽了。

  

互不通信的服务器集群架构

   正式这个特性使得 Memcached 从 JBoss Cache、OSCache等众多分布式缓存中脱颖而出,满足网站对海量缓存数据的需求。而其客户端路由算法一致性 Hash 更成为数据存储伸缩性架构设计的经典范式。事实上,正式集群内服务器互不通信使得集群可以做到几乎无限制的线性伸缩,这也正式目前流行的许多大数据技术的基本架构特点。

  4.3.2 异步操作

  使用消息队列将调用异步化,可改善网站的扩展性。事实上,使用消息队列还可以改善网站系统的性能。

  在不使用消息队列的情况下,用户的请求数据直接写入数据库,在高并发的情况下,会对数据库造成巨大的压力,同时也使得响应延迟加剧。

  在使用消息队列后,用户请求的数据发送给消息队列后立即返回,再由消息队列的消费者进程(通常情况下,该进程通常独立部署在专门的服务器集群上)从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度远快于数据库(消息队列服务器也比数据库具有更好的伸缩性),因此用户的响应延迟可得到有效改善。

 

  消息队列具有很好的削峰作用------即通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。在电子商务网站促销活动中,合理使用消息队列,可有效抵御促销活动刚开始大量涌入的订单对系统造成的冲击。如下图:

  需要注意的是,由于数据写入消息队列后立即返回给用户,数据在后续的业务校验、写数据库等操作可能失败,因此在使用消息队列进行业务异步处理后,需要适当修改业务流程进行配合,如订单提交后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单,甚至商品出库后,再通知用户订单成功,以免交易纠纷。

  4.3.3 使用集群

    在网站高并发访问的场景下,使用负载均衡技术为一个应用构建一个由多台服务器组成的服务器集群,将并发访问请求分发到多台服务器上处理,避免单一服务器因负载压力过大而响应缓慢,使用户请求具有更好的响应延迟特性。

    

 

  三台 Web 服务器共同处理来自用户浏览器的访问请求,这样每台 Web 服务器需要处理的 http 请求只有总并发请求数的三分之一,根据性能测试曲线,使服务器的并发请求数目控制在最佳运行区间,获得最佳的访问请求延迟。

  4.3.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 容器管理的线程,还是应用程序自己创建的线程,一台服务器上启动多少线程合适?假设服务器上执行的都是相同类型任务,针对该类任务启动的线程数有个简化的估算公式可供参考:

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

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

  多线程编程一个需要注意的问题是线程安全问题,即多线程并发对某个资源进行修改,导致数据混乱。这也是最容易犯错的地方,而线程安全 Bug又难以测试重现,网站故障中,许多所谓偶然发生的“灵异事件”都和多线程并发问题有关。对网站而言,不管有没有进行多线程编程,工程师写的每行代码都会被多线程执行,因为用户请求是并发提交的,也就是说,所有资源---对象、内存、文件、数据库,乃至另一个线程都可能被多线程并发访问。

  编程上,解决线程安全的主要手段有如下几点。

  将对象设计为无状态对象:所谓无状态对象是指本身不存储状态信息(对象无成员变量,或者成员变量也是无状态对象),这样多线程并发访问的时候就不会出现状态不一致,Java Web 开发中常用的 Servlet 对象就设计为无状态对象,可以被应用服务器多线程并发调用处理用户请求。而 Web 开发中常用的贫血模型对象都是些无状态对象。不过从面向对象设计的角度看,无状态对象是一种不良设计。

  使用局部对象:即在方法内部创建对象,这些对象会被每个进入该方法的线程创建,除非程序有意识地将这些对象传递给其他线程,否则不会出现对象被多线程并发访问的情形。

  并发访问资源时使用锁:即多线程访问资源的时候,通过锁的方式使多线程并发操作转化为顺序操作,从而避免资源被并发修改。随着操作系统和编程语言的进步,出现各种轻量级锁,使得运行期线程获取锁和释放锁代价都变得更小,但是锁导致线程同步顺序执行,可能会对系统性能产生严重影响。

  2.资源复用

  系统运行时,要尽量减少那些开销很大的系统资源的创建和销毁,比如数据库连接、网络通信连接、线程、复杂对象等。从编程角度,资源复用主要有两种模式:单例和对象池。

  单例虽然是 GoF 经典设计模式中较多被诟病的一个模式,但由于目前 Web开发中主要使用贫血模式,从 Service 到Dao 都是些无状态对象,无需重复创建,使用单例模式也就自然而然了。事实上,Java 开发常用的对象容器 Spring 默认构造的对象都是单例。

  对象池模式通过复用对象实例,减少对象创建和资源消耗。对于数据库连接对象,每次创建连接,数据库服务端都需要创建专门的资源以应对,因此频繁创建关闭数据库连接,对数据库服务器而言是灾难性的,同时频繁创建关闭连接也需要花费较长的时间。因此在实践中,应用程序的数据库连接基本都使用连接池的方式。数据库连接对象 创建好之后,将连接对象放入对象池容器中,应用程序要连接的时候,就从对象池中获取一个空闲的连接使用,使用完毕再将改该对象归还到对象池中即可,不需要创建新的连接。

  前面说过,对于每个 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”的 HahsCode 是 2211.这在某些应用场景是不能接受的, 这种情况下,一个可行的方案是对字符串取信息指纹,再对信息指纹求 HashCode,由于字符串微小的变化就可以引起信息指纹的巨大不同,因此可以获得较好地随机散列

          原始字符串--MD5--->信息指纹---Hash计算--->HashCode

   4.垃圾回收

  如果 Web 应用运行在 JVM 等具有垃圾回收功能的环境中,那么垃圾回收可能会对系统 的性能特性产生巨大影响。理解垃圾回收机制有助于程序优化和参数调优,以及编写内存安全的代码。

  以 JVM 为例,其内存主要可划分为 堆(heap)和 堆栈(stack)。堆栈用于存储线程上下文信息,如方法参数、局部变量等。堆则是存储对象的内存空间,对象的创建和释放、垃圾回收就在这里进行。通过对对象生命周期的观察,发现大部分对象的生命周期都及其短暂,这部分对象产生的垃圾应该被更快地收集,以释放内存,这就是 JVM 分代垃圾回收。基本原理如下图


  在JVM分代垃圾回收机制中,将应用程序可用的堆空间分为年轻代(Young Generaion)和老年代(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。

4.4 存储性能优化

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

  4.4.1 机械硬盘 vs. 固态硬盘

  机械硬盘是目前最常用的一种硬盘,通过马达驱动磁头臂,带动磁头到指定的磁盘位置访问数据,由于每次访问数据都需要移动磁头臂,因此机械硬盘在数据连续访问(要访问的数据存储在连续的磁盘空间上)和随机访问(要访问的数据存储在不连续的磁盘空间)时,由于移动磁头臂的次数相差巨大,性能表现差别也非常大。机械硬盘结构如下图

  机械硬盘如图:

  

  固态硬盘又称为 SSD 或 Flash 硬盘,这种硬盘没有机械装置,数据存储在可持久记忆的硅晶体上,因此可以像内存一样快速随机访问。而且 SSD 具有更小的功耗和更少的磁盘震动与噪声。SSD硬盘如图:

  

  在网站应用中,大部分应用访问数据都是随机的,这种情况下 SSD 具有更好的性能表现。但是目前 SSD 硬盘还不太成熟,可靠性、性价比有待提升,因此 SSD 的使用还在摸索阶段。

  4.4.2 B+树 vs. LSM树

  由于传统的机械磁盘具有快速顺序读写、慢速随机读写的访问特性,这个存储特性对磁盘存储结构和算法的选择影响甚大。

  为了改善数据访问特性,文件系统或数据库系统通常会对数据排序后存储,加快数据检索速度,这就需要保证数据在不断更新、插入、删除后依然有序,传统关系数据库的做法是使用 B+树,如图所示

  

  B+树是一种专门针对磁盘存储而优化的N叉排序树,以树节点为单位存储在磁盘中,从根开始查找所需数据所在的节点编号和磁盘位置,将其加载到内存中然后继续查找,知道找到所需的数据。

  目前数据库多采用两级索引的B+树,树的层次最多三层。因此可能需要5次磁盘访问才能更新一条记录(三次磁盘访问获得数据索引及行 ID,然后再进行一次数据文件读操作及一次数据文件写操作)

  但是由于每次磁盘访问都是随机的,而传统机械硬盘在数据随机访问时性能较差,每次数据访问都需要多次访问磁盘影响数据访问性能。

  目前许多 NoSQL产品采用 LSM 树作为主要数据结构

  

  LSM 树可以看作是一个 N 阶合并树。数据写操作(包括插入、修改、删除)都在内存中进行,并且都会创建一个新记录(修改会记录新的数据值,而删除会记录一个删除标志),这些数据在内存中仍然还是一颗排序树,当数据量超过设定的内存阈值后,会将这颗排序树和磁盘上最新的排序树合并。当这可排序树的数据量也超过设定阈值后,和磁盘上下一级的排序树合并。合并过程中,会用最新更新的数据覆盖旧的数据(或者记录为不同版本)。

  在需要进行读操作时,总是从内存中排序树开始搜索,如果没有找到,就从磁盘上的排序树顺序查找。

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

  作为存储结构,B+ 树不是关系数据库所独有的,NoSQL 数据库也可以使用B+树。同理,关系数据库也可以使用 LSM。

  4.4.3 RAID vs. HDFS

  

  

  

  

 

 

 

 

     

 

posted @ 2019-08-10 22:13  veggiegfei  阅读(709)  评论(0编辑  收藏  举报