Storage-CheatSheet

返回目录

存储与 IO 必知必会

计算机的内存往往包括了 RAM 与 ROM,ROM 表示的是只读存储器,即:它只能读出信息,不能写入信息,计算机关闭电源后其内的信息仍旧保存,如计算机启动用的 BIOS 芯片。RAM 表示的是读写存储器,可其中的任一存储单元进行读或写操作,计算机关闭电源后其内的信息将不在保存,再次开机需要重新装入,通常用来存放操作系统,各种正在运行的软件、输入和输出数据、中间结果及与外存交换信息等,我们常说的内存主要是指 RAM。

所谓外存/辅存(Storage),狭义上是讲的硬盘;准确地说,是外部存储器(需要通过 IO 系统与之交换数据,全称为辅助存储设备)。

计算机硬件性能在过去十年间的发展普遍遵循摩尔定律,通用计算机的 CPU 主频早已超过 3GHz,内存也进入了普及 DDR4 的时代。然而传统硬盘虽然在存储容量上增长迅速,但是在读写性能上并无明显提升,同时 SSD 硬盘价格高昂,不能在短时间内完全替代传统硬盘。传统磁盘的 IO 读写速度成为了计算机系统性能提高的瓶颈,制约了计算机整体性能的发展。

DMA

CPU 操作外设时,将外设的数据读到内部寄存器中,再将数据传送至内存中,之所以还要讲数据送到内存,在于 CPU 内部寄存器数量很少,一般都是靠 RAM 来临时存储大量的代码和数据的。CPU 工作的核心就是一个 PC 指针,PC 指针指向什么地址,CPU 就会把相应地址处的二进制数据送至内部译码器进行译码后运行,RAM 是一个临时存放代码和数据的地方,CPU 要执行代码时,就要到内存(RAM)中去取指令。

DMA:在现代操作系统中,外设有数据到来时,基本上都采用中断方式通知 CPU,操作系统响应中断,然后再从外设读取数据,这时,如果外设的数据比较频繁,那么是否每到一个数据都中断一次呢??这样 CPU 就非常频繁地被外调中断打断,操作系统在处理中断时要浪费一定时间,而且 CPU 读外部 IO 速度也很慢,这样的话,大量时间被用在了响应中断上,而去调度其它任务的时间减少,让人感觉系统响应速度不够,也会影响外设的数据传输速度(如果外设传输速度太快,操作系统就有可能丢失部分数据),由此引出 DMA 的机制:

image

外设直接将一块数据放在了 RAM 中,然后再产生一次中断,这样操作系统直接将内存中的那块数据传给想要获取这块数据的一个任务(或者放在内存的另一空闲部分),此时,系统就少了频繁响应外设中断的开销,也少了读取外设 IO 的时间开销(读取 RAM 比读取外设 IO 要快很多)。

IO

对 Linux 系统而言,所有设备都是文件,其中包括磁盘、内存、网卡、键盘、显示器等等,对所有这些文件的访问都属于 IO。针对所有的 IO 对象,可以将 IO 分成三类:网络 IO、磁盘 IO 和内存 IO。而通常我们说的是前两种。

只有网络 IO 是能够单线程事件循环,文件 IO 暂时只能用线程池来模拟事件循环。

Unix 的 5 种 IO 模型:阻塞式 IO, 非阻塞式 IO,IO 复用模型,信号驱动式 IO 和异步 IO。

  • 阻塞式 IO

blocking_io
  • 非阻塞式 IO

nonblocking_io
  • IO 复用(select,poll)

IO 多路复用通过把多个 IO 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况可以同时处理多个客户端请求。目前支持 IO 多路复用的系统调用有 select,pselect,poll,epoll,在 linux 网络编程过程中,很长一段时间都是用 select 做轮询和网络事件通知,然而 select 的一些固有缺陷导致了它的应用受到了很大的限制,最终 linux 不得不载新的内核版本中寻找 select 的替代方案,最终选择了 epoll。

multplexing_io
  • 信号驱动式 IO

signal_driven
  • 异步 IO

asynchronous_io

一般有三种操作 IO 的方式:

blocking IO: 发起 IO 操作后阻塞当前线程直到 IO 结束,标准的同步 IO,如默认行为的 posix read 和 write。 non-blocking IO: 发起 IO 操作后不阻塞,用户可阻塞等待多个 IO 操作同时结束。non-blocking 也是一种同步 IO:“批量的同步”。如 linux 下的 poll,select, epoll,BSD 下的 kqueue。 asynchronous IO: 发起 IO 操作后不阻塞,用户得递一个回调待 IO 结束后被调用。如 windows 下的 OVERLAPPED + IOCP。linux 的 native AIO 只对文件有效。

