导读

日拱一卒,功不唐捐,分享是最好的学习,一个知识领域里的 “道 法 术 器” 这四个境界需要从 微观、中观以及宏观 三个角度来把握。微观是实践,中观讲套路,宏观靠领悟。本系列文章我把它命名为《分布式系统架构设计三十六式》,讲诉分布式系统里最重要的三十六个虚数的中观套路,而微服务的本质也是分布式,因此搞明白这三十六个最重要的知识点也就同时能搞明白微服务。

实现一个分布式系统通常会面临三大难题: 故障传播性、业务拆分与聚合以及分布式事务 。本系列中的服务治理章节主要是为了解决故障传播性的难题,它包括: 隔离、熔断、降级、限流、容错以及资源管控 ,本文将讲诉服务治理里的 “降级” 模式。

动机

  1. 某些时候系统会遇到负载过高的问题,当系统外来的或内部的负载过高超过预先定义的阈值,为了保证更重要的更紧急的业务的服务质量,希望将一些非核心的、不紧急的业务降低服务质量,从而释放一些额外的资源给紧急业务使用。比如一个分布式系统里的读、写、数据校验、空间回收都比较消耗资源,在白天为了保证读和写的服务质量,可以把数据校验的服务通过限流或减少线程数之类的方式使得可以调用的资源配额减少,从而释放部分资源给读和写使用,保证读写的服务质量。同样在读和写业务不繁忙的时候,降低读和写的资源配额,从而释放资源给空间回收使用,通过这种方式动态调整局部业务的服务质量从而保证关键业务的服务质量,提升用户体验。

  2. 在云服务里“可用性”指标是一个非常重要的SLA指标,在可用性出现不达标的情况下需要根据SLA进行赔偿,因此,我们希望分布式系统不管出现怎么样的故障,比如服务器故障,磁盘故障,网络故障都能保持可用性,起码要保证单点故障不会造成系统故障,比如,在系统出现严重故障的时候,可以停止负载较高的写操作从而保证“读”或者“查询“服务。

降级模式

从故障处理角度来讲,服务降级简单来说就是这一功能或服务直接不能用,而在动态调整系统整体的服务质量的时候,降级是降低某些当前非重要或非核心业务的资源,从而释放部分资源给重要的或紧急的业务使用。

在故障处理的时候,对比“熔断”,降级是更严重的故障处理方式,最后拿来兜底用的。比如某个功能出故障,“熔断”是不管怎么样,都希望这个功能还能救活,降级是发现试着救了几次发现还是救不活,就下狠心砍掉这个部分,断臂求生,起码要保证整体是活的,这样整体还有救活的希望。

从系统的角度来说降级有 读功能降级,写功能降级以及级联组件降级,还有自动降级或者人工降级。比如,在云服务里,为了保证高可用性,在出现系统级的故障后,可以把写功能降级,就是这个服务只能读,只能查询不能写了,因此在设计的比较好的云服务里,按时间的维度来度量可用性已经没有太大的意义,因为不管怎么样它都是服务可用的,系统都是活着的,起码部分服务可用,因此在云服务里更合理的新的衡量可用性的指标方式是请求失败比率。

降级模式设计思路

降级触发策略

  • 超时降级:在超时重试的次数达到一个阈值后就触发降级;
  • 失败比率降级:当某个服务的失败的比率达到一定比率后就开始降级;
  • 系统故障降级:比如网络故障,硬盘故障,电源故障,服务器故障,数据中心故障等;
  • 限流降级:某些访问量太大的场景会触发限流,当达到限流阈值后,请求也会被降级;
  • 重要业务救急降级:比如为了保证读或者查询的功能,降低写或者数据校验的资源配额,从而实现读服务的质量保证。

降级处理措施

  • 资源配额调度,调度不紧急的业务支援紧急的重要的业务;
  • 抛出异常,直接抛出异常,打印出出错日志,然后就不管了,请求会丢失,这在需要保证幂等性的请求里不合适;
  • 直接返回, 直接返回拒绝服务,这里请求也会丢失,这在需要保证幂等性的请求里不合适;
  • 调用回退方法,调用出现服务降级时对应的业务处理逻辑,不同场景降级处理的逻辑不同,比如可以把请求再挂到等待队列里继续重试之类,这里需要根据业务场景合理设计回退方法;

降级分级策略

一般可以把降级的等级分为几个层次,比如P0级,P1级,P2级,P3级,级别越高表示问题越严重, 比如:

  1. 重要业务救急降级可以定义为P0级降级,只是调度次要的资源去救急,并不会出现故障;
  2. 限流降级可以定义为P1级降级,只是为了保证服务质量,而且如果不限流可能会出现系统负载过高从而出现故障;
  3. 超时降级以及失败比率降级可以定义为P2级降级,出现小范围故障,触发P2级降级,保证小故障不蔓延不传播从而造成大范围的故障;
  4. 系统故障降级可以定义为P3级降级,系统出现大范围故障,从而触发P3级降级,比如,此时可以只保证最低资源的的读请求服务,写和其他业务全部被禁止。

配置中心

如下图所示是一个简单的配置中心物理架构图:

配置中心

在分布式系统里每个服务的配置信息会给保存在一个配置中心里,这个配置中心里有每个服务的开关信息以及一些重要的资源配置信息。通过动态调整服务的配置信息,比如降级触发策略、降级处理措施、降级分级策略或者开关信息可以实现服务降级功能。

