Apache Flink 进阶教程(六):Flink 作业执行深度解析

news2025/1/31 3:06:19

目录

前言

Program 到 StreamGraph 的转化

StreamGraph 到 JobGraph 的转化

为什么要为每个 operator 生成 hash 值?

每个 operator 是怎样生成 hash 值的?

JobGraph 到 ExexcutionGraph 以及物理执行计划

Fink on Yarn 的缺陷

引入 Dispatcher 的原因主要有两点:

没有 Dispatcher job 运行过程

在有 Dispatcher 的模式下

新的资源调度框架下 single cluster job on Yarn 流程介绍

subTask 在执行时是怎么运行的?


前言

本文根据 Apache Flink 系列直播课程整理而成,由 Apache Flink Contributor、网易云音乐实时计算平台研发工程师岳猛分享(整理: 毛鹤)。主要分享内容为 Flink Job 执行作业的流程,文章将从两个方面进行分享:一是如何从 Program 到物理执行计划,二是生成物理执行计划后该如何调度和执行。

来源:Apache Flink学习网

Flink 有四层转换流程,第一层为 Program 到 StreamGraph;第二层为 StreamGraph 到 JobGraph;第三层为 JobGraph 到 ExecutionGraph;第四层为 ExecutionGraph 到物理执行计划。通过对 Program 的执行,能够生成一个 DAG 执行图,即逻辑执行图。如下:

null

第一部分将先讲解四层转化的流程,然后将以详细案例讲解四层的具体转化。

  • 第一层 StreamGraph 从 Source 节点开始,每一次 transform 生成一个 StreamNode,两个 StreamNode 通过 StreamEdge 连接在一起,形成 StreamNode 和 StreamEdge 构成的DAG。
  • 第二层 JobGraph,依旧从 Source 节点开始,然后去遍历寻找能够嵌到一起的 operator,如果能够嵌到一起则嵌到一起,不能嵌到一起的单独生成 jobVertex,通过 JobEdge 链接上下游 JobVertex,最终形成 JobVertex 层面的 DAG。
  • JobVertex DAG 提交到任务以后,从 Source 节点开始排序,根据 JobVertex 生成ExecutionJobVertex,根据 jobVertex的IntermediateDataSet 构建IntermediateResult,然后 IntermediateResult 构建上下游的依赖关系,形成 ExecutionJobVertex 层面的 DAG 即 ExecutionGraph。
  • 最后通过 ExecutionGraph 层到物理执行层。

Program 到 StreamGraph 的转化

Program 转换成 StreamGraph 具体分为三步:

  • 从 StreamExecutionEnvironment.execute 开始执行程序,将 transform 添加到 StreamExecutionEnvironment 的 transformations。
  • 调用 StreamGraphGenerator 的 generateInternal 方法,遍历 transformations 构建 StreamNode 及 StreamEage。
  • 通过 StreamEdge 连接 StreamNode。

null

通过 WindowWordCount 来看代码到 StreamGraph 的转化,在 flatMap transform 设置 slot 共享组为 flatMap_sg,并发设置为 4,在聚合的操作中设置 slot 共享组为 sum_sg, sum() 和 counts() 并发设置为 3,这样设置主要是为了演示后面如何嵌到一起的,跟上下游节点的并发以及上游的共享组有关。

WindowWordCount 代码中可以看到,在 readTextFile() 中会生成一个 transform,且 transform 的 ID 是 1;然后到 flatMap() 会生成一个 transform, transform 的 ID 是 2;接着到 keyBy() 生成一个 transform 的 ID 是 3;再到 sum() 生成一个 transform 的 ID 是 4;最后到 counts()生成 transform 的 ID 是 5。

null

transform 的结构如图所示,第一个是 flatMap 的 transform,第二个是 window 的 transform,第三个是 SinkTransform 的 transform。除此之外,还能在 transform 的结构中看到每个 transform 的 input 是什么。

接下来介绍一下 StreamNode 和 StreamEdge。

  • StreamNode 是用来描述 operator 的逻辑节点,其关键成员变量有 slotSharingGroup、jobVertexClass、inEdges、outEdges以及transformationUID;
  • StreamEdge 是用来描述两个 operator 逻辑的链接边,其关键变量有 sourceVertex、targetVertex。

WindowWordCount transform 到 StreamGraph 转化如图所示,StreamExecutionEnvironment 的 transformations 存在 3 个 transform,分别是 Flat Map(Id 2)、Window(Id 4)、Sink(Id 5)。

