enmm首先,写这个也不指望有啥人认同,毕竟这个PR我也是在看别人的文档和代码。可以把这个系列的博客看成读后感吧
flink1.14的广义增量检查点: github.com/apache/flin…
这个监控背压的提案是flink的德国母公司 的提案人Roman提出来的
本文将从几个部分讲解这个新的featrue
- 本次变更的出发点是什么
- 做这个变更会给状态恢复带来什么影响
- 需要如何实现这个变更
基于WAL解决Checkpoint小文件压缩时的崩溃恢复
在Flink的官方资料当中,state的意义为记录下来当前每个Task提交的状态,并当每一个TaskManager当中每一组的Task其中出现算子任务失败的时候进行崩溃恢复,比如从kafka topic当中进行消费,那记录下来上一次成功执行到哪一条消息的位置,下次继续从这个位置之后开始执行,由于随着上游实时数据源的流量过大,记录下来的状态也随之增加,比如一个topic 每个topic存在1000个分区。此时在生产环境上,比较大的公司甚至有上万个topic,当每个topic的分区对应的算子计算任务Task都开始向TaskManagaer提交状态的时候,写入的状态大小是一件非常恐怖的事情(可以通过flink1.13监控的state size看到)。所以flink官方推荐状态管理用rocksdb
ps:由于最近天池参赛的缘故,将rocksdb的前身leveldb的源码翻了个底朝天,源码的注释版请见: github.com/complone/le…
简要说下采用rocksdb进行数据压缩的整个流程 官方文档:github.com/facebook/ro…
以上是rocksdb的put API写入一条数据流程。我们可以看到这边每条数据在写入memtable的时候,同时会记录下来写入操作的日志(WAL)
引用 <<数据库系统内幕>> 一书中 LSM树更新和删除的定义 因为rocksdb的追加写机制本身是基于LSM树的结构实现的,有较小的内存驻留组件(memtable)和较大的磁盘驻留组件(sstable)组成,要在磁盘上写入不可变的文件内容,首先需要在内存对其进行缓冲以及排序。 内存驻留组件是可变的,也就是记录需要写入缓冲区,并充当读写操作的目标,当其中的大小到达一个阈值时,memtable会将其中的内容持久化到磁盘上,memtable的更新不需要磁盘访问,也没有相关的IO开销,但是需要一个单独的预先写入日志文件(WAL)保证数据记录的持久性。就像上图所示,在向客户端确认操作之后,数据记录会被追加到日志并提交到内存.
在memtable持久化到sstable期间,memtable的内容会被分块刷写到磁盘上,刷写期间,对于每个被刷写的memtable,都可以在磁盘上找到一个sstable。 www.processon.com/view/link/6…
在保证子树刷写时候需要确保三件事 1.刷写过程一旦开始,所有新的写操作都必须要转移到新的memtable 2.子树刷鞋期间,磁盘驻留子树和正在刷写的memtable都要保持可以被读取的状态 3.在刷写之后,对于发布合并的内容,丢弃原始的磁盘驻留与内存驻留的内容需要以原子的方式执行
但是只有一个磁盘驻留组件,那写满了不是刷写就变得频繁了嘛?
所以引入了多磁盘驻留组件(SSTable)也就是我们现在提到的rocksdb compact操作,flink官网贴了一张图,但是感觉像是把leveldb论文的图改了改,具体见RocketMQ冷热存储分离当中的leveldb设计解析。
flink on rocksdb: flink.apache.org/2021/01/18/… rocksdb调优指南: www.ververica.com/blog/the-im…
flink 1.14 rocksdb的背压测试平台: codespeed.dak8s.net:8000/
如上图所示,当数据已经在内存中进行了排序之后,因此可以将内存驻留的内容依次写入磁盘来创建磁盘驻留表。在刷写期间,正在刷写的memtable和当前的memtable都可以被读取
在完全刷写memtable之前,其内容唯一的磁盘驻留版本存储在预写日志(WAL)当中,当memtable内容完全被刷写到磁盘上时,日志可以被修剪(trim),并且保存对被刷写过的memtable进行的操作日志段可以被丢弃
这就是flink当前的两种检查点方式之一增量检查点为什么只能基于rocksdb来开启,而本系列文章的主角广义增量检查点也因此进行拓展
roman的状态管理提案:docs.google.com/document/d/…
在变动目标中,他提到如下改动主旨
**
State ownership means here “who decides when to delete the state” (such as SST files). Note that it doesn't mean the deletion itself (which can be delegated).Currently, JM owns the state in most cases, which has some disadvantages (see below).The goals are to assess the current model and then to understand how and whether we should change it.\
This can be broken down into questions:- Do we want to change ownership for some specific states/backends/setups or in general (e.g. RocksDB SST files too)?
- - In particular, do different models for different states itself add complexity (e.g. if logs are TM-owned and materialized files are JM-owned)?
- Do we want to change it ASAP (before changelog backend) or as we have further use cases (like merging some existing implementations or implementing non-global checkpoints)?
- What model(s) do we want to adopt?**
当用户开启增量检查点的时候,始终会有一个疑问,在什么时候决定删除状态(比如SSTable)(删除状态这里解释起来很麻烦。因为LSM树的删除操作如果从memtable中移除,删除会无效,rocksdb会复活(resurrent)之前的数据)。这里在每次状态落盘的时候,会为这个KVStateRegistery创建一个stateHandle状态句柄。每当每个Task将状态落盘之后会上报给JM(JobManager)
这里就会存在一个弊端,那就是用户不知道状态什么时候会被读取或者写入,为了解决这个问题,自顶向下分析有这三种case
- 前面提到rocksdb在每次进行memtable刷写之前都会先写一份操作记录到预写日志当中,那是否应该将每组TM的WAL物化下来 保存为一种新的backend(状态后端),也就是本次变更的changlogbackend
- 既然每次TaskManager中的状态落盘之后,都会给JM一个通知,那JM自然也应当持有TM的WAL记录,是否也应该物化下来
社区这里也提到了一个bug @PengFei li issues.apache.org/jira/browse…
下面,通过分析具体的TaskManager执行细节,来对本次变更做进一步描述
TaskManager底层的执行任务细节
Roman在flink当中支持的广义增量检查点变更分支: github.com/rkhachatrya…
这里的TM其实指的就是TaskManager,这里的Task Executor指的就是每个TaskManager中执行一组任务的执行器TaskExecutor。在flink源码中可以跑一下这个测试类
org/apache/flink/runtime/taskexecutor/TaskExecutorITCase#
testJobRecoveryWithFailingTaskExecutor
可以看到startTaskManager的函数调用里,会从本地的执行器以一个MiniCluster(最小集群资源启动)
@Test
public void testJobRecoveryWithFailingTaskExecutor() throws Exception {
final JobGraph jobGraph = createJobGraphWithRestartStrategy(PARALLELISM);
final CompletableFuture<JobResult> jobResultFuture = submitJobAndWaitUntilRunning(jobGraph);
// start an additional TaskExecutor
miniCluster.startTaskManager();
miniCluster.terminateTaskManager(0).get(); // this should fail the job
BlockingOperator.unblock();
assertThat(jobResultFuture.get().isSuccess(), is(true));
}
MiniCluster#startTaskManager
可以看到这理全局维护了一个重入锁(在每一次集群启动的时机分配的TaskExecutor都可以拿到)。在这里我们可以看到上述流程图当中的TaskManagerRunner,他的作用旨在为每个提交的Task分配一个随机的UUID,并返回所归属的TaskExecutor对象
(具体申请资源的操作在最后一行的 taskExecutor.start())
public void startTaskManager() throws Exception {
synchronized (lock) {
final Configuration configuration = miniClusterConfiguration.getConfiguration();
final TaskExecutor taskExecutor =
TaskManagerRunner.startTaskManager(
configuration,
new ResourceID(UUID.randomUUID().toString()),
taskManagerRpcServiceFactory.createRpcService(),
haServices,
heartbeatServices,
metricRegistry,
blobCacheService,
useLocalCommunication(),
ExternalResourceInfoProvider.NO_EXTERNAL_RESOURCES,
taskManagerTerminatingFatalErrorHandlerFactory.create(
taskManagers.size()));
taskExecutor.start();
taskManagers.add(taskExecutor);
}
}
Continue..开始下雪咯,赶紧开始更新