小结

本文讲诉了服务治理里的 “降级”模式,在前一篇《分布式系统架构设计三十六式之服务治理-熔断模式》里讲诉了分布式系统服务治理的熔断模式。另作者能力与认知都有限,欢迎大家拍砖留念。

作者简介

常平,中科大硕,10年+数据相关经验,主要工作背景为分布式系统、存储、缓存、微服务、云计算以及大数据,现就职于DELL EMC。

版权申明

本文的版权协议为 CC-BY-NC-ND license:https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh

在遵循署名、非商业使用(以获利为准)以及禁止演绎的前提下可以自由阅读、分享、转发、复制、分发等。

参考资料

[1] https://medium.com/@felipedutratine/microservices-should-be-design-for-failure-b58bccdce0b6

[2] https://blog.risingstack.com/designing-microservices-architecture-for-failure

导读

日拱一卒,功不唐捐,分享是最好的学习,一个知识领域里的 “道 法 术 器” 这四个境界需要从 微观、中观以及宏观 三个角度来把握。微观是实践,中观讲套路,宏观靠领悟。本系列文章我把它命名为《分布式系统架构设计三十六式》,讲诉分布式系统里最重要的三十六个虚数的中观套路,而微服务的本质也是分布式,因此搞明白这三十六个最重要的知识点也就同时能搞明白微服务。

实现一个分布式系统通常会面临三大难题: 故障传播性、业务拆分与聚合以及分布式事务 。本系列中的服务治理章节主要是为了解决故障传播性的难题,它包括: 隔离、熔断、降级、限流、容错以及资源管控 ,本文将讲诉服务治理里的 “熔断” 模式。

动机

在分布式系统里经常会遇到这样的场景:

  1. 系统负载突然过高,比如突发的访问量、过多的请求并发数以及过多的IO等都会造成某个节点故障,比如节点A,然后节点A挂了,又把负载转给节点B,然后节点B又负载过高,接着B又挂了,就这样一连串的挂过去从单点故障造成系统级的级联故障。

  2. 当一个服务出现故障时,希望这个服务能在一个时间段内恢复,在请求被拒绝后隔一段时间再自动的去探测服务的可服务性。

对应这两个场景,我们希望在分布式系统里能避免级联故障、提供快速失败快速恢复服务的能力,因此,这里引出 “熔断模式”

熔断模式

熔断模式也称之为断路器模式,英文单词是“circuit breaker”,“circuit breaker”是一个电路开关,其基本功能是检测到电流过载就中断电路,在检测到电流正常时又能自动或手动恢复工作,从而保护断路器背后的电源设备安全。这里需要将”断路器“与 “保险丝”进行区分,断路器可以通过手动或自动的复位从而恢复正常工作,而保险丝是运行一次必须更换。

实现一个分布式系统通常会面临三大难题: 业务拆分与聚合,分布式事务以及故障传播性。本系列中的服务治理章节主要是为了解决故障传播性的难题,它包括:隔离、熔断、降级、限流、容错以及资源管控,本文将讲诉服务治理里的 “熔断”模式。

在分布式系统里 “熔断模式”的设计思想来源于此,当系统里响应时间或者异常比率或者异常数超过某个阈值时,比如超时次数或重试次数超过某个阈值就会触发熔断,接着所有的调用都快速失败,从而保证下游系统的负载安全,在断开一段时间后,熔断器又打开一点试着让部分请求负载通过,如果这些请求成功那么断路器就恢复正常工作,如果继续失败,则继续关闭服务走快速失败通道,接着继续这个过程直到重试的次数超过一定的阈值从而触发更为严重的“降级模式”

熔断模式设计思路

下图是一个熔断模式的设计思路:

熔断器

图片来源于引文[2],版权归原作者所有

  1. 首先熔断器是处于闭合(closed)状态的,如果请求超时次数,异常数或者异常比率超过一定的阈值则熔断器会被打开;

  2. 接着熔断器处于打开(Open)状态,所有走到这个路径里的请求会走快速失败通道从而避免负载下行,但是这里不会一直都是打开的,过一个时间周期会自动切换到半打开(Half-open)状态;

  3. 在接下来是半打开(half-open)状态,在这里认为之前的错误可能被修复了,因此允许通过部分请求试着看看能不能处理成功,如果这些请求处理成功,那么就认为之前导致失败的错误已被修复,此时熔断器就切换到闭合状态并且将错误计数器重置。如果这些试着发送的请求还是处理失败,则认为导致之前失败的问题仍然存在,熔断器切回到打开方式,然后开始重置计时器给系统一定的时间来修复错误。半打开状态能够有效防止正在恢复中的服务被突然而来的大量请求再次打挂;

  4. 接着重复以上过程,直到半打开状态重复的次数达到一定的阈值发现故障还没被修复,从而触发”降级“状态

小结

本文讲诉了服务治理里的 “熔断”模式,在前一篇《分布式系统架构设计三十六式之服务治理-隔板模式》里讲诉了分布式系统服务治理的隔板模式。另作者能力与认知都有限,欢迎大家拍砖留念。

作者简介

常平,中科大硕,10年+数据相关经验,主要工作背景为分布式系统、存储、缓存、微服务、云计算以及大数据,现就职于DELL EMC。

版权申明

本文的版权协议为 CC-BY-NC-ND license:https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh

在遵循署名、非商业使用(以获利为准)以及禁止演绎的前提下可以自由阅读、分享、转发、复制、分发等。

参考资料

[1] https://en.wikipedia.org/wiki/Circuit_breaker

[2] https://martinfowler.com/bliki/CircuitBreaker.html

导读

日拱一卒,功不唐捐,分享是最好的学习,一个知识领域里的 “道 法 术 器” 这四个境界需要从 微观、中观以及宏观 三个角度来把握。微观是实践,中观讲套路,宏观靠领悟。本系列文章我把它命名为《分布式系统架构设计三十六式》,讲诉分布式系统里最重要的三十六个虚数的中观套路,而微服务的本质也是分布式,因此搞明白这三十六个最重要的知识点也就同时能搞明白微服务。

实现一个分布式系统通常会面临三大难题: 故障传播性、业务拆分与聚合以及分布式事务 。本系列中的服务治理章节主要是为了解决故障传播性的难题,它包括: 隔离、熔断、降级、限流、容错以及资源管控 ,本文将讲诉服务治理里的 “隔板” 模式。

动机

在分布式系统里通常将进程容器化以进行资源隔离,然后在同一个进程里的各种业务都共享线程池对外提供服务,这就经常会遇到这样的问题:

  1. 业务A负载较高,抢占了线程池里的大部分线程资源,从而造成其他业务的服务质量下降;
  2. 同一个进程内新加入一个业务,这个业务会抢占其他业务的资源,从而造成系统的不稳定,比如业务性能抖动;
  3. 难以调试,比如同一个进程里的10个业务共享同一个线程池,当出现故障时难以通过简单的日志判断是哪个业务出了问题。

因此,我们希望找出一个机制解决这样的问题。

隔板模式

首先我来看一个英文单词“Bulkhead”,翻译成中文就是“舱壁”‘或“隔板”,在分布式系统里有个资源隔离的设计模式叫做”舱壁模式”或者“隔板模式”。

模式来源: 通过万能的Wiki百科我们可以了解到轮船里的两个舱位之间的挡板就是隔板/舱壁(Bulkhead),如下图:


bulkhead

图片来源于引文[1],版权归原作者所有




在造船的时候,”船农们“(对应码农)通常会把一个大的船舱用隔板分成N个小的空间,以便万一船体破裂或着火的时候,只有这个被分割开的小船舱受到影响,而其他的船舱是被隔离而不受影响的,从而提高整个船只的安全度。

同样这种隔板模式可以应用在分布式系统的资源隔离设计里,在分布式系统里,资源隔离通常按业务分为进程级别的隔离和线程级别的隔离,某些简单的服务质量要求不高的业务场景下实现进程级别的隔离就够了,但是在某些对服务质量要求较高的分布式场景下需要线程级别的细粒度隔离。


进程隔离

进程级别隔离通常指的是容器化隔离,比如通过使用docker实现业务进程之间的资源隔离。

线程隔离

线程级别隔离是指给每个跑在进程里的业务都按业务类型创建一个线程池,从而实现线程级别细粒度的资源隔离,线程隔离具有以下优势:

  1. 提高业务可靠性,减少业务受其他业务影响的程度,当一个业务耗尽自身的线程资源后也不会影响另外一个业务的服务质量;
  2. 降低新加入的业务的给系统带来的风险,比如当前系统的一个进程用例中有10个业务。当新加入一个业务时,必然会抢占此前10个业务的线程资源,从而给系统带来不稳定,比如性能抖动;
  3. 利于调试,给每一个业务都分配一个线程池名称,当业务出故障时,通过线程池名称可以很方便地定位是哪个业务出了故障,并且通过监控线程池的请求失败次数、超时次数、拒绝请求次数等可以实时的反应当前业务服务质量。

事物都有二元性,线程池隔离,有利自然也有弊,线程池隔离也会引入额外的一些开销,开销类型有:

  1. 对象分配,每个调用都会实例化一个新的线程对象及其中的关联对象,占用系统资源;
  2. 并发,共享数据结构,计数器等,也占用系统资源;
  3. 线程的执行开销:切换,调度,执行,同样也占用资源。

因此,线程池的隔离带来了好处但是也会引起一些顾虑,比如给每个业务都创建一个线程池是否会给系统带来太大的开销。通过Hystrix的数据分析可以得出结论是: “开销是有的,但是对比好处,通过权衡,其开销在一些要求不苛刻的场景可以忽略。”

线程池的开销分析

Hystrix官网[3],统计了线程池带来的开销成本,如下图表示在单个API实例上以60个请求/秒执行一个HystrixCommand:

Hystrix

图片来源于引文[3],版权归原作者所有

通过分析这个统计图(注意不同的颜色),我们可以看到:

  1. 中位数(P50)和更低的场景下,对比不使用线程池隔离模式,隔离线程池基本没有成本开销。
  2. 在P90的场景下,对比不使用线程池隔离模式,隔离线程池的耗时差距为3毫秒。
  3. 在P99的场景下,对比不使用线程池隔离模式,隔离线程池的耗时差距为9毫秒。

但是从上图可以看出,成本增加的幅度远小于单独一个线程的执行时间增加的幅度,当未使用线程池隔离的线程执行时间从2ms跳到28ms时,线程池隔离的耗时成本从0ms跳到9ms。