transform 的时候首先递归处理 transform 的 input,生成 StreamNode,然后通过 StreamEdge 链接上下游 StreamNode。需要注意的是,有些 transform 操作并不会生成StreamNode 如 PartitionTransformtion,而是生成个虚拟节点。

null

在转换完成后可以看到,streamNodes 有四种 transform 形式,分别为 Source、Flat Map、Window、Sink。

每个 streamNode 对象都携带并发个数、slotSharingGroup、执行类等运行信息。

StreamGraph 到 JobGraph 的转化

null

StreamGraph 到 JobGraph 的转化步骤:

  • 设置调度模式,Eager 所有节点立即启动。
  • 广度优先遍历 StreamGraph,为每个 streamNode 生成 byte 数组类型的 hash 值。
  • 从 source 节点开始递归寻找嵌到一起的 operator,不能嵌到一起的节点单独生成 jobVertex,能够嵌到一起的开始节点生成 jobVertex,其他节点以序列化的形式写入到 StreamConfig,然后 merge 到 CHAINED_TASK_CONFIG,再通过 JobEdge 链接上下游 JobVertex。
  • 将每个 JobVertex 的入边(StreamEdge)序列化到该 StreamConfig。
  • 根据 group name 为每个 JobVertext 指定 SlotSharingGroup。
  • 配置 checkpoint。
  • 将缓存文件存文件的配置添加到 configuration 中。
  • 设置 ExecutionConfig。

从 source 节点递归寻找嵌到一起的 operator 中,嵌到一起需要满足一定的条件,具体条件介绍如下:

  • 下游节点只有一个输入。
  • 下游节点的操作符不为 null。
  • 上游节点的操作符不为 null。
  • 上下游节点在一个槽位共享组内。
  • 下游节点的连接策略是 ALWAYS。
  • 上游节点的连接策略是 HEAD 或者 ALWAYS。
  • edge 的分区函数是 ForwardPartitioner 的实例。
  • 上下游节点的并行度相等。
  • 可以进行节点连接操作。

null

JobGraph 对象结构如上图所示,taskVertices 中只存在 Window、Flat Map、Source 三个 TaskVertex,Sink operator 被嵌到 window operator 中去了。

为什么要为每个 operator 生成 hash 值?

Flink 任务失败的时候,各个 operator 是能够从 checkpoint 中恢复到失败之前的状态的,恢复的时候是依据 JobVertexID(hash 值)进行状态恢复的。相同的任务在恢复的时候要求 operator 的 hash 值不变,因此能够获取对应的状态。

每个 operator 是怎样生成 hash 值的?

如果用户对节点指定了一个散列值,则基于用户指定的值能够产生一个长度为 16 的字节数组。如果用户没有指定,则根据当前节点所处的位置,产生一个散列值。

考虑的因素主要有三点:

  • 一是在当前 StreamNode 之前已经处理过的节点的个数,作为当前 StreamNode 的 id,添加到 hasher 中;
  • 二是遍历当前 StreamNode 输出的每个 StreamEdge,并判断当前 StreamNode 与这个 StreamEdge 的目标 StreamNode 是否可以进行链接,如果可以,则将目标 StreamNode 的 id 也放入 hasher 中,且这个目标 StreamNode 的 id 与当前 StreamNode 的 id 取相同的值;
  • 三是将上述步骤后产生的字节数据,与当前 StreamNode 的所有输入 StreamNode 对应的字节数据,进行相应的位操作,最终得到的字节数据,就是当前 StreamNode 对应的长度为 16 的字节数组。

JobGraph 到 ExexcutionGraph 以及物理执行计划

JobGraph 到 ExexcutionGraph 以及物理执行计划的流程:

  • 将 JobGraph 里面的 jobVertex 从 Source 节点开始排序。
  • 在 executionGraph.attachJobGraph(sortedTopology)方法里面,根据 JobVertex 生成 ExecutionJobVertex,在 ExecutionJobVertex 构造方法里面,根据 jobVertex 的 IntermediateDataSet 构建 IntermediateResult,根据 jobVertex 并发构建 ExecutionVertex,ExecutionVertex 构建的时候,构建 IntermediateResultPartition(每一个 Execution 构建 IntermediateResult 数个IntermediateResultPartition );将创建的 ExecutionJobVertex 与前置的 IntermediateResult 连接起来。
  • 构建 ExecutionEdge ,连接到前面的 IntermediateResultPartition,最终从 ExecutionGraph 到物理执行计划。

