【大数据专场 学习资料二】第四届字节跳动青训营

第四届字节跳动青训营讲师非常用心给大家整理了课前、中、后的学习内容,同学们自我评估,选择性查漏补缺,便于大家更好的跟上讲师们的节奏,祝大家学习愉快,多多提问交流~

第一节:流式计算中的 Window 计算

概述

本课程主要分为四个部分:

  1. 概述流式计算跟批计算,以及实时数仓和离线数仓的区别;引出流式计算中的window计算定义以及挑战
  1. 介绍实时计算中的Watermark概念,以及如何产生、传递,还有一些典型的生产实践中遇到的问题
  1. 介绍三种最基本的window类型,以及他们的实现原理;同时会结合业务场景介绍一些高级优化的功能和原理
  1. 结合两个真实业务场景的需求,讲解window是如何解决实际生产问题的

课前部分主要罗列课程中涉及到的概念,方便对于流式计算或者Flink不熟悉的同学提前查询和学习;课中部分会将课程的关键思路做一个整理,帮助同学们提前了解课程节奏,更容易跟上课程的节奏;课后是一些小的思考问题,帮助同学们在课后梳理本课程的重点内容。

本课程是Flink相关课程的最后一节课,前面会有《流/批/OLAP 一体的 Flink 引擎介绍》和《Exactly Once 语义在 Flink 中的实现》两节课程,都会介绍Flink相关的架构、原理、使用等各方面的概念,所以本节课程也会假设学员对于Flink已经有了一些基础的理解。

课前 (必须)

Flink 运行时角色:

Task模型

Subtask概念

Operator概念

Shuffle方式

Checkpoint & State:

Checkpoint

Barrier

Keyed state

Memory/FileSystem/Rocksdb statebackend

\

SQL :

Aggregate

UDAF (User defined aggregate function)

TVF (Table valued functions)

Window Aggregate function (TUMBLE/HOP/SESSION)

Watermark:

时间属性:处理时间,事件时间

Watermark generator

watermark传递

Idle source

开源项目:

Flink

Calcite

课中

整个课程将围绕 Flink 的 Window 机制来展开,会涉及到一些相关的核心概念,比如 Watermark,也会做一个较为细致的讲解。

概述

这部分内容主要会从离线数仓到实时数仓的对比开始,从传统的大数据计算到实时计算是如何演变和过度的,以及实时计算中的核心挑战,最终引出实时计算的 Window 计算以及支撑实时计算的核心概念:Watermark。

这里面会涉及到一些核心概念,比如:

  • T+1 离线计算模型
  • 事件时间
  • Exactly-Once/At-Least-Once

这部分内容不会做技术的细节展开,主要是让学员对于流式计算、Window、Watermark等有一个基本的认识,对于接下来的部分做一个铺垫。

Watermark

这部分会对 Watermark 的概念、产生、传递以及典型的一些生产实践中的遇到的问题进行依次讲解。

Watermark定义:当前系统认为的事件时间所在的真实时间。

Watermark产生:一般是从数据的事件时间来产生,产生策略可以灵活多样,最常见的包括使用当前事件时间的时间减去一个固定的delay,来表示可以可以容忍多长时间的乱序。

Watermark传递:这个类似于上节课中介绍的Checkpoint的制作过程,传递就类似于Checkpoint的barrier,上下游task之间有数据传输关系的,上游就会将watermark传递给下游;下游收到多个上游传递过来的watermark后,默认会取其中最小值来作为自身的watermark,同时它也会将自己watermark传递给它的下游。经过整个传递过程,最终系统中每一个计算单元就都会实时的知道自身当前的watermark是多少。

后面会介绍典型的watermark在生产实践中经常遇到的几个问题:

  • 怎么观察一个任务中的watermark是多少,是否是正常的

    • 一般通过Flink Web UI上的信息来观察当前任务的watermark情况
    • 这个问题是生产实践中最容易遇到的问题,大家在开发事件时间的窗口任务的时候,经常会忘记了设置watermark,或者数据太少,watermark没有及时的更新,导致窗口一直不能触发。
  • Per-partition / Per-subtask 生成watermark的优缺点

    • 在Flink里早期都是per-subtask的方式进行watermark的生成,这种方式比较简单。但是如果每个source task如果有消费多个partition的情况的话,那多个partition之间的数据可能会因为消费的速度不同而最终导致数据的乱序程度增加。
    • 后期(上面图中)就逐步的变成了per-partition的方式来产生watermark,来避免上面的问题。
  • 如果有部分partition/subtask会断流,应该如何处理

    • 数据断流是很常见的问题,有时候是业务数据本身就有这种特点,比如白天有数据,晚上没有数据。在这种情况下,watermark默认是不会更新的,因为它要取上游subtask发来的watermark中的最小值。此时我们可以用一种IDLE状态来标记这种subtask,被标记为这种状态的subtask,我们在计算watermark的时候,可以把它先排除在外。这样就可以保证有部分partition断流的时候,watermark仍然可以继续更新。
  • 算子对于时间晚于watermark的数据的处理

    • 对于迟到数据,不同的算子对于这种情况的处理可以有不同的实现(主要是根据算子本身的语义来决定的)
    • 比如window对于迟到的数据,默认就是丢弃;比如双流join,对于迟到数据,可以认为是无法与之前正常数据join上。

