0%

微服务一致性

CAP

CAP 是指在一个分布式系统下,包含三个要素:Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),并且三者不可得兼。

  • 一致性(Consistency),是指对于每一次读操作,要么都能够读到最新写入的数据,要么错误,所有数据变动都是同步的。
  • 可用性(Availability),是指对于每一次请求,都能够得到一个及时的、非错的响应,但是不保证请求的结果是基于最新写入的数据。即在可以接受的时间范围内正确地响应用户请求。
  • 分区容错性(Partition tolerance),是指由于节点之间的网络问题,即使一些消息丢包或者延迟,整个系统仍能够提供满足一致性和可用性的服务。

事务模式 -两阶段提交(2PC)

事物模式的代表就是两阶段提交,它的含义就是每个服务有自己的子事务,服务之间其实也是通过分布式事务来保障事务的一致性。

所以它在第一阶段的时候,这个协调器会发一个propose的请求给三个服务,如果每个服务都可以接受接下来的事务请求,它就会回复yes或者是no。回复之后就会到第二个阶段,如果前面三个服务回复的都是一个yes,那协调器就会发送commit这个请求,去执行这三个服务的事务操作,否则就会发送一个abort请求。执行完成之后,三个服务会回来一个ack,表示这个事务已经结束,或者是已经取消。

优点:有事务的原子性,因为在每次做事务操作的时候,都会锁定资源,所以所有进入协调器的请求,其实都是一个线性的请求,它不能去同步所有的事务。

缺点:它是一个阻塞式的模式,它需要锁定资源,它的复杂度也会相对比较高,比较难扩展。

预约模式 -TCC(Try-Confirm/Cancel)

预约模式比较典型的叫Try-Confirm/Cancel,缩写就是TCC。它也是一个两阶段的模式。第一个阶段它会发一个try的请求给三个服务,如果三个服务可以执行,它们就会回复yes,不能回复no,这和两阶段提交是一致的。不一样的地方是它不会锁定资源。所以在A处理完它的子事务,B可能还没有处理完它的子事务的时候,A可以接受其他的请求。在第二阶段,像两阶段提交一样,协调器会根据第一阶段ABC的返回结果去发送Confirm或者Cancel,这样的请求,最后ABC会返回ack。

优点:

在try阶段如果失败,服务会回复至原状,什么意思呢?比如说要发一个会议邀请,如果用TCC的模式,第一阶段会先发一个询问的请求给所有的与会人员,询问是否可以在这一个时间点来参会,如果所有的参会人员都回答yes,第二步就会发送一个确认请求,实际的一个会议邀请,如果有任何一个人员不能参加,就会发送一个Cancel,取消这个会议。

缺点:

  • 它不是原子操作,它可以并行处理好多的分布式的事务,然后它在Confirm这个阶段,只能重试,如果有一个服务失败的话,只能重试,直到成功,或者是采取回退措施,需要人工去做干预。
  • 它需要一个额外的try流程,服务需要提供额外的try的结构,也就需要提供额外的reserved的状态。

可靠消息最终一致性

可靠事件模式属于事件驱动架构,微服务完成操作后向消息代理发布事件,关联的微服务从消息代理订阅到该 事件从而完成相应的业务操作,关键在于可靠事件投递和避免事件重复消费。

可靠事件投递有两个特性:

  • 每个服务原子性的完成业务操作和发布事件;
  • 消息代理确保事件投递至少一次 (at least once)。避免重复消费要求消费事件的服务实现幂等性。

有两种实现方式:

本地事件表

本地事件表方法将事件和业务数据保存在同一个数据库中,使用一个额外的“事件恢复”服务来恢 复事件,由本地事务保证更新业务和发布事件的原子性。考虑到事件恢复可能会有一定的延时,服务在完成本 地事务后可立即向消息代理发布一个事件。

a9bcbd355bae4ae06f623413a0ef3cee.png

使用本地事件表将事件和业务数据保存在同一个数据库中,会在每个服务存储一份数据,在一定程度上会造成代码的重复冗余。同时,这种模式下的业务系统和事件系统耦合比较紧密,额外增加的事件数据库操作也会给数据库带来额外的压力,可能成为瓶颈。

外部事件表

针对本地事件表出现的问题,提出外部事件表方法,将事件持久化到外部的事件系统,事件系统 需提供实时事件服务以接收微服务发布的事件,同时事件系统还需要提供事件恢复服务来确认和恢复事件。

%!(EXTRA markdown.ResourceType=, string=, string=)

Saga的补偿机制

Saga支持向前和向后恢复:

向后恢复:如果任意一个子事务失败,则补偿所有已完成的事务

