突破硬件瓶颈(三):旧时代的遗珠——并行流水线架构

  • 20年前,CPU的主流设计是流水线架构,增长流水线层级就可以提升性能。然而AMD的K8架构证明intel的发展方向是偏激的,它用更低的功耗和频率,战胜了使用31级流水线的Pentium 4

  • Intel被迫解散了NetBurst小组,承认了其失败,转而发展Core 2系列,借鉴了AMD将IMC加入CPU DIE的设计,大大加快内存的处理速度,解放了CPU性能。紧接着又转向了多核心处理器的研发,从单条流水线转向了并行流水线,打的AMD毫无还手之力。

  • 十多年过去,截止目前的主流CPU设计,本质是仍然是并行流水线架构,内存和缓存的改进也不再被轻视

  • 当软件对于性能的需求发展到极致的时候,架构设计方向忽然变得更加贴近于底层硬件。

FASS架构设计之初

  1. FASS的定位是下一代全闪存储产品,其核心理念是性能驱动数据。

  2. 如何开发最快的分布式全闪存储?最简单的出发点就是最大化的利用硬件,即为利用CPU多核心,并提高使用率。

  3. 利用多CPU核心就代表必须多控制器,如果一个执行步骤需要多个CPU核心完成,必然造成频繁的中断。

  4. 而高效的多控制器必须流水线化,同控制器也必须并行处理。

  5. 因此,并行流水线设计,成为了FASS的基本理念。

时代的遗珠:流水线架构

理解流水线架构非常有助于理解FASS的软件设计架构,流水线的设计不仅存在于CPU,实际上它广泛的用于各种场景。
例如家庭包饺子,由妈妈负责擀饺子皮,爸爸每拿到一个饺子皮就直接包成饺子,而孩子待饺子装满一盘后,端走下锅煮
我们假设一共需要三盘饺子,相对于三个人都一起擀饺子皮,而后一起包饺子,再一起下锅等待煮熟,区别在于:

  • 减少需要的存储空间(饺子皮、饺子等堆积需要的额外空间)

  • 减少不必要的等待时间(一锅煮的饺子是有限的,本可以利用包饺子的时间分批煮饺子)

  • 减少每个人需要会的工作种类(妈妈只需要精通擀面皮,孩子只需要会下锅,就可以完成工作流)

  • 降低了饺子的最低出锅的时间(部分饺子可以更早的出锅)

现代CPU拥有十数级或者更多的流水线,一条指令的执行被分为很多步骤,为了简化场景,我们以简单的五级流水线为例

这里,CPU执行一条指令需要进行如下步骤:

  1. 获取指令(Fetch)

  2. 译码指令(Decode)

  3. 进入执行端口执行指令(Execute)

  4. 访存获取操作数(Memory)

  5. 写回结果(Write)

我们假设CPU内部有五个物理单元,可以分别完成上述的五个功能,他们互相协作便可以完成所有的功能。
因为操作是有顺序的,不可逆的,所以就像流水一样执行。
假设不使用流水线设计,而是简单的顺序执行,他们的执行过程是这样的:

第一个指令较为简单,IF->ID->EX->WB的循序执行,横坐标为时间流逝(时钟) 

 

可以看到,当第一条指令(A指令)执行到Decode的时候,Fetch单元就处于闲置状态了。每个逻辑单元都有4/5的时间处于闲置。

而在流水线设计下,我们让第二条指令立即进入Fetch,以此类推,理想状况是每个执行单元都处于不停歇的工作状态。

可以简单分析一下CPU流水线架构的优势和劣势

流水线架构的优势:

  • 将单元分离,去耦合,利于开发阶段分工,也增加了可维护性

  • 提高了处理单元利用率(理论上从1/N提升到100%)

流水线架构的劣势:

  • 性能浪费:CPU执行的指令是有不确定性的,并不能完美的预测即将执行的指令是什么,一旦某条指令被发现是不需要执行的,而它已经运行在流水线之中,就必须将流水线中尚未执行的所有指令全部丢弃

  • 成本提高:不同的执行操作需要的时间不一定是相同的,为了符合流水线架构同时提高主频,可能需要把本不必要拆解的执行单元拆解为更多的单元,使得流水线级数提高