本节课中也会涉及到一些基础的概念(这些概念在前面两节课中应该已经进行了讲解),比如:

  • Task
  • Subtask
  • Operator
  • Checkpoint
  • Barrier

Window

这部分会分成两部分来讲解:

  1. window的基本概念、分类、以及三种最常见的window的功能;同时也会讲解使用window的时候的一些典型的问题;
  1. window中涉及到的一些高级的优化及其实现原理。

Window 基本功能

TUMBLE Window (滚动窗口)

这是最常见的窗口类型,就是根据数据的时间(可以是处理时间,也可以是事件时间)划分到它所属的窗口中windowStart = timestamp - timestamp % windowSize,这条数据所属的window就是[windowStart, windowStart + windowSize)

在我们使用window的过程中,最容易产生的一个疑问是,window的划分是subtask级别的,还是key级别的。这里大家要记住,Flink 中的窗口划分是key级别的。 比如下方的图中,有三个key,那每个key的窗口都是单独的。所以整个图中,一种存在14个窗口。

窗口的触发,是时间大于等于window end的时候,触发对应的window的输出(计算有可能提前就增量计算好了),目前的实现是给每个window都注册一个timer,通过处理时间或者事件时间的timer来触发window的输出。

HOP Window (滑动窗口)

了解了上面的TUMBLE窗口的基本原理后,HOP窗口就容易理解了。上面的TUMBLE窗口是每条数据只会落在一个窗口中。在HOP窗口中,每条数据是可能会属于多个窗口的(具体属于多少,取决于窗口定义的大小和滑动),比如下图中假设滑动是1h的话,那窗口大小就是2h,这种情况每条数据会属于两个窗口。除了这一点之外,其它的基本跟HOP窗口是类似的,比如也是key级别划分窗口,也是靠timer进行窗口触发输出。

\

SESSION Window (会话窗口)

会话窗口跟上面两种窗口区别比较大,上面两个窗口的划分,都是根据当前数据的时间就可以直接确定它所属的窗口。会话窗口的话,是一个动态merge的过程。一般会设置一个会话的最大的gap,比如10min。

那某个key下面来第一条数据的时候,它的window就是 [event_time, event_time + gap),当这个key后面来了另一条数据的时候,它会立即产生一个窗口,如果这个窗口跟之前的窗口有overlap的话,则会将两个窗口进行一个merge,变成一个更大的窗口,此时需要将之前定义的timer取消,再注册一个新的timer。

所以会话窗口要求所有的聚合函数都必须有实现merge。

迟到数据处理

根据上面说到的watermark原理,watermark驱动某个窗口触发输出之后,这个窗口如果后面又来了数据,那这种情况就属于是迟到的数据了。(注意,不是数据的时间晚于watermark就算是迟到,而是它所属的窗口已经被触发了,才算迟到)。

对于迟到的数据,我们现在有两种处理方式:

  1. 使用side output方式,把迟到的数据转变成一个单独的流,再由用户自己来决定如何处理这部分数据
  1. 直接drop掉

注意:side output只有在DataStream的窗口中才可以用,在SQL中目前还没有这种语义,所以暂时只有drop这一个策略。

增量计算 VS 全量计算

这个问题也是使用窗口的时候最典型的问题之一。先定义一下:

  • 增量计算:每条数据到来后,直接参与计算(但是还不需要输出结果)
  • 全量计算:每条数据到来后,先放到一个buffer中,这个buffer会存储到状态里,直到窗口触发输出的时候,才把所有数据拿出来统一进行计算

在SQL里面,主要是窗口聚合,所以都是可以增量计算的,也就是每条数据来了之后都可以直接进行计算,而不用把数据都存储起来。举个例子,比如要做sum计算,那每来一条数据,就直接把新的数据加到之前的sum值上即可,这样我们就只需要存储一个sum值的状态,而不需要存储所有buffer的数据,状态量会小很多。

DataStream里面要用增量计算的话,需要用reduce/aggregate等方法,就可以用到增量计算。如果用的是process接口,这种就属于是全量计算。

EMIT触发

上面讲到,正常的窗口都是窗口结束的时候才会进行输出,比如一个1天的窗口,只有到每天结束的时候,窗口的结果才会输出。这种情况下就失去了实时计算的意义了。

那么EMIT触发就是在这种情况下,可以提前把窗口内容输出出来的一种机制。比如我们可以配置一个1天的窗口,每隔5s输出一次它的最新结果,那这样下游就可以更快的获取到窗口计算的结果了。

这个功能只在SQL中,如果是在DataStream中需要完成类似的功能,需要自己定义一些trigger来做。

