说说如何实现高性能的网站架构

来源:互联网 发布:c语言 property get 编辑:程序博客网 时间:2024/06/07 07:17

性能既是客观指标,诸如响应时间、吞吐量等技术指标;又是实际参与者的主观感受。

1 性能测试

性能测试是性能优化的前提与基础,也是优化结果的检查与度量标准。

1.1 不同视角下的性能

1.1.1 用户视角

用户在浏览器上感受到网站响应速度的快慢,包括用户计算机与服务器通信的时间、服务器处理时间以及浏览器构造请求、解析响应数据的时间。

用户视角下感受到的性能

在实践中有这些前端架构优化手段:
1. 优化页面 HTML;
2. 调整浏览器缓存策略;
3. CDN 服务;
4. 反向代理

1.1.2 开发人员视角

开发人员关注的是应用程序以及子系统的性能。包括响应延迟、系统吞吐量、并发处理能力以及系统稳定性等技术指标。

目前有这些优化手段:
1. 使用缓存加速数据的读取;
2. 使用集群提高吞吐能力;
3. 使用异步消息加快请求响应并实现削峰;
4. 代码优化。

1.1.3 运维人员视角

运维人员关注基础设施性能与资源利用率。包括网络运营商的带宽能力、服务器硬件配置、数据中心网络架构、服务器和网络带宽的利用率等。

目前有这些优化手段:
1. 优化骨干网;
2. 使用高性价比定制服务器;
3. 利用虚拟化技术优化资源利用率。

1.2 性能测试指标

1.2.1 响应时间

指的是应用执行一个操作所需的时间,即从发出请求开始到收到响应数据所需要的时间。它是系统最重要的性能指标,直观地反应了系统的快慢。

通过模拟应用程序,记录发出请求到收到响应之间的时间差,即为响应时间。但如果测试目标本身需要花费的时间极少(几微秒),而模拟操作本身也需要消耗一定的时间,这时就无法通过测试,得到响应时间。

在实践中,我们一般采用重复请求,即一个操作重复执行一万次,求得总的响应时间,然后求平均,取得单次请求的响应时间。

1.2.2 并发数

即能够同时处理请求的数目,它也反映了系统的负载能力。

对于网站而言,有这些数需要关注:
1. 网站并发用户数 - 同时提交请求的用户数。
2. 网站在线用户数 - 当前登录网站的用户数。
3. 网站系统用户数 - 网站注册的用户数。

它们的关系是:
网站系统用户数 > 网站在线用户数 > 网站并发用户数

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

现实中,经常看到某些网站做打折促销。活动一开始,网站就因为并发用户数超过网站最大负载而响应缓慢,急性子的用户不断刷新浏览器,导致系统的并发数更高,最终导致系统崩溃。这就是技术准备不充分而导致的结果,也有可能是运营人员错误地估计并发用户数而导致的结果。

可以通过编写多线程来模拟并发的用户数,来测试系统的并发处理能力。为了更真实地模拟用户行为,测试程序会在两次请求之间加入一个随机等待时间,即思考时间。

1.2.3 吞吐量

指的是单位时间内系统处理请求的数量。对于网站,一般以 “请求数/秒” 或 “页面数/秒” 来衡量。而 TPS (Transaction processing systems),即事务数/秒 是吞吐量的常用量化指标。

我们要尽可能提高系统的吞吐量,最大限度地利用服务器资源。

1.2.4 性能计数器

它是描述服务器或操作系统性能的数据指标。包括系统负载、对象与线程数、内存使用情况、CPU 使用情况以及磁盘与网络 I/O 等指标。它们是系统监控的重要参数,对这些参数设置报警阈值,如果这些参数值超过阈值,就会主动向运维与开发人员进行报警。

系统负载(System Load),指的是当前正在被 CPU 执行的进程数与等待被 CPU 执行的进程数之和,它是反映系统忙闲程度的重要指标。在多核 CPU 的情况下,Load 的理想值是 CPU 的数目。如果 Load 值低于 CPU 的数目,就表示 CPU 有空闲,存在资源浪费。如果 Load 值高于 CPU 的数目,表示有进程正在排队等待被 CPU 执行,此时资源不足。在 linux 系统中可以使用 top 命令进行查看,它有三个浮点数,表示最近 1 分钟、10分钟、15分钟内的运行队列下的平均进程数:

