大型网站架构模式(三)

瞬时响应:网站的高性能架构


什么叫高性能的网站?

两个网站性能架构设计方案:A方案和B方案,A方案在小于100个并发用户访问时,每个请求的响应时间是1秒,当并发请求达到200的时候,请求的响应时间将骤增到10秒。B方案不管是100个并发用户访问还是200个并发用户访问,每个请求的响应时间都差不多是1.5秒。那个性能更好?如果老板说“我们要改善网站的性能”,他指的是什么?

同类型的两个网站,X网站服务器平均每个请求的处理时间是500毫秒,Y网站服务器平均每个请求的处理时间是1000毫秒,为什么用户却反映Y网站的速度快呢?

网站性能是指客观的指标,可以具体体现到响应时间、吞吐量等技术指标,同时也是主观的感受,而感受则是一种与具体参与者相关的微妙的东西,用户的感受和工程师的感受不同,不同的用户感受也不同。

一、网站性能测试

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

1.1、不同视角下的网站性能

软件工程师说道网站性能的时候,通常和用户说的不一样。

1、用户视角的网站性能

从用户角度,网站性能就是用户在浏览器上直观感受到的网站响应速度快还是慢。用户感受到的时间,包括用户计算机和网站服务通信的时间、用户服务器处理的时间、用户计算机浏览器构造请求解析响应数据的时间,如图所示。

图4-1 用户视角的网站性能

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

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

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

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

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

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

1.2、性能测试指标

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

1、响应时间

指应用执行一个操作需要的时间,包括从发出请求开始到收到后最后响应数据所需要的时间。响应时间是系统最重要的性能指标,直观的反映了系统的“快慢”。下表列出了一些常用系统操作需要的响应时间。

常用系统操作响应时间表
操作 响应时间
打开一个网站 几秒
在数据库中查询一条记录(有索引) 十几毫秒
机械磁盘一次寻址定位 4毫秒
从机械磁盘顺序读取1MB数据 2毫秒
从SSD磁盘顺序读取1MB数据 0.3毫秒
从远程分布式缓存Redis读取一个数据 0.5毫秒
从内存中读取1MB数据 十几微秒
Java程序本地方法调用 几微秒
网络传输2KB数据 1微秒

测试程序通过模拟应用程序,记录收到响应和响应发出请求的时间差来计算系统想用时间。但是记录及获取系统时间这个操作也需要花费一定时间,如果测试目标操作本身需要花费的时间极少,比如几微秒,那么测试程序就无法测试得到系统的响应时间。实践中通常采用的办法是重复请求,比如一个方法请求操作重复执行一万次,测试一万次执行需要的总响应之和,然后除以一万,得到单次请求的响应时间。

2、并发数

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

与网站并发用户数相对应的还有网站在线用户数(当前登录网站的用户总数)和网站系统用户数(可能访问系统的总用户数,对多数网站而言就是注册用户数)。其数量比较关系为:

网站系统用户数>>网站在线用户数>>网站并发用户数

在网站产品设计初期,产品经理和运营人员就需要规则不同发展阶段的网站系统用户数,并以此为基础,根据产品特性和运营手段,推算在线用户数和并发用户数。这些指标将成为系统非功能设计的重要依据。

现实中,经常看到某些网站,特别是电商网站,市场推广人员兴致勃勃地打广告打折促销,用户兴致勃勃地去抢购,结果活动刚一开始,就因为并发用户数超过网站最大负载而响应缓慢,急性子的用户不停的刷新浏览器,导致系统并发数量更高,最后以服务器系统崩溃,用户浏览器显示“Service is too busy”而告终。出现这种情况,有可能是网站技术准备不充分导致,也有肯呢个是运营人员错误地评估并发用户数导致。

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

3、吞吐量

指单位时间内系统处理的请求数量,体现系统的整体处理能力。对于网站,可以用“请求数/秒”或是“页面数/秒”来衡量,也可以用“访问人数/天”或是“处理的业务数/小时”等衡量。TPS(每秒事务数)是吞吐量的一个常用量化指标,此外还有HPS(每秒HTTP请求数)、QPS(每秒查询数)等。

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

而这个过程中,响应时间则是先保持小幅上升,到达吞吐量极限后,快速上升,到达系统崩溃点后,系统失去响应。系统吞吐量、系统并发数及响应时间之间的关系将在本章后面内容中介绍。