上节课中,有讲到retract机制,这里需要提一下,这种emit的场景就是一个典型的retract的场景,发送的结果类似于+[1], -[1], +[2], -[2], +[4]这样子。这样才能保证window的输出的最终结果是符合语义的。

Window Offset

按照上面提到的,滚动窗口的计算方式是:windowStart = timestamp - timestamp % windowSize [windowStart, windowStart + windowSize),这个时间戳是按照unix timestamp来算的。比如我们要用一个一周的窗口,想要的是从周一开始,到周日结束,但是按照上面这种方式计算出来的窗口的话,就是从周四开始的(因为1970年1月1日是周四)。

那么window offset的功能就是可以在计算窗口的时候,可以让窗口有一个偏移。所以最终计算window的公式就变成了:windowStart = timestamp - (timestamp - offset + windowSize) % windowSize

DataStream原生就是支持offset的,但是SQL里并不支持,字节内部版本扩展支持了SQL的window offset功能。

Window 高级优化

以下说的所有的高级优化,都只限于在SQL中的window中才有。在DataStream中,用户需要自己通过代码来实现类似的能力。

Mini-batch

一般来讲,Flink的状态比较大一些都推荐使用rocksdb statebackend,这种情况下,每次的状态访问就都需要做一次序列化和反序列化,这种开销还是挺大的。为了降低这种开销,我们可以通过降低状态访问频率的方式来解决,这就是mini-batch最主要解决的问题:即赞一小批数据再进行计算,这批数据每个key的state访问只有一次,这样在单个key的数据比较集中的情况下,对于状态访问可以有效的降低频率,最终提升性能。

这个优化主要是适用于没有窗口的聚合场景,字节内部也扩展了window来支持mini-batch,在某些场景下的测试结果可以节省20-30%的CPU开销。

mini-batch看似简单,实际上设计非常巧妙。假设用最简单的方式实现,那就是每个算子内部自己进行攒一个小的batch,这样的话,如果上下游串联的算子比较多,任务整体的延迟就不是很容易控制。所以真正的mini-batch实现,是复用了底层的watermark传输机制,通过watermark事件来作为mini-batch划分的依据,这样整个任务中不管串联的多少个算子,整个任务的延迟都是一样的,就是用户配置的delay时间。

下面这张图展示的是普通的聚合算子的mini-batch原理,window的mini-batch原理是一样的。

Local-global

local-global优化是分布式系统中典型的优化,主要是可以降低数据shuffle的量,同时也可以缓解数据的倾斜。

所谓的local-global,就是将原本的聚合划分成两阶段,第一阶段先做一个local的聚合,这个阶段不需要数据shuffle,是直接跟在上游算子之后进行处理的;第二个阶段是要对第一个阶段的结果做一个merge(还记得上面说的session window的merge么,这里要求是一样的。如果存在没有实现merge的聚合函数,那么这个优化就不会生效)。

如下图所示,比如是要对数据做一个sum,同样颜色的数据表示相同的group by的key,这样我们可以再local agg阶段对他们做一个预聚合;然后到了global阶段数据倾斜就消除了。

Distinct状态复用

对于distinct的优化,一般批里面的引擎都是通过把它优化成aggregate的方式来处理,但是在流式window中,我们不能直接这样进行优化,要不然算子就变成会下发retract的数据了。所以在流式中,对于count distinct这种情况,我们是需要保存所有数据是否出现过这样子的一个映射。

在SQL中,我们有一种方式可以在聚合函数上添加一些filter,如下面的SQL所示:

像这种情况,我们会对同一个字段用不同的filter来进行count distinct的计算。如果每个指标都单独用一个map来记录每条数据是否出现过,那状态量是很大的。

我们可以把相同字段的distinct计算用一个map的key来存储,在map的value中,用一个bit vector来实现就可以把各个状态复用到一起了。比如一个bigint有64位,可以表示同一个字段的64个filter,这样整体状态量就可以节省很多了。

滑动窗口pane复用

滑动窗口如上面所述,一条数据可能会属于多个window。所以这种情况下同一个key下的window数量可能会比较多,比如3个小时的窗口,1小时的滑动的话,每条数据到来会直接对着3个窗口进行计算和更新。这样对于状态访问频率是比较高的,而且计算量也会增加很多。

优化方法就是,将窗口的状态划分成更小粒度的pane,比如上面3小时窗口、1小时滑动的情况,可以把pane设置为1h,这样每来一条数据,我们就只更新这条数据对应的pane的结果就可以了。当窗口需要输出结果的时候,只需要将这个窗口对应的pane的结果merge起来就可以了。

注意:这里也是需要所有聚合函数都有merge的实现的

案例分析

案例一:计算实时抖音DAU曲线

DAU(Daily Active User):指的是每天的去重活跃用户数

输出:每个5s更新一下当前的DAU数值,最终获得一天内的DAU变化曲线

要求:通过上面课程中学到的窗口的功能以及相关的优化,开发一个Flink SQL任务,使得可以高效的计算出来上面要求的实时结果。

