机械硬盘
在很早的时候计算机还没有硬盘。整个操作系统都安装在 5 寸或者 3.5 寸的软盘里。后来才用上了安装在主板上的机械硬盘,到现在磁带,软盘和光盘基本被替代和淘汰了。
机械硬盘的 IOPS 大概只能做到每秒 100 次左右。
物理构造
前面的硬盘构造包含接口,对应的控制电路及实际的 I/O 设备。这里的 IO 设备也就是机械硬盘。
image如图,一块机械硬盘是由盘面、磁头和悬臂三个部件组成的。
- 盘面:是实际存储数据的盘片。盘面上有一层磁性的涂层。数据就存储在这个磁性的涂层上。 盘面中间有一个受电机控制的转轴。这个转轴会控制盘面去旋转。 与盘面有关系的指标叫转速,如硬盘有5400 转的、7200 转的,乃至 10000 转的。这个多少多少转,指的就是盘面中间电机控制的转轴的旋转速度,英文单位叫RPM,也就是每分钟的旋转圈数。7200RPM,指的就是一旦电脑开机供电之后,我们的硬盘就可以一直做到每分钟转上 7200 圈。如果折算到每一秒钟,就是 120 圈。
- 磁头:数据并不能直接从盘面传输到总线上,而是通过磁头,从盘面上读取到,然后再通过电路信号传输给控制电路、接口,再到总线上的。通常,一个盘面上会有两个磁头,分别在盘面的正反面。盘面在正反两面都有对应的磁性涂层来存储数据,而且一块硬盘也不是只有一个盘面,而是上下堆叠了很多个盘面,各个盘面之间是平行的。每个盘面的正反两面都有对应的磁头。
- 悬臂:悬臂链接在磁头上,并且在一定范围内会去把磁头定位到盘面的某个特定的磁道上。
一个盘面通常是圆形的,由很多个同心圆组成,每一圈都是一个磁道。每个磁道都有自己的一个编号。悬臂其实只是控制,到底是读最里面那个圈的数据,还是最外面圈的数据。
一个磁道,会分成一个一个扇区。上下平行的一个一个盘面的相同扇区呢,叫作一个柱面,读取数据,两个步骤。
- 把盘面旋转到某一个位置。在这个位置上,悬臂可以定位到整个盘面的某一个子区间。这个子区间的形状有点儿像一块披萨饼,一般把这个区间叫作几何扇区。意思是,在“几何位置上”,所有这些扇区都可以被悬臂访问到。
- 就是把悬臂移动到特定磁道的特定扇区,也就在这个“几何扇区”里面,找到我们实际的扇区。找到之后,磁头会落下,就可以读取到正对着扇区的数据。
这两部分组成了硬盘随机访问数据的时间耗费组成:
- 平均延时:这个时间,其实就是把盘面旋转,把几何扇区对准悬臂位置的时间。随机情况下,平均找到一个几何扇区,需要旋转半圈盘面。如:7200 转的硬盘,那么一秒里面,就可以旋转 240 个半圈。那么,这个平均延时就是
1s / 240 = 4.17ms
- 平均寻道时间:也就是在盘面旋转之后,悬臂定位到扇区的的时间。现在用的 HDD 硬盘的平均寻道时间一般在 4-10ms。
所以可得,随机在整个硬盘上找一个数据,需要 8-14 ms。一块 7200 转的硬盘,一秒钟随机的 IO 访问次数,也就是1s / 8 ms = 125 IOPS 或者 1s / 14ms = 70 IOPS
,符合前述HDD 硬盘的 IOPS 每秒 100 次左右。
相应的,如果不是去进行随机的数据访问,而是进行顺序的数据读写,最大化读取效率就是:我们可以选择把顺序存放的数据,尽可能地存放在同一个柱面上。这样,我们只需要旋转一次盘面,进行一次寻道,就可以去写入或者读取,同一个垂直空间上的多个盘面的数据。如果一个柱面上的数据不够,也不要去动悬臂,而是通过电机转动盘面,这样就可以顺序读完一个磁道上的所有数据。所以,其实对于 HDD 硬盘的顺序数据读写,吞吐率还是很不错的,可以达到 200MB/s 左右。
Partial Stroking - 根据场景提升性能
只有 100 的 IOPS,其实很难满足现在互联网海量高并发的请求。所以,今天的数据库,都会把数据存储在 SSD 硬盘上。但是过去的 SSD 硬盘却很昂贵。数据库里面的数据,只能存放在 HDD 硬盘上。今天,即便是数据中心用的 HDD 硬盘,一般也是 7200 转的,因为如果要更快的随机访问速度,我们会选择用 SSD 硬盘。但是在当时,SSD 硬盘价格非常昂贵,还没有能够商业化。硬盘厂商们在不断地研发转得更快的硬盘。在数据中心里,往往会用上 10000 转,乃至 15000 转的硬盘。
不过,10000 转,乃至 15000 转的硬盘也更昂贵。如果要节约成本,提高性价比,只能使用 Partial Stroking 或者 Short Stroking,即“缩短行程”技术。即缩短硬盘的寻道时间。只要缩短了“平均延时 + 寻道时间”这两个之一,就可以提升 IOPS 了。
一般情况下,硬盘的寻道时间都比平均延时要长。其中缩短寻道时间最极端的办法就是不需要寻道,即我们把所有数据都放在一个磁道上。比如始终把磁头放在最外道的磁道上。这样,我们的寻道时间就基本为 0,访问时间就只有平均延时了。那样,IOPS,就变成了1s / 4ms = 250 IOPS
。当然只用一个磁道,能存储的数据则很少。实践当中,可以只用 1/2 或者 1/4 的磁道,也就是最外面 1/4 或者 1/2 的磁道。这样,硬盘可以使用的容量可能变成了 1/2 或者 1/4。但是寻道时间,也变成了 1/4 或者 1/2,因为悬臂需要移动的“行程”也变成了原来的 1/2 或者 1/4, IOPS 就能够大幅度提升了。
比如说,一块 7200 转的硬盘,正常情况下,平均延时是 4.17ms,而寻道时间是 9ms。那么,它原本的 IOPS 就是1s / (4.17ms + 9ms) = 75.9 IOPS
。如果我们只用其中 1/4 的磁道,那么,它的 IOPS 就变成了1s / (4.17ms + 9ms/4) = 155.8 IOPS
这样,IOPS 提升了一倍,和一块 15000 转的硬盘的性能差不多了。同样此时硬盘能用的空间也只有原来的 1/4 了。但在当时,同样容量的 15000 转的硬盘的价格可不止是 7200 转硬盘的 4 倍。所以,这样通过软件去格式化硬盘,只保留部分磁道让系统可用的情况,可以大大提升硬件的性价比。
参考:https://haokan.baidu.com/v?vid=8650689199706446390&pd=bjh&fr=bjhauthor&type=video
总结
机械硬盘的硬件,主要由盘面、磁头和悬臂三部分组成。我们的数据在盘面上的位置,可以通过磁道、扇区和柱面来定位。实际的一次对于硬盘的访问,需要把盘面旋转到某一个“几何扇区”,对准悬臂的位置。然后,悬臂通过寻道,把磁头放到我们实际要读取的扇区上。
受制于机械硬盘的结构,我们对于随机数据的访问速度,就要包含旋转盘面的平均延时和移动悬臂的寻道时间。通过这两个时间,我们能计算出机械硬盘的 IOPS。
7200 转机械硬盘的 IOPS,只能做到 100 左右。在互联网时代的早期,我们也没有 SSD 硬盘可以用,所以工程师们就想出了 Partial Stroking 这个浪费存储空间,但是可以缩短寻道时间来提升硬盘的 IOPS 的解决方案。这个解决方案,也是一个典型的、在深入理解了硬件原理之后的软件优化方案。
SSD
由于无论是 10000 转的企业级机械硬盘,还是用 Short Stroking 这样的方式进一步提升 IOPS HDD也已经无法满足使用需求了,上面的 Short Stroking 优化方式顶死也在 1000 以下提升,而使用 SSD 却可以轻松突然万级,达到 1W - 2W,所以后来 SSD 进行了商用。况且如果SATA接口的SSD还不够,可以换成PCI Express 接口的 SSD。
SSD 没有像机械硬盘那样的寻道过程,所以它的随机读写都更快。
image机械硬盘的耐用性远强于 SSD,如果我们需要频繁地重复写入删除数据,那么机械硬盘要比 SSD 性价比高很多。我们可以简单地认为,因为 SSD 硬盘类似CPU Cache 用的 SRAM 是用一个电容来存放一个比特的数据。它是由一个电容加上一个电压计组合在一起,记录了一个或者多个比特。
SLC、MLC、TLC 和 QLC
SLC:记录一个比特的方式就是:给电容里面充上电有电压的时候就是 1,给电容放电里面没有电就是 0。这样的 SSD 硬盘被称为使用了 SLC 的颗粒。 一个存储单元中只有一位数据。
image这种方式的问题:和 CPU Cache 类似的问题,那就是,同样的面积下,能够存放下的元器件是有限的。
如果只用 SLC,就会遇到,存储容量上不去,并且价格下不来的问题。后来就陆续实现了能在一个电容里面存下 2 个、3 个乃至 4 个比特 - 通过电压计区分不同电压。
QLC:4 个比特一共可以从0000-1111表示16个不同的数。那么,如果能往电容里面充电的时候,充上 15 个不同的电压,并且电压计能够区分出这 15 个不同的电压。加上电容被放空代表的 0,就能够代表从 0000-1111 这样 4 个比特了。
这种方式的问题:表示 15 个不同的电压,充电和读取的时候,对于精度的要求就会更高。这会导致充电和读取的时候都更慢,所以 QLC 的 SSD 的读写速度,要比 SLC 的慢上好几倍。
P/E 擦写问题
SSD的物理构造是自顶向下的。 image和其他IO设备一样,它有对应的接口和控制电路,现在的 SSD 硬盘用的是 SATA 或者 PCI Express 接口。并且控制电路中有个闪存置换层FTL,也是 SSD 的核心模块,SSD 硬盘性能的好坏,很大程度上也取决于 FTL 的算法的好坏。
现在新的大容量 SSD 硬盘都是 3D 封装的,即由很多个裸片叠在一起的,就好像机械硬盘把很多个盘面叠放再一起,这样可以在同样的空间下放下更多的容量。
image一张裸片上可以放多个平面,一般一个平面上的存储容量大概在 GB 级别。一个平面上面,会划分成很多个块,一般一个块的存储大小, 通常几百 KB 到几 MB 大小。一个块里面,还会区分很多个页,就和内存里面的页一样,一个页的大小通常是 4KB。其中处在最下面的两层块和页非常重要。
其中,对于 SSD 硬盘来说,数据的写入叫Program。写入不能像机械硬盘一样,通过覆写来进行的,而是要先去擦除,然后再写入。
- SSD 的读取和写入的基本单位,是一个页。
- SSD 的擦除单位比较大,是按照块来进行的。
SSD 的使用寿命,是每一个块的擦除的次数。可以把 SSD 硬盘的一个平面看成是一张白纸。在上面写入数据,就好像用铅笔在白纸上写字。如果想要把已经写过字的地方写入新的数据,先要用橡皮把已经写好的字擦掉。但是,如果频繁擦同一个地方,那这个地方就会破掉,之后就没有办法再写字了。
- SLC 的芯片,可以擦除的次数大概在 10 万次
- MLC 在 1 万次左右
- TLC 和 QLC 在几千次了。
所以在购买 SSD 硬盘,会看到同样的容量的价格差别很大,因为它们的芯片颗粒和寿命完全不一样。
SSD 读写生命周期
三种颜色分别来表示 SSD 硬盘里面的页的不同状态
- 白色代表这个页从来没有写入过数据
- 绿色代表里面写入的是有效的数据
- 红色代表里面的数据,在操作系统看来已经是删除的了。
一开始,所有块的每一个页都是白色的。随着我们开始往里面写数据,里面的有些页就变成了绿色。然后,因为我们删除了硬盘上的一些文件,所以有些页变成了红色。但是这些红色的页,并不能再次写入数据。因为 SSD 硬盘不能单独擦除一个页,必须一次性擦除整个块,所以新的数据,我们只能往后面的白色的页里面写。这些散落在各个绿色空间里面的红色空洞,就好像硬盘碎片。如果有哪一个块的数据一次性全部被标红了,那我们就可以把整个块进行擦除。它就又会变成白色,可以重新一页一页往里面写数据。这种情况其实也会经常发生。毕竟一个块不大,也就在几百 KB 到几 MB。你删除一个几 MB 的文件,数据又是连续存储的,自然会导致整个块可以被擦除。
随着硬盘里面的数据越来越多,红色空洞占的地方也会越来越多。于是,你会发现,我们就要没有白色的空页去写入数据了。这个时候,我们要做一次类似于 Windows 里面“磁盘碎片整理”或者 Java 里面的“内存垃圾回收”工作。找一个红色空洞最多的块,把里面的绿色数据,挪到另一个块里面去,然后把整个块擦除,变成白色,可以重新写入数据。不过,这个“磁盘碎片整理”或者“内存垃圾回收”的工作,我们不能太主动、太频繁地去做。因为 SSD 的擦除次数是有限的。如果动不动就搞个磁盘碎片整理,那么我们的 SSD 硬盘很快就会报废了。
这样就没办法把SSD很好的利用,解决办法:- 预留空间
image即一块 SSD 的硬盘容量,是没办法完全用满的。不过,为了不得罪消费者,生产 SSD 硬盘的厂商,其实是预留了一部分空间,专门用来做这个“磁盘碎片整理”工作的。一块标成 240G 的 SSD 硬盘,往往实际有 256G 的硬盘空间。SSD 硬盘通过我们的控制芯片电路,把多出来的硬盘空间,用来进行各种数据的闪转腾挪,让你能够写满那 240G 的空间。这个多出来的 16G 空间,叫作预留空间。一般 SSD 的硬盘的预留空间都在 7%-15% 左右。
SSD 硬盘,特别适合读多写少的应用。在日常应用里面,我们的系统盘适合用 SSD。但是,如果我们用 SSD 做专门的下载盘,一直下载各种影音数据,然后刻盘备份就不太好了,特别是现在 QLC 颗粒的 SSD,它只有几千次可擦写的寿命。
SSD 对于数据的写入,只能是一页一页的,不能对页进行覆写。对于数据的擦除,只能整块进行。所以,我们需要用一个,类似“磁盘碎片整理”或者“内存垃圾回收”这样的机制,来清理块当中的数据空洞。而 SSD 硬盘也会保留一定的预留空间,避免出现硬盘无法写满的情况。
磨损均衡、TRIM 和写入放大效应
由于SSD受擦除次数影响,同时我们计算机在使用SSD的过程中,计算机并不会把SSD进行区别对待,所以可能会出现如下图情况,存放操作系统常用软件的物理位置不会经常擦除块数据,而SSD盘的其他存储日常开发代码的地方将会经常面临数据被擦除的情况。这样时间久了,就会出现SSD硬盘的一部分坏了不可用,进而整个SSD硬盘的可用空间变小了。
imageFTL 和磨损均衡
因为上述情况的发生,所以会导致一些坏块的提前出线。并且还有一些块擦鞋次数的浪费,所以就有了磨损均衡这个策略,来让 SSD 硬盘各个块的擦除次数,均匀分摊到各个块上。实现这个技术的核心办法,和虚拟内存一样,就是添加一个间接层。这个间接层,就是 FTL - 闪存置换层。就像在管理内存的时候,通过一个页表映射虚拟内存页和物理页一样,在 FTL 里面,存放了逻辑块地址到物理块地址的简单映射。
image操作系统访问的硬盘地址,都是逻辑地址。只有通过 FTL 转换之后,才会变成实际的物理地址,找到对应的块进行访问。操作系统本身,不需要去考虑块的磨损程度,只要和操作机械硬盘一样来读写数据就好了。操作系统所有对于 SSD 硬盘的读写请求,都要经过 FTL。FTL 里面有逻辑块对应的物理块,所以 FTL 能够记录下来,每个物理块被擦写的次数。如果一个物理块被擦写的次数多了,FTL 就可以将这个物理块,挪到一个擦写次数少的物理块上(擦写次数多的块和次数少的块互换指针,同时把其数据也转移到次数少的块上)。但是,逻辑块不用变,操作系统也不需要知道这个变化。
这也是我们在设计大型系统中的一个典型思路,也就是各层之间是隔离的,操作系统不需要考虑底层的硬件是什么,完全交由硬件的控制电路里面的 FTL,来管理对于实际物理硬件的写入。
TRIM 指令的支持
不过,操作系统不去关心实际底层的硬件是什么,在 SSD 硬盘的使用上,也会带来一个问题。这个问题就是,操作系统的逻辑层和 SSD 的逻辑层里的块状态,是不匹配的。当在操作系统里面去删除一个文件,其实并没有真的在物理层面去删除这个文件,只是在文件系统里面,把对应的 inode 里面的元信息清理掉,这代表这个 inode 还可以继续使用,可以写入新的数据。这个时候,实际物理层面的对应的存储空间,在操作系统里面被标记成可以写入了。
所以,其实日常的文件删除,都只是一个操作系统层面的逻辑删除。这也是为什么,很多时候我们不小心删除了对应的文件,我们可以通过各种恢复软件,把数据找回来。同样的,这也是为什么,如果我们想要删除干净数据,需要用各种“文件粉碎”的功能才行。
这个删除的逻辑在机械硬盘层面没有问题,因为机械硬盘可以复写数据,因为文件被标记成可以写入,后续的写入可以直接覆写这个位置,不需要真实的把文件删除。而SSD硬盘是不一样的。
image当我们在操作系统里面,删除掉一个刚刚下载的文件,比如标记成黄色 openjdk.exe 这样一个 jdk 的安装文件,在操作系统里面,对应的 inode 里面,就没有文件的元信息。但是,这个时候,SSD 的逻辑块层面,其实并不知道上层操作系统的这个事情。所以在FTL的逻辑块层面,openjdk.exe 仍然是占用了对应的空间。对应的物理页,也仍然被认为是被占用了的。
这个时候,如果我们需要对 SSD 进行垃圾回收操作,openjdk.exe 对应的物理页,仍然要在这个过程中,被搬运到其他的 Block 里面去(因为不知道这个物理块中的数据已经是无效数据)。只有当操作系统,再在刚才的 inode 里面写入数据的时候,我们才会知道原来的些黄色的页,其实都已经没有用了,我们才会把它标记成废弃掉。所以,在使用 SSD 的硬盘情况下,操作系统对于文件的删除,SSD 硬盘其实并不知道。这就导致,我们为了磨损均衡,很多时候在都在搬运很多已经删除了的数据。这就会产生很多不必要的数据读写和擦除,既消耗了 SSD 的性能,也缩短了 SSD 的使用寿命。
为了解决这个问题,现在的操作系统和 SSD 的主控芯片,都支持TRIM 命令。这个命令可以在文件被删除的时候,让操作系统去通知 SSD 硬盘,对应的逻辑块已经标记成已删除了。现在的 SSD 硬盘都已经支持了 TRIM 命令。
如果没有这个命令的通知,在操作系统层面逻辑删除一个块上的数据之后,SSD并不知情,在操作系统下次再在该位置写入数据前,SSD都不会知晓这些数据是无效数据。而会因为该块的磨损均衡策略进而对块中有效数据进行迁移,所以,这些数据会被当成有效数据来进行无效的迁移。进而影响了SSD性能和磨损数。
写入放大
TRIM 命令的发明,也反应了一个使用 SSD 硬盘的问题,那就是,SSD 硬盘容易越用越慢。当 SSD 硬盘的存储空间被占用得越来越多,每一次写入新数据,都可能没有足够的空白。可能不得不去进行垃圾回收,合并一些块里面的页,然后再擦除掉一些页,才能匀出一些空间来。 这个时候,从应用层或者操作系统层面来看,可能只是写入了一个 4KB 或者 4MB 的数据。但是,实际通过 FTL 之后,可能要去搬运 8MB、16MB 甚至更多的数据。
可以通过,实际的闪存写入的数据量 / 系统通过 FTL 写入的数据量(迁移) = 写入放大,可以得到,写入放大的倍数越多,意味着实际的 SSD 性能也就越差,会远远比不上实际 SSD 硬盘标称的指标。而解决写入放大,需要我们在后台定时进行垃圾回收,在硬盘比较空闲的时候,就把搬运数据、擦除数据、留出空白的块的工作做完,而不是等实际数据写入的时候,再进行这样的操作。
AeroSpike
所以其实用好SSD硬盘十分的困难。无法单纯的通过替换HDD来有效利用SSD的高性能,除非合理利用好它的特性。而AeroSpike这样的KV数据库就是一个好的实现。
首先,AeroSpike 操作 SSD 硬盘,并没有通过操作系统的文件系统。而是直接操作 SSD 里面的块和页。因为操作系统里面的文件系统,对于 KV 数据库来说,只是让我们多了一层间接层,只会降低性能,对我们没有什么实际的作用。
其次,AeroSpike 在读写数据的时候,做了两个优化。在写入数据的时候,AeroSpike 尽可能去写一个较大的数据块,而不是频繁地去写很多小的数据块。这样,硬盘就不太容易频繁出现磁盘碎片。并且,一次性写入一个大的数据块,也更容易利用好顺序写入的性能优势。AeroSpike 写入的一个数据块,是 128KB,远比一个页的 4KB 要大得多。
另外,在读取数据的时候,AeroSpike 倒是可以读取 512 字节这样的小数据。因为 SSD 的随机读取性能很好,也不像写入数据那样有擦除寿命问题。而且,很多时候我们读取的数据是键值对里面的值的数据,这些数据要在网络上传输。如果一次性必须读出比较大的数据,就会导致我们的网络带宽不够用。
因为 AeroSpike 是一个对于响应时间要求很高的实时 KV 数据库,如果出现了严重的写放大效应,会导致写入数据的响应时间大幅度变长。所以 AeroSpike 做了这样几个动作:
- 是持续地进行磁盘碎片整理。AeroSpike 用了所谓的高水位算法。其实这个算法很简单,就是一旦一个物理块里面的数据碎片超过 50%,就把这个物理块搬运压缩,然后进行数据擦除,确保磁盘始终有足够的空间可以写入。
- 是在 AeroSpike 给出的最佳实践中,为了保障数据库的性能,建议你只用到 SSD 硬盘标定容量的一半。也就是说,我们人为地给 SSD 硬盘预留了 50% 的预留空间,以确保 SSD 硬盘的写放大效应尽可能小,不会影响数据库的访问性能。
正是因为做了这种种的优化,在 NoSQL 数据库刚刚兴起的时候,AeroSpike 的性能把 Cassandra、MongoDB 这些数据库远远甩在身后,和这些数据库之间的性能差距,有时候会到达一个数量级。这也让 AeroSpike 成为了当时高性能 KV 数据库的标杆。
问题
AeroSpike为什么现在的受欢迎程度不如Redis?
作者回复:
你好,其实AeroSpike据我所知在国内外应用都很普遍了。没有Redis火的核心原因我觉得是因为开源得晚了。
另外,就是对于大部分数据量没有那么大的创业公司,用内存作为缓存,存储空间也就够了,那用Redis也就足够了,暂时还用不上AeroSpike。
DMA
DMA:即便有了 PCI Express 接口的 SSD,但比起 CPU,总还是太慢。所以如果对于 I/O 的操作,都是由 CPU 发出对应的指令,然后等待 I/O 设备完成操作之后返回,那 CPU 有大量的时间其实都是在等待 I/O 设备完成操作。并没有实际的意义。并且我们对于 I/O 设备的大量操作,其实都只是把内存里面的数据,传输到 I/O 设备而已。在这种情况下,其实 CPU 只是在傻等而已。特别是当传输的数据量比较大的时候,比如进行大文件复制,如果所有数据都要经过 CPU,实在是有点儿太浪费时间了。所以后来发明了 DMA 技术,直接内存访问(Direct Memory Access),来减少 CPU 等待的时间。
DMA技术:即在主板上放一块独立的芯片,在进行内存和 I/O 设备的数据传输的时候,不再通过 CPU 来控制数据传输,而直接通过 DMA 控制器。这块芯片,就是一个==协处理器==。 协助 CPU 完成对应的数据传输工作。
DMAC 最有价值的地方体现在,当要传输的数据特别大、速度特别快,或者传输的数据特别小、速度特别慢的时候。比如说,我们用千兆网卡或者硬盘传输大量数据的时候,如果都用 CPU 来搬运的话,肯定忙不过来,所以可以选择 DMAC。而当数据传输很慢的时候,DMAC 可以等数据到齐了,再发送信号,给到 CPU 去处理,而不是让 CPU 在那里忙等待。
DMAC 也是一个特殊的 I/O 设备,它和 CPU 以及其他 I/O 设备一样,通过连接到总线来进行实际的数据传输。
总线上的设备,有两种类型。而 DMAC 既是主设备,又是从设备。
- 主设备:想要主动发起数据传输,必须要是一个主设备才可以,CPU 就是主设备。
- 从设备:从设备(比如硬盘)只能接受数据传输。所以,如果通过 CPU 来传输数据,要么是 CPU 从 I/O 设备读数据,要么是 CPU 向 I/O 设备写数据。一定是 CPU 主设备主动
从设备也可以向主设备发起请求,但不是发送的数据内容,而是控制信号。I/O 设备可以告诉 CPU,我这里有数据要传输给你,但是实际数据是 CPU 拉走的,而不是 I/O 设备推给 CPU 的。
imageDMAC 对于 CPU 来说,它是一个从设备;对于硬盘这样的 IO 设备来说呢,它又变成了一个主设备。
使用 DMAC 进行数据传输的过程:
- 首先,CPU 还是作为一个主设备,向 DMAC 设备发起请求。这个请求,其实就是在 DMAC 里面修改配置寄存器。
- CPU 修改 DMAC 的配置的时候,会告诉 DMAC 这样几个信息:
-
源地址的初始值以及传输时候的地址增减方式。
- 源地址:即数据要从哪里传输过来。如果我们要从内存里面写入数据到硬盘上,那么就是要读取的数据在内存里面的地址。如果是从硬盘读取数据到内存里,那就是硬盘的 I/O 接口的地址。
- 地址的增减方式:即数据是从大的地址向小的地址传输,还是从小的地址往大的地址传输。
- 目标地址初始值和传输时候的地址增减方式。
- 要传输的数据长度。也就是一共要传输多少数据。
-
源地址的初始值以及传输时候的地址增减方式。
- 设置完这些信息之后,DMAC 就会变成一个空闲的状态。
- 如果要从硬盘上往内存里面加载数据,这个时候,硬盘就会向 DMAC 发起一个数据传输请求。==这个请求并不是通过总线,而是通过一个额外的连线==。
- 然后,DMAC 需要再通过一个额外的连线响应这个申请。
- 于是,DMAC 这个芯片,就向硬盘的接口发起要总线读的传输请求。数据就从硬盘里面,读到了 DMAC 的控制器里面。
- 然后,DMAC 再向我们的内存发起总线写的数据传输请求,把数据写入到内存里面。
- DMAC 会反复进行上面第 6、7 步的操作,直到 DMAC 的寄存器里面设置的数据长度传输完成。
- 数据传输完成之后,DMAC 重新回到第 3 步的空闲状态。
所以,整个数据传输的过程中,我们不是通过 CPU 来搬运数据,而是由 DMAC 这个芯片来搬运数据。但是 CPU 在这个过程中也是必不可少的。因为传输什么数据,从哪里传输到哪里,其实还是由 CPU 来设置的。这也是为什么,DMAC 被叫作“协处理器”。
image最早,计算机里是没有 DMAC 的,所有数据都是由 CPU 来搬运的。随着人们对于数据传输的需求越来越多,先是出现了主板上独立的 DMAC 控制器。到了今天,各种 I/O 设备越来越多,数据传输的需求越来越复杂,使用的场景各不相同。加之显示器、网卡、硬盘对于数据传输的需求都不一样,所以各个设备里面都有自己的 DMAC 芯片了。
Kafka
Kafka 就很好的利用了 DMA 的数据传输方式,通过 DMA 实现了非常大的性能提升。
Kafka 是一个用来处理实时数据的管道,我们常常用它来做一个消息队列,或者用来收集和落地海量的日志。作为一个处理实时数据和日志的管道,瓶颈自然也在 I/O 层面。
Kafka 里面会有两种常见的海量数据传输的情况。一种是从网络中接收上游的数据,然后需要落地到本地的磁盘上,确保数据不丢失。另一种情况呢,则是从本地磁盘上读取出来,通过网络发送出去。
后一种情况:从磁盘读数据发送到网络上去。最直观的办法,用一个文件读操作,从磁盘上把数据读到内存里面来,然后再用一个 Socket,把这些数据发送到网络上去。
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
在这个过程中,看似两步操作,实则数据一共发生了四次传输的过程。其中两次是 DMA 的传输,另外两次,则是通过 CPU 控制的传输。
- 第一次传输,是从硬盘上,读到操作系统内核的缓冲区里。这个传输是通过 ==DMA 搬运==的。
- 第二次传输,需要从内核缓冲区里面的数据,复制到我们应用分配的内存里面。这个传输是通过 ==CPU 搬运==的。
- 第三次传输,要从我们应用的内存里面,再写到操作系统的 Socket 的缓冲区里面去。这个传输,还是由 ==CPU 搬运==的。
- 最后一次传输,需要再从 Socket 的缓冲区里面,写到网卡的缓冲区里面去。这个传输又是通过 ==DMA 搬运==的。
这种传输数据的方式是比较大的,只是一次简单的传输却需要4次数据的运输。其中上述的4次运输步骤,从内核的读缓冲区传输到应用的内存里,再从应用的内存里传输到 Socket 的缓冲区里,
其实都是把同一份数据在内存里面进行运转,没有效率。
像 Kafka 这样的应用场景,其实大部分最终利用到的硬件资源,其实又都是在干这个搬运数据的事儿。所以,我们就需要尽可能地减少数据搬运的需求。而Kafka 做的事情就是,把这个数据搬运的次数,从上面的四次,变成了两次,并且只有 DMA 来进行数据搬运,而不需要 CPU。
@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}
Kafka 的源码,最终调用了 Java NIO 库里的 transferTo 方法
Kafka 的代码调用了 Java NIO 库,具体是 FileChannel 里面的 transferTo 方法。我们的数据并没有读到中间的应用内存里面,而是直接通过 Channel,写入到对应的网络设备里。并且,对于 Socket 的操作,也不是写入到 Socket 的 Buffer 里面,而是直接根据描述符(Descriptor)写入到网卡的缓冲区里面。于是,在这个过程之中,我们只进行了两次数据传输。省去了数据在内存中的滞留过程。
image- 第一次,是通过 DMA,从硬盘直接读到操作系统内核的读缓冲区里面。
- 第二次,则是根据 Socket 的描述符信息,直接从读缓冲区里面,写入到网卡的缓冲区里面。
这样,我们同一份数据传输的次数从四次变成了两次,并且没有通过 CPU 来进行数据搬运,所有的数据都是通过 DMA 来进行传输的。 在这个方法里面,没有在内存层面去“复制”数据,所以这个方法,也被称之为零拷贝。
IBM Developer Works 里面有一篇文章,专门写过程序来测试过,在同样的硬件下,使用零拷贝能够带来的性能提升。我在这里放上这篇文章链接。在这篇文章最后,你可以看到,无论传输数据量的大小,传输同样的数据,使用了零拷贝能够缩短 65% 的时间,大幅度提升了机器传输数据的吞吐量。想要深入了解零拷贝,建议你可以仔细读一读这篇文章。
总结
如果我们始终让 CPU 来进行各种数据传输工作,会特别浪费。一方面,我们的数据传输工作用不到多少 CPU 核心的“计算”功能。另一方面,CPU 的运转速度也比 I/O 操作要快很多。所以,我们希望能够给 CPU“减负”。
于是,工程师们就在主板上放上了 DMAC 这样一个协处理器芯片。通过这个芯片,CPU 只需要告诉 DMAC,我们要传输什么数据,从哪里来,到哪里去,就可以放心离开了。后续的实际数据传输工作,都会由 DMAC 来完成。随着现代计算机各种外设硬件越来越多,光一个通用的 DMAC 芯片不够了,我们在各个外设上都加上了 DMAC 芯片,使得 CPU 很少再需要关心数据传输的工作了。
在我们实际的系统开发过程中,利用好 DMA 的数据传输机制,也可以大幅提升 I/O 的吞吐率。最典型的例子就是 Kafka。
传统地从硬盘读取数据,然后再通过网卡向外发送,我们需要进行四次数据传输,其中有两次是发生在内存里的缓冲区和对应的硬件设备之间,我们没法节省掉。但是还有两次,完全是通过 CPU 在内存里面进行数据复制。
在 Kafka 里,通过 Java 的 NIO 里面 FileChannel 的 transferTo 方法调用,我们可以不用把数据复制到我们应用程序的内存里面。通过 DMA 的方式,我们可以把数据从内存缓冲区直接写到网卡的缓冲区里面。在使用了这样的零拷贝的方法之后呢,我们传输同样数据的时间,可以缩减为原来的 1/3,相当于提升了 3 倍的吞吐率。
这也是为什么,Kafka 是目前实时数据传输管道的标准解决方案。
数据完整性 - 单比特反转
单比特翻转:比如内存中某一数据的二进制表示是00100100
,遇到单比特翻转问题之后则会变成00110100
。
内存里面的单比特翻转或者错误,并不是一个特别罕见的现象。无论是因为内存的制造质量造成的漏电,还是外部的射线,都有一定的概率,会造成单比特错误。而内存层面的数据出错,软件工程师并不知道,而且这个出错很有可能是随机的。
image解决办法:
奇偶校验:把内存里面的 N 位比特当成是一组。常见的,比如 8 位就是一个字节。然后,用额外的一位去记录,这 8 个比特里面有奇数个 1 还是偶数个 1。如果是奇数个 1,那额外的一位就记录为 1;如果是偶数个 1,那额外的一位就记录成 0。这个额外的一位,称之为校验码位。
image如图,如果这个字节中发生了单比特翻转,那么校验码位就和实际的校验码位不一致了。就知道该数据出错了。但同样,这种方法还有其他缺陷。
- 奇偶校验只能解决遇到单个位的错误,或者说奇数个位的错误。
- 它只能发现错误,但是不能纠正错误。即使在内存里面发现数据错误了,我们也只能中止程序,而不能让程序继续正常地运行下去。
所以,为了弥补上述两个问题的解决办法出来了。ECC,ECC 内存的全称是 Error-Correcting Code memory,中文名字叫作纠错内存。顾名思义,就是在内存里面出现错误的时候,能够自己纠正过来。并且它能够发现更多位的错误。
它不仅能捕捉到错误,还要能够纠正发生的错误。这个策略,通常叫作纠错码。它还有一个升级版本,叫作纠删码,不仅能够纠正错误,还能够在错误不能纠正的时候,直接把数据删除。无论是 ECC 内存,还是网络传输,乃至硬盘的 RAID,其实都利用了纠错码和纠删码的相关技术。
==数据量和负载上来,没有ECC的话,单比特翻转其实是一个大概率发生的故障。==
从老师的描述看单比特翻转问题的概率不低啊,但是大部分PC机都没有用ECC,为什么PC机很少听说有出现这个问题带来的bug?
作者回复: 有铭同学,
你好,这有两种情况:
-
第一种是PC实际的负载比服务器低很多,大部分时间你的PC是很空闲的,CPU占用率和内存使用率都不高,也没有什么东西在计算。而服务器常常是24小时高负载在运转的。服务器可能一天进行的计算量比你PC一年还多。数据中心里又有可能同时有1000台计算机,意味着服务器一天遇到的问题可能PC要一辈子才遇到一次。
-
第二是很多时候发生了你没有意识到,比如程序忽然Crash了,机器蓝屏重启了,甚至有程序数据错了,你并会关心到哪个是单比特翻转引起的。
参考:「单比特错误」真的会造成整个程序乃至计算机系统的崩溃么?现代计算机有哪些检错、纠错机制?
数据完整性 - 海明码
海明码解决了上述检错码的遗留问题,它不仅可以排查出错误,并且能排查到哪里出错了。 它也叫纠错码。纠错码需要更多的冗余信息,通过这些冗余信息,不仅可以知道哪里的数据错了,还能直接把数据给改对。 ECC 内存也还在用海明码来纠错。
最基础的海明码叫7-4 海明码。这里的“7”指的是实际有效的数据,一共是 7 位。而这里的“4”,指的是我们额外存储了 4 位数据,用来纠错,但纠错码的纠错能力是有限的,在 7-4 海明码里面,我们只能纠正某 1 位的错误。
4 位的校验码,一共可以表示 2^4 = 16 个不同的数。根据数据位计算出来的校验值,一定是确定的。所以,如果数据位出错了,计算出来的校验码,一定和确定的那个校验码不同。那可能的值,就是在 2^4 - 1 = 15 那剩下的 15 个可能的校验值当中。
15 个可能的校验值,对应 15 个可能出错的位。对于数据位的单比特翻转错误,其实3位就够了2^3 - 1 = 7,但同样的校验位也会出错。所以,7 位数据位和 3 位校验位,如果只有单比特出错,可能出错的位数就是 10 位,2^3 - 1 = 7,就无法定位是哪一位出错了。它需要满足如下等式。
K + N + 1 <= 2^N
数据位有 K 位,校验位有 N 位
在有 7 位数据位,也就是 K=7 的情况下,N 的最小值就是 4。4 位校验位,其实最多可以支持到 11 位数据位。如下,数据位数和校验位数的对照表
image海明码纠错原理
海明码的编码方式:一个4-3 海明码(也就是 4 位数据位,3 位校验位)。我们把 4 位数据位,分别记作 d1、d2、d3、d4。我们把 3 位校验位,分别记作 p1、p2、p3。
从 4 位的数据位里面,我们拿走 1 位,然后计算出一个对应的校验位。这个校验位的计算用之前讲过的奇偶校验就可以了。比如,我们用 d1、d2、d4 来计算出一个校验位 p1;用 d1、d3、d4 计算出一个校验位 p2;用 d2、d3、d4 计算出一个校验位 p3。就像下面这个对应的表格一样:
image此时,如果 d1 这一位的数据出错了,我们会发现,p1 和 p2 和校验的计算结果都不一样。d2 出错了,p1 和 p3 的校验的计算结果不一样;d3 出错了,则p2 和 p3;如果 d4 出错了,则是 p1、p2、p3 都不一样。你会发现,当数据码出错的时候,至少会有 2 位校验码的计算是不一致的。
倒过来,如果是 p1 的校验码出错了,则只有 p1 的校验结果出错。p2 和 p3 的出错的结果也是一样的,只有一个校验码的计算是不一致的。
所以校验码不一致,一共有 2^3-1=7 种情况,正好对应了 7 个不同的位数的错误。
image海明码的生成规则
首先,先确定编码后,要传输的数据是多少位。比如说,我们这里的 7-4 海明码,就是一共 11 位。
然后,我们给这 11 位数据从左到右进行编号,并且也把它们的二进制表示写出来。
接着,先把这 11 个数据中的==二进制的整数次幂==找出来。在这个 7-4 海明码里面,就是 1、2、4、8。这些数,就是我们的校验码位,我们把他们记录做 p1~p4。如果从二进制的角度看,它们是这 11 个数当中,唯四的,在 4 个比特里面只有一个比特是 1 的数值。
那么剩下的 7 个数,就是我们 d1-d7 的数据码位了。
然后,对于校验码位,还是用奇偶校验码。但是每一个校验码位,不是用所有的 7 位数据来计算校验码。而是 p1 用 3、5、7、9、11 来计算。也就是,这些数在二进制表示下,从右往左数的第一位比特是 1 的情况下,用 p1 作为校验码。
剩下的 p2,我们用 3、6、10、11 来计算校验码,也就是在二进制表示下,从右往左数的第二位比特是 1 的情况下,用 p2。那么,p3 自然是从右往左数,第三位比特是 1 的情况下的数字校验码。而 p4 则是第四位比特是 1 的情况下的校验码。如图:
image此时可以看到,任何一个数据码出错了,就至少会有对应的两个或者三个校验码对不上,这样就能反过来找到是哪一个数据码出错了。如果校验码出错了,那么只有校验码这一位对不上,我们就知道是这个校验码出错了。
海明距离
换一个角度来理解海明码的作用:对于两个二进制表示的数据,他们之间有差异的位数,我们称之为海明距离。比如 1001 和 0001 的海明距离是 1,因为他们只有最左侧的第一位是不同的。而 1001 和 0000 的海明距离是 2,因为他们最左侧和最右侧有两位是不同的。
image所以,所谓的进行一位纠错,也就是所有和我们要传输的数据的海明距离为 1 的数,都能被纠正回来。
而任何两个实际我们想要传输的数据,海明距离都至少要是 3。
为什么不能是 2 呢?因为如果是 2 的话,那么就会有一个出错的数,到两个正确的数据的海明距离都是 1。当我们看到这个出错的数的时候,我们就不知道究竟应该纠正到那一个数了。
image根据偏向来纠正该数的原本值。
总结
海明码看起来简单但直到今天的 ECC 内存里面,我们还在使用这个技术方案。而海明也因为海明码获得了图灵奖。
通过在数据中添加多个冗余的校验码位,海明码不仅能够检测到数据中的错误,还能够在只有单个位的数据出错的时候,把错误的一位纠正过来。在理解和计算海明码的过程中,有一个很重要的点,就是不仅原来的数据位可能出错。我们新添加的校验位,一样可能会出现单比特翻转的错误。这也是为什么,7 位数据位用 3 位校验码位是不够的,而需要 4 位校验码位。
实际的海明码编码的过程也并不复杂,我们通过用不同过的校验位,去匹配多个不同的数据组,确保任何一个数据位出错,都会产生一个多个校验码位出错的唯一组合。这样,在出错的时候,我们就可以反过来找到出错的数据位,并纠正过来。当只有一个校验码位出错的时候,我们就知道实际出错的是校验码位了。
分布式计算
如果我们只有一台计算机在数据中心,我们会遇到三个核心问题:
- 垂直扩展和水平扩展的选择问题
- 如何保持高可用性
- 一致性问题
水平扩展
如果你利用云厂商提供的服务器搭建了一个自己的网站,对用户提供服务。机器配置1 个 CPU 核心、3.75G 内存以及一块 10G 的 SSD 系统盘。这样一台服务器每个月的价格差不多是 28 美元。
但后来用户访问量上来了,服务器性能有点儿不够了,你需要升级服务器,但此时面临两个选择:
- 第一个选择是升级现在这台服务器的硬件,变成 2 个 CPU 核心、7.5G 内存。这样的选择我们称之为垂直扩展。
- 第二个选择则是我们再租用一台和之前一样的服务器。于是,我们有了 2 台 1 个 CPU 核心、3.75G 内存的服务器。这样的选择我们称之为水平扩展。
在这个阶段,这两个选择,从成本上看起来没有什么差异。2 核心、7.5G 内存的服务器,成本是 56.61 美元,而 2 台 1 核心、3.75G 内存的服务器价格,成本是 57 美元,这之间的价格差异不到 1%。不过,垂直扩展和水平扩展看似是两个不同的选择,但是随着流量不断增长。到最后,只会变成一个选择。那就是既会垂直扩展,又会水平扩展,并且==最终依靠水平扩展==,来支撑 Google、Facebook、阿里、腾讯这样体量的互联网服务。
垂直扩展背后的逻辑和优势都很简单。一般来说,垂直扩展通常不需要我们去改造程序,也就是说,我们没有研发成本。但最终还是会使用水平扩展的原因其实很简单,因为我们没有办法不停地去做垂直扩展。我们在 Google Cloud 上现在能够买到的性能最好的服务器,是 96 个 CPU 核心、1.4TB 的内存。如果我们的访问量逐渐增大,一台 96 核心的服务器也支撑不了了,那么我们就没有办法再去做垂直扩展了。这个时候,我们就不得不采用水平扩展的方案了。
然而,一旦开始采用水平扩展,我们就会面临在软件层面改造的问题了。也就是我们需要开始进行分布式计算了。我们需要引入负载均衡 这样的组件,来进行流量分配。我们需要拆分应用服务器和数据库服务器,来进行垂直功能的切分。我们也需要不同的应用之间通过消息队列,来进行异步任务的执行。
image所有这些软件层面的改造,其实都是在做分布式计算的一个核心工作,就是通过消息传递而不是共享内存的方式,让多台不同的计算机协作起来共同完成任务。而因为我们最终必然要进行水平扩展,我们需要在系统设计的早期就基于消息传递而非共享内存来设计系统。即使这些消息只是在同一台服务器上进行传递。(因为之后必然会被垂直拆分开)
高可用性 - 单点故障
水平扩展的好处:
- 可以“强迫”从开发的角度,尽早地让系统能够支持水平扩展,避免在真的流量快速增长的时候,垂直扩展的解决方案跟不上趟。
- 系统的可用性的保证。
上面的 1 核变 2 核的垂直扩展的方式,扩展完之后,我们还是只有 1 台服务器。如果这台服务器出现了一点硬件故障,比如,CPU 坏了,那我们的整个系统就坏了,就不可用了。如果采用了水平扩展,即便有一台服务器的 CPU 坏了,我们还有另外一台服务器仍然能够提供服务。负载均衡能够通过健康检测发现坏掉的服务器没有响应了,就可以自动把所有的流量切换到第 2 台服务器上,这个操作就叫作故障转移。我们的系统仍然是可用的。
系统的可用性 指的就是,我们的系统可以正常服务的时间占比。无论是因为软硬件故障,还是需要对系统进行停机升级,都会让我们损失系统的可用性。可用性通常是用一个百分比的数字来表示,比如 99.99%。我们说,系统每个月的可用性要保障在 99.99%,也就是意味着一个月里,你的服务宕机的时间不能超过 4.32 分钟。
有些系统可用性的损失,是在我们计划内的。比如上面说的停机升级,这个就是所谓的计划内停机时间。有些系统可用性的损失,是在我们计划外的,比如一台服务器的硬盘忽然坏了,这个就是所谓的计划外停机时间。
我们的系统是一定不可能做到 100% 可用的,特别是计划外的停机时间。从简单的硬件损坏,到机房停电、光缆被挖断,乃至于各种自然灾害,比如地震、洪水、海啸,都有可能使得我们的系统不可用。作为一个工程师和架构师,我们要做的就是尽可能低成本地提高系统的可用性。
现在的服务器的可用性都已经很不错了,通常都能保障 99.99% 的可用性了。如果我们有一个小小的三台服务器组成的小系统,一台部署了 Nginx 来作为负载均衡和反向代理,一台跑了 PHP-FPM 作为 Web 应用服务器,一台用来作为 MySQL 数据库服务器。每台服务器的可用性都是 99.99%。那么我们整个系统的可用性就是99.99% × 99.99% × 99.99% = 99.97%
。在这个系统当中,这个数字看起来似乎没有那么大区别。不过反过来看,我们是从损失了 0.01% 的可用性,变成了损失 0.03% 的可用性,不可用的时间变成了原来的 3 倍。如果我们有 1000 台服务器,那么整个的可用性,就会变成 99.99% ^ 1000 = 90.5%。也就是说,我们的服务一年里有超过一个月是不可用的。
在这个场景下,任何一台服务器出错了,整个系统就没法用了。这个问题就叫作单点故障问题。
要解决单点故障问题,第一点就是要移除单点。即水平扩展,就是让两台服务器提供相同的功能,然后通过负载均衡把流量分发到两台不同的服务器去。即使一台服务器挂了,还有一台服务器可以正常提供服务。不过光用两台服务器是不够的,单点故障其实在数据中心里面无处不在。我们现在用的是云上的两台虚拟机。如果这两台虚拟机是托管在同一台物理机上的,那这台物理机本身又成为了一个单点。那我们就需要把这两台虚拟机分到两台不同的物理机上。不过这个还是不够。如果这两台物理机在同一个机架上,那机架上的交换机就成了一个单点。即使放到不同的机架上,还是有可能出现整个数据中心遭遇意外故障的情况。
image除了上述问题,部署在云上的服务所在的数据中心,还有可能因为散热问题触发了整个数据中心所有服务器被关闭的问题。面对这种情况,就需要设计进行异地多活 的系统设计和部署。所以,在现代的云服务,你在买服务器的时候可以选择服务器的 area(地区)和 zone(区域),而要不要把服务器放在不同的地区或者区域里,也是避免单点故障的一个重要因素。
只是能够去除单点,可用性问题还没有解决。比如,上面我们用负载均衡把流量均匀地分发到 2 台服务器上,当一台应用服务器挂掉的时候,我们的确还有一台服务器在提供服务。但是负载均衡会把一半的流量发到已经挂掉的服务器上,所以这个时候只能算作一半可用。想要让整个服务完全可用,我们就需要有一套故障转移机制。想要进行故障转移,就首先要能发现故障。
以我们这里的 PHP-FPM 的 Web 应用为例,负载均衡通常会定时去请求一个 Web 应用提供的健康检测的地址。这个时间间隔可能是 5 秒钟,如果连续 2~3 次发现健康检测失败,负载均衡就会自动将这台服务器的流量切换到其他服务器上。于是,我们就自动地产生了一次故障转移。故障转移的自动化在大型系统里是很重要的,因为服务器越多,出现故障基本就是个必然发生的事情。而自动化的故障转移既能够减少运维的人手需求,也能够缩短从故障发现到问题解决的时间周期,提高可用性。
image可以在 Web 应用上设置了一个 Heartbeat 接口,每 20 秒检查一次,出现问题的时候可以进行故障转移切换。
这样,通过水平扩展相同功能的服务器来去掉单点故障,并且通过健康检查机制来触发自动的故障转移的系统的可用性就是100% - (100% - 99.99%) × (100% - 99.99%) = 99.999999%
。这样不能提供服务的时间就减少到了原来的万分之一。在实际情况中,可用性没法做到那么理想的地步。光从硬件的角度,从服务器到交换机,从网线连接到机房电力,从机房的整体散热到外部的光纤线路等等,可能出现问题的地方太多了。这也是为什么,我们需要从整个系统层面,去设计系统的高可用性,因为在机器数量上去之后,每个环节都能产生很大的影响。
总结
水平扩展和可用性,光有这两点还是不够的。一旦系统里面有了很多台服务器。特别是,为了保障可用性,对于同样功能的、有状态的数据库进行了水平的扩展,我们就会面临一个新的挑战,那就是分区一致性问题。不过,这个问题更多的是一个软件设计问题。
通过升级硬件规格来提升服务能力的垂直扩展。除此之外,也可以通过增加服务器数量来提升服务能力。不过归根到底,我们一定要走上水平扩展的路径。
一方面是因为垂直扩展不可持续;另一方面,则是只有水平扩展才能保障高可用性。而通过水平扩展保障高可用性,则需要我们做三件事情。第一个是理解可用性是怎么计算的。服务器硬件的损坏只是可能导致可用性损失的因素之一,机房内的电力、散热、交换机、网络线路,都有可能导致可用性损失。而外部的光缆、自然灾害,也都有可能造成我们整个系统的不可用。所以,在分析设计系统的时候,我们需要尽可能地排除单点故障。进一步地,对于硬件的故障,我们还要有自动化的故障转移策略。在这些策略都齐全之后,我们才能真的长舒一口气,在海量的负载和流量下安心睡个好觉。
网友评论