Flink源码解析之:如何根据算法生成StreamGraph过程
Flink源码解析之:如何根据算法生成StreamGraph过程
在我们日常编写Flink应用的时候,会首先创建一个StreamExecutionEnvironment.getExecutionEnvironment()
对象,在添加一些自定义处理算子后,会调用env.execute
来执行定义好的Flink应用程序。我们知道,Flink在实际执行任务前,会根据应用生成StreamGraph,再生成JobGraph,最终提交到集群中进行执行。那么Flink是如何将我们自定义的应用程序转换成StreamGraph的呢?这一过程中实现了什么逻辑? 接下来,我们通过源码来深入了解一下。
在本次分析源码的过程中,主要涉及到StreamExecutionEnvironment
、DataStream
、Transformation
、StreamGraph
、StreamGraphGenerator
几下个类,这里先汇总介绍一下在生成StreamGraph过程中,这些类的交互处理流程,有了这个印象后,再阅读下面的源码流程,更容易串起来和理解。
一、Function -> Transformation转换
在我们编写Flink应用程序时,会自定义一系列算子拼接在数据流链路中,比如,当我们调用datastream.flatMap(flatMapFunction)
方法时,就会将传入的算子函数,转换成Transformation对象,添加到StreamExecutionEnvironment
对象的List<Transformation<?>> transformations
属性中。接下来,我们就来看一下是如何进行转换的。
首先进入到DataStream
类中,找到比如flatMap
方法:
public <R> SingleOutputStreamOperator<R> flatMap(FlatMapFunction<T, R> flatMapper) {TypeInformation<R> outType =TypeExtractor.getFlatMapReturnTypes(clean(flatMapper), getType(), Utils.getCallLocationName(), true);return flatMap(flatMapper, outType);
}public <R> SingleOutputStreamOperator<R> flatMap(FlatMapFunction<T, R> flatMapper, TypeInformation<R> outputType) {return transform("Flat Map", outputType, new StreamFlatMap<>(clean(flatMapper)));
}
上面代码中,将flatMap
封装到StreamFlatMap
方法中,用于表示一个StreamOperator操作符。StreamFlatMap
操作符会针对每一个StreamRecord,通过processElement
方法调用用户函数去处理该流数据:
@Override
public void processElement(StreamRecord<IN> element) throws Exception {collector.setTimestamp(element);// 调用用户函数执行数据流元素处理逻辑userFunction.flatMap(element.getValue(), collector);
}
回到DataSteram
的FlatMap
方法中,我们继续看transform
方法里做了什么:
@PublicEvolving
public <R> SingleOutputStreamOperator<R> transform(String operatorName,TypeInformation<R> outTypeInfo,OneInputStreamOperator<T, R> operator) {return doTransform(operatorName, outTypeInfo, SimpleOperatorFactory.of(operator));
}
上面根据传入的StreamOperator
创建一个SimpleOperatorFactory
对象,StreamOperatorFactory是一个工厂类,其主职责是为特定类型的StreamOperator在运行时创建实例。它还提供了其他附加功能如做一些操作配置,比如chaining。
接下来继续进入doTransform
方法:
protected <R> SingleOutputStreamOperator<R> doTransform(String operatorName,TypeInformation<R> outTypeInfo,StreamOperatorFactory<R> operatorFactory) {// read the output type of the input Transform to coax out errors about MissingTypeInfo// 获取当前数据流(上一个Transformation)的输出类型。这样可以做类型检查,并在类型信息缺失时提前引发错误。transformation.getOutputType();// 创建一个新的OneInputTransformation,这个新的OneInputTransformation即为要添加的新操作// 对于flatMap操作来说,不存在分区,所以上下游是一对一的关系,所以这里用的是OneInputTransformationOneInputTransformation<T, R> resultTransform =new OneInputTransformation<>(this.transformation,operatorName,operatorFactory,outTypeInfo,environment.getParallelism());// 创建一个SingleOutputStreamOperator对象,该对象将接收新加入的操作的输出@SuppressWarnings({"unchecked", "rawtypes"})SingleOutputStreamOperator<R> returnStream =new SingleOutputStreamOperator(environment, resultTransform);// 将新的Transformation添加到当前的执行环境中,这个操作将并入到计算流图中。getExecutionEnvironment().addOperator(resultTransform);// 代表了新添加的操作输出结果的数据流,便于在这个数据流上继续构建后续的计算。return returnStream;
}
上述代码内容就是将userFunction转换成Transformation的具体执行逻辑了,因为我们最初举例是flatMap
方法,因此在将userFunction转换成Transformation时,会使用OneInputTransformation
来表示。同时这里可以看到,在转换完成后,会调用getExecutionEnvironment().addOperator(resultTransform)
将得到的Transformation
添加到当前执行环境的计算流图中,实际上也就是添加到我们刚刚所说的执行环境的List<Transformation<?>> transformations
属性中了。
二、StreamGraphGenerator生成StreamGraph
在将用户函数userFunction转换成Transformation并保存到StreamExecutionEnvironment的transformations属性中后,我们就收集抽象好了所有的用户函数及处理链路,接下来,就是根据这些封装好的Transformation来生成StreamGraph。
首先进入到StreamExecutionEnvironment
的execute
执行入口方法中:
public JobExecutionResult execute() throws Exception {return execute(getStreamGraph());
}@Internal
public StreamGraph getStreamGraph() {return getStreamGraph(true);
}@Internal
public StreamGraph getStreamGraph(boolean clearTransformations) {final StreamGraph streamGraph = getStreamGraphGenerator(transformations).generate();if (clearTransformations) {transformations.clear();}return streamGraph;
}
在上面的getStreamGraph
方法中,使用getStreamGraphGenerator
方法生成一个StreamGraphGenerator
对象,这里的transformations
参数,实际上指的就是上面保存的每个用户函数转换得到的Transformation
对象。
接下来,我们主要看generator
方法,进入到StreamGraphGenerator
类中,这个类也是创建StreamGraph最核心的类。
public StreamGraph generate() {// 根据不同的配置信息创建一个StreamGraph对象streamGraph = new StreamGraph(executionConfig, checkpointConfig, savepointRestoreSettings);// 设置 StreamGraph 是否在任务结束后启用checkpoint,这个布尔值从配置中获取。streamGraph.setEnableCheckpointsAfterTasksFinish(configuration.get(ExecutionCheckpointingOptions.ENABLE_CHECKPOINTS_AFTER_TASKS_FINISH));shouldExecuteInBatchMode = shouldExecuteInBatchMode();configureStreamGraph(streamGraph);// 初始化一个哈希映射alreadyTransformed,用于存储已经被转换过的Transformation。alreadyTransformed = new HashMap<>();// 遍历transformations列表,对每个transformation对象进行转换// 这里是转换的核心逻辑for (Transformation<?> transformation : transformations) {transform(transformation);}// 将slotSharingGroupResources设置为StreamGraph的资源配置。streamGraph.setSlotSharingGroupResource(slotSharingGroupResources);setFineGrainedGlobalStreamExchangeMode(streamGraph);// 获取StreamGraph中所有的StreamNode,检查它们的输入边缘是否满足禁用未对齐的checkpointing的条件,如果满足条件,则将边的supportsUnalignedCheckpoints属性设置为false。for (StreamNode node : streamGraph.getStreamNodes()) {if (node.getInEdges().stream().anyMatch(this::shouldDisableUnalignedCheckpointing)) {for (StreamEdge edge : node.getInEdges()) {edge.setSupportsUnalignedCheckpoints(false);}}}// 清理streamGraph和alreadyTransformed以释放资源,并防止后续的错误使用,并保存当前的streamGraph实例到builtStreamGraph中。final StreamGraph builtStreamGraph = streamGraph;alreadyTransformed.clear();alreadyTransformed = null;streamGraph = null;// 最后返回构建好的StreamGraph。return builtStreamGraph;
}
上面代码中,最主要的核心逻辑在for循环遍历transformations中,调用transform
方法对每个Transformation对象进行转换。我们主要进入到该方法中进行分析:
/*** Transforms one {@code Transformation}.** <p>This checks whether we already transformed it and exits early in that case. If not it* delegates to one of the transformation specific methods.*/
private Collection<Integer> transform(Transformation<?> transform) {// 快速检查传入的 transform 对象是否已经在 alreadyTransformed 字典(一个缓存)中,如果已存在则直接返回对应的ID,这种早期退出的机制避免了对同一任务的重复转换。if (alreadyTransformed.containsKey(transform)) {return alreadyTransformed.get(transform);}LOG.debug("Transforming " + transform);if (transform.getMaxParallelism() <= 0) {// if the max parallelism hasn't been set, then first use the job wide max parallelism// from the ExecutionConfig.int globalMaxParallelismFromConfig = executionConfig.getMaxParallelism();if (globalMaxParallelismFromConfig > 0) {transform.setMaxParallelism(globalMaxParallelismFromConfig);}}// 若 transform 对象指定了 SlotSharingGroup ,那么会从 SlotSharingGroup 中提取资源并更新到 slotSharingGroupResources 中。transform.getSlotSharingGroup().ifPresent(slotSharingGroup -> {final ResourceSpec resourceSpec =SlotSharingGroupUtils.extractResourceSpec(slotSharingGroup);if (!resourceSpec.equals(ResourceSpec.UNKNOWN)) {slotSharingGroupResources.compute(slotSharingGroup.getName(),(name, profile) -> {if (profile == null) {return ResourceProfile.fromResourceSpec(resourceSpec, MemorySize.ZERO);} else if (!ResourceProfile.fromResourceSpec(resourceSpec, MemorySize.ZERO).equals(profile)) {throw new IllegalArgumentException("The slot sharing group "+ slotSharingGroup.getName()+ " has been configured with two different resource spec.");} else {return profile;}});}});// call at least once to trigger exceptions about MissingTypeInfo// 调用 transform.getOutputType() 进行安全检查,确保类型信息的完整性。transform.getOutputType();// 根据 transform 对象的类型获取对应的转换逻辑 translator. @SuppressWarnings("unchecked")final TransformationTranslator<?, Transformation<?>> translator =(TransformationTranslator<?, Transformation<?>>)translatorMap.get(transform.getClass());// 如果找到了相应的 translator,使用它进行转换;否则,使用旧的转换策略 legacyTransform()。Collection<Integer> transformedIds;if (translator != null) {transformedIds = translate(translator, transform);} else {transformedIds = legacyTransform(transform);}// 在转换完成后,检查 transform 是否已经被记录在 alreadyTransformed 字典中。如果尚未记录,则将转换后的对象ID添加到字典中。// need this check because the iterate transformation adds itself before// transforming the feedback edgesif (!alreadyTransformed.containsKey(transform)) {alreadyTransformed.put(transform, transformedIds);}// 将转换后产生的节点ID返回以供后续使用。return transformedIds;
}
很明显,这种转换不可能多次进行,因为这会浪费计算资源。因此,我们需要一个机制来记录哪些Transformation已经被转换过。在Flink中,这是通过一个名为alreadyTransformed的哈希映射实现的。如果当前的Transformation已经存在于alreadyTransformed中,那么就无需再次进行转换,直接返回对应的集合即可。
接下来,根据transform的具体类型,从translatorMap中获取相应的translator转换器(具体的translatorMap内容可以在代码中看到)。找到转换器后,调用translate方法来执行转换。那么我们又需要进入到translate
方法中一探究竟:
private Collection<Integer> translate(final TransformationTranslator<?, Transformation<?>> translator,final Transformation<?> transform) {checkNotNull(translator);checkNotNull(transform);// 通过调用getParentInputIds()方法获取当前transform对象的所有输入(父级Transformation)的ID。final List<Collection<Integer>> allInputIds = getParentInputIds(transform.getInputs());// 再次检查当前transform对象是否已在alreadyTransformed字典中,如果是,直接返回对应的ID。// the recursive call might have already transformed thisif (alreadyTransformed.containsKey(transform)) {return alreadyTransformed.get(transform);}// 确定slotSharingGroup,这是一个根据transform输入和slotSharingGroup名称,决定slot sharing策略的过程。final String slotSharingGroup =determineSlotSharingGroup(transform.getSlotSharingGroup().isPresent()? transform.getSlotSharingGroup().get().getName(): null,allInputIds.stream().flatMap(Collection::stream).collect(Collectors.toList()));// 创建一个TransformationTranslator.Context对象,里面包含了StreamGraph,slotSharingGroup和配置信息,该上下文会在转换过程中使用。final TransformationTranslator.Context context =new ContextImpl(this, streamGraph, slotSharingGroup, configuration);// 根据执行模式不同,调用转换方法translateForBatch()或translateForStreaming()进行具体的转换工作。return shouldExecuteInBatchMode? translator.translateForBatch(transform, context): translator.translateForStreaming(transform, context);
}
每一个TransformationTranslator实例都绑定了一个特定类型的Transformation的转换逻辑,例如OneInputTransformationTranslator,SourceTransformation等。通过这份代码,我们可以看到Flink的灵活性和可扩展性。你可以为特定的Transformation添加不同的激活逻辑或者处理逻辑。这种设计确保了Flink在处理不同类型Transformation时的高效性,并且很容易添加新类型的Transformation。
这里,我们仍然以OneInputTransformationTranslator的转换逻辑来举例,看一下Flink的Transformation转换逻辑执行了什么操作?
protected Collection<Integer> translateInternal(final Transformation<OUT> transformation,final StreamOperatorFactory<OUT> operatorFactory,final TypeInformation<IN> inputType,@Nullable final KeySelector<IN, ?> stateKeySelector,@Nullable final TypeInformation<?> stateKeyType,final Context context) {checkNotNull(transformation);checkNotNull(operatorFactory);checkNotNull(inputType);checkNotNull(context);// 即获取 StreamGraph、slotSharingGroup 和transformation的 ID。final StreamGraph streamGraph = context.getStreamGraph();final String slotSharingGroup = context.getSlotSharingGroup();final int transformationId = transformation.getId();final ExecutionConfig executionConfig = streamGraph.getExecutionConfig();// addOperator() 方法把转换Transformation 添加到 StreamGraph 中。此操作包括transformation的 ID,slotSharingGroup,CoLocationGroupKey,工厂类,输入类型,输出类型以及操作名。streamGraph.addOperator(transformationId,slotSharingGroup,transformation.getCoLocationGroupKey(),operatorFactory,inputType,transformation.getOutputType(),transformation.getName());// 如果 stateKeySelector(用于从输入中提取键的函数)非空,使用 stateKeyType 创建密钥序列化器,并在 StreamGraph 中设置用于接收单输入的状态键if (stateKeySelector != null) {TypeSerializer<?> keySerializer = stateKeyType.createSerializer(executionConfig);streamGraph.setOneInputStateKey(transformationId, stateKeySelector, keySerializer);}// 根据 Transformation 和 executionConfig 设置并行度。int parallelism =transformation.getParallelism() != ExecutionConfig.PARALLELISM_DEFAULT? transformation.getParallelism(): executionConfig.getParallelism();streamGraph.setParallelism(transformationId, parallelism);streamGraph.setMaxParallelism(transformationId, transformation.getMaxParallelism());final List<Transformation<?>> parentTransformations = transformation.getInputs();checkState(parentTransformations.size() == 1,"Expected exactly one input transformation but found "+ parentTransformations.size());// 根据转换的输入和输出添加边到 StreamGraph。每个输入转换都添加一条边。for (Integer inputId : context.getStreamNodeIds(parentTransformations.get(0))) {streamGraph.addEdge(inputId, transformationId, 0);}// 方法返回包含转换 ID 的单个元素集合。return Collections.singleton(transformationId);
}
上面这段代码,实际上就是构建StreamGraph的主体逻辑部分了,translateInternal() 方法实现了从 Transformation 到 StreamGraph 中操作的转换。在该方法中,对于每一个Transformation,会调用streamGraph.addOperator
方法,生成一个StreamNode对象,存储在StreamGraph的streamNode
属性中,该属性是一个Map<Integer, StreamNode>
结构,表示每个Transformation ID对应的StreamNode节点。
protected StreamNode addNode(Integer vertexID,@Nullable String slotSharingGroup,@Nullable String coLocationGroup,Class<? extends TaskInvokable> vertexClass,StreamOperatorFactory<?> operatorFactory,String operatorName) {if (streamNodes.containsKey(vertexID)) {throw new RuntimeException("Duplicate vertexID " + vertexID);}StreamNode vertex =new StreamNode(vertexID,slotSharingGroup,coLocationGroup,operatorFactory,operatorName,vertexClass);streamNodes.put(vertexID, vertex);return vertex;
}
看完translateInternal
方法中streamGraph.addOperator
的执行逻辑后,接下来还需要关注的一个步骤是streamGraph.addEdge
,这里是连接StreamGraph中各StreamNode节点的逻辑所在:
public void addEdge(Integer upStreamVertexID, Integer downStreamVertexID, int typeNumber) {addEdgeInternal(upStreamVertexID,downStreamVertexID,typeNumber,null,new ArrayList<String>(),null,null);
}
在addEdgeInternal
方法中,会区分当前节点是虚拟节点还是物理节点,从而添加物理边还是虚拟边。由于我们用OneInputTransformationTranslator
会创建物理节点,所以进入到创建物理边的分支代码中:
private void createActualEdge(Integer upStreamVertexID,Integer downStreamVertexID,int typeNumber,StreamPartitioner<?> partitioner,OutputTag outputTag,StreamExchangeMode exchangeMode) {// 首先通过节点ID获取上游和下游的StreamNode。StreamNode upstreamNode = getStreamNode(upStreamVertexID);StreamNode downstreamNode = getStreamNode(downStreamVertexID);// 检查分区器partitioner是否已经设置,如果没有设置,且上游节点与下游节点的并行度相等,那么使用ForwardPartitioner; 如果并行度不相等,则使用RebalancePartitioner。// If no partitioner was specified and the parallelism of upstream and downstream// operator matches use forward partitioning, use rebalance otherwise.if (partitioner == null&& upstreamNode.getParallelism() == downstreamNode.getParallelism()) {partitioner = new ForwardPartitioner<Object>();} else if (partitioner == null) {partitioner = new RebalancePartitioner<Object>();}if (partitioner instanceof ForwardPartitioner) {if (upstreamNode.getParallelism() != downstreamNode.getParallelism()) {throw new UnsupportedOperationException("Forward partitioning does not allow "+ "change of parallelism. Upstream operation: "+ upstreamNode+ " parallelism: "+ upstreamNode.getParallelism()+ ", downstream operation: "+ downstreamNode+ " parallelism: "+ downstreamNode.getParallelism()+ " You must use another partitioning strategy, such as broadcast, rebalance, shuffle or global.");}}if (exchangeMode == null) {exchangeMode = StreamExchangeMode.UNDEFINED;}/*** Just make sure that {@link StreamEdge} connecting same nodes (for example as a result of* self unioning a {@link DataStream}) are distinct and unique. Otherwise it would be* difficult on the {@link StreamTask} to assign {@link RecordWriter}s to correct {@link* StreamEdge}.*/// 在上述配置都设置好之后,创建StreamEdge对象,并将其添加到上游节点的出边和下游节点的入边。int uniqueId = getStreamEdges(upstreamNode.getId(), downstreamNode.getId()).size();StreamEdge edge =new StreamEdge(upstreamNode,downstreamNode,typeNumber,partitioner,outputTag,exchangeMode,uniqueId);getStreamNode(edge.getSourceId()).addOutEdge(edge);getStreamNode(edge.getTargetId()).addInEdge(edge);
}
从上述代码中可以看出,createActualEdge()
方法实现了在StreamGraph中添加实际的边的过程,这是构建Flink StreamGraph的一个重要步骤。
至此,我们就看到了创建StreamGraph,并根据Transformation来生成StreamNode,并添加StreamEdge边的过程,最终构建好一个完成的StreamGraph来表示Flink应用程序的数据流执行拓扑图。
当然这里我们只是以OneInputTransformationTranslator
转换器举例来分析流程,实际上其他的转换器应该会更复杂一些,有兴趣的可以继续深入研究,本文便不再赘述。同时,本文也仍然有很多细节暂时因为理解不够深入没有涉及,欢迎各位一起交流学习。
最终,在我们构造好StreamGraph后,就需要考虑如何将StreamGraph转换成JobGraph了,下一篇,将继续介绍StreamGraph -> JobGraph的转换。
相关文章:
Flink源码解析之:如何根据算法生成StreamGraph过程
Flink源码解析之:如何根据算法生成StreamGraph过程 在我们日常编写Flink应用的时候,会首先创建一个StreamExecutionEnvironment.getExecutionEnvironment()对象,在添加一些自定义处理算子后,会调用env.execute来执行定义好的Flin…...
【Spring MVC 核心机制】核心组件和工作流程解析
在 Web 应用开发中,处理用户请求的逻辑常常会涉及到路径匹配、请求分发、视图渲染等多个环节。Spring MVC 作为一款强大的 Web 框架,将这些复杂的操作高度抽象化,通过组件协作简化了开发者的工作。 无论是处理表单请求、生成动态页面&#x…...
2、Bert论文笔记
Bert论文 1、解决的问题2、预训练微调2.1预训练微调概念2.2深度双向2.3基于特征和微调(预训练下游策略) 3、模型架构4、输入/输出1.输入:2.输出:3.Learned Embeddings(学习嵌入)1. **Token Embedding**2. **Position Embedding**3…...
hadoop搭建
前言 一般企业中不会使用master slave01 slave02来命名 vmware创建虚拟机 打开vmware软件,新建虚拟机 典型 稍后安装系统 选择centos7 虚拟机名称和安放位置自行选择(最小化安装消耗空间较少) 默认磁盘大小即可 自定义硬件 选择centos7的i…...
19_HTML5 Web Workers --[HTML5 API 学习之旅]
HTML5 Web Workers 是一种允许 JavaScript 在后台线程中运行的技术,从而不会阻塞用户界面或其他脚本的执行。通过使用 Web Workers,你可以执行复杂的计算任务而不影响页面的响应速度,提升用户体验。 Web Workers 的特点 Web Workers 是 HTM…...
【PCIe 总线及设备入门学习专栏 5.1 -- PCIe 引脚 PRSNT 与热插拔】
文章目录 OverviewPRSNT 与热插拔PRSNT 硬件设计 Overview Spec 定义的热插拔是把一个PCIe卡(设备)从一个正在运行的背板或者系统中插入/或者移除。这个过程需要不影响系统的其他功能。插入的新的设备可以正确工作。 显然,这里面需要考虑的问…...
使用docker compose安装gitlab
使用docker compose安装gitlab GitLab简介设置GITLAB_HOME路径创建docker挂载目录获取可用的GitLab版本编写docker-compose.yml文件启动docker基础配置 GITLAB_OMNIBUS_CONFIG修改配置 中文设置数据库配置系统邮箱配置 GitLab简介 GitLab是一个基于Git的开源项目,…...
性能中 UV、PV 和并发量的关系
在性能测试中,UV(独立访客数)、PV(页面浏览量)和并发量是重要的指标,用于评估系统的负载能力。它们之间关系紧密,需要通过合理的计算和示例进行说明。 1. 概念解析 UV(Unique Visito…...
Go语言zero项目服务恢复与迁移文档
## 一. 服务器环境配置 在迁移和配置 项目时,首先需要确保服务器环境正确配置。以下是配置步骤: ### 1. 安装 Go 语言环境 首先,确保 Go 语言环境已经安装,并且配置正确。执行以下步骤: # 下载 Go 语言安装包 wge…...
Redis - Token JWT 概念解析及双token实现分布式session存储实战
Token 定义:令牌,访问资源接口(API)时所需要的资源凭证 一、Access Token 定义:访问资源接口(API)时所需要的资源凭证,存储在客户端 组成 组成部分说明uid用户唯一的身份标识time…...
QT中使用OpenGL function
1.前言 QT做界面编程很方便,QTOpenGL的使用也很方便,因为QT对原生的OpenGL API进行了面向对象化的封装。 如: 函数:initializeOpenGLFunctions()...... 类:QOpenGLVertexArrayObject、QOpenGLBuffer、QOpenGLShader…...
STM32-笔记18-呼吸灯
1、实验目的 使用定时器 4 通道 3 生成 PWM 波控制 LED1 ,实现呼吸灯效果。 频率:2kHz,PSC71,ARR499 利用定时器溢出公式 周期等于频率的倒数。故Tout 1/2KHZ;Ft 72MHZ PSC71(喜欢设置成Ft的倍数&…...
MAC M4安装QT使用国内镜像源在线安装
MAC M4安装QT使用国内镜像源在线安装 一、下载安装包1. 访问[https://www.qt.io/](https://www.qt.io/)下载在线安装包2. 下载结果 二、创建QT账户,安装的时候需要三、安装1. 终端打开安装包2. 指定安装源3. 运行安装完的QT 一、下载安装包 1. 访问https://www.qt.…...
go语言中zero框架项目日志收集与配置
在 GoZero 项目中,日志收集和配置是非常重要的,尤其是在分布式系统中,日志可以帮助开发人员追踪和排查问题。GoZero 提供了灵活的日志系统,能够方便地进行日志的配置和管理。 以下是如何在 GoZero 项目中进行日志收集与配置的基本…...
springboot496基于java手机销售网站设计和实现(论文+源码)_kaic
摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本手机销售网站就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息&am…...
iClient3D for Cesium在Vue中快速实现场景卷帘
作者:gaogy 1、背景 iClient3D for Cesium是由SuperMap提供的一个前端3D地图客户端,提供了丰富的功能与接口,使得开发者能够在Web应用中快速集成并展现3D地理信息。而在Vue框架中集成iClient3D,不仅可以利用Vue的响应式特性提高开…...
Elasticsearch-索引的批量操作
索引的批量操作 批量查询和批量增删改 批量查询 #批量查询 GET product/_search GET /_mget {"docs": [{"_index": "product","_id": 2},{"_index": "product","_id": 3}] }GET product/_mget {"…...
TVS二极管选型【EMC】
TVS器件并联在电路中,当电路正常工作时,他处于截止状态(高阻态),不影响线路正常工作,当线路处于异常过压并达到其击穿电压时,他迅速由高阻态变为低阻态,给瞬间电流提供一个低阻抗导通…...
反编译APK获取xml资源
第一步去官网下载 jar 包 最新的即可 apktool官网下载地址 下载好重命名一下 改成 apktool.jar 第二步将你的 apk 和 jar 包放在同一个文件夹下面 第三步在该文件夹下打开 命令行 并输入 java -jar apktool.jar d 测试.apk回车后会正在解析 解析完成后,文件夹下…...
C++ 设计模式:装饰模式(Decorator Pattern)
链接:C 设计模式 链接:C 设计模式 - 桥接模式 装饰模式(Decorator Pattern)是一种结构型设计模式,它允许向一个现有的对象添加新的功能,同时又不改变其结构。装饰模式通过创建一个装饰类来包装原始类&…...
排序算法之快速排序、归并排序
目录 快速排序归并排序的意义 快速排序 思维步骤 具体思想 测试样例解释 代码实现 归并排序 思维步骤 具体思想 测试样例解释 代码实现 快速排序归并排序的意义 快速排序和归并排序不仅仅是一种方法,更重要的是其作为一种算法而节省时间,在…...
一文读懂变分自编码(VAE)
一文读懂变分自编码(VAE) 概述 变分自编码器(Variational Autoencoder, VAE)是一种生成模型,用于学习数据的潜在表示并生成与原始数据分布相似的新数据。它是一种概率模型,通过结合深度学习和变分推断的思想,解决了传…...
【每日学点鸿蒙知识】webview性能优化、taskpool、热更新、Navigation问题、调试时每次都卸载重装问题
1、HarmonyOS webview页面第二次,第三次打开感觉和第一次打开速度差不多,有优化吗,或者有没有webview秒开方案之类的? 目前没有webview秒开的方案,针对web场景的优化参考一下文档:https://developer.huawe…...
周记-唐纳德的《计算机程序设计艺术》
用代码生成代码 开发一个协议,字段有些多,每个字段是QT的属性,需要写Q_PROPERTY,一个一个编辑的话比较繁琐,耗费时间。后来就用代码生成了头文件和源文件,get和set还有signal函数,内容基本都是…...
AR 模型的功率谱
功率谱密度(Power Spectral Density, PSD)的表达式是从信号的自相关函数和系统的频率响应推导出来的,特别是对于 AR(Auto-Regressive,自回归)模型。以下是推导的过程: 1. AR 模型的定义…...
抖音小程序登录(前端通过tt.login获取code换取openId)
抖音小程序登录 抖音开放平台小程序登录: https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/tutorial/basic-ability/microapp-login 前端(通过tt.login获取code) 流程 静默登录依赖小程序 API tt.login,把tt.loginsuccess 回调…...
Linux 更改Jenkins使用其他账户启动
Linux 更改Jenkins使用其他账户启动 步骤一:修改 Jenkins 配置文件1. 编辑 Jenkins 的 systemd 服务文件:2. 在编辑器中添加以下内容:3. 保存并退出编辑器 步骤二:更改 Jenkins 目录的权限步骤三:重新加载 systemd 配置…...
117.【C语言】数据结构之排序(选择排序)
目录 1.知识回顾 2.分析 设想的思路 代码 执行结果 编辑 错误排查和修复 详细分析出错点 执行结果 3.正确的思路 4.其他问题 1.知识回顾 参见42.5【C语言】选择排序代码 点我跳转 2.分析 知识回顾里所提到的文章的选择排序一次循环只比一个数字,和本文接下来要…...
读书系列2024
认知类 1、《人生没有太晚的开始》: 作者摩西奶奶。 书中经典语录:“与其着急忙慌地不知从何开始,不如一切都慢慢来,开始并坚持了,总会有结果的那一天。喜欢一件事,你就慢慢去做吧。” 2、《忏悔录》托尔…...
如何快速又安全的实现端口转发【Windows MAC linux通用】
背景 有很多程序是在虚拟机上运行的,返回的url 又是127.0.0.1。在个人电脑上调试需要解决这个问题。端口转发是一个不错的方法 可能的解决办法: 1.修改程序,返回虚拟机的ip (要改代码,换虚拟机还要再改代码…...
OpenGL变换矩阵和输入控制
在前面的文章当中我们已经成功播放了动画,让我们的角色动了起来,这一切变得比较有意思了起来。不过我们发现,角色虽然说是动了起来,不过只是在不停地原地踏步而已,而且我们也没有办法通过键盘来控制这个角色来进行移动…...
51单片机学习笔记——找不到REG52.H头文件,点亮一个LED
创建工程 将STC型号导入keil并使用 STC可以从官网下载,也可我这的网盘: 链接:https://pan.baidu.com/s/1bO85DPN3IFaXGhiKSwyOrA?pwd7f4h 提取码:7f4h 打开STC,选择“keil仿真设置”,选择“添加型号和头…...
07 基于OpenAMP的核间通信方案
引言 ZYNQ7020有两个CPU核心,这两个核心可以采用SMP或AMP方式进行调度,当采用AMP方式进行调度时核0和核1可以运行不同的操作系统,如核0运行Linux系统,提供有些复杂的用户交互工作,核1运行实时操作系统,对设…...
Ubuntu升级ssh版本到9.8
方案一:实测只有8.9有漏洞不推荐 1、更新软件包列表 sudo apt update 2、查找可用版本 apt-cache policy openssh-server 3、 选择版本 sudo apt install openssh-server1:9.8p1-<具体版本号> 4、 重启 sudo systemctl restart ssh 5、验证版本 /usr/sbin/ss…...
git设置项目远程仓库指向github的一个仓库
要将你的Git项目设置为指向GitHub上的远程仓库,你需要执行以下步骤: 创建GitHub仓库: 登录到你的GitHub账户。点击右上角的 “” 号,选择 “New repository” 创建一个新的仓库。填写仓库的名称,可以添加描述ÿ…...
【实战示例】面向对象的需求建模
前言 博主准备写一个以面向对象为核心思想的软件需求建模、领域建模的系列,总结一整套可落地的DDD的打法,前面几篇文章论述了如何进行面向对象的需求建模,本文将以一个简单的购物商城的需求来演示如何进行面向对象的需求建模。 面向对象的需…...
平方数的判断不用sqrt()函数
//判断一个数是不是平方数,13…(2*m-1)m*mn #include<stdio.h> int main(){ int n; scanf("%d",&n); int i; for(i1;n>0;i2){ nn-1; } if(n0){ printf("YES!\n"); …...
node.js之---回调函数
什么是回调函数? 为什么会有回调函数? 回调函数的特性 回调函数的应用场景 怎么解决回调地狱 什么是回调函数? 回调函数是一个函数,他作为参数传递给另外一个函数,并且会在另外一个函数执行完毕之后被调用&#…...
浏览器http缓存问题
一、什么是浏览器缓存 浏览器将请求过的资源(html、js、css、img)等,根据缓存机制,拷贝一份副本存储在浏览器的内存或者磁盘上。如果下一次请求的url相同时则根据缓存机制决定是读取内存或者磁盘上的数据还是去服务器请求资源文件…...
编写一个简单的引导加载程序(bootloader)
编写一个简单的引导加载程序(bootloader)通常用于嵌入式系统或自定义操作系统。这里,我将为你提供一个基于x86架构的简单汇编语言 bootloader 示例。这个 bootloader 将会在启动时打印一条消息到屏幕上。 使用 NASM 汇编器来编写这个 bootlo…...
Three.js 字体
在 Three.js 中,我们可以通过 FontLoader 加载字体,并结合 TextGeometry 创建 3D 文本。加载字体是因为字体文件包含了字体的几何信息,例如字体的形状、大小、粗细等,而 TextGeometry 则是根据字体信息生成 3D 文本的几何体。 在…...
Jenkins 构建流水线
在 Linux 系统上安装 Jenkins 服务,以及配置自动化构建项目 前置准备环境:docker、docker-compose、jdk、maven 一、环境搭建 1. Jenkins 安装 (1)拉取镜像 # 安装镜像包,默认安装最新版本 docker pull jenkins/jen…...
ES 磁盘使用率检查及处理方法
文章目录 1. 检查原因2. 检查方法3. 处理方法3.1 清理数据3.2 再次检查磁盘使用率 1. 检查原因 磁盘使用率在 85%以下,ES 可正常运行,达到 85%及以上会影响 PEIM 数据存储。 在 ES 磁盘分配分片控制策略中,为了保护数据节点的安全࿰…...
【回溯】LeetCode经典题目总结:组合、排列、子集、分割、N皇后、单词搜索
回溯 组合问题组合总和全排列子集分割回文串N皇后电话号码的字母组合单词搜索括号生成 组合问题 给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。 示例: 输入: n 4, k 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ] 树形结构࿱…...
uniapp开发小程序内嵌h5页面,video视频两边有细小黑色边框
1.问题如图 2.原因分析 是否为设置上述属性呢? 设置了,但是仍然有黑边。经过选中页面元素分析后,判断video元素本身就有这种特点,就是视频资源无法完全铺满元素容器。 3.解决方案...
Ubuntu meson使用
一 下载pip3 ,使用pip3下载 meson sudo apt install python3 sudo apt install python3-pip二 下载 nanjia sudo apt-get install ninja-build三 测试 meson 使用 1 同一个目录下创建两个文件 main.c #include<stdio.h> int main() {printf("meson t…...
实用技巧:关于 AD修改原理图库如何同步更新到有原理图 的解决方法
若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/144738332 长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV…...
算法排序算法
文章目录 快速排序[leetcode 215数组中的第K个最大元素](https://leetcode.cn/problems/kth-largest-element-in-an-array/)分析题解快速排序 桶排序[leetcode 347 前K个高频元素](https://leetcode.cn/problems/top-k-frequent-elements/)分析题解 快速排序 leetcode 215数组…...
植物大战僵尸杂交版3.0.2版本
更新内容 植物大战僵尸杂交版3.0.2版本的更新内容如下: • 修复BUG: • 游戏内贴图错乱的BUG。 • 无尽模式卡死的BUG。 • 卡牌模仿者的一系列BUG。 • 干扰车可能同时出现多辆的BUG。 • 冒险模式部分关卡无法过关的BUG。 • 新增内容…...
Kafka数据迁移全解析:同集群和跨集群
文章目录 一、同集群迁移二、跨集群迁移 Kafka两种迁移场景,分别是同集群数据迁移、跨集群数据迁移。 一、同集群迁移 应用场景: broker 迁移 主要使用的场景是broker 上线,下线,或者扩容等.基于同一套zookeeper的操作。 实践: 将需要新添加…...