总结

  • 文章的前作中提到的上下文切换、分支预测失败,都会造成性能浪费,就好比在煮饺子的时候发现饺子皮太薄,会露馅,为了保证饺子完整,可能需要将已经包好的饺子和饺子皮全部舍弃。

  • Intel当年正是因为盲目提高流水线级数和主频,才一度在架构上落后于AMD。

  • 因此,如果采用流水线架构,必须谨记intel的教训,层级设计一定要合理,不能盲目追求数量,而且要注重内存和缓存,提高命中率,避免中断

并行流水线

现代CPU经过多年的发展,多核和多线程技术已经成熟,甚至开始向多DIE(如MCM)方向发展,从指令执行的角度看,现代CPU是并行流水线设计的。

以截止目前IPC最高的CPU架构:AMD ZEN3架构为例,CPU DIE中包含8个核心(Z3),每个核心内部封装了L1 Cache(图中省略),外部封装L2,8个核心公用32MB的L3缓存。
任务经由系统调度,分给多多核心处理,每个核心都拥完整的十数级流水线。

FASS的并行流水线

FASS的控制器

从CPU的设计得到启发,FASS的软件架构模型也是并行流水线。
总体上说,FASS的作用就是整合复数服务器的存储和计算资源,并对外提供存储服务。
具体来说,FASS的执行单元包括:

  • TgtCtl:目标端控制器,转发客户端的IO请求(iSCSI或NVMe over Fabric 协议)到本节点内同一NUMA节点上的frctl并进行下一步IO处理

  • FrCtl:前端控制器,负责IO路径管理与选择,从RangCtl或许请求IO的元数据和版本号,并将IO转发到最新版本的相应bactl进行读写落盘

  • RangCtl:子卷控制器,向mdctl查询并缓存子卷元数据信息,并负责子卷的数据分布管理,协调卷IO读写、修复、平衡等任务

  • MdCtl:元数据控制器,运行KV数据库,提供元数据查询与持久化写入管理,其中一个mdctl充当master角色。

  • BaCtl:后端控制器,负载管理本地磁盘的数据读写,提供数据的写入和读取服务。

  • PoliCtl: 策略控制器,负责非核心业务,如均衡、修复。

可以想象,要达到最大和可以线性提升的性能,核心控制器必然要有多个,但是每个IO的执行过程,都几乎需要所有的控制器参与,这更像一个流水线。

并行流水线,由此而生。
FASS的核心是六种控制器,高速IO流上的控制器有四个,包括TgtCtl、FrCtl、RangCtl、BaCtl,他们采用流水线设计。
每个控制器可能在每台服务器上拥有多个,在同种的控制器之前是并行处理任务的,就像多个核心、多个家庭一样。而不同控制器之间是流程上下级的关系,就像擀饺子皮的妈妈和捏饺子的爸爸一样。

FASS的存储卷可通过任一目标端控制器(TGctl)导出,当应用向存储卷发起读写I/O访问请求时,导出该存储卷的iSCSI Target或Nvme over Fabrics Target服务(即TGctl))将相关请求分发到多个前端控制器(FRctl)上执行。随后每个前端控制器会访问对应的子卷控制器(RangCtl),获取子卷控制器内存中的子卷元数据信息。前端控制器找到对应的后端控制器(BActl),下发读写I/O请求到各个BActl(分布在多个节点),最终由BActl完成数据落盘读写并返回完成信息。

FASS的突破

如前作所讲,当前CPU体系结构有如下瓶颈:

  1. 过多核心、线程导致内存的调用效率低下,大量使用同步锁导致延迟迅速提升

  2. 多路CPU导致内存的延迟进一步增高

  3. 核心的缓存利用率不够高,命中率低

  4. 如果出现上下文切换,过多的CPU核心会导致更严重的延迟

为了解决这些问题,FASS在架构设计之初,就做好了准备。

Polling(轮询)模式

这一点FASS和SPDK采用了同样的理念,为了最大程度的利用CPU,减少中断和上下文切换带来的开销,每个控制器都在对应的CPU核心上Polling,完全抢占了该核心,其他进程无法使用。
专任务专核,使得该核心在逻辑上成为了FASS系统的一个控制器。

哈希环分配

同级控制器的任务分配非常重要,他是并行的关键,然而这部分分配有三个难点

  1. 分配不均会导致性能降低

  2. 分配重合会导致一致性错误或者主次问题

  3. 避免了使用锁来处理数据同步,而让并发沦为形式

