53.MESI协议:如何让多核CPU的高速缓存保持一致
- CPU Cache解决的是内存访问速度和CPU速度差距过大的问题.
- 多核CPU是通过增加CPU核心来增加CPU吞吐率的办法.
- 缓存一致性问题
- 核心1和核心2都将内存数据A加载到对应的CPU Cache中.
- 核心1在对应的CPU Cache中对A进行了修改,核心2的CPU Cache及主内存中还是老数据.
- 缓存一致性问题的解决: 总线嗅探机制
- 总线嗅探机制的本质就是将所有的读写请求都通过总线广播给所有的CPU核心,然后让各个CPU核心去'嗅探'这些请求,再根据本地的情况进行响应.
- MESI协议
- 基于总线嗅探机制,最常用的就是MESI协议.
- MESI协议是一种叫做写失效的协议.
- 写失效协议里,同时只有1个CPU核心负责写入数据
- 其他核心会同步读取这个写入
- 这个CPU核心写入CPU Cache后,会广播1个失效请求告知其他所有CPU核心.
- 其他所有CPU核心,收到失效请求,会校验自己是否包含对应的Cache Block,将其标记为失效.
- 写广播协议
- 写失效协议,写入CPU Cache的CPU核心,只需告诉其他CPU核心哪个内存地址对应的CPU缓存失效了
- 写广播协议,除了上述内容,还有把对应的数据传输给其他CPU核心.
- 数据往往比操作信号和地址信号大得多,所以写广播需要占用更多的总线带宽.
- MESI的4种状态: 独占, 共享, 已修改, 已失效
- M:代表已修改(Modified)
- 已修改,就是这是一个'脏'的Cache Block.
- 这个Cache Block中的内存已经更新过,但还未写回主内存
- E:代表独占(Exclusive)
- 这个Cache Block中的数据是干净的,和主内存中一致
- 对应的 Cache Line 只加载到了当前 CPU 核所拥有的 Cache 里。其他的 CPU 核,并没有加载对应的数据到自己的 Cache.
- S:代表共享(Shared)
- 同样的数据在多个 CPU 核心的 Cache 里都有
- 更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他 CPU 核心里面的 Cache,都变成无效的状态,然后再更新当前 Cache 里面的数据.
- 这个广播操作,一般叫作 RFO(Request For Ownership),也就是获取当前对应 Cache Block 数据的所有权.
- I:代表已失效(Invalidated)
- 这个Cache Block中的数据已失效,不能继续使用.
-
MESI的4种状态转化
54.理解内存(上):虚拟内存和内存保护是什么
- 日常使用的Windows或Linux系统,程序并不能直接访问物理内存.
- 程序直接访问的地址,都是虚拟内存地址.
- 内存被分成固定大小的页,再通过虚拟内存地址到物理内存地址的转换,最终到达实际存放数据的物理内存地址.
- 虚拟内存地址如何转换成物理内存地址?
- 简单页表
- 多级页表
- 简单页表
- 将1个内存地址分为 页号 + 偏移量 两部分
- 以一个 32 位的内存地址为例
- 前面的高位,就是内存地址的页号
- 后面的低位,就是内存地址在里面的偏移量
- 同一个页面里的内存,在物理层面是连续的.以1个页的大小是4K为例,我们需要20位的高位,12位的低位
- 4K = 4 * 1024 = 2^2 * 2^10 = 2^12 .即偏移量占12位,可以表示4K空间.
- 32位的内存地址,12位是偏移量,页号就占 20位,需要 2^20页.
- 虚拟内存到物理内存的转换
- 将虚拟内存地址,切分成页号和偏移量的组合
- 找到虚拟页号对应的物理页号
- 物理页号加上偏移量,就是物理内存地址
- 简单页表的问题:简单页表会占用大量的内存空间
- 以32位内存地址,每页4K为例. 每个内存地址32位占4字节/Byte
- 2^20 页,需要占用 4 * 2^20 Byte = 4 * 1024 * 1024 Byte = 4M
- 我们每一个进程,都有属于自己独立的虚拟内存地址空间.
- 虚拟内存地址空间 是和 单个进程 相关联.
- 假设系统同时运行了100-200个进程,光是页表就占用了400M-800M
- 刚刚看了一下自己Windows任务管理器,168个进程在运行 - 上面是32位的内存地址,现在都是64位的处理器和操作系统,内存都超过4G,这样简单页表占用的空间就更大了.
- 多级页表
-
以4级页表为例
- 同样一个虚拟内存地址,偏移量的部分和上面简单页表一样不变,但是原先的页号部分,我们把它拆成四段,从高到低,分成 4 级到 1 级这样 4 个页表索引.
- 32位的内存地址,12位低位是偏移量,20位高位分为4段.每段5位.
- 4级页表,3级页表,2级页表,1级页表,填满状态下,都包含 2^5 = 32 个条目
- 4级页表的1个条目,存储了1个3级页表的位置/32个3级条目的位置
- 3级页表的1个条目,存储了1个2级页表的位置/32个2级条目的位置
- 2级页表的1个条目,存储了1个1级页表的位置/32个1级条目的位置
- 1级页表里面的条目,对应的数据内容就是物理页号/32位占4字节.
- 拿到了物理页号之后,我们同样可以用“页号 + 偏移量”的方式,来获取最终的物理内存地址
- 每1级页表1个条目占4字节,1个页表占128字节
- 32位条目占用4字节/Byte,可代表4K的内存空间
- 1个填满的1级页表对应4K * 32 = 128K 内存空间.
- 1个填满的2级页表对应 128K * 32 = 4M 内存空间.
- 1个填满的3级页表对应 4M * 32 = 128M 内存空间.
- 1个填满的1级页表对应 128M * 32 = 4G 内存空间.
- 在程序运行的时候,内存地址从顶部往下,不断分配占用的栈的空间。而堆的空间,内存地址则是从底部往上,是不断分配占用.
- 所以,在一个实际的程序进程里面,虚拟内存占用的地址空间,通常是两段连续的空间。
- 实例:
- 假设1个进程总共占用8M内存空间,因为是2段连续的空间,1个填满的2级页表对应4M. 所以需要2个填满的2级页表/64个2级页表条目
- 2个填满的2级页表对应64个填满的1级页表
- 2个填满的2级页表对应2个3级条目,因为内存是在2头的,所以属于2个3级表.
- 2个3级表来源于1个4级表.
- 总共有64+2+2+1 = 69个页表. 128 * 69 = 8832B. 多级页表仅占用9K空间.
55.理解内存(下):解析TLB和内存保护
- 虚拟内存到物理内存的转换,是通过多级页表实现的.
- 每一条指令和数据都存在内存里面,所以'地址转换'是1个非常高频的动作.
- 加速地址转换: TLB/地址变换高速缓冲/Translation-Lookaside Buffer
- 多级页表节约了存储开销,但增加了时间上的开销,是1个以时间换空间的策略.
- 原本进行1次地址转换,只要访问1次虚拟内存,即得到物理内存地址
- 使用4级页表,需要访问4次虚拟内存地址,才得到物理内存地址
- 程序需要的指令,是顺序放在虚拟内存中,指令的执行,也是按顺序一条条执行.
- 对应指令地址的访问,存在之前所述的'空间局部性'和'时间局部性'
- 程序需要的数据也是一样的,存在'空间局部性''和'时间局部性'.
- 我们连续执行了 5 条指令。因为内存地址都是连续的,所以这 5 条指令通常都在同一个“虚拟页”里。因此,这连续 5 次的内存地址转换,其实都来自于同一个虚拟页号,转换的结果自然也就是同一个物理页号。那我们就可以用前面几讲说过的,用一个“加个缓存”的办法。把之前的内存转换地址缓存下来,使得我们不需要反复去访问内存来进行内存地址转换.
- 对于存在'时间局部性'和'空间局部性'的访问,可以添加'缓存'来提升访问性能.
- TLB
- CPU专门有一块缓存芯片,称为TLB(Translation-Lookaside Buffer),地址变换高速缓冲.
- TLB存放了之前已经进行过地址转换的查询结果。这样,当同样的虚拟地址需要进行地址转换的时候,我们可以直接在 TLB 里面查询结果,而不需要多次访问内存来完成一次转换.
- TLB和CPU Cache类似:
- 同样可以分为L1,L2级别
- 同样分成指令的TLB和数据的TLB
- MMU
- 为了性能,内存转换过程要由硬件来完成-MMU.
- 在CPU芯片中,我们封装了内存管理单元芯片(MMU Memory Management Uit),用于完成地址转换.
-
MMU进行和TLB的访问及交互.
- 安全性及内存保护机制:
- 可执行空间保护
- 地址空间布局随机化
- 可执行空间保护
- 对于1个进程使用的内存,只把其中的指令部分设置成'可执行'的.对于其他部分,比如数据部分,不给与'可执行'权限.
- 因为无论是指令还是数据,在CPU看来都是都是二进制数据,如果一条数据经过CPU解码后也变成一条合理的指令,其实就是可执行的.
- 对于进程里内存空间的执行权限进行控制,可以使得 CPU 只能执行指令区域的代码。对于数据区域的内容,即使找到了其他漏洞想要加载成指令来执行,也会因为没有权限而被阻挡掉.
- 地址空间布局随机化/Address Space Layout Randomization
- 其他人,程序,进程,可能会修改特定进程A的指令及数据,导致指令A执行了错误的指令和数据.
- 如果1个进程A的内存布局空间是固定的,其他进程很容易知道A的指令在哪里,数据在哪里,程序栈在哪里,堆在哪里. 很容易被篡改.
- 地址空间布局随机化,就是让这些区域的位置不固定,在内存空间随机分配不同部分的位置.
- 破坏者不知道要修改的指令或数据的位置,随便修改,只会让程序崩溃,也不会执行到错误的指令或数据
- 破坏者让程序崩溃后,程序再次启动,内存中不同部分的布局依然是重新随机分配,依然无法执行错误指令及数据.
- 随机化,类似保存用户密码,会加盐.服务端保存的是
- 用户输入的密码 和 1个随机生成的字符串/盐 按一定规则组合的字符串的哈希值
- 盐
56.总线:计算机内部的高速公路
计算机5大组成部分: 运算器, 控制器, 存储器, 输入设备, 输出设备
运算器+控制器: CPU
CPU要和内存,输入输出设备进行通信
-
总线的设计思想:降低复杂度
- 计算机有很多硬件设备:CPU,内存,键盘,鼠标,硬盘,USB外接设备等
- 若N个设备之间互相通信,则复杂度是N^2
- 为了降低复杂度,引入总线,将设备间通信复杂度降低为N
-
总线如何降低复杂度
- 避免各个设备之间直接通信.
- 设计1个共用的线路:设备A想要和别的设备通信,通信的指令是什么,对应的数据是什么,都发到这个总线上
-
总线本质
- 总线,其实就是一组线路。我们的 CPU、内存以及输入和输出设备,都是通过这组线路,进行相互间通信的。总线的英文叫作 Bus,就是一辆公交车。这个名字很好地描述了总线的含义。我们的“公交车”的各个站点,就是各个接入设备。要想向一个设备传输数据,我们只要把数据放上公交车,在对应的车站下车就可以了
-
应用开发中各个模块之间的通信,也会用到事件总线这种设计模式.
- 各个模块触发对应的事件,发送到总线上,每个模块都是一个发布者.
- 各个模块都会把自己注册到总线上,监听总线上的事件,进而进行特定响应.
-
各个模块之间是弱耦合的,无依赖关系
-
总线分类
- 后端总线/本地总线
- 用于CPU和高速缓存之间通信
- 前端总线
- 用于CPU和主内存及IO设备进行通信
- CPU中的北桥芯片,将前端总线一分为3
- CPU到北桥芯片之间的称为系统总线
- 北桥芯片到内存之间的称为内存总线
- 北桥芯片到IO设备之间的称为IO总线
- 因为不同设备的速度有差异,因而需要多个总线(后端总线,系统总线,内存总线,IO总线)
- 后端总线/本地总线
-
总线裁决
- 同一个总线是给多个设备共用的,但同一条总线不能同时给多个设备提供通信功能
- 那多个设备都想要用总线,我们就需要有一个机制,去决定这种情况下,到底把总线给哪一个设备用。这个机制,就叫作总线裁决(Bus Arbitraction).
57.输入输出设备:我们并不是只能用灯泡显示“0”和“1”
- 输入输出设备,都有2个组成部分:
- 第一个是它的接口
- 第二个是实际的IO设备
- 硬件设备并不是直接接入到总线上和CPU进行通信的,而是通过接口,用接口连接到总线上,再通过总线和CPU通信.
- 接口本身就是一块电路板.
- 硬件设备里的3类寄存器,都在这个设备的接口电路上.
- 状态寄存器
- 命令寄存器
- 数据寄存器
- 接口中还有接口控制电路.
- 接口电路通过总线和CPU进行通信,接收来自CPU的数据和指令.
- 接口电路中的控制电路,对指令进行解码,再实际操作对应的硬件设备
- 硬件设备里的3类寄存器,都在这个设备的接口电路上.
- CPU和IO设备的通信,一样是通过CPU支持的机器指令来执行的.
- CPU 并不是发送一个特定的操作指令来操作不同的 I/O 设备。因为如果是那样的话,随着新的 I/O 设备的发明,我们就要去扩展 CPU 的指令集了.
- 在 I/O 设备这一侧,我们把 I/O 设备拆分成,能和 CPU 通信的接口电路,以及实际的 I/O 设备本身。接口电路里面有对应的状态寄存器、命令寄存器、数据寄存器、数据缓冲区和设备内存等等。接口电路通过总线和 CPU 通信,接收来自 CPU 的指令和数据。而接口电路中的控制电路,再解码接收到的指令,实际去操作对应的硬件设备。
- 对 CPU 来说,它看到的并不是一个个特定的设备,而是一个个内存地址或者端口地址。CPU 只是向这些地址传输数据或者读取数据。所需要的指令和操作内存地址的指令其实没有什么本质差别。
58.理解IO_WAIT:I/O性能到底是怎么回事儿
- 在开发应用系统的时候,我们遇到的性能瓶颈大部分都在IO上.
- 把内存当做缓存,来提升系统的整体性能,很多情况下不能解决问题.
- 当硬盘上的数据量过大,使用内存做缓存,内存的空间是不够的.
- 大部分时间,我们的请求还是要打到硬盘上。
- 硬盘的性能指标
- 数据传输率/Data Transfer Rate
- 例如 200MB/s
- 响应时间/Response Time
- 程序发起一个硬盘的写入请求,直到这个请求返回的时间.
- IOPS
- 每秒输入输出操作的次数
- IOPS特指随机读写下硬盘每秒输入输出的次数.
- SSD硬盘的IOPS可以达到2W
- 机械硬盘的IOPS只有100左右
- IOPS 和 Data Transfer Rate才是输入输出性能的核心指标.
- 因为在实际应用开发中,对于数据的访问,往往是随机读写
- 服务器承受的并发压力,大部分都是不同进程访问硬盘中随机位置的数据.
- 数据传输率/Data Transfer Rate
- 对于硬盘的 顺序读写 及 随机读写
- 在顺序读写和随机读写情况下,硬盘的性能是完全不同的.
- 在随机读写的情况下,接口本身的速度已经不是我们硬盘访问速度的瓶颈了.
- 随机读写下硬盘的数据传输率只有顺序读写的几十分之一
- 为什么说'性能瓶颈在IO上'
- 即使是最快的用上了 PCI Express 接口的 SSD 硬盘,IOPS也就是2万左右.
- 而CPU主频通常都是2G以上,即每秒做20亿次以上运算.
- 即使CPU向硬盘发起一条读写指令,需要多个时钟周期,一秒钟内CPU可以执行的指令数量和硬盘可以进行的读写数量,也有好几个数量级的差距.
- 很多时候,CPU指令发出之后,不得不去等我们的IO操作完成,才能进行下一步的操作.
- 实际遇到服务端程序的性能问题时,如何定位是不是CPU在等IO?
- 使用linux命令进行排查: top , iostat, iotop
- top
- top 命令的输出结果里面,有一行是以 %CPU 开头的。这一行里,有一个叫作 wa 的指标,这个指标就代表着 iowait,也就是 CPU 等待 IO 完成操作花费的时间占 CPU 的百分比.
- sy:系统调用.
- iostat
- 输入“iostat”,就能够看到实际的硬盘读写情况
- tps 指标,其实就对应着我们上面所说的硬盘的 IOPS 性能。而 kB_read/s 和 kB_wrtn/s 指标,就对应着我们的数据传输率的指标.
- 知道实际硬盘读写的 tps、kB_read/s 和 kb_wrtn/s 的指标,我们基本上可以判断出,机器的性能是不是卡在 I/O 上了.
- iotop
- 通过 iotop 这个命令,你可以看到具体是哪一个进程实际占用了大量 I/O,那么你就可以有的放矢,去优化对应的程序了.
- 总结
- 在 Linux 下,我们可以通过 top 这样的命令,来看整个服务器的整体负载。在应用响应慢的时候,我们可以先通过这个指令,来看 CPU 是否在等待 I/O 完成自己的操作。进一步地,我们可以通过 iostat 这个命令,来看到各个硬盘这个时候的读写情况。而 iotop 这个命令,能够帮助我们定位到到底是哪一个进程在进行大量的 I/O 操作。
59.机械硬盘:Google早期用过的“黑科技”
- 机械硬盘的构造
- 盘面
- 盘面上实际存储数据的盘片.
- 盘面上有1层磁性的涂层,数据就存储在这个磁性地方图层上.
- 盘面正反两面都有1层磁性涂层存储数据.
- 盘面中间有1个受电机控制的转轴,这个转轴会控制我们的盘面旋转.
- 硬盘的转速,就是盘面中间受电机控制的转轴的旋转速度,即每分钟的旋转圈数,RPM(Rotations Per Minute)
- 7200转,就是7200圈每分钟,120转每秒.
- 磁头
- 数据并不能直接从盘面传输到总线,而是要通过磁头,从盘面上读取到.
- 再通过电路信号传输给控制电路,接口,再传输到总线上.
- 一个盘面有2个磁头,正反面各1个.
- 悬臂
- 悬臂连接在磁头上,并在一定范围内将磁头定位到盘面的某个特定的磁道上.
- 磁道
- 盘面通常是圆形的,有很多个同心圆组成,好像多个半径不同的'甜甜圈'嵌套而成
- 每个甜甜圈都是1个磁道,都有自己的编号
- 悬臂只是控制到底读哪个磁道的数据.
- 机械硬盘如何读取数据
- 首先:将盘面旋转到某一位置,使悬臂正对着 指定数据所在'半径'.
- 盘面旋转的时间,称为平均延时.
- 假设每次都要旋转半圈/取中值,上面 7200 转的硬盘,那么一秒里面,就可以旋转 240 个半圈。那么,这个平均延时就是1s / 240 = 4.17ms.
- 然后:悬臂定位到 指定数据所在的磁道/甜甜圈.
- 定位成功后,磁头会落下,就可以读取磁头下的数据
- 实际上多个盘面上下平行堆叠,现在磁头可以读写垂直位置下所有盘面该磁道的数据.
- 这个时间称为平均寻道时间.现在HDD硬盘的平均寻道时间是4-10ms.
- 以7200转的HDD硬盘为例,在硬盘上随机读取1个数据,需要 4.17 + (1-10) = 8-14ms.对应IOPS:
- 1000ms/8ms = 125
- 1000ms/14ms = 71
- 所以对于HDD硬盘,IOPS是100左右.
- 如何提升机械硬盘的IOPS
- IOPS = 1000 / (平均延时 + 平均寻道时间)
- 对于转速固定的硬盘,平均延时是固定值,所以只能降低平均寻道时间
- 因为硬盘数据都是从最外圈开始写的,所以,越是在外圈的数据,寻道时间越短/悬臂需要向圆心移动的距离越短.
- 如果我们只用最外层的1/2或1/4的磁道,寻道时间就可以降低为为原先的1/2或1/4.
- 例如:一块 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 倍啊。所以,这样通过软件去格式化硬盘,只保留部分磁道让系统可用的情况,可以大大提升硬件的性价比。
60.SSD硬盘(上):如何完成性能优化的KPI
- 无论是使用更高转速的机械硬盘,还是只用几分之一的磁道来提升IOPS,无非就是将IOPS从100提升到300,500就到头了.
- HDD硬盘已经无法满足现状的服务端压力
- SSD硬盘在2010年左右,进入了主流的商业应用.
- 一块普通的SSD硬盘可以轻松支撑10000到20000的IOPS.
- 不少互联网公司要完成性能优化的KPI,最终解决方案都变成了换SSD硬盘,如果还不够,就换上使用PCI Express接口的SSD.
-
SSD硬盘和机械硬盘的比较
- 无论是HDD不擅长的随机读写,还是HDD不错的顺序写入,SSD性能都好于HDD.
- 但是HDD硬盘的耐用性,要远远好于SSD硬盘.
- 如果要频繁的重复写入删除数据,HDD硬盘比SSD硬盘性价比高很多.
- SSD硬盘是由1个电容加1个电压计,记录了1个或多个比特.
- SLC, MLC, TLC, QLC
- SLC:给电容充上电有电压时候是1,电容放电没有电压就是0,称为使用了SLC的颗粒.Single-Level Cell,就是1个存储单元中只有一位数据.
- 使用SLC,存储容量上不去,价格贵.
- 硬件工程师们就陆续发明了 MLC(Multi-Level Cell)、TLC(Triple-Level Cell)以及 QLC(Quad-Level Cell),也就是能在一个电容里面存下 2 个、3 个乃至 4 个比特.
- 只有一个电容,我们怎么能够表示更多的比特呢?别忘了,这里我们还有一个电压计。4 个比特一共可以从 0000-1111 表示 16 个不同的数。那么,如果我们能往电容里面充电的时候,充上 15 个不同的电压,并且我们电压计能够区分出这 15 个不同的电压。加上电容被放空代表的 0,就能够代表从 0000-1111 这样 4 个比特了
- 要想表示 15 个不同的电压,充电和读取的时候,对于精度的要求就会更高。这会导致充电和读取的时候都更慢,所以 QLC 的 SSD 的读写速度,要比 SLC 的慢上好几倍.
- SLC:给电容充上电有电压时候是1,电容放电没有电压就是0,称为使用了SLC的颗粒.Single-Level Cell,就是1个存储单元中只有一位数据.
- SSD硬盘的实际IO设备的构造: 裸片 -> 平面 -> 块 -> 页
- SSD硬盘由多个裸片叠在一起
- 一张裸片上有多个平面
- 一个平面上有多个块
- 1个平面的存储容量在GB级别
- 一个块里面还会区分很多个页
- 1个块的存储容量在几百K到几M
- 一个页的存储容量和内存中的页一样,通常是4K.
- SSD硬盘数据的读,写,擦除
- SSD读取和写入的基本单位,不是bit也不是byte,而是1个页.
- SSD擦除就更夸张,必须按照块为单位来擦除.
- 你可以把 SSD 硬盘的一个平面看成是一张白纸。我们在上面写入数据,就好像用铅笔在白纸上写字。如果想要把已经写过字的地方写入新的数据,我们先要用橡皮把已经写好的字擦掉。但是,如果频繁擦同一个地方,那这个地方就会破掉,之后就没有办法再写字了.
- SSD 的使用寿命,其实是每一个块(Block)的可以被擦除的次数.
- SLC颗粒的SSD可以被擦除的次数是10万次左右
- MLC是1万次左右
- TLC和QLC只有几千次
- 所以同样容量的SSD硬盘价格差别很大,因为芯片颗粒和寿命完全不一样.
- SSD硬盘读写的生命周期
- 用3种颜色代表SSD硬盘中页的不同状态:白色 , 绿色, 红色
- 白色代表页是空白的.
- 绿色代表页被写入数据,且数据是有效的.
- 红色代表页之前被写入,但后来该页的数据已被删除.
- 最开始,所有的页都是白色,随着写入数据,有些页变成绿色
- 然后删除部分文件,对应的页变成红色
- 继续写入数据,不能在红色的页中写入,因为SSD不能单独擦除一个页,必须整块擦除,所以只能在后续的白色页继续写入.
- 这些散落在绿色页中的红色页,就好像磁盘碎片
- 如果哪一个快的页都变成红色,就可以整块的将其擦除重新变成白色,可以重新一页一页地写入数据.
- 这种情况其实也会经常发生。毕竟一个块不大,也就在几百 KB 到几 MB。你删除一个几 MB 的文件,数据又是连续存储的,自然会导致整个块可以被擦除
- 随着磁盘中的数据越来越多,红色的页也越来越多,最后没有白色页可以用了.
- 这时候我们需要做'磁盘碎片整理',将红色空洞最多的块A其中的绿色页的数据移动到其他块B中,然后将A整个擦除变成白色,就可以继续按页写入数据.
- 但是'磁盘碎片整理'不能太频繁,因为SSD的块的擦除次数有限,频繁擦除很快SSD硬盘寿命就耗尽.
- 因为'磁盘碎片整理'机制的存在,导致SSD硬盘容量其实是不能被用完的,因为总会遇到红色的页,必须保留部分空间用于'磁盘碎片整理'.
- 商家为了不得罪消费者,标注240G的SSD硬盘,实际总容量是256G,剩余的16G就是为了磁盘碎片整理时候,将A块中的绿色数据转移到冗余的B块中.
- 多出来的16G,叫做预留空间.一般SSD预留空间占7%-15%左右.
- 也明白了 SSD 硬盘的使用寿命受限于可以擦除的次数.因而适用于 读多写少的场景.
- 在日常应用里面,我们的系统盘适合用 SSD。但是,如果我们用 SSD 做专门的下载盘,一直下载各种影音数据,然后刻盘备份就不太好了,特别是现在 QLC 颗粒的 SSD,它只有几千次可擦写的寿命啊。
- 在数据中心里面,SSD 的应用场景也是适合读多写少的场景。我们拿 SSD 硬盘用来做数据库,存放电商网站的商品信息很合适。但是,用来作为 Hadoop 这样的 Map-Reduce 应用的数据盘就不行了。因为 Map-Reduce 任务会大量在任务中间向硬盘写入中间数据再删除掉,这样用不了多久,SSD 硬盘的寿命就会到了
- 日志系统不适合存放在SSD硬盘上,因为日志存在大量的写入,还会删除老旧的日志,写多读少.更适合HDD硬盘.
61.SSD硬盘(下):如何完成性能优化的KPI
- Windows电脑用了SSD的系统盘,就不要主动进行磁盘碎片整理,因为磁盘碎片整理会执行块的擦除,对应块的寿命就少了1次.
- 我们使用SSD硬盘,安装的系统及常用软件,一般都不会删除或修改,这些数据对应的SSD的块只是去读,不会擦除. 但是下载目录这样的位置,我们会经常删除文件,这样对应的SSD的块就被标红/已删除. 当SSD中空白的块过少,会进行'碎片整理',经常删除文件对应的块就会被反复擦除.
-
反复擦除的块次数到了就变成了坏块,导致安装操作系统和软件的地方还没坏,但是SSD可用磁盘空间却变小了.
- 如何提升SSD硬盘的寿命: 让各个块的磨损次数变得均衡 -> 磨损均衡
- 磨损均衡
- 实现磨损均衡的核心,是添加1个间接层,就是FTL/闪存转换层.
- 在闪存转换层中,存放了逻辑块地址(Logic Block Address)到物理块地址(Physical Block Address)的映射.
- 操作系统访问的硬盘地址,都是逻辑块地址,经过FTL转换找到对应的物理块进行访问.
- 操作系统本身不用考虑块的磨损程度,和操作机械硬盘一样读写数据.
- 操作系统所有对SSD硬盘的读写请求,都要经过FTL.FTL又有逻辑块和物理块的映射,所以FTL可以记录下来每个块被擦写的次数.当1个逻辑块A对应的物理块的擦写次数过多,就将A映射到1个擦写次数少的物理块上.
- 添加中间层,是设计大型系统的一个典型思路,各层之间是隔离的.操作系统完全不需要底层的硬件是什么,完全交给硬件控制电路中的FTL,来管理对实际物理硬件的写入.
- TRIM指令的支持
- 操作系统不关心实际底层的硬件是什么,在SSD硬盘的使用上会带来问题:操作系统的逻辑层和SSD的逻辑层里的块状态,是不匹配的.
- 操作系统里面删除1个文件,并没有真的在物理层面删除这个文件,只是在文件系统里,将对应的inode里面的元信息清理掉,代表该inode可以继续使用,写入新的数据.这时,实际物理层面的存储空间,在操作系统里面被标记成可以写入了.
- 日常的文件删除,都只是操作系统层面的逻辑删除,实际物理层面数据仍在.
- 不小心删除的文件通过数据恢复软件找回来,就是物理层面数据还在.
- 想要删除干净,就要用'文件粉碎'才行,将对应物理空间的数据清除.
- 操作系统层面的逻辑删除,对于HDD硬盘没有问题,因为HDD硬盘写入是覆盖的.但是SDD硬盘不行,SDD硬盘监测到物理块上的页还是绿色,就不能写入,只能找白色的页.
- 例如1个文件被操作系统删除,操作系统中对应inode里面已经没有文件的元信息.
- 但SDD逻辑层,之前文件占用的块/物理页,仍然是被占用的.
- 这时候如果需要进行垃圾回收,上述物理页也需要被搬运到其他块中.
- 只有当操作系统再次在刚刚的inode里面写入数据,SDD硬盘才知道上述页已经失效了,才会将其废弃掉.
- 在使用SDD硬盘情况下,操作系统对文件进行删除,SDD硬盘其实并不知道.导致为了磨损均衡,我们很多时候都在搬运很多已经删除了的数据,产生很多不必要的数据读写和擦除,消耗了SSD的性能和使用寿命.
- TRIM指令解决操作系统逻辑层和SSD逻辑层里的块状态不匹配问题.
- TRIM指令可以在操作系统删除文件时,让操作系统通知SSD硬盘,对应逻辑块已经被标记成删除状态/红色.
- 现在的操作系统和 SSD 的主控芯片,都支持 TRIM 命令.
- 写入放大
- 当SSD硬盘的存储空间被占用的越来越多,每一次写入新数据,可能都需要进行'碎片整理',转移一部分块中数据到预留空间,然后将对应块擦除,再执行新数据的写入.
- 这时候,从操作系统或应用层面,我们可能写入了4K或4M数据,但实际上我们可能搬运了8M,16M甚至更多数据.
- 实际的闪存写入的数据量 / 系统通过 FTL 写入的数据量 = 写入放大.
- 写入放大越大,意味着SSD性能越差,会远远低于SSD硬盘标注的数值.
- 如何解决写入放大
- 在SSD硬盘比较空闲时候,将已删除页的数据转移到其他块,执行块的擦除.
- 避免在实际数据写入时执行转移及擦除.
几种NoSql数据库性能对比,利用SSD硬件特性的AeroSpike性能极好:
https://www.infoq.com/news/2013/04/NoSQL-Benchmark/AeroSpike:如何最大化 SSD 的使用效率
AeroSpike是专门针对SDD硬盘特性设计的Key-Value数据库.做了如下优化点:
- AeroSpike操作SSD硬盘,跳过了操作系统,直接操作SSD硬盘中的块和页.
- 因为操作系统里面的文件系统,对KV数据库而言只是多了一个间接层,只会降低性能.
- AeroSpike写数据时候,会尽可能写入1个较大的数据块/128K,远大于一页的存储空间/4K.,
- 每次写入较大数据块,避免频繁写入很多小的数据块,可以避免硬盘频繁出现碎片.
- 每次写入较大的数据库,能更好利用顺序写的性能优势.
- AeroSpike读取数据可以读取小数据/512字节.
- 因为SSD随机读性能很好,也不存在擦除影响寿命问题.
- 而且很多时候读取的数据是KV中的V,要在网络上传输,如果一次读出比较大的数据,会导致网络带宽不够.
- AeroSpike是对响应时间要求很高的实时KV数据库.要尽量避免写放大效应,快速写入完成.
- 持续进行磁盘碎片整理.
- AeroSpike用了高水位算法,一旦一个物理块中的碎片数量超过50%,就对其执行数据搬运,擦除.确保SSD硬盘始终有足够写入空间.
- AeroSpike最佳实践建议只用到SSD硬盘标注容量的一半,人为的给SSD的预留空间增加到50%,最大限度的避免写放大效应.
- 持续进行磁盘碎片整理.
- AeroSpike为什么现在的受欢迎程度不如Redis
- 国内外其实已经普遍应用,只是相对于Redis开源较晚,知道的人相对少.
- 大部分并发量不够大的场景,使用内存缓存就足够了,暂时用不上
62.DMA:为什么Kafka这么快
- IO性能无论如何提升,比起CPU还是太慢.SSD硬盘的IOPS可以到2万,4万,但CPU主频有2G以上,意味着每秒20亿次操作.
- 如果对于IO的操作,都是由CPU发出指令,等待IO设备操作完成后返回,CPU会有大量时间等待IO操作.
- CPU等待IO操作大部分情况没有实际意义,对IO设备的操作,大部分是将内存中的数据,传输到IO设备,CPU是在无谓等待.
- DMA(Direct Memory Access)直接内存访问技术用于降低CPU等待时间.
- DMA技术本质上就是在主板上用一块独立的芯片,在进行内存和IO设备数据传输时,不再通过CPU进行数据传输,而是使用DMA控制器(DMA Controller,简称DMAC).
- DMAC的作用是解决CPU和IO性能巨大差距,避免CPU等待IO数据传输
- DMA并不能脱离CPU完成数据传输,需要CPU进行控制.
- DMA也是1个特殊的IO设备,和其他硬件一样,通过连接到总线进行实际的数据传输.
- 总线上的设备有2中类型:
- 主设备
- 从设备
- 想要发起数据传输,必须是1个主设备才可以.CPU就是主设备,其他设备是从设备.
- 从设备只能接受数据传输,不能发起数据传输.
- 通过CPU传输数据,要么是CPU从IO设备读取数据,要么是CPU向IO设备写入数据.
- 从设备可以向主设备发起请求,但发送的不是数据内容,而是控制信号.
- IO设备可以告诉CPU,我这里有数据要传输给你
- 但实际数据是CPU拉走的,而不是IO设备推送给CPU的.
- DMAC既是主设备,也是从设备.
- 对于CPU,DMAC是一个从设备
- 对于硬盘这样的IO设备,DMAC又变成了一个主设备
-
使用DMAC进行数据传输的实际过程
- 最开始计算机中没有DMAC,所有数据都由CPU进行搬运.随着数据传输的需求,先是主板上增加了独立的DMAC,后来各种硬件设备的数据传输需求都不同,各个硬件中都有自己的DMAC芯片了.
- Kafka:利用DMA实现性能提升
- 相关文章:Efficient data transfer through zero copy
- 这篇文章最后,你可以看到,无论传输数据量的大小,传输同样的数据,使用了零拷贝能够缩短 65% 的时间,大幅度提升了机器传输数据的吞吐量.
- Kafka最终调用了Java NIO库,使用FileChannel中的transferTo方法.
- 数据并没有读到应用内存中,而是直接通过Channel,写入到对应的网络设备里.
- 对于Socket的操作,也不是写入到Socket的Buffer里,而是根据描述符(Descriptor)写入到网卡的缓冲区.
- 这个过程只进行了2次数据传输,都是使用了DMAC,没有使用CPU传输数据.
- 第一次是通过DMA,将数据从硬盘直接读到操作系统内核的读缓冲区;
- 第二次是根据Socket描述符信息,通过DMA将数据从操作系统内核的读缓冲区,写入到网卡的缓冲区.
- 在内存层面去“复制(Copy)”数据,所以这个方法,也被称之为零拷贝(Zero-Copy).
如果我们的应用程序需要对数据做进一步的加工,那还能使用零拷贝吗
作者回复: yhh同学,
你好,那就不能了,我们需要把数据复制到内存里面来,在用户态用程序进行处理。
- Kafka和零拷贝对应资料
- Efficient data transfer through zero copy 可以下载IBM的代码看
- 零拷贝的原理及Java实现
- 什么是“零拷贝”技术
- 高频面试题:什么是零拷贝?在哪些地方使用了?
- Java中的零拷贝
- 掘金搜索 零拷贝
63.数据完整性(上):硬件坏了怎么办
- 普通内存的 单比特翻转
- 内存中的二进制数据0101**其中的一位或多位从0变成1,或从1变成0
- 单比特翻转是内存的硬件错误,靠软件是解决不了的
- 单比特翻转是一种随机现象,不能稳定复现.
- 内存的制造质量造成的漏电,或者外部的射线都有可能导致单比特翻转
- 内存数据出错,软件无法知道
- 奇偶校验 和 校验码位
- 通过奇偶校验可以发现单比特翻转
- 奇偶校验的思路:
- 将内存中的N位比特当做1组,比如8位就是1个字节.
- 用额外的1位去记录,这8个比特种有奇数个1还是偶数个1.
- 如果是奇数个1,额外一位就是1
- 如果有偶数个1,额外一位就是0
- 这额外的一位称为校验码位
- 如果这个字节里发生了单比特翻转,那么8位新得到的校验码 就和 实际校验码位的值不一致.我们就知道内存出错了.
- 奇偶校验/校验码位的缺陷
- 奇偶校验只能发现奇数次内存单比特翻转.偶数次发现不了.
- 奇偶校验只能发现错误,但无法纠正错误.
- 纠错码 和 纠删码
- 纠错码:不仅能够捕捉错误,还要能纠正错误
- 纠删码:纠错码的升级版
- 能纠正就纠正
- 数据错误无法纠正,则将数据删除
- 为什么个人PC不会遇到内存错误/单比特翻转?
从老师的描述看单比特翻转问题的概率不低啊,但是大部分PC机都没有用ECC,为什么PC机很少听说有出现这个问题带来的bug?
作者回复: 有铭同学,
你好,这有两种情况:
1. 第一种是PC实际的负载比服务器低很多,大部分时间你的PC是很空闲的,CPU占用率和内存使用率都不高,也没有什么东西在计算。而服务器常常是24小时高负载在运转的。服务器可能一天进行的计算量比你PC一年还多。数据中心里又有可能同时有1000台计算机,意味着服务器一天遇到的问题可能PC要一辈子才遇到一次。
2. 第二是很多时候发生了你没有意识到,比如程序忽然Crash了,机器蓝屏重启了,甚至有程序数据错了,你并会关心到哪个是单比特翻转引起的。
64.数据完整性(下):如何还原犯罪现场
- 纠错码
- 纠错码需要更多冗余信息,通过冗余信息,不仅可以知道哪一位的数据出错,还能直接将出错的那一位纠正.
- 海明码
- 最知名的纠错码就是海明码.
- ECC内存也还在使用海明码来纠错.
- 最基础的海明码叫做 7-4海明码 . 7指实际有效的数据是7位, 4指额外存储了4位数据用于纠错.
- 纠错码的纠错能力是优先的,对于7-4海明码,只能纠正某一位的错误.
- 可能是数据位中的1位
- 可能是校验位中的1位
- 单比特翻转,不仅可能出现在数据位,也可能出现在校验位.
- 假设数据位是K位,校验位是N位
- 校验位只有1个组合值是正确对应数据位的
- 校验位错误的组合数量是 2^N - 1
- 数据位和校验位的某一位,都可能出错,则发生一次单比特翻转的组合数量是:K + N
- 要保证校验位的长度足够发现所有的单比特翻转错误,要满足: 2^N -1 >= (K + N)
- 当K=7,N最小是4
-
可见下面对照表
-
海明码的纠错原理,以7-4海明码为例
3.1. 首先确认编码后,要传输的数据是多少位,7-4海明码,就是11位.(7+4=11)
3.2. 给11个数据从左到右进行编号,并将其二进制数据表示出来.(1-11)
3.3. 将11个数据中二进制的整数次幂找出来.
3.3.1. 11个数据中对应着1,2,4,8.
3.3.2. 二进制的整数次幂,就是我们的校验位.
3.3.3. 从二进制的角度看,校验位是4个比特位中只有1个比特是1的数值.
3.4. 剩下7个数,就是我们的数据位.
3.5. 对于校验码位,还是用奇偶校验码,但是每一个校验码位,都只是用部分数据位进行计算.
3.5.1. p1/第1个校验码位用二进制下,从右向左第1位比特位是1的多个数据位进行计算.
3.5.2. p2是用二进制下,从右向左第2位比特位是1的多个数据位进行计算.
3.5.3. p3是用从右向左第3位比特位是1的多个数据位进行计算.
3.5.4. p4是用第4位是1的进行计算.
3.6. 如上图,任何1个数据位出错,都会导致不同的校验码组合出现错误,二任何1个校验码位出错,只会导致自身出错.
- 即可进行对应修正.
海明距离
- 对于2个二进制数据,他们之间有差异的位数,称为海明距离.
- 1001和1000海明距离是1
- 1001和0101海明距离是2
- 进行1位纠错,实际就是将所有和我们实际要传输的二进制数的海明距离为1的数,都进行纠正.
- 而任何两个实际我们想要传输的数据,海明距离都至少要是 3:没看懂.
65.分布式计算:如果所有人的大脑都联网会怎样
- 分布式计算的3个核心问题:
- 垂直扩展和水平扩展的选择问题
- 如何保持高可用性
- 一致性问题
- 垂直扩展
- 升级一台服务器的硬件
- 水平扩展
- 增加服务器的数量
- 最终还是要靠水平扩展支撑巨大体量的互联网访问量.因为单纯升级一台服务器的硬件,很快就到极限了,一台服务器的性能不能无限增加.
- 我们在 Google Cloud 上现在能够买到的性能最好的服务器,是 96 个 CPU 核心、1.4TB 的内存。如果我们的访问量逐渐增大,一台 96 核心的服务器也支撑不了了,那么我们就没有办法再去做垂直扩展了。
- 水平扩展,就是分布式计算
- 分布式计算的核心工作,就是通过消息传递,而不是共享内存的方式,让多台不同的计算机协作起来共同完成任务.
- 水平扩展面临在软件层面的改造:
- 引入负载均衡组件进行流量分配.
- 拆分应用服务器和数据库服务器,进行垂直功能的切分.
- 不同的应用之间通过消息队列,进行异步任务的执行.
- 理解高可用性和单点故障
- 系统的可用性:
- 指的是我们的系统可以正常服务的时间占比.
- 故障转移:
- 采用水平扩展后,即使其中一台服务器坏了,负载均衡组件能通过健康检测发现坏掉的服务器没有响应,可以自动将对应的流量切换到其他服务器上,这个操作叫做故障转移.
- 单点故障
- 任何一台服务器出问题,整个系统就没法运行,就称为单点故障问题.
- 解决单点故障,最重要就是要移除单点,移除单点最典型的场景,就是水平扩展服务器,让多台服务器提供相同的功能,通过负载均衡将流量进行分配,其中一台服务器挂了,其他服务器仍然可以正常运行.
66.设计大型DMP系统(上):MongoDB并不是什么灵丹妙药
- DMP:数据管理平台
- DMP全称是Data Management Platform,数据管理平台.目前广泛应用在互联网的广告定向,个性化推荐领域.
- DMP会通过处理海量的互联网访问数据及机器学习算法,给用户标注上各种各样的标签.
- 做实际的广告投放和个性化推荐,就会利用这些标签进行实际的广告排序,推荐.
- 对于外部使用DMP的系统或用户,可以简单将DMP看成一个KV数据库.
- 用户标识:用户信息
- 对于DMP的要求: 低响应时间, 高可用性, 高并发, 海量数据, 成本低.
- 低响应时间
- 广告系统留给广告投放决策的时间大约10ms,所以对于方位KV数据库获取用户数据,要在1ms以内
- 高可用性
- DMP往往用于广告系统,可用性低意味着投放广告的时间会降低,会损失钱
- 高并发
- 以广告系统为例,如果每天响应100亿次广告请求,每秒并发请求就在100亿/86400 = 120K = 12万.
- 海量数据
- 如果我们的产品针对中国市场,那么我们需要有 10 亿个 Key,对应的假设每个用户有 500 个标签,标签有对应的分数。标签和分数都用一个 4 字节(Bytes)的整数来表示,那么一共我们需要 10 亿 x 500 x (4 + 4) Bytes = 4 TB 的数据
- 低成本
- 以广告系统为例,收入通常使用千次曝光统计,比如千次曝光收入是0.10美元,那么DMP的1000次请求的成本必须低于0.10美元,甚至低于0.01美元,才能尽可能赚到钱.
- DMP在外部看就是1个KV数据库,但是要生成这个KV数据库,要做非常多的事情.
3.1. 在web端和app端的数据采集模块,不断将用户数据发送到服务端.
3.2. 服务端要将数据放到数据管道.
3.3. 数据管道后端,需要连接数据仓库 及 实时数据处理模块.
3.4. 数据仓库存储实际数据,将所有数据结构化的存储起来.
- 可以将结构化的数据生成各种报表
- 可以将结构化的数据运行机器学习算法
3.5. 实时数据处理模块会读取数据管道中的数据进行各种实时计算,将计算结果写入到DMP的KV数据库中. - DMP系统各个部分软硬件选型
4.1. KV数据库要支持高并发.
- KV数据库的容量在100TB-1PB,使用内存存储成本太高
- 高并发的随机访问不适合HDD硬盘,使用SSD硬盘及对应的AeroSpike 这样的 KV 数据库,即保证性能,又相对便宜.
4.2. 数据管道自然是Kafka
- Kafka是数据管道的第一方案
- 为了追求吞吐率,采用了零拷贝和DMA机制的Kafka是最优选择.
- 数据管道都是顺序读写,不用支持随机读写,可以使用HDD硬盘.
4.3. 数据仓库
- 存储的数据量更大,使用HDD硬盘是必然选择.
- 在存储上我们需要定义清楚Schema,使得每个字段不需要额外存储原数据,能通过ProtoBuffer 这样的二进制序列化的方存储下来,或者直接使用明确了字段定义的数据库. - 《数据密集型应用系统设计》这本书
https://book.douban.com/subject/30329536/
67.设计大型DMP系统(下):SSD拯救了所有的DBA
- 对于DMP系统,传统的关系型数据库,或者MongoDB无法通过SQL优化,添加缓存这样的调优满足性能要求.
- 传统的关系型数据库不适用DMP的原因
- 传统的关系型数据库给行号,或者其他字段添加索引,索引可以加载到内存或放到硬盘上.当要查询某一行数据,通过索引直接找到其对应的硬盘地址,查询速度很快.
- 但索引带来了问题,写入一条数据,所有对应的索引也需要进行更新,会触发多个随机写入的更新.
- 而随机读写如果使用HDD硬盘,高并发情况下HDD硬盘无法满足,其IOPS就是100左右.
- 添加索引对任意字段查询的灵活性是DMP不需要的,因为我们都是根据用户主键随机查询,不需要使用其他字段查询.
- 数据管道需要不断追加写入和顺序读取,不涉及查询
- 数据仓库进行数据分析,也不需要根据字段查询筛选,通常是全量扫描数据分析汇总.
-
Cassandra适合作为分布式KV数据库的原因:DMP需要的访问场景,没有复杂的索引需求,但有高并发性的要求.
3.1.Cassandra 的写操作:- Cassandra解决随机写的方案是:不随机写,只顺序写
- 首先向硬盘顺序写入一条提交日志;
- 提交日志成功后,在内存的数据结构上更新数据;
- 一旦内存中的条目或数据量超过一定限制,Cassandra就会将内存中的数据结构dump到硬盘上.这个dump操作也是顺序写不是随机写.
- 随着dump到硬盘上的数据增多,Cassandra会在后台进行文件的对比合并.合并同样是顺序读取多个文件,在内存中完成合并,再dump出1个新文件,整个过程在硬盘层面仍然是顺序读写.
3.2. Cassandra 的读操作
- 当我们要从Cassandra中读数据,会先在内存中找数据,再到硬盘中读数据,然后将两部分数据合并为最终结果.
- 硬盘中的文件在内存中有对应的Cache,只有Cache中找不到才会真正请求硬盘中数据
- 如果必须访问硬盘,而硬盘中dump了很多不同时间节点的文件,我们要按照时间从新向旧按顺序找.
- 实际上Cassandra已经为每一个dump的文件生成了一个BloomFilter,所有的BloomFilter都放在内存中.想要查询的数据如果在BloomFilter中不存在,99%以上情况都不需要访问硬盘.
2, 只有当数据在内存的Cache和BloomFilter中都找不到,才会真正触发一次对硬盘的请求.
3.3. Cassandra 也在文件之前加了一层 BloomFilter,把本来因为 Dump 文件带来的需要多次读硬盘的问题,简化成多次内存读和一次硬盘读.
3.4. Cassandra 即使在HDD硬盘上也有很好的性能,因为所有的写入都是顺序写入或直接写入内存,写入可以做到高并发.
3.5. Cassandra 使用HDD读取数据无法做到高并发,因为DMP的K量非常大,内存缓存空间有限,导致命中率很低,大部分情况还是要落到HDD的随机读取.
3.5.1. 但HDD的随机读取性能太差,IOPS就是100左右
3.5.2. SSD硬盘的降价解决了这个问题,SSD硬盘随机读的性能是HDD的100倍
3.5.3. Cassandra 的写入机制完美匹配了我们在第 46 和 47 讲所说的 SSD 硬盘的优缺点。在数据写入层面,Cassandra 的数据写入都是 Commit Log 的顺序写入,也就是不断地在硬盘上往后追加内容,而不是去修改现有的文件内容。一旦内存里面的数据超过一定的阈值,Cassandra 又会完整地 Dump 一个新文件到文件系统上。这同样是一个追加写入。数据的对比和紧凑化(Compaction),同样是读取现有的多个文件,然后写一个新的文件出来。写入操作只追加不修改的特性,正好天然地符合 SSD 硬盘只能按块进行擦除写入的操作。在这样的写入模式下,Cassandra 用到的 SSD 硬盘,不需要频繁地进行后台的 Compaction,能够最大化 SSD 硬盘的使用寿命。这也是为什么,Cassandra 在 SSD 硬盘普及之后,能够获得进一步快速发展.
68.理解Disruptor(上):带你体会CPU高速缓存的风驰电掣
disruptor
disruptor Introduction
- Disruptor通过利用CPU和高速缓存的硬件特性,实现极限的性能.
- Padding Cache Line/缓存行填充,尽可能的使用高速缓存.
- Disruptor中的代码
abstract class RingBufferPad
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
abstract class RingBufferFields extends RingBufferPad
{
......
private final long indexMask;
private final Object[] entries;
protected final int bufferSize;
protected final Sequencer sequencer;
......
}
public final class RingBuffer extends RingBufferFields implements Cursored, EventSequencer, EventSink
{
......
protected long p1, p2, p3, p4, p5, p6, p7;
......
}
3.1. p1到p7没有实际意义,只是帮助我们进行缓存行填充,使得我们能尽可能用上CPU高速缓存.
3.2. CPU的L1 Cache和L2 Cache,访问延时是内存的1/15至1/100.为了极限性能,要尽可能多的从CPU Cache中拿数据,而不是从内存里.
3.3. CPU Cache装载内存中的数据,是按照缓存行为单位.
- 64位的CPU,缓存行通常是64个字节/Byte.
- 一次性从内存中加载连续位置的64字节.
3.4. 以long[]为例.
- 1个long数据是8个字节
- 会一次性加载8个long类型数据,即一次性加载数组中连续的8个long元素.
- 这样的加载方式使得遍历数组元素的性能很高,因为后面连续7次的数据访问都会命中CPU Cache,不需要从内存中读取数据.
3.5. 不使用数组时候
- 当不使用数组,而是使用单独变量,就会出现问题.
- CPU缓存行不仅会加载目标数据,目标数据前后定义的变量,也会被一起加载到1个缓存行.
- 如果涉及到多线程,目标数据前后的那些变量会被其他线程更新,读取.为了保证缓存一致性,整个缓存行里包括目标数据需要重新写回到内存或者重新从内存加载.
- 导致不使用数组时读取目标数据大大变慢了.
3.6. Disruptor的代码技巧解决了目标数据的读取性能问题
- RingBufferFields里面的几个long类型变量,是我们的'目标数据'
- RingBufferFields继承于RingBufferPad,相当于:
p1,p2,p3,p4,p5,p6,p7, + indexMask等目标数据
- RingBuffer继承于RingBufferFields,相当于:
p1,p2,p3,p4,p5,p6,p7, + indexMask等目标数据 + p1, p2, p3, p4, p5, p6, p7
- RingBuffer中的这14p都是final的,我们既不去读,也不去写.目的就是为了将真正的目标数据夹在中间.这样缓存行加载时候,目标数据就和final的p一起加载到同一个缓存行.
- 只要我们对目标数据进行的是频繁的读取不是修改,就不会被换出Cache,每次读取都能命中高速缓存,不必从内存中再次加载.
- Disruptor
4.1. Disruptor整个框架,就是一个高速的 生产者-消费者 模型下的队列.
4.2. 生产者不停的往队列添加需要处理的任务,消费者不停的从队列里面处理这些任务.
4.3. 实现一个队列最合适的数据结构是链表.Java也提供了LinkedBlockingQueue这样的类可以直接用于生产者-消费者模式.
4.4. Disruptor却没有使用链表,而是使用了底层为一个固定长度的数组的RingBuffer.
- 因为相比于链表,数组中的元素在内存中会存在空间局部性.
- 数组的多个元素会一并加载到Cache line中,所以访问遍历的速度更快.
- 链表的各个节点数据,在内存中都是分散的,不会出现在相邻的内存空间,也就不会被一起加载到1个Cache line,无法享受CPU Cache命中的高性能.
- 数组元素除了能一起被加载到1个Cache line中,对CPU执行过程中的分支预测也很有利.
- 更准确的分支预测,可以使得我们更好地利用好 CPU 的流水线,让代码跑得更快.
69.理解Disruptor(下):不需要换挡和踩刹车的CPU,有多快
- 缓慢的锁
1.1. Disruptor作为一个高性能的生产者-消费者队列系统,核心之一就是通过RingBuffer实现1个无锁队列.
1.2. Java基础库中LinkedBlockingQueue队列,比Disruptor实现的RingBuffer慢许多.
- 首先是链表各个节点在内存中不连续,对CPU Cache并不友好.
- 其次是链表对于锁的依赖会严重降低性能.
1.3. 生产者-消费者 队列对锁的依赖
- 队列可能存在多个生产者,多个消费者,每个生产者都要向队列的尾部添加任务,每个消费者都要向队列的头部取出任务.
- 多个生产者之间/多个线程之间间存在竞争,多个消费者之间也存在竞争.
- 即使只有1个生产者1个消费者,一般消费者处理速度要大于生产者,不然队列中的任务会大量积压会占用大量内存,导致这个队列很多时候是空的,进而导致队列的头部和尾部是同一个节点,1个生产者和1个消费者也产生了竞争.
1.4. 在LinkedBlockingQueue上,这个锁机制是通过ReentrantLock实现的,锁的争夺,会将没有拿到锁的线程挂起等待,也就需要经历一次上下文切换.
- 上下文切换导致当前线程对应的已经加载到CPU Cache中的指令和数据重新回到主内存
- 下次切换回来需要重新从主内存加载,会严重拖慢性能.
1.5. 加锁会严重降低性能,代码验证
package jisuanjizuchengyuanli;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockBenchmark {
public static void main(String[] args) {
runIncrement();
runIncrementWithLock();
}
public static void runIncrement() {
long counter = 0;
long max = 500000000L;
long start = System.currentTimeMillis();
while (counter < max) {
counter++;
}
long end = System.currentTimeMillis();
System.out.println("Time spent is " + (end - start) + "ms without lock");
}
public static void runIncrementWithLock() {
Lock lock = new ReentrantLock();
long counter = 0;
long max = 500000000L;
long start = System.currentTimeMillis();
while (counter < max) {
if (lock.tryLock()) {
counter++;
lock.unlock();
}
}
long end = System.currentTimeMillis();
System.out.println("Time spent is " + (end - start) + "ms with lock");
}
}
Time spent is 196ms without lock
Time spent is 9664ms with lock
可见加锁后,耗时增加了40-50倍!
- Disruptor的无锁方案-->使用CAS代替加锁
2.1. 加锁很慢,Disruptor的解决方案就是无锁.无锁指的是没有操作系统层面的锁.
2.2. Disruptor利用了CPU硬件支持的指令,CAS/Compare And Swap/比较和交换.
2.3. Disruptor的RingBuffer创建了一个Sequence对象,用来指向当前RingBuffer的头和尾.头和尾的标识,不是通过指针实现,而是通过一个序号.
2.4. RingBuffer中进行生产者和消费者的协调,采用的是对比序号的方式.当生产者想要向队列加入新的数据,会将当前生产者的Sequence序号,加上需要加入的新数据的数量,和消费者当下位置对比,看队列中是否有足够空间,而不会覆盖掉消费者未消费的数据.
2.5. Sequence代码中,就是通过compareAndSet这个方法,并最终调用了UNSAFE.compareAndSwapLong,也就是直接使用了CAS指令.
- 看最新的Disruptor源码,已经用VarHandle替代了UNSAFE.
public class Sequence extends RhsPadding{
***
private static final VarHandle VALUE_FIELD;
/**
* Perform a compare and set operation on the sequence.
*
* @param expectedValue The expected current value.
* @param newValue The value to update to.
* @return true if the operation succeeds, false otherwise.
*/
public boolean compareAndSet(final long expectedValue, final long newValue)
{
return (boolean) VALUE_FIELD.compareAndSet(this, expectedValue, newValue);
}
***
}
2.6. CAS操作是原子性的,意味着我们不需要加锁,直接调用即可.
2.7. 使用CAS代替加锁,虽然CAS属于复杂的的机器指令,但当下的线程不需要停下来等待,不涉及上下文切换,相比于加锁性能还是好很多.
2.8. CAS的性能.使用AtomicLong验证.AtomicLong底层也是使用了Unsafe.
public class AtomicLong extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
}
public static void runIncrementAtomic() {
AtomicLong counter = new AtomicLong(0);
long max = 500000000L;
long start = System.currentTimeMillis();
while (counter.incrementAndGet() < max) {
}
long end = System.currentTimeMillis();
System.out.println("Time spent is " + (end - start) + "ms with cas");
}
public static void main(String[] args) {
runIncrement();
runIncrementWithLock();
runIncrementAtomic();
}
运行结果:
Time spent is 203ms without lock
Time spent is 9488ms with lock
Time spent is 3302ms with cas
CAS所花费的时间,虽然要比没有任何锁的操作慢上一个数量级,但是比起使用 ReentrantLock 这样的操作系统锁的机制,还是减少了一半以上的时间