Flink之Watermark源码解析

1. WaterMark源码分析

在Flink官网中介绍watermark和数据是异步处理的,通过分析源码得知这个说法不够准确或者说不够详细,这个异步处理要分为两种情况:
  • watermark源头
  • watermark下游
这两种情况的处理方式并不相同,在watermark的源头确实是异步处理的,但是在下游只是做的判断,这里会结合源码进行说明.
  • 代码

    public class FlinkWaterMark {public static void main(String[] args) throws Exception {StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();env.setParallelism(1);env.getConfig().setAutoWatermarkInterval(1000);// 获取Socket数据源DataStreamSource<String> socketSource = env.socketTextStream("localhost", 8888);// 将数据转换成UserEvent2SingleOutputStreamOperator<UserEvent2> mapStream = socketSource.map(s -> {String[] split = s.split(",");UserEvent2 userEvent2 = UserEvent2.builder().uId(split[0]).name(split[1]).event(split[2]).time(split[3]).build();return userEvent2;}).returns(UserEvent2.class).disableChaining(); // 这里做算子链的解绑// 构造Watermark策略,使用允许时间乱序策略,并设置允许时间乱序时间最大值为2000msWatermarkStrategy<UserEvent2> watermark = WatermarkStrategy.<UserEvent2>forBoundedOutOfOrderness(Duration.ofMillis(2000)) // 设置乱序时间.withTimestampAssigner(new SerializableTimestampAssigner<UserEvent2>() {@Overridepublic long extractTimestamp(UserEvent2 userEvent2, long l) {/** 抽取事件时间逻辑, 根据数据中的实际情况来看 **/String time = userEvent2.getTime();// 将事件中携带的时间转换成毫秒值DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");LocalDateTime parse = LocalDateTime.parse(time, dateTimeFormatter);Instant instant = parse.atZone(ZoneId.systemDefault()).toInstant();long timestamp = instant.toEpochMilli();return timestamp;}});// 将构造完成的watermark分配给数据流SingleOutputStreamOperator<UserEvent2> mapStream2 = mapStream.assignTimestampsAndWatermarks(watermark);// 通过process算子打印watermark信息mapStream2.process(new ProcessFunction<UserEvent2, UserEvent2>() {@Overridepublic void processElement(UserEvent2 value, ProcessFunction<UserEvent2, UserEvent2>.Context ctx, Collector<UserEvent2> out) throws Exception {// 获取水位线long l = ctx.timerService().currentWatermark();System.out.println("当前watermark: " + l);// 直接输出数据out.collect(value);}}).startNewChain().print(); // 这里要注意,一定要新开一个算子链,如果watermark和process算子绑定到一个算子链中就不会形成上下游的关系,后续的一些实验也就无法验证env.execute();}
    }
    

    下面将会围绕上面这段业务代码对watermark源码进行解析,在进行解析前说一个小技巧:当对某个框架进行源码解析时,最好是通过Debug的方式进行,就以Flink为例,如果直接通过点击查看源码是很难进行追溯的,代码执行过程中所用到的很多类通过点击的方式是查看不到的.

1.1 watermark源头源码分析
  1. 通过forBoundedOutOfOrderness(Duration.ofMillis(2000))方法点进源码,再通过源码进入到BoundedOutOfOrdernessWatermarks类中,对onEvent方法和onPeriodicEmit方法中的方法体中的内容打上断点

        @Overridepublic void onEvent(T event, long eventTimestamp, WatermarkOutput output) {maxTimestamp = Math.max(maxTimestamp, eventTimestamp); // 断点位置}@Overridepublic void onPeriodicEmit(WatermarkOutput output) {output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1)); // 断点位置}
    
    • onEvent

      onEvent方法其实就是筛选最大时间的通过maxTimestampeventTimestamp进行比较来判断是否根据事件时间更新maxTimestamp.

    • onPeriodicEmit

      onPeriodicEmit方法根据名称就可以看出是周期性发射,而发射的内容就是watermark,通过方法体可以看到每次发射的watermark都是maxTimestamp - outOfOrdernessMillis - 1,maxTimestamp就是上面onEvent方法获取的最大时间戳,outOfOrdernessMillis就是在调用forBoundedOutOfOrderness(Duration.ofMillis(2000))这个方法时所给的2000的时间容错,1则是恒定减1.

  2. 通过Debug执行代码,在Debugger中是可以看到代码执行过程所调用的源码依赖顺序,如下图
    在这里插入图片描述
    通过上面的图片可以很清晰的看到代码执行过程类的调用顺序,这里不要关注每一个类的源码内容,可以直接点击onProcessingTime这个方法,在图片中就是第二行中的方法,点击这个方法后就可以定位到TimestampsAndWatermarksOperator这个类.

  3. TimestampsAndWatermarksOperator关注点

    TimestampsAndWatermarksOperator这个类中只需要关注三个方法即可openprocessElementonProcessingTime

    public class TimestampsAndWatermarksOperator<T> extends AbstractStreamOperator<T>implements OneInputStreamOperator<T, T>, ProcessingTimeCallback {// ...// 生命周期方法,初始化各种信息@Overridepublic void open() throws Exception {super.open();timestampAssigner = watermarkStrategy.createTimestampAssigner(this::getMetricGroup);watermarkGenerator =emitProgressiveWatermarks? watermarkStrategy.createWatermarkGenerator(this::getMetricGroup): new NoWatermarksGenerator<>();wmOutput = new WatermarkEmitter(output);// 根据配置获取watermark发送间隔,默认200mswatermarkInterval = getExecutionConfig().getAutoWatermarkInterval(); if (watermarkInterval > 0 && emitProgressiveWatermarks) {final long now = getProcessingTimeService().getCurrentProcessingTime();// 开启定时(200ms),执行onProcessingTime(long timestamp)方法getProcessingTimeService().registerTimer(now + watermarkInterval, this);}}// 由数据驱动的方法,只有数据进来时才执行这个方法@Overridepublic void processElement(final StreamRecord<T> element) throws Exception {final T event = element.getValue();final long previousTimestamp =element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE;final long newTimestamp = timestampAssigner.extractTimestamp(event, previousTimestamp);element.setTimestamp(newTimestamp);// 发送数据output.collect(element);// 根据BoundedOutOfOrdernessWatermarks类中的onEvent方法修改maxTimestampwatermarkGenerator.onEvent(event, newTimestamp, wmOutput);}// watermark定时触发器方法@Overridepublic void onProcessingTime(long timestamp) throws Exception {// 执行BoundedOutOfOrdernessWatermarks类中的onPeriodicEmit方法,发射watermarkwatermarkGenerator.onPeriodicEmit(wmOutput);final long now = getProcessingTimeService().getCurrentProcessingTime();// 重新定时,并在定时时间到达时再次触发onProcessingTime方法getProcessingTimeService().registerTimer(now + watermarkInterval, this);}// ...
    }
    
    • open

      open方法就是一个线程的生命周期方法,在这个方法里面会加载各种初始化信息,其中就包含了多久发送一次watermark的信息,就是watermarkInterval = getExecutionConfig().getAutoWatermarkInterval();这行代码,watermark的默认发送周期是200ms,这个是可以在代码中进行配置的如env.getConfig().setAutoWatermarkInterval(1000);这样就将默认的200ms修改成了1000ms,在获取到watermark发送周期后,就会启动定时器getProcessingTimeService().registerTimer(now + watermarkInterval, this)这里就第一次执行了定时器,其实就是执行的onProcessingTime方法.

    • processElement
      processElement方法只有在有数据流入的时候才会执行,这个方法中只需要关注两行代码即可output.collect(element);watermarkGenerator.onEvent(event, newTimestamp, wmOutput);在源码中可以看到,首先执行的就是output.collect(element);也就是发送数据,然后才执行的watermarkGenerator.onEvent(event, newTimestamp, wmOutput);也就是根据事件时间确定watermark,其实由这两行代码就可以看出Flink的机制其实就是数据优先,由数据驱动时间

    • onProcessingTime

      onProcessingTime方法就是watermark的定时触发器,代码中的内容也极其简单,首先就是调用BoundedOutOfOrdernessWatermarks类中的onPeriodicEmit方法,将watermark发射,然后再次通过getProcessingTimeService().registerTimer(now + watermarkInterval, this);执行定时器,可以看做类似于循环或者是递归,不断的启动定时器不断地发射watermark,这个跟是否有数据进来没有关系,这样watermark就会根据设定好的周期不断地进行发送,看到这里就说一点在watermark的源头数据本身和watermark就是异步执行的互不影响.

  4. 将源码内容拷贝到项目中,通过添加打印控制台的代码查看执行过程

    只看源码其实很难弄清楚过程到底是怎么执行的,到底是谁先谁后,这里就可以通过将源码拷贝到项目中并且根据自己的需求添加对应的代码的这种小技巧来搞清楚代码的具体执行逻辑,方法很简单首先在IDEA中查看对应的源码类,然后拷贝该类的全路径,在项目下新建一个一模一样的类即可,这样代码再执行的过程中就会优先调用项目中相同的类,这里以BoundedOutOfOrdernessWatermarksTimestampsAndWatermarksOperator为例.

    • BoundedOutOfOrdernessWatermarks

      右键BoundedOutOfOrdernessWatermarks --> Copy Reference --> 在项目的Java目录下右键 --> 新建Java Class --> 将全路径粘贴 --> 点击OK --> 再将源码中的代码全部粘贴到新建的类中即可 --> 根据自己需要添加代码

      代码

      package org.apache.flink.api.common.eventtime;import org.apache.flink.annotation.Public;import java.time.Duration;import static org.apache.flink.util.Preconditions.checkArgument;
      import static org.apache.flink.util.Preconditions.checkNotNull;
      public class BoundedOutOfOrdernessWatermarks<T> implements WatermarkGenerator<T> {// ...@Overridepublic void onEvent(T event, long eventTimestamp, WatermarkOutput output) {maxTimestamp = Math.max(maxTimestamp, eventTimestamp);// 在这里添加打印信息System.out.println(maxTimestamp);}@Overridepublic void onPeriodicEmit(WatermarkOutput output) {// 在这里添加打印信息System.out.printf("周期性输出watermark: %d \n", maxTimestamp - outOfOrdernessMillis - 1);output.emitWatermark(new Watermark(maxTimestamp - outOfOrdernessMillis - 1));}
      }
      

      代码中部分内容省略.

    • TimestampsAndWatermarksOperator

      步骤同上,这里省略

      代码

      package org.apache.flink.streaming.runtime.operators;
      // ...
      public class TimestampsAndWatermarksOperator<T> extends AbstractStreamOperator<T>implements OneInputStreamOperator<T, T>, ProcessingTimeCallback {// ...@Overridepublic void open() throws Exception {super.open();timestampAssigner = watermarkStrategy.createTimestampAssigner(this::getMetricGroup);watermarkGenerator =emitProgressiveWatermarks? watermarkStrategy.createWatermarkGenerator(this::getMetricGroup): new NoWatermarksGenerator<>();wmOutput = new WatermarkEmitter(output);watermarkInterval = getExecutionConfig().getAutoWatermarkInterval();if (watermarkInterval > 0 && emitProgressiveWatermarks) {final long now = getProcessingTimeService().getCurrentProcessingTime();// 添加打印信息System.out.println("第一次执行定时器");getProcessingTimeService().registerTimer(now + watermarkInterval, this);}}@Overridepublic void processElement(final StreamRecord<T> element) throws Exception {final T event = element.getValue();final long previousTimestamp =element.hasTimestamp() ? element.getTimestamp() : Long.MIN_VALUE;final long newTimestamp = timestampAssigner.extractTimestamp(event, previousTimestamp);element.setTimestamp(newTimestamp);// 添加打印信息System.out.printf("准备发送数据: %s \n", element);output.collect(element);// 添加打印信息System.out.print("根据onEvent方法修改了maxTimestamp: ");watermarkGenerator.onEvent(event, newTimestamp, wmOutput);}@Overridepublic void onProcessingTime(long timestamp) throws Exception {// 添加打印信息System.out.println("watermark定时器触发,开始调用onPeriodicEmit方法");watermarkGenerator.onPeriodicEmit(wmOutput);final long now = getProcessingTimeService().getCurrentProcessingTime();getProcessingTimeService().registerTimer(now + watermarkInterval, this);}// ...
      }
      
  5. 执行代码查看结果

    • 无数据进入

      [2023-10-09 09:19:57,219]-[INFO] -org.apache.flink.streaming.api.functions.source.SocketTextStreamFunction -2015 -org.apache.flink.streaming.api.functions.source.SocketTextStreamFunction.run(SocketTextStreamFunction.java:103).run(103) | Connecting to server socket localhost:8888
      第一次启动定时器
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: -9223372036854775808 
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: -9223372036854775808 
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: -9223372036854775808 
      watermark定时器触发,开始调用onPeriodicEmit方法
      

      通过结果可以看出open方法只有开始的时执行了一次,后面就是一直在执行onProcessingTime方法,不断的启动定时器,然后发送watermark,而且也可以看出就算没有数据到达watermark也是根据定时周期不断地发送,数据能影响的只是watermark的值,但是不会影响watermark的发送.

    • 第一条数据到达

      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: -9223372036854775808 
      准备发送数据: Record @ 1696648332245 : UserEvent2(uId=101, name=Tom, event=查看商品, time=2023-10-07 11:12:12.245) 
      根据onEvent方法修改了maxTimestamp: 1696648332245
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: 1696648330244 
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: 1696648330244 
      watermark定时器触发,开始调用onPeriodicEmit方法
      周期性输出watermark: 1696648330244 
      

      当第一条数据到达后可以看到,优先执行的数据处理的方法,当数据处理的方法执行完成后,才会变更对应的maxTimestamp,然后再通过定时触发器发射watermark.

1.2 watermark下游源码分析
  1. process算子中打上断点

            mapStream2.process(new ProcessFunction<UserEvent2, UserEvent2>() {@Overridepublic void processElement(UserEvent2 value, ProcessFunction<UserEvent2, UserEvent2>.Context ctx, Collector<UserEvent2> out) throws Exception {out.collect(value); // 断点位置}}).print();
    
  2. 通过Debug运行,以同样的方式找到代码执行过程用到的调用栈,如下图
    在这里插入图片描述
    找到AbstractStreamTaskNetworkInput类的processElement方法

  3. processElement方法体分析

    源码内容如下:

    public abstract class AbstractStreamTaskNetworkInput<T, R extends RecordDeserializer<DeserializationDelegate<StreamElement>>>implements StreamTaskInput<T> {// ..private void processElement(StreamElement recordOrMark, DataOutput<T> output) throws Exception {if (recordOrMark.isRecord()) { // 判断是否是用户数据output.emitRecord(recordOrMark.asRecord());} else if (recordOrMark.isWatermark()) { // 判断是否是水位线数据statusWatermarkValve.inputWatermark(recordOrMark.asWatermark(), flattenedChannelIndices.get(lastChannel), output);} else if (recordOrMark.isLatencyMarker()) { // 判断是否是标记性水位线数据(用以兼容以前的版本[Flink-1.12以前的版本])output.emitLatencyMarker(recordOrMark.asLatencyMarker());} else if (recordOrMark.isWatermarkStatus()) { // 判断是否是水位线状态数据statusWatermarkValve.inputWatermarkStatus(recordOrMark.asWatermarkStatus(),flattenedChannelIndices.get(lastChannel),output);} else {throw new 8 ("Unknown type of StreamElement");}}// ...
    }
    

    源码省略了部分代码,这里只看需要的主体代码, 在processElement方法体中可以看出watermark下游的算子当数据到达后首先会判断是什么类型的数据,然后做不同的处理,这里就和watermark的处理方式不同了,这里是指在一个方法体中做了一个if else判断,然后将数据发往不同的分支.

  4. 通过同样的方式,将源码拷贝到项目中,并以打印到控制台的方式修改代码

    代码:

    public abstract class AbstractStreamTaskNetworkInput<T, R extends RecordDeserializer<DeserializationDelegate<StreamElement>>>implements StreamTaskInput<T> {// ..private void processElement(StreamElement recordOrMark, DataOutput<T> output) throws Exception {if (recordOrMark.isRecord()) {// 打印控制台System.out.println("下游process算子开始发送数据: " + recordOrMark);output.emitRecord(recordOrMark.asRecord());} else if (recordOrMark.isWatermark()) {// 打印控制台System.out.println("下游process算子开始发送watermark: " + recordOrMark);statusWatermarkValve.inputWatermark(recordOrMark.asWatermark(), flattenedChannelIndices.get(lastChannel), output);} else if (recordOrMark.isLatencyMarker()) {output.emitLatencyMarker(recordOrMark.asLatencyMarker());} else if (recordOrMark.isWatermarkStatus()) {statusWatermarkValve.inputWatermarkStatus(recordOrMark.asWatermarkStatus(),flattenedChannelIndices.get(lastChannel),output);} else {throw new UnsupportedOperationException("Unknown type of StreamElement");}}// ...
    }
    

    结果:

    # watermark源头开始打印信息
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: -9223372036854775808 
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: -9223372036854775808# 这里不需要管重复的信息,重复只是因为方法被多次调用,从这里需要关注watermark和数据的先后关系
    下游process算子开始发送数据: Record @ (undef) : 102,Jack,添加购物车,2023-10-07 11:12:14.209
    下游process算子开始发送数据: Record @ (undef) : UserEvent2(uId=102, name=Jack, event=添加购物车, time=2023-10-07 11:12:14.209)
    准备发送数据: Record @ 1696648334209 : UserEvent2(uId=102, name=Jack, event=添加购物车, time=2023-10-07 11:12:14.209) # 这里将源头的watermark修改成了事件中的时间,要注意这里只是修改了maxTimestamp但是还没有更新watermark
    根据onEvent方法修改了maxTimestamp: 1696648334209
    # 开始发送数据
    下游process算子开始发送数据: Record @ 1696648334209 : UserEvent2(uId=102, name=Jack, event=添加购物车, time=2023-10-07 11:12:14.209)
    # 这里是在procees算子中打印的水位线信息,可以看到是第一条数据到达之前的watermark
    当前watermark: -9223372036854775808
    # print sink打印的数据
    UserEvent2(uId=102, name=Jack, event=添加购物车, time=2023-10-07 11:12:14.209)# 到这里就要注意,这个时候才更新watermark信息,这也就说明,一个问题随着数据达到process算子的watermark其实就是更新之前的watermark,只有当新的数据到达后,才会再次将watermark更新为前一条数据的中的事件时间
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: 1696648332208# 下游刚刚获取到新的watermark
    下游process算子开始发送watermark: Watermark @ 1696648332208# 在没有新数据到达时,process算子不会被触发,一直打印watermark源头的信息,虽然源头一直发送watermark,但是只有来数据时,process才会触发,而新数据到达时,proces触发拿到的其实就是上一条的watermark
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: 1696648332208 
    watermark定时器触发,开始调用onPeriodicEmit方法
    周期性输出watermark: 1696648332208
    

    具体信息在注释中写明,这里不做过多阐释.

1.3 watermark整体流程图

流程图的说明也是结合本章中的代码进行说明,如下图:
在这里插入图片描述

  1. 当程序刚启动时每个并行度都会执行open方法且只执行一次,这个时候的watermarkLong.MIN_VALUE + outOfOrdernessMillis + 1;Long类型的最小值 + 设置的容错时间(forBoundedOutOfOrderness(Duration.ofMillis(2000))) + 1,然后第一次启动定时器,定时器启动的周期则是通过读取配置获得,默认200ms.
  2. open执行后启动定时器,也就是onProcessingTime方法,在业务数据到达之前,定时器会根据watermark定时周期不断地自我触发,一直向下游发送watermark.
  3. 当第一条数据到达后首先会执行processElement方法,先将业务数据发送后再执行onEvent方法.
  4. onEvent方法获取maxTimestamp,就是从业务数据中获取事件时间和当前的watermark进行比较,保留最大值.
  5. 获取到maxTimestamp后,当定时触发器执行时onPeriodicEmit方法就会执行,这个时候就会用到maxTimestamp,再根据maxTimestamp - outOfOrdernessMillis - 1这个逻辑生成新的watermark.
  6. watermark生成后就会发往下游算子,这个还是根据watermark触发周期不断地发送,这个和数据是异步发送的,互不影响,只不过在发送watermark前,业务数据已经发往的下游.
  7. 下游的process算子只有接收到数据后才会执行,在没有数据到达之前是不会触发的,接收到数据后首先会判断数据类型
  8. 如果数据类型为业务数据则走output.emitRecord(recordOrMark.asRecord());方法,如果是watermark则走statusWatermarkValve.inputWatermark(...)方法,但是数据必定是先到达到达的,如果output.emitRecord(recordOrMark.asRecord())这一步阻塞了,就不会进行到接收watermark的判断,比如在process算子中通过Thread.sleep(...)就可以到达这个效果.
  9. 下游的process算子获取到的watermark并不是最新的,比如第一条数据到达后当前获取的watermark还是Long.MIN_VALUE + ...,只有下一条数据到达后才能获取到第一条数据中携带的事件时间.
  10. process处理完数据和watermark后会继续发往下游算子,以此类推.

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

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

相关文章

TensorFlow学习:在web前端如何使用Keras 模型

前言 在上篇文章 TensorFlow学习&#xff1a;使用官方模型进行图像分类、使用自己的数据对模型进行微调中我们学习了如何使用官方模型&#xff0c;以及使用自己的数据微调模型。 但是吧&#xff0c;代码一直是跑在Python里&#xff0c;而我本身是做前端开发的。我是很想让它在…

es elasticsearch 基础

es https://www.elastic.co/guide/en/elasticsearch/reference/6.8/getting-started.html 倒排索引&#xff1a; 正排–>从目录到文章内容 倒排–>内容到目录文章标题 if we dont need full text search ,we dont need to create 倒排 using text type 分词&#xff1a…

C语言-贪吃蛇 1.输入控制ncurse

一、为什么要用nurse C语言中的gets()、scanf()、getchar()等函数是在用户输入后需要按下Enter键才能执行代码&#xff0c;而贪吃蛇要求按下按键后立即对蛇的方向进行操作&#xff0c;所以根据贪吃蛇功能的需求引入ncurse&#xff0c;让用户输入后就能让蛇进行对应的行动。 二、…

10.本项目的简单介绍及所用工具

本项目的简单介绍及所用工具 本项目的层次架构适合初学者或者有一些基础的同学&#xff0c;项目整体简单明了&#xff0c;有着非常严谨的逻辑思维&#xff0c;并且在前面文章中也讲了一些项目中所需要的软件安装配置以及一些前置的在本项目中所需要的java基础知识。 项目名称 …

8路高速光栅尺磁栅尺编码器4倍频计数转Modbus TCP网络模块 YL99-RJ45

特点&#xff1a; ● 光栅尺磁栅尺解码转换成标准Modbus TCP协议 ● 高速光栅尺磁栅尺4倍频计数&#xff0c;频率可达5MHz ● 模块可以输出5V的电源给光栅尺或传感器供电 ● 支持8个光栅尺同时计数&#xff0c;可识别正反转 ● 可以设置作为16路独立DI高速计数器 ● 可网…

智慧办公数据可视化大屏设计(数据可视化)、大数据、数据大屏、办公数据大屏、办公数据

本次分享的作品是用软件Axure8.0&#xff08;兼容9和10&#xff09;制作的智慧办公数据进行的可视化大屏设计&#xff0c;主要是针对办公的综合数据、工位数据、会议室数据、访客数据、能耗数据以及设备智控数据进行可视化数据分析。 1、综合分析:对办公室的整体数据、空气质量…

JVM第一讲:JVM相关知识体系详解+面试(P6熟练 P7精通)

JVM相关知识体系详解面试(P6熟练 P7精通) 面试时常常被面试官问到JVM相关的问题。本系列将给大家构建JVM核心知识点全局知识体系&#xff0c;本文是JVM第一讲&#xff0c;JVM相关知识体系详解和相关面试题梳理。 文章目录 JVM相关知识体系详解面试(P6熟练 P7精通)1、JVM学习建议…

设计模式 - 行为型考点模式:责任链模式(概述 | 案例实现 | 优缺点 | 使用场景)

目录 一、行为型模式 1.1、责任链模式 1.1.1、概述 1.1.2、案例实现 1.1.3、优缺点 1.1.4、使用场景 一、行为型模式 1.1、责任链模式 1.1.1、概述 为了避免请求发送者和多个请求处理者耦合在一起&#xff0c;就将所有请求处理者通过前一个对象记住下一个对象的引用的方…

联邦学习综述二

联邦学习漫画 联邦学习漫画链接: https://federated.withgoogle.com/ Federated Analytics: Collaborative Data Science without Data Collection 博客链接: https://blog.research.google/2020/05/federated-analytics-collaborative-data.html 本篇博客介绍了联邦分析&a…

Filebeat、metricbeat、kafka

kibana机子上安装filebeat&#xff08;因为有nginx&#xff09; 上传filebrat包 在es概览查看 上传metricbeat 排错 tailf !$ Kibana-Vlsualize查看 kafka(卡夫卡) kafka是一个分布式的消息发布—订阅系统&#xff08;kafka其实是消息队列&#xff09;消息队列中间件 Kafka的…

Pycharm远程debug代码,一直进入remote_sources

最近debug发现代码一直跳转到 AppData\Local\JetBrains\PyCharm2022.2\remote_sources\xxx这样的目录下&#xff0c;查找百度也没有找到解决的方法。 最后发现&#xff0c;在Run的配置这&#xff0c;有一个Path mappings是空的&#xff0c;把这里的映射填成本地项目和远程项目…

HDR-ISP_unpack_depwl_01_20231002

https://github.com/JokerEyeAdas/HDR-ISP/tree/main 1.unpack&#xff1a;解析raw图 (1)unpack&#xff1a;2个字节1个像素 (2)mipi10&#xff1a;5个字节4个像素 [p1 9:2][p2 9:2][p3 9:2][p4 9:2][(p1 1:0)(p2 1:0)(p3 1:0)(p4 1:0)] (3)mipi12&#xff1a;3个字节2个像…

vscode安装svn扩展(windows)

一、安装 1.1 环境说明 操作系统 windows 10 1.2 安装过程 1. 安装svn 双击安装程序 点击next 继续next 继续next 点击Install 在弹出框中点击 是 开始安装进度&#xff0c;一会将安装成功 安装结束 右键菜单栏中已经有svn选项&#xff0c;并且能正常拉取以…

vue3+elementui实现表格样式可配置

后端接口传回的数据格式如下图 需要依靠后端传回的数据控制表格样式 实现代码 <!-- 可视化配置-表格 --> <template><div class"tabulation_main" ref"myDiv"><!-- 尝试过在mounted中使用this.$refs.myDiv.offsetHeight,获取父元素…

[NPUCTF2020]ezinclude 文件包含两大 getshell方式

PHP LFI 利用临时文件 Getshell 姿势 | 码农家园 说一下我的思路吧 robots没有 扫描发现存在 dir.php 然后404.html 报错 apache 2.18 ubuntu 这个又正好存在漏洞 所以前面全去看这个了 结果根本不是这样做。。。 正确的思路是这样 发现变量 认为是 name和 pass传递参数…

Jetson Orin NX 开发指南(6): VINS-Fusion-gpu 的编译和运行

一、前言 由于 Jetson 系列的开发板 CPU 性能不是很好&#xff0c;因此在处理图像数据时往往需要 GPU 加速&#xff0c;而 VINS-Fusion 是针对同步定位与建图&#xff08;SLAM&#xff09;问题中十分出色的视觉算法&#xff0c;但是其在图像处理过程中资源消耗较大&#xff0c…

HTML 笔记:初识 HTML(HTML文本标签、文本列表、嵌入图片、背景色、网页链接)

1 何为HTML 用来描述网页的一种语言超文本标记语言(Hyper Text Markup Language)不是一种编程语言&#xff0c;而是一种标记语言 (markup language) 2 HTML标签 HTML 标签是由尖括号包围的关键词&#xff0c;比如 <html> 作用是为了“标记”页面中的内容&#xff0c;使…

抖音手机实景无人直播间怎么搭建?

手机无人直播已成为用户直播和商家直播带货的一项热门技术趋势&#xff0c;为消费者提供了全新的观看体验。无人直播&#xff0c;顾名思义&#xff0c;即通过无人直播软件或数字人来进行无人直播。这一技术的广泛应用&#xff0c;不仅为短视频渠道带来了更丰富的玩法&#xff0…

【排序算法】选择排序

文章目录 一&#xff1a;基本介绍1.1 概念1.2 算法思想1.3 思路分析图1.4 思路分析1.5 总结1.5.1 选择排序一共有数组大小-1轮排序1.5.2 每一轮排序&#xff0c;又是一个循环&#xff0c;循环的规则如下&#xff08;在代码中实现&#xff09;&#xff1a; 二&#xff1a;代码实…

【ComfyUI】MacBook Pro 安装(Intel 集成显卡)

文章目录 环境概述配置pip镜像配置pip代理git配置&#xff08;选配&#xff09;下载comfyUI代码创建、激活虚拟环境下载依赖安装torchvision启动comfyUI为什么Mac不支持CUDA&#xff0c;即英伟达的显卡&#xff1f;安装Intel工具包 环境 显卡&#xff1a;Intel Iris Plus Grap…