(具体内容会在课上展开)

案例二:计算大数据任务的资源使用

问题描述:大数据任务(特指离线任务)运行时通常会有多个container启动并运行,每个container在运行结束的时候,YARN会负责将它的资源使用(CPU、内存)情况上报。一般大数据任务运行时间从几分钟到几小时不等。

需求:根据YARN上报的各个container的信息,在任务结束的时候,尽快的计算出一个任务运行所消耗的总的资源。假设前后两个container结束时间差不超过10min。

(具体内容会在课上展开)

课后

  1. 复习实时计算产生的背景,与离线计算最主要的区别,以及流式窗口计算的最大挑战
  1. watermark的产生、传递、使用原理,以及在各种断流或者上游出现问题的情况下应该如何处理
  1. 三种基本的window的功能和原理
  1. window的基本功能扩展有哪些
  1. 四种高级的window的优化分别是为了解决什么问题,又是什么原理

参考资料

  1. Streaming 101

www.oreilly.com/radar/the-w…

  1. Streaming 102

www.oreilly.com/radar/the-w…

  1. Flink 官方文档

flink.apache.org/

  1. Flink SQL 开发文档

nightlies.apache.org/flink/flink…

  1. DataStream Window 官方文档

nightlies.apache.org/flink/flink…

  1. SQL Window 官方文档

nightlies.apache.org/flink/flink…

  1. DataStream 中对于 Watermark 的介绍

nightlies.apache.org/flink/flink…

  1. SQL 中的时间属性的文档介绍

nightlies.apache.org/flink/flink…

第二节: Spark 原理与实践

概述

本课程主要有以下几个目标:

  1. 了解大数据处理常见的场景链路
  1. 了解Spark技术栈,包括SparkCore中的RDD/调度/Shuffle/内存管理等概念机制,以及SQL在Spark引擎中执行的详细流程
  1. 了解目前业界主要遇到的挑战以及解决方案

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的重点思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

课前

大数据处理引擎Spark介绍

Spark生态组件:

  • Spark Core:Spark核心组件,它实现了Spark的基本功能,包含任务调度、内存管理、错误恢复、与存储系统交互等模块。
  • Spark SQL:用来操作结构化数据的核心组件,通过Spark SQL可以直接查询Hive、HBase等多种外部数据源中的数据。
  • Spark Structured Streaming:Spark提供的流式计算框架,支持高吞吐量、可容错处理的实时流式数据处理。
  • MLlib:Spark提供的关于机器学习功能的算法程序库,包括分类、回归、聚类、协同过滤算法等,还提供了模型评估、数据导入等额外的功能。
  • GraphX:Spark提供的分布式图处理框架,拥有对图计算和图挖掘算法的API接口以及丰富的功能和运算符。
  • 独立调度器、Yarn、Mesos、Kubernetes:Spark框架可以高效地在一个到数千个节点之间伸缩计算,集群管理器则主要负责各个节点的资源管理工作,为了实现这样的要求,同时获得最大灵活性,Spark支持在各种集群管理器(Cluster Manager)上运行。

Spark 运行架构和工作原理:

  • Application(应用):Spark上运行的应用。Application中包含了一个驱动器(Driver)进程和集群上的多个执行器(Executor)进程。
  • Driver Program(驱动器):运行main()方法并创建SparkContext的进程。
  • Cluster Manager(集群管理器):用于在集群上申请资源的外部服务(如:独立部署的集群管理器、Mesos或者Yarn)。
  • Worker Node(工作节点):集群上运行应用程序代码的任意一个节点。
  • Executor(执行器):在集群工作节点上为某个应用启动的工作进程,该进程负责运行计算任务,并为应用程序存储数据。
  • Task(任务):执行器的工作单元。
  • Job(作业):一个并行计算作业,由一组任务(Task)组成,并由Spark的行动(Action)算子(如:save、collect)触发启动。
  • Stage(阶段):每个Job可以划分为更小的Task集合,每组任务被称为Stage。

Spark目前支持几个集群管理器:

  • Standalone :Spark 附带的简单集群管理器,可以轻松设置集群。
  • Apache Mesos:通用集群管理器,也可以运行 Hadoop MapReduce 和服务应用程序。(已弃用)
  • Hadoop YARN: Hadoop 2 和 3 中的资源管理器。
  • Kubernetes:用于自动部署、扩展和管理容器化应用程序的开源系统。

SparkCore

RDD(Resilient Distributed Dataset):弹性分布式数据集,是一个容错的、并行的数据结构

RDD算子:对任何函数进行某一项操作都可以认为是一个算子,RDD算子是RDD的成员函数

Transform(转换)算子: 根据已有RDD创建新的RDD

Action(动作)算子: 将在数据集上运行计算后的数值返回到驱动程序,从而触发真正的计算

DAG(Directed Acyclic Graph): 有向无环图,Spark中的RDD通过一系列的转换算子操作和行动算子操作形成了一个DAG