向前恢复:如果子事务失败,则重试失败的事务

BBR

今天花了一个下午学习bbr,真的是太奇妙了这个东西。计算机网络的东西我都忘得差不多了,一边学习一边回顾,也不敢写什么研究心得,主要是看了知乎上李博杰关于bbr的回答,然后看了原文(以下简称,对于一些不太清楚的概念做一些笔记,以便时候回顾。

一些概念

  • 加性增,乘性减:简单讲就是说,tcp loss-based拥塞控制传输时,拥塞窗口会已加的形式增大,而一旦遇到包丢失的情况,就会以除二的方式减小窗口。详情可以查看tcp慢启动和指数退避相关内容。

ebea4cbf1e53cb23dc31199e706ddda6.png

这张图我看了半个小时(学渣!),图的整体意思是说:单位时间内,随着发送数据的增加,到达链路运输能力(实际带宽)前发送速率(数据成功到达receiver的)增加,数据延迟不变;一旦达到链路的运输能力,发送速率不会变化,而数据发送耗时会增加。BBR将数据发送量限制到带宽,从而达到发送速率最大同时耗时最短,而传统的基于丢包的拥塞控制则控制的是数据量为buffer的极限。一点上图中的概念解析:

  • inflight:拥塞窗口(?不确定),可以理解为发送的数据量
  • loss-based congestion control:基于包丢失的拥塞控制。传统TCP拥塞控制方法,由于链路中存在buffer,buffer容量会比链路大,当buffer缓冲区到达limit时,会产生丢包行为,因此会把‘丢包’这个行为视为拥塞控制信号。因此,在高丢包率的长肥管道(带宽大,延迟高)中,这种机制表现非常差,会不停地进行指数退避。
  • RTT:round-trip time,指一个包从sender方到达receiver方耗时
  • RTprop:物理延迟。当链路中没有任何排队和其他耗时时,RTprop=RTT
  • deliveryRate:发送速率。虽然有rate但并不是说的比率,Delivery Rate = CWND/SRTT,其中CWND = 可发送包的个数 * 包的大小;SRTT 是平滑RTT,动态测量的结果。所以deliveryRate 其实可以解释为每单位时间可以发送的数据量。中有个公式,大意为BtlBw=max(deliveryRatet),也就是评估瓶颈带宽的方式。
  • BtlBw:瓶颈带宽,也就是链路的最大传输能力。
  • BDP带宽时延积:(Bandwidth-Delay Product ,BDP)即链路上的最大比特数,也称以比特为单位的链路长度。计算方法:Bandwidth-Delay Product = delay_bandwidth=RTprop_BtlBw
    BBR的方法可以理解为将链路上的比特数控制为BDP的值。
    论文:
    一开始讲了RTprop和BtlBw的回归方程计算。表明BBR需要实时统计deliveryRate和RTT的值来获得RTprop和BtlBw的值。
    然后说了RTprop和BtlBw不能同时测得。

自旋锁 ,自旋锁的其他种类,阻塞锁,可重入锁 ,读写锁 ,互斥锁 ,悲观锁 ,乐观锁 ,公平锁 ,偏向锁, 对象锁,线程锁,锁粗化, 锁消除,轻量级锁,重量级锁, 信号量,独享锁,共享锁,分段锁

c1108590c9b36555c13c9114731207a7.png

不可不说的Java“锁”事

乐观锁 VS 悲观锁

对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。

而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。

  1. 乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。例如AtomicInteger.incrementAndGet()
  2. 版本号机制。一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

场景:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

CAS虽然很高效,但是它也存在三大问题:

  1. ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
    • JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
  2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
  3. 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
    • Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

自旋锁 VS 适应性自旋锁

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

例:

  • TicketLock,基于先进先出(FIFO) 队列的自旋锁,保证首先注册的线程有更高级别优先性获取锁。有两个值,一个当前处理队列号,一个等待处理队列号。TicketLock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量queueNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。CLHLock和MCSLock是更好的解决方式
  • CLHLock,基于隐式链表的自旋锁,线程将会获取上一个线程注册的CLHNode对象,只有当CLHNode.isLocked==false时,自己才获得了锁
  • MCSLock,基于显式链表的自旋锁

无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁

偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)

Java并发——关键字synchronized解析

Java锁—偏向锁、轻量级锁、自旋锁、重量级锁

synchronized & Java对象头

synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?

我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

下面给出四种锁状态对应的的Mark Word内容

锁状态 存储内容 存储内容
无锁 对象的hashCode、对象分代年龄、是否是偏向锁(0) 01
偏向锁 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10