系统吞吐量和系统并发数,以及响应时间的关系可以形象的理解为告诉公路的通行状况:吞吐量是每天通过收费站的车辆数目(可以换算郑收费站收取的高速费)并发数是高速公路上的正在行驶的车辆数目,响应时间是车速。车辆很少时,车速很快,但是收到的高速费也相对较少;随着高速公路上车辆数目的增多,车速略受影响,但是收到的高速费增加很快;随着车辆的继续增加,车速变得越来越慢,高速公路变得越来越堵,收费不增反降;如果车流量继续增加,超过某个极限之后,任何偶然因素都会导致高速公路瘫痪,车走不动,费当然也收不着,而高速公路成了停车场(资源耗尽)。

网站性能优化的目的,除了改善用户体验的响应时间,还要尽量提高系统吞吐量,最大限度利用服务器资源。

4、性能计数器

它是描述服务器或者操作系统性能的一些数据指标。包括System Load、对象与线程数、内存使用、CPU使用、磁盘与网络I/O等指标。这些指标也是系统监控的重要参数,对这些指标设置报警阈值,当监控系统发现性能计数器超过阈值时,就向运维和开发人员报警,及时发现处理系统异常。

System Load即系统负载,指当前正在被CPU执行和等待被CPU执行的进程数目总和,是反映系统忙闲程度的重要指标。多核CPU的情况下,完美情况是所有CPU都在使用,没有进行在等待处理,所以Load的理想值是CPU的数目。当Load值低于CPU数目的时候,表示CPU有空闲,资源存在浪费,当Load值低于CPU数目的时候,表示进程在排队等待CPU调度,表示系统资源不足,影响应用程序的执行性能。在linux系统中使用top命令查看,该值是三个浮点数,表示最近1分钟,10分钟,15分钟的运行队列平均进程数,如下图所示。

在Linux命令行查看系统负载

 

1.3、性能测试方法

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

性能测试

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

负载测试

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

压力测试

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

稳定性测试

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

性能测试一个不断对系统增加访问压力。以获得系统性能指标、最大负载能力、最大压力承受能力的过程。所谓的增加访问压力,在系统测试环境中,就是不断增加测试程序的并发请求数,一般说来,性能测试遵循下图所示的抛物线规律。

下图中的横坐标表示小号的系统资源,纵坐标表示系统处理能力(吞吐量)。在开始阶段,随着并发请求数目的增加,系统使用较少的资源就达到了较好的处理能力(a~b段),这一段是网站的日常运行区间,网站的绝大部分访问负载压力都集中在这一段区间,被称为性能测试,测试目标是评估系统性能是否符合需求及设计目标;随着压力的持续增加,系统处理能力增加变缓,直到一个最大值(c点),这是系统的最大负载点,这一段被称作负载测试。测试目标是评估当系统因为突发事件超出日常访问压力的情况下,保证系统正常运行情况下能够承受的最大访问负载压力;超过这个点后,在增加压力,系统的处理能力反而下降,而资源消耗却更多,指导资源消耗达到极限(d点),这个点可以看做是系统的崩溃点,超过这个点继续加大并发请求数目,系统不能再处理任何请求,这一段被称作压力测试,测试目标是评估可能导致系统崩溃的最大访问负载压力。

性能测试曲线

性能测试反映的是系统在实际生产环境中使用时,随着用户并发访问数量的增加,系统的处理能力。与性能曲线相对应的是用户访问的等待时间(系统响应时间),如下图所示。

并发用户访问响应时间曲线

在日常运行区间,可以获得最好的用户响应时间,随着并发用户数的增加,响应延迟越来越大,直到系统崩溃,用户失去响应。

1.4 性能测试报告

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

性能测试结果报告
并发数 响应时间(ms) TPS 错误率(%) Load 内存(GB) 备注
10 500 20 0 5 8 性能测试
20 800 30 0 10 10 性能测试
30 1000· 40 2 15 14 性能测试
40 1200 45 20 30 16 负载测试
60 2000 30 40 50 16 压力测试
80 超时 0 100 不详 不详 压力测试

1.5 性能优化策略

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

1、性能分析

大型网站结构负载,用户总浏览器发出请求指导数据库完成操作事务,中间需要经过很多环节,如果测试或者用户报告网站响应缓慢,存在性能问题,必须对请求经历的各个环节进行分析,排查可能出现性你那个瓶颈的地方,定位问题。

排查一个网站的性能的瓶颈和排查一个程序的性能瓶颈的手法基本相同:检查请求处理的各个环节的日式,分析哪个环节响应时间不合理、超过预期;然后检查监控数据,分析影响性能的主要因素是内存、磁盘、网络、还是CPU,是代码问题还是架构设计不合理,或者系统资源确实不足。

2、性能优化

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

二、WEB前端性能优化

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