因此,对于大多数的使用场景而言,在P90及以上的线程池隔离带来的开销被认为是可接受的,从而获得资源隔离带来的好处。

但是在某些场景这样的开销可能过高,比如缓存场景,在这种情况下,可以选用信号量来进行隔离,缺点是信号量不允许设置超时,难以实现熔断、降级之类的服务治理行为。

小结

本文讲诉了服务治理里的 “隔板”模式,在下一篇将讲诉分布式系统服务治理的熔断模式。另作者能力与认知都有限,欢迎大家拍砖留念。

作者简介

常平,中科大硕,10年+数据相关经验,主要工作背景为分布式系统、存储、缓存、微服务、云计算以及大数据,现就职于DELL EMC。

版权申明

本文的版权协议为 CC-BY-NC-ND license:https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh

在遵循署名、非商业使用(以获利为准)以及禁止演绎的前提下可以自由阅读、分享、转发、复制、分发等。

参考资料

[1] https://en.wikipedia.org/wiki/Bulkhead_(partition)

[2] http://writing.engr.psu.edu/uer/bassett.html

[3] https://github.com/Netflix/Hystrix/wiki/FAQ%20:%20General

ceph逻辑架构图

ceph后端支持多种存储引擎,以插件化的形式来进行管理使用,目前支持filestore,kvstore,memstore以及bluestore,目前默认使用的是filestore,但是目前bluestore也可以上生产。下图是ceph的逻辑架构图:

ceph-bluestore

Firestore存在的问题是:

  1. 在写数据前需要先写journal,会有一倍的写放大;

  2. 若是另外配备SSD盘给journal使用又增加额外的成本;

  3. filestore一开始只是对于SATA/SAS这一类机械盘进行设计的,没有专门针对SSD这一类的Flash介质盘做考虑。

而Bluestore的优势在于:

  1. 减少写放大;

  2. 针对FLASH介质盘做优化;

  3. 直接管理裸盘,进一步减少文件系统部分的开销。

但是在机械盘场景Bluestore与firestore在性能上并没有太大的优势,bluestore的优势在于flash介质盘。

FileStore逻辑架构

下图为ceph filestore逻辑架构图:

ceph-bluestore

  1. 首先,为了提高写事务的性能,FileStore增加了fileJournal功能,所有的写事务在被FileJournal处理以后都会立即callback(上图中的第2步)。日志是按append only的方式处理的,每次都是被append到journal文件末尾,同时该事务会被塞到FileStore op queue;

  2. 接着,FileStore采用多个thread的方式从op queue 这个 thread pool里获取op,然后真正apply事务数据到disk(文件系统pagecache)。当FileStore将事务落到disk上之后,后续的读请求才会继续(上图中的第5步)。

  3. 当FileStore完成一个op后,对应的Journal才可以丢弃这部分Journal。对于每一个副本都有这两步操作,先写journal,再写到disk,如果是3副本,就涉及到6次写操作,因此性能上体现不是很好。

Bluestore逻辑架构

下图为ceph bluestore逻辑架构图:

ceph-bluestore

  1. Bluestore实现了直接管理裸设备的方式,抛弃了本地文件系统,BlockDevice实现在用户态下使用linux aio直接对裸设备进行I/O操作,去除了本地文件系统的消耗,减少系统复杂度,更有利于Flash介质盘发挥性能优势;

  2. 为了惯例裸设备就需要一个磁盘的空间管理系统,Bluestore采用Allocator进行裸设备的空间管理,目前支持StupidAllocator和BitmapAllocator两种方式;

  3. Bluestore的元数据是以KEY-VALUE的形式保存到RockDB里的,而RockDB又不能直接操作裸盘,为此,bluestore实现了一个BlueRocksEnv,继承自EnvWrapper,来为RocksDB提供底层文件系统的抽象接口支持;

  4. 为了对接BlueRocksEnv,Bluestore自己实现了一个简洁的文件系统BlueFS,只实现RocksDB Env所需要的接口,在系统启动挂在这个文件系统的时候将所有的元数据都加载到内存中,BluesFS的数据和日志文件都通过BlockDevice保存到底层的裸设备上;

  5. BlueFS和BlueStore可以共享裸设备,也可以分别指定不同的设备,比如为了获得更好的性能Bluestore可以采用 SATA SSD 盘,BlueFS采用 NVMe SSD 盘。

2018年2月12日到2019年2月11日,刚好满一年,不知不觉间居然写了35770行代码,

2018-02-12 ~ 2019-02-11, 刚好入职EMC满一年,里程碑两件:

1,个人代码量突破3万5千行,排列第一;

2,专利公司内部通过且美国专利局审核中2个。

codeline

当前大数据处理平台存在的问题

如图1是目前大数据处理平台最常见的Lambda架构,它的优势在于实时处理与批处理统一,但是它的缺点也很明显:

  1. 实时处理一条路径,批处理另外一条路径,不同的路径采用了不同的计算组件,这就增加了系统的复杂度;
  2. 数据存储多组件化、多份化,如下图,同样的数据会被存储在ElasticSearch 里、S3对象存储系统里、Kafka里、HDFS里以及Cassandra里,而且考虑到数据的可靠性,数据还都是多份冗余的,这就极大的增加了用户的存储成本;
  3. 系统里组件太多太复杂,也增加了用户的运维成本。

lambda架构

​图1. Lambda架构