linux 下查看系统负载量

1.3 性能测试方法

1.3.1 性能测试

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

1.3.2 负载测试

对系统不断增加并发请求以增加系统压力,直到系统的多项性能指标达到安全临界值。

1.3.3 压力测试

超过安全负载的情况下,对系统继续施加压力,直到系统崩溃,以获得系统最大压力的承受能力。

1.3.4 稳定性测试

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

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

图 3 性能测试曲线

图中的横坐标指的是消耗的资源,纵坐标表示的是系统处理能力(吞吐量)。在开始阶段,系统使用较少的资源就能达到比较好的处理能力(a ~
b),这一段是网站的日常运行区间,网站的绝大部分负载压力都集中在这一段区间内。这一段区间的测试目标是评估系统是否符合需求及设计的目标。随着压力的逐渐增加,系统处理能力达到一个最大值(c 点),这是系统的最大负载点。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.5.1 性能分析

必须对用户请求所经历的各个环节进行分析,排查出可能出现瓶颈的地方,定位出问题所在。

方法是:检查请求所经历的各个环节的日志,分析哪一个环节响应时间不合理,超出了预期;然后检查监控数据,分析影响性能的因素(内存、磁盘、网络、CPU、代码问题、架构设计或是系统资源不足)。

1.5.2 性能优化

根据网站的分层架构,可分为:
1. web 前端性能优化。
2. 应用服务器性能优化。
3. 存储服务器性能优化。

后面会逐一展开分析哦O(∩_∩)O~

2 优化 web 前端性能

2.1 浏览器优化

2.1.1 减少 http 请求

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

减少 HTTP 请求的方法是:合并 CSS、合并 JavaScript、合并图片。将 CSS、JavaScript 合并为一个文件,这样浏览器就只需要一次请求就可以获得这些资源啦。通过把多张图片合并为一张,然后使用 CSS 偏移来实现只显示某个具体的图片。

2.1.2 使用浏览器缓存

对网站而言,CSS、JavaScript、Logo、图标等静态资源文件的更新频率相对较低,而它们又是每次请求必须获取的资源,所以如果把这些文件缓存在浏览器中,就可以极好地改善性能。通过设置 HTTP 头中的 Cache-Control 和 Expires 的属性,就可以设定浏览器的缓存时间。

有时候,静态资源文件需要更新。这时可以通过改变文件名来实现,即生成一个新的文件并更新 HTML 页面中的引用即可。

更新静态资源时,应该采取批量更新的方式,一个文件一个文件地逐步更新,并留有一定的间隔时间。这样可以避免用户浏览器突然发生大量缓存失效导致的负载骤增、网络堵塞的情况。

2.1.3 启动压缩

HTML、CSS、JavaScript 文件启用 GZip 压缩,可以有效地减少通信传输的数据量。但压缩对服务器和浏览器都会产生一定的压力,所以在带宽良好、而服务器资源有不足的情况下最好不要用压缩。

2.1.4 CSS 放在页面最上、JavaScript 放在页面最下

因为浏览器会在下载完所有的 CSS 之后,才会对整个页面进行渲染,所以应该把 CSS 放在页面最上部。而浏览器在加载 JavaScript 后就会立即执行,所以有可能会阻塞页面,造成页面显示缓慢,所以最好把JavaScript 放在页面最下部。当然,如果页面解析有用到 JavaScript ,那么就应该放在恰当的位置。

Cookie 会包含在每次请求和响应中,所以太大的 Cookie 会严重影响数据的传输,建议尽量减少 Cookie 中传输的数据量。

对于某些静态资源(CSS、Script 等)的访问,不会用到 Cookie,所以没有必要把 Cookie 发送给它们。建议为这些静态资源使用独立的域名访问,这样就减少了 Cookie 传输的次数啦O(∩_∩)O~

2.2 CDN 加速

CDN (Content Distribute Network,内容分发网络 ) 的本质是一个缓存。

CDN 架构