2.1 浏览器访问优化

1、减少 http 请求

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

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传输的次数。

2.2 CDN 加速

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

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

利用CDN的网站架构

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

2.3 反向代理

传统代理服务器位于浏览器一侧,代理浏览器将HTTP请求发送到互联网上,而反向代理服务器位于网站机房一侧,代理网站服务器接收HTTP请求。如下图所示。

利用反向代理的网站架构

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

除了安全功能,代理服务器也可以通过配置缓存功能急速Web请求。当用户第一次访问静态内容的时候,静态内容就被缓存在反向代理服务器上,这样当其他用户访问改静态内容的时候,静态内容就被缓存在反向代理服务器上,这样当其他用户访问该静态内容的时候,就可以直接从反向代理服务器上返回,加速Web请求响应速度,减轻web服务器负载压力。事实上,有些网站会把动态内容也缓存在代理服务器上,比如维基百科及某些博客论坛网站,把人们词条、帖子、博客缓存在反向代理服务器上加速用户访问速度,当这些动态内容有变化时,通过内部通知机制通知反向代理缓存失效,反向代理会重新加载最新的动态内容再次缓存起来。

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

三、应用服务器性能优化

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

3.1、 分布式缓存

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

        网站性能优化第一定律:优先考虑使用缓存优化性能

1、缓存的基本原理

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

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

计算 KV 对中 Key 的 HashCode 对应的 Hash 表索引,可快速访问 Hash 表中的数据。许多语言支持获得任意对象的HashCode,可以把 HashCode 理解为对象的唯一标识符,Java语言中 HashCode 方法包含在根对象 Object 中,其返回值是一个 Int。然后通过 Hashcode计算 Hash 表的索引下标。最简单的是余数法,使用Hash表数组长度对Hashcode求余,余数即为Hash表索引,使用该索引可直接访问得到 Hash表中存储的 KV 对。Hash 表是软件开发中常用到的一种数据结构,其设计思想在很多场景下都可以应用。

Hash 表存储例子

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

使用缓存存取数据

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

2、合理使用缓存

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

频繁修改的数据

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

没有热点的访问

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

数据不一致与脏读

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

缓存可用性

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

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

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

产品在设计之初就需要一个明确的定位:什么是产品要实现的功能,什么不是产品提供的特性。在产品漫长的生命周期中,会有形形色色的困难和诱惑来改变产品的发展方向,左右摇摆、什么都想做的产品,最后有可能成为一个失去生命力的四不像。

缓存预热

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

缓存穿透

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

3、分布式缓存架构

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

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

需要更新同步的JBoss Cache

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

4、Memcached

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

 简单的通信协议

远程通信设计需要考虑两方面的要素,一是通信协议,即选择TCP协议还是UDP协议,抑或HTTP协议;一是通信序列协议,数据传输的两端,必须使用彼此可识别的数据序列化方式才能使通信得以完成,如XML、JSON等文本序列化协议,或者Google Protobuffer等二进制序列化协议。Memcached使用TCP协议(UDP也支持)通信,其序列化协议规则是一套基于文本的自定义协议,非常简单,以一个命令关键字开头,后面是一组命令操作数。例如读取一个数据的命令协议是get <key>。 Memcached 以后,许多NoSQL产品都借鉴了或直接支持这两套协议。

丰富的客户端程序

Memcached 通信协议非常好简单,只要支持该协议的客户端都可以和Memcached服务器通信,因此Memcached发展处非常丰富的客户端程序,几乎支持所有主流网站编程语言,Java、C/C++/C#、Perl、Python、PHP、Ruby等,因此在混合使用多种编程语言的网站,Memcached更是如鱼得水。

高性能的网络通信

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

高效的内存管理

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

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

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

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

虽然近些年许多NoSQL产品层出不穷,在数据持久化、支持负载数据结构、甚至性能方面有许多产品优于Memcached,但Memcached由于其简单、稳定、专注的特点,仍然在分布式缓存领域占着重要地位。

3.2、异步操作

使用消息队列将调用异步化,可改善网站的扩展性。事实上,使用消息队列还可改善网站系统的性能,如图4-12和图4-13所示。

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

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

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

    任何可以晚点做的事情都应该晚点做。

3.3、使用集群

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

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

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、资源复用

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

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

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