因此,为了解决Lambda架构的以上三大缺点,流式架构被提出。在流式架构里,流计算一般选用Flink作为计算组件,那么对于存储来说又意味着什么呢?为了降低系统复杂度、减少用户的存储成本与运维成本,我们推出了 流存储,目的之一就是为了重构Lambda架构里的存储栈,这样流式架构就可以由”流计算+流存储“组成。

第4种存储类型 - 流存储

首先,流式大数据处理平台里的数据一般被称之为“流数据”,流数据在百度百科里是这样被定义的:

流数据是一组顺序、大量、快速、连续到达的数据序列,一般情况下,数据流可被视为一个随时间延续而无限增长的动态数据集合。应用于网络监控、传感器网络、航空航天、气象测控和金融服务等领域。

那么目前又有哪种存储系统最适合用于“流数据”呢?正如当前技术条件下最适合“流数据”计算的是类似Flink这样的流计算应用,最适合“流数据”存储的应当是流存储系统。

如图2所示,从 存储的视角来说,每种类型的数据都有其原生的属性和需求,对应有最佳的适用场景以及最合适的存储系统。

存储类型

​ 图2. 4大存储类型

简单来说就是传统数据库这类对于IOPS要求高的业务需要块存储系统。文件共享场景下需要在用户间共享文件进行读写操作,因此适合采用分布式文件存储系统。而互联网业务文件以及图片、视频等适合采用对象存储系统。

流数据存储具有性能要求高、严格次序保证、连续而又无限、大规模租户隔离等特点,而目前市面上又没有这样一个专门针对流数据进行设计的存储系统。因此,为了满足业务需求、平衡商业成本与技术成本,也为了给流数据提供最佳最合适的存储系统,分布式流存储Pravega被推出。

I/O路径隔离

​ 图3. 日志结构

如图3所示:在Pravega里,日志是作为共享存储原语而存在的。Pravega被推出的目的之一就是为了 重构Lambda架构里的存储栈:流批统一、降低存储成本以及运维成本。 一般数据的批处理对应于处理历史数据,因此Pravega支持高吞吐量的追赶读;数据的流处理对应于处理实时数据,因此Pravega又支持低时延的尾部读取以及写入;同时Pravega通过分层存储以及资源自动伸缩降低了用户的存储成本以及运维成本。

Pravega关键架构

架构目标

  • 持久化:在客户端确认写入前,数据被复制并且写入磁盘;
  • 严格的顺序保证以及恰好一次语义:支持追赶读、尾部读以及从中间任意位置读,支持事务
  • 轻量级:一个流就如同一个文件,可以在单集群里创建千万量级起的流;
  • 可弹性:可基于负载和吞吐量智能地动态扩展或者收缩流;
  • 无限性:存储空间大小不受单个节点的容量限制;
  • 高性能:写入延迟低于10ms,吞吐量仅受网络带宽限制,读模式(例如:追赶读)不影响写性能;

逻辑架构

”技术在某种程度上一定是来自此前已有技术的新的组合“ – 《技术的本质》,布莱恩·阿瑟

Pravega为连续而又无限的数据提供了一种新的存储原语 - 流存储,然而Pravega也并不是凭空发明出来的,它是以前成熟技术与新技术的组合,例如Pravega的 范围、流、段、事件就跟Kafka的主题、分区、段、消息对应,而一层存储又用了Bookkeeper,协调器用了Zookeeper等,如图4 :Pravega的逻辑架构。

逻辑架构图

​ 图4. 逻辑架构

  1. Pravega提供了一个用Java编写的客户端库,抽象出了流协议层接口,用于支持客户端应用,例如Flink、Spark以及一些检索系统等;
  2. Pravega实现了一个流数据抽象层,用于事件流和字节流的抽象;
  3. Pravega遵循软件定义存储的设计规则,其控制面与数据面分离,控制实例组成控制面,实现了检索流信息、监控集群、收集相关指标等功能,同时为了实现高可用,通常有多个(建议至少3个)控制实例同时对外提供服务;
  4. Pravega采用Zookeeper作为集群中的协调组件;
  5. Pravega的第1层存储系统由bookkeeper实现,第2层存储系统由开源的HDFS、Ceph、GlusterFS、Swift或者商业存储产品组成。

流批统一 - 降低系统复杂度

通过使用Pravega,实现了流批统一的大数据处理架构,重构了大数据处理平台的存储栈,有效降低了系统复杂度.

存储分层 - 降低存储成本

如图4所示,在Pravega里,底层存储系统由两部分组成:第1层为低时延存储层,主要关注性能,用于存储热点数据,由bookkeeper实现,保证了存储系统的低时延、高性能。第2层为长期存储层,主要关注低成本、高吞吐量以及高可扩展性,提供数据的长期存储,由开源的或者商业的存储产品组成。随着数据的老化,第1层中的数据将自动分层流入第2层。通过这种方式,冷热数据分离有效降低了数据存储成本。

资源自动缩放 - 减少运维成本

在Pravega里,当流中的负载上升或下降时,流中段的数量会随着负载自动增长或收缩,此特性被称之为“自动缩放”,该特性无需人工干预自动完成,有效减少了系统的运维成本。当创建流时,可以使用缩放策略配置流,该策略确定流如何响应其负载变化,目前支持三种策略:1)固定,流段的数量不随负载而变化;2)基于写入的字节数,当每秒写入流的数据字节数增量超过某个目标速率时,流段的数量增加,相应的如果它低于某个流速时,流段数量减少;3)基于事件的个数,与基于字节数的扩展策略类似,不同之处在于它使用事件的个数而不是字节数。