它被部署在网络运营商的机房,而运营商又是终端用户的网络服务提供商,所以用户的请求会先到达 CDN 服务器。如果 CDN 中存在浏览器请求的资源时,就可以从 CDN 中直接返回给浏览器咯O(∩_∩)O~

CDN 能够缓存的一般是静态资源(图片、文件、CSS、Script 脚本、静态网页等),这些文件的访问频率很高,所以把它们缓存在 CDN 可以极大地改善网页的打开速度。

2.3 反向代理

反向代理服务器位于网站机房的一侧,由它来接收 HTTP 请求:

反向代理架构

反向代理服务器也可以保护网站的安全,因为它在用户与 web 服务器之间建立了一个屏障。

代理服务器也可以通过配置缓存来加速 web 的请求响应速度。当用户第一次访问静态内容时,这些内容就被缓存在反向代理服务器上。这样当其他用户也需要访问这些资源时,就可以从反向代理服务器上直接读取咯。

有些网站(如维基百科)会把动态内容(热点词条、帖子等)也缓存在代理服务器上,以期加速用户的访问速度。当这些动态内容发生变化时,会通知反向代理服务器的缓存失效,这是反向代理服务器就会重新加载最新的内容再次缓存起来。

也可以使用反向代理服务器实现负载均衡的功能哦O(∩_∩)O~

3 优化应用服务器性能

应用服务器就是处理网站业务的服务器,与业务相关的代码都部署在这里,因此是最复杂、变化最多的地方。

3.1 分布式缓存

当网站遇到性能瓶颈时,第一个想到的解决方案就是缓存。比如数据缓存、文件缓存甚至页面片段缓存。

优先考虑使用缓存来优化性能。

3.1.1 缓存的基本原理

缓存指的是把数据存储在访问速度相对较快的存储介质上,以供系统使用。一方面可以减少数据访问的时间;另一方面,如果缓存的数据是经过计算得到的,那么这些数据以后就无需计算就可以直接使用啦O(∩_∩)O~

缓存的本质是一个内存的哈希表。数据是以一对 key、value 的形式存储在哈希表中。 哈希表数据读写的时间复杂度为 O ( 1 ),所以速度很快哦O(∩_∩)O~

哈希表存储示意图

哈希表是软件开发中常见的一种数据结构。

缓存主要用来存储那些读写比很高,很少变化的数据。应用程序读取数据时,先到缓存中查找;如果找不到或者数据已失效,就去数据库中查找,并把找到的数据写入缓存:

使用缓存存储数据

数据的访问遵循二八定律,即 80% 的访问是落在 20% 的数据上。

3.1.2 合理使用缓存

虽然使用缓存有各种好处,但不能滥用。

频繁修改的数据

这些数据写入缓存后,应用还来不及读取,数据就已经失效咯,白白徒增了系统的负担。一般数据的读写比在 2:1 以上时,即写入一次,至少读取两次的情况下,缓存才有意义。实践中,这个读写比会非常高。比如新浪微博中的热门微博,缓存后可能会被读取数百万次。

没有热点的访问

内存的资源因为有限,所以只能缓存最新访问的数据,并将历史数据清理出缓存。如果缓存中的数据不是热点,那么缓存就没什么意义哦O(∩_∩)O~

数据不一致与脏读

一般会对缓存的数据设置失效时间,这样一旦超过失效时间,就要从数据库中重新加载。所以应用逻辑需要容忍一定时间的数据不一致性。比如卖家更新了商品的属性,卖家要隔一段时间才能看到这个更新的值。在互联网应用中,这种延迟通常是可以接受的。

还有一种策略是:数据更新时,立即更新缓存。但这也带来了更多系统开销和事务一致性的问题。

缓存可用性

随着业务的发展,缓存会承担大部分的数据访问压力。这时的数据库服务器已经习惯了有缓存的日子,一旦缓存服务器崩溃,数据库就会因为完全承受不了如此巨大的压力而宕机,最终导致网站不可用。业界称为“缓存雪崩”。

通过使用分布式的缓存服务器集群,把缓存数据分布到集群中的多台服务器上,这可以在一定程度上改善缓存的可用性。

缓存预热