FASS采用哈希处理,例如TgtCtl通过hash算法确定将任务分发给哪一个FrCtl,确定之后分配规则会缓存,实现了无锁的任务分配,通过优化哈希环,可以实现在实际场景下的均衡问题。
即便节点或者控制器有变动,也可以检测到并重置哈希环,将新的分配规则写入缓存。

无锁队列ringbuffer

控制器之间需要通信,而系统的内存调度效率低下,FASS选择自己实现Ringbuffer
环形缓冲区(Ringbuffer )通常有一个读指针和一个写指针。读指针指向环形缓冲区中可读的数据,写指针指向环形缓冲区中可写的缓冲区。通过移动读指针和写指针就可以实现缓冲区的数据读取和写入。

如图,红色标记的部分数据,就是已经经过写和读,已经“废弃”的数据,即将再次被写入,而未标记的数据,已经经过写入,还未读取。
Ringbuffer具有如下优点:

  1. 高速:无锁让它有极高的速度,同时Ringbuffer的本质类似数组,容易预测,几乎每次访问都可以在Cache获取,避免了大量回到内存中访问的延迟。

  2. 简单:Ringbuffer长度一定,可以直接在内存中分配定量的空间,不需要考虑垃圾回收

  3. 可靠:因为设计原因,在单消费者和单生产者模型下,不需要考虑一致性问题

协程调度

尽管核心被控制器主线程Polling调度,FASS设计中实际执行任务的是协程。
协程不是被操作系统内核所管理的,而是完全由程序所控制,在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。
FASS自己实现了协程库,所有需要调用CPU的操作都赋予协程执行,每个控制器内部的协程任务顺序执行,是单队列串行执行的。

协程不是进程也不是线程,而是一个特殊的函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行,挂起和继续运行是协程最大的魅力所在。
协程的栈内存空间可以自我保留,随时恢复,不需要回调函数就可以返回主线程,而主线程可以继续执行下一个协程,避免计算资源浪费。
待上一个协程阻塞完成,可以瞬间恢复,中间状态和数据不会丢失,不仅极大的方便了开发,更重要的是避免了非本节点通信时所需的复杂状态机。例如现在某个协程需要从其他节点读取数据,发出了read请求,这个操作是异步的,势必产生阻塞,这是高性能FASS所不能允许的。 


此时,Polling线程会继续执行下一个协程的任务,而当read请求被返回时,主线程会在执行完当前协程后,恢复上一个协程的全部上下文信息,从而继续执行该协程,这种方便是线程级别无法相比的,他必须面对大量的中间状态和信息保存。

FASS的分布式并行流水线

FASS是分布式全闪存储,和CPU的并行流水线显著的区别在于分布式
传统CPU仅仅是在本机或者NUMA内部并行,而FASS需要在不同的节点上并行,本地core的数据交换使用ringbuffer,而不同节点的数据交换则使用rdma实现。前作提到过,rdma同样减少了上下文切换和内存复制,直接对用户态内存进行操作。
总览FASS的设计,对于任何一个控制器来说,它通过pooling被绑定到一个core上,而不同的控制器可能位于不同的节点。同类的控制器之间并行处理,数据交换和分配通过无锁的。
不同类的控制器按照业务逻辑的顺序执行,当前控制器执行完一个任务,立即执行下一个任务,他们的数据交换通过Ringbuffer,同样是无锁的。
每一个控制器之中使用协程来处理任务,协程的执行是串行的。

总结

技术的发展就像历史的轮回,不去接受新事物的人会被淘汰,但是忘记回味老事物的人,永远只能疲于接受新事物。
iPhone手机几乎所有的功能都是曾经出现的,不论是安装第三方软件,或者是浏览网页,亦或是触屏,在诺基亚和黑莓时代都已经做到了,和但是他引领了智能手机时代。
微信的所有功能都已经在QQ、支付宝、陌陌上有过,他并没有真的发明新东西,却统治了移动社交。
优秀的产品,只是在适当的时侯,使用适当的技术,加以改进和优化,而达到最适合的效果。
在操作系统调度严重低效、CPU核心越来越多,但业务IO需求越来越大的今天,并行流水线设计正是全闪SDS未来的答案。

参考文章

(TaoCloud团队原创)

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页