前面说过,对于每个Web请求(HTTP Request),Web应用服务器都需要创建一个独立的线程去处理,这方面,盈通服务器也采用线程池(Thread Pool)的方式。这些所谓的连接池、线程池,本质上都是对象池,即连接、线程都是对象,池管理方式也基本相同。

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(Grabage Collection,垃圾回收),将还被使用的对象复制到From区,这样整个Eden区都是未被使用的空间,可供继续创建对象,当Eden区再次用完,再次触发一次Youngb GC,将Eden区和From区还被使用的对象复制到From区。因此经过多次Young GC,某些对象会在From区和To区还被使用的对象复制到From区。因此,经过多次Young GC,某些对象会在From区和To区多次复制,如果超过某个阈值对象还未被释放,则将该对象复制到Old Generation。如果Old Generation空间也已经用完,那么就会触发Full GC,即所谓的全量回收,全量回收会对系统性能呢产生较大影响,因此应根据系统业务特点和对象生命周期,合理设置Young Generation 和Old Generation 大小,尽量减少Full GC。事实上,某些Web应用在整个运行期间可以做到从不进行Full GC。

四、存储性能优化

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

4.1 机械硬盘 vs 固态硬盘

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

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

在网站应用中,大部分应用访问数据都是随机的,这种情况下SSD具有更好的性能表现。但目前SSD硬盘还不太成熟,可靠性、性价比有待提升,因此SSD的使用还在摸索阶段。但是相信随着SSD工艺水平的提高,逐步替代传统机械硬盘是迟早的事。

4.2 B+数 vs LSM数

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

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

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

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

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

目前许多NoSQL产品采用LSM树作为主要数据结构,如图4-21所示。

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

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

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

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

4.3 RAID vs HDFS

RAID(廉价磁盘冗余阵列)技术主要是为了改善磁盘的访问延迟,增强磁盘的可用性和容错能力。目前服务器级别的计算机都支持插入多块磁盘(8块或者更多),通过使用RAID技术,实现数据在多块磁盘上的并发读写和数据备份。

常用的RAID技术有以下几种,如下图所示。

假设服务器有N块磁盘。

RAID0

数据在从内存缓冲区写入磁盘时,根据磁盘数量将数据分成N份,这些数据同时并发写入N块磁盘,使得数据整体写入速度是一块磁盘的N倍。读取时也是一样,因此RAID0具有极快的数据读写速度,但是RAID0不做数据备份,N块磁盘中只要有一块损坏,数据完整性就被破坏,所有磁盘数据都会损坏。

RAID1

数据在写入磁盘时,将一份数据同时写入两块磁盘,这样任何一块磁盘损坏都不会导致数据丢失,插入一块新新磁盘就可以通过复制数据的方式自动修复,具有极高的可靠性。

RAID10

结合RAID0和RAID1两种方案,将所有磁盘平均分成两份,数据同时在两份磁盘写入,相当于RAID1,但是在每一分磁盘里面的N/2块磁盘上,利用RAID0技术并发读写,既提高可靠性又改善性能,不过RAID10的磁盘利用率较低,有一半的磁盘用来写备份数据。

RAID3

一般情况下,一台服务器上不会出现同时损坏两块磁盘的情况,在只损坏了一块磁盘的情况时,如果能利用其它磁盘的数据恢复损坏磁盘的数据,这样在保证可靠性和性能的同时,磁盘利用率也得到大幅提升。

在数据写入磁盘的时候,将数据分成N-1份,并发写入N-1块磁盘,并在第N块磁盘记录校验数据,任何一块磁盘损坏(包括检验数据磁盘),都可以利用其它N-1块磁盘的数据修复。

但是在数据修改较多的场景中,修改任何磁盘数据都会导致第N块磁盘重写校验数据,频繁写入的后果是第N块磁盘比其它磁盘容易损坏,需要频繁更换,所以RAID3很少在实践中使用。

RAID5

相比RAID3,方案RAID5被更多地使用。

RAID5和RAID3很相似,但是校验数据不是写入第N块磁盘,而是螺旋式地写入所有磁盘中。这样校验数据的修改也被平均到所有磁盘上,避免RAID3频繁写坏一块磁盘的情况。

RAID6

如果数据需要很高的可靠性,在出现同时损坏两块磁盘的情况下(或者运维管理水平比较落后,导致坏了一块磁盘),仍需要修复数据,这时候可以使用RAID6。

RAID6

如果数据需要很高的可靠性,在出现同时损坏两块磁盘的情况下(或者运维管理水平比较落后,坏了一块磁盘但是迟迟没有更换,导致又坏了一块磁盘),仍需要修复数据,这时候可以使用RAID6.

RAID6和RAID5类似,但是数据只写入N-2块磁盘,并螺旋式地在两块磁盘中写入校验信息(使用不同算法生成)。