基于 Yarn 层面的架构类似 Spark on Yarn 模式,都是由 Client 提交 App 到 RM 上面去运行,然后 RM 分配第一个 container 去运行 AM,然后由 AM 去负责资源的监督和管理。需要说明的是,Flink 的 Yarn 模式更加类似 Spark on Yarn 的 cluster 模式,在 cluster 模式中,dirver 将作为 AM 中的一个线程去运行。Flink on Yarn 模式也是会将 JobManager 启动在 container 里面,去做个 driver 类似的任务调度和分配,Yarn AM 与 Flink JobManager 在同一个 Container 中,这样 AM 可以知道 Flink JobManager 的地址,从而 AM 可以申请 Container 去启动 Flink TaskManager。待 Flink 成功运行在 Yarn 集群上,Flink Yarn Client 就可以提交 Flink Job 到 Flink JobManager,并进行后续的映射、调度和计算处理。

Fink on Yarn 的缺陷
  • 资源分配是静态的,一个作业需要在启动时获取所需的资源并且在它的生命周期里一直持有这些资源。这导致了作业不能随负载变化而动态调整,在负载下降时无法归还空闲的资源,在负载上升时也无法动态扩展。
  • On-Yarn 模式下,所有的 container 都是固定大小的,导致无法根据作业需求来调整 container 的结构。譬如 CPU 密集的作业或许需要更多的核,但不需要太多内存,固定结构的 container 会导致内存被浪费。
  • 与容器管理基础设施的交互比较笨拙,需要两个步骤来启动 Flink 作业: 1.启动 Flink 守护进程;2.提交作业。如果作业被容器化并且将作业部署作为容器部署的一部分,那么将不再需要步骤2。
  • On-Yarn 模式下,作业管理页面会在作业完成后消失不可访问。
  • Flink 推荐 **per job clusters** 的部署方式,但是又支持可以在一个集群上运行多个作业的 session 模式,令人疑惑。

在 Flink 版本 1.5 中引入了 Dispatcher,Dispatcher 是在新设计里引入的一个新概念。Dispatcher 会从 Client 端接受作业提交请求并代表它在集群管理器上启动作业。

引入 Dispatcher 的原因主要有两点:
  • 第一,一些集群管理器需要一个中心化的作业生成和监控实例;
  • 第二,能够实现 Standalone 模式下 JobManager 的角色,且等待作业提交。在一些案例中,Dispatcher 是可选的(Yarn)或者不兼容的(kubernetes)。

没有 Dispatcher job 运行过程

客户端提交 JobGraph 以及依赖 jar 包到 YarnResourceManager,接着 Yarn ResourceManager 分配第一个 container 以此来启动 AppMaster,Application Master 中会启动一个 FlinkResourceManager 以及 JobManager,JobManager 会根据 JobGraph 生成的 ExecutionGraph 以及物理执行计划向 FlinkResourceManager 申请 slot,FlinkResoourceManager 会管理这些 slot 以及请求,如果没有可用 slot 就向 Yarn 的 ResourceManager 申请 container,container 启动以后会注册到 FlinkResourceManager,最后 JobManager 会将 subTask deploy 到对应 container 的 slot 中去。

在有 Dispatcher 的模式下

会增加一个过程,就是 Client 会直接通过 HTTP Server 的方式,然后用 Dispatcher 将这个任务提交到 Yarn ResourceManager 中。

新框架具有四大优势,详情如下:

  • client 直接在 Yarn 上启动作业,而不需要先启动一个集群然后再提交作业到集群。因此 client 再提交作业后可以马上返回。
  • 所有的用户依赖库和配置文件都被直接放在应用的 classpath,而不是用动态的用户代码 classloader 去加载。
  • container 在需要时才请求,不再使用时会被释放。
  • “需要时申请”的 container 分配方式允许不同算子使用不同 profile (CPU 和内存结构)的 container。

新的资源调度框架下 single cluster job on Yarn 流程介绍

