├── README.md └── docs ├── engine ├── Broadcast.md ├── Dynamic-Resource-Allocation设计的思考.md ├── Flink学习.pdf ├── Flink实战总结.pdf ├── Spark学习笔记.md ├── impala集群搭建.md ├── kubernetes-federation深度解析.md ├── spark-job-submit.md └── spark2-4-0触发的executor内存溢出排查.md ├── learning ├── Google-With-Borg.pdf ├── Raft论文学习.md ├── Snapshots-for-Distributed-Dataflows.pdf ├── Volcano.pdf ├── chandy-lamport-algorithm.pdf ├── massive-parallel-processing.pdf ├── raft.pdf ├── sigmod_structured_streaming.pdf ├── spark设计论文.pdf └── 优秀博文汇总.pdf ├── olap ├── Kylin二次开发——测试环境搭建.md ├── Kylin学习笔记.md ├── kylin-master-slave同步原理及问题排查.md ├── kylin-query原理剖析.md └── okhttp-support-100-continue-for-palo.md └── scheduler ├── Hadoop-Rpc源码分析.md ├── MR任务在Hadoop子系统中状态流转.md ├── Yarn-Federation源码串读.md ├── Yarn架构解析.pdf └── airflow实战总结.md /README.md: -------------------------------------------------------------------------------- 1 | # awesome-big-data 2 | 大数据&&分布式系统学习过程中一些经验总结 3 | 4 | ## 计算/查询/存储引擎相关 5 | 6 | ### RemoteShuffleService(RSS) 7 | - [各大厂RSS实现](https://zhuanlan.zhihu.com/p/462338206) 8 | 9 | ### flink 10 | - [flink实战总结](./docs/engine/Flink实战总结.pdf) 11 | - [flink学习总结](./docs/engine/Flink学习.pdf) 12 | 13 | ### spark 14 | - [spark实战总结](./docs/engine/Spark学习笔记.md) 15 | - [【spark-tips】spark2-4-0触发的executor内存溢出排查.md](./docs/engine/spark2-4-0触发的executor内存溢出排查.md) 16 | - [【Spark源码分析】Dynamic-Resource-Allocation设计的思考](./docs/engine/Dynamic-Resource-Allocation设计的思考.md) 17 | - [【Spark源码分析】Broadcast](./docs/engine/Broadcast.md) 18 | - [【Spark源码分析】Job提交执行过程详解](./docs/engine/spark-job-submit.md) 19 | 20 | ### impala 21 | - [impala集群搭建](./docs/engine/impala集群搭建.md) 22 | 23 | ## 任务&资源调度相关 24 | 25 | - [Airflow 实战总结](./docs/scheduler/airflow实战总结.md) 26 | 27 | ### Hadoop 28 | - [Yarn架构实现解析](./docs/scheduler/Yarn架构解析.pdf) 29 | - [Yarn-Federation源码串读](./docs/scheduler/Yarn-Federation源码串读.md) 30 | - [Hadoop&Yarn Rpc源码剖析](./docs/scheduler/Hadoop-Rpc源码分析.md) 31 | - [MR任务在Hadoop子系统中状态流转](./docs/scheduler/MR任务在Hadoop子系统中状态流转.md) 32 | - [Hadoop Pipes Ping Timeout问题排查](https://zhuanlan.zhihu.com/p/358167020) 33 | - [动态调整MR运行时object property方案](https://zhuanlan.zhihu.com/p/349907241) 34 | 35 | ### kubernetes 36 | - [kubernetes federation深度解析](./docs/engine/kubernetes-federation深度解析.md) 37 | - [kubernetes shared Informer 源码解析](https://zhuanlan.zhihu.com/p/255078405) 38 | 39 | ## OLAP 相关 40 | 41 | ### kylin 42 | - [Kylin学习笔记](./docs/olap/Kylin学习笔记.md) 43 | - [Kylin二次开发——测试环境搭建](./docs/olap/Kylin学习笔记.md) 44 | - [kylin-master-slave同步原理及问题排查.md](./docs/olap/kylin-master-slave同步原理及问题排查.md) 45 | - [kylin-query原理剖析.md](./docs/olap/kylin-query原理剖析.md) 46 | 47 | ### Doris 48 | - [okhttp-support-100-continue-for-palo.md](./docs/olap/okhttp-support-100-continue-for-palo.md) 49 | 50 | ## 分布式系统论文 51 | 52 | - [Howard大佬维护的共识算法知识库](https://github.com/heidihoward/distributed-consensus-reading-list) 53 | 54 | - [Raft](./docs/learning/raft.pdf) 55 | - [Raft学习总结](./docs/learning/Raft论文学习.md) 56 | - Flink分布式数据流的异步快照核心实现 57 | - [chandy-lamport-algorithm](./docs/learning/chandy-lamport-algorithm.pdf) 58 | - [Lightweight Asynchronous Snapshots for Distributed Dataflows](./docs/learning/Snapshots-for-Distributed-Dataflows.pdf) 59 | - Massively Parallel Processing (MPP Engine) 60 | - [Massively Parallel Databases and MapReduce 61 | Systems](./docs/learning/massive-parallel-processing.pdf) 62 | - 火山模型 63 | - [Volcano an Extensible and Parallel Query Evaluation System](./docs/learning/Volcano.pdf) 64 | 65 | - spark论文 66 | - [Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing](./docs/learning/spark设计论文.pdf) 67 | - [Structured Streaming: A Declarative API for Real-Time Applications in Apache Spark](./docs/learning/sigmod_structured_streaming.pdf) 68 | 69 | - [Google Borg](./docs/learning/Google-With-Borg.pdf) 70 | 71 | 72 | ## 优秀博文汇总 73 | - [大数据&&后端优秀博文汇总](./docs/learning/优秀博文汇总.pdf) 74 | 75 | -------------------------------------------------------------------------------- /docs/engine/Broadcast.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 【Spark源码分析】Broadcast 3 | date: 2019-06-02 13:31:16 4 | tags: 5 | - 学习 6 | - 大数据 7 | - Spark 8 | --- 9 | 10 | ## Broadcast 原理 11 | 12 | ### 满足broadcast join的条件源码分析 13 | 14 | - 来看SparkStrategies.scala文件 15 | - Broadcast 策略入口 broadcastSideBySizes 16 | ![](http://imgs.wanhb.cn/spark-broadcast1.png) 17 | 18 | - 可以发现broadcast 左表或者是右表是根据两个策略来控制的:canBuildLeft/canBuildRight, canBroadcast; 19 | 20 | - canBroadcast控制的是数据大小是否符合参数设定 21 | ![](http://imgs.wanhb.cn/spark-broadcast2.png) 22 | 23 | - canBuildLeft/canBuildRight是判断被广播的表是否作为left或right join基表的情况;如果作为基表的话是不能被broadcast的;当然Inner join不用管是不是基表 24 | ![](http://imgs.wanhb.cn/spark-broadcast3.png) 25 | 26 | ![](http://imgs.wanhb.cn/spark-broadcast4.png) 27 | 28 | - 基表不能被广播的原因 29 | - left/right join 之所以基表不能broadcast是因为这样做会破坏left join语义,产生重复的数据(比如广播了n份基表,因为最后都要保留基表的数据,不管有没有匹配上,所以会导致归并的时候有重复的情况) 30 | 31 | - 翻阅其他博客对broadcast的解释,也能发现基表不能被广播的事实 [Spark SQL中Join常用的几种实现](https://www.iteblog.com/archives/2086.html) 32 | 33 | ![](http://imgs.wanhb.cn/spark-broadcast5.png) -------------------------------------------------------------------------------- /docs/engine/Dynamic-Resource-Allocation设计的思考.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 【Spark源码分析】Dynamic Resource Allocation设计的思考 3 | date: 2019-05-26 13:46:21 4 | tags: 5 | - 学习 6 | - 大数据 7 | - Spark 8 | --- 9 | 10 | ## 前言 11 | 12 | 最近在用spark的dynamicAllocation时发现:如果一个executor超过了设置的executorIdleTimeout时间,触发了回收策略,停止executor之后在sparkUI上会显示该executor的状态为Dead的情况 13 | 14 | 这引起了我的疑问,因为凭我自身的经验判断会误以为这个executor是一种**被动退出**的情况;也即是executor进程因为某种原因被nodemanager kill了,导致driver将这个executor状态置为dead并进行一系列的清理工作。而如果是dynamicAllocation的话,我认为是一种**主动退出**的情况,是安全的。spark自身系统设计不应该将这两个概念的状态笼统的用一个Dead来混淆视听 15 | 16 | 本着对真理追求到底的态度,我决定对sparkUI统计数据的来源这块代码逻辑进行梳理,以给自己提出的问题寻求答案 17 | 18 | ## 源码分析 19 | 20 | #### Spark UI Server启动 21 | 22 | - 我们知道启动一个spark application之后相应的也会启动一个sparkUI server,用于实时监控展示 jobs,stages, executors等一些统计信息,那这些统计数据来自哪里呢?spark内部通过LiveListenerBus实现了一种监听订阅的模式,application内部所有的变更状态通过发布变更事件,交由订阅这些变更事件的实现去处理(这里称之为spark listener)。处理完之后的最新状态将反应在sparkUI上。 23 | 24 | ![](http://imgs.wanhb.cn/sparkui-1.jpeg) 25 | 26 | - 从图中我们可以看出DAGSchedule是主要产生各类SparkListenerEvent的起源,SparkListenerEvent通过ListenerBus事件队列,期间定时器匹配将事件匹配到不同的SparkListener实现上去 27 | 28 | #### Executor页面渲染 29 | 30 | - 在SparkUI的初始化方法中可以看到绑定了我们在界面中见到的几个Tab,如Executors,stages,storage等 31 | 32 | ![](http://imgs.wanhb.cn/sparkui-2.png) 33 | 34 | 跟进ExecutorsTab中看具体的页面渲染逻辑 35 | 36 | ![](http://imgs.wanhb.cn/sparkui-3.png) 37 | 38 | 整个代码层次分明,页面渲染包括页面顶部通用的bar以及body里面具体的内容,这里将渲染页面顶部的逻辑模块化了;我们主要看的是executorspage.js这个文件,这里面是获取executor summary数据并渲染的主逻辑。在executorspage.js内部,发现为获取all-executors数据,发送了一个ajax请求 39 | 40 | ![](http://imgs.wanhb.cn/sparkui-4.png) 41 | 42 | 这个allexecutors接口有我们想要的executors数据来源信息。全局搜索这个endpoint,发现在AbstractApplicationResource 声明定义了该接口实现 43 | 44 | ![](http://imgs.wanhb.cn/sparkui-5.png) 45 | 46 | 意外的发现做了一个类似于请求存储的操作,跟进去发现是AppStatusStore 47 | 48 | ![](http://imgs.wanhb.cn/sparkui-6.png) 49 | 50 | 查看类说明,发现这是一个spark 自身kv store的的访问封装实现 51 | 52 | ![](http://imgs.wanhb.cn/sparkui-7.png) 53 | 54 | ![](http://imgs.wanhb.cn/sparkui-8.png) 55 | 56 | 追踪到这里,算是对数据来源钻到了尽头,可以知道最终sparkUI上executors summary数据是存在自身实现的kvstore里的 57 | 58 | - 关于kvstore的由来,可以详细看这个[issule](https://issues.apache.org/jira/browse/SPARK-18085) 。大致的点和思路是:Spark History server在查看某一个application运行记录的时候需要从eventlog里面拿出数据渲染;对于少数几个任务来说,目前的实现没有问题,但是如果管理了大量的application,history server就会变的几乎不可用;于是思路是实现一套存储(基于LevelDB或Inmemory结合) 可供history server读写,能大幅提升其页面加载速度 59 | 60 | - 现在我们需要关注一下executorAdded或者removed事件对kvstore里面的数据处理逻辑,看SparkListener中对executor增减接口的定义,追溯到AppStatusListener实现,这也恰好是改变AppStatusStore的入口 61 | 62 | ![](http://imgs.wanhb.cn/sparkui-9.png) 63 | 64 | 可见当executor被remove的时候只是将状态置为false,并更新了kvstore里面的值,而不是将其删除,所以前端查询的时候如果发现executor状态不是active且没在blacklist里面的话,默认就把状态format称Dead了 65 | 66 | ![](http://imgs.wanhb.cn/sparkui-10.png) 67 | 68 | ![](http://imgs.wanhb.cn/sparkui-11.png) 69 | 70 | #### DynamicAllocation 实现机制 71 | 72 | - 这里再补充一下DynamicAllocation的底层实现分析。回到之前SparkListener里定义的两个事件处理接口:onExecutorAdded,onExecutorRemoved;其实不止AppStatusListener对这两个事件做了处理,还有ExecutorAllocationListener。这个监听器是触发ExecutorAllocationManager增删executors的入口 73 | 74 | ![](http://imgs.wanhb.cn/sparkui-12.png) 75 | 76 | - 可以看出里面都是调用的allocationManger里面的具体实现。在onExecutorAdded的callback处理逻辑中,会对新加入的executor做idle记录(onExecutorIdle中实现),先判断当前executor有没有缓存的blocks,走不同的计算timeout分支。其中**cachedExecutorIdleTimeoutS**默认是**Integer.MAX_VALUE** ,然后将记录存入hash结构()里,方便**ExecutorAllocationManager**在定时任务下一个周期做检查排除过期的executor 77 | 78 | ![](http://imgs.wanhb.cn/sparkui-13.png) 79 | 80 | 检查逻辑如下: 81 | 82 | ![](http://imgs.wanhb.cn/sparkui-14.png) 83 | 84 | 85 | 86 | #### 总结 87 | 88 | - 从源码分析来看,确实主动和被动释放executor,在sparkUI上面对应的executor状态都会变为Dead。对于使用者来说,如果不清楚spark是否开启了dynamic allocation也确实会引起歧义。毕竟Dead总归是一种不好的状态,甚至逼迫着运维同学去分析一波日志。不知道spark以后的版本中是否会增加一个新的状态?比如引入Released之类的状态将主动和被动区分开,我想这样的话用户体验会更好。 89 | -------------------------------------------------------------------------------- /docs/engine/Flink学习.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/engine/Flink学习.pdf -------------------------------------------------------------------------------- /docs/engine/Flink实战总结.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/engine/Flink实战总结.pdf -------------------------------------------------------------------------------- /docs/engine/Spark学习笔记.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Spark实战总结 3 | date: 2018-10-15 00:12:35 4 | tags: 5 | - 学习 6 | - 大数据 7 | - Spark 8 | --- 9 | 10 | ## Spark 基础 11 | 12 | ### Spark 的构成 13 | 14 | - ClusterManager: 在standalone模式中即为,Master主节点,控制整个集群,监控worker。在yarn模式中为资源管理器 15 | - worker :从节点,负责控制计算节点,启动Executro和Driver。在yarn模式中NodeManager,负责计算节点的控制。 16 | - Driver:运行Application的main()函数并且创建SparkContext。 17 | - Executor: 执行器,是为某Application运行在worker node上的一个进程,启动线程池运行任务上,每个Application拥有一组独立的executors 18 | - SparkContext: 整个应用程序的上下文,控制整个应用的生命周期 19 | - RDD:Spark的基本计算单元,一组RDD形成执行的有向无环图RDD Graph(DAG) 20 | - DAG Scheduler: 根据Job构建基于stage的DAG,并且提交stage给TaskScheduler 21 | - TaskScheduler: 可以将提交给它的stage 拆分为更多的task并分发给Executor执行 22 | - SparkEnv: 线程级别的上下文,存储运行时的重要组件的引用 23 | - DStream: 是一个RDD的序列,由若干RDD组成。在一个batchInterval中,会产生一个RDD,产生的数据统一塞入到这个RDD中,采用内存+磁盘的模式,尽可能放到内存中,当数据量太大时会spill到磁盘中。 24 | 25 | ### Spark 概念释义 26 | 27 | - Transformation返回值还是一个RDD。它使用了链式调用的设计模式,对一个RDD进行计算后,变换成另外一个RDD,然后这个RDD又可以进行另外一次转换。这个过程是分布式的。 Action返回值不是一个RDD。它要么是一个Scala的普通集合,要么是一个值,要么是空,最终或返回到Driver程序,或把RDD写入到文件系统中。 28 | - Action是返回值返回给driver或者存储到文件,是RDD到result的变换,Transformation是RDD到RDD的变换。只有action执行时,rdd才会被计算生成,这是rdd懒惰执行的根本所在。 29 | - Driver是我们提交Spark程序的节点,并且所有的reduce类型的操作都会汇总到Driver节点进行整合。节点之间会将map/reduce等操作函数传递一个独立副本到每一个节点,这些变量也会复制到每台机器上,而节点之间的运算是相互独立的,变量的更新并不会传递回Driver程序。 30 | - Spark中分布式执行的条件 31 | - 只要生成了task,就都是在executor中执行的,在driver中执行不会单独生成task 32 | - 生成task的操作有: spark.read 读取文件,之后对文件做各种map, filter, reduce操作,都是针对partition而言的 33 | 34 | 35 | ## spark 工作机制 36 | 37 | - 一个Job被拆分成若干个Stage,每个Stage执行一些计算,产生一些中间结果。它们的目的是最终生成这个Job的计算结果。而每个Stage是一个task set,包含若干个task。Task是Spark中最小的工作单元,在一个executor上完成一个特定的事情。 38 | - 除非用户指定持久化操作,否则转换过程中产生的中间数据在计算完毕后会被丢弃,即数据是非持久化的。 39 | - 窄依赖:父RDD中的一个分区最多只会被子RDD中的一个分区使用,父RDD中,一个分区内的数据是不能被分割的,必须整个交付给子RDD中的一个分区。 40 | - 宽依赖(Shuffle依赖):父RDD中的分区可能会被多个子RDD分区使用。因为父RDD中一个分区内的数据会被分割,发送给子RDD的所有分区。因此Shuffle依赖也意味着父RDD与子RDD之间存在着Shuffle过程。 41 | 42 | ### Spark作业 43 | 44 | - Application: 用户自定义的Spark程序,用户提交之后,Spark为App分配资源程序转换并执行。 45 | - Driver Program: 运行Application的main函数并且创建SparkContext 46 | - RDD DAG: 当RDD遇到Action算子,将之前的所有算子形成一个有向无环图(DAG)。再在Spark中转化为Job,提交到集群进行执行,一个App中可以包含多个Job 47 | - Job: RDD Graph触发的作业,由spark Action算子触发,在SparkContext中通过runJob方法向spark提交Job 48 | - stage: 每个Job会根据RDD的宽依赖关系被切分很多stage ,每个stage包含一组相同的task,这一组task也叫taskset 49 | - Task: 一个分区对应一个Task,Task 执行RDD中对应stage中所包含的算子,Taksk 被封装好后放入Executor的线程池中执行。 50 | 51 | 52 | ## spark调度原理 53 | 54 | ### 作业调度 55 | 56 | 系统的设计很重要的一环便是资源调度。设计者将资源进行不同粒度的抽象建模,然后将资源统一放入调度器,通过一定的算法进行调度。 57 | 58 | - spark的多种运行模式:Local模式,standalone模式、YARN模式,Mesos模式。 59 | 60 | ### Standalone VS Yarn 61 | 62 | - 角色对比 63 | 64 | ``` 65 | standalone: yarn: 66 | client client 67 | Master ApplicationMaster 68 | Worker ExecutorRunnable 69 | Scheduler YarnClusterScheduler 70 | SchedulerBackend YarnClusterSchedulerBackend 71 | 72 | ``` 73 | - 在yarn中application Master 与Application Driver 运行于同一个JVM进程中 74 | - standalone架构图 75 | 76 | ![standalone](http://ol7zjjc80.bkt.clouddn.com/standalone.png) 77 | 78 | - on yarn架构图 79 | 80 | ![on yarn](http://ol7zjjc80.bkt.clouddn.com/on%20yarn.png) 81 | 82 | 83 | ### application调度 84 | 85 | 用户提交到spark中的作业集合,通过一定的算法对每个按一定次序分配集群中资源的过程。 86 | 87 | - FIFO模式,用户先提交的作业1优先分配需要的资源,之后提交的作业再分配资源,依次类推。 88 | - Mesos: 粗粒度模式和细粒度模式 89 | - YARN模式:独占模式,可以控制应用分配资源 90 | - yarn-cluster: 适用于生产环境。client将用户程序提交到到spark集群中就与spark集群断开联系了,此时client将不会发挥其他任何作用,仅仅负责提交。在此模式下。AM和driver是同一个东西,但官网上给的是driver运行在AM里,可以理解为AM包括了driver的功能就像Driver运行在AM里一样,此时的AM既能够向AM申请资源并进行分配,又能完成driver划分RDD提交task等工作 91 | 92 | - yarn-client: y适用于交互、调试,希望立即看到app的输出。Driver运行在客户端上,先有driver再用AM,此时driver负责RDD生成、task生成和分发,向AM申请资源等 ,AM负责向RM申请资源,其他的都由driver来完成 93 | 94 | ### Job调度 95 | 96 | Job调度就是在application内部的一组Job集合,在application分配到的资源量,通过一定的算法,对每个按一定次序分配Application中资源的过程。 97 | - FIFO模式:先进先出模式 98 | - FAIR模式:spark在多个job之间以轮询的方式给任务进行资源分配,所有的任务拥有大致相当的优先级来共享集群的资源。这就意味着当一个长任务正在执行时,短任务仍可以分配到资源,提交并执行,并且获得不错的响应时间。 99 | 100 | ### tasks延迟调度 101 | 102 | - 数据本地性:尽量的避免数据在网络上的传输,传输任务为主,将任务传输到数据所在的节点 103 | 104 | - 延时调度机制:拥有数据的节点当前正被其他的task占用,如果预测当前节点结束当前任务的时间要比移动数据的时间还要少,那么调度会等待,直到当前节点可用。否则移动数据到资源充足节点,分配任务执行。 105 | 106 | 107 | ## spark transformation和action的算子 108 | 109 | ### transformation 110 | - [ ] map(func) 返回一个新的分布式数据集,由每个原元素经过func函数处理后的新元素组成 111 | - [ ] filter(func) 返回一个新的数据集,由经过func函数处理后返回值为true的原元素组成 112 | - [ ] flatMap(func) 类似于map,但是每一个输入元素,会被映射为0个或多个输出元素,(因此,func函数的返回值是一个seq,而不是单一元素) 113 | - [ ] mapPartitions(func) 类似于map,对RDD的每个分区起作用,在类型为T的RDD上运行时,func的函数类型必须是Iterator[T]=>Iterator[U] 114 | - [ ] mapPartitionsWithIndex(func) 和mapPartitions类似,但func带有一个整数参数表上分区的索引值,在类型为T的RDD上运行时,func的函数参数类型必须是(int,Iterator[T])=>Iterator[U] 115 | - [ ] sample(withReplacement,fraction,seed) 根据给定的随机种子seed,随机抽样出数量为fraction的数据 116 | - [ ] pipe(command,[envVars]) 通过管道的方式对RDD的每个分区使用shell命令进行操作,返回对应的结果 117 | - [ ] union(otherDataSet) 返回一个新的数据集,由原数据集合参数联合而成 118 | - [ ] intersection(otherDataset) 求两个RDD的交集 119 | - [ ] distinct([numtasks]) 返回一个包含源数据集中所有不重复元素的i新数据集 120 | - [ ] groupByKey([numtasks]) 在一个由(K,v)对组成的数据集上调用,返回一个(K,Seq[V])对组成的数据集。默认情况下,输出结果的并行度依赖于父RDD的分区数目,如果想要对key进行聚合的话,使用reduceByKey或者combineByKey会有更好的性能 121 | - [ ] reduceByKey(func,[numTasks]) 在一个(K,V)对的数据集上使用,返回一个(K,V)对的数据集,key相同的值,都被使用指定的reduce函数聚合到一起,reduce任务的个数是可以通过第二个可选参数来配置的 122 | - [ ] sortByKey([ascending],[numTasks]) 在类型为(K,V)的数据集上调用,返回以K为键进行排序的(K,V)对数据集,升序或者降序有boolean型的ascending参数决定 123 | - [ ] join(otherDataset,[numTasks]) 在类型为(K,V)和(K,W)类型的数据集上调用,返回一个(K,(V,W))对,每个key中的所有元素都在一起的数据集 124 | - [ ] cogroup(otherDataset,[numTasks]) 在类型为(K,V)和(K,W)类型的数据集上调用,返回一个数据集,组成元素为(K,Iterable[V],Iterable[W]) tuples 125 | - [ ] cartesian(otherDataset) 笛卡尔积,但在数据集T和U上调用时,返回一个(T,U)对的数据集,所有元素交互进行笛卡尔积 126 | - [ ] coalesce(numPartitions) 对RDD中的分区减少指定的数目,通常在过滤完一个大的数据集之后进行此操作 127 | - [ ] repartition(numpartitions) 将RDD中所有records平均划分到numparitions个partition中 128 | 129 | ### action算子操作 130 | - [ ] reduce(func) 通过函数func聚集数据集中的所有元素,这个函数必须是关联性的,确保可以被正确的并发执行 131 | - [ ] collect() 在driver的程序中,以数组的形式,返回数据集的所有元素,这通常会在使用filter或者其它操作后,返回一个足够小的数据子集再使用 132 | - [ ] count() 返回数据集的元素个数 133 | - [ ] first() 返回数据集的第一个元素(类似于take(1)) 134 | - [ ] take(n) 返回一个数组,由数据集的前n个元素组成。注意此操作目前并非并行执行的,而是driver程序所在机器 135 | - [ ] takeSample(withReplacement,num,seed) 返回一个数组,在数据集中随机采样num个元素组成,可以选择是否用随机数替换不足的部分,seed用于指定的随机数生成器种子 136 | - [ ] saveAsTextFile(path) 将数据集的元素,以textfile的形式保存到本地文件系统hdfs或者任何其他hadoop支持的文件系统,spark将会调用每个元素的toString方法,并将它转换为文件中的一行文本 137 | - [ ] takeOrderd(n,[ordering]) 排序后的limit(n) 138 | - [ ] saveAsSequenceFile(path) 将数据集的元素,以sequencefile的格式保存到指定的目录下,本地系统,hdfs或者任何其他hadoop支持的文件系统,RDD的元素必须由key-value对组成。并都实现了hadoop的writable接口或隐式可以转换为writable 139 | - [ ] saveAsObjectFile(path) 使用java的序列化方法保存到本地文件,可以被sparkContext.objectFile()加载 140 | - [ ] countByKey() 对(K,V)类型的RDD有效,返回一个(K,Int)对的map,表示每一个可以对应的元素个数 141 | - [ ] foreache(func) 在数据集的每一个元素上,运行函数func,t通常用于更新一个累加器变量,或者和外部存储系统做交互 142 | 143 | ## Spark常用存储格式parquet 详解 144 | 145 | - [新型列式存储格式 Parquet 详解 - 后端 - 掘金](https://juejin.im/entry/589932fab123db16a3ace2d1) 146 | - 三个组成部分 147 | - 存储格式(storage format) 148 | - 对象模型转换器(object model converters) 149 | - 对象模型(object models) :简单理解为数据在内存中的表示 150 | ![parquet](http://ol7zjjc80.bkt.clouddn.com/parquet.png) 151 | 152 | - 列式存储 153 | - 把某一列数据连续存储,每一行数据离散存储技术 154 | - 带来的优化 155 | - 查询的时候不需要扫描全部的数据,而只需要读取每次查询涉及的列,这样可以将I/O消耗降低N倍,另外可以保存每一列的统计信息(min、max、sum等),实现部分的谓词下推 156 | - 由于每一列的成员都是同构的,可以针对不同的数据类型使用更高效的数据压缩算法,进一步减小I/O 157 | - 由于每一列的成员的同构性,可以使用更加适合CPU pipeline的编码方式,减小CPU的缓存失效 158 | 159 | - 数据模型 160 | 161 | ``` 162 | message AddressBook { 163 | required string owner; 164 | repeated string ownerPhoneNumbers; 165 | repeated group contacts { 166 | required string name; 167 | optional string phoneNumber; 168 | } 169 | } 170 | 171 | 172 | ``` 173 | - 根被叫做message,有多个field 174 | - 每个field包含三个属性:repetition, type, name 175 | - repetition可以是required(出现1次), optional(出现0次或1次),repeated(出现0次或者多次)。type可以是一个group或者一个primitive类型 176 | - parquet数据类型不需要复杂的Map, List, Set等,而是使用repeated fields 和 groups来表示。例如List和Set可以被表示成一个repeated field,Map可以表示成一个包含有Key-value对的repeated group, 而且key是required的 177 | 178 | - 两个概念 179 | - repetition level :指明该值在路径中哪个repeated field重复 180 | - 针对的是repeted field的 。 181 | - 它能用一个数字告诉我们在路径中的什么重复字段,此值重复了,以此来确定此值的位置 182 | - 我们用深度0表示一个纪录的开头(虚拟的根节点),深度的计算忽略非重复字段(标签不是repeated的字段都不算在深度里) 183 | - definition level:指明该列的路径上多少个可选field被定义了 184 | - 如果一个field是定义的,那么它的所有的父节点都是被定义的 185 | - 从根节点开始遍历,当某一个field的路径上的节点开始是空的时候我们记录下当前的深度作为这个field的Definition Level 186 | - 如果一个field的definition Level等于这个field的最大definition Level就说明这个field是有数据的 187 | - **注意**:是指该路径上有定义的repeated field 和 optional field的个数,不包括required field,因为required field是必须有定义的 188 | 189 | - 谓词下推:通过将一些过滤条件尽可能的在最底层执行可以减少每一层交互的数据量,从而提升性能 190 | - 例如”select count(1) from A Join B on A.id = B.id where A.a > 10 and B.b < 100″SQL查询中 191 | - 在处理Join操作之前需要首先对A和B执行TableScan操作,然后再进行Join,再执行过滤,最后计算聚合函数返回 192 | - 但是如果把过滤条件A.a > 10和B.b < 100分别移到A表的TableScan和B表的TableScan的时候执行,可以大大降低Join操作的输入数据 193 | - 无论是行式存储还是列式存储,都可以在将过滤条件在读取一条记录之后执行以判断该记录是否需要返回给调用者,在Parquet做了更进一步的优化 194 | - 优化的方法时对每一个Row Group的每一个Column Chunk在存储的时候都计算对应的统计信息,包括该Column Chunk的最大值、最小值和空值个数。 195 | - 通过这些统计值和该列的过滤条件可以判断该Row Group是否需要扫描。 196 | - 另外Parquet未来还会增加诸如Bloom Filter和Index等优化数据,更加有效的完成谓词下推 197 | 198 | - 映射下推:它意味着在获取表中原始数据时只需要扫描查询中需要的列 199 | - 在Parquet中原生就支持映射下推,执行查询的时候可以通过Configuration传递需要读取的列的信息 200 | - 这些列必须是Schema的子集,映射每次会扫描一个Row Group的数据,然后一次性得将该Row Group里所有需要的列的Cloumn Chunk都读取到内存中,每次读取一个Row Group的数据能够大大降低随机读的次数,除此之外,Parquet在读取的时候会考虑列是否连续,如果某些需要的列是存储位置是连续的,那么一次读操作就可以把多个列的数据读取到内存 201 | 202 | ## Spark Stremaing 相关 203 | 204 | ### 概念 205 | - BlockRDD:生成由spark.stremaing.blockInterval决定,一个BatchDuration 有几个block就会产生几个 partition 206 | 207 | - 消息消费速率限定 208 | - 开启背压模式:spark.streaming.backpressure.enabled=true 209 | - 此模式如果消息堆积严重,会一次性拉取kafka中所有堆积的消息进行处理。很可能会导致程序崩溃 210 | - 设置每个partition消费速率, spark.streaming.kafka.maxRatePerPartition 211 | - 对应每个batch拉取到的消息为: 212 | 213 | ``` 214 | maxRatePerPartition*partitionNum*batch_interval 215 | 216 | ``` 217 | 218 | ## Spark 优化相关 219 | 220 | ### 优化建议 221 | 222 | - stage 的数量跟一个job中是否要进行shuffle有关,像reduceByKey,groupbyKey等等 223 | - 尽量用broadcast和filter规避join操作 224 | - 因为每次job partition数量过多,导致hive表中过多小文件产生,所以需要重新指定分区,有以下俩种方法:repartition(numPartitions:Int):RDD[T]和coalesce(numPartitions:Int,shuffle:Boolean=false):RDD[T] 225 | 他们两个都是RDD的分区进行重新划分,repartition只是coalesce接口中shuffle为true的简易实现,(假设RDD有N个分区,需要重新划分成M个分区) 226 | - NM并且N和M相差不多,(假如N是1000,M是100)那么就可以将N个分区中的若干个分区合并成一个新的分区,最终合并为M个分区,这时可以将shuff设置为false,在shuffl为false的情况下,如果M>N时,coalesce为无效的,不进行shuffle过程,父RDD和子RDD之间是窄依赖关系。 228 | - 如果N>M并且两者相差悬殊,这时如果将shuffle设置为false,父子RDD是窄依赖关系,他们同处在一个stage中,就可能造成Spark程序的并行度不够,从而影响性能,如果在M为1的时候,为了使coalesce之前的操作有更好的并行度,可以讲shuffle设置为true。 229 | 230 | - **总之**:如果shuffle为false时,如果传入的参数大于现有的分区数目,RDD的分区数不变,也就是说不经过shuffle,是无法将RDD的分区数变多的。 231 | 232 | ### 参数调优 233 | 234 | - [美团Spark参数调优参考文章](http://tech.meituan.com/spark-tuning-basic.html) 235 | - 最重要的是数据序列化和内存调优。对于大多数程序选择Kyro序列化器并持久化序列后的数据能解决常见的性能问题。 236 | - Executor 237 | - 每个节点可以起一个或多个Executor。每个Executor上的一个核只能同时执行一个task,如果一个Executor被分到了多个task只能排队依次执行 238 | - Executor内存主要分三块: 239 | - 1、让task执行我们自己编写的代码,默认占总内存的20%。 240 | - 2、让task通过shuffle过程拉取了上一个stage的task输出后,进行聚合等操作时,默认占用总内存20%; 241 | - 3、让RDD持久化使用,默认60% 242 | - task的执行速度是跟每个Executor进程的CPU core数量有直接关系的。 243 | - 一个CPU core同一时间只能执行一个线程。 244 | - 每个Executor进程上分配到的多个task,都是以每个task一条线程的方式,多线程并发运行的。如果CPU core数量比较充足,而且分配到的task数量比较合理,那么通常来说,可以比较快速和高效地执行完这些task线程。 245 | 246 | - 广播大变量 247 | - 当需要用到外部变量时,默认每个task都会存一份,这样会增加GC次数,使用广播变量能确保一个Executor中只有一份 248 | 249 | - 使用Kryo优化序列化性能(如果希望RDD序列化存储在内存中,面临GC问题的时候,优先使用序列化缓存技术) 250 | - spark没有默认使用Kryo作为序列化类库,是因为Kryo要求注册所有需要序列化的自定义类型,这对开发比较麻烦 -------------------------------------------------------------------------------- /docs/engine/impala集群搭建.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: impala集群搭建 3 | date: 2018-08-12 20:17:45 4 | tags: 5 | --- 6 | 7 | ## 前言 8 | 9 | - 说起Impala,很多人都不会陌生。它区别于MapReduce 中间结果溢写,跨节点数据获取的低效,采用MPP 查询引擎,各查询节点并发执行查询语句,并将生成的查询结果汇总输出。 10 | - 近期开始真正的使用impala,之前只是小玩过已经集成好的环境,并没有真正的从0到1的去构建Impala集群。基于我司所有的大数据组件都是采用容器的方式部署以便统一管理,我们需要先构建Impala镜像 11 | 12 | ## impala 相关介绍 13 | 14 | - impala是由cloudera公司主导开发的一款大数据实时查询的分析工具,区别于Hive底层传统的MapReduce批处理方式,采用MPP查询引擎架构,相比于Hive 能带来查询性能30-90倍的提升 15 | - 特点 16 | - 查询速度快: 底层MPP查询引擎。基于内存计算,中间结果不写入磁盘,由coordinator汇总数据结果 17 | - 灵活性高:可以兼容存储在HDFS上的原生数据,也可以兼容优化处理过的压缩数据,与Hive共用metastore兼容从Hive上导入的所有sql语句 18 | - 可伸缩性:可以很好的和一些BI工具去配合使用,如Microstrategy、Tableau、Qlikview等。 19 | 20 | - 架构 21 | 22 | ![impala-architecture](http://ol7zjjc80.bkt.clouddn.com/impala-architecture.png) 23 | 24 | - 集群角色 25 | 26 | - impalad(query planner,coordinator, exec engine): 27 | 28 | - 分布在datanode节点上,接受客户端的查询请求 29 | - 接收查询请求的impalad 会作为本次查询的coordinator,生成查询计划树,并分发给具有相应数据的impalad执行 30 | - 汇总各个impalad上执行的查询结果,返回给客户端 31 | - statestore 32 | - 跟踪集群中impalad的健康状态及信息位置,并把健康状况同步到所有的impalad进程节点 33 | - catalog 34 | - 将元数据的变化通知给集群的各个节点,减少refresh和invalidate metadata语句使用 35 | 36 | ## 镜像构建篇 37 | 38 | ### hive镜像构建 39 | 40 | - 因为Impala依赖hive metastore,所以在构建impala镜像之前,先要构建hive镜像 41 | - 构建过程 42 | - impala 不支持 hive 2.x以上的系列,于是选择1.1.0版本,在cloudera官网下载编译好的tarball 43 | - 原本的hive lib 中缺少连接metastore的mysql jdbc驱动,自己下载jdbc connector jar放入lib目录下即可 44 | - 由于我hive 用的cdh的版本,而hadoop用的是apache的版本,导致真正运行hive的时候会找不到mapreduce指定的类,为此lib目录下需加入hadoop-core-2.6.0-mr1-cdh5.9.1.jar 45 | - 使用自带的schematool 创建元数据表 46 | 47 | ``` 48 | ./schematool -initSchema -dbType mysql 49 | 50 | ``` 51 | 52 | ### impala镜像构建 53 | 54 | - impala 我使用的是2.12.0的版本,这个版本cloudera官方没有提供tarball文件,只提供的rpm包。考虑到自身编译impala 成本比较大,于是采用rpm 安装的方法 55 | - 详细的安装流程参考链接 [Impala安装配置–RPM方式](http://lxw1234.com/archives/2017/06/862.htm) 我这边只记录一下安装过程中遇到的坑 56 | - 坑一:跟hive一样,由于我使用的hadoop是apache开源版本,有些class对应的包中没有 57 | - 软链hadoop-core-2.6.0-mr1-cdh5.9.1.jar到impala/lib中 58 | - 需要对服务的启动文件catalogd, impalad, stastored改动 59 | 60 | ``` 61 | for JAR_FILE in ${IMPALA_HOME}/lib/*.jar; do 62 | export CLASSPATH="${JAR_FILE}:${CLASSPATH}" 63 | done 64 | #hadoop share lib 65 | for HADOOP_JAR_FILE in $HADOOP_HOME/share/hadoop/tools/lib/*.jar; do 66 | export CLASSPATH="${HADOOP_JAR_FILE}:${CLASSPATH}" 67 | done 68 | 69 | ``` 70 | - 坑二:软链jdbc driver到impala/lib 并修改catalogd,其他启动文件相应都要修改 71 | 72 | - 坑三:启动catalogd,impala-server时报错 73 | 74 | ``` 75 | E0804 16:42:09.008862 543 MetaStoreUtils.java:1274] Got exception: java.io.IOException No FileSystem for scheme: hdfs 76 | Java exception follows: 77 | java.io.IOException: No FileSystem for scheme: hdfs 78 | ``` 79 | 80 | core-site.xml中加入配置 81 | 82 | ``` 83 | 84 | fs.file.impl 85 | org.apache.hadoop.fs.LocalFileSystem 86 | The FileSystem for file: uris. 87 | 88 | 89 | fs.hdfs.impl 90 | org.apache.hadoop.hdfs.DistributedFileSystem 91 | The FileSystem for hdfs: uris. 92 | 93 | 94 | ``` 95 | 96 | - 坑四 (class org.apache.hdfs.DistributedFileSystem not found): 97 | 98 | ``` 99 | 在hadoop2.8.3的版本中org.apache.hadoop.hdfs.DistributedFileSystem can be found in hadoop-hdfs-client jar. 100 | 只需要将包引入impala/lib 目录下即可 101 | 102 | 103 | ``` 104 | 105 | - 坑五:HBase client各种依赖包没有 106 | 107 | ``` 108 | ln -s $HBASE_HOME/lib/hbase-shaded-miscellaneous-2.1.0.jar hbase-shaded-miscellaneous.jar 109 | ln -s $HBASE_HOME/lib/hbase-shaded-protobuf-2.1.0.jar hbase-shaded-protobuf.jar 110 | ln -s $HBASE_HOME/lib/commons-lang3-3.6.jar commons-lang3.jar 111 | 以 112 | 113 | ``` 114 | 115 | - 坑六: 启动impala-server提示sasl plugin not found 116 | 117 | ``` 118 | yum install cyrus-sasl* 119 | restart impala service 120 | 121 | 122 | ``` 123 | 124 | ## impala 集群搭建 125 | 126 | ### 集群规划 127 | 128 | - 20 个 impalad服务 与datanode混部,以最大化利用分布式本地查询的优势 129 | - catalog, statestore 与namenode节点混部以减少namenode网络IO带来的影响 130 | - 全部服务部署docker化,脚本化,提供200与503脚本以便统一批量操作服务启动终止 131 | - 200启动脚本是对impala提供的原生的启动命令的封装,包括监听运行进程的存活,便于对集群机器批量操作 132 | 133 | - 200脚本封装代码 134 | 135 | ``` 136 | #!/usr/bin/env bash 137 | EXEC_FILE=/impala/impala-server 138 | PROGRESS=/usr/lib/impala/bin/impalad 139 | 140 | function usage() { 141 | echo -e "\n A tool used for starting impala services 142 | Usage: 200.sh {statestore|catalog|server} 143 | " 144 | } 145 | 146 | check_alive() { 147 | PID=`ps -ef | grep $IMPALA_USER_NAME | grep "$PROGRESS" | awk '{print $2}'` 148 | [ -n "$PID" ] && return 0 || return 1 149 | } 150 | 151 | start_service() { 152 | if [ ! -f $EXEC_FILE ];then 153 | echo "file not exists" 154 | exit 1 155 | fi 156 | check_alive 157 | if [ $? -ne 0 ];then 158 | $EXEC_FILE restart 159 | sleep 10 160 | check_alive 161 | if [ $? -ne 0 ];then 162 | echo "service start error" 163 | exit 1 164 | else 165 | echo "service start success" 166 | exit 0 167 | fi 168 | else 169 | echo "service alreay started" 170 | exit 0 171 | fi 172 | } 173 | 174 | function main() { 175 | # $SERVICE_POOL是对应的服务池,通过docker run -e 参数传入 176 | [[ -f /data0/hcp/conf/pools/${SERVICE_POOL}/init-env.sh ]] && source /data0/hcp/conf/pools/${HCP_POOL}/init-env.sh 177 | [[ -f /data0/hcp/sbin/impala/init-impala.sh ]] && source /data0/hcp/sbin/impala/init-impala.sh 178 | case "$1" in 179 | statestore) 180 | echo "starting impala statestore" 181 | EXEC_FILE=/data0/hcp/sbin/impala/impala-state-store 182 | PROGRESS=/usr/lib/impala/sbin/statestored 183 | start_service 184 | ;; 185 | server) 186 | echo "starting impala server" 187 | EXEC_FILE=/data0/hcp/sbin/impala/impala-server 188 | PROGRESS=/usr/lib/impala/sbin/impalad 189 | start_service 190 | ;; 191 | catalog) 192 | echo "starting impala catalog" 193 | EXEC_FILE=/data0/hcp/sbin/impala/impala-catalog 194 | PROGRESS=/usr/lib/impala/sbin/catalogd 195 | start_service 196 | ;; 197 | *) 198 | usage 199 | exit 1 200 | esac 201 | } 202 | main "$@" 203 | 204 | ``` 205 | 206 | - 503脚本设计思路类似 207 | 208 | 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /docs/engine/kubernetes-federation深度解析.md: -------------------------------------------------------------------------------- 1 | ## Federation v1 2 | 3 | ### 基本概念 4 | 5 | 项目地址 (**deprecated**): 6 | 7 | [kubernetes-retired/federationgithub.com![图标](https://pic3.zhimg.com/v2-406bd93b1ff9779bd3cfd693d0a6b552_ipico.jpg)](https://link.zhihu.com/?target=https%3A//github.com/kubernetes-retired/federation) 8 | 9 | 架构图 10 | 11 | ![img](https://pic3.zhimg.com/80/v2-6115cfbfc11c62f64423f018edbe495a_720w.jpg) 12 | 13 | 主要由 API Server、Controller Manager 和外部存储 etcd 构成。v1.11之后被废弃 14 | 15 | - federation-apiserver:类似kube-apiserver,兼容k8s API,只是对联邦处理的特定资源做了过滤(部分资源联邦不支持,故使用apiserver来过滤) 16 | - federation-controller-manager:提供多个集群间资源调度及状态通同步,工作原理类似kube-controller-manager 17 | - kubefed:Federation CLI工具,用来将子集群加入到联邦中 18 | - etcd:存储federation层面的资源对象,供federation control plane同步状态 19 | - 定义demo;基于annotation 20 | 21 | ![img](https://pic4.zhimg.com/80/v2-96198f56d616222ff724e4cc2c22f7df_720w.jpg) 22 | 23 | ### 缺点 24 | 25 | - 将成员集群作为一种资源进行管理,但是并未添加新的资源定义,以至于每当创建一种新资源都要新增Adapter 26 | - 无法有效的在多个集群管理权限,如不支持 RBAC 27 | - 没有独立的API,版本迭代不好演进,联邦层级的设定与策略依赖 API 资源的 Annotations 内容,这使得弹性不佳 28 | 29 | ## Federation v2 30 | 31 | Github项目: 32 | 33 | [https://github.com/kubernetes-sigs/kubefedgithub.com](https://link.zhihu.com/?target=https%3A//github.com/kubernetes-sigs/kubefed%5d(https%3A/github.com/kubernetes-sigs/kubefed)) 34 | 35 | 36 | 官方文档 37 | 38 | [https://github.com/kubernetes-sigs/kubefed/blob/master/docs/userguide.md#verify-your-deployment-is-workinggithub.com](https://link.zhihu.com/?target=https%3A//github.com/kubernetes-sigs/kubefed/blob/master/docs/userguide.md%23verify-your-deployment-is-working) 39 | 40 | 架构图 41 | 42 | ![img](https://pic1.zhimg.com/80/v2-cc5ce2ba68be184aa114635cfd76fec4_720w.jpg) 43 | 44 | ### 基本组成 45 | 46 | - admission-webhook:提供了准入控制;做各种校验(api version, 参数校验) 47 | - kubefed controller-manager: 处理自定义资源以及协调不同集群间的状态 48 | 49 | ### 基本概念 50 | 51 | 相比v1来说,v2在架构上有较大的调整,去掉aggregated api server实现,采用CRD+operator的方式定义一系列自定义联邦资源,并通过自定义controller实现跨集群资源协调能力 52 | 53 | - Federate:联邦(Federate)是指联结一组 Kubernetes 集群,并为其提供公共的跨集群部署和访问接口 54 | - KubeFed:Kubernetes Cluster Federation,为用户提供跨集群的资源分发、服务发现和高可用 55 | - Host Cluster:部署 Kubefed API 并允许 Kubefed Control Plane 通过 kubefedctl join 使得成员集群加入到主集群(Host Cluster) 56 | - Member Cluster:通过 KubeFed API 注册为成员并受 KubeFed 管理的集群,主集群(Host Cluster)也可以作为成员集群(Member Cluster) 57 | - ServiceDNSRecord: 记录 Kubernetes Service 信息,并通过DNS 使其可以跨集群访问 58 | - IngressDNSRecord:记录 Kubernetes Ingress 信息,并通过DNS 使其可以跨集群访问 59 | - DNSEndpoint:一个记录(ServiceDNSRecord/IngressDNSRecord的) Endpoint 信息的自定义资源 60 | 61 | ### Admission Webhook 62 | 63 | 为custom resource 提供的准入控制,作用在CR的创建过程的校验阶段 64 | 65 | - mutating 和validating两种类型的hook差别在于mutating可以在校验过程中通过打补丁的形式修改resource,而validating只能允许或拒绝resource;kubeFed实现的是validating模式 66 | 67 | ![img](https://pic2.zhimg.com/80/v2-bfb30c55a2ffd3fe8dd670e73b34fa51_720w.jpg) 68 | 69 | 目前有三种CR的admissionHook;用于校验自定义资源的格式正确性 70 | 71 | ![img](https://pic2.zhimg.com/80/v2-05d97bee9b14188534f64ed3b01de7c9_720w.jpg) 72 | 73 | ### CRD详解 74 | 75 | - 主要CRD以及交互图 76 | 77 | ![img](https://pic4.zhimg.com/80/v2-6e0cb36a54d7e2b3d96fe2cee1a233f3_720w.jpg) 78 | 79 | - CRD 两大基本块 80 | 81 | - - Type Configuration:自定义的被联邦托管的对象;主要组成是:Templates, Placement, Overrides… 82 | - Cluster Configuration:用来保存被联邦托管的集群的 API 认证信息 83 | 84 | - Type Configuration:用来描述将被联邦托管的资源类型, 定义了哪些Kubernetes API资源要被用于联邦管理;可通过`kubefedctl enable <> `来使新的api 资源可以被联邦管理;该操作会在HostCluster中创建一个新的CRD,然后通过sync controller 同步到其他member cluster 85 | 86 | - - Templates:用于描述被联邦的资源 87 | - Placement:用来描述将被部署的集群 88 | - Overrides:允许对部分集群的部分资源进行覆写;schedule的过程就是通过复写member cluster中配置的replica数量来动态调整跨集群负载的 89 | - demo 90 | 91 | ![img](https://pic2.zhimg.com/80/v2-fe7268d9605757cdcbd7e92ac9e80781_720w.jpg) 92 | 93 | - Cluster Configuration:用来定义哪些Kubernetes集群要被联邦。可透过kubefedctl join/unjoin来加入/删除集群,当成功加入时,会建立一个KubeFedCluster组件来储存集群相关信息,如API Endpoint、CA Bundle等。这些信息会被用在KubeFed Controller存取不同Kubernetes集群上,以确保能够建立Kubernetes API资源 94 | 95 | - - Cluster类型 96 | 97 | - - Host: 用于提供KubeFed API与控制平面的集群 98 | - Member: 通过KubeFed API注册的集群,并提供相关身份凭证来让KubeFed Controller能够存取集群。Host集群也可以作为Member被加入 99 | 100 | ![img](https://pic3.zhimg.com/80/v2-36e0d718473940d54ae9b47896e9ced6_720w.jpg) 101 | 102 | ### 源码详解 103 | 104 | ### 总览 105 | 106 | 四种API群组 107 | 108 | ![img](https://pic1.zhimg.com/80/v2-3390976612b1782f334d4793bbb6265c_720w.jpg) 109 | 110 | 代码级组件交互流程(不包含网络模块) 111 | 112 | ![img](https://pic2.zhimg.com/80/v2-103179db76c75bea0efd7dfa2ac9ebbd_720w.jpg) 113 | 114 | - StatusController和SyncController 都使用了**FederatedInformer,**用来感知所有member cluster中某中联邦资源的变更。如果变更则从HostCluster中获取最新的同步到memberCluster中 115 | 116 | **FederatedInformer**实现原理图 117 | 118 | ![img](https://pic2.zhimg.com/80/v2-4677a9a5f3977eac2964dd8170623d59_720w.jpg) 119 | 120 | ### 多集群调度方式 121 | 122 | - KubeFed提供了一种自动化机制来将工作负载实例分散到不同的集群中,这能够基于总副本数与集群的定义策略来将Deployment或ReplicaSet资源进行编排。编排策略是通过建立ReplicaSchedulingPreference(RSP)文件,再由KubeFed RSP Controller监听与RSP内容来将工作负载实例建立到指定的集群上 123 | - 目前仅支持Deployment和ReplicaSet两种调度类型,社区有支持StatefulSet的设计文档,还未实现 [https://github.com/kubernetes/community/pull/437/files](https://link.zhihu.com/?target=https%3A//github.com/kubernetes/community/pull/437/files) 124 | 125 | ![img](https://pic1.zhimg.com/80/v2-0e454ba069efb9a6b29d00ceacfb4dd0_720w.jpg) 126 | 127 | - 以下为一个RSP 范例:假设有三个集群被联邦,名称分别为:ap-northeast、us-east 与 us-west 128 | 129 | ![img](https://pic2.zhimg.com/80/v2-88d362c882e843a378013e54ba399bb9_720w.jpg) 130 | 131 | - 分配机制;源码可参考:kubefed/pkg/controller/util/planner/planner.go 132 | 133 | - - 若spec.clusters未定义的话,replicas会被均匀的分配到member clusters中,即配置为:`{“*”:{Weight: 1}}` 134 | - 第一轮分配时,先按照每个集群minReplica 分配,如果总的分配未达到totalReplica,则再按照权重进行第二次分配(分配过程中需注意目标集群是否overflow) 135 | - 如果RSP可以打开reblance 开关,用于平衡集群负载:将长期pending的pod move到资源较为空闲的集群 136 | - 集群是否空闲是通过check capacity 来计算:capacity根据当前集群上所有pod的状态来决定 137 | 138 | ### Enable Resource过程详解 139 | 140 | - 针对某个apiResource enable federation时,需根据apiResource生成**FederatedTypeConfig**;这是用来生成custom resource的配置,将生成新的CR资源,kind带Federated 前缀 141 | 142 | ![img](https://pic2.zhimg.com/80/v2-7625983764c25ac8795e58bc822b97a9_720w.jpg) 143 | 144 | - 通过`newSchemaAccessor` 获得待federate的apiResource json schema 145 | 146 | ![img](https://pic4.zhimg.com/80/v2-b021269bba0949c09f98fd152fa9a60f_720w.jpg) 147 | 148 | - 第三步是生成由federateTypeConfig和validation组成的自定义资源CustomResourceDefinition(真正重要的部分) 149 | 150 | ![img](https://pic4.zhimg.com/80/v2-4c96ae24f96a10e44954da0434a078c7_720w.jpg) 151 | 152 | - - validation是通过`federatedTypeValidationSchema`生成 153 | 154 | ![img](https://pic2.zhimg.com/80/v2-d0978d06f8c48653562d7f1c329f5cc5_720w.png) 155 | 156 | - - 这其中有我们熟悉的placements,overrides,template的校验定义,用于校验federated resource 的一些基本属性,template不做校验 157 | 158 | ![img](https://pic4.zhimg.com/80/v2-03d59eed980883a940b64b5b76743ddf_720w.jpg) 159 | 160 | - - crd 和federateTypeConfig 共同组成typeResource,更新到etcd中 161 | 162 | ![img](https://pic3.zhimg.com/80/v2-5a723440c55e178831cea1aae210cd4e_720w.jpg) 163 | 164 | ### 网络模式 165 | 166 | - 跨集群访问设计文档:[ https://github.com/kubernetes/community/blob/master/contributors/design-proposals/multicluster/federated-services.md](https://link.zhihu.com/?target=https%3A//github.com/kubernetes/community/blob/master/contributors/design-proposals/multicluster/federated-services.md) 167 | - Kubefed 还有一个亮点功能是跨集群间的网络访问。Kubefed 通过引入外部 DNS(外部DNS可使用自建的CoreDNS),将 Ingress Controller 和 metallb 等外部 LB 结合起来,使跨集群的流量可配置 168 | - 以 Ingress 举例,当集群中创建了一个Ingress(路由规则)Ingress DNS Controller 就能观察到,并在所有联邦集群中同步更新IngressDNSRecord;IngressDNSRecord 变更导致被DNSEndpointController感知到也重新sync一遍DNSEndpoints;此时全局DNSController紧接着更新自己的dns记录 169 | 170 | ![img](https://pic2.zhimg.com/80/v2-a107b39ac385ba2b0ec5435db5621215_720w.jpg) -------------------------------------------------------------------------------- /docs/engine/spark-job-submit.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 【Spark源码分析】Job提交执行过程详解 3 | date: 2019-06-10 13:08:55 4 | tags: 5 | - 学习 6 | - 大数据 7 | - Spark 8 | --- 9 | 10 | ## 前言 11 | 12 | 最近恰好有点时间梳理一下整个Spark job提交执行流程的相关源码。首先,给一个总的代码流程图(在Executor那块还需补充完整),方便理解整个处理逻辑 13 | 14 | ![](https://pic3.zhimg.com/80/v2-6e794771cf2317012b3986ed35979476_hd.jpg) 15 | 16 | ## Spark Job 提交处理过程源码解析 17 | 18 | ### submitJob解析 19 | 20 | - macos IntelliJ 中 command+7 查看DagScheduler所有方法,从submitJob方法开始分析,提交了JobSubmitted事件进事件队列,等待处理 21 | 22 | ![](http://imgs.wanhb.cn/spark-job1.png) 23 | 24 | - 在**DagScheduler**中有**DAGSchedulerEventProcessLoop**类,主要用来集中分发处理事件队列中的事件 25 | 26 | ![](http://imgs.wanhb.cn/spark-job2.png) 27 | 28 | - 移步DagScheduler.handleJobSubmitted方法,更新UI数据,同时调用submitStage方法;这里finalStage是createResultStage这个方法从最后一个stage生成所有stage的过程 29 | 30 | ![](http://imgs.wanhb.cn/spark-job3.png) 31 | 32 | ### submitStage解析 33 | 34 | ![](http://imgs.wanhb.cn/spark-job4.png) 35 | 36 | ![](http://imgs.wanhb.cn/spark-job5.png) 37 | 38 | 可以看到**getOrCreateParentStages**方法中只有shuffle操作时才会创建新的stage 39 | 40 | - 再来看SubmitStage方法实现细节,看之前如果对spark运行有了解的话,也大概知道,submitStage里面是提交task的细节 41 | 42 | ![](http://imgs.wanhb.cn/spark-job6.png) 43 | 44 | 这里先判断几个集合:**waitingStages,runningStages,failedStages**中是否已经存在该stage,防止重复提交stage;通过**getMissingParentStages**,深度遍历地从后往前判断当前stage是否存在需要重新计算的stage,加入**missing stages**集合中。那么什么条件下才算是一个missing stage呢?我们来分析**getMissingParentStages**实现 45 | 46 | ![](http://imgs.wanhb.cn/spark-job7.png) 47 | 48 | 可以发现判断当前rdd是否被cache了是通过DagScheduler.getCacheLocs获取缓存的location,观察到cacheLocs的数据结构是一个HashMap,key为rdd id,value为TaskLocation集合;虽说HashMap是个非线程安全集合,不过这里写操作线程安全通过加锁实现,所以说用HashMap实现倒也无妨 49 | 50 | ![](http://imgs.wanhb.cn/spark-job8.png) 51 | 52 | 这个集合是在getCacheLocs中写入的 53 | 54 | ![](http://imgs.wanhb.cn/spark-job9.png) 55 | 56 | 如果当前rdd本身没有设置storage level的话,也就无需查找缓存了,直接返回,否则通过blockManagerMaster.getLocations查找具体block对应的位置;blockManagerMaster上存储了所有Executor汇报上来的所有block位置元数据信息**(后面有一小节来分析block的写入和上报过程)** 57 | 58 | ### **submitMissingTasks解析** 59 | 60 | - 接下来分析submitStage中submitMissingTasks实现,这个方法是根据需要计算的stage来提交stage中的taskset。taskIdToLocation获取task要处理的数据的所在节点 61 | 62 | ![](http://imgs.wanhb.cn/spark-job10.png) 63 | 64 | 然后根据task所属的stage类型来创建实际的task实例(ShuffleMapTask与ResultTask) 65 | 66 | ![](http://imgs.wanhb.cn/spark-job11.png) 67 | 68 | 最后如果待计算的tasks集合不为空,则通过taskScheduler引用将task set提交到TaskScheduler中去调度 69 | 70 | ![](http://imgs.wanhb.cn/spark-job12.png) 71 | 72 | 具体看**TaskSchedulerImpl.submitTasks**实现,首先会创建一个**TaskSetManager**。**TaskSetManager**实际调度TaskSet的的实现,跟踪并且根据数据优先级分发task,以及重试失败的task。因为一个stage同一时刻只能有至多一个**TaskSetManager**处于活跃状态,所以创建完**TaskSetManager**实例之后,需要将Stage中其他**TaskSetManager**实例标记为**Zombie**状态 73 | 74 | ![](http://imgs.wanhb.cn/spark-job13.png) 75 | 76 | 随后,根据运行模式来判断要不要启动资源分配情况是否是饥饿状态的监控线程,最后调用**CoarseGrainedSchedulerBackend.reviveOffers()** 方法开始task调度 77 | 78 | ![](http://imgs.wanhb.cn/spark-job14.png) 79 | 80 | 实际上是发了一个actor消息,直接看receive中针对ReviveOffers消息的处理方法(**CoarseGrainedSchedulerBackend.makeOffers**)实现 81 | 82 | ![](http://imgs.wanhb.cn/spark-job15.png) 83 | 84 | ### Scheduler.resourceOffers解析 85 | 86 | - 先筛选出存活的executor,然后调用TaskSchedulerImpl.resourceOffers方法开始为每个TaskSet中的task分配具体执行节点 87 | - 在分配前将那些之前被加入黑名单又重新生效的节点包括进来;然后打散workerOffer集合,防止task分配不均 88 | 89 | ![](http://imgs.wanhb.cn/spark-job16.png) 90 | 91 | - 获取shuffleOffer中节点剩余cpu以及slot(cores/CPU_PER_TASK(default 1))集合availableCpus,availableSlots 92 | 93 | ```text 94 | val availableCpus = shuffledOffers.map(o => o.cores).toArray 95 | val availableSlots = shuffledOffers.map(o => o.cores / CPUS_PER_TASK).sum 96 | ``` 97 | 98 | - 获取sortedTaskSets,在循环期间随时关注是否有新的executor加入 99 | 100 | ![](http://imgs.wanhb.cn/spark-job17.png) 101 | 102 | - 对sortedTaskSets集合的每个taskSet,如果taskSet是barrier模式,且可用slot小于taskSet中的task数量,则直接跳过分配;因为barrier模式中,所有的task都需要并行启动 103 | 104 | ![](http://imgs.wanhb.cn/spark-job18.png) 105 | 106 | - 对于非barrier模式的taskSet,根据taskSet中所有tasks的数据优先级调度task。如下图,myLocalityLevels是taskSet中所有tasks数据本地性优先级集合。由TaskSetManager. computeValidLocalityLevels方法计算得到 107 | 108 | ![](http://imgs.wanhb.cn/spark-job19.png) 109 | 110 | - 优先级从高到低依次为**PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY** 也是按照这个顺序优先调度task 111 | 112 | ![](http://imgs.wanhb.cn/spark-job20.png) 113 | 114 | - 具体的调度见**TaskSchedulerImpl.resourceOfferSingleTaskSet**方法,里面实际依赖**TaskSetManager.resourceOffer**方法 115 | 116 | - - 首先对应的executor不能是被拉入黑名单,且当前TaskSetManager不能被标记为zombie 117 | - 从taskSet中出队一个指定locality的 task(实现见TaskSetManager.dequeueTask)加入runningTasks结合中。对于非barrier模式的stage来说,只要有task被调度成功了就可以跑起来 118 | 119 | - 这里再回过头看CoarseGrainedSchedulerBackend.makeOffers实现。当调用scheduler.resourceOffers之后如果有TaskDescription集合返回的的话,就可以调用launchTasks了 120 | 121 | - - 在launchTasks方法中,发送了LaunchTask消息,将序列化的Task信息通过rpc发送给Executor端(CoarseGrainedExecutorBackend实现) 122 | - ![](http://imgs.wanhb.cn/spark-job22.png) 123 | 124 | - 看CoarseGrainedExecutorBackend.receive中对LaunchTask消息的处理逻辑 125 | 126 | ![](http://imgs.wanhb.cn/spark-job23.png) 127 | 128 | - executor.launchTask中,实例化了TaskRunner,并将taskRunner提交到线程池中调度执行。**具体的执行逻辑在下一个小节描述** 129 | 130 | ## **ShuffleMapTask block写入过程分析** 131 | 132 | - 在上文中,我们分析到了TaskRunner。直接跳到TaskRunner里面的run方法实现 133 | 134 | ![](http://imgs.wanhb.cn/spark-job25.png) 135 | 136 | 可以看到通过执行执行task.run方法拿到执行task后的结果,跟进去看结果是什么数据结构。 137 | 138 | ![](http://imgs.wanhb.cn/spark-job26.png) 139 | 140 | 发现调用了runTask方法,查看接口的定义,发现有多个实现 141 | 142 | ![](http://imgs.wanhb.cn/spark-job27.png) 143 | 144 | 看到了熟悉的**ShuffleMapTask**,**ResultTask**字眼,结果明朗了,其实就是根据宽窄依赖来调用具体的Task实现。**ResultTask**生成的result是func在rdd各个partition上的执行结果而**ShuffleMapTask**生成的result是shuffle 文件输出信息(**MapStatus**) 145 | 146 | - 我们选**ShuffleMapTask.runTask**实现分析,返回的数据结构是**MapStatus,MapStatus**封装了task 所在的blockManager的信息(executorId+host+port)以及map task到每个reducer task的输出FileSegment的大小 147 | 148 | ![](http://imgs.wanhb.cn/spark-job28.png) 149 | 150 | 来分析outputFile的实现细节,首先这里需要获取具体的ShuffleWriter实现 151 | 152 | ![](http://imgs.wanhb.cn/spark-job29.png) 153 | 154 | 每个shuffleId对应的ShuffleHandle(也即是ShuffleWriter实现)由ShuffleManager统一管理,通过registerShuffle注册具体的ShuffleWriter 155 | 156 | ![](http://imgs.wanhb.cn/spark-job30.png) 157 | 158 | ![](http://imgs.wanhb.cn/spark-job31.png) 159 | 160 | 如图所示,目前spark中的shuffleWriter实现大概有三种,这里不详细比较,后续有专门文章分析Spark ShuffleWriter实现;选最常用的**SortShuffleWriter.write** 实现深入分析MapStatus产生过程 161 | 162 | ![](http://imgs.wanhb.cn/spark-job32.png) 163 | 164 | 可以看到SortShuffleWriter对于每个mapTask只会产生一个output,通过indexfile start和end offset 来计算后续reduceTask获取数据的位置,这样做大大减小了output 文件数量。最终返回mapStatus结果 165 | 166 | - [于是现在知道调用TaskRunner.run](https://link.zhihu.com/?target=http%3A//%E4%BA%8E%E6%98%AF%E7%8E%B0%E5%9C%A8%E7%9F%A5%E9%81%93%E8%B0%83%E7%94%A8TaskRunner.run) 根据task的类型不同返回的结果也是不同的,统一将其包装成DirectResult发送到driver上;这里根据实际得到的resultSize有不同的处理情况 167 | 168 | - - 如果result比较大,超过了maxDirectResultSize则会先把result存到本地的blockManager托管,storageLevel是内存+磁盘,然后把存储信息封装成IndirectTaskResult发送给driver 169 | - 否则直接将序列化的result发送给driver。通过statusUpdate封装StatusUpdate事件将result发送给driver端处理 170 | - ![](http://imgs.wanhb.cn/spark-job33.png) 171 | 172 | - 这里可以再分析一下result过大,blockManager是如何处理的细节。先看**blockManager.doPutBytes**,这里可以看到优先将result写入本地内存(LinkedHashMap实现),如果内存不够**(totalSize>memory\*spark.storage.memoryFraction)**,则会将result通过diskStore直接写入磁盘 173 | 174 | ![](http://imgs.wanhb.cn/spark-job34.png) 175 | 176 | - 看CoarseGrainedSchedulerBackend中具体处理StatusUpdate的实现,这里其实嵌套的比较多。按正常的路径首先会经过**taskResultGetter.enqueueSuccessfulTask**方法,在这里会将result反序列化(有**DirectTaskResult**与**IndirectTaskResult**之分),接着调用**DagScheduler.handleSuccessfulTask**。这里按照task类型不同有不同的处理方式: 177 | 178 | - - task是ResultTask的话,可以使用ResultHandler对result进行driver端计算(比如count()会对所有ResultTask的result做sum) 179 | - 如果是ShuffleMapTask的话会注册mapOutputTracker,方便后续reduce task查询,然后submit 下一个stage 180 | - ![](http://imgs.wanhb.cn/spark-job35.png) 181 | 182 | - 所以从这里可以看出如果,如果ShuffleMapTask不做显示cache的话,output只是在磁盘上,不会经过blockManager托管,下次若有相同的依赖从DagScheduler.getCacheLocs找不到对应的信息,还是得重新计算一遍 -------------------------------------------------------------------------------- /docs/engine/spark2-4-0触发的executor内存溢出排查.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 【spark-tips】spark2.4.0触发的executor内存溢出排查 3 | date: 2019-01-12 12:01:01 4 | tags: 5 | - spark 6 | --- 7 | 8 | ### 版本升级背景 9 | 10 | spark 2.4.0 最近刚发版,新增了很多令人振奋的特性。由于本司目前使用的是spark 2.3.0版本,本没打算这么快升级到2.4.0。无奈最近排查出的两个大bug迫使我们只能对spark进行升级。排查的两个bug如下: 11 |  12 | - spark2.3.0 bug导致driver跑一段时间内存溢出,经过dump下来的堆转储文件发现占绝大内存的对象是spark自身的ElementTrackingStore。这是统计任务运行时资源占用情况的类,在每一个批次处理完之后都没有释放,导致driver内存溢出 13 | ![](http://imgs.wanhb.cn/memory.png) 14 | 15 | - 详情参考文章:[导致driver OOM的一个SparkPlanGraphWrapper源码的bug](https://www.cnblogs.com/bethunebtj/p/9103547.html) 16 | - spark streaming 2.3.0 + kafka 0.8.2.1 + zk管理offset 每次重启,会导致offsetrange的左区间莫名向右移动若干offset size,导致每个批次通过offsetrange从kafka消费的数据普遍会丢失部分数据,具体问题还在通过源码定位中 17 | 18 | 第一个bug在spark 2.4.0中得到解决([参考issue](https://issues.apache.org/jira/browse/SPARK-23670)),于是对spark进行了升级。所幸spark升级对spark on yarn这种运行方式来说非常解耦,只需要定义spark.jars依赖就行,yarn nodemanager会对依赖包进行下载。 19 | 20 | ### 遇到的问题 21 | 22 | #### 问题描述 23 | spark 版本升级之后,当天对在线任务观察,运行平稳,上一节提到的bug也修复了;但是第二天离线任务的运行却出现了问题:部分离线任务在做聚合运算的时候出现executor 集体内存溢出,任务执行失败 24 | 25 | #### 问题排查 26 | 27 | - 查看日志发现是内存溢出导致executor触发钩子异常退出 28 | 29 | ![](http://imgs.wanhb.cn/spark_1.png) 30 | 31 | - 进一步发现在某个计算步骤,需要读入上一个步骤写入hdfs的数据,每个task处理的数据量比较大,且都放到了内存中(*导火线*) 32 | ![](http://imgs.wanhb.cn/spark_2.png) 33 | 34 | - 接着因为要做各种聚合运算(reduceby, groupgy, join…)execution 的内存也不断增大,濒临内存的限制边缘 8G * 0.6(spark.memory.fraction) =4.8G,很容易就会来不及spill到磁盘,导致内存溢出 35 | ![](http://imgs.wanhb.cn/spark_2.png) 36 | 37 | - 于是基本可以得到问题原因:从读入hdfs的源头去排查,为什么导致一个task处理的数据量过大;发现hdfs中上一步save到hdfs中的每一个part都是将近500M大小的parquet+snappy 压缩文件,而这种格式无法切分,导致一个map task只能对这400多M的文件照单全收,而由于我应用申请的配置是 *8 cores 8 mem / executor* 导致8个task同时读入大文件到executor jvm中,最终jvm报内存溢出异常 38 | ![](http://imgs.wanhb.cn/spark_4.png) 39 | 40 | #### 解决方案 41 | - 限制executor 并行度,将cores 调小,内存适当调大 42 | - 由于上一步写hdfs的操作并行度太小(只有40),重新调整并行度,让输出的每个part文件减小 -------------------------------------------------------------------------------- /docs/learning/Google-With-Borg.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/learning/Google-With-Borg.pdf -------------------------------------------------------------------------------- /docs/learning/Raft论文学习.md: -------------------------------------------------------------------------------- 1 | # Raft 论文读后总结 2 | 3 | ## 前言 4 | 5 | 分布式系统领域自然离不开一致性协议,而其中以Paxos和Raft最为著称。Paxos和Raft早两年有接触过,受限于当时的知识水平,对实现细节难免囫囵吞枣;最近决心专供分布式系统,于是重新拾起相关Paper开始拜读。以下是Raft 论文读后总结。 6 | 7 | ## Raft 五大性质 8 | - **Election Safety**: 在每一个term里,*至多*(有可能没有)只能有一个leader被选出 9 | - **Leader Append-Only**: leader节点不会对自身的log entries 进行覆写/删除的操作;只是单纯的append 10 | - **Log Matching**: 如果两个log entry 拥有相同的index和term,那么这两个entry是相等 11 | - **Leader Completeness**: 在一个term中,如果log entry被commit了,那么这个entry 将会存在于所有的其他任期的leader中(*也是作为Candidate是否被选中的一个条件*) 12 | - **State Machine Safety**: 如果一个节点apply 了一个log entry,那么带有相同index却不同的log entry是不能被其他任何一个节点所apply 13 | 14 | ## Raft 组成部分 15 | - Raft 由 Leader,Follower以及Candidate三种角色组成,三者之间组成有限状态机,可在一定事件下互相切换,具体如下图 16 | ![](http://imgs.wanhb.cn/raft-roles-switch.png) 17 | - 根据上图,角色对应的分工如下 18 | - Follower 19 | - 响应candidates和leader的 rpc请求 20 | - 如果leader在timeout之内未发送心跳,则主动切换为candidate发起新一轮选举 21 | - Candidate:主要是选举 22 | - 将currentTerm +1,且投给自己,并发起Request Vote RPC给所有其他节点寻求投票 23 | - 如果收到大多数节点投票,则变成leader,通知所有节点切换为follower 24 | - 如果通过AppendEntries RPC说明新的leader选举成功,则将自己置为follower 25 | - *可能出现都投自己的情况(极端)*:这种情况的处理机制是所有candidate 任意sleep 一段时间(***150-300ms***),再触发新一轮选举 26 | - Leader: 27 | - 维持心跳,防止触发leader选举 28 | - 如果接收到客户端append log请求,leader 会并发地向followers 发起AppendEntries Rpc请求,等大多数follower 节点都返回成功之后再将log entry本地commit, 并将结果最终结果返回给客户端;如果失败则retry,正常的请求处理流程如下图 29 | ![](http://imgs.wanhb.cn/client-request.png) 30 | - 在收到客户端append log 请求后,检测是否最新的log index大于nexIndex 中的值,如果是,则需要给follower 发送AppendEntries RPC请求 31 | - 请求成功:更新nextIndex和matchIndex 32 | - 请求失败:一般是因为leader重选导致*数据不一致*,则减小nextIndex 重新发送AppendEntries RPC,如此往复,直到找到follower 与 leader 同步的最近一条log entry为止 33 | ![](http://imgs.wanhb.cn/client-request-2.png) 34 | - 如果存在N, N>CommitIndex,大多数matchIndex[follower]>=N,且log[N].term == currentTerm,则将commitIndex 置为N 35 | 36 | ## 实现Raft的数据结构 37 | - 消息状态划分 38 | ``` 39 | Uncommit: 未提交转态(Client发到主节点,主节点还没有得到大多数从节点的提交回执) 40 | Commited: 已提交转态(从节点收到主节点的消息提交,还未收到确认报文) 41 | Applied: 已确认转态(从节点收到主节点的确认报文,或主节点已收到大多数从节点的提交回执) 42 | ``` 43 | - State :每个节点的状态 44 | - 在所有节点上都有的 45 | 46 | ``` 47 | //实际落盘的 48 | currentTerm:通过rpc接到的最新的任期,初始化为0,随着选举次数增加而增加 49 | votedFor: 保存着一次选举过程中所投的candidateId,为空表示还未投票 50 | log[]: log entries集合,每个entry由记录和所属任期组成 tuple2 51 | //在内存中实时可见的 52 | commitIndex: 已确认被commit了的最高位的log entry index 53 | lastApplied: 被当前节点applied的最高位的log entry index 54 | ``` 55 | 56 | - 在leader 上的状态,每一次选举过后都会在新的leader上重新初始化 57 | 58 | ``` 59 | nextIndex[]: 保存着每一个follower节点的下一个log entry index;初始化中leader last log index +1 60 | matchIndex[]: 保存着每一个follower已经被确认replicate成功的最高位的log entry index;初始化为0 61 | ``` 62 | 63 | - RequestVote RPC 工作模式 64 | ![](http://imgs.wanhb.cn/request-vote-rpc.png) 65 | 66 | - AppendEntries RPC工作模式 67 | - 由leader 发起log replicate,以及维护leader to follower 心跳,防止新一轮election触发 68 | ``` 69 | //rpc 请求参数 70 | term:leader term 71 | leaderId: 72 | pervLogIndex: 上一次apply过的 log 对应的Index 73 | prevLogTerm: 上一次apply过的log 对应的term 74 | entries[]: 要同步的log entries,之所以是数组是优化性能,减少rpc调用次数 75 | leaderCommit: leader最近一次提交的commitIndex 76 | //rpc 返回值 77 | term: follower 当前的term 78 | succss: 如果follower mactch了prevLogIndex和prevLogTerm返回true 79 | //replicate 处理逻辑 80 | 如果term< currentTerm,则返回false 81 | 如果match 不上prevLogIndex和prevLogTerm 则返回fase 82 | 如果当前节点存在相同index但是不同term的entry,则强制删掉该index之后所有的entry,从该节点往后同步leader log entry 83 | 如果 leaderCommit > commitIndex, 将commitIndex 设置为min(leaderCommit, index of last new entry) 84 | ``` 85 | 86 | ![](http://imgs.wanhb.cn/append-entry-rpc.png) 87 | 88 | ## Leader崩溃 89 | ### 如何保证follower跟新leader的数据一致性 90 | - 问题:旧leader挂掉之后,follower通过心跳感知,并转为candidate,触发新一轮选举。新leader产生之后,leader和follower之间很可能存在数据不一致的情况:某些log entry在leader上不存在 91 | - Raft的做法是:leader会强制follower 完全复制自己的数据,这样会导致follower上的log entries 可能会被覆写删除(**Kafka中partition leader与follower 之间的Sync参考了这一点**) 92 | ![](http://imgs.wanhb.cn/log-consistency.png) 93 | - 如上图,通过不断的retry之后找到leader和follower之间一致的log entry;从那个entry之后开始同步(强行覆写) 94 | ### 如何防止brain split后log entries正确性 95 | - 问题:如果集群中某一个follower 由于网络问题,长时间没收到leader心跳,如果这时它选自己为leader,等到网络恢复后是不是会成为新的leader覆写之前被commit 的log entry? 96 | - Raft做法:增加被选为Leader的限制(**参考性质*Leader Completeness**) 97 | - Raft 确保只有那些包含所有committed log entries(majority) 的candidates 才有资格被选为leader 98 | - 实现:Vote RPC中包含了candidate 的log 信息,这样voter就可以通过对比自己的日志中log entry 的 index和term来判断candidate 是不是比自己日志更latest 99 | ### 如何继续leader crash之前的commit操作 100 | - 这个问题存在的前提是新一轮leader election 被选为新leader的节点上保存了上一个leader 未commit成功的log entry;**在raft协议中只确保commit 当前leader中的log entries会按照副本数机制实现(num of replicas > num of node / 2 )** 101 | ![](http://imgs.wanhb.cn/commit-before.png) 102 | - 这种确保的是:如果一条log entry 被当前leader commit成功,那么可以认为之前所有的entries 都commit成功了(**参考特性5 — Log Matching Property** ),**也不需将之前的log entry的term 改成current term** 103 | 104 | ## Follower&&Candidate崩溃 105 | - follower 和 candidate 崩溃处理方式比较简单 106 | - 如果一个follower 或者 candidate 挂掉了,RequestVote 和 AppendEntries RPC 都会失败,处理的方式就是无限次的retry,只要服务重启,就能随着rpc 同步到最新的状态 107 | 108 | ## 集群扩缩容 109 | - 目前我们讨论的都是在一组固定的节点上操作,但是在现实中存在因为节点的down掉以及扩容的需求,需要变更集群节点。 如果直接变更的话,可能会出现一段时间brain split的情况。最稳妥的方案就是将服务全部下线,扩容完成之后再重新上线,但是这过于低效 110 | ![](http://imgs.wanhb.cn/configuration-change1.png) 111 | - 如图表示的是滚动升级的情况,逐个重启旧server,会存在新旧两个leader同时存在的情况(各自都赢得了所在集群大多数的vote) 112 | - 解决方案:*引入一种特殊类型的log entry*,专门用来做集群配置更替,把它叫做C (old,new),当C(old,new)被commit之后集群进入 joint consensus(联合一致性),即*新旧集群共存*的状态。在这种状态下,需遵循的规则如下: 113 | - Log entries将被replicate到新旧配置的所有server节点中 114 | - 任何一个节点通过新旧任何一份配置都有权利在选举中成为leader 115 | - 选举结果和log entry commitment的决定需要各自配置中的大多数节点认可 116 | - 讨论集群扩容的例子 117 | ![](http://imgs.wanhb.cn/cluster-change.png) 118 | - 第一阶段:逐台变更时,部分server上处于C(old,new) 状态,此时leader选举只能从C(old, new) 或 C(old) 中产生,具体取决于candidate是否接收到了C(old,new) log entry;当C(old, new) 被最终committed,则只拥有C(new)和C(old) 的server将再无法被选举为leader(**参考特性4 — Log Matching Property**) 119 | - 第二阶段:接着再引入一种log entry C(new) ,将它同步到所有节点,等C(new) 最终committed之后则集群切到了C(new) 120 | - 需注意的点 121 | - 新上的节点会存在相对于老集群数据落后的情况,需要一段时间的sync,以追上其他节点,这期间不做任何*投票*操作(此处可类比Doris 里面Observer的设计理念) 122 | - 第二阶段结束时,下掉的节点可能不在新集群的配置里面,也就不会接收到心跳,这样可能触发下掉的server leader选举 123 | - 为防止扰乱集群可以规定:server如果在timeout允许的范围内正常的接收到了leader的心跳,则会忽略其他RequestVote Rpc请求 124 | 125 | ## 日志压缩 126 | - 日志如果不做压缩处理,理论上会无限期膨胀,期间可能很多重复多余的数据,浪费空间 127 | - 最简单的做法就是利用snapshot,将系统整个的状态数据作为一个snapshot保存到stable storage上,这样在上一个时间点的snapshot就可以被删除了(FLink的 checkpoint 和Doris的metadata里面也是这么做的) 128 | ![](http://imgs.wanhb.cn/log-snapshot.png) 129 | - 一些其他的方式如:LSM Tree, log cleaning 等都可以 130 | 131 | ## 客户端设计的原则 132 | - 首先客户端需要具备请求超时重发机制:请求random server会被reject,如果leader 挂掉触发选举也需要再一次的retry 133 | - Raft 对客户端的设计目标是要实现线性一致性语义,这样要求客户端每次command需要分配一个unique serial numer,在server端的state machine中会跟踪client最近一次的serial number,如果被serial number表示的command已经被执行完了则不会被再次执行(**类似Doris 里面mini load Label的概念**) 134 | - **只读订阅需求**:(**范例可了解Doris 元数据设计**)为了降低leader节点的负载,可以允许client 请求follower节点读取数据;但是有一个缺点就是随着leader选举的过程,可能会读到过期的数据(被commited的数据没有被读到,这不满足线性一致性设计理念),针对这个, 有两种预防措施 135 | - 主节点选举成功之后,立即发一个空的log entry到所有节点,这样就触发了集群中所有follower节点向leader强制同步的过程 136 | - 主节点在响应read-only请求之前必须确认自己是否已经过期,防止自身的信息处于过期的状态;**确认方法是集群中大多数节点发送心跳** 137 | 138 | ## 与Paxos的差异 139 | - [Paxos](https://zh.wikipedia.org/zh-cn/Paxos%E7%AE%97%E6%B3%95) 可以同时提交和处理多个提案,但是发生冲突时,理论上会有更高的延时(协商时间),而Raft算法会天生地把消息确定一个先后顺序。大幅减少了冲突的可能性 -------------------------------------------------------------------------------- /docs/learning/Snapshots-for-Distributed-Dataflows.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/learning/Snapshots-for-Distributed-Dataflows.pdf -------------------------------------------------------------------------------- /docs/learning/Volcano.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/learning/Volcano.pdf -------------------------------------------------------------------------------- /docs/learning/chandy-lamport-algorithm.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/learning/chandy-lamport-algorithm.pdf -------------------------------------------------------------------------------- /docs/learning/massive-parallel-processing.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/learning/massive-parallel-processing.pdf -------------------------------------------------------------------------------- /docs/learning/raft.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/learning/raft.pdf -------------------------------------------------------------------------------- /docs/learning/sigmod_structured_streaming.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/learning/sigmod_structured_streaming.pdf -------------------------------------------------------------------------------- /docs/learning/spark设计论文.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/learning/spark设计论文.pdf -------------------------------------------------------------------------------- /docs/learning/优秀博文汇总.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/learning/优秀博文汇总.pdf -------------------------------------------------------------------------------- /docs/olap/Kylin二次开发——测试环境搭建.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Kylin二次开发——测试环境搭建 3 | date: 2017-08-07 17:55:39 4 | tags: 5 | - kylin 6 | - Java 7 | --- 8 | 9 | ### 调研背景 10 | 11 | 虽然公司目前在生产环境上正式用上了kylin,但是由于其本身年龄不长,社区并不完善,难难免会暴露出各种各样的源码级别的问题(包括上一篇介绍的kylin的同步机制的问题)。这时候使用者想等着官方推出新的release未免太过于被动。于是,我们想着对kylin进行二次开发以满足我们对定制化需求。事实上,目前我们使用的所有开源框架在一定程度上都进行了多多少少的二次开发: 12 | 13 | - superset接入kylin,完成了自编译集成进了docker,并修改了 flask-appbuilder的源码逻辑,兼容ldap与原本的账号系统。 14 | - airflow 正在考虑从输出信息中判断task是否执行成功,而不是单纯的靠进程是否异常退出判断(主要考虑到支持kylin的任务调度) 15 | 16 | 其实在kylin的官网对于开发环境搭建大致的步骤都做了介绍下面描述整个过程 17 | 18 | ### 搭建过程 19 | 20 | - 想应用经过自己二次开发的kylin当然必须得全覆盖的跑一遍所有的单元测试。这就意味着必须得有Hadoop+hive+hbase等一整套测试环境。这里使用kylin官方推荐的Hortonworks Sandbox。为了方便,直接使用Sandbox on docker。 21 | - [按照官网的教程(docker占用的内存至少8G以上,否则运行不了)](https://hortonworks.com/tutorial/sandbox-deployment-and-install-guide/section/3/) 22 | - 在执行start_sandbox-hdp.sh的时候需要往映射的端口里面加入hive metastore的thrift端口9083,否则本地跑单元测试的时候连不上metastore 23 | - ssh上运行的container,修改admin密码: 24 | 25 | ``` 26 | ssh -p 2222 root@localhost 27 | 或者http://127.0.0.1:4200/ 进入shell浏览器界面 28 | //执行 29 | ambari-admin-password-reset 30 | 31 | ``` 32 | 33 | - 用修改的admin密码登录[http://localhost:8080](http://localhost:8080),确保dashboard中hive+mapreduce+hdfs+hbase正常启动 34 | 35 | - 修改kylin.properties的几个值: 36 | 37 | ``` 38 | //KYLIN_HOME/examples/test_case_data/sandbox/kylin.properties 39 | 40 | kylin.job.use-remote-cli=true 41 | kylin.job.remote-cli-hostname=sandbox 42 | kylin.job.remote-cli-username=root 43 | kylin.job.remote-cli-password=xxxx 44 | //这个默认是22端口,由于我本地不生效,就直接设置为2222端口 45 | kylin.job.remote-cli-port=2222 46 | 47 | ``` 48 | 49 | - 如果单个单元进行测试,不想每次从头开始,方便集中debug某个moudle的错误,可以注释掉pom.xml中的check-style插件 50 | 51 | ``` 52 | 80 | 81 | ``` 82 | 83 | - 执行测试命令 84 | 85 | ``` 86 | //base 87 | mvn test -fae -Dhdp.version=${HDP_VERSION:-"2.6.1.0-129"} -P sandbox -X 88 | //全覆盖 89 | mvn verify -Dhdp.version=${HDP_VERSION:-"2.6.1.0-129"} -fae 2>&1 | tee mvnverify.log 90 | 91 | ``` 92 | 93 | ### 踩坑记录 94 | 95 | - 该暴露的端口得暴露出来,否则本地测试的时候对指定的端口无法进行tcp通信 96 | 97 | ``` 98 | 9083:9083 //hive metastore 99 | 8050:8050 // kylin 获取job output信息端口 100 | 50010:50010 // dfs.datanode.address,这里踩坑很久,不配置的话会有:createBlockOutputStream when copying data into HDFS错误 101 | 102 | ``` 103 | - 启动start_sandbox-hdp脚本的时候,可能会出现postgresql服务启动不了,这很可能是因为其申请的共享内存超过了系统的,只需要进入到容器里面做相关操作 104 | 105 | ``` 106 | ssh -p root@localhost 107 | sudo sysctl -w kernel.shmmax=17179869184 //假设你有16G内存,按实际扩充 108 | sudo service postgresql start 109 | //postgresql启动之后在容器外重新执行 110 | ./start_sandbox-hdp.sh 就可以了 111 | 112 | ``` 113 | 114 | -------------------------------------------------------------------------------- /docs/olap/Kylin学习笔记.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Kylin学习笔记 3 | date: 2017-05-02 10:40:01 4 | tags: 5 | - kylin 6 | - infrastructure 7 | - data 8 | --- 9 | 10 | ## 基础知识 11 | 12 | ### OLAP(on-Line AnalysisProcessing)的实现方式 13 | 14 | - ROLAP: 15 | 基于关系数据库的OLAP实现(Relational OLAP)。ROLAP将多维数据库的多维结构划分为两类表:一类是事实表,用来存储数据和维关键字;另一类是维表,即对每个维至少使用一个表来存放维的层次、成员类别等维的描述信息。维表和事实表通过主关键字和外关键字联系在一起,形成了"星型模式"。对于层次复杂的维,为避免冗余数据占用过大的存储空间,可以使用多个表来描述,这种星型模式的扩展称为"雪花模式"。特点是将细节数据保留在关系型数据库的事实表中,聚合后的数据也保存在关系型的数据库中。这种方式查询效率最低,不推荐使用。 16 | - MOLAP: 17 | 多维数据组织的OLAP实现(Multidimensional OLAP。以多维数据组织方式为核心,也就是说,MOLAP使用多维数组存储数据。多维数据在存储中将形成"立方块(Cube)"的结构,在MOLAP中对"立方块"的"旋转"、"切块"、"切片"是产生多维数据报表的主要技术。特点是将细节数据和聚合后的数据均保存在cube中,所以以空间换效率,查询时效率高,但生成cube时需要大量的时间和空间。 18 | 19 | - HOLAP: 基于混合数据组织的OLAP实现(Hybrid OLAP)。如低层是关系型的,高层是多维矩阵型的。这种方式具有更好的灵活性。特点是将细节数据保留在关系型数据库的事实表中,但是聚合后的数据保存在cube中,聚合时需要比ROLAP更多的时间,查询效率比ROLAP高,但低于MOLAP。 20 | 21 | - kylin的cube数据是作为key-value结构存储在hbase中的,key是每一个维度成员的组合值,不同的cuboid下面的key的结构是不一样的,例如cuboid={brand,product,year}下面的一个key可能是brand='Nike',product='shoe',year=2015,那么这个key就可以写成Nike:shoe:2015,但是如果使用这种方式的话会出现很多重复,所以一般情况下我们会把一个维度下的所有成员取出来,然后保存在一个数组里面,使用数组的下标组合成为一个key,这样可以大大节省key的存储空间,kylin也使用了相同的方法,只不过使用了字典树(Trie树),每一个维度的字典树作为cube的元数据以二进制的方式存储在hbase中,内存中也会一直保持一份。 22 | 23 | ### cube 构建 24 | 25 | - Dimension:Mandatory、hierarchy、derived 26 | - 增量cube: kylin的核心在于预计算缓存数据,因此无法达到真正的实时查询效果。一个cube中包含了多个segment,每一个segment对应着一个物理cube,在实际存储上对应着一个hbase的一个表。每次查询的时候会查询所有的segment聚合之后的值进行返回,但是当segment数量较多时,查询效率会降低,这时会对segment进行合并。被合并的几个segment所对应的hbase表并没有被删除。 27 | - cube词典树:cube数据是作为key-value结构存储在HBase中的。key是每一个维度成员的组合值 28 | 29 | ### Streaming cubing 30 | 31 | - 支持实时数据的cub。与传统的cub一样,共享storage engine(HBase)以及query engine。kylin Streaming cubing相比其他实时分析系统来说,不需要特别大的内存,也不需要实现真正的实时分析。因为在OLAP中,存在几分钟的数据延迟是完全可以接受的。于是实现手法上采用了micro batch approach。 32 | - micro batch approach:将监听到的数据按照时间窗口的方式划分,并且为每个窗口封装了一个微量批处理,批处理后的结果直接存到HBase。 33 | - Streaming cubing data 最终会慢慢转换成普通的cubes,因为所有的数据是直接保存到HBase中的,并且保存为一个新的segment,当segment数量到达一定程度时,job engine会将segment 合并起来形成一个大的cube。 34 | 35 | 36 | ### 实战问题总结 37 | 38 | 由于集群环境是CDH集群,所以选择了kylin CDH 1.6的版本,支持从Kafka读取消息建立Streaming cubes直接写入HDFS中 39 | 40 | - 选择一个集群namenode节点,将解压包放入/opt/cloudrea/parcels/目录中。如果是部署单节点,暂时不用更改配置文件。所有的配置加载都在bin/kylin.sh中。 41 | - 直接kylin.sh start/stop 运行脚本,服务就会在7070端口起一个web界面。这个界面是可以进行可视化操作的。 42 | 43 | ### Hive 数据源 44 | 45 | - 直接测试hive数据源是没有问题的,这一功能比较完善,也是主打功能。 46 | 47 | ### kafka数据源 48 | 49 | 从kylin 1.6 版本开始正式支持Kafka做数据源,将Streaming Cubes实时写入 HBase中。这一块在测试的时候也出现了问题: 50 | 51 | - Kafka版本问题 52 | - 由于实验环境的CDH集群Kafka版本是0.9的,而kylin 仅支持0.10以上的版本,所以需要对CDH kafka集群进行升级。 53 | 54 | - mapreduce运行环境无jar包 55 | - kylin中提交cube build之后,map reduce任务直接抛错。错误提示是,找不到Kafka的Consumer类。根本原因是kylin默认集群上的map reduce classpath是会加载kafka-clients.jar包的,所以在提交任务的时候没有将kafka-clients.jar包打进去。这时可以有三种做法: 56 | - 直接修改kylin的源码,将kafka-clients.jar包给包括进去(待尝试)。 57 | - 可以通过修改集群的HADOOP_ClASSPATH的路径,将jar包给包括进去。 58 | - hadoop classpath 查看classpath目录信息 将对应jar包直接拷入map reduce classpath中,这方法简单,但是缺点就是需要逐个得对node进行操作。 59 | 60 | - Property is not embedded format 61 | - 现在意识到,使用开源框架不会看其源码是不行的...就在我折腾俩天终于将mapreduce任务跑起来之后,新的错误出现了:"ava.lang.RuntimeException: java.io.IOException: Property 'xxx' is not embedded format"。莫名奇妙的错误。迫使我直接去github上看kylin kafka模块的源码。在TimedJsonStreamParser.java中发现代码逻辑中默认json数据中,如果key存在下划线就会将该key按照下划线split... 然后看key对应的value是不是map类型,如果不是直接抛出标题的错误。 62 | 63 | - 明确了问题之后,如何复写默认下划线split的配置成为问题。由于官网的文档十分鸡肋,很多坑都没有涉及到,所以继续看源码。发现StreamingParser.java这个类中会去写一些默认的配置。 64 | 65 | ``` 66 | public static final String PROPERTY_TS_COLUMN_NAME = "tsColName"; 67 | public static final String PROPERTY_TS_PARSER = "tsParser"; 68 | public static final String PROPERTY_TS_PATTERN = "tsPattern"; 69 | public static final String EMBEDDED_PROPERTY_SEPARATOR = "separator"; 70 | 71 | static { 72 | derivedTimeColumns.put("minute_start", 1); 73 | derivedTimeColumns.put("hour_start", 2); 74 | derivedTimeColumns.put("day_start", 3); 75 | derivedTimeColumns.put("week_start", 4); 76 | derivedTimeColumns.put("month_start", 5); 77 | derivedTimeColumns.put("quarter_start", 6); 78 | derivedTimeColumns.put("year_start", 7); 79 | defaultProperties.put(PROPERTY_TS_COLUMN_NAME, "timestamp"); 80 | defaultProperties.put(PROPERTY_TS_PARSER, "org.apache.kylin.source.kafka.DefaultTimeParser"); 81 | defaultProperties.put(PROPERTY_TS_PATTERN, DateFormat.DEFAULT_DATETIME_PATTERN_WITHOUT_MILLISECONDS); 82 | defaultProperties.put(EMBEDDED_PROPERTY_SEPARATOR, "_"); 83 | } 84 | 85 | ``` 86 | 87 | 自然而然会联想到,这个默认的配置肯定是可以在用户设置的时候通过key(separator)去覆盖的...于是发现在构建Streaming table的时候,可以通过Parse Properties去覆盖配置。 88 | 于是直接写成如下的形式: 89 | 90 | ``` 91 | tsColName=timestamp;separator=no 92 | 93 | //源码中拿到这个配置之后会做覆盖处理,然后执行 getValueByKey: 94 | 95 | protected String getValueByKey(String key, Map rootMap) throws IOException { 96 | if (rootMap.containsKey(key)) { 97 | return objToString(rootMap.get(key)); 98 | } 99 | 100 | String[] names = nameMap.get(key); 101 | if (names == null && key.contains(separator)) { 102 | names = key.toLowerCase().split(separator); 103 | nameMap.put(key, names); 104 | } 105 | 106 | if (names != null && names.length > 0) { 107 | tempMap.clear(); 108 | tempMap.putAll(rootMap); 109 | //这块如果复写了separator属性的话split后的names数组长度为1会跳过这一步循环,防止解析出错 110 | for (int i = 0; i < names.length - 1; i++) { 111 | Object o = tempMap.get(names[i]); 112 | if (o instanceof Map) { 113 | tempMap.clear(); 114 | tempMap.putAll((Map) o); 115 | } else { 116 | throw new IOException("Property '" + names[i] + "' is not embedded format"); 117 | } 118 | } 119 | Object finalObject = tempMap.get(names[names.length - 1]); 120 | return objToString(finalObject); 121 | } 122 | 123 | return StringUtils.EMPTY; 124 | } 125 | 126 | ``` 127 | -------------------------------------------------------------------------------- /docs/olap/kylin-master-slave同步原理及问题排查.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: kylin master-slave同步原理及问题排查 3 | date: 2017-07-06 15:11:38 4 | tags: 5 | - Kylin 6 | - infrastructure 7 | - 源码 8 | - 成长 9 | --- 10 | 11 | ### 背景 12 | 13 | 最近俩个月,团队整个数据基础架构慢慢转移到kylin上面来。而kylin也不负众望,对于一些复杂的聚合查询响应速度远超于hive。随着数据量的上来,kylin的单体部署逐渐无法支撑大量的并行读写任务。于是,自然而然的考虑到kylin的读写分离。一写多读,正好也符合kylin官方文档上的cluster架构。然而在实际的使用中也出现了一些问题: 14 | 15 | - 主节点更新了schema而从节点未sync 16 | - 从节点中部分sync成功,而不是全部 17 | 18 | 而很明显的是kylin中所有的数据,包括所有元数据都是落地在HBase中的,那唯一导致节点间数据不一致的可能就只有各个节点都有本地缓存的情况了。为了理解原理方便debug,我对kylin master-slave的同步原理做了一些源代码层面的剖析。 19 | 20 | 21 | ### 原理剖析 22 | 23 | #### 主从配置方式 24 | 25 | 关于配置的格式,不得不吐槽官方文档的滑水。并没有给出详细的节点配置格式,查阅相关源码才发现正确的配置格式: 26 | 27 | ``` 28 | //kylin.properties下面的配置,根据源码,配置的格式为:user:pwd@host:port 29 | kylin.server.cluster-servers=user:password@host:port,user:password@host:port,user:password@host:port 30 | 31 | ``` 32 | 33 | #### 流程解析 34 | 35 | ![流程解析](http://ol7zjjc80.bkt.clouddn.com/master-slave-kylin.png) 36 | 37 | #### 源码解析 38 | 39 | - 先来看看整个同步机制的核心BroadCaster类的实现 40 | 41 | ``` 42 | //Broadcaster的构造函数 43 | private Broadcaster(final KylinConfig config) { 44 | this.config = config; 45 | //获取kylin.properties中"kylin.server.cluster-servers"配置的值 46 | //也就是集群中所有节点的配置了 47 | final String[] nodes = config.getRestServers(); 48 | if (nodes == null || nodes.length < 1) { 49 | logger.warn("There is no available rest server; check the 'kylin.server.cluster-servers' config"); 50 | broadcastEvents = null; // disable the broadcaster 51 | return; 52 | } 53 | logger.debug(nodes.length + " nodes in the cluster: " + Arrays.toString(nodes)); 54 | 55 | //开一个单线程,不间断的循环从broadcastEvents队列里面获取注册的事件。 56 | Executors.newSingleThreadExecutor(new DaemonThreadFactory()).execute(new Runnable() { 57 | @Override 58 | public void run() { 59 | final List restClients = Lists.newArrayList(); 60 | for (String node : config.getRestServers()) { 61 | //根据配置的节点信息注册RestClient 62 | restClients.add(new RestClient(node)); 63 | } 64 | final ExecutorService wipingCachePool = Executors.newFixedThreadPool(restClients.size(), new DaemonThreadFactory()); 65 | while (true) { 66 | try { 67 | final BroadcastEvent broadcastEvent = broadcastEvents.takeFirst(); 68 | logger.info("Announcing new broadcast event: " + broadcastEvent); 69 | for (final RestClient restClient : restClients) { 70 | wipingCachePool.execute(new Runnable() { 71 | @Override 72 | public void run() { 73 | try { 74 | restClient.wipeCache(broadcastEvent.getEntity(), broadcastEvent.getEvent(), broadcastEvent.getCacheKey()); 75 | } catch (IOException e) { 76 | logger.warn("Thread failed during wipe cache at " + broadcastEvent, e); 77 | } 78 | } 79 | }); 80 | } 81 | } catch (Exception e) { 82 | logger.error("error running wiping", e); 83 | } 84 | } 85 | } 86 | }); 87 | } 88 | 89 | ``` 90 | 91 | 通过Broadcaster的构造函数其实就能清楚整个同步过程的大概逻辑了。无非就是启动一个线程去轮询阻塞队列里面的元素,有的话就消费下来广播到其他从节点从而达到清理缓存的目的。 92 | 93 | - 再来看看广播的实际逻辑实现,基本封装在RestClient中 94 | 95 | ``` 96 | 97 | //此处是根据配置的节点信息正则匹配:"user:pwd@host:port" 98 | public RestClient(String uri) { 99 | Matcher m = fullRestPattern.matcher(uri); 100 | if (!m.matches()) 101 | throw new IllegalArgumentException("URI: " + uri + " -- does not match pattern " + fullRestPattern); 102 | 103 | String user = m.group(1); 104 | String pwd = m.group(2); 105 | String host = m.group(3); 106 | String portStr = m.group(4); 107 | int port = Integer.parseInt(portStr == null ? "7070" : portStr); 108 | 109 | init(host, port, user, pwd); 110 | } 111 | 112 | ``` 113 | 114 | 根据配置的节点信息实例化RestClient,然后在init方法中,拼接wipe cache的url 115 | 116 | ``` 117 | private void init(String host, int port, String userName, String password) { 118 | this.host = host; 119 | this.port = port; 120 | this.userName = userName; 121 | this.password = password; 122 | //拼接rest接口 123 | this.baseUrl = "http://" + host + ":" + port + "/kylin/api"; 124 | 125 | client = new DefaultHttpClient(); 126 | 127 | if (userName != null && password != null) { 128 | CredentialsProvider provider = new BasicCredentialsProvider(); 129 | UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(userName, password); 130 | provider.setCredentials(AuthScope.ANY, credentials); 131 | client.setCredentialsProvider(provider); 132 | } 133 | } 134 | 135 | ``` 136 | 发现kylin所有的交互接口基本上底层都是调用的自己的rest接口,它自己所谓的jdbc的查询方式其实也只是在rest接口上封装了一层,底层还是http请求。可谓是挂羊头卖狗肉了。看看RestClient中怎么去通知其他节点wipe cache的 137 | 138 | ``` 139 | public void wipeCache(String entity, String event, String cacheKey) throws IOException { 140 | String url = baseUrl + "/cache/" + entity + "/" + cacheKey + "/" + event; 141 | HttpPut request = new HttpPut(url); 142 | 143 | try { 144 | HttpResponse response = client.execute(request); 145 | String msg = EntityUtils.toString(response.getEntity()); 146 | 147 | if (response.getStatusLine().getStatusCode() != 200) 148 | throw new IOException("Invalid response " + response.getStatusLine().getStatusCode() + " with cache wipe url " + url + "\n" + msg); 149 | } catch (Exception ex) { 150 | throw new IOException(ex); 151 | } finally { 152 | request.releaseConnection(); 153 | } 154 | } 155 | 156 | ``` 157 | 已经很明了了,就是调的rest接口:/kylin/api/cache/{entity}/{cacaheKey}/{event} 158 | 159 | - 当slave节点接收到wipeCache的指令时的处理逻辑如下: 160 | 161 | ``` 162 | public void notifyMetadataChange(String entity, Event event, String cacheKey) throws IOException { 163 | Broadcaster broadcaster = Broadcaster.getInstance(getConfig()); 164 | 165 | //这里会判断当前节点是否注册为listener了,如果注册了,此逻辑会被ignored 166 | broadcaster.registerListener(cacheSyncListener, "cube"); 167 | 168 | broadcaster.notifyListener(entity, event, cacheKey); 169 | } 170 | 171 | //注册listener的逻辑 172 | public void registerListener(Listener listener, String... entities) { 173 | synchronized (CACHE) { 174 | // ignore re-registration 175 | List all = listenerMap.get(SYNC_ALL); 176 | if (all != null && all.contains(listener)) { 177 | return; 178 | } 179 | 180 | for (String entity : entities) { 181 | if (!StringUtils.isBlank(entity)) 182 | addListener(entity, listener); 183 | } 184 | //注册几种事件类型 185 | addListener(SYNC_ALL, listener); 186 | addListener(SYNC_PRJ_SCHEMA, listener); 187 | addListener(SYNC_PRJ_DATA, listener); 188 | } 189 | } 190 | ``` 191 | 192 | notifyListener主要就是对所有事件处理逻辑的划分,根据事件类型选择处理逻辑,一般sheme的更新走的是默认逻辑 193 | 194 | ``` 195 | public void notifyListener(String entity, Event event, String cacheKey) throws IOException { 196 | synchronized (CACHE) { 197 | List list = listenerMap.get(entity); 198 | if (list == null) 199 | return; 200 | 201 | logger.debug("Broadcasting metadata change: entity=" + entity + ", event=" + event + ", cacheKey=" + cacheKey + ", listeners=" + list); 202 | 203 | // prevents concurrent modification exception 204 | list = Lists.newArrayList(list); 205 | switch (entity) { 206 | case SYNC_ALL: 207 | for (Listener l : list) { 208 | l.onClearAll(this); 209 | } 210 | clearCache(); // clear broadcaster too in the end 211 | break; 212 | case SYNC_PRJ_SCHEMA: 213 | ProjectManager.getInstance(config).clearL2Cache(); 214 | for (Listener l : list) { 215 | l.onProjectSchemaChange(this, cacheKey); 216 | } 217 | break; 218 | case SYNC_PRJ_DATA: 219 | ProjectManager.getInstance(config).clearL2Cache(); // cube's first becoming ready leads to schema change too 220 | for (Listener l : list) { 221 | l.onProjectDataChange(this, cacheKey); 222 | } 223 | break; 224 | //大部分的走向 225 | default: 226 | for (Listener l : list) { 227 | l.onEntityChange(this, entity, event, cacheKey); 228 | } 229 | break; 230 | } 231 | 232 | logger.debug("Done broadcasting metadata change: entity=" + entity + ", event=" + event + ", cacheKey=" + cacheKey); 233 | } 234 | } 235 | 236 | ``` 237 | 238 | 看到default分支会执行onEntityChange这个方法,看一下这个方法干的是什么 239 | 240 | ``` 241 | private Broadcaster.Listener cacheSyncListener = new Broadcaster.Listener() { 242 | @Override 243 | public void onClearAll(Broadcaster broadcaster) throws IOException { 244 | removeAllOLAPDataSources(); 245 | cleanAllDataCache(); 246 | } 247 | 248 | @Override 249 | public void onProjectSchemaChange(Broadcaster broadcaster, String project) throws IOException { 250 | removeOLAPDataSource(project); 251 | cleanDataCache(project); 252 | } 253 | 254 | @Override 255 | public void onProjectDataChange(Broadcaster broadcaster, String project) throws IOException { 256 | removeOLAPDataSource(project); // data availability (cube enabled/disabled) affects exposed schema to SQL 257 | cleanDataCache(project); 258 | } 259 | 260 | @Override 261 | public void onEntityChange(Broadcaster broadcaster, String entity, Event event, String cacheKey) throws IOException { 262 | if ("cube".equals(entity) && event == Event.UPDATE) { 263 | final String cubeName = cacheKey; 264 | new Thread() { // do not block the event broadcast thread 265 | public void run() { 266 | try { 267 | Thread.sleep(1000); 268 | cubeService.updateOnNewSegmentReady(cubeName); 269 | } catch (Throwable ex) { 270 | logger.error("Error in updateOnNewSegmentReady()", ex); 271 | } 272 | } 273 | }.start(); 274 | } 275 | } 276 | }; 277 | 278 | ``` 279 | 280 | 看到对于cache的同步是单独实现了一个listener的,Event为update的时候,会单独启动一个线程去执行刷新缓存操作 281 | 282 | ### 加入简单的重试逻辑 283 | 284 | 由于目前对于同步失败的猜想是目标服务短暂不可用(响应超时或者处于失败重启阶段),于是我只是单纯的将失败的任务重新塞入broadcastEvents队列尾部供再一次调用。当然这种操作过于草率和暴力,却也是验证猜想最简单快速的方式。 285 | 286 | ``` 287 | 288 | for (final RestClient restClient : restClients) { 289 | wipingCachePool.execute(new Runnable() { 290 | @Override 291 | public void run() { 292 | try { 293 | restClient.wipeCache(broadcastEvent.getEntity(), broadcastEvent.getEvent(), 294 | broadcastEvent.getCacheKey()); 295 | } catch (IOException e) { 296 | logger 297 | .warn("Thread failed during wipe cache at {}, error msg: {}", broadcastEvent, 298 | e.getMessage()); 299 | try { 300 | //这里重新塞入队列尾部,等待重新执行 301 | broadcastEvents.putLast(broadcastEvent); 302 | logger.info("put failed broadcastEvent to queue. broacastEvent: {}", 303 | broadcastEvent); 304 | } catch (InterruptedException ex) { 305 | logger.warn("error reentry failed broadcastEvent to queue, broacastEvent:{}, error: {} ", 306 | broadcastEvent, ex); 307 | } 308 | } 309 | } 310 | }); 311 | } 312 | } 313 | 314 | ``` 315 | 316 | 编译部署之后,日志中出现了如下错误: 317 | 318 | ``` 319 | Thread failed during wipe cache at java.lang.IllegalStateException: Invalid use of BasicClientConnManager: connection still allocated. 320 | 321 | ``` 322 | 323 | 比较意外,不过也终于发现了问题的所在。Kylin在启动的时候会按照配置的nodes实例化一次RestClient,之后就直接从缓存中拿了,而kylin用的DefaultHttpClient每次只允许一次请求,请求完必须释放链接,否则无法复用HttpClient。所以需要修改wipeCache方法的逻辑如下: 324 | 325 | ``` 326 | public void wipeCache(String entity, String event, String cacheKey) throws IOException { 327 | String url = baseUrl + "/cache/" + entity + "/" + cacheKey + "/" + event; 328 | HttpPut request = new HttpPut(url); 329 | 330 | HttpResponse response =null; 331 | try { 332 | response = client.execute(request); 333 | String msg = EntityUtils.toString(response.getEntity()); 334 | 335 | if (response.getStatusLine().getStatusCode() != 200) 336 | throw new IOException("Invalid response " + response.getStatusLine().getStatusCode() + " with cache wipe url " + url + "\n" + msg); 337 | } catch (Exception ex) { 338 | throw new IOException(ex); 339 | } finally { 340 | //确保释放连接 341 | if(response!=null) { 342 | EntityUtils.consume(response.getEntity()); 343 | } 344 | request.releaseConnection(); 345 | } 346 | } 347 | 348 | ``` 349 | -------------------------------------------------------------------------------- /docs/olap/kylin-query原理剖析.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: kylin query原理剖析 3 | date: 2017-10-31 11:15:14 4 | tags: 5 | - kylin 6 | - Java 7 | - 源码 8 | --- 9 | 10 | 11 | ## 前言 12 | 13 | 最近我们组负责数据建模的同学抱怨kylin的relization选择策略:同一个project下一条查询语句本来期望命中某一个cube的,结果系统却选择了其他cube。之前也有大概翻阅过kylin这块的实现源码,知道如果同一个project下如果有多个满足条件的的实现,会按照成本排序并选择成本最低的那个实现。对于成本这块的度量标准,没有做过多研究,于是带着问题,对这块源码进行了一次梳理。 14 | 15 | ## 源码剖析 16 | 17 | 为使博文简洁相关实现只贴部分核心代码,以下所指的Realization对应于构建好的Cube。 18 | 19 | #### 查询入口 20 | 21 | - QueryService.doQueryWithCache() 22 | 23 | ``` 24 | //kylin.query.cache-enabled是否开启,如果开启将会从cache里面去读结果 25 | if (queryCacheEnabled) { 26 | sqlResponse = searchQueryInCache(sqlRequest); 27 | } 28 | 29 | try { 30 | if (null == sqlResponse) { 31 | if (isSelect) { 32 | //查询入口 33 | sqlResponse = query(sqlRequest); 34 | } else if (kylinConfig.isPushDownEnabled() && kylinConfig.isPushDownUpdateEnabled()) { 35 | //如果开启了pushDown的话允许非查询的sql,如update 36 | sqlResponse = update(sqlRequest); 37 | } else { 38 | logger.debug("Directly return exception as the sql is unsupported, and query pushdown is disabled"); 39 | throw new BadRequestException(msg.getNOT_SUPPORTED_SQL()); 40 | } 41 | ... 42 | catch(){ 43 | ... 44 | } 45 | 46 | ``` 47 | 48 | 这里,我们忽略从缓存中查找(searchQueryInCache),以及非select查询的情况,单单从一次正常的查询进行分析,进入query方法。 49 | 50 | - QueryService.query() 51 | 52 | query方法相对来说比较简单,记录了query开始和结束的信息,相当于做了一个切面的工作 53 | 54 | ``` 55 | public SQLResponse query(SQLRequest sqlRequest) throws Exception { 56 | SQLResponse ret = null; 57 | try { 58 | final String user = SecurityContextHolder.getContext().getAuthentication().getName(); 59 | badQueryDetector.queryStart(Thread.currentThread(), sqlRequest, user); 60 | 61 | ret = queryWithSqlMassage(sqlRequest); 62 | return ret; 63 | 64 | } finally { 65 | String badReason = (ret != null && ret.isPushDown()) ? BadQueryEntry.ADJ_PUSHDOWN : null; 66 | badQueryDetector.queryEnd(Thread.currentThread(), badReason); 67 | } 68 | } 69 | 70 | ``` 71 | 72 | 其中badQueryDetector是一个单起的线程,用来统计和监测bad query的。当有bad query时notify相关的观察者,做一些操作,如打印日志,记录bad query等。kylin 中很多事件的通知都是通过生产者消费者模式订阅发布的。继续进入queryWithSqlMessage() 73 | 74 | - QueryService.queryWithSqlMessage() 75 | 76 | ``` 77 | 78 | Connection conn = null; 79 | try { 80 | conn = QueryConnection.getConnection(sqlRequest.getProject()); 81 | ... 82 | return execute(correctedSql, sqlRequest, conn); 83 | ... 84 | } finally { 85 | DBUtils.closeQuietly(conn); 86 | } 87 | 88 | ``` 89 | 90 | 这个方法里首先获取了数据库连接,kylin的查询的中间层是基于Calcite的,接下来会看一下QueryConnection背后的逻辑。不过话说回来kylin这种整个大块的try catch异常捕获的机制某种意义上来说是种不负责任的表现。 91 | 92 | - QueryConnection.getConnection(): 93 | 94 | ``` 95 | public static Connection getConnection(String project) throws SQLException { 96 | if (!isRegister) { 97 | DriverManager.registerDriver(new Driver()); 98 | isRegister = true; 99 | } 100 | File olapTmp = OLAPSchemaFactory.createTempOLAPJson(project, KylinConfig.getInstanceFromEnv()); 101 | Properties info = new Properties(); 102 | info.put("model", olapTmp.getAbsolutePath()); 103 | return DriverManager.getConnection("jdbc:calcite:", info); 104 | } 105 | 106 | ``` 107 | 108 | 方法比较简单,主要是通过OLAPSchemaFactory.createTempOLAPJson()生成了连接的元数据文件,用来创建连接 109 | 110 | 111 | - OLAPSchemaFactory 112 | 113 | OLAPSchemaFactory 实现了calcite的 SchemaFactory接口,实现了create方法,用来创建连接时生成Schema 114 | 115 | ``` 116 | @Override 117 | public Schema create(SchemaPlus parentSchema, String schemaName, Map operand) { 118 | String project = (String) operand.get(SCHEMA_PROJECT); 119 | Schema newSchema = new OLAPSchema(project, schemaName, false); 120 | return newSchema; 121 | } 122 | 123 | 124 | 125 | ``` 126 | 127 | 在OLAPSchema的init方法中调用了KylinConfigBase.getStorageUrl方法,此方法返回了我们在配置文件中配置的kylin数据的存储信息 128 | 129 | ``` 130 | public StorageURL getStorageUrl() { 131 | String url = getOptional("kylin.storage.url", "default@hbase"); 132 | 133 | // for backward compatibility 134 | // 对2.0早期版本的配置做了兼容 135 | if ("hbase".equals(url)) 136 | url = "default@hbase"; 137 | 138 | return StorageURL.valueOf(url); 139 | } 140 | 141 | ``` 142 | 这里也可以看出kylin默认的存储系统是HBase 143 | 144 | - QueryService.execute() 145 | 146 | 从之前的QueryService.queryWithSqlMessage()方法继续往下深入到 execute()方法 147 | 148 | ``` 149 | ResultSet resultSet = null; 150 | if (isPrepareStatementWithParams(sqlRequest)) { 151 | stat = conn.prepareStatement(correctedSql); // to be closed in the finally 152 | PreparedStatement prepared = (PreparedStatement) stat; 153 | processStatementAttr(prepared, sqlRequest); 154 | for (int i = 0; i < ((PrepareSqlRequest) sqlRequest).getParams().length; i++) { 155 | setParam(prepared, i + 1, ((PrepareSqlRequest) sqlRequest).getParams()[i]); 156 | } 157 | resultSet = prepared.executeQuery(); 158 | } else { 159 | stat = conn.createStatement(); 160 | processStatementAttr(stat, sqlRequest); 161 | resultSet = stat.executeQuery(correctedSql); 162 | } 163 | 164 | ``` 165 | 166 | - OLAPTable 167 | 168 | 最后查出的结果是在resultSet里,追踪到这一步发现再往下追踪都是Calcite底层的逻辑了,kylin肯定是对Calcite 做了一定的扩展,并且将结果按照kylin预定义的规则做了各种聚合操作。Calcite文档中表示,可以实现三种类型的Table: 169 | 170 | - a simple implementation of Table, using the ScannableTable interface, that enumerates all rows directly; 171 | - a more advanced implementation that implements FilterableTable, and can filter out rows according to simple predicates; 172 | - advanced implementation of Table, using TranslatableTable, that translates to relational operators using planner rules. 173 | 174 | 发现在core-query模块中OLAPTable 实现了TranslatableTable。而OLAPTable 中实现的asQueryable方法有三种Enumerator的实现,这里默认选的是OLAP的实现。 175 | 176 | ``` 177 | 178 | public Queryable asQueryable(QueryProvider queryProvider, SchemaPlus schema, String tableName) { 179 | return new AbstractTableQueryable(queryProvider, schema, this, tableName) { 180 | @SuppressWarnings("unchecked") 181 | public Enumerator enumerator() { 182 | final OLAPQuery query = new OLAPQuery(EnumeratorTypeEnum.OLAP, 0); 183 | return (Enumerator) query.enumerator(); 184 | } 185 | }; 186 | } 187 | 188 | 189 | OLAPQuery.enumerator 190 | public Enumerator enumerator() { 191 | OLAPContext olapContext = OLAPContext.getThreadLocalContextById(contextId); 192 | switch (type) { 193 | case OLAP: 194 | return BackdoorToggles.getPrepareOnly() ? new EmptyEnumerator() : new OLAPEnumerator(olapContext, optiqContext); 195 | case LOOKUP_TABLE: 196 | return BackdoorToggles.getPrepareOnly() ? new EmptyEnumerator() : new LookupTableEnumerator(olapContext); 197 | case HIVE: 198 | return BackdoorToggles.getPrepareOnly() ? new EmptyEnumerator() : new HiveEnumerator(olapContext); 199 | default: 200 | throw new IllegalArgumentException("Wrong type " + type + "!"); 201 | } 202 | } 203 | 204 | ``` 205 | 206 | - OLAPEnumerator.queryStorage() 207 | 208 | 由OLAPTable.asQueryable进入,到了OLAPEnumerator.queryStorage(),终于能看到真实的查库操作了。 209 | 210 | ``` 211 | private ITupleIterator queryStorage() { 212 | logger.debug("query storage..."); 213 | 214 | // bind dynamic variables 215 | bindVariable(olapContext.filter); 216 | 217 | olapContext.resetSQLDigest(); 218 | SQLDigest sqlDigest = olapContext.getSQLDigest(); 219 | 220 | // query storage engine 221 | IStorageQuery storageEngine = StorageFactory.createQuery(olapContext.realization); 222 | ITupleIterator iterator = storageEngine.search(olapContext.storageContext, sqlDigest, olapContext.returnTupleInfo); 223 | if (logger.isDebugEnabled()) { 224 | logger.debug("return TupleIterator..."); 225 | } 226 | 227 | return iterator; 228 | } 229 | 230 | ``` 231 | 232 | 这里StorageEngine 由StorageFactory创建,且有三种不同的实现,默认还是HBase 233 | 234 | ``` 235 | private static ThreadLocal> storages = new ThreadLocal<>(); 236 | 237 | public static IStorage storage(IStorageAware aware) { 238 | ImplementationSwitch current = storages.get(); 239 | if (storages.get() == null) { 240 | current = new ImplementationSwitch<>(KylinConfig.getInstanceFromEnv().getStorageEngines(), IStorage.class); 241 | storages.set(current); 242 | } 243 | return current.get(aware.getStorageType()); 244 | } 245 | 246 | //KylinConfig.getInstanceFromEnv().getStorageEngines() 247 | public Map getStorageEngines() { 248 | Map r = Maps.newLinkedHashMap(); 249 | // ref constants in IStorageAware 250 | r.put(0, "org.apache.kylin.storage.hbase.HBaseStorage"); 251 | r.put(1, "org.apache.kylin.storage.hybrid.HybridStorage"); 252 | r.put(2, "org.apache.kylin.storage.hbase.HBaseStorage"); 253 | r.putAll(convertKeyToInteger(getPropertiesByPrefix("kylin.storage.provider."))); 254 | return r; 255 | } 256 | 257 | ``` 258 | 259 | - OLAPTableScan.register() 260 | 261 | 由于OLAPTable实现了TranslatableTable,它会通过一系列的relation operators将结果聚合,relation operators的注册逻辑在OLAPTableScan中。 262 | 263 | ``` 264 | @Override 265 | public void register(RelOptPlanner planner) { 266 | // force clear the query context before traversal relational operators 267 | OLAPContext.clearThreadLocalContexts(); 268 | 269 | // register OLAP rules 270 | planner.addRule(OLAPToEnumerableConverterRule.INSTANCE); 271 | planner.addRule(OLAPFilterRule.INSTANCE); 272 | planner.addRule(OLAPProjectRule.INSTANCE); 273 | planner.addRule(OLAPAggregateRule.INSTANCE); 274 | planner.addRule(OLAPJoinRule.INSTANCE); 275 | planner.addRule(OLAPLimitRule.INSTANCE); 276 | planner.addRule(OLAPSortRule.INSTANCE); 277 | planner.addRule(OLAPUnionRule.INSTANCE); 278 | planner.addRule(OLAPWindowRule.INSTANCE); 279 | ... 280 | } 281 | 282 | ``` 283 | 284 | 这里着重看OLAPToEnumerableConverterRule 里返回的 OLAPToEnumerableConverter的实现,它是解释我在前言里提到的问题的关键。 285 | 286 | - OLAPToEnumerableConverter.implement() 287 | 288 | 这里面有对所有满足query条件的realization选择的实现 289 | 290 | ``` 291 | 292 | public Result implement(EnumerableRelImplementor enumImplementor, Prefer pref) { 293 | 294 | ... 295 | // identify model & realization 296 | List contexts = listContextsHavingScan(); 297 | 298 | // intercept query 299 | List intercepts = QueryInterceptorUtil.getQueryInterceptors(); 300 | for (QueryInterceptor intercept : intercepts) { 301 | intercept.intercept(contexts); 302 | } 303 | 304 | //RealizationChooser 中有对Realization选择的具体实现 305 | RealizationChooser.selectRealization(contexts); 306 | ... 307 | 308 | return impl.visitChild(this, 0, inputAsEnum, pref); 309 | } 310 | 311 | 312 | ``` 313 | 314 | - RealizationChooser.attemptSelectRealization() 315 | 316 | attemptSelectRealization方法里面主要干了两件事: 1)拉取属于该project与factTableName下的所有Realization,经过一系列的条件过滤掉不符合query的Realization,并将符合条件的Realization按照RealizationCost排序。2)对第一步收集的Realization map,调用QueryRouter.selectRealization(),一旦QueryRouter.selectRealization()有返回值立即中断循环返回最终选择的Realization 317 | 318 | - RealizationChooser.makeOrderedModelMap() 部分的实现逻辑如下: 319 | 320 | ``` 321 | //按条件过滤realization 322 | for (IRealization real : realizations) { 323 | //过滤disabled cube 324 | if (real.isReady() == false) { 325 | context.realizationCheck.addIncapableCube(real, 326 | RealizationCheck.IncapableReason.create(RealizationCheck.IncapableType.CUBE_NOT_READY)); 327 | continue; 328 | } 329 | //过滤不包含querycontext里面全部的columns 330 | if (containsAll(real.getAllColumnDescs(), first.allColumns) == false) { 331 | context.realizationCheck.addIncapableCube(real, RealizationCheck.IncapableReason 332 | .notContainAllColumn(notContain(real.getAllColumnDescs(), first.allColumns))); 333 | continue; 334 | } 335 | //过滤存在黑名单里面的cube 336 | if (RemoveBlackoutRealizationsRule.accept(real) == false) { 337 | context.realizationCheck.addIncapableCube(real, RealizationCheck.IncapableReason 338 | .create(RealizationCheck.IncapableType.CUBE_BLACK_OUT_REALIZATION)); 339 | continue; 340 | } 341 | 342 | //过滤完,按RealizationCost排序 343 | RealizationCost cost = new RealizationCost(real); 344 | DataModelDesc m = real.getModel(); 345 | Set set = models.get(m); 346 | if (set == null) { 347 | set = Sets.newHashSet(); 348 | set.add(real); 349 | models.put(m, set); 350 | costs.put(m, cost); 351 | } else { 352 | set.add(real); 353 | RealizationCost curCost = costs.get(m); 354 | if (cost.compareTo(curCost) < 0) 355 | costs.put(m, cost); 356 | } 357 | } 358 | 359 | ``` 360 | 361 | 重点就在RealizationCost的实现里了 362 | 363 | ``` 364 | public RealizationCost(IRealization real) { 365 | // ref Candidate.PRIORITIES 366 | this.priority = Candidate.PRIORITIES.get(real.getType()); 367 | 368 | // ref CubeInstance.getCost() 369 | int c = real.getAllDimensions().size() * CubeInstance.COST_WEIGHT_DIMENSION 370 | + real.getMeasures().size() * CubeInstance.COST_WEIGHT_MEASURE; 371 | for (JoinTableDesc join : real.getModel().getJoinTables()) { 372 | if (join.getJoin().isInnerJoin()) 373 | c += CubeInstance.COST_WEIGHT_INNER_JOIN; 374 | } 375 | this.cost = c; 376 | } 377 | 378 | 379 | ``` 380 | 381 | 到此,对于kylin的Realization的成本计算规则清楚了。就是对dimension,measure,jointable三个维度的数量进行加权求和,得到的就是每个Realization对应的成本。相对的,每个维度对应的权重是有所斟酌的,dimension对应的是10,measure为1(考虑到是预计算的结果),jointable为100。从这也能看出建模时应该考虑的优化方向:避免过多的dimension,以及jointable的操作,结果尽量从预计算中出。 382 | 383 | ## 总结 384 | 385 | 这次经过对kylin query源码的分析,基本上对kylin的核心代码都过了一遍,学习了不少优秀的代码解耦方式,也对底层原理加深了理解。关于RealizationCost的实现,目前kylin实现比较简单,在遍历所有满足条件的实现时找到Realization便返回处理的有些过于仓促。对于其是map的结构或许kylin在之后的扩展方面也是有所考虑。目前我们还不打算扩展Realizaiton的选择策略,了解了源码就可以在建模层面将查询结果不如意的情况给规避了。 386 | -------------------------------------------------------------------------------- /docs/olap/okhttp-support-100-continue-for-palo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: okhttp support 100-continue for palo 3 | date: 2018-04-24 12:40:32 4 | tags: 5 | - kylin 6 | - Java 7 | - 源码 8 | --- 9 | 10 | ## 前言 11 | 12 | 虽然百度的Palo是个很强大的,基于MPP Search Engine的OLAP框架,但是由于处于开源的早期阶段,各方面都不是很完善。其中,Palo集群的稳定性对于日渐依赖Palo的核心的业务来说显得尤为重要。最近也一直在做Palo稳定性建设相关的工作。在对全链路监控这块,自然而然地想到对业务中使用频繁的http-mini-load接口进行SDK封装,以实现对请求进行失败重试以及失败率的监控报警的功能。 13 | 14 | ## 遇到的问题 15 | 16 | #### 问题描述 17 | 18 | - 在实际的SDK封装中,用到了流行的okhttp,发送请求如下: 19 | 20 | ``` 21 | PaloHttpUtil paloHttpUtil = PaloHttpUtil.builder().build(); 22 | String bodyStr = "1 1 2018-04-12 20:13:00 4101628087476389 1\n1 1 2018-04-12 20:13:00 4205819141030267 2"; 23 | System.out.println(paloHttpUtil 24 | .put(String.format("http://xxxx:8030/api/feed/comment_trend/_load?" + 25 | "label=comment_trend_load_%s&columns=trend_type,user_type,timestamp,mid,count", prefix)) 26 | .auth("feed", "feed") 27 | .header("Expect", "100-continue") 28 | .body(bodyStr, ContentType.WILDCARD) 29 | .asyncSend(3, 10000, TimeUnit.MILLISECONDS) 30 | .string() 31 | 32 | ``` 33 | 34 | response: code 307 Temporary {"Status":"Failed"} 也就是说发生了重定向。对于服务器为何不在一个request中直接接收PUT的数据,这块贴一下100-continue的定义。 35 | 36 | ``` 37 | 38 | 100 (Continue/继续) :如果服务器收到头信息中带有100-continue的请求,这是指客户端询问是否可以在后续的请求中发送附件。 39 | 在这种情况下,服务器用100(SC_CONTINUE)允许客户端继续或用417 (Expectation Failed)告诉客户端不同意接受附件。这个状态码是 HTTP 1.1中新加入的。 40 | 41 | 42 | ``` 43 | 44 | 至于为什么发生了重定向先不考虑,先研究一下为什么okhttp不支持307重定向。 45 | 46 | 47 | #### 源码追踪 48 | 49 | - 我们通过debug深入源码看看在哪一步处理的307重定向 50 | 51 | ![](http://ol7zjjc80.bkt.clouddn.com/realInterceptor.png) 52 | 53 | 由图,我们可以看到对于request/response的处理,okhttp采取了插件的形式,类似于Spring AOP 源码中切面invoke方法的处理方式。这种插件的方式意味着我们可以定制化请求处理逻辑。借官方原图: 54 | 55 | ![](https://raw.githubusercontent.com/wiki/square/okhttp/interceptors@2x.png) 56 | 57 | - 由于interceptor里面是可以执行重试逻辑或直接返回response,所以,我们再深入看看在哪个Interceptor里直接返回了response。断点打到RetryAndFollowUpInterceptor里如下的代码块: 58 | 59 | ``` 60 | Request followUp = this.followUpRequest(response, streamAllocation.route()); 61 | //这里followUP返回null 62 | if (followUp == null) { 63 | if (!this.forWebSocket) { 64 | streamAllocation.release(); 65 | } 66 | 67 | return response; 68 | } 69 | 70 | ``` 71 | 72 | 由于followUp返回了null,导致response直接返回。说明当前的redirect策略不支持307重定向,再深入具体的重定向策略followUpRequest 73 | 74 | ![](http://ol7zjjc80.bkt.clouddn.com/redirectInterceptor.png) 75 | 76 | 发现307,308如果request method不等于GET且不为HEAD时直接返回了null,由此对于307 的PUT重定向操作okhttp是不支持的 77 | 78 | 79 | ## 问题的解决 80 | 81 | #### okhttp 添加自定义redirect interceptor 82 | 83 | - 前面提到,我们可以往okhttpClient里面添加自定义的interceptor来达到对request/response灵活劫持的目的。于是考虑加一个支持palo put 307 重定向的redirect interceptor. 84 | 85 | - 大致的策略还是跟RetryAndFollowUpInterceptor一样,对followUpRequest的方法做了修改 86 | 87 | ``` 88 | case 300: 89 | case 301: 90 | case 302: 91 | case 303: 92 | case 307: 93 | 94 | ``` 95 | 将307放到了300-303并列的位置,进入redirect逻辑。去掉将method统一替换成GET的逻辑: 96 | 97 | ``` 98 | if (HttpMethod.redirectsToGet(method)) { 99 | requestBuilder.method("GET", (RequestBody)null); 100 | } else { 101 | RequestBody requestBody = maintainBody ?userResponse.request().body() : null; 102 | requestBuilder.method(method, requestBody); 103 | } 104 | 105 | ``` 106 | 改为: 107 | 108 | ``` 109 | boolean maintainBody = HttpMethod.requiresRequestBody(method); 110 | RequestBody requestBody = maintainBody ? userResponse.request().body() : null; 111 | requestBuilder.method(method, requestBody); 112 | 113 | ``` 114 | 为了带上用户名密码,去掉逻辑: 115 | 116 | ``` 117 | if (!this.sameConnection(userResponse, url)){ 118 | requestBuilder.removeHeader("Authorization"); 119 | } 120 | 121 | ``` 122 | 123 | - 至此,Palo http-mini-load put 307 Temporary 重定向问题得到了解决 124 | 125 | #### 使用apache httpcomponents 126 | 127 | - 在apache httpcomponents中,可以设置redirectStrategy,来达到重定向的策略,且不受http code的约束 128 | 129 | ![](http://ol7zjjc80.bkt.clouddn.com/apacheHttpComponents.png) 130 | 131 | 可以看到本身的redirect机制还是比较强大的 132 | 133 | - 不过鉴于本人习惯用okhttp,且用自定义的interceptro也能解决问题,所以暂时没有采用这种方法。 134 | 135 | ## 总结 136 | 137 | - okhttp 不支持307除get意外其他request method重定向的原因不得而知。不过对于开源的组件,也不必要满足各种各样奇怪的胃口,对于需求的定制化留好可扩展接口就行。 138 | -------------------------------------------------------------------------------- /docs/scheduler/Hadoop-Rpc源码分析.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hadoop Rpc源码分析 3 | date: 2019-11-05 01:38:28 4 | tags: 5 | - 架构 6 | - Hadoop 7 | - Yarn 8 | --- 9 | 10 | Hadoop生态系统中Rpc底层基本都是走的一套实现,所以有必要对Rpc底层实现做一次系统性的梳理总结。 11 | [知乎链接](https://zhuanlan.zhihu.com/p/88768710) 12 | 13 | **Client&Server实现入口** 14 | 15 | RpcEngine作为Rpc实现的接口,用来获取client端proxy和server端的server 16 | 17 | - 主要的实现是WritableRpcEngine,ProtobufRpcEngine(现默认),两者的区别主要是序列化与反序列化的协议不同;内部都有继承Server构成完整Rpc Server的实现类 18 | - IPC.Server是两种序列化协议的基类,org.apache.hadoop.ipc.Server 主要实现了Reactor的请求处理模式 19 | 20 | ![img](https://pic4.zhimg.com/v2-c8db58fa71163284be19a5ef5d18226b_b.jpg) 21 | 22 | ## Client & Server 构造方式 23 | 24 | - 按照序列化协议区分两种实现:ProtobufRpcEngine, WriteableRpcEngine 25 | - 通过接口getProxy 构造RpcClient 26 | 27 | ![img](https://pic2.zhimg.com/v2-88bd21ee56a1cd64a95d970c67c743a5_b.jpg) 28 | 29 | ![img](https://pic1.zhimg.com/v2-92891b943d699abd3dbb68332be7c6e4_b.jpg) 30 | 31 | ![img](https://pic2.zhimg.com/v2-1a71694dbcf4d96672256ed37fa33cf9_b.jpg) 32 | 33 | - getServer构造RpcServer 34 | 35 | ![img](https://pic2.zhimg.com/v2-7ea92e7484f556a3f658e79a751e19a1_b.jpg) 36 | 37 | ### RPC Client剖析 38 | 39 | 总体来说Client端实现比较简单,用hashTable的结构来维护connectionId -> connections以及callId -> calls 对应关系,使得请求响应不需要有严格的顺序性 40 | 41 | - Ipc.Client构成 42 | - callIdCounter:callId 发号器 43 | - connections: HashTable结构,用来维护Id → Connection的映射 44 | - sendParamsExecutor:请求发送线程池 45 | 46 | ![img](https://pic2.zhimg.com/v2-9b923f98235d7881f79f9525f2a025f5_b.jpg) 47 | 48 | - Connection:自身是一个线程 49 | - calls: HashTable结构,请求结束将从call从HashTable中移除 50 | - sendRpcRequest:用户线程中通过call入口调用,用户线程阻塞 51 | - receiveRpcResponse: run中不断轮询server看结果是否就绪 52 | - client 处理过程 53 | 54 | ![img](https://pic1.zhimg.com/v2-6a94a1d6f6792b1ae190c54992ba3158_b.jpg) 55 | 56 | 图片摘自《Hadoop技术内幕:深入解析MapReduce架构设计与实现原理 》 57 | 58 | - 通过反射获取到方法描述,走client Invoker调用远程实现 59 | 60 | ![img](https://pic3.zhimg.com/v2-a73c35b6ffe44b32f40f638f110d0ac6_b.jpg) 61 | 62 | - getConnection中与远程server 建立socket 连接,并将连接加入connections集合中 63 | - 在用户线程中调用connection.sendRpcRequest,阻塞的获取结果 64 | 65 | ![img](https://pic3.zhimg.com/v2-9ace991f5df04fde6559437de1e46402_b.jpg) 66 | 67 | - Connection自身run方法中不停的轮询Server接收返回结果 68 | 69 | ![img](https://pic4.zhimg.com/v2-d4ebd6403c80e86bb7ea7acff2b3e643_b.jpg) 70 | 71 | - waitForWork用来判断当前connection是否应该继续存在,返回true则继续轮询server,如果是false则关闭当前connection 72 | 73 | ![img](https://pic4.zhimg.com/v2-c46e3a852373a74fa752a8b1edfe176f_b.jpg) 74 | 75 | - **receiveRpcResponse**接收服务端返回结果,将calls移除table,可以乱序,通过ConnectionId索引,**不需要同步代码块,因为只有一个receiver** 76 | 77 | ![img](https://pic1.zhimg.com/v2-347d4bb3e5d6bafe471a1b8e686343e8_b.jpg) 78 | 79 | ### RPC Server剖析 80 | 81 | - Server端采用经典的Reactor模式,利用IO多路复用实现事件驱动 82 | - 痛点在于多路复用之前的处理模式,socket read/write是阻塞的,一个线程只能处理一个socket;使用selector之后一个进程可以监视多个进程文件描述符 83 | 84 | 参考阅读:[Reactor模式](https://www.cnblogs.com/crazymakercircle/p/9833847.html)、[Java NIO 底层原理 ](https://www.cnblogs.com/crazymakercircle/p/10225159.html#4310290) 、[select、poll、epoll](https://www.jianshu.com/p/dfd940e7fca2) 85 | 86 | ![img](https://pic1.zhimg.com/v2-58df22a9108b9c724c8b757f6357d8c4_b.jpg) 87 | 88 | 图片摘自《Hadoop技术内幕:深入解析MapReduce架构设计与实现原理 》 89 | 90 | - Reactor 工作图 91 | - Reactor:负责响应IO事件,将事件派发到工作线程 92 | - Acceptor:用来接收Client端的请求,建立Client与handler的联系;向Reactor注册handler 93 | - Reader/Sender:为了加快速度,同时做到请求和处理过程的隔离,reader和sender 分别是两个线程池,用来存放该过程处理完后的连接,处理完之后塞入中间队列,等待下一个过程的线程拿去处理就行 94 | - Handler:connection对应的工作线程,会做一些decode, compute, encode工作 95 | 96 | ![img](https://pic4.zhimg.com/v2-64e02ba7cce407bf7d191ce4b08bfdeb_b.jpg) 97 | 98 | **Hadoop RpcServer组成结构** 99 | 100 | - **序列化层**:RpcRequestWrapper, RpcResponseWrapper 101 | - **接口调用层**:RpcInvoker,通过反射方式阻塞调用Server端具体的Service方法;调用前后记录一些metrics信息 102 | - 在handler线程处理逻辑中,通过注册的rpcKind获取对应的RpcInvoker实现,通过反射来调用工作层的Service 103 | - **请求接收/返回层Ipc.Server**:基于Java NIO实现的Reactor 事件驱动模式 104 | - Listener 105 | - selector:监听请求 → 建立连接 → 派发到Reader线程 106 | - Readers 107 | - readSelector:解析&封装Call → 塞入CallQueue 108 | - Handlers:工作线程 109 | - 并行pull CallQueue,调用RpcInvoker处理 110 | - Responder:read request和write response采用不同的selector实现读写分离 111 | - writeSelector 112 | - connectionManager: 定时清理idle时间过长的Connection 113 | - CallQueue:reader handler之间的缓冲队列,**生产消费者模型** 114 | 115 | ### RPC Server 处理流程 116 | 117 | ![img](https://pic2.zhimg.com/v2-38a1ef7504f6e74ba8bb4aa3a0a1bdb5_b.jpg) 118 | 119 | 120 | 121 | - Listener → Reader 请求建立过程:Listener*Reader*Connection 122 | - Listener线程只有一个,通过Selector方式监听客户端的Rpc请求(OP_ACCEPT事件),调用doAccept方法建立连接;此时connectionManager线程开始工作 123 | - 建立连接后,roundbin方式获取一个reader线程,将连接塞入reader线程的pending队列和connectionManager中 124 | 125 | ![img](https://pic4.zhimg.com/v2-3d7ddb917b9a9bba707c2208e9b71c4f_b.jpg) 126 | 127 | ![img](https://pic4.zhimg.com/v2-fb8edf28b12530ea9f49ce030d780f7b_b.jpg) 128 | 129 | - Reader线程doRunLoop中,将pending的connections注册到readSelector中,用来监听一个connection读就绪事件 130 | 131 | ![img](https://pic3.zhimg.com/v2-c3056224c1b460860b0b9c26c2ccc182_b.jpg) 132 | 133 | - 数据读入 → 工作线程 : Reader*Connection*CallQueue 134 | - 而后Reader通过selector方式,只要监听的channel有读事件,则调用doRead方法;其中通过selectionKey获取关联的connection对象,调用connection的readAndProcess方法 135 | - connection.readAndProcess: 主要是将channel里面的数据读入data byteBuffer中,数据读完之后调用processOneRpc 进一步处理 136 | 137 | ![img](https://pic3.zhimg.com/v2-01d207ac87060718231e7d6a32599412_b.jpg) 138 | 139 | ![img](https://pic4.zhimg.com/v2-74ff999fb2c4844c9470e9d3bfefa943_b.jpg) 140 | 141 | - connection. processOneRpc 对buffer decode构造成DataInputStream以及RpcHeader(请求元信息,协议类型等)通过processRpcRequest将请求塞入CallQueue中,等待handlers处理 142 | - connection.processRpcRequest:通过header中指定的rpc engine将dataInputStream根据不同engine反序列化协议反序列化成rpcRequestWrapper;构造Call对象塞入CallQueue, 并incrRpcCount 143 | 144 | ![img](https://pic2.zhimg.com/v2-46cd9573bbe1ffd820a5a22ca16628d9_b.jpg) 145 | 146 | ![img](https://pic1.zhimg.com/v2-195514acc9cc988cd2b7c14f45a535d4_b.jpg) 147 | 148 | - Handler → RpcInvoker → Responder 149 | - Handler线程在Server start的时候就已经构建启动了 150 | - 并行pull callQueue获取队列中未处理的call,调用call方法 151 | 152 | ![img](https://pic1.zhimg.com/v2-cc649767778d9769384fde11b8b7a5c0_b.jpg) 153 | 154 | - 通过rpcKind获取对应的RpcInvoker实现;主看ProtoBufRpcInvoker.call 155 | 156 | ![img](https://pic1.zhimg.com/v2-07fc870601bd5199e9c298828106c598_b.jpg) 157 | 158 | - 通过反射获取server端对应的接口实现,阻塞调用,在调用前后记录一些metrics信息;最后将结果包装成RpcResponseWrapper 159 | 160 | ![img](https://pic2.zhimg.com/v2-2c1300aac072c27f7a7f6d02326643d5_b.jpg) 161 | 162 | ![img](https://pic3.zhimg.com/v2-10f7ea1450e7fcba8eeba0b28cfade16_b.jpg) 163 | 164 | - 当结果处理完成之后,通过setupResponse将结果序列化成byte buffer根据不同engine实现的wrapper 序列化方式有所不同 165 | 166 | ![img](https://pic4.zhimg.com/v2-ced2fa047079e775740313b85008ea2b_b.jpg) 167 | 168 | ![img](https://pic3.zhimg.com/v2-0ec60ac2232816c3d38b72ce5ac1c02e_b.jpg) 169 | 170 | - 调用Responder.doRespond将请求结果返回客户端 171 | - **请求返回处理过程:** 通过Responder线程+ writeSelector 172 | - Responder.doRespond 173 | - 在handler中尽可能的将response一次性写入channel buffer,如果没有剩余则不用注册Responder的Responder.doRespond 174 | - 如果一次性写不完且是在handler线程中,则唤醒writeSelector,将当前channel 注册 SelectionKey.OP_WRITE 异步去处理 175 | 176 | ![img](https://pic4.zhimg.com/v2-3b339c9e8e15060518996edccee9aebf_b.jpg) 177 | 178 | - Responder 线程自身的doRunLoop里面也是通过writeSelector监听OP_WRITE事件处理 179 | 180 | ![img](https://pic1.zhimg.com/v2-7c4c403ee9bff19653e58088d81d7428_b.jpg) 181 | 182 | - **CallQueueManager** 相关 183 | - 默认实现是LinkedBlockingQueue 184 | - 大小通过queueSizePerHandler或ipc.server.handler.queue.size * handler_count 决定 185 | 186 | ![img](https://pic3.zhimg.com/v2-56e6f38dd99dc26c8dbe3ccb84a581aa_b.jpg) 187 | 188 | 189 | 190 | - **ConnectionManager相关**:用来定时清理idle时间过长的connection 191 | - idleScanThreshold: 每次轮询扫描的connections 阈值default 4000 192 | - idleScanInterval: 定时检测线程轮询间隔 default 10000 193 | - maxIdleTime: 一个connection最长idle时间,default 2* 10000 194 | - maxIdleToClose : 一次轮询最多关闭的连接数 default 10 195 | - 一个connection是不是可以被清理由以下条件决定 196 | - connection.isIdle(): rpcCount为0, 也就是Call没有塞入callQueue;在connection.processRpcRequest末尾,如果成功塞入callQueue中的话会incrRpcCount 197 | - lastContact < minLastContact: 198 | - minLastContact: Time.now() - maxIdleTime 199 | - startIdleScan:开启清理线程,随Listener线程启动 200 | 201 | -------------------------------------------------------------------------------- /docs/scheduler/MR任务在Hadoop子系统中状态流转.md: -------------------------------------------------------------------------------- 1 | 深入做hadoop相关的工作也有一段时间了,期间零零散散看了不少源码,但很多都是看完就忘了,很形成结构化的记忆。于是决定通过流程图的方式来刻画一个MR任务在Hadoop子系统中的状态机流转过程。 2 | 3 | ## MR任务提交过程 4 | 5 | ![img](https://pic1.zhimg.com/80/v2-73ff12c7a51dc7962821e06d67c9be1c_hd.png) 6 | 7 | 一个MR任务在hadoop 客户端通过rpc 方式提交到yarn上;大致过程如上图 8 | 9 | - JobSubmitter 10 | - 封装了向yarn(ClientRMService)提交的过程 11 | - 与hdfs交互计算任务输入数据的分片大小,以及将jar包加入DistributedCache中 12 | - ClientServiceDelegate 13 | - 设计的目的是统一封装monitorJob过程获取任务执行状态,counters等信息的rpc client代理;其背后通过Java反射的方式,在任务的不同阶段会分别请求RM, AM, 或MR History Server(以下简称MHS)服务 14 | - 因各种情况会有部分线上任务流量降级穿透到MHS服务,而MHS服务自身实现有较大瓶颈,我们其进行了leveldb方案的改造,整体查询性能提升**20倍** 15 | 16 | ## 分片计算过程 17 | 18 | 下面看一下getSplits过程,我们默认用的是CombineFileInputFormat实现,先上图 19 | 20 | ![img](https://pic4.zhimg.com/80/v2-8add590ce801446aef9d08c031a8c3ff_hd.png) 21 | 22 | 我们知道,MR计算框架强调的是数据本地性。在图中有三个结构nodeToBlocks,rackToBlocks,blockToNodes;其中nodeToBlocks代表的是local级别,优先选择此集合中的分片,当剩下的blocks不足minSizeNode阈值时会通过blockToNodes数据结构,进行次优的分片划分的过程,以此类推。 23 | 24 | ## ApplicationMaster启动过程 25 | 26 | 当一个任务提交到RM后,需要等待RM分配资源启动AM之后才开始后续自己的资源&任务处理过程,先上图(**以下板块省去了内部自实现的调度算法**) 27 | 28 | ![img](https://pic1.zhimg.com/80/v2-616195ee1bab87905cb4e3c31182142c_hd.png) 29 | 30 | 此时涉及到RMApp, RMAppAttempt,RMNode,以及RMContainer等状态机的轮转 31 | 32 | ![img](https://pic4.zhimg.com/80/v2-3f2f539ad5ebb736d7a85d4b606a9683_hd.png) 33 | 34 | ## MR任务状态机流转 35 | 36 | 当Yarn RM通过ContainerManagerProtocol协议将AM Container启动之后,AM便开始了Map/Reduce(一般map执行完之后)任务调度过程 37 | 38 | ![img](https://pic3.zhimg.com/80/v2-c9babe0d2fb818f1037c5c00a369da74_hd.png) 39 | 40 | 运行期间涉及到的状态机有Job, Task, 以及TaskAttempt,当然还有AM register/unregister过程Yarn RM系统对应的状态机转换;大致描述一下流程: 41 | 42 | - MR AM启动,通过Job状态机初始化 43 | - 初始化CommitterEventHandler,用于最后job完成时通过commit过程将temp目录的数据转移到final 目录中 44 | - 初始化Map/Reduce Task以及对应的TaskAttempt(Task具体的某次尝试),通过RMContainerAllocator先对map task进行调度,后进行reduce task调度 45 | - 通过ApplicationMasterService向RM注册自己,代表某个RMAppAttempt对应的AM Container已启动,可以定期向RM发送allocate心跳了 46 | - 在RMContainerAllocator中通过allocate心跳向RM请求资源,得到response之后将分得的container再按优先级assign给对应的task 47 | - 在TaskAttempt得到container之后通过ContainerLanucher向NodeManager请求启动Container 48 | - 任务运行完成做对应的commit,clean操作之后,通过ApplicatioinMasterService告知RM任务完成,此时RMApp/Attempt做任务完成的状态转换 -------------------------------------------------------------------------------- /docs/scheduler/Yarn-Federation源码串读.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Yarn Federation源码串读 3 | date: 2019-11-05 01:47:46 4 | tags: 5 | - 架构 6 | - Hadoop 7 | - Yarn 8 | --- 9 | 10 | [知乎链接](https://zhuanlan.zhihu.com/p/79378807) 11 | 12 | ## Federation架构总览 13 | 14 | - Federation: 主要有四个模块,Router ,StateStore,AMRMProxy, Global Policy Generator;从架构上来看,有点类似于后端的微服务架构中**服务注册发现**模块 15 | 16 | ![img](https://pic4.zhimg.com/v2-7ee20bc86d8be49d25b5ca3897d3278f_b.png) 17 | 18 | ## Router模块 19 | 20 | - 类似于微服务的网关模块;通过state store获取具体的集群配置策略,将client端submit请求转发到对应的subCluster中 21 | - 代码结构 22 | - hadoop-yarn-server-router:router组件核心实现,分为对接admin用户的协议和client用户协议,以及web server三个子模块实现 23 | 24 | ![img](https://pic3.zhimg.com/v2-b6ee24339266083c97b6642c4f3a081e_b.png) 25 | 26 | 27 | 28 | - hadoop-yarn-server-common-federation-router:包含了Router的各种Policy,具体控制router给子集群分配app的策略 29 | 30 | ![img](https://pic2.zhimg.com/v2-e7402699fce4808743375957f68a8b11_b.png) 31 | 32 | 33 | 34 | ### **Router- clientrm** 35 | 36 | - 负责接收客户端命令请求,并根据对应router具体配置的policy将客户端请求转发到HomeSubcluster上 37 | - 在每一个router服务上随着启动,用来监听客户端作业提交,实现了Client与RM沟通的RPC协议接口(ApplicationClientProtocol);作为client的proxy,执行一系列的chain interceptor),通常FederationClientInterceptor需作为最后一个拦截器 38 | - 当然RouterClientRMService某种程度上针对的是Server测,取代原来RM侧**RMClientService**;在客户端具体的调用还是在**YarnClientImpl**;之间通过RPC通信 39 | - 初始化: 获取配置文件中配置的拦截器,默认是DefaultClientRequestInterceptor 40 | 41 | ![img](https://pic3.zhimg.com/v2-74b97de7e4a90be9f4f7f5e703dcbd56_b.png) 42 | 43 | - DefaultClientRequestInterceptor只是做了简单的请求透明转发;没涉及到多子集群的处理 44 | - FederationClientInterceptor:面向client,隐藏了多个sub cluster RM;但是目前只实现了四个接口:**getNewApplication, submitApplication, forceKillApplication and getApplicationReport** 45 | - **FederationClientInterceptor** 46 | - clientRMProxies: 子集群id与对应的通信client的key value集合 47 | - federationFacade: 对应的state store具体实现 48 | - policyFacade: 路由策略的工厂 49 | 50 | ![img](https://pic1.zhimg.com/v2-d6bbfd466b88388bdb9777d17d159210_b.png) 51 | 52 | - 一个任务的提交需经过**FederationClientInterceptor.getNewApplication**和**submitApplication**接口,前者获得新的**applicationId**, 后者通过获得的**applicationId**将任务提交到具体的sub Cluster RM;这一个阶段没有经过与state store的写操作 53 | 54 | ![img](https://pic2.zhimg.com/v2-6824ab1d9c5489ea0264ef5b79f9f075_b.png) 55 | 56 | - getNewApplication实现只是**随机**的选择一个active sub cluster来获取一个新的**applicationId**;而subClustersActive是通过具体实现的**state store**来获取,此处有过滤active的字段 57 | - submitApplication,方法注释有讨论各种failover的处理情况; 58 | - RM没挂的情况:如果state store 更新成功了,则多次提交任务都是幂等的 59 | - RM挂了:则router time out之后重试,选择其他的sub cluster 60 | - Client挂了:跟原来的/ClientRMService/一样 61 | - 通过policyFacade加载策略,根据context与blacklist为当前提交选择sub cluster;具体逻辑在**FederationRouterPolicy.getHomeSubcluster** 62 | 63 | ![img](https://pic4.zhimg.com/v2-c6dbd82e45d5c69bd30b07e4f7e077a3_b.png) 64 | 65 | - 同步提交任务至目标sub cluster 66 | 67 | ![img](https://pic3.zhimg.com/v2-da2fa220baae8ba7238a1b77bebe009a_b.png) 68 | 69 | **疑问&&待确定的点** 70 | 71 | - client —> router —> rm: 这条链路如果router挂了如何failover;**在submitApplication方法上方有较为详细的边界情况处理解释** 72 | - **是否支持多个router?以及在配置中如何指定多个router?防止一个router挂掉的情况** 73 | - **需要确定是否有机制来维系真正存活的cluster,是否会动态摘除down掉的RM** 74 | 75 | ## Policy State Store模块 76 | 77 | ### FederationStateStoreFacade 78 | 79 | - 作为statestore的封装,抽象出一些重试和缓存的逻辑 80 | 81 | ### FederationStateStore 82 | 83 | - 一般采用**ZookeeperFederationStateStore**的方式 84 | - **ZookeeperFederationStateStore** 实现中,对应的数据存储结构如下 85 | 86 | ![img](https://pic4.zhimg.com/v2-b4e79639629bdec688ec4efb6f9b275f_b.png) 87 | 88 | 89 | 90 | - 通过心跳维系了RM是否是active;通过**filterInactiveSubClusters**来决定是否需要过滤存活的RM 91 | 92 | ![img](https://pic1.zhimg.com/v2-1de1b20d5b5a5c01c26e6724695cf440_b.png) 93 | 94 | - **实例化过程** 95 | - 加载配置***yarn.federation.state-store.class***:默认实现是***MemoryFederationStateStore*** 96 | 97 | ![img](https://pic4.zhimg.com/v2-e5888bd36c0c482c10c2bb22782ceb27_b.png) 98 | 99 | ### SubClusterResolver 100 | 101 | - 用来判断某个指定的node是属于哪个子集群的工具类;主要有getSubClusterForNode,getSubClustersForRack方法 102 | - 实例化过程 103 | - 加载配置yarn.federation.subcluster-resolver.class: 默认实现是DefaultSubClusterResolverImpl 104 | 105 | ![img](https://pic2.zhimg.com/v2-cf9ebad47e5c250ef5a545ee4e1c1b05_b.png) 106 | 107 | - 在**load**方法中,获取了machineList,定义list的地方是在一个文件中通过**yarn.federation.machine-list**获取文件位置;且文件中的内容格式如下 108 | 109 | ![img](https://pic3.zhimg.com/v2-7d1685ce8602e9884412e42a9faaf72e_b.png) 110 | 111 | - 解析文件之后,将machine依次添加到**nodeToSubCluster**,**rackToSubClusters**集合中 112 | 113 | ## AMRMProxy模块 114 | 115 | - 看完client—>rm侧的提交任务模块之后(**router**),接下来可以分析AM与RM侧的交互模块(**AMRMProxy**) 116 | 117 | ![img](https://pic4.zhimg.com/v2-d90061b8beeb05e40586a3dada4222a7_b.png) 118 | 119 | - AMRMProxyService :如上图所示,起于所有的NM之上的服务,作为AM与RM之间通信的代理;会将AM请求转发到正确的HomeSubCluster 120 | - FederationInterceptor: 作为AMRMProxyService中的拦截器,主要做AM与RM之间请求转发 121 | 122 | ### AMRMProxyService — FederationInterceptor 123 | 124 | - 类比Router,FederationInterceptor作为AMRMProxy的请求拦截处理 125 | - 在AM的视角,**FederationInterceptor**的作用就RM上的**ApplicationMasterService**;AM通过**AMRMClientAsyncImpl**或**AMRMClientImpl** 走RPC协议与**AMRMProxyService** 交互 126 | 127 | ![img](https://pic1.zhimg.com/v2-0ec5e08fa702ab8a00bb11528da4b138_b.png) 128 | 129 | **registerApplicationMaster详解** 130 | 131 | - 按照正常的AM流程分析,由**AMLauncher**启动container之后须首先会调用**registerApplicationMaster**方法初始化权限信息以及将自己注册到对应的RM上去;对应到**FederationInterceptor**是如下方法 132 | 133 | ![img](https://pic1.zhimg.com/v2-a322c31e9908ed4f8b08c05130c67688_b.png) 134 | 135 | - 制造一种假象:RM永不会挂掉;有可能会因为超时或者RM挂掉等原因而导致发出多个重复注册的请求,此时都会返回最近一次成功的注册结果;所以这也就是为什么registermaster这个方法必须为线程安全的原因 136 | 137 | ![img](https://pic4.zhimg.com/v2-e127ca2f6e0120575bc76c5b38126a43_b.png) 138 | 139 | - 目前只是往HomeSubCluster上注册AM,而不会往其他子集群上注册。是为了不影响扩展性;即不会随着集群的增多AM呈线性扩展;应该是后续按需注册sub-cluster rm 140 | 141 | ![img](https://pic1.zhimg.com/v2-571134517c85d3c6fd5a237412618a74_b.png) 142 | 143 | - **this.homeRMRelayer**是具体的跟RM通信的代理,其创建方式在**FederationInterceptor.init**方法中 144 | 145 | ![img](https://pic1.zhimg.com/v2-4afe5125de9a0e80a3440f6807e12e84_b.png) 146 | 147 | - 最后在返回response之前,会根据作业所属的queue信息从statestore中获取对应的策略,并初始化**policyInterpreter** 148 | 149 | ![img](https://pic1.zhimg.com/v2-bab165a60bd63a3c43549950825d28a0_b.png) 150 | 151 | ### Allocate详解 152 | 153 | - 周期性的通过心跳与HomeCluster和SubCluster RMs交互;期间可能伴随有SubCluster 上AM的启动和注册 154 | - **splitAllocateRequest**:将原来的request重新构造成面向所有已经注册的sub-cluster rm request 155 | 156 | ![img](https://pic2.zhimg.com/v2-bf455e34f88d53a7dcc5ffea1d51d8a9_b.png) 157 | 158 | - 具体到实现:通过requestMap来放置clusterId与allocateRequest的对应关系;通过uamPool获取已经注册UAM的sub clusterId并构建request 159 | 160 | ![img](https://pic3.zhimg.com/v2-3bb90c887c818638bb335ff6d46b9d46_b.png) 161 | 162 | - 后面的步骤是根据所有已经注册的home cluster和sub cluster id构建release, ask, blacklist等请求 163 | - 对于资源的请求拆分:这里会去调federation policy interpreter将原来request中的**askList(Resource Request List)**根据策略拆分到各个子集群;所以这里会涉及到Federation Policy调用,具体的分析接下来会单独拎出一小节解释 164 | 165 | ![img](https://pic1.zhimg.com/v2-675411d4dfdc72c8d89ee301a8709b7c_b.png) 166 | 167 | - 拿到**asks**后,会将的对应关系,加入到**requestMap**中 168 | - **注意:**这里借助**findOrCreateAllocateRequestForSubCluster**方法实现如果requestMap中不存在asks中对应的subClusterId,会新new一个request塞入map;后续这个request会在对应的subCluster上启动**UAM** 169 | - **因为对于新的job,刚开始确实是只在homeCluster上启动了AM** 170 | - **sendRequestsToResourceManagers** 171 | - splitAllocateRequest之后就是将构造好的请求发送到对应的cluster上;顺带在所有的subcluster启动UAM并注册上(如果之前没有启动的话);返回值是所有新注册上的UAM 172 | 173 | ![img](https://pic3.zhimg.com/v2-34f142d2135a9f8b12ae234966bde9b6_b.png) 174 | 175 | - **registerWithNewSubClusters** 用来在其他子集群中创建新的UAM实例 176 | - 在uamPool中不存在的被认为是新集群(*有点与**splitAllocateRequest**) 取AllUAMIds逻辑矛盾*) 177 | - 对newSubClusters集合迭代,依次在subClaster上启动UAM,并注册UAM 178 | 179 | ![img](https://pic4.zhimg.com/v2-1051b6772a9c893a910f3497cb9cc327_b.png) 180 | 181 | - 最后针对不同的cluster,调用不同的clientRPC请求资源 182 | 183 | ![img](https://pic2.zhimg.com/v2-2e0d3265a416a8bd9131559ce958b2f1_b.png) 184 | 185 | - **mergeAllocateResponses** 186 | - 用于合并所有资源请求返回的allocateResponse。实现里面是对**asyncResponseSink**容器的迭代,而asyncResponseSink的写入是在HeartBeatCallback逻辑里的 187 | - 对于allocateResponse的合并操作在**mergeAllocateResponse**中 188 | - **mergeRegistrationResponses** 189 | - 是在注册完其他的sub cluster之后将UAM加入到最终合并的AllocateResponse中;主要是对allocatedContainers以及NMTokens集合做增加 190 | 191 | ### finishApplicationMaster详解 192 | 193 | - 结束任务的时候有点类似allocate,需要向所有的sub cluster发送finish请求;目前是丢到一个compSvc线程池中批量执行*finshApplicationMaster 194 | - 在线程池中执行sub cluster finish的同时,也会调用home cluster rm进行finish操作 195 | 196 | ## Federation Policy模块 197 | 198 | - federation policy模块通过FederationPolicyManager的接口实现来统一加载 199 | 200 | ![img](https://pic1.zhimg.com/v2-6a2db128fc5762aba3591cf4912bfc40_b.png) 201 | 202 | - **FederationPolicyInitializationContext**:初始化FederationAMRMProxyPolicy和FederationRouterPolicy的上下文类 203 | - **federationStateStoreFacade**: policy state strore的具体实现实例 204 | - **federationPolicyConfiguration**: 具体的策略配置 205 | - **federationSubclusterResolver**:用来判断某个指定的node是属于哪个子集群的工具类 206 | - **homeSubcluster**:当前application实际AM运行的集群ID 207 | 208 | ## Policy 具体的实现列举 209 | 210 | ### amrmproxy模块的policy实现 211 | 212 | - **LocalityMulticastAMRMProxyPolicy** 213 | - \1. 如果是有偏好的host的话,会根据*SubClusterResolver* resolve cluster的结果转发到对应的cluster,但如果没有resolve的话,会默认将请求转向home cluster 214 | - \2. 如果有机架的限制,策略同上 215 | - \3. 如果没有host/rack偏好的话,会根据*weights*转发到对应的集群;weights的计算根据*WeightedPolicyInfo*以及*headroom*中的信息 216 | - \4. 所有请求量为0的请求都会转发到所有我们曾经调度过的子集群中(以防用户在尝试取消上一次的请求) 217 | - 注:该实现始终排除当前未活跃的RM 218 | - **具体实现细节待深究** 219 | 220 | **router模块的policy实现** 221 | 222 | - 总体来说router端的策略偏简单,自己定制也容易 223 | - 默认实现是**UniformRandomRouterPolicy**,随机转发client请求到某个alive的cluster 224 | 225 | ## 一些问题 226 | 227 | - 在NM侧,不能开启**FederationRMFailoverProxyProvider**,这个统一在获取RMAddress逻辑上有不足,导致NM启动时拿到的RMAddress是localhost无法通过ResourceTracker连上RM,最终注册失败 228 | 229 | ![img](https://pic1.zhimg.com/v2-edc97c0d9ba93b9094e561e6cd7b5464_b.png) -------------------------------------------------------------------------------- /docs/scheduler/Yarn架构解析.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lichaojacobs/awesome-big-data/6ef233e8a3f37ce2b63f6feffe7b5f196807cfff/docs/scheduler/Yarn架构解析.pdf -------------------------------------------------------------------------------- /docs/scheduler/airflow实战总结.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: airflow实战总结 3 | date: 2018-08-30 20:16:45 4 | tags: 5 | --- 6 | 7 | ## 介绍 8 | 9 | - airflow是一款开源的,分布式任务调度框架,它将一个具有上下级依赖关系的工作流,组装成一个有向无环图。 10 | - 特点: 11 | - 分布式任务调度:允许一个工作流的task在多台worker上同时执行 12 | - 可构建任务依赖:以有向无环图的方式构建任务依赖关系 13 | - task原子性:工作流上每个task都是原子可重试的,一个工作流某个环节的task失败可自动或手动进行重试,不必从头开始任务 14 | - 工作流示意图 15 | 16 | ![airflow-dags](http://ol7zjjc80.bkt.clouddn.com/airflow-dags.png) 17 | 18 | - 一个dag表示一个定时的工作流,包含一个或者多个具有依赖关系的task 19 | 20 | - task依赖图 21 | 22 | ![airflow-tasks](http://ol7zjjc80.bkt.clouddn.com/airflow-graph.png) 23 | 24 | - 架构图及集群角色 25 | 26 | ![airflow-infra](http://ol7zjjc80.bkt.clouddn.com/airflow-infra.png) 27 | 28 | - webserver : 提供web端服务,以及会定时生成子进程去扫描对应的目录下的dags,并更新数据库 29 | - scheduler : 任务调度服务,根据dags生成任务,并提交到消息中间件队列中 (redis或rabbitMq) 30 | - celery worker : 分布在不同的机器上,作为任务真正的的执行节点。通过监听消息中间件: redis或rabbitMq 领取任务 31 | - flower : 监控worker进程的存活性,启动或关闭worker进程,查看运行的task 32 | 33 | ## 实战 34 | 35 | - 构建docker镜像 36 | - 采用的airflow是未发行的1.10.0版本,原因是从1.10.0开始,支持时区的设置,而不是统一的UTC 37 | ``` 38 | //self.registry.domain 为docker私有镜像仓库 39 | //self.mvn.registry.com maven 私有镜像仓库 40 | //data0 为数据目录,data1为日志目录,运维统一配置日志清楚策略 41 | #docker build --network host -t self.registry.domain/airflow_base_1.10.7:1.0.0 . 42 | FROM self.registry.domain/airflow/centos_base_7.4.1708:1.0.0 43 | LABEL AIRFLOW=1.10.7 44 | 45 | ARG CELERY_REDIS=4.1.1 46 | ARG DOCKER_VERSION=1.13.1 47 | ARG AIRFLOW_VERSION=1.10.7 48 | 49 | ADD sbin /data0/airflow/sbin 50 | 51 | ENV SLUGIFY_USES_TEXT_UNIDECODE=yes \ 52 | #如果构建镜像的机器需要代理才能连接外网的话,配置https_proxy 53 | https_proxy=https://ip:port 54 | 55 | RUN curl http://self.mvn.registry.com/python/python-3.5.6.jar -o /tmp/Python-3.5.6.tgz && \ 56 | curl http://self.mvn.registry.com/airflow/${AIRFLOW_VERSION}/airflow-${AIRFLOW_VERSION}.jar -o /tmp/incubator-airflow-${AIRFLOW_VERSION}.tar.gz && \ 57 | curl http:/self.mvn.registry.com/docker/${DOCKER_VERSION}/docker-${DOCKER_VERSION}.jar -o /tmp/docker-${DOCKER_VERSION}.tar.gz && \ 58 | tar zxf /tmp/docker-${DOCKER_VERSION}.tar.gz -C /data0/software && \ 59 | tar zxf /tmp/Python-3.5.6.tgz -C /data0/software && \ 60 | tar zxf /tmp/incubator-airflow-${AIRFLOW_VERSION}.tar.gz -C /data0/software && \ 61 | yum install -y libtool-ltdl policycoreutils-python && \ 62 | rpm -ivh --force --nodeps /data0/software/docker-${DOCKER_VERSION}/docker-engine-selinux-${DOCKER_VERSION}-1.el7.centos.noarch.rpm && \ 63 | rpm -ivh --force --nodeps /data0/software/docker-${DOCKER_VERSION}/docker-engine-${DOCKER_VERSION}-1.el7.centos.x86_64.rpm && \ 64 | yum -y install gcc && yum -y install gcc-c++ && yum -y install make && \ 65 | yum -y install zlib-devel mysql-devel python-devel cyrus-sasl-devel cyrus-sasl-lib libxml2-devel libxslt-devel && \ 66 | cd /data0/software/Python-3.5.6 && ./configure && make && make install && \ 67 | ln -sf /usr/local/bin/pip3 /usr/local/bin/pip && \ 68 | ln -sf /usr/local/bin/python3 /usr/local/bin/python && \ 69 | cd /data0/software/incubator-airflow-${AIRFLOW_VERSION} && python setup.py install && \ 70 | pip install -i https://pypi.douban.com/simple/ apache-airflow[crypto,celery,hive,jdbc,mysql,hdfs,password,redis,devel_hadoop] && \ 71 | pip install -i https://pypi.douban.com/simple/ celery[redis]==$CELERY_REDIS && \ 72 | pip install -i https://pypi.douban.com/simple/ docutils && \ 73 | ln -sf /usr/local/lib/python3.5/site-packages/apache_airflow-1.10.0-py3.5.egg/airflow /data0/software/airflow && \ 74 | mkdir -p /data0/airflow/bin && \ 75 | ln -sf /data0/airflow/sbin/airflow-200.sh /data0/airflow/bin/200.sh && \ 76 | ln -sf /data0/airflow/sbin/airflow-503.sh /data0/airflow/bin/503.sh && \ 77 | chown -R root:root /data0/software/ && \ 78 | chown -R root:root /data0/airflow/ && \ 79 | chmod -R 775 /data0/airflow/sbin/* && \ 80 | chmod -R 775 /data0/airflow/bin/* && \ 81 | echo 'source /data0/airflow/sbin/init-airflow.sh' >> ~/.bashrc && \ 82 | rm -rf /tmp/* /data0/software/Python-3.5.6 /data0/software/incubator-airflow-${AIRFLOW_VERSION} /data0/software/docker-${DOCKER_VERSION} 83 | 84 | ENV PATH=$PATH:/data0/software/jdk/bin:/data0/software/airflow/bin:/data0/airflow/sbin/:/data0/airflow/sbin/airflow/:/data0/airflow/bin/ 85 | 86 | WORKDIR /data0/airflow/bin/ 87 | ``` 88 | 89 | - 通过docker 启动容器的话需要暴露几个端口 90 | ``` 91 | webserver: 8081 92 | worker: 8793 93 | flower: 5555 94 | //启动示例 95 | docker run --name airflow -it -d --privileged --net=host -p 8081:8081 -p 5555:5555 -p 8793:8793 -v /var/run/docker.sock:/var/run/docker.sock -v /data1:/data1 -v /data0/airflow:/data0/airflow self.registry.domain/airflow_1.10.7:1.0.0 96 | 97 | ``` 98 | - airflow 升级到未release的1.10.0的版本 99 | 100 | ``` 101 | //如果之前用的是低版本的话,需要执行 102 | airflow upgradedb 来更新迁移数据库的schema 103 | //执行之前首先需要set mysql property 104 | set global explicit_defaults_for_timestamp=1 //会提示is readonly variable 105 | 需要在my.cnf中添加这个设置:explicit_defaults_for_timestamp=1 并重启mysql 106 | //update celery几个设置 107 | celeryd_concurrency -> worker_concurrency 108 | celery_result_backend -> result_backend 109 | 110 | ``` 111 | - 修改时区,以及界面上执行时间的显示(airlfow 默认界面上还是按照UTC显示) 112 | 113 | ``` 114 | //需要update configuration 115 | default_timezone = Etc/GMT-8 116 | 117 | //修改dags.html中的显示时间,使得界面上看起来方便 118 | // jinjia2 传入转换函数,在views.py 的homeview的render中 119 | //(方法验证有点问题,再优化) 120 | def utc2local(utc): 121 | epoch = time.mktime(utc.timetuple()) 122 | offset = datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(epoch) 123 | return utc + offset 124 | utc2local(last_run.execution_date).strftime("%Y-%m-%d %H:%M") 125 | utc2local(last_run.start_date).strftime("%Y-%m-%d %H:%M")` 126 | 127 | ``` 128 | 129 | - airflow plugins 定制化开发 130 | - [官方文档](https://airflow.apache.org/plugins.html) 131 | - plugin 这个没法传给worker,还是得重新分发到各个worker节点,建议打入airflow基础镜像中 132 | - 增加operator时需要重启webserver和scheduler 133 | - 由于dag的删除现在官方没有暴露直接的api,而完整的删除又牵扯到多个表,总结出删除dag的sql如下 134 | 135 | ``` 136 | set @dag_id = 'BAD_DAG'; 137 | delete from airflow.xcom where dag_id = @dag_id; 138 | delete from airflow.task_instance where dag_id = @dag_id; 139 | delete from airflow.sla_miss where dag_id = @dag_id; 140 | delete from airflow.log where dag_id = @dag_id; 141 | delete from airflow.job where dag_id = @dag_id; 142 | delete from airflow.dag_run where dag_id = @dag_id; 143 | delete from airflow.dag where dag_id = @dag_id; 144 | 145 | ``` 146 | 147 | - 自己实现的200和503脚本,用于集群统一的上下线操作 148 | - 200脚本 149 | ``` 150 | #!/usr/bin/env bash 151 | function usage() { 152 | echo -e "\n A tool used for starting airflow services 153 | Usage: 200.sh {webserver|worker|scheduler|flower} 154 | " 155 | } 156 | 157 | PORT=8081 158 | ROLE=webserver 159 | ENV_ARGS="" 160 | check_alive() { 161 | PID=`netstat -nlpt | grep $PORT | awk '{print $7}' | awk -F "/" '{print $1}'` 162 | [ -n "$PID" ] && return 0 || return 1 163 | } 164 | 165 | check_scheduler_alive() { 166 | PIDS=`ps -ef | grep "/usr/local/bin/airflow scheduler" | grep "python" | awk '{print $2}'` 167 | [ -n "$PIDS" ] && return 0 || return 1 168 | } 169 | 170 | function get_host_ip(){ 171 | local host=$(ifconfig | grep "inet " | grep "\-\->" | awk '{print $2}' | tail -1) 172 | if [[ -z "$host" ]]; then 173 | host=$(ifconfig | grep "inet " | grep "broadcast" | awk '{print $2}' | tail -1) 174 | fi 175 | echo "${host}" 176 | } 177 | 178 | start_service() { 179 | if [ $ROLE = 'scheduler' ];then 180 | check_scheduler_alive 181 | else 182 | check_alive 183 | fi 184 | if [ $? -ne 0 ];then 185 | nohup airflow $ROLE $ENV_ARGS > $BASE_LOG_DIR/$ROLE/$ROLE.log 2>&1 & 186 | sleep 5 187 | if [ $ROLE = 'scheduler' ];then 188 | check_scheduler_alive 189 | else 190 | check_alive 191 | fi 192 | if [ $? -ne 0 ];then 193 | echo "service start error" 194 | exit 1 195 | else 196 | echo "service start success" 197 | exit 0 198 | fi 199 | else 200 | echo "service alreay started" 201 | exit 0 202 | fi 203 | } 204 | 205 | function main() { 206 | if [ -z "${POOL}" ]; then 207 | echo "the environment variable POOL cannot be empty" 208 | exit 1 209 | fi 210 | source /data0/hcp/sbin/init-hcp.sh 211 | case "$1" in 212 | webserver) 213 | echo "starting airflow webserver" 214 | ROLE=webserver 215 | PORT=8081 216 | start_service 217 | ;; 218 | worker) 219 | echo "starting airflow worker" 220 | ROLE=worker 221 | PORT=8793 222 | local host_ip=$(get_host_ip) 223 | ENV_ARGS="-cn ${host_ip}@${host_ip}" 224 | start_service 225 | ;; 226 | flower) 227 | echo "starting airflow flower" 228 | ROLE=flower 229 | PORT=5555 230 | start_service 231 | ;; 232 | scheduler) 233 | echo "starting airflow scheduler" 234 | ROLE=scheduler 235 | start_service 236 | ;; 237 | *) 238 | usage 239 | exit 1 240 | esac 241 | } 242 | 243 | 244 | main "$@" 245 | 246 | ``` 247 | 248 | - 503脚本 249 | 250 | ``` 251 | #!/usr/bin/env bash 252 | function usage() { 253 | echo -e "\n A tool used for stop airflow services 254 | Usage: 200.sh {webserver|worker|scheduler|flower} 255 | " 256 | } 257 | 258 | function get_host_ip(){ 259 | local host=$(ifconfig | grep "inet " | grep "\-\->" | awk '{print $2}' | tail -1) 260 | if [[ -z "$host" ]]; then 261 | host=$(ifconfig | grep "inet " | grep "broadcast" | awk '{print $2}' | tail -1) 262 | fi 263 | echo "${host}" 264 | } 265 | 266 | function main() { 267 | if [ -z "${POOL}" ]; then 268 | echo "the environment variable POOL cannot be empty" 269 | exit 1 270 | fi 271 | source /data0/hcp/sbin/init-hcp.sh 272 | case "$1" in 273 | webserver) 274 | echo "stopping airflow webserver" 275 | cat $AIRFLOW_HOME/airflow-webserver.pid | xargs kill -9 276 | ;; 277 | worker) 278 | echo "stopping airflow worker" 279 | PORT=8793 280 | PID=`netstat -nlpt | grep $PORT | awk '{print $7}' | awk -F "/" '{print $1}'` 281 | kill -9 $PID 282 | local host_ip=$(get_host_ip) 283 | ps -ef | grep celeryd | grep ${host_ip}@${host_ip} | awk '{print $2}' | xargs kill -9 284 | ;; 285 | flower) 286 | echo "stopping airflow flower" 287 | PORT=5555 288 | PID=`netstat -nlpt | grep $PORT | awk '{print $7}' | awk -F "/" '{print $1}'` 289 | kill -9 $PID 290 | start_service 291 | ;; 292 | scheduler) 293 | echo "stopping airflow scheduler" 294 | PID=`ps -ef | grep "/usr/local/bin/airflow scheduler" | grep "python" | awk '{print $2}'` 295 | kill -9 $PID 296 | ;; 297 | *) 298 | usage 299 | exit 1 300 | esac 301 | } 302 | 303 | 304 | main "$@" 305 | 306 | ``` 307 | 308 | ## 遇到的坑以及定制化解决方案 309 | 310 | - 问题1: airflow worker 角色不能使用根用户启动 311 | - 原因:不能用根用户启动的根本原因,在于airflow的worker直接用的celery,而celery 源码中有参数默认不能使用ROOT启动,否则将报错, [源码链接](http://docs.celeryproject.org/en/latest/_modules/celery/platforms.html) 312 | 313 | ``` 314 | C_FORCE_ROOT = os.environ.get('C_FORCE_ROOT', False) 315 | 316 | ROOT_DISALLOWED = """\ 317 | Running a worker with superuser privileges when the 318 | worker accepts messages serialized with pickle is a very bad idea! 319 | 320 | If you really want to continue then you have to set the C_FORCE_ROOT 321 | environment variable (but please think about this before you do). 322 | 323 | User information: uid={uid} euid={euid} gid={gid} egid={egid} 324 | """ 325 | 326 | ROOT_DISCOURAGED = """\ 327 | You're running the worker with superuser privileges: this is 328 | absolutely not recommended! 329 | 330 | Please specify a different user using the --uid option. 331 | 332 | User information: uid={uid} euid={euid} gid={gid} egid={egid} 333 | """ 334 | 335 | ``` 336 | 337 | - 解决方案一:修改airlfow源码,在celery_executor.py中强制设置C_FORCE_ROOT 338 | 339 | ``` 340 | from celery import Celery, platforms 341 | 在app = Celery(…)后新增 342 | platforms.C_FORCE_ROOT = True 343 | 重启即可 344 | 345 | ``` 346 | 347 | - 解决方案二:在容器初始化环境变量的时候,设置C_FORCE_ROOT参数,以零侵入的方式解决问题 348 | 349 | ``` 350 | #强制celery worker运行采用root模式 351 | export C_FORCE_ROOT=True 352 | 353 | ``` 354 | 355 | - 问题2: docker in docker 356 | - 在dags中以docker方式调度任务时,为了container的轻量话,不做重型的docker pull等操作,我们利用了docker cs架构的设计理念,只需要将宿主机的/var/run/docker.sock文件挂载到容器目录下即可 [docker in docker 资料](http://wangbaiyuan.cn/docker-in-docker.html#prettyPhoto) 357 | 358 | - 问题3: 由于我们运行airlfow的机器是高配机器切分的虚机,host并非是传统的ip段,多节点执行后无法在master节点上通过worker节点提供的日志服务获取执行日志 359 | - 查看celery源码(celery/celery/worker/worker.py) 360 | 361 | ``` 362 | from celery.utils.nodenames import default_nodename, worker_direct 363 | self.hostname = default_nodename(hostname) 364 | // 查看default_nodename方法 365 | def default_nodename(hostname): 366 | """Return the default nodename for this process.""" 367 | name, host = nodesplit(hostname or '') 368 | return nodename(name or NODENAME_DEFAULT, host or gethostname()) 369 | 370 | //默认在worker.py 的构造方法中没有传入hostname 所以在celery nodenames.py中default_nodename方法里面调用了gethostname 371 | //可以看到gethostname的实现,调用了socket.gethostname,这个直接得到了虚拟机的host 372 | gethostname = memoize(1, Cache=dict)(socket.gethostname) 373 | 374 | ``` 375 | 376 | - 解决方案:发现airflow worker的启动命令中其实提供了设置celery host name的参数 377 | 378 | ``` 379 | airflow worker -cn=ip@ip 380 | 381 | ``` 382 | 383 | - 问题4: 多个worker节点进行调度反序列化dag执行的时候,报找不到module的错误 384 | - 当时考虑到文件更新的一致性,采用所有worker统一执行master下发的序列化dag的方案,而不依赖worker节点上实际的dag文件,开启这一特性操作如下 385 | 386 | ``` 387 | worker节点上: airflow worker -cn=ip@ip -p //-p为开关参数,意思是以master序列化的dag作为执行文件,而不是本地dag目录中的文件 388 | master节点上: airflow scheduler -p 389 | 390 | ``` 391 | 392 | - 错误原因在于远程的worker节点上不存在实际的dag文件,反序列化的时候对于当时在dag中定义的函数或对象找不到module_name 393 | - 解决方案一:在所有的worker节点上同时发布dags目录,缺点是dags一致性成问题 394 | - 解决方案二:修改源码中序列化与反序列化的逻辑,主体思路还是替换掉不存在的module为main。修改如下: 395 | 396 | ``` 397 | //models.py 文件,对 class DagPickle(Base) 定义修改 398 | import dill 399 | class DagPickle(Base): 400 | id = Column(Integer, primary_key=True) 401 | # 修改前: pickle = Column(PickleType(pickler=dill)) 402 | pickle = Column(LargeBinary) 403 | created_dttm = Column(UtcDateTime, default=timezone.utcnow) 404 | pickle_hash = Column(Text) 405 | 406 | __tablename__ = "dag_pickle" 407 | def __init__(self, dag): 408 | self.dag_id = dag.dag_id 409 | if hasattr(dag, 'template_env'): 410 | dag.template_env = None 411 | self.pickle_hash = hash(dag) 412 | raw = dill.dumps(dag) 413 | # 修改前: self.pickle = dag 414 | reg_str = 'unusual_prefix_\w*{0}'.format(dag.dag_id) 415 | result = re.sub(str.encode(reg_str), b'__main__', raw) 416 | self.pickle =result 417 | 418 | //cli.py 文件反序列化逻辑 run(args, dag=None) 函数 419 | // 直接通过dill来反序列化二进制文件,而不是通过PickleType 的result_processor做中转 420 | 修改前: dag = dag_pickle.pickle 421 | 修改后:dag = dill.loads(dag_pickle.pickle) 422 | 423 | ``` 424 | 425 | - 解决方案三:源码零侵入,使用python的types.FunctionType重新创建一个不带module的function,这样序列化与反序列化的时候不会有问题(待验证) 426 | ``` 427 | new_func = types.FunctionType((lambda df: df.iloc[:, 0].size == xx).__code__, {}) 428 | 429 | ``` 430 | 431 | - 问题5:由于airflow在master查看task执行日志是通过各个节点的http服务获取的,但是存入task_instance表中的host_name不是ip,可见获取hostname的方式有问题. 432 | - 解决方案:修改airflow/utils/net.py 中get_hostname函数,添加优先获取环境变量中设置的hostname的逻辑 433 | 434 | ``` 435 | //models.py TaskInstance 436 | self.hostname = get_hostname() 437 | //net.py 在get_hostname里面加入一个获取环境变量的逻辑 438 | import os 439 | def get_hostname(): 440 | """ 441 | Fetch the hostname using the callable from the config or using 442 | `socket.getfqdn` as a fallback. 443 | """ 444 | # 尝试获取环境变量 445 | if 'AIRFLOW_HOST_NAME' in os.environ: 446 | return os.environ['AIRFLOW_HOST_NAME'] 447 | # First we attempt to fetch the callable path from the config. 448 | try: 449 | callable_path = conf.get('core', 'hostname_callable') 450 | except AirflowConfigException: 451 | callable_path = None 452 | 453 | # Then we handle the case when the config is missing or empty. This is the 454 | # default behavior. 455 | if not callable_path: 456 | return socket.getfqdn() 457 | 458 | # Since we have a callable path, we try to import and run it next. 459 | module_path, attr_name = callable_path.split(':') 460 | module = importlib.import_module(module_path) 461 | callable = getattr(module, attr_name) 462 | return callable() 463 | 464 | ``` --------------------------------------------------------------------------------