f46717d97f3012274c5be1f2b5c952d9.png

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Monitor

Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。

Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

结构如下:

9c8258bd38cb99dab01b37f75d22a040.png

Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL

EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程

RcThis:表示blocked或waiting在该monitor record上的所有线程的个数

Nest:用来实现重入锁的计数

HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)

Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁

这四种锁是指锁的状态,专门针对synchronized的,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。

依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。

无锁

无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。

无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。

偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

轻量级锁

是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。

在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。

拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。

重量级锁

轻量级锁自旋

升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。

锁的转换

b6a12dd002fbd1bb74d5fdcd8243c1e3.png

公平锁 VS 非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

可重入锁 VS 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

独享锁 VS 共享锁

独享锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和JUC中Lock的实现类就是互斥锁。

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

锁粗化/锁消除

锁消除:

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

锁粗化:

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部

1
2
3
4
5
6
7
8
9
10
11
package com.paddx.test.string;

public class StringBufferTest {
StringBuffer stringBuffer = new StringBuffer();

public void append(){
stringBuffer.append("a");
stringBuffer.append("b");
stringBuffer.append("c");
}
}

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

Yarn

背景

hadoop1.0中架构:

e2915ca78140dd83da4677f5378aa00f.png

问题:

  • 由于大量的数据处理Job提交给Job Tracker,且Job Tracker需要协调的Data Node可能有数千台,Job Tracker极易成为整个系统的性能、可用的瓶颈。
  • 无法有效地调配资源,导致资源分配不均。如以下例子,假设有3台Data Node(DN),每个DN的内存为4GB。用户提交了6个Job,每个Job需要1GB内存进行处理,且数据均在DN2上。由于DN2只有4GB内存,所以Job1-4在DN2上运行,Job5和6则在排队等待。但是,此时DN1和3均在闲置的状态下,而未能有效的被利用。

模块

ee95fc416fa0d343124e61ec779c0117.png

ResourceManager

负责对各个NodeManager 上的资源进行统一管理和调度。包含两个组件:

  • Scheduler:调度器根据容量、队列等限制条件(如每个队列分配一定的资源,最多执行一定数量的作业等),将系统中的资源分配给各个正在运行的应用程序
  • Applications Manager:应用程序管理器负责管理整个系统中所有应用程序,包括应用程序提交、与调度器协商资源以启动ApplicationMaster、监控ApplicationMaster运行状态并在失败时重新启动它等

NodeManager

NM 是每个节点上的资源和任务管理器。

  • 定时地向RM 汇报本节点上的资源使用情况和各个Container 的运行状态
  • 接收并处理来自AM 的Container启动/ 停止等各种请求

ApplicationMaster

用户提交的每个应用程序均包含一个AM,主要功能包括:

  • 与RM 调度器协商以获取资源(用 Container 表示)
  • 将得到的任务进一步分配给内部的任务
  • 与 NM 通信以启动 / 停止任务
  • 监控所有任务运行状态,并在任务运行失败时重新为任务申请资源以重启任务

Container

Container 是YARN 中的资源抽象, 它封装了某个节点上的多维度资源, 如内存、CPU、磁盘、网络等,当AM 向RM 申请资源时,RM 为AM 返回的资源便是用Container表示的

工作流程

e6e70223847d15cc2d580c3d5769e598.jpg

  1. 用户向YARN 中提交应用程序, 其中包括ApplicationMaster 程序、启动ApplicationMaster 的命令、用户程序等。
  2. ResourceManager 为该应用程序分配第一个Container,并与对应的Node-Manager 通信,要求它在这个Container中启动应用程序的ApplicationMaster。
  3. ApplicationMaster 首先向ResourceManager 注册,这样用户可以直接通过ResourceManage 查看应用程序的运行状态,然后它将为各个任务申请资源,并监控它的运行状态,直到运行结束,即重复步骤4~7。
  4. ApplicationMaster 采用轮询的方式通过RPC 协议向ResourceManager 申请和领取资源。
  5. 一旦ApplicationMaster 申请到资源后,便与对应的NodeManager 通信,要求它启动任务。
  6. NodeManager 为任务设置好运行环境(包括环境变量、JAR 包、二进制程序等)后,将任务启动命令写到一个脚本中,并通过运行该脚本启动任务。
  7. 各个任务通过某个RPC 协议向ApplicationMaster 汇报自己的状态和进度,以让ApplicationMaster 随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务。在应用程序运行过程中,用户可随时通过RPC向ApplicationMaster 查询应用程序的当前运行状态。
  8. 应用程序运行完成后,ApplicationMaster 向ResourceManager 注销并关闭自己。