缓存中存放的是热点数据,而这些数据又是缓存系统利用 LRU(最近最久未被使用)算法筛选出来的。这个过程需要花费较长的时间。如果系统的性能与数据库负载不太好,那么最好在缓存启动时就把热点数据加载好,也就是“缓存预热”。

缓存穿透

因为不恰当的业务逻辑或者恶意攻击持续高并发地请求某个不存在的数据,那么所有的请求都会落在数据库服务器上,这会对数据库造成很大的压力,甚至崩溃。一种策略是把不存在的数据(value 为 null)也缓存起来。

3.1.3 分布式缓存架构

分布式缓存架构指的是:以集群的方式提供缓存服务。

JBoss Cache 分布式缓存(更新同步)

JBoss Cache 分布式缓存,会在集群中的所有服务器中保存相同的缓存数据。当某台服务器有缓存数据更新的时候,会通知集群中的其他服务器更新或清除缓存数据。

JBoss Cache 架构

JBoss Cache 通常把应用程序与缓存部署在同一台服务器上,这样应用程序就可以从本地快速地获取缓存数据。这种方式带来的问题是缓存数据的数量受限于单一服务器的内存空间;而且当集群规模较大时,缓存更新的信息需要同步到集群中的所有服务器,这样做的代价是惊人的。所以这种方案多见于企业应用系统中,而大型网站却很少使用。

Memcached 分布式缓存(互不通信)

大型网站需要缓存的数据量非常大,可能需要 TB 级的内存,这时候就会用到 Memcached:

Memcached 架构

它是一种集中式的缓存集群管理。缓存与应用分离部署,应用程序通过路由算法选择缓存服务器获取缓存数据,缓存服务器之间互不通信。这样的缓存集群很容易实现扩容,具有良好的可伸缩性。

Memcached 使用 TCP 协议(UDP 也支持)进行通信,是一套基于文本的自定义协议。以命令关键字开头,后面跟着一组命令操作数。形如:get 。因为非常简单,所以许多的 NoSQL 产品都借鉴甚至直接支持这套协议。

只要是支持这套协议的客户端就能够与 Memcached 服务器通信,所以 Memcached 发展出支持各种语言的客户端程序。所以在混合使用多种语言的网站,Memcached 更是如鱼得水。

Memcached 服务端的通信模块基于 Libevent(支持事件触发的网络通信程序),它在长连接上的表现非常好。

内存管理中,最令人头痛的就是碎片管理。Memcached 采用的是固定空间分配。它把内存空间分为一组 slab,每个 slab 又包含一组 chunk,同一个 slab 里的每个 chunk 的大小是固定的,拥有相同大小 chunk 的 slab 组合成为 slab_class:

Memcached   内存管理

存储时根据数据的大小(size),在大于 size 的 chunk 中,查找一个最小的,把数据写入。因为内存的分配与释放都是以 chunk 为单位的,所以避免了碎片管理的问题。Memcached 采用 LRU 算法释放数据,释放的 chunk 被标记为未使用,等待下一次有合适大小的数据使用。

当然,Memcached 不是银弹,它存在内存被浪费的问题。因为数据只能存入比它大的 chunk 里,一个 chunk 只能存一个数据,其他的空间都被浪费掉咯。

但因为集群内的服务器互不通信的特性,使得集群几乎可以做到无限制的线性伸缩。

Memcached 因为其简单、稳定、专注的特点,使得它在分布式缓存领域中始终占据着重要的地位。

3.2 异步操作

使用消息队列可以改善网站的性能:

不使用消息队列

使用消息队列

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

使用消息队列的情况下,用户的请求数据发送给消息队列之后会立即返回,然后由消费者进程(一般情况下,消费者进程是独立部署在专用的服务器集群中的)从消息队列中获取数据,异步写入数据库。因为消息队列服务器处理的速度远快于数据库,所以用户的响应延迟会得到有效的改善。

消息队列有很好的削峰作用,即把短时间内高并发产生的事务消息存储在队列中,从而削平高峰期的并发事务。比如在网站的促销活动中使用消息队列,可以有效地抵御促销活动刚开始时大量涌入的订单对系统造成的冲击:

消息队列的削峰作用

要注意的是,因为数据写入消息队列后会立即返回给用户,但数据在后续的业务校验、写数据等操作可能会失败。所以我们要适当地修改业务流程配合。比如订单提交后,订单数据写入消息队列后,不能立即告知用户订单提交成功,而要在订单消费者进程真正处理完订单后,再通过电子邮件或短消息通知用户订单成功,以避免交易纠纷。

3.3 使用集群

使用负载均衡技术为应用构建一个由多台服务器组成的集群中,并将并发访问请求分发到多台服务器上进行处理,这样使得用户的请求得到更快的响应:

负载均衡改善性能

3.4 代码优化

3.4.1 多线程

因为线程比进程更少地占用系统资源,切换代价更小,所以目前的 web 应用服务器都是采用多线程的方式来响应并发用户的请求。

从资源利用的角度来看,使用多线程的原因是因为 IO 阻塞与多 CPU。因为 IO 操作(磁盘或网络)需要较长的时间,这时的 CPU 可以调度其他线程进程处理。所以理想的系统负载是没有线程等待也没有 CPU 空闲,以期望最大限度地利用 CPU 资源。

那么一台服务器需要启动多少个线程才是适合的呢?假设服务器上执行的都是相同类型的任务,那么我们这里有一个简化的公式可供参考:

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

如果任务主要是使用 CPU 进行计算的话,那么线程数最多不超过 CPU 的内核数;如果任务需要等待 IO 操作,那么启动多个线程有助于提高任务的并发度,提高系统的吞吐能力,改善系统的性能。

多线程编程要注意线程安全的问题,因为多线程并发需要对某个资源进行修改,这样有可能导致数据混乱。系统发生故障,许多所谓的“灵异事件”都和多线程的并发问题有关。

解决线程安全的手段有以下这些:
* 把对象设计为无状态的对象 - 无状态的对象指的是对象本身不存储状态信息(对象无成员变量或成员变量也是无状态对象),这样多线程并发访问就不会出现状态不一致的情况咯。web 开发中常用的 servlet 对象就是无状态对象。但对于面向对象设计来说,无状态的对象是一种不良设计。
* 使用局部对象 - 在方法内部创建对象,这些对象会被每一个进入该方法的线程所创建,所以除非程序有意把这些对象传递给其他线程,否则就不会出现线程安全问题。
* 并发访问资源时,使用锁 - 通过锁把多线程并发操作转化为顺序操作。现在出现了各种轻量级的锁,这样使得运行期间线程获取锁和释放锁的代价变小咯,但锁会导致线程同步顺序执行,仍然会对系统的性能产生很大的影响。

3.4.2 资源复用

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

  • 单例 - 因为 web 开发主要是使用贫血模式,即从 service 到 dao 都是无状态对象,所以无需重复创建,这样使用单例模式就很自然啦。像 Spring 默认构造的对象都是单例哦O(∩_∩)O~
  • 对象池 - 复用对象实例,来减少对象的创建与资源消耗。比如数据库连接都会使用连接池。这样数据库连接对象创建好之后,会把对象放入池中。当应用程序需要连接时,就会从对象池中获取一个空闲的连接,使用后再把这个对象返回到池中即可。每一个 web 请求(HTTP Request),web 应用服务器都会创建一个独立的线程去处理,所以这些服务器都会采用线程池的模式。其实,连接池与线程池在本质上都是对象池啦O(∩_∩)O

3.4.3 数据结构

合理设计数据结构,可以有效地改善数据的读写与计算特性从而极大地提升程序的性能。

之前说过的哈希表,它的读写性能很大程度上依赖 hashCode 的随机性,随机性越高,发生的冲突就会越少,读写性能也会越好。目前比较好的字符串哈希散列算法有 time33 算法,即对字符串的每个字符迭代乘以 33,求得哈希值:

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

虽然 time33 算法可以较好地解决冲突,但可能相似字符串的 hashCode 也比较接近,这在某些应用场景下是不可接受的。一个可行的方案是:对字符串取信息指纹,然后再对信息指纹求 hashCode,因为字符串的微小变化,可以引起信息指纹的巨大不同,所以可以获得较好的随机序列。