DAGScheduler:将作业的DAG划分成不同的Stage,每个Stage都是TaskSet任务集合,并以TaskSet为单位提交给TaskScheduler。

TaskScheduler:通过TaskSetManager管理Task,并通过集群中的资源管理器(Standalone模式下是Master,Yarn模式下是ResourceManager)把Task发给集群中Worker的Executor

Shuffle:Spark中数据重分发的一种机制。

SparkSQL

DataFrame: 是一种以RDD为基础的分布式数据集, 被称为SchemaRDD

Catalyst:SparkSQL核心模块,主要是对执行过程中的执行计划进行处理和优化

DataSource:SparkSQL支持通过 DataFrame 接口对各种数据源进行操作。

Adaptive Query Execution:自适应查询执行

Runtime Filter:运行时过滤

Codegen:生成程序代码的技术或系统,可以在运行时环境中独立于生成器系统使用

SparkSql执行过程:

  • Unresolved Logical Plan:未解析的逻辑计划,仅仅是数据结构,不包含任何数据信息。
  • Logical Plan:解析后的逻辑计划,节点中绑定了各种优化信息。
  • Optimized Logical Plan:优化后的逻辑计划
  • Physical Plans:物理计划列表
  • Selected Physical Plan 从列表中按照一定的策略选取最优的物理计划

业界挑战与实践

向量化(vectorization):将循环转换为向量操作的编译器优化

代码生成(Codegen:Code generation):生成程序代码的技术或系统,可以在运行时环境中独立于生成器系统使用

课中

Spark介绍

目标:了解常见的大数据处理pipeline。了解spark技术栈。通过提交一个基本的Spark程序开始Spark学习之路。

Spark运行架构和工作原理

Spark应用在集群上运行时,包括了多个独立的进程,这些进程之间通过驱动程序(Driver Program)中的SparkContext对象进行协调,SparkContext对象能够与多种集群资源管理器(Cluster Manager)通信,一旦与集群资源管理器连接,Spark会为该应用在各个集群节点上申请执行器(Executor),用于执行计算任务和存储数据。Spark将应用程序代码发送给所申请到的执行器,SparkContext对象将分割出的任务(Task)发送给各个执行器去运行。

需要注意的是

  • 每个Spark application都有其对应的多个executor进程。Executor进程在整个应用程序生命周期内,都保持运行状态,并以多线程方式执行任务。这样做的好处是,Executor进程可以隔离每个Spark应用。从调度角度来看,每个driver可以独立调度本应用程序的内部任务。从executor角度来看,不同Spark应用对应的任务将会在不同的JVM中运行。然而这样的架构也有缺点,多个Spark应用程序之间无法共享数据,除非把数据写到外部存储结构中。
  • Spark对底层的集群管理器一无所知,只要Spark能够申请到executor进程,能与之通信即可。这种实现方式可以使Spark比较容易的在多种集群管理器上运行,例如Mesos、Yarn、Kubernetes。
  • Driver Program在整个生命周期内必须监听并接受其对应的各个executor的连接请求,因此driver program必须能够被所有worker节点访问到。
  • 因为集群上的任务是由driver来调度的,driver应该和worker节点距离近一些,最好在同一个本地局域网中,如果需要远程对集群发起请求,最好还是在driver节点上启动RPC服务响应这些远程请求,同时把driver本身放在离集群Worker节点比较近的机器上。

SparkCore:

目标:认识spark的核心概念RDD,RDD两种算子处理过程,理解RDD依赖,学习RDD在Spark中的执行流程。了解spark中调度、内存管理机制、shuffle机制。

RDD执行过程

\

划分Stage的整体思路:从后往前推,遇到宽依赖就断开,划分为一个Stage。遇到窄依赖,就将这个RDD加入该Stage中,DAG最后一个阶段会为每个结果的Partition生成一个ResultTask。每个Stage里面的Task数量由最后一个RDD的Partition数量决定,其余的阶段会生成ShuffleMapTask。

当RDD对象创建后,SparkContext会根据RDD对象构建DAG有向无环图,然后将Task提交给DAGScheduler。DAGScheduler根据ShuffleDependency将DAG划分为不同的Stage,为每个Stage生成TaskSet任务集合,并以TaskSet为单位提交给TaskScheduler。TaskScheduler根据调度算法(FIFO/FAIR)对多个TaskSet进行调度,并通过集群中的资源管理器(Standalone模式下是Master,Yarn模式下是ResourceManager)把Task调度(locality)到集群中Worker的Executor,Executor由SchedulerBackend提供。

内存管理

Spark 作为一个基于内存的分布式计算引擎,Spark采用统一内存管理机制。重点在于动态占用机制。

  • 设定基本的存储内存(Storage)和执行内存(Execution)区域,该设定确定了双方各自拥有的空间的范围,UnifiedMemoryManager统一管理Storage/Execution内存
  • 双方的空间都不足时,则存储到硬盘;若己方空间不足而对方空余时,可借用对方的空间
  • 当Storage空闲,Execution可以借用Storage的内存使用,可以减少spill等操作, Execution内存不能被Storage驱逐。Execution内存的空间被Storage内存占用后,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间
  • 当Execution空闲,Storage可以借用Execution内存使用,当Execution需要内存时,可以驱逐被Storage借用的内存,可让对方将占用的部分转存到硬盘,然后"归还"借用的空间