资源调度算法

FIFO

FIFO 调度算法用于最早的 Hadoop 系统中,Hadoop 面向的是单用户提交的大规模数据处理作业。在 FIFO 调度算法中只有一个用户,所有作业按照优先级提交至具有不同优先级的队列中,然后由调度器(Scheduler)先按照作业提交的时间顺序选择待执行的作业。FIFO 的队列设置了 5 个优先等级,分别为:Very Low、Low、Normal、High 及 Very High。每个等级对应一个队列,按照队列的优先级从高到低选取队列,在同级队列中,按照提交作业的时间先后顺序提取并执行。其调度大体流程如图所示。

b8f63e0a703e9afc7cee319589a96269.png

Capacity 调度算法

  • 队列中单个任务使用的资源不会超过队列的容量。
  • 如果队列满,且集群有空闲的资源,调度器可以把资源分配给此队列(可配置),弹性队列。
  • 正常情况下,容量调度器不会抢占容器,因此如果一个队列随着使用,资源不够时,只能等待其他队列释放资源。
  • 容量调度器也可以执行work-preserving preemption,RM会请求应用返回容器。

Capacity 调度算法是由 Yahoo!开发的多用户调度策略,它以队列为单位划分资源,每个队列可设定一定比例的资源最低保证和使用上限,同时每个用户也可设定一定的资源使用上限以防止资源滥用。而当一个队列的资源有剩余时,可暂时将剩余资源共享给其他队列。Capacity Scheduler 支持多个队列,每个队列可分配一定的资源量,队列中资源的调度策略可以为 FIFO 或 DRF (Dominant Resource Fairness),多用户下,为防止同一个用户提交的作业独占队列的所有资源,Capacity Scheduler 对每个用户可提交的作业可分配的资源量进行了限制。

412e104e76eb20f736f641984a6372ce.png

应用程序提交后,ResourceManager 会向 CapacityScheduler 发送一个事件,CapacityScheduler 收到后,将为应用程序创建一个对象跟踪和维护其运行时的信息,同时,将它提交到对应的叶子队列中,队列会对应用程序进行合法性检查。通过检查,应用程序才算提交成功。提交成功后,它的ApplicationMaster会为它申请资源,CapacityScheduler收到资源申请后,暂时将这些请求存放到一个数据结构中,以等待为其分配合适的资源。

NodeManager 发送的心跳信息有两类需要 CapacityScheduler 处理:一类是最新启动的 Container;另一类是运行完成的 Container。CapacityScheduler 将回收它使用的资源进行再分配。CapacityScheduler 采用三级资源分配策略(即将双层调度机制中的第一层分为选择队列与选择应用程序两级),当一个节点上有空闲资源时,它会依次选择队列、应用程序和 Container (请求)使用该资源,接下来介绍三级资源分配策略。

第一级:选择队列。CapacityScheduler 采用基于优先级的深度优先遍历算法选择队列:从根队列开始,按照资源使用率由小到大遍历子队列。如果子队列是叶子队列,则按照第二、三级的方法在队列中选择一个 Container,否则以该队列为根队列,重复以上过程,直到找到合适的 Container 并退出。

第二级:选择应用程序。选中一个叶子队列后,CapacityScheduler 按照ApplicationID 对叶子队列中的应用程序进行排序,依次遍历排序后的应用程序,找到合适的 Container。

第三级:选择 Container。选中一个应用程序后,先满足优先级高的Container。同一优先级,先满足本地化的 Container,依次选择节点本地化、机架本地化和非本地化的 Container。

Fair 调度算法

  • 每个队列有权重元素,用于fair share的计算。
  • 默认队列和动态创建的队列,权重为1(默认队列的可配置)。
  • 调度器会使用最小资源数量来进行资源分配进行优先排序。如果两个队列的资源都低于fair share额度,那么远低于最小资源数量的队列,会被有限分配资源。

Fair Scheduler 是 Facebook 开发的多用户调度器,同 Capacity Scheduler 相似,都以队列为单位划分资源,且每个队列可设定一定比例的资源最低保证和使用上限。Facebook 设计 Fair Scheduler 算法的初衷是让 Hadoop 平台可以更高效的处理不同类型的作业,最大化的保证了系统中各作业能够分配到的系统资源 。如果当前集群中只有一个作业,则该作业会独占整个集群中全部系统资源。当有新的作业被提交至系统时,一些任务会在完成后将资源分配给新提交的作业,Fair Scheduler 会尽量保证各作业间可以获得基本相同的系统资源,从而保证短作业具有较短的响应时间。