Pravega的一些关键概念与特性

本章节将简要介绍一些Pravega的关键特性。

范围(scope):在Pravega里,范围是流的命名空间,例如可以把一台机器命名为一个范围,也可以把一个无人车命名为一个范围,还可以把整个工厂命名为一个范围。

流(stream):在同一个范围内流具有命名唯一性,所有流的名称在同一个范围内都是唯一的。在pravega里数据被组织到流中的,流是一种可持久化、可伸缩、仅附加、字节大小无限制的序列,具有高性能和强一致性的特性。

段(segment):流由段组成,段是流的分片。

事件(event): 段由事件组成,事件存储在段里,事件是流中的可以表示为一组字节的任何事物。例如:来自温度传感器的读数,它包含少量的字节且由时间戳,度量标识符和温度值组成。另外事件也可以是与用户点击网站或APP相关联的日志数据等。

写客户端(writers):写客户端是一个可以创建事件并将事件写入流中的应用,所有的事件数据都可以通过附加到流的尾部来写入。

读客户端(readers):读客户端是一个可以从流中读取事件的应用,读客户端可以从流中的任何一点读取,比如头部、尾部、中间任何一点。

读者组(readerGroups):读者组由读客户端组成,读者组本质上是为了实现同一个组内读客户端的平衡以及不同组的扇出。同一个读者组内的读客户端可以一起并行读取给定的一组流段内的事件,比如一个读客户端对应一个段。不同的应用可以定义不同的读者组实现扇出,比如定义一个Flink读者组,再定义一个检索读者组,这样二者互不影响,互不干涉,可以优雅而又和谐地一起读取同一个流段内的事件。

顺序保证:流是由段组成的,写入流的事件被写入单个段,在同一个段内的事件具有顺序性。对于读客户端来说,可以分配多个可并行读取的段,从多个段读取的也许是交错的事件,但在同一个段内读取的数据是有严格有序的。

检查点:Pravega为应用提供了在读者组上初始化检查点的功能,使用检查点的意图是通过使用检查点事件来确保每个读客户端能保存原来的使用状态。

事务: Pravega提供了事务功能,事务是写客户端可以“批处理”一堆事件并将它们作为一个处理单元原子性地提交到流中。这一堆事件要么所有都处理成功,要么所有都处理失败。在提交事务之前,发布到事务中的事件永远不会被读客户端看到。

状态同步器: Pravega也提供了在分布式计算环境中作为协调器的功能,类似Zookeeper、ETCD这样的提供分布式共识和领导者选举能力。这样的组件在Pravega里被称作“状态同步器”。状态同步器为在集群中运行的多个进程之间的共享状态提供同步机制,使用户可以轻松地构建高级服务,从而使用户更加的容易构建分布式应用。

恰好一次: Pravega确保每个事件只被处理一次,即使客户端、服务器或网络出现故障也能保证精确的处理顺序。

性能: Pravega的延迟目标为毫秒级(<10ms);

永久保留: Pravega将流的抽象与实际数据存储分离,这使得Pravega可以透明地将数据从低延迟、持久的存储层移到云存储服务层。

高效存储: Pravega统一了流(有序)数据和批量(并行)数据的访问,可以将批量和实时应用程序结合起来而无需为流式计算流水线(比如Flink)的每个步骤复制数据从而有效的提高了数据的存储效率。



## 与kafka对比

前面我们已经提到过Pravega是从 存储的视角来看待流数据,而Kafka本身的定位是消息系统而不是存储系统,它是从 消息的视角来看待流数据。消息系统与存储系统的定位是不同的,简单来说,消息系统是消息的传输系统,关注的是数据传输与生产消费的过程。而存储系统除了关注存储用的物理媒介,数据的持久化、安全、可靠性、一致性、隔离等都是它的原生属性,它关注数据的生产、传输、存放、访问等整个数据的生命周期。

这里我们把Pravega与Kafka做了对比,大体在功能上的差异如下表所示。功能上的差异也只是说明各个产品针对的业务场景不同,看待数据的视角不同,并不是说明这个产品不好,另外每个产品自身也在演进,因此本对比仅供参考。

名称 Kafka 2.1.0 Pravega GA
自动扩容缩容 部分支持 支持
完全不丢数据 不支持 支持
多协议可入 支持 支持
无限个流 不支持 支持
事务 支持 支持
恰好一次 支持 支持
顺序保证 支持 支持
兼容Kafka API 支持 支持
数据链接与汇聚 支持 部分支持
多种二层存储支持(ECS,HDFS,S3,etc) 不支持 支持
安全与加密 支持 支持
无限多租户 不支持 部分支持
服务质量保证 部分支持 部分支持
流计算应用集成 支持 支持
数据治理 不支持 支持

总结

本文讲述了推出分布式流存储Pravega的原因,介绍了一些Pravega的关键架构以及关键特性,另外还与Kafka做了简要对比。有关Pravega的更多详细信息,请参阅官方网站以及关注我们的后续文章。另作者能力有限,如有不足之处欢迎留言批评指正。

问题思考