user memory存储用户自定义的数据结构或者spark内部元数据

Reserverd memory:预留内存,防止OOM,

堆内(On-Heap)内存/堆外(Off-Heap)内存:Executor 内运行的并发任务共享 JVM 堆内内存。为了进一步优化内存的使用以及提高 Shuffle 时排序的效率,Spark 可以直接操作系统堆外内存,存储经过序列化的二进制数据。减少不必要的内存开销,以及频繁的 GC 扫描和回收,提升了处理性能。

SparkSQL:

目标:了解SQL执行链路。重点学习核心模块calalyst优化器以及SparkSQL三大重点特性(Codegen/AQE/RuntimeFilter)

SparkSQL执行过程

  • SQL Parse: 将SparkSQL字符串或DataFrame解析为一个抽象语法树/AST,即Unresolved Logical Plan
  • Analysis:遍历整个AST,并对AST上的每个节点进行数据类型的绑定以及函数绑定,然后根据元数据信息Catalog对数据表中的字段进行解析。 利用Catalog信息将Unresolved Logical Plan解析成Analyzed Logical plan
  • Logical Optimization:该模块是Catalyst的核心,主要分为RBO和CBO两种优化策略,其中RBO是基于规则优化,CBO是基于代价优化。 利用一些规则将Analyzed Logical plan解析成Optimized Logic plan
  • Physical Planning: Logical plan是不能被spark执行的,这个过程是把Logic plan转换为多个Physical plans
  • CostModel: 主要根据过去的性能统计数据,选择最佳的物理执行计划(Selected Physical Plan)。
  • Code Generation: sql逻辑生成Java字节码

影响SparkSQL性能两大技术:

  1. Optimizer:执行计划的优化,目标是找出最优的执行计划
  1. Runtime:运行时优化,目标是在既定的执行计划下尽可能快的执行完毕。

Catalyst优化

  1. Rule Based Optimizer(RBO): 基于规则优化,对语法树进行一次遍历,模式匹配能够满足特定规则的节点,再进行相应的等价转换。
  1. Cost Based Optimizer(CBO): 基于代价优化,根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能执行计划的代价,从中选用COST最低的执行方案,作为实际运行方案。CBO依赖数据库对象的统计信息,统计信息的准确与否会影响CBO做出最优的选择。

AQE

AQE对于整体的Spark SQL的执行过程做了相应的调整和优化,它最大的亮点是可以根据已经完成的计划结点真实且精确的执行统计结果来不停的反馈并重新优化剩下的执行计划。

AQE框架三种优化场景:

  • 动态合并shuffle分区(Dynamically coalescing shuffle partitions)
  • 动态调整Join策略(Dynamically switching join strategies)
  • 动态优化数据倾斜Join(Dynamically optimizing skew joins)

RuntimeFilter

实现在Catalyst中。动态获取Filter内容做相关优化,当我们将一张大表和一张小表等值连接时,我们可以从小表侧收集一些统计信息,并在执行join前将其用于大表的扫描,进行分区修剪或数据过滤。可以大大提高性能

Runtime优化分两类:

  1. 全局优化:从提升全局资源利用率、消除数据倾斜、降低IO等角度做优化。包括AQE。
  1. 局部优化:提高某个task的执行效率,主要从提高CPU与内存利用率的角度进行优化。依赖Codegen技术。

Codegen

从提高cpu的利用率的角度来进行runtime优化。

  1. Expression级别

表达式常规递归求值语法树。需要做很多类型匹配、虚函数调用、对象创建等额外逻辑,这些overhead远超对表达式求值本身,为了消除这些overhead,Spark Codegen直接拼成求值表达式的java代码并进行即时编译

  1. WholeStage级别

传统的火山模型:SQL经过解析会生成一颗查询树,查询树的每个节点为Operator,火山模型把operator看成迭代器,每个迭代器提供一个next()接口。通过自顶向下的调用 next 接口,数据则自底向上的被拉取处理,火山模型的这种处理方式也称为拉取执行模型,每个Operator 只要关心自己的处理逻辑即可,耦合性低。

火山模型问题:数据以行为单位进行处理,不利于CPU cache 发挥作用;每处理一行需要调用多次next() 函数,而next()为虚函数调用。会有大量类型转换和虚函数调用。虚函数调用会导致CPU分支预测失败,从而导致严重的性能回退

Spark WholestageCodegen:为了消除这些overhead,会为物理计划生成类型确定的java代码。并进行即时编译和执行。

Codegen打破了Stage内部算子间的界限,拼出来跟原来的逻辑保持一致的裸的代码(通常是一个大循环)然后把拼成的代码编译成可执行文件。

业界挑战与实践