通过 MD5 计算 HashCode

3.4.4 垃圾回收

Java 的 JVM 内存,可分为堆(heap)和堆栈(stack)。堆栈用于存储线程上下文信息(如方法参数、全局变量等)。堆用于存储对象的内存空间,对象的创建与释放、垃圾回收就在这里。JVM 使用的是分代垃圾回收机制:

分代垃圾回收

堆空间分为年轻代与年老代。
年轻代又分为 Eden 区、From 区与 To 区。新建的对象总是在 Eden 区被创建。当 Eden 区满了,就触发一次年轻代级别的垃圾回收(Young GC),将还被使用的对象复制到 From 区,这样整个 Eden 区就被清空已备下一次使用。当 Eden 区再一次被用完的时候,就再执行一次 Young GC,把 Eden 区与 From 区中还被使用的对象复制到 To 区。下一次 Young GC 会将 Eden 区与 To 区中还被使用的对象复制到 From 区。这样每一次的 Young GC,那些还被使用的对象就会从 From 区与 To 区之间不断被复制。
这些对象的被复制数有一个阈值上限,超过这个阈值,这个对象就会被复制到老年代区。如果老年代区也被用完,就会触发所有代(年轻代与年老代)级别的垃圾回收(Full GC),即全量回收。全量回收对系统性能有较大的影响,所以我们要根据系统的业务特性与对象的生命周期,合理设置年轻代与年老代的大小,尽量减少全量回收的执行次数。通过合理的设置,是可以做到 web 应用在整个运行期间都不执行全量回收操作的!

4 优化存储性能

很多时候,磁盘是系统中最严重的性能瓶颈。而且磁盘中存储的数据是最重要的资产,所以它的可用性与容错性也很重要。

4.1 机械硬盘 vs. 固态硬盘

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

机械硬盘结构图

固态硬盘,简称为 SSD(Solid State Drives)或 Flash 硬盘。数据是存储在可持久记忆的硅晶体上,因此可以像内存一样快速随机访问。而且 SSD 有着更小的功耗与更少的磁盘震动与噪声(因为它不需要磁头臂来回扫呀O(∩_∩)O~)。

SSD 硬盘结构图

但 SSD 硬盘的可靠性与性价比还有待提升。不过相信不久的将来,随着 SSD 工艺水平的提高,取代机械硬盘是早晚的事O(∩_∩)O~

4.2 B+ 树 vs. LSM 树

因为传统的机械硬盘具有快速顺序读写、慢速随机读写的访问特性,所以磁盘的存储结构与算法的选择都是以这个特性进行设计与实现的。

文件系统或数据库系统都会先对数据进行排序然后再存储,为了保证数据被删除、插入、删除后依然有序,传统的关系型数据库会使用 B+ 树的数据结构:

B+ 树原理

B+ 树是一种专门针对磁盘存储而优化的 N 叉排序树,它以树节点为单位存储在磁盘中。

目前的数据库软件多采用两级索引,树的层次最多三层。因此需要 5 次磁盘访问才能更新一条记录:
* 三次磁盘访问,获得数据索引以及行的 ID。
* 一次读取数据文件。
* 一次写数据文件。

因为每次磁盘访问都是随机的,而机械硬盘在数据随机访问的性能上表现不好,而且每次数据访问都要多次访问磁盘,这样的性能自然较差咯。

所以目前的 NoSQL 产品一般采用 LSM 树作为主要的数据结构:

LSM 树原理

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

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

4.3 RAID vs. HDFS

4.3.1 RAID

RAID(Redundant Arrays of Independent Disks),即廉价磁盘冗余阵列,可以减少磁盘的访问延迟,增强磁盘的可用性与容错能力。目前服务器级别的机器都支持插入多块硬盘(8 块及以上),通过 RAID 技术,可以实现数据在多个磁盘的并发读写与数据备份。

假设服务器有 N 块磁盘。

RAID0

RAID0

根据磁盘数量把数据分为 N 份(图示是分为 2 份),然后把这些数据同时并发写入 N 块磁盘,使得数据整体的写入速度是一块磁盘的 N 倍。读取也是这样。所以 RAID0 有着极快的数据读写速度。但 RAID0 不做数据备份,N 块磁盘中只要有一块发生损坏,数据的完整性就会被破坏,所有的数据也就损坏咯。