在相同磁盘数目(N)的情况下,各种RAID技术的比较如表4-3所示。

RAID技术可以通过硬件实现,比如专用的RAID卡或者主板直接支持,也可以通过软件实现,RAID技术在传统关系数据库及文件系统中应用比较广泛,但是在大型网站比较喜欢用的NoSQL,以及分布式文件系统中,RAID技术却遭冷落。

例如在HDFS(Hadoop分布式文件系统)中,系统在整个存储集群的多台服务器上进行数据并发读写和备份,可以看做在服务器集群规模上实现了类似RAID的功能,因此不需要磁盘RAID。

当对文件进行处理计算时,通过MapReduce并发计算任务框架,可以启动多个计算子任务(MapReduce Task),同时读取文件的多个Block,并发处理,相当于实现了RAID0的并发访问功能。

HDFS架构如图4-23所示。

在HDFS中有两种重要的服务器角色:NameNode(名字服务节点)和DataNode(数据存储节点)。NameNode在整个HDFS中只部署一个实例,提供元数据服务,相当于操作系统的文件分配表(FAT),管理文件名Block的分配,维护整个文件系统的目录树结构。DataNode则部署在HDFS集群中其他所有服务器上,提供真正数据存储服务。

和操作系统一样,HDFS对数据存储空间的管理以数据块(Block)为单位,只是比操作系统中的数据块(512字节)要大得多,默认为64MB。HDFS将DataNode上的磁盘空间分成N个这样的块,供程序使用。

应用程序(Client)需要写文件时,首先访问NameNode,请求分配数据块,NameNode根据管理的DataNode服务器的磁盘空间,按照一定的负载均衡策略,分配给若干数据块供Client使用。

当CLient写完一个数据块时,HDFS会将这个数据块再复制两份存储在其他DataNode服务器上,HDFS默认同一份数据有三个副本,保证数据可靠性。因此在HDFS中,及时DataNode服务器有多块磁盘,也不需要使用RAID进行数据备份,而是在整个集群上进行数据复制,而且系统一旦发现某台服务器宕机,会自动利用其它机器上的数据将这台服务器上存储的数据块自动再备份一份,从而获得更高的数据可靠性。

HDFS配合MapReduce等并行计算框架进行大数据处理时,可以在整个集群上并发读写访问所有的磁盘,无需RAID支持。

五、小结

网站性能优化技术是在网站性能遇到问题时的解决方案,而网站的性能问题很多是在用户高并发访问时产生的。所以网站性能优化的主要工作是改善高并发用户访问情况下的网站响应速度。本章开篇所举的例子,当老板说“我们要改善网站性能”的时候,他期望的是A方案的基础上,不管是100个并发访问还是200个并发访问,响应时间都能达到1秒。而架构师能做到的,则是利用分布式的方案改善网站并发特性,由于分布式不可避免地带来架构负载、网络通信延迟等问题,所以最终设计出来的可能是B方案:缩短高并发访问响应延迟的同时,却延长了原来低并发访问时的响应延迟。架构师对这种可能性要心中有数,合理调整相关各方对性能优化的心理预期。

网站性能对最终用户而言是一种主管感受,性能优化的最终目的就是改善用户的体验,使他们感觉网站很快。离开这个目的,追求技术上的所谓高性能,是舍本逐末,没有多大意义。而用户体验的快或是慢,可以通过技术手段改善,也可以通过优化交互体验改善。

即使在技术层面,性能优化也需要全面考虑,综合权衡:性能提升一倍,但服务器数量也需要增加一倍;或者响应时间缩短,同时数据一致性也下降,这样的优化是都可以接受?这类问题的答案不是技术团队能回答的。归根结底,技术是为业务服务的,技术选型和架构决策和架构决策依赖业务规划乃至企业战略规划,离开业务发展的支撑和驱动,技术走不远,甚至还会迷路。

    前沿技术总是出现前沿业务领域。近几年,以Google为首的互联网企业领跑IT前沿技术潮流,是因为互联网企业的业务发展远超传统IT企业领域,面临更多的挑战,对IT系统提出了更高的要求。

新技术的出现优惠驱动企业开展新的业务。亚马逊等互联网公司利用自己的技术优势进军企业级市场,以技术驱动业务,开展云计算、SaaS等新兴IT业务,逐步蚕食IBM、HP、Oracle、微软等传统软件巨头的市场。

 

各位小伙伴们想要了解更多技术内容,可以加群技术交流:834223478

发布了4 篇原创文章 · 获赞 0 · 访问量 215

猜你喜欢

转载自blog.csdn.net/xieguanxiong/article/details/105095701