【数据蒋堂】第 7 期:硬盘的性能特征
我们都知道内存比硬盘要快得多,大概能快出一两个数量级(价格也要贵这么多)。不过,硬盘的问题并不只是访问速度慢。
硬盘不适合做频繁小量访问
所谓频繁小量访问,是指运算过程中每次获取的数据量很小,而读取的次数很多。对于内存,每次取 10 字节、取 1000 万次、总共访问 1 亿字节;和每次取 1000 万字节、取 10 次、也访问 1 亿字节;这两者耗时是差不多的。内存访问数据的最小单位就是字节,无论怎么做,最后涉及到的字节数是一样的。
但硬盘的机制完全不同,在硬盘中数据是按扇区存放的,数据访问的最小单位是扇区(一般是 512 字节),也就是说从硬盘上读 1 个字节和读 512 字节的时间几乎一样的(内存复制时间相对于硬盘时间忽略不计了)。这样,每次取 10 字节取 1000 万次,和每次取 1000 万字节取 10 次,访问量一样,但耗时就可能会有巨大的差异,前者可能会多取很多无用的数据。
实际情况还要更恶劣一些,我们常常直接使用操作系统中的文件存储数据,而大部分文件系统的读取单位是簇,缺省大小一般是 4K,比 512 字节要大 8 倍。而且,读取硬盘是一个复杂的动作,不象内存那样执行一条 CPU 指令就可以读取,访问硬盘需要一系列的动作来启动硬盘控制器并确定读取位置,读取 1000 万次就要执行 1000 万次这样的动作,当然一次读 1000 万字节时也会涉及较多次的磁盘动作(1000 万字节会在很多扇区中),但总次数仍然会少得多,批量不频繁读取的性能就会远远好于频繁小量读取。
不连续的随机访问是罪魁
不过,我们在上述结论中用了“可能”二字,而没有用肯定的语气,这是因为还要考虑数据存储是否连续。如果要访问的数据在硬盘上是连续存储的,那取 1000 万次 10 字节也不会很慢,因为后面的 10 字节已经在前面读出的扇区里面而不必再读,硬盘控制器以及操作系统都有缓存功能,实际硬盘读取次数并没有那么多,结果的性能相差也不会非常大。所以我们还要在 "频繁小量" 前面加上“随机”这个定语,也就是读取的内容不连续,这时候,前面读出的扇区取出需要的部分外,其它内容和后面要访问的数据没有关系,只能浪费掉,而且后面再访问又要再次读,”可能“就会变成”肯定“了。
对于目前仍在大量采用的机械硬盘,随机访问还存在磁头跳动问题,也就是硬盘指标中的平均寻道时间。寻道是个非常慢的机械动作,比读一个扇区要慢得多得多。这样,即使每次读出扇区(簇)都没有任何浪费,在随机访问时的寻道成本却可能超过读取本身。使用机械硬盘时要特别注意避免随机频繁小量访问。
固态硬盘的情况要好一些,它没有寻道的机械动作了,这样在随机读取时也没有寻道时间问题。不过,尽管固态硬盘并不是按扇区形式存储数据,操作系统仍将它模拟得和机械硬盘类似,必须按某个基本单位(簇)读写,这样,随机的频繁小量(小于簇)访问还是会有前述的低效问题。对于许多需要随机访问小量数据的运算,簇(扇区)这个单位太大了。
并行和并发导致随机访问
那么,对于只需要连续批量访问的运算(比如遍历汇总或查找),使用硬盘时的性能是否就只是其本身访问速度决定的呢?
对于单线程或单任务的运算基本上是这样。但现在研究高性能计算时显然不可能不考虑并行运算,而且许多运算服务也需要支持多并发。并行和并发运算会使原本可以连续访问的硬盘数据一定程度变成随机访问,原因很简单:多线程实际上在共享同一套硬盘,不同线程的访问请求显然不会连续,硬盘要同时响应这些请求就会发生跳动,也就是随机访问了。
对于机械硬盘这个后果常常很严重,如果线程之间切换频繁,就会导致频繁的寻道动作,结果就会发生多线程反而比单线程慢的现象。而有些单任务时性能尚可的场景,一旦并发了性能就会急剧下降。一个补救的办法是加大读数据的缓冲区,每个线程每次读出的数据足够多且连续,这样能使寻道时间占比变小而感觉不明显。但同时也会加剧内存占用,线程越多对内存需求越大。固态硬盘就好得多,缓冲区达到簇(扇区)的规模就可了。
有点类似的场景是列式存储,数据是按列连续存放的,需要多列计算时,即使单线程也会发生硬盘随机访问现象,涉及列较多时就未必会比行存有优势,多线程还会进一步加剧这个问题。在机械硬盘上用列存时要特殊注意这个问题,不一定总能提高性能。
外存运算需要采用不同的算法
由于硬盘的这些特征,有些运算从内存换到外存时会采用完全不同的方法,性能下降的程度也不是简单按比例做乘法。
比较典型的是 JOIN 运算,对于外键式 1:N 的 JOIN,如果数据全部在内存中,可以使用针对外键维表主键做 HASH 索引,遍历事实表时用索引直接可以找到维表的相应记录,事实表有多个外键指向不同维表时也只要遍历一次就可以全解析。但如果维表过大而不能装入内存时就不能采用同样的算法了,因为硬盘无法支持随机频繁小量的访问,而访问维表记录恰恰是这种运算,强行使用这种方案的性能会恶劣到还不如把数据先排序再来归并的地步。外存大表 JOIN 时要按键值(的 HASH 值)分段,每段都足够小可以装入内存做 JOIN,这种方法一次只能解析掉一个关联,事实表指向多个外键大维表时就要做多轮解析,运算量比内存 JOIN 大得多。
单机内存不足时还可以利用集群来避免复杂的外存运算。网络本身的延迟和硬盘差不多,通过网络获取集群机的内存数据时,单位数据量的访问性能不会比硬盘好多少,但却可以利用内存的随机访问能力和并发能力。不过网络和硬盘类似,每次启动一个网络连接的消耗较大,也不合适频繁小量的访问,要把多次获取数据的请求和返回结果积累起来一起传输。用这种方法做大维表 JOIN,比内存计算麻烦,但比外存运算还是简单很多,仍然可以一次性解析掉多个关联。