single cluster job on Yarn 模式涉及三个实例对象:

  • clifrontend
    • Invoke App code;
    • 生成 StreamGraph,然后转化为 JobGraph;
  • YarnJobClusterEntrypoint(Master)
    • 依次启动 YarnResourceManager、MinDispatcher、JobManagerRunner 三者都服从分布式协同一致的策略;
    • JobManagerRunner 将 JobGraph 转化为 ExecutionGraph ,然后转化为物理执行任务Execution,然后进行 deploy,deploy 过程会向 YarnResourceManager 请求 slot,如果有直接 deploy 到对应的 YarnTaskExecutiontor 的 slot 里面,没有则向 Yarn 的 ResourceManager 申请,带 container 启动以后 deploy。
  • YarnTaskExecutorRunner (slave)
    • 负责接收 subTask,并运行。

整个任务运行代码调用流程如下图:

subTask 在执行时是怎么运行的?

调用 StreamTask 的 invoke 方法,执行步骤如下:
* initializeState()即operator的initializeState()
* openAllOperators() 即operator的open()方法
* 最后调用 run 方法来进行真正的任务处理

我们来看下 flatMap 对应的 OneInputStreamTask 的 run 方法具体是怎么处理的。

    @Override
    protected void run() throws Exception {
        // cache processor reference on the stack, to make the code more JIT friendly
        final StreamInputProcessor<IN> inputProcessor = this.inputProcessor;

        while (running && inputProcessor.processInput()) {
            // all the work happens in the "processInput" method
        }
    }

最终是调用 StreamInputProcessor 的 processInput() 做数据的处理,这里面包含用户的处理逻辑。

public boolean processInput() throws Exception {
        if (isFinished) {
            return false;
        }
        if (numRecordsIn == null) {
            try {
                numRecordsIn = ((OperatorMetricGroup) streamOperator.getMetricGroup()).getIOMetricGroup().getNumRecordsInCounter();
            } catch (Exception e) {
                LOG.warn("An exception occurred during the metrics setup.", e);
                numRecordsIn = new SimpleCounter();
            }
        }

        while (true) {
            if (currentRecordDeserializer != null) {
                DeserializationResult result = currentRecordDeserializer.getNextRecord(deserializationDelegate);

                if (result.isBufferConsumed()) {
                    currentRecordDeserializer.getCurrentBuffer().recycleBuffer();
                    currentRecordDeserializer = null;
                }

                if (result.isFullRecord()) {
                    StreamElement recordOrMark = deserializationDelegate.getInstance();
                    //处理watermark
                    if (recordOrMark.isWatermark()) {
                        // handle watermark
                        //watermark处理逻辑,这里可能引起timer的trigger
                        statusWatermarkValve.inputWatermark(recordOrMark.asWatermark(), currentChannel);
                        continue;
                    } else if (recordOrMark.isStreamStatus()) {
                        // handle stream status
                        statusWatermarkValve.inputStreamStatus(recordOrMark.asStreamStatus(), currentChannel);
                        continue;
                        //处理latency watermark
                    } else if (recordOrMark.isLatencyMarker()) {
                        // handle latency marker
                        synchronized (lock) {
                            streamOperator.processLatencyMarker(recordOrMark.asLatencyMarker());
                        }
                        continue;
                    } else {
                        //用户的真正的代码逻辑
                        // now we can do the actual processing
                        StreamRecord<IN> record = recordOrMark.asRecord();
                        synchronized (lock) {
                            numRecordsIn.inc();
                            streamOperator.setKeyContextElement1(record);
                            //处理数据
                            streamOperator.processElement(record);
                        }
                        return true;
                    }
                }
            }

            //这里会进行checkpoint barrier的判断和对齐,以及不同partition 里面checkpoint barrier不一致时候的,数据buffer,

            final BufferOrEvent bufferOrEvent = barrierHandler.getNextNonBlocked();
            if (bufferOrEvent != null) {
                if (bufferOrEvent.isBuffer()) {
                    currentChannel = bufferOrEvent.getChannelIndex();
                    currentRecordDeserializer = recordDeserializers[currentChannel];
                    currentRecordDeserializer.setNextBuffer(bufferOrEvent.getBuffer());
                }
                else {
                    // Event received
                    final AbstractEvent event = bufferOrEvent.getEvent();
                    if (event.getClass() != EndOfPartitionEvent.class) {
                        throw new IOException("Unexpected event: " + event);
                    }
                }
            }
            else {
                isFinished = true;
                if (!barrierHandler.isEmpty()) {
                    throw new IllegalStateException("Trailing data in checkpoint barrier handler.");
                }
                return false;
            }
        }
    }