最后给大家留一个问题:一般来说从开源项目到商业产品还是有一段距离的(注意这里的用词:开源的“项目”,商业的“产品”),那么对于设计开发人员来说应该如何弥补这段距离,从而使得开源项目产品化?

Pravega架构

”技术在某种程度上一定是来自此前已有技术的新的组合“ – 《技术的本质》,布莱恩·阿瑟

Pravega为连续而又无限的数据提供了一种新的存储原语 - 流存储,然而Pravega也并不是凭空发明出来的,它是以前成熟技术与新技术的组合,例如Pravega的 范围、流、段、事件就跟Kafka的主题、分区、段、消息对应,而一层存储又用了Bookkeeper,协调器用了Zookeeper等。

设计原则与目标

  • 持久化:在客户端确认写入前,数据被复制并且写入磁盘;

  • 保序:段内严格保序;

  • 恰好一次:支持恰好一次语义;

  • 轻量级:一个流就如同一个文件,可以在单集群里创建千万量级起的流;

  • 可弹性:可基于负载和吞吐量智能地动态扩展或者收缩流;

  • 无限性:存储空间大小不受单个节点的容量限制;

  • 高性能:写入延迟低于10ms,吞吐量仅受网络带宽限制,读模式(例如:追赶读)不影响写性能;

Pravega设计创新

  1. 支持“无限流”分层

  2. 零接触动态缩放

    • 根据负载和SLO自动调整读/写并行度

    • 没有服务中断

    • 无需手动重新配置客户端
    • 无需手动重新配置服务资源
  3. 智能工作负载分配

    • 无需为峰值负载过度配置服务器
  4. I / O路径隔离

    • 支持尾部写入
    • 支持尾部读
    • 支持追赶读
  5. 支持“恰好一次”事务

逻辑架构

下图为Pravega的逻辑架构图:


逻辑架构图

  1. 首先,Pravega提供了一个用Java编写的客户端库,抽象出了流协议层接口,用于支持客户端应用,例如Flink、Spark以及一些检索系统等;
  2. 其次,Pravega实现了一个流数据抽象层,用于事件流和字节流的抽象;
  3. 再者,从整体架构上来讲Pravega符合软件定义存储的设计规则,其控制面与数据面分离,数据面的集合统称为段存储层,控制实例组成控制面,实现了检索流信息、监控集群、收集相关指标等功能,同时为了实现高可用,通常有多个(建议至少3个)控制实例同时对外提供服务。
  4. Pravega采用Zookeeper作为集群中的协调组件。
  5. Pravega的存储系统由两部分组成:第1层为短期存储层,主要关注性能,用于存储热点数据,由bookkeeper实现,保证了存储系统的低时延、高性能。第2层为长期存储层,主要关注成本,提供数据的持久性以及长期存储,由开源的或者商业的存储产品组成。第1层保留热点数据,随着第1层中数据的老化,数据将自动分层流入第2层。

数据架构

下图展示了Pravega的数据架构图以及数据流分层:

数据架构图

  1. Pravega客户端可以通过调用控制器接口管理流的创建、删除和缩放以及进行事务管理:启动事务、创建事务、跟踪事务状态;
  2. 所有的数据对读来说都是透明的,客户端的读写操作直接与段存储(数据面)进行交互,而不通过控制器;
  3. 段存储里有缓存组件保证了读写的高性能,热点数据放在bookkeeper里作为一层存储;
  4. 数据老化后会自动流转到长期存储(例如:对象存储系统,文件存储系统,HDFS等)里以便降低存储成本;

关键子功能 - 零接触缩放

零接触缩放:段的动态拆分与合并

段的拆分与合并

如上图所示,1)拆分:在t1时刻系统负载加大,段0被拆分成段1和段2,同时段0封装不再写入;t2时刻系统负载继续加大,段2被拆分成段3与段4,同时段2被封装不再写入;t3时刻系统负载又继续加大,段1被拆分成段5和段6,同时段1被封装不再写入;2)合并:t4时刻系统负载降低,段6与段3被合并成段7,同时段6与段3被封装不再写入。而且所有的这些行为都是Pravega里自动完成的无需人工干预。

零接触缩放:写并行 - 与Kafka比较

写并行

当并行写入的时候:

  1. 在Pravega里流段的数量会根据负载和服务质量目标而动态变化,并且段的拆分与合并都是自动进行的无需人工干预,同时拆分或合并流段是,写客户端的配置是静态不变的;

  2. 在Kafka里主题分区数(写并行性)是静态的,添加或删除分区时需要手动配置服务并且当分区数更改时,必须手动更新生产者配置。

零接触缩放:读并行 - 与Kafka比较

读并行

并行读取时:

  1. 在Pravega里,当拆分或者合并流段时,读客户端通过流协议获得通知从而使得读并行与流段缩放保持同步;
  2. 在Kafka里,当分区数更改时,必须手动更改使用者配置。

关键子功能 - 智能工作负载分配

智能工作负载分配 - 与Kafka比较

智能工作负载分配

在Pravega里,热点段会自动拆分,子段在整个集群中重新分配缓解热点,同时最大限度地利用集群的可用IOPS能力;而在Kafka里没有减轻“热点”分区的机制,其强制部署并且过度配置资源以获得处理其“峰值负载”的能力。

关键子功能 - I/O路径隔离

I/O路径隔离

流存储的基础数据结构为仅附加写入的日志结构。考虑到高吞吐量,Pravega支持追赶读,同时为了保证低时延,Pravega还支持尾部读取以及尾部写入,从而进行了IO路径的隔离。