RAID1

RAID1

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

RAID10

RAID10

这是一种结合 RAID0 和 RAID1 的方案,把所有的磁盘平均分为两份,数据同时写入两份磁盘,这相当于 RAID1。然后在每一份磁盘的 N/2 块磁盘上,使用 RAID0 技术实现并发读写。这样即提高了可靠性又改善了性能。不过 RAID10 的磁盘利用率较低,因为其中有一半的磁盘是用来备份数据的。

RAID 3

RAID 3

把数据分为 N-1 份(图示是 3 份),并发写入 N-1 块磁盘,并在第 N 块(图示是第 4 块)磁盘记录校验数据,任何一块磁盘(不包括校验盘)损坏了,都可以通过其他 N-1 块磁盘进行数据恢复。

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

RAID 5

RAID 5

RAID 5 与 RAID 3 很相似,但校验数据是螺旋式地写入所有的磁盘。这样修改校验数据也就被平均分配到了所有的磁盘上,大大减少了 RAID 3 频繁写入校验盘,导致校验盘损坏的情况发生。

RAID 6

RAID 6

如果数据需要很高的可靠性,又有可能出现两块磁盘同时损坏的情况(磁盘越多,损坏的概率越高),如果这时需要修复数据,那么就要使用 RAID6 咯O(∩_∩)O~

RAID 6 与 RAID 5 类似,但数据只写入 N-2 块磁盘,并螺旋式地在两块磁盘中写入校验信息(不同算法)。


在相同磁盘数目(N)的情况下,各种 RAID 技术比较:

RAID 类型 访问速度 数据可靠性 磁盘利用率 RAID0 很快 很低 100% RAID1 很慢 很高 50% RAID10 中等 很高 50% RAID5 较快 较高 (N-1)/N RAID6 较快 较高(相对于 RAID5) (N-2)/N

RAID 技术可以通过硬件实现(专用的 RAID 卡或主板直接支持),也可以通过软件实现。这种技术在关系型数据库或文件系统中应用广泛。

然而,大型网站更喜欢使用 NoSQL 以及分布式文件系统。

4.3.2 HDFS

HDFS,即 Hadoop 分布式文件系统,系统在整个存储集群的多台服务器上进行数据并发读写与备份,所以可以看做是在服务器集群规模上实现了类似 RAID 功能,原来的 RAID 技术就被冷落了哦。

HDFS 架构原理

HDFS 以块为单元管理文件内容,一个文件被分割为多个 Block(数据块)。客户端写文件时,没写完一个 Block,HDFS 就会将其自动复制到另外两台机器上,这样就保证每个 Block 都会有三个副本。这样即使发生两台服务器宕机,数据依然可以正常访问,这就相当于实现了 RAID1 的数据复制功能。

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

HDFS 有两种服务器角色:
* NameNode - 名字服务节点。
* DataNode - 数据存储节点。

NameNode 在整个 HDFS 中只部署一个实例,提供元数据服务,相当于操作系统中的文件分配表(FAT),管理 Block 的分配,并维护整个文件系统的目录树。
DataNode 部署在 HDFS 集群中的其他所有服务器上,以提供真正的数据存储服务。

HDFS 以 Block 为单位,默认为 64MB。它会把 DataNode 所在的磁盘空间分为 N 个这样的块,以供 Client (应用程序)使用。

当 Client 写文件时,首先访问 NameNode,请求分配数据块。NameNode 根据 DataNode 服务器的磁盘空间,按照一定的负载均衡策略,分配多个数据块以供 Client 使用。

当 Client 写好一个数据块时,HDFS 会把这个数据块再复制两份存储在其他 DataNode 服务器上,即同一份数据有三个副本,保证了数据的可靠性。所以不需要 RAID 进行数据备份咯。

HDFS 配合 MapReduce 进行大数据处理,就可以在整个集群上并发读写访问所有的磁盘,所以也就完全不需要 RAID 了哦O(∩_∩)O~

原创粉丝点击