linux 一般使用 non-blocking IO 提高 IO 并发度。当 IO 并发度很低时,non-blocking IO 不一定比 blocking IO 更高效,因为后者完全由内核负责,而 read/write 这类系统调用已高度优化,效率显然高于一般得多个线程协作的 non-blocking IO。但当 IO 并发度愈发提高时,blocking IO 阻塞一个线程的弊端便显露出来:内核得不停地在线程间切换才能完成有效的工作,一个 cpu core 上可能只做了一点点事情,就马上又换成了另一个线程,cpu cache 没得到充分利用,另外大量的线程会使得依赖 thread-local 加速的代码性能明显下降,如 tcmalloc,一旦 malloc 变慢,程序整体性能往往也会随之下降。而 non-blocking IO 一般由少量 event dispatching 线程和一些运行用户逻辑的 worker 线程组成,这些线程往往会被复用(换句话说调度工作转移到了用户态),event dispatching 和 worker 可以同时在不同的核运行(流水线化),内核不用频繁的切换就能完成有效的工作。线程总量也不用很多,所以对 thread-local 的使用也比较充分。这时候 non-blocking IO 就往往比 blocking IO 快了。不过 non-blocking IO 也有自己的问题,它需要调用更多系统调用,比如 epoll_ctl,由于 epoll 实现为一棵红黑树,epoll_ctl 并不是一个很快的操作,特别在多核环境下,依赖 epoll_ctl 的实现往往会面临棘手的扩展性问题。non-blocking 需要更大的缓冲,否则就会触发更多的事件而影响效率。non-blocking 还得解决不少多线程问题,代码比 blocking 复杂很多。

IO 类别

不同应用通常具有不同的 IO 类型,了解应用的 IO 类型是为其设计解决方案、排错性能问题的首要工作。那 IO 类型通常包括哪些需要考虑的因素?我们今天就来谈一谈 IO 类型的几个重要方面。

读 vs. 写

应用程序的读写请求必须量化,了解他们之间的比列,因为读写对存储系统的资源消耗是不通的。了解读写比率直接关系到如何应用缓存、RAID类型等子系统的最佳实践。写通常需要比读更多的资源,SSD的写操作相对读更是慢得多。

顺序 vs. 随机

传统存储系统通常都是机械硬盘,因此整个系统设计为尽可能顺序化IO,减少由于磁盘寻道所带来的延迟。所以,顺序IO相对随机IO的性能会好很多。随机小IO消耗比顺序大IO更多的处理资源。随机小IO更在意系统处理IO的数量,即IOPS;而顺序大IO则更在意带宽,即MB/s。因此,如果系统承载了多种不同的应用,必须了解它们各自的需求,是对IOPS有要求,还是对带宽有要求。这往往需要在两种之间进行折衷考虑。闪盘是一个例外,它没有机械寻道操作,因此对随机小IO的处理是非常迅速的,由此是读操作。

大 IO vs. 小 IO

我们通常把<=16KB的IO认为是小IO,而>=32KB的IO认为是大IO。就单个IO来讲,大IO从微观的角度相比小IO会需要更多处理资源,不过对于智能存储系统来说,会尽可能把IO整理为顺序的,以单个操作执行,如此依赖,将多个小IO整理成单个大IO处理后,反而会更快。IO的大小依然取决于应用程序本身,了解IO的大小,影响到后期对缓存、RAID类型、LUN的一些属性的调优。

位置引用

数据的位置分布影响到后期对二级缓存或存储分层技术的应用,因为这些技术都会根据IO的位置分布来判断是否将IO放置到缓存或快速的层级。位置引用是指那些被频繁的存储位置,我们通常认为最新创建的数据以及最近被访问过的数据,它们周围的数据也同时被访问的可能性会比较大。因此,了解应用程序的IO位置特性,有助于应用正确的性能优化技术。

稳定 vs. 爆发

IO数量在一天中的不同时段会有不同的表现。例如,早高峰时段的IO数量相比下班后的IO会多出许多。如果能准确预测和估计应用的IO在不同时间段的稳定性和爆发性,可以正确分配资源,提高资源利用率。在前期的设计阶段,就应该考虑系统是否能够处理IO高峰期。

多线程 vs. 单线程

多线程是实现并发操作的一种方式,同时也意味着对存储系统的资源消耗更多。这种高IOPS的请求方式,在有些情况下会造成磁盘繁忙,进而导致IO排队,增加了响应时间。因此,适度的调整线程数量,不仅可以实现并发,而且能在不拖累整个存储系统的情况下,达到最优的响应时间。
image

Concurrent IO | 并发 IO

IO 多路复用

select,poll,epoll 都是 IO 多路复用的机制。IO 多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select,poll,epoll 本质上都是同步 IO,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 IO 则无需自己负责进行读写,异步 IO 的实现会负责把数据从内核拷贝到用户空间。