关键子功能 - 事务

智能工作负载分配

Pravega提供了事务功能,事务是写客户端可以“批处理”一堆事件并将它们作为一个处理单元原子性地提交到流中。这一堆事件要么所有都处理成功,要么所有都处理失败。在提交事务之前,发布到事务中的事件永远不会被读客户端看到。如上图所示,第一步,先将一堆事件封装在一个事务里;第二步,提交这个事务。这个事务里所有的事件要么全部都处理成功要么全部都处理失败。

总结

本文分析了物联网场景下的数据存储商业现状以及技术现状,为平衡商业成本与技术成本推出了分布式流存储系统Pravega,同时本文还介绍了流存储的特殊需求点以及与Kafka做了简要对比,此外还介绍了一些Pravega的关键架构以及一些关键特性。有关Pravega的更多详细信息,请参阅官方网站。另作者能力有限,如有不足之处欢迎留言批评指正。

任务和算子链

对于分布式执行,Flink将算子子任务链接到任务中。每个任务由一个线程执行。将算子链接到任务中是一项有用的优化:它可以减少线程到线程切换和缓冲的开销,并在降低延迟的同时提高整体吞吐量。可以配置链接行为; 有关详细信息,请参阅链接文档。

下图中的示例数据流由五个子任务执行,因此具有五个并行线程。

算子链接到任务

作业管理器,任务管理器,客户端

Flink运行时包含两种类型的进程:

  • JobManagers(也称为主作业)协调分布式执行。他们调度任务,协调检查点,协调故障恢复等。

总是至少有一个Job Manager。高可用性配置将具有多个JobManagers,其中一个始终是领导者,其他人则是备用者。

  • TaskManagers(也叫工作者)执行数据流的任务(或者更具体地说,子任务),并且缓冲和交换数据流。

必须至少有一个TaskManager。

JobManagers和TaskManagers可以通过多种方式启动:直接作为独立集群、在容器中、或由YARN或Mesos等资源框架管理。TaskManagers连接到JobManagers,宣布它们自己是可用,并被分配工作。

客户端不是运行时和程序执行的一部分,而是被用来准备和发送的数据流的JobManager。之后,客户端可以断开连接或保持连接以接收进度报告。客户端既可以作为触发执行的Java / Scala程序的一部分运行,也可以在命令行进程中运行./bin/flink run …。

执行Flink数据流所涉及的过程

任务槽和资源

每个worker(TaskManager)都是一个JVM进程,可以在不同的线程中执行一个或多个子任务。为了控制worker接受的任务数量,worker有所谓的任务槽(至少一个)。

每个任务槽代表TaskManager的固定资源子集。例如,具有三个插槽的TaskManager将其托管内存的1/3专用于每个插槽。对资源进行分隔意味着子任务不会与来自其他作业的子任务竞争托管内存,而是具有一定数量的保留托管内存。请注意,此处不会发生CPU隔离; 当前插槽只分离任务的托管内存。

通过调整任务槽的数量,用户可以定义子任务如何相互隔离。每个TaskManager有一个插槽意味着每个任务组在一个单独的JVM中运行(例如,可以在一个单独的容器中启动)。拥有多个插槽意味着更多子任务共享同一个JVM。同一JVM中的任务共享TCP连接(通过多路复用)和心跳消息。它们还可以共享数据集和数据结构,从而减少每任务开销。

具有任务槽和任务的TaskManager

默认情况下,Flink允许子任务共享插槽,即使它们是不同任务的子任务,只要它们来自同一个作业。结果是一个槽可以容纳整个作业的管道。允许此插槽共享有两个主要好处:

Flink集群需要与作业中使用的最高并行度一样多的任务槽。无需计算程序总共包含多少任务(具有不同的并行性)。

更容易获得更好的资源利用率。如果没有插槽共享,非密集型源/ map()子任务将阻止与资源密集型窗口子任务一样多的资源。通过插槽共享,将示例中的基本并行性从2增加到6可以充分利用插槽资源,同时确保繁重的子任务在TaskManagers之间公平分配。

具有共享任务槽的TaskManagers

API还包括可用于防止不期望的插槽共享的资源组机制。

根据经验,一个好的默认任务槽数就是CPU核心数。使用超线程,每个插槽然后需要2个或更多硬件线程上下文。

状态后端

存储键/值索引的确切数据结构取决于所选的状态后端。一个状态后端将数据存储在内存中的哈希映射中,另一个状态后端使用RocksDB作为键/值存储。除了定义保存状态的数据结构之外,状态后端还实现逻辑以获取键/值状态的时间点快照,并将该快照存储为检查点的一部分逻辑。

检查点和快照

保存点

用Data Stream API编写的程序可以从保存点恢复执行。保存点允许更新程序和Flink群集,而不会丢失任何状态。

保存点是手动触发的检查点,它将程序的快照写入状态后端。他们依赖于常规的检查点机制。在执行期间,程序会周期性地在工作节点上创建快照并生成检查点。对于恢复,仅需要最后完成的检查点,并且一旦新的检查点完成,就可以安全地丢弃旧的检查点。

保存点与这些定期检查点类似,不同之处在于它们由用户触发,并且在完成较新的检查点时不会自动过期。可以从命令行或通过REST API取消作业时创建保存点。