streamOperator.processElement(record) 最终会调用用户的代码处理逻辑,假如 operator 是 StreamFlatMap 的话,

    @Override
    public void processElement(StreamRecord<IN> element) throws Exception {
        collector.setTimestamp(element);
        userFunction.flatMap(element.getValue(), collector);//用户代码
    }

如有不正确的地方,欢迎指正,关于 Flink 资源调度架构调整,网上有一篇非常不错的针对 FLIP-6 的翻译,推荐给大家。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1329711.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSS自适应分辨率 amfe-flexible 和 postcss-pxtorem:Webpack5 升级后相关插件和配置更新说明

前言 项目对应的 webpack5 版本如下&#xff1a; npm i webpack5.89.0 -D npm i webpack-cli5.1.4 -D升级插件 说明一下&#xff0c;我更喜欢固定版本号&#xff0c;这样随机bug会少很多&#xff0c;更可控~ npm i postcss-loader6.1.1 -D npm i postcss-pxtorem6.0.0 -D配…

Redis单机、主从、哨兵、集群配置

单机配置启动 Redis安装 下载地址&#xff1a;Download | Redis 安装步骤&#xff1a; 1: 安装gcc编译器&#xff1a;yum install gcc 2: 将下载好的redis‐5.0.3.tar.gz文件放置在/usr/local文件夹下&#xff0c;并解压redis‐5.0.3.tar.gz文件 wget http://download.re…

编译原理----算符优先级的分析(自底向上)

自底向上分析的分类如下所示&#xff1a; 算符优先分析 算符优先分析只规定算符之间的优先关系&#xff0c;也就是只考虑终结符之间的优先关系。 &#xff08;一&#xff09;若有文法G&#xff0c;如果G没有形如A->..BC..的产生式&#xff0c;其中B和C为非终结符&#xff…

BigQuery 分区表简介和使用

大纲 什么是分区表 我们先看定义&#xff1a; 分区表是一种数据库表设计和管理技术&#xff0c;它将表中的数据划分为逻辑上的多个分区&#xff0c;每个分区包含一组特定的数据。每个分区都根据定义的分区键&#xff08;通常是一个列或字段&#xff09;的值进行分类&#xff…

vue3使用echarts漏斗,根据数据计算比例大小

需求&#xff1a;我们在开发过程中会遇到漏斗图的使用&#xff0c;如果用echarts里面自带的算法绘制渲染漏斗图时候&#xff0c;如果后端给的数据相差不大很接近时候&#xff0c;漏斗图渲染的结果不明显看不出来变化的。 优化之前的漏斗图&#xff1a; 优化之后的漏斗图&…

Flink 运行时[Runtime] 整体架构

一、基本组件栈 在Flink整个软件架构体系中&#xff0c;同样遵循着分层的架构设计理念&#xff0c;在降低系统耦合度的同时&#xff0c;也为上层用户构建Flink应用提供了丰富且友好的接口。从下图中可以看出整个Flink的架构体系基本上可以分为三层&#xff0c;由上往下依次是 …

BUUCTF-Linux Labs

Linux Labs 根据题目给出的内容&#xff0c;在kali中连接靶机&#xff0c;输入密码进入命令行模式 ls发现什么都没有&#xff0c;有可能进入到了一个空文件夹 cd .. 切换到上一层目录&#xff0c;ls查看此目录下的内容&#xff0c;发现flag.txt文件&#xff0c;查看文件是flag …

nodejs+vue+微信小程序+python+PHP医疗机构药品及耗材信息管理系统-计算机毕业设计推荐

为了帮助用户更好的了解和理解程序的开发流程与相关内容&#xff0c;本文将通过六个章节进行内容阐述。 第一章&#xff1a;描述了程序的开发背景&#xff0c;程序运用于现实生活的目的与意义&#xff0c;以及程序文档的结构安排信息&#xff1b; 第二章&#xff1a;描述了程序…

OpenCV与YOLO学习与研究指南

引言 OpenCV是一个开源的计算机视觉和机器学习软件库&#xff0c;而YOLO&#xff08;You Only Look Once&#xff09;是一个流行的实时对象检测系统。对于大学生和初学者而言&#xff0c;掌握这两项技术将大大提升他们在图像处理和机器视觉领域的能力。 基础知识储备 在深入…

【第七在线】数据分析与人工智能在商品计划中的应用

随着技术的不断进步&#xff0c;数据分析和人工智能&#xff08;AI&#xff09;已经成为了现代商品计划的关键组成部分。在服装行业&#xff0c;这两项技术正在帮助企业更好地理解市场需求、优化库存管理、提高生产效率和提供更好的客户体验。本文将深入探讨数据分析和人工智能…