a664c5abbfebb5bc8d174004cb39f4b8.png

与CapacityScheduler不同的是,FairScheduler 提供了更多样化的调度策略,调度策略在队列间和队列内部可单独设置。当前有三种策略可选,分别是先来先服务(FIFO)、公平调度(Fair)和主资源公平调度(Dominant ResourceFairness,DRF)。

  1. 先来先服务:按优先级高低调度,优先级相同,则按提交时间先后顺序调度;提交时间相同,则按名称大小调度。
  2. 公平调度:按内存资源使用比率大小调度。
  3. 主资源公平调度:按主资源公平调度算法进行调度,所需份额最大的资源称为主资源,把最大最小公平算法应用于主资源上,将多维资源调度问题转化为单资源调度问题。

DRF算法伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
R = <r1, ··· , rm> //m种资源对应容量
C = <c1, ··· , cm> //已经使用资源量,初始为0
si (i = 1…n) //用户i的主资源所需份额,初始为0
Ui = <ui,1, ··· , ui,m> (i = 1…n) //分配给用户i的资源量,初始为0
挑选出所需主资源si最小的用户i;
Di ← 用户i下一个任务所需的资源量
if C + Di <= R then
C = C + Di //更新C
Ui = Ui + Di //更新U
else
return //资源已经用完
end if

饥饿和抢占

FairShare的计算会被用于判断饥饿以及是否进行抢占。在计算FairShare时,有两种:

  • Steady FairShare,按照配置文件中所有queue的weight,计算出的。
  • Instantaneous FairShare,,按照配置文件中所有queue的weight,仅对包含活动应用程序的queue计算出的。
    在配置yarn.scheduler.fair.preemption和yarn.scheduler.fair.preemption.cluster-utilization-threshold后,抢占会启用。

饥饿有两种:

  1. FairShare Starvation
    要注意的是,在同一个队列里面,如果存在多个应用程序,它们会平均的分摊Instantaneous FairShare。因此可能存在队列整体不是饥饿状态,但是每个应用程序是。
    判定条件为:
    1. 未获得所要求的资源。
    2. 应用程序资源使用低于Instantaneous FairShare。
    3. 应用程序的资源使用低于fairSharePreemptionThreshold,并持续fairSharePreemptionTimeout。
  2. MinShare Starvation
    判定条件为:
    1. 未获得所要求的资源。
    2. 应用程序资源使用低于MinShare。
    3. 应用程序的资源使用低于MinShare,并持续MinSharePreemptionTimeout。

决定需要进行抢占的时候,可能在多个队列中都有可抢占的container,决定container是否可以被抢占,需要满足:

  1. 所在队列是可抢占的。
  2. 杀死container以后不会导致应用程序的资源低于Instantaneous FairShare。

启用抢占并不能保证队列或应用程序能够获得所有的Instantaneous FairShare。只能最终保证脱离饥饿的状态,即获得fairSharePreemptionThreshold份额的资源。

FairShare Starvation、MinShare Starvation以及抢占的关系如下:

779c225d3c0a4ff5c4774b5c10f13e5b.jpg

数据仓库

分层

模型层次 英文名 中文名 层次定义
APP Application 应用数据层 该层级的主要功能是提供差异化的数据服务、满足业务方的需求;在该层级实现报表、自助取数等需求。
DM Data Market 数据集市层 该层次主要功能是加工多维度冗余的宽表(解决复杂的查询)、多角度分析的汇总表。
DWM Data Warehouse Model 汇总数据层 面向分析主题的、统一的数据访问层,所有的基础数据、业务规则和业务实体的基础指标库以及多维模型都在这里统一计算口径、统一建模,大量基础指标库以及多维模型在该层实现。该层级以分析需求为驱动进行模型设计,实现跨业务主题域数据的关联计算或者轻度汇总计算,因此会有大数据量的多表关联汇总计算。
DWD Data Warehouse Detail 明细数据层 该层的主要功能是基于主题域的划分,面向业务主题、以数据为驱动设计模型,完成数据整合,提供统一的基础数据来源。在该层级完成数 据的清洗、重定义、整合分类功能。
DIM Dimension 维度层 该层主要存储对实体属性描述相对静态的维表,包括从OLTP层抽取转换维表、根据业务或分析需求构建的维表。
ODS Operational Data Store 操作数据层 该层级主要功能是存储从源系统直接获得的数据(数据从数据结构、数据之间的逻辑关系上都与源系统基本保持一致)。实现某些业务系统字段的数据仓库技术处理、少量的基础的数据清洗(比如脏数据过滤、字符集转换、维值处理)、生成增量数据表。