目标:了解三种业内Spark面临的问题及解决策略,学习思考问题的产生、寻找解决问题思路。

课后

  1. 大数据的基础链路是?
  1. Spark RDD是如何执行的?如何划分stage?
  1. Spark内存是如何管理的?有什么特别机制?
  1. Spark Shuffle是怎么产生的?基本流程是?
  1. SparkSQL执行流程有哪些步骤?每个步骤的左右是?
  1. Runtime优化的方式都有哪些?
  1. Spark业界面临的问题是如何产生的?都有什么优化方向?

第三节:大数据 Shuffle 原理与实践

概述

本课程主要分为四个部分:

  1. shuffle概述:shuffle是什么,shuffle的基本过程是什么
  1. spark中的shuffle算子:学习使用spark中的会产生shuffle的算子,了解其基本特性
  1. spark中的shuffle过程:spark中shuffle的核心原理和实现细节
  1. Push based shuffle:push shuffle社区的实现方案以及字节自己的实现方案

课前部分主要罗列课程中涉及到的概念。对于不熟悉的概念,同学们可以提前查询预习;课中部分主要罗列每一部分的关键思路,帮助同学们跟上课程的进度;课后部分是一些问题,帮助同学们在课后梳理本课程的重点。

期望通过该课程,各位同学能实现如下目标:

  1. 了解spark中shuffle的发展历史以及主要实现机制。包括如何划分stage、partition分区、spill、combine等。
  1. 了解spark shuffle的底层实现原理以及Push-based shuffle总体架构方案。
  1. 了解shuffle优化,包括实际业务场景下如何避免产生shuffle、减少shuffle数据量、shuffle参数优化等。

课前

  1. 熟悉spark中会产生shuffle的算子:

    1. 重分区
    2. ByKey
    3. Join
  1. 使用spark的RDD算子,完成WordCount功能并运行,并观察作业的stage划分
  1. 使用spark分析CSV文件,并编写相关的sql,其中包含join,并观察作业的stage划分以及sql plan
  1. 熟悉netty的基本用法,尝试了解netty server和client的经典用法

课中

shuffle概述

  • Mapreduce

    • 《MapReduce:Simplified Data Processing on Large Clusters》
  • 经典shuffle过程

    • map阶段
    • shuffle阶段
    • reduce阶段
  • 为什么shuffle如此重要

    • 数据shuffle表示了不同分区数据交换的过程,不同的shuffle策略性能差异较大。目前在各个引擎中shuffle都是优化的重点,在spark框架中,shuffle是支撑spark进行大规模复杂数据处理的基石。

shuffle算子

  • 常见的触发shuffle的算子

    • repartition

      • coalesce、repartition
    • ByKey

      • groupByKey、reduceByKey、aggregateByKey、combineByKey、sortByKeysortBy
    • Join

      • cogroup、join
  • 算子使用例子
val text = sc.textFile("mytextfile.txt")
val counts = text
  .flatMap(line => line.split(" "))
  .map(word => (word,1))
  .reduceByKey(_+_)
counts.collect
  • Shuffle Dependency

    • 创建会产生shuffle的RDD时,RDD会创建Shuffle Dependency来描述Shuffle相关的信息

    • 构造函数

      • A single key-value pair RDD, i.e. RDD[Product2[K, V]],
      • Partitioner (available as partitioner property),
      • Serializer,
      • Optional key ordering (of Scala’s scala.math.Ordering type),
      • Optional Aggregator,
      • mapSideCombine flag which is disabled (i.e. false) by default.
  • Partitioner

    • 用来将record映射到具体的partition的方法

    • 接口

      • numberPartitions
      • getPartition
  • Aggregator

    • 在map侧合并部分record的函数

    • 接口

      • createCombiner:只有一个value的时候初始化的方法
      • mergeValue:合并一个value到Aggregator中
      • mergeCombiners:合并两个Aggregator

shuffle过程

  • spark中的shuffle变迁过程

    • HashShuffle

      • 优点:不需要排序
      • 缺点:打开,创建的文件过多
    • SortShuffle

      • 优点:打开的文件少、支持map-side combine
      • 缺点:需要排序
    • TungstenSortShuffle

      • 优点:更快的排序效率,更高的内存利用效率
      • 缺点:不支持map-side combine
  • Register Shuffle

    • 由action算子触发DAG Scheduler进行shuffle register
    • Shuffle Register会根据不同的条件决定注册不同的ShuffleHandle

  • 三种ShuffleHandle对应了三种不同的ShuffleWriter的实现

    • BypassMergeSortShuffleWriter:HashShuffle
    • UnsafeShuffleWriter:TunstonShuffle
    • SortSHuffleWriter:SortShuffle
  • ShuffleReader网络请求流程

使用netty作为网络框架提供网络服务,并接受reducetask的fetch请求