select 本身是轮询式、无状态的,每次调用都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。epoll 则是触发式处理连接,维护的描述符数目不受到限制,而且性能不会随着描述符数目的增加而下降。

方法

数量限制

连接处理

内存操作

select

描述符个数由内核中的 FD_SETSIZE 限制,仅为 1024;重新编译内核改变 FD_SETSIZE 的值,但是无法优化性能

每次调用 select 都会线性扫描所有描述符的状态,在 select 结束后,用户也要线性扫描 fd_set 数组才知道哪些描述符准备就绪(O(n))

每次调用 select 都要在用户空间和内核空间里进行内存复制 fd 描述符等信息

poll

使用 pollfd 结构来存储 fd,突破了 select 中描述符数目的限制

类似于 select 扫描方式

需要将 pollfd 数组拷贝到内核空间,之后依次扫描 fd 的状态,整体复杂度依然是 O(n)的,在并发量大的情况下服务器性能会快速下降

epoll

该模式下的 Socket 对应的 fd 列表由一个数组来保存,大小不限制(默认 4k)

基于内核提供的反射模式,有活跃 Socket 时,内核访问该 Socket 的 callback,不需要遍历轮询

epoll 在传递内核与用户空间的消息时使用了内存共享,而不是内存拷贝,这也使得 epoll 的效率比 poll 和 select 更高

IO

每个外设都是通过读写其寄存器来控制的。外设寄存器也称为 IO 端口,通常包括:控制寄存器、状态寄存器和数据寄存器三大类。根据访问外设寄存器的不同方式,可以把 CPU 分成两大类。一类 CPU(如 M68K,Power PC 等)把这些寄存器看作内存的一部分,寄存器参与内存统一编址,访问寄存器就通过访问一般的内存指令进行,所以,这种 CPU 没有专门用于设备 IO 的指令。这就是所谓的“IO 内存”方式。另一类 CPU(典型的如 X86),将外设的寄存器看成一个独立的地址空间,所以访问内存的指令不能用来访问这些寄存器,而要为对外设寄存器的读/写设置专用指令,如 IN 和 OUT 指令。这就是所谓的“ IO 端口”方式。但是,用于 IO 指令的“地址空间”相对来说是很小的,如 x86 CPU 的 IO 空间就只有 64KB(0-0xffff)。

内存空间:内存地址寻址范围,32 位操作系统内存空间为 2 的 32 次幂,即 4G。 IO 空间:X86 特有的一个空间,与内存空间彼此独立的地址空间,32 位 X86 有 64K 的 IO 空间。

IO 端口:当寄存器或内存位于 IO 空间时,称为 IO 端口。一般寄存器也俗称 IO 端口,或者说 IO ports,这个 IO 端口可以被映射在 Memory Space,也可以被映射在 IO Space。

IO 内存:当寄存器或内存位于内存空间时,称为 IO 内存。

存储调优

RAID

RAID 独立磁盘冗余阵列,都是指在利用多块硬盤,做到数据保护或加速的方式; RAID 0,条带式,对所有硬盤做平均分散的读写,盤愈多速度最快,创建至少需要 2 颗 HD,安全性差。

RAID 1,镜像式,每块盤的上数据都完全相同,创建至少需要 2 颗 HD, 只要留有 1 颗盤数据都安全,安全性最高。

RAID 5,有 1 块盤的容量来存放校验码,创建至少需要 3 颗 HD, 可以去除 1 颗数据都安全。性价比最高。

RAID 10,先做镜像再做条带,创建至少需要 4 颗 HD。可以同时去除半数的盤(但要确认是在镜像保护下的盤),数据都安全。

raid0 就是把多个(最少 2 个)硬盘合并成 1 个逻辑盘使用,数据读写时对各硬盘同时操作,不同硬盘写入不同数据,速度快。

raid1 就是同时对 2 个硬盘读写(同样的数据)。强调数据的安全性。比较浪费。

raid5 也是把多个(最少 3 个)硬盘合并成 1 个逻辑盘使用,数据读写时会建立奇偶校验信息,并且奇偶校验信息和相对应的数据分别存储于不同的磁盘上。当 RAID5 的一个磁盘数据发生损坏后,利用剩下的数据和相应的奇偶校验信息去恢复被损坏的数据。相当于 raid0 和 raid1 的综合。

raid10 就是 raid1+raid0,比较适合速度要求高,又要完全容错,当然¥也很多的时候。最少需要 4 块硬盘(注意:做 raid10 时要先作 RAID1,再把数个 RAID1 做成 RAID0,这样比先做 raid0,再做 raid1 有更高的可靠性)

顺序读写

但是顺序 IO 时,读和写的区域都是被 OS 智能 Cache 过的热点区域,不会产生大量缺页中断,文件的 IO 几乎等同于内存的 IO,性能当然就上去了。

Zero Copy

Todos