无约束优化问题求解(3):共轭梯度法

目录 4. 共轭梯度法4.1 共轭方向4.2 共轭梯度法4.3 共轭梯度法的程序实现4.4 非二次函数的共轭梯度法 Reference 4. 共轭梯度法 4.1 共轭方向 最速下降法的线搜索采取精确线搜索时&#xff0c;由精确线搜索需要满足的条件&#xff1a;迭代点列 x k 1 x k α k d k x_{k1}…

CSS-SVG-环形进度条

线上代码地址 <div class"circular-progress-bar"><svg><circle class"circle-bg" /><circle class"circle-progress" style"stroke-dasharray: calc(2 * 3.1415 * var(--r) * (var(--percent) / 100)), 1000" …

【智慧办公】如何让智能会议室的电子标签实现远程、批量更新信息?Dusun物联网硬件网关让解决方案更具竞争力

近年来&#xff0c;为了减少办公耗能、节能环保、降本增效&#xff0c;越来越多的企业开始从传统的办公模式转向智慧办公。 以智能会议室为例&#xff0c;会议是企业业务中不可或缺的一部分&#xff0c;但在传统办公模式下&#xff0c;一来会议前行政人员需要提前准备会议材料…

【Amazon 实验③】Amazon WAF功能增强之追踪 Amazon WAF RequestID,排查误杀原因

文章目录 1. 方案介绍2. 架构图3. 操作演示 本实验将介绍如何利用 Amazon LambdaEdge&#xff0c;在 Amazon CloudFront 自定义错误页面 上展示每个由 Amazon WAF 返回的“403 Forbidden”错误的 Request ID。通过这个唯一的 WAF Request ID&#xff0c;网站运维工程师能够快速…

SuperMap iServer发布的ArcGIS REST 地图服务如何通过ArcGIS API加载

作者&#xff1a;yx 文章目录 一、发布服务二、代码加载三、结果展示 一、发布服务 SuperMap iServer支持将地图发布为ArcGIS REST地图服务&#xff0c;您可以在发布服务时直接勾选ArcGIS REST地图服务&#xff0c;如下图所示&#xff1a; 也可以在已发布的地图服务中&#x…

DshanMCU-R128s2 SDK 架构与目录结构

R128 S2 是全志提供的一款 M33(ARM)C906(RISCV-64)HIFI5(Xtensa) 三核异构 SoC&#xff0c;同时芯片内部 SIP 有 1M SRAM、8M LSPSRAM、8M HSPSRAM 以及 16M NORFLASH。 本文档作为 R128 FreeRTOS SDK 开发指南&#xff0c;旨在帮助软件开发工程师、技术支持工程师快速上手&am…

Apache Flink 进阶教程(七):网络流控及反压剖析

目录 前言 网络流控的概念与背景 为什么需要网络流控 网络流控的实现&#xff1a;静态限速 网络流控的实现&#xff1a;动态反馈/自动反压 案例一&#xff1a;Storm 反压实现 案例二&#xff1a;Spark Streaming 反压实现 疑问&#xff1a;为什么 Flink&#xff08;bef…

关于游戏性能优化的技巧

关于游戏性能优化的技巧 游戏性能优化对象池Jobs、Burst、多线程间隔处理定时更新全局广播缓存组件缓存常用数据2D残影优化2D骨骼转GPU动画定时器优化DrawCall合批处理优化碰撞层优化粒子特效 游戏性能优化 好久没有在CSDN上面写文章了&#xff0c;今天突然看到鬼谷工作室技术…

华为端口隔离简单使用方法同vlan下控制个别电脑不给互通

必须得用access接口&#xff0c;hybrid口不行 dhcp enable interface Vlanif1 ip address 192.168.1.1 255.255.255.0 dhcp select interface interface MEth0/0/1 interface GigabitEthernet0/0/1 port link-type access port-isolate enable group 1 interface GigabitEther…

eventbus,在this.$on监听事件时无法在获取数据

问题&#xff1a;vue中eventbus被多次触发&#xff0c;在this.$on监听事件时&#xff0c;内部的this发生改变导致&#xff0c;无法在vue实例中添加数据。 项目场景 一开始的需求是这样的&#xff0c;为了实现两个组件(A.vue ,B.vue)之间的数据传递。 页面A&#xff0c;点击页面…