首先发起openBlocks请求获得streamId,然后再处理stream或者chunk请求

  • ShuffleBlockFetchIterator

    • 区分local和remote节省网络消耗

    • 防止OOM

      • maxBytesInFlight
      • maxReqsInFlight
      • maxBlocksInFlightPerAddress
      • maxReqSizeShuffleToMem
      • maxAttemptsOnNettyOOM
  • External Shuffle Service

为了解决Executor为了服务数据的fetch请求导致无法退出问题,我们在每个节点上部署一个External Shuffle Service,这样产生数据的Executor在不需要继续处理任务时,可以随意退出。

  • shuffle优化

    • 避免shuffle ——使用broadcast替代join

    • //传统的join操作会导致shuffle操作。
      //因为两个RDD中,相同的key都需要通过网络拉取到一个节点上,由一个task进行join操作。
      val rdd3 = rdd1.join(rdd2)
      
      //Broadcast+map的join操作,不会导致shuffle操作。
      //使用Broadcast将一个数据量较小的RDD作为广播变量。
      val rdd2Data = rdd2.collect()
      val rdd2DataBroadcast = sc.broadcast(rdd2Data)
      
      //在rdd1.map算子中,可以从rdd2DataBroadcast中,获取rdd2的所有数据。
      //然后进行遍历,如果发现rdd2中某条数据的key与rdd1的当前数据的key是相同的,那么就判定可以进行join。
      //此时就可以根据自己需要的方式,将rdd1当前数据与rdd2中可以连接的数据,拼接在一起(String或Tuple)。
      val rdd3 = rdd1.map(rdd2DataBroadcast...)
      
      //注意,以上操作,建议仅仅在rdd2的数据量比较少(比如几百M,或者一两G)的情况下使用。
      //因为每个Executor的内存中,都会驻留一份rdd2的全量数据。
      
    • 使用可以map-side预聚合的算子

    • Shuffle 参数优化

      • spark.default.parallelism && spark.sql.shuffle.partitions
      • spark.hadoopRDD.ignoreEmptySplits
      • spark.hadoop.mapreduce.input.fileinputformat.split.minsize
      • spark.sql.file.maxPartitionBytes
      • spark.sql.adaptive.enabled && spark.sql.adaptive.shuffle.targetPostShuffleInputSize
      • spark.reducer.maxSizeInFlight
      • spark.reducer.maxReqsInFlight spark.reducer.maxBlocksInFlightPerAddress
    • Shuffle 倾斜优化

      • 什么叫倾斜?有什么危害

      • 解决倾斜方法举例

        • 增大并发度
        • AQE
  • 零拷贝

    • sendfile+DMA gather copy
  • Netty 零拷贝

    • 可堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝。
    • CompositeByteBuf 、 Unpooled.wrappedBuffer、 ByteBuf.slice ,可以合并、包装、切分数组,避免发生内存拷贝
    • Netty 使用 FileRegion 实现文件传输,FileRegion 底层封装了 FileChannel#transferTo() 方法,可以将文件缓冲区的数据直接传输到目标 Channel,避免内核缓冲区和用户态缓冲区之间的数据拷贝

Push Shuffle

  • 上一部分所讲的shuffle过程存在哪些问题?

    • 数据存储在本地磁盘,没有备份
    • IO 并发:大量 RPC 请求(M*R)
    • IO 吞吐:随机读、写放大(3X)
    • GC 频繁,影响 NodeManager
  • 为了优化该问题,有很多公司都做了思路相近的优化,push shuffle

  • Magnet主要流程

主要为边写边push的模式,在原有的shuffle基础上尝试push聚合数据,但并不强制完成,读取时优先读取push聚合的结果,对于没有来得及完成聚合或者聚合失败的情况,则fallback到原模式。

  • Cloud Shuffle Service架构

    • Zookeeper WorkerList [服务发现]
    • CSS Worker [Partitions / Disk | Hdfs]
    • Spark Driver [集成启动 CSS Master]
    • CSS Master [Shuffle 规划 / 统计]
    • CSS ShuffleClient [Write / Read]
    • Spark Executor [Mapper + Reducer]
  • Cloud Shuffle Service 读写流程

  • Cloud Shuffle Service 支持AQE

    • 在聚合文件时主动将文件切分为若干块,当触发AQE时,按照已经切分好的文件块进行拆分。

课后

  1. 自己构造一个会产生shuffle 的spark作业,修改shuffle相关的参数,对比一下不同参数对作业运行的影响
  1. 在spark中shuffle实现的发展过程中,每一次变化都优化了之前哪些缺点,又带来了哪些问题?
  1. Push Shuffle相对比Fetch Shuffle最大的挑战是什么?

参考文献

  1. Apache Spark™ - Unified Engine for large-scale data analytics
  1. Magnet: A scalable and performant shuffle architecture for Apache Spark
  1. MapReduce: Simplified Data Processing on Large Clusters
  1. [SPARK-30602] SPIP: Support push-based shuffle to improve shuffle efficiency - ASF JIRA
  1. GitHub - apache/spark: Apache Spark - A unified analytics engine for large-scale data processing

猜你喜欢

转载自juejin.im/post/7123908203590451207