微服务架构笔记(架构解密从分布式到微服务)
微服务架构笔记(架构解密从分布式到微服务)多核CPU与内存共享问题下面给出了Intel Sandy Bridge CPU的架构图。可以看出,CPU如果要访问内存中的数据,则要经过L1、L2与L3这三道关卡后才能抵达目的地,在这个过程中CPU“亲自出马”,3个级别的Cache层层转发内存指令,最终抵达内存。第2个问题:CPU的运算速度与内存条的访问速度之间的差距究竟有多大?通常来说 CPU的运算速度与内存访问速度之间的差距不过是100倍。既然CPU的速度与内存的速度还是存在高达两个数量级的巨大鸿沟,所以它们注定不能“幸福地在一起”,于是CPU的亲密“伴侣”Cache闪亮登场。与来自DRAM家族的内存(Memory)出身不同,Cache来自SRAM 家族。DRAM与SRAM最简单的区别是后者特别快,容量特别小,电路结构非常复杂,造价特别高。造成Cache与内存之间巨大性能差距的主要原因是工作原理和结构不同,如下所述。Cache是被集
深入浅析内存除了CPU,内存大概是最重要的计算资源了。基本成为分布式系统标配的缓存中间件、高性能的数据处理系统及当前流行的大数据平台 都离不开对计算机内存的深入理解与巧妙使用。在本章中我们将探索这个让人感到既熟悉又复杂的领域。
你所不知道的内存知识复杂的CPU与单纯的内存
首先,我们澄清几个容易让人混淆的CPU术语。
- Socket或者 Processor:指一个物理CPU芯片,盒装的或者散装的,上面有很多针脚,直接安装在主板上。
- Core:指在Socket里封装的一个CPU核心,每个Core 都是完全独立的计算单元,我们平时说的4核心CPU,就是指在一个Socket (Processor)里封装了4个Core。
- HT超线程:目前Intel 与AMD 的Processor大多支持在一个Core里并行执行两个线程,此时在操作系统看来就相当于两个逻辑CPU(Logical Processor)。在大多数情况下,我们在程序里提到CPU这个概念时,就是指一个Logical Processor。
然后,我们先从第1个非常简单的问题开始:CPU可以直接操作内存吗?可能99%的程序员会不假思索地回答:“肯定的,不然程序怎么跑。”如果理性地分析一下,你会发现这个回答有问题:CPU与内存条是独立的两个硬件,而且CPU上没有插槽和连线可以让内存条挂上去,也就是说,CPU并不能直接访问内存条,而是要通过主板上的其他硬件(接口)间接访问内存条。
第2个问题:CPU的运算速度与内存条的访问速度之间的差距究竟有多大?通常来说 CPU的运算速度与内存访问速度之间的差距不过是100倍。既然CPU的速度与内存的速度还是存在高达两个数量级的巨大鸿沟,所以它们注定不能“幸福地在一起”,于是CPU的亲密“伴侣”Cache闪亮登场。与来自DRAM家族的内存(Memory)出身不同,Cache来自SRAM 家族。DRAM与SRAM最简单的区别是后者特别快,容量特别小,电路结构非常复杂,造价特别高。
造成Cache与内存之间巨大性能差距的主要原因是工作原理和结构不同,如下所述。
- DRAM 存储一位数据只需要一个电容加一个晶体管,SRAM则需要6个晶体管。由于DRAM 的数据其实是被保存在电容里的,所以每次读写过程中的充放电环节也导致了DRAM 读写数据有一个延时的问题,这个延时通常为十几到几十ns。
- 内存可以被看作一个二维数组,每个存储单元都有其行地址和列地址。由于SRAM的容量很小,所以存储单元的地址(行与列)比较短,可以被一次性传输到SRAM中;DRAM则需要分别传送行与列的地址。
- SRAM的频率基本与CPU的频率保持一致;而DRAM的频率直到 DDR4 以后才开始接近CPU的频率。
Cache是被集成到CPU内部的一个存储单元,一级Cache(Ll Cache)通常只有32~64KB的容量,这个容量远远不能满足CPU大量、高速存取的需求。此外,由于存储性能的大幅提升往往伴随着价格的同步飙升,所以出于对整体成本的控制,在现实中往往采用金字塔形的多级Cache体系来实现最佳缓存效果,于是出现了二级Cache(L2 Cache)及三级Cache(L3 Cache) 每一级Cache都牺牲了部分性能指标来换取更大的容量,目的是缓存更多的热点数据。以Intel家族Intel Sandy Bridge架构的CPU为例,其Ll Cache容量为64KB,访问速度为1ns 左右;L2Cache容量扩大4倍,达到256KB,访问速度则降低到3ns 左右;L3 Cache的容量则扩大512倍,达到32MB,访问速度也下降到12ns左右,即便如此,也比访问主存的105ns ( 40ns 65ns)快一个数量级。此外 L3 Cache是被一个Socket上的所有CPU Core共享的 其实最早的L3 Cache被应用在AMD 发布的K6-I 处理器上,当时的L3 Cache受限于制造工艺,并没有被集成到CPU内部,而是被集成在主板上。
下面给出了Intel Sandy Bridge CPU的架构图。可以看出,CPU如果要访问内存中的数据,则要经过L1、L2与L3这三道关卡后才能抵达目的地,在这个过程中CPU“亲自出马”,3个级别的Cache层层转发内存指令,最终抵达内存。
多核CPU与内存共享问题
现在恐怕很难再找到单核的CPU了,问题来了:在多核CPU的情况下,如何共享内存?如果擅长多线程高级编程,那么你肯定会毫不犹豫地给出以下伪代码解决方案:
synchronized (memory)
{
writeAddress (....)
}
如果真这么简单,那么这个世界上就不会只剩下两家主流CPU制造商了。
多核心CPU共享内存的问题也被称为Cache一致性问题,简单地说,就是多个CPU核心所看到的Cache数据应该是一致的,在某个数据被某个CPU写入自己的Cache (Ll Cache)以后,其他CPU都应该能看到相同的Cache数据;如果在自己的Cache中有旧数据,则抛弃旧数据。考虑到每个CPU都有自己内部独占的Cache,所以这个问题与分布式Cache保持同步的问题是同一类问题。来自Intel的 MESI协议是目前业界公认的Cache一致性问题的最佳方案,大多数SMP架构都采用了这一方案,虽然该协议是一个CPU内部的协议,但由于它对我们理解内存模型及解决分布式系统中的数据一致性问题有重要的参考价值,所以这里对它进行简单介绍。
首先说说Cache Line,如果有印象的话,你会发现IO操作从来不以字节为单位,而是以“块”为单位。这里有两个原因:首先,因为IO操作比较慢,所以读一个字节与一次读连续N个字节所花费的时间基本相同;其次,数据访问往往具有空间连续性的特征,即我们通常会访问空间上连续的一些数据。举个例子,在访问数组时通常会循环遍历,比如查找某个值或者进行比较等,如果把数组中连续的几个字节都读到内存中,那么CPU的处理速度会提升几倍。对于CPU来说,由于Memory 也是慢速的外部组件,所以针对Memory的读写也采用类似于IO块的方式就不足为奇了。实际上,CPU Cache里的最小存储单元就是Cache Line,Intel CPU的一个Cache Line存储64个字节,每一级Cache都被划分为很多组Cache Line,典型的情况是4条Cache Line为一组,当Cache 从 Memory中加载数据时,一次加载一条Cache Line的数据。下图给出了Cache的结构。
在每个Cache Line的头部都有两个Bit来表示自身的状态,总共有4种状态。
- M(Modified):修改状态,在其他CPU上没有数据的副本,并且在本CPU上被修改过,与存储器中的数据不一致,最终必然会引发系统总线的写指令,将Cache Line 中的数据写回Memory中。
- E (Exclusive):独占状态,表示当前Cache Line中的数据与Memory中的数据一致,此外,在其他CPU上没有数据的副本。
- s (Shared):共享状态,表示Cache Line中的数据与Memory中的数据一致,而且当前CPU至少在其他某个CPU中有副本。
- I (Invalid):无效状态,在当前Cache Line中没有有效数据或者该Cache Line数据已经失效,不能再用;当Cache要加载新数据时 优先选择此状态的Cache Line,此外,CacheLine的初始状态也是Ⅰ状态。
MESI 协议是用Cache Line的上述4种状态命名的 对Cache 的读写操作引发了Cache Line的状态变化,因而可以将其理解为一种状态机模型。但MESI的复杂和独特之处在于状态有两种视角:一种是当前读写操作(Local Read/Write)所在CPU看到的自身的Cache Line状态及其他 CPU上对应的Cache Line状态;另一种是一个 CPU上的Cache Line状态的变迁会导致其他CPU上对应的Cache Line 状态变迁。如下所示为MESI 协议的状态图。
结合这个状态图,我们深入分析MESI 协议的一些实现细节。
(1)某个 CPU (CPU A)发起本地读请求(Local Read),比如读取某个内存地址的变量,如果此时在所有CPU的Cache中都没加载此内存地址 即此内存地址对应的Cache Line为无效状态(Invalid),则 CPUA中的Cache会发起一个到Memory的内存Load 指令,在相应的CacheLine中完成内存加载后,此 Cache Line 的状态会被标记为Exclusive。接下来,如果其他CPU(CPU B)在总线上也发起对同一个内存地址的读请求,则这个读请求会被CPU A嗅探到(SNOOP),然后CPU A在内存总线上复制一份Cache Line作为应答,并将自身的Cache Line状态改为Shared,同时CPUB收到来自总线的应答并将其保存到自己的Cache里,也修改对应的Cache Line 状态为Shared。
(2)某个CPU(CPUA)发起本地写请求(Local Write),比如对某个内存地址的变量赋值,如果此时在所有CPU的Cache中都没加载此内存地址 即此内存地址对应的Cache Line为无效状态(Invalid),则在 CPU A中的 Cache Line保存了最新的内存变量值后,其状态被修改为Modified。随后,如果 CPUB发起对同一个变量的读操作(Remote Read),则CPUA在总线上嗅探到这个读请求以后,先将在Cache Line里修改过的数据回写(Write Back)到Memory中,然后在内存总线上复制一份Cache Line作为应答 最后将自身的Cache Line状态修改为Shared 由此产生的结果是CPUA与CPUB里对应的Cache Line状态都为Shared。
(3)以上面的第2条内容为基础,CPUA发起本地写请求并导致自身的Cache Line状态变为Modified,如果此时CPUB发起同一个内存地址的写请求(Remote Write),则我们看到在状态图里此时CPUA的Cache Line状态为Invalid,其原因如下。
CPUB此时发出的是一个特殊的请求——读并且打算修改数据,CPUA在从总线上嗅探到这个请求后,会先阻止此请求并取得总线的控制权(Takes Control of Bus),随后将在Cache Line里修改过的数据回写到Memory中 再将此Cache Line 的状态修改为Invalid(这是因为其他CPU要修改数据,所以没必要将其改为Shared)。与此同时,CPU B发现之前的请求并没有得到响应,于是重新发起一次请求,此时由于在所有CPU的Cache里都没有内存副本,所以CPU B的Cache 从 Memory中加载最新的数据到Cache Line中,随后修改数据,然后改变Cache Line的状态为Modified。
(4)如果内存中的某个变量被多个CPU加载到各自的Cache中,从而使变量对应的CacheLine状态为Shared,若此时某个CPU打算对此变量进行写操作,则会导致所有拥有此变量缓存的CPU的Cache Line状态都变为Invalid,这是引发性能下降的一种典型的Cache Miss问题。
在理解了MESI 协议以后,我们明白了一个重要的事实,即存在多个处理器时,对共享变量的修改操作会涉及多个CPU之间的协调问题及Cache失效问题,这就引发了著名的“Cache伪共享”问题。
下面说说缓存命中的问题。如果要访问的数据不在CPU的运算单元里,则需要从缓存中加载,如果在缓存中恰好有此数据而且数据有效,就命中一次(Cache Hit),反之发生一次CacheMiss 此时需要从下一级缓存或主存中再次尝试加载。根据之前的分析 如果发生了Cache Miss 则数据的访问性能瞬间下降很多!在我们需要大量加载运算的情况下,数据结构、访问方式及程序算法方面是否符合“缓存友好”的设计,就成为“量变引起质变”的关键性因素了。这也是为什么最近国外很多大数据领域的专家都热衷于研究设计和采用新一代的数据结构和算法,而其核心之一就是“缓存友好”。
著名的Cache伪共享问题
Cache伪共享问题是编程中真实存在的一个问题,考虑如下所示的Java Class结构:
class MyObject
{
private long a;
private long b;
private long c;
}
按照Java规范,MyObject的对象是在堆内存上分配空间存储的,而且a、b、c三个属性在内存空间上是近邻,如下所示。
我们知道,在X86的CPU中 Cache Line的长度为64个字节,这也就意味着MyObject的3个属性(长度之和为24个字节)是完全可能加载在一个Cache Line里的。如此一来,如果我们有两个不同的线程(分别运行在两个CPU上)分别同时独立修改a与b这两个属性,那么这两个CPU上的Cache Line可能出现如下所示的情况 即 a与b这两个变量被放入同一个Cache Line中,并且被两个不同的CPU 共享。
根据4.1.2节里MESI协议的相关知识,我们知道,如果Thread 0要对α变量进行修改,则在CPU 1上有对应的Cache Line,这会导致CPU1的Cache Line无效,从而使得Thread 1被迫重新从Memory里获取b的内容(b并没有被其他CPU改变 这样做是因为b与a在同一个CacheLine里)。同样,如果Thread 1要对b变量进行修改,则同样导致Thread 0的Cache Line 失效,不得不重新从Memory里加载a。如此一来,本来是逻辑上无关的两个线程,就完全可以在两个不同的CPU上同时执行,但阴差阳错地共享了同一个Cache Line并相互抢占资源,导致并行成为串行,大大降低了系统的并发性,这就是Cache 伪共享问题。
解决Cache伪共享问题的方法很简单,将a与b两个变量分到不同的Cache Line里,通常可以用一些无用的字段填充a与b之间的空隙。由于伪共享问题对性能的影响比较大 所以JDK8首次提供了正式的普适性方案,即采用@Contended注解来确保一个Object或者Class里的某个属性与其他属性不在同一个CacheLine里。在下面的VolatileLong的多个实例之间就不会产生Cache伪共享问题:
econtended
class volatileLong {
public volatile long value= 0L;
}
深入理解不一致性内存
MESI 协议解决了多核CPU 下的Cache一致性问题,因而成为SMP架构的唯一选择,而SMP架构近几年迅速在PC领域(X86)发展。SMP架构是一种平行的架构,所有 CPU Core都被连接到一个内存总线上,它们平等访问内存,同时整个内存是统一结构、统一寻址的。如下所示给出了SMP架构的示意图。
但是,随着CPU核心数量的不断增加,SMP架构也暴露出天生的短板,其根本瓶颈是共享内存总线的带宽无法满足CPU数量的增加,同时,在一条“马路”上通行的“车”多了,难免会陷入“拥堵模式”。在这种情况下,分布式解决方案应运而生,系统的内存与CPU进行分割并捆绑在一起,形成多个独立的子系统,这些子系统之间高速互联,这就是NUMA (NoneUniform Memory Architecture)架构,如下图所示。
我们可以认为NUMA架构第1次打破了“大锅饭”模式,内存不再是一个整体,而是被分割为相互独立的几块,被不同的CPU私有化(Attach到不同的CPU 上)。因此,当CPU访问自身私有的内存地址时(Local Access)会很快得到响应,而如果需要访问其他CPU控制的内存数据(Remote Access),则需要通过某种互联通道(Inter-connect通道)访问,响应速度与之前相比变慢。NUMA的主要优点是伸缩性 NUMA 的这种体系结构在设计上已经超越了SMP,可以扩展到几百个CPU而不会导致性能严重下降。
NUMA技术最早出现于20世纪80年代,主要运行在一些大中型UNIX系统中,Sequent公司是世界公认的NUMA技术领袖。早在1986年,Sequent公司就率先利用微处理器构建了大型系统 开发了基于UNIX的SMP体系结构 开创了业界转入SMP领域的先河。1999年9月,IBM公司收购了Sequent 公司,将NUMA技术集成到IBM UNIX阵营中,并推出了能够支持和扩展Intel平台的NUMA-Q系统及解决方案,为全球的大型企业客户适应高速发展的电子商务市场提供了更加多样化、高可扩展性及易于管理的选择,成为NUMA技术的领先开发者与革新者。随后很多老牌UNIX服务器厂商也采用了NUMA技术,例如 IBM、Sun、惠普、Unisys、SGI等公司。2000年,在全球互联网泡沫破灭后,X86 Linux系统开始以低廉的成本侵占UNIX的地盘,AMD率先在其AMD Opteron系列处理器中的X86 CPU上实现了NUMA架构,Intel也跟进并在 Intel Nehalem中实现了NUMA 架构(Intel服务器芯片志强E5500 以上的CPU和桌面的i3、i5、i7均基于此架构),至此 NUMA这个贵族技术开始真正普及。
下面详细分析一下 NUMA技术的特点。首先,在NUMA架构中引入了一个重要的新名词——Node,一个Node由一个或者多个Socket Socket组成,即物理上的一个或多个CPU芯片组成一个逻辑上的Node。如下所示为来自Dell PowerEdge系列服务器的说明手册中的NUMA的图片 4个Intel Xeon E5-4600处理器形成4个独立的NUMA Node 由于每个Intel Xeon E5-4600都为8 Core,支持双线程,所以每个Node里的Logic CPU数量都为16个,占每个Node分配系统总内存的1/4,每个Node之间都通过Intel QPl (QuickPath Interconnect)技术形成了点到点的全互联处理器系统。
其次 我们看到NUMA这种基于点到点的全互联处理器系统与传统的基于共享总线的处理器系统的SMP还是有巨大差异的。在这种情况下无法通过嗅探总线的方式来实现Cache一致性,因此为了实现 NUMA 架构下的 Cache 一致性,Intel引入了MESI协议的一个扩展协议——MESIF。MESIF 采用了一种基于目录表的实现方案,该协议由Boxboro-EX处理器系统实现,但独立研究MESIF协议并没有太大的意义,因为目前Intel并没有公开Boxboro-EX处理器系统的详细设计文档。
最后说说NUMA架构的当前困境与我们对其未来的展望。
NUMA 架构打破了传统的“全局内存”概念,所以目前还没有任意一种编程语言从内存模型上支持它,当前也很难开发适应 NUMA 的软件。但这方面已经有很多尝试和进展了。Java在支持NUMA的系统里,可以开启基于NUMA的内存分配方案,使得当前线程所需的内存从对应的Node 上分配,从而大大加快对象的创建过程。在大数据领域,NUMA系统正发挥着越来越强大的作用,SAP的高端大数据系统HANA被SGI在其UV NUMA Systems上实现了良好的水平扩展。在云计算与虚拟化方面,OpenStack 与VMware已经支持基于NUMA技术的虚机分配能力 使得不同的虚机运行在不同的Core上 同时虚机的内存不会跨越多个NUMA Node。
NUMA技术也会推进基于多进程的高性能单机分布式系统的发展,即在4个Socket、每个Socket为 16Core的强大机器里,只要启动4个进程,通过NUMA技术便可将每个进程都绑定到一个Socket上,并保证每个进程只访问不超过Node本地的内存,即可让系统以最高性能并发,而进程间的通信通过高性能进程间的通信技术实现即可。
本文给大家讲解的内容是架构解密从分布式到微服务:深入浅析内存,你所不知道的内存知识- 下篇文章给大家讲解的是架构解密从分布式到微服务:深入浅析内存,内存计算技术的前世今生;
- 觉得文章不错的朋友可以转发此文关注小编;
- 感谢大家的支持!