【Flink】Flink 源码阅读笔记(20)- Flink 基于 Mailbox 的线程模型

在这里插入图片描述

1.概述

转载:Flink 源码阅读笔记(20)- Flink 基于 Mailbox 的线程模型

相似文章:【Flink】Flink 基于 MailBox 实现的 StreamTask 线程模型

Flink 1.10 对内部事件处理的线程模型做了一个大的改进,采用了类似 Actor 的信箱模型。这篇文章我们将深入 Flink 内部 Mailbox 线程模型的设计即实现。

2.背景

在之前的线程模型中,StreamTask 中可能存在多个潜在的线程会修改内部的状态,因此需要通过加锁的方式来确保线程安全的状态,这个全局的锁就是著名的 checkpointLock。通过 checkpointLock 控制线程间的并发会让程序代码变得很复杂,并且锁对象还通过一些 API 暴露给了用户(例如 SourceFunction#getCheckpointLock()),如果没有正确加锁很容易引发线程安全问题。

为了解决这个问题,社区提出了基于 Mailbox 的线程模型,见 FLINK-12477。Mailbox 机制借鉴了 Actor 模型,通过单个 Mailbox 线程配合阻塞队列的方式,将内部状态的修改交由单个线程完成,从而避免多线程的问题。相比于使用 checkpointLock,Mailbox 模型另一个好处是方便控制事件处理的优先级,通过锁竞争很难达到类似的效果。

在原始的线程模型中,checkpointLock 主要用在三个地方:

  • 事件处理:包括 events, watermarks, barriers, latency markers 的处理和发送

  • checkpoint 触发:通过 RPC 调用触发 checkpoint(在 Source 中)、通知 checkpoint 的完成情况,(注:对下游来说,checkpoint 触发和取消是通过 barrier 触发的,归为第一种情况)

  • Processing Time Timers: 处理时间定时器是通过 ScheduledExecutor 异步执行的(事件事件定时器触发是通过 watermark 触发的,归为第一种情况)

在新的改进方案中,对锁的替换不仅仅要做到排他的效果,对于事件处理还需要保证原子性。

3.改进方案

Mailbox 模型的核心思想其实比较简单,其底层就是 FIFO 的队列 + 一个单线程的循环事件处理。所有需要处理的事件都封装成一个 Mail 投递到 Mailbox 中,然后按先后顺序由单线程加以处理,从而简化了并发访问问题。

在使用 Mailbox 以前,StreamTask 的核心逻辑是在 StreamTask#run() 中,内部是一个循环的事件处理。除此以外,checkpoint triggerprocessing time timer 在其它线程中运行。

在改进方案中,StreamTask 的基础逻辑大致如下(伪代码,来自设计文档):

BlockingQueue<Runnable> mailbox = ...

void runMailboxProcessing() {
    //TODO: can become a cancel-event through mailbox eventually
    Runnable letter;
    while (isRunning()) { 
        while ((letter = mailbox.poll()) != null) {
            letter.run();
        }

        defaultAction();
    }
}

void defaultAction() {
    // e.g. event-processing from an input
}

上面只是核心代码的大致逻辑,具体的实现还有一些优化,比如队列的公平性。之前的抢锁操作是完全没有任何公平性而言的。

在这个模型下,事件处理的循环被移到了 Mailbox 处理线程中,因此以往在 StreamTask#run() 中的循环逻辑就不再需要了。但这里会有个问题,因为历史原因,Flink Source Function 的核心逻辑是一个循环,这个循环不能和 Mailbox 的事件循环穿插执行,因此需要进行兼容性处理。在 FLIP-27 提出的新的 Source 接口中,已经可以比较好地和 Mailbox 模型进行兼容了。

对于 checkpoint trigger 和 processing time timer,只需要将对应的操作封装为 Mail 投递到 Mailbox 中,等待 Mailbox 线程进行处理即可

4.具体实现

4.1 整体设计

下面这张图展示了 Mailbox 线程模型中的核心抽象。
在这里插入图片描述
Mail 中封装了需要处理的消息和相应的动作,checkpoint trigger 和 processing time timer 就是通过 Mail 触发的TaskMailbox 用于存储 Mail(需要处理的消息);MailboxProcessor 负责从 TaskMailbox 中取出信件并处理;其它的调用方通过 MailboxExecutor 向 TaskMailbox 中投递信件。

MailboxDefaultAction 则是 MailboxProcessor 的默认动作,如前所述,MailboxDefaultAction 主要负责处理基础的 stream event、barrier、watermark 等。在 Mailbox 主线程的循环中,处理完新的 Mail 后就会执行该动作。MailboxDefaultAction 通过一个 MailboxControllerMailbox 进行交互,可以借此获悉所有的事件都处理完毕,或者临时暂停 MailboxDefaultAction

4.2 Mailbox

TaskMailbox 的内部使用了一个普通的 Deque 存储写入的 Mail,对 Deque 读写通过一个 ReentrantLock 来加以保护。Mailbox 的一个主要特性是可以做优先级控制,每一个 Mail 都有其优先级,从 TaskMailbox 获取 Mail 时可以指定优先级,实际实现时就是通过遍历队列元素比较优先级

为了减少读取队列时的同步开销TaskMailbox 支持创建一个 batch 后续消费,相当于把队列中的元素存入一个额外的队列,后续消费时就避免了加锁的操作。

4.3 MailboxProcessor

MailboxProcessot 核心就是前面提过的事件循环,在这个事件循环中,除了处理 TaskMailbox 中的事件外,还有一个 MailboxDefaultAction 用做默认的行为

MailboxDefaultActionTaskMailbox 内部的 Mail 的区别在于,Mail 通常用于一些控制类的消息处理,例如 checkpoint 触发,而 MailboxDefaultAction用于数据流上的普通消息处理(如正常的数据记录,barrier)等。数据流上的消息数据量比较大,通过邮箱内部队列进行处理显然开销比较大。

public class MailboxProcessor implements Closeable {
    //邮箱
    protected final TaskMailbox mailbox;

    // 默认行为,用于普通的数据流上的消息数据处理
    protected final MailboxDefaultAction mailboxDefaultAction;

 /**
     * Runs the mailbox processing loop. This is where the main work is done. This loop can be
     * suspended at any time by calling {@link #suspend()}. For resuming the loop this method should
     * be called again.
     *
     *  // 运行邮箱处理循环。 这是完成主要工作的地方。
     */
    public void runMailboxLoop() throws Exception {
        suspended = !mailboxLoopRunning;

        final TaskMailbox localMailbox = mailbox;

        // 检查当前运行线程是否是 mailbox 线程,只有 mailbox 线程能运行该方法
        //确保当前调用必须发生在 Mailbox 的事件处理线程中
        checkState(
                localMailbox.isMailboxThread(),
                "Method must be executed by declared mailbox thread!");

		// mailbox 状态必须是 OPEN
        assert localMailbox.getState() == TaskMailbox.State.OPEN : "Mailbox must be opened!";

        final MailboxController defaultActionContext = new MailboxController(this);

        // TODO:邮箱里有邮件,就进行处理. 邮件就是类似map之类的任务...
        while (isNextLoopPossible()) {
            // The blocking `processMail` call will not return until default action is available.
            // 在默认操作可用之前,阻塞的`processMail`调用将不会返回。
            // 处理事件,这是一个阻塞方法,如果默认行为不可用,方法不会返回
            processMail(localMailbox, false);
            // 再做一次检查,因为上面的 mail 处理可能会改变运行状态
            if (isNextLoopPossible()) {
                // TODO: 执行一个默认的动作 邮箱默认操作在StreamTask构造器中指定,为 processInput
                mailboxDefaultAction.runDefaultAction(
                        // 根据需要在默认操作中获取锁
                        defaultActionContext); // lock is acquired inside default action as needed
            }
        }
    }

4.4 MailboxExecutor

MailboxExecutor 的主要作用是向 TaskMailbox 中投递 Mail,这个接口被设计为类似 java.util.concurrent.Executor 接口。提交 Mail 的行为可以在任意线程中进行,因为 TaskMailbox 内部有基于锁的同步控制

除了提交 Mail 外,MailboxExecutor 还有一个比较重要的作用体现在 MailboxExecutor#yield 方法中。yield 这个词在程序设计语言中非常常见,但其含义往往又让人摸不着头脑。从字面解释来看,yield 有“让出”,“屈服”之意,在一些场景下也有“生成”的意思。这里我们不纠结这个,还是来看看这个方法设计的意图的什么。

Mailbox 模型中所有的事件都是在单个事件处理线程中处理的,排除掉优先级的因素,所有的事件按照 FIFO 的顺序加以处理。正常情况下,这种处理顺序是没有问题的。但是考虑到一种特殊的情况,如果要完成对事件A的处理需要等待一个条件,只有在处理完事件B之后这个条件才能满足,但是事件B在队列里的顺序是在事件A之后的,这样某种程度上来说就造成了一种 “死锁”。

yield 方法就是为了解决上面的问题,yield 会从队列中取出下一个事件进行处理,看上去像是暂时“让出”了对当前事件的处理。

说起来有点抽象,看一个示例:

MailboxExecutor mailboxExecutor = ....

mailboxExecutor.executr(() -> {
    // ...
    // 当前事件处理的逻辑,要完成,需要依赖后面某个事件的处理
    while (resource not available) {
        // 取出下一个事件处理
        mailboxExecutor.yield();
    }
    // 继续处理当前事件
    // ...
})

注意,为了不破坏 Mailbox 模型单线程执行的特性,这个方法必须在 Mailbox 事件处理线程中调用。这是一个阻塞方法,因此可能会阻塞事件处理线程。有些场景下可能还需要依赖事件处理线程来提交新的事件,因此也提供了非阻塞的 tryYield 方法。

5.StreamTask 如何应用 Mailbox 模型

StreamTask 的核心是处理消息流中的 StreamRecord,这个处理逻辑是 MailboxProcessor 的默认行为,即:

class StreamTask {
    protected void processInput(MailboxDefaultAction.Controller controller) throws Exception {
        InputStatus status = inputProcessor.processInput(); //处理输入
        if (status == InputStatus.MORE_AVAILABLE && recordWriter.isAvailable()) {
            return;
        }
        if (status == InputStatus.END_OF_INPUT) {
            // 没有后续的输入了,告知 MailboxDefaultAction.Controller 
            controller.allActionsCompleted();
            return;
        }

        // 暂时没有输入的情况
        TaskIOMetricGroup ioMetrics = getEnvironment().getMetricGroup().getIOMetricGroup();
        TimerGauge timer;
        CompletableFuture<?> resumeFuture;
        if (!recordWriter.isAvailable()) {
            timer = ioMetrics.getBackPressuredTimePerSecond();
            resumeFuture = recordWriter.getAvailableFuture();
        } else {
            timer = ioMetrics.getIdleTimeMsPerSecond();
            resumeFuture = inputProcessor.getAvailableFuture();
        }
        // 一旦有输入了,就告知 controller 要恢复 MailboxDefaultAction 的处理
        assertNoException(
                resumeFuture.thenRun(
                        // 首先会暂停 MailboxDefaultAction 的处理
                        new ResumeWrapper(controller.suspendDefaultAction(timer), timer)));
    }

    private static class ResumeWrapper implements Runnable {
        private final Suspension suspendedDefaultAction;
        private final TimerGauge timer;

        public ResumeWrapper(Suspension suspendedDefaultAction, TimerGauge timer) {
            this.suspendedDefaultAction = suspendedDefaultAction;
            timer.markStart();
            this.timer = timer;
        }

        @Override
        public void run() {
            timer.markEnd();
            suspendedDefaultAction.resume();
        }
    }
}

对于 checkpoint 的触发,是通过 MailboxExecutor 提交一个 Mail 来实现的:

@Override
    public Future<Boolean> triggerCheckpointAsync(
            CheckpointMetaData checkpointMetaData, CheckpointOptions checkpointOptions) {

        CompletableFuture<Boolean> result = new CompletableFuture<>();
        mainMailboxExecutor.execute(
                () -> {
                    try {
                        // 触发Checkpoint操作 这里可以看到,其实现跟方案设计中的是一致,Checkpoint trigger
                        // 这里的操作就是向 MailBox 提交一个 Task,等待 MailBox 去处理。
                        result.complete(
                                triggerCheckpointAsyncInMailbox(
                                        checkpointMetaData, checkpointOptions));
                    } catch (Exception ex) {
                        // Report the failure both via the Future result but also to the mailbox
                        result.completeExceptionally(ex);
                        throw ex;
                    }
                },
                "checkpoint %s with %s",
                checkpointMetaData,
                checkpointOptions);
        return result;
    }

checkpoint 完成或者放弃的通知也是提交到 Mailbox 中运行的:

class StreamTask {
    // checkpoint 完成或者失败的回调通知操作
    private Future<Void> notifyCheckpointOperation(
            RunnableWithException runnable, String description) {
        CompletableFuture<Void> result = new CompletableFuture<>();
        mailboxProcessor
                .getMailboxExecutor(TaskMailbox.MAX_PRIORITY)
                .execute(
                        () -> {
                            try {
                                runnable.run();
                            } catch (Exception ex) {
                                result.completeExceptionally(ex);
                                throw ex;
                            }
                            result.complete(null);
                        },
                        description);
        return result;
    }
}

对于 processing time timer 的触发也是类似的:

class streamTask {
    public ProcessingTimeServiceFactory getProcessingTimeServiceFactory() {
        return mailboxExecutor ->
                new ProcessingTimeServiceImpl(
                        timerService,
                        callback -> deferCallbackToMailbox(mailboxExecutor, callback));
    }

    ProcessingTimeCallback deferCallbackToMailbox(
            MailboxExecutor mailboxExecutor, ProcessingTimeCallback callback) {
        return timestamp -> {
            // 提交到 mailbox 中运行
            mailboxExecutor.execute(
                    () -> invokeProcessingTimeCallback(callback, timestamp),
                    "Timer callback for %s @ %d",
                    callback,
                    timestamp);
        };
    }
}

6.Legacy Source 的兼容处理

前面提到,因为历史遗留的问题,SourceFunction 被设计成一个无限的循环,这个循环不能和 Mailbox 的事件循环穿插执行,因此需要进行兼容性处理。

SourceStreamTask 被设计为 StreamTask 的子类,会启动另外一个独立的线程 LegacySourceFunctionThread 运行 SourceFunction 中的循环。这样相当于有两个线程在同时运行

  1. 一个是 SourceFunction 中生成数据流中的数据
  2. 另一个是 Mailbox 中的事件处理线程。

为了防止这两个线程发生冲突,在 SourceStreamTask 中保留了 checkpoint lock,用于在这两个线程间进行并发控制。

为了达到这样的效果,Flink 提供了一个 StreamTaskActionExecutor 的封装,用来运行 Runnable。正常情况下,StreamTaskActionExecutor 的实现就是直接去运行 Runnable;同时也提供了一个 SynchronizedStreamTaskActionExecutor 的实现,在运行 Runnable 的时候会进行加锁控制,这样就把获取锁的操作引入到 Mailbox 处理线程中了:

class SynchronizedStreamTaskActionExecutor implements StreamTaskActionExecutor {
    private final Object mutex;

    public SynchronizedStreamTaskActionExecutor(Object mutex) {
        this.mutex = mutex;
    }

    @Override
    public void run(RunnableWithException runnable) throws Exception {
        synchronized (mutex) {
            runnable.run();
        }
    }
}

7.小结

Mailbox 模型是常见的用来控制并发的一种设计,通过引入 Mailbox 的线程模型,Flink 简化了 StreamTask 的代码逻辑,规避了多线程竞争带来的并发问题。

通过对 Mailbox、 MailboxProcessor、MailboxExecutor 这几个接口的设计进行分析,可以看出 Flink 的 Mailbox 模型设计还是比较优雅的,在使用方面也比较简单,很值得我们在开发其它项目的时候参考。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Flink shaded hadoop 2 uber 2.7.5-10.0是一个Flink和Hadoop之间的连接库,可帮助Flink与Hadoop的解耦和互操作。Flink shaded hadoop 2 uber 2.7.5-10.0有助于Flink将数据存储到Hadoop并从Hadoop中检索数据,以及使用Hadoop的文件系统功能。此外,它还简化了Flink与Hadoop之间的版本兼容性问题,并减少了不必要的依赖项。 要从源码下载Flink shaded hadoop 2 uber 2.7.5-10.0,可以到Apache Flink的官方网站寻找下载链接。您需要找到适当的版本号并下载相应的源代码。此外,您还需要阅读有关如何使用flink shaded hadoop 2 uber 2.7.5-10.0的文档,并确保您的系统符合相应的要求。 要使用flink shaded hadoop 2 uber 2.7.5-10.0,您需要将它集成到您的Flink应用程序中。这样,您的Flink应用程序将能够无缝地与Hadoop集成,并从Hadoop中读取和写入数据。请注意,您还需要正确配置Flink和Hadoop的环境变量以及相应的类路径才能使flink shaded hadoop 2 uber 2.7.5-10.0正常工作。 总之,flink shaded hadoop 2 uber 2.7.5-10.0是一个重要的库,可帮助Flink与Hadoop协同工作,顺利完成各种任务。如果您使用Flink和Hadoop来处理大数据,那么flink shaded hadoop 2 uber 2.7.5-10.0将是一个非常有用的工具。 ### 回答2: Flink是一个分布式流处理系统,用于处理流数据和实时事件。Shaded Hadoop 2 Uber是Flink的一个依赖包,用于实现与Hadoop的集成。 Hadoop 2.7.5-10.0是一个开源的分布式计算框架,用于处理大规模的数据集。Flink shaded hadoop 2 uber 2.7.5-10.0源码下载就是指Flink中,用于与Hadoop集成的一个依赖包,用于实现Flink与Hadoop的数据互通,帮助Flink实现更高效的数据处理方式。在下载Flink shaded hadoop 2 uber 2.7.5-10.0源码时,需要注意源码的版本号和依赖库,以确保程序的正确运行。此外,源码的下载和编译需要一定的技术和经验,需要有一定的开发经验和相关的开发环境和工具。总之,Flink shaded hadoop 2 uber 2.7.5-10.0源码下载是一个需要技术和经验的过程,但对于Flink的开发和集成是必须的。 ### 回答3: Flink shaded hadoop 2 uber 2.7.5-10.0 是什么? Flink shaded hadoop 2 uber 2.7.5-10.0 是一个用于 Apache HadoopFlink shaded hadoop uber 发行版。 该版本包含了许多来自 Apache Hadoop 2.7.5 的库,同时也集成了 Flink 所需的库和依赖项。 如果您需要运行 Flink 分布式应用程序,且需要和 Hadoop 2 一起工作,那么 Flink shaded hadoop 2 uber 2.7.5-10.0 是必不可少的。该版本允许您在 Hadoop 集群中运行 Flink 作业,同时也能够充分利用 Hadoop 的数据管理和处理能力。 如何进行 Flink shaded hadoop 2 uber 2.7.5-10.0 的源码下载? 如果您需要下载 Flink shaded hadoop 2 uber 2.7.5-10.0 的源码,可以遵循以下步骤: 1. 通过 Flink 的官方网站(https://flink.apache.org/)访问 Flink 的下载页面。 2. 在下载页面中,找到 Flink shaded hadoop 2 uber 2.7.5-10.0 的链接。 点击该链接,将跳转到 Flink 的 Maven 仓库页面。 3. 在 Maven 仓库页面中,您将看到 Flink shaded hadoop 2 uber 2.7.5-10.0 的所有构件。 您可以查看并选择需要的文件,然后通过“直接下载”按钮直接下载该文件或使用 Maven 命令下载。 例如,您可以使用以下 Maven 命令: ```xml <dependency> <groupId>org.apache.flink</groupId> <artifactId>flink-shaded-hadoop2-uber</artifactId> <version>2.7.5-10.0</version> </dependency> ``` 总之,Flink shaded hadoop 2 uber 2.7.5-10.0 是一个重要的发行版,使得 Flink 能够更好地与 Hadoop 集成,同时也方便了用户的部署和使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值