阅读 WiscKey 论文时随手记录一些笔记。

这篇论文的核心思想理解起来还是很简单的,但是具体涉及到实现还有一些想不明白的地方,后来看到 TiKV 的 Titan 实现也很有趣,索性把这些问题都记录下来并抛出来。

本文中和论文相关的内容,斜体均为我个人的主观想法,关于 Titan 的实现,我只看过几篇公开文章以及粗浅的扫过一遍代码,如果这两部分的内容有理解错误欢迎指出,感谢!

背景

基于 LSM 树(Log-Structured Merge-Trees)的键值存储已经广泛应用,其特点是保持了数据的顺序写入和存储,利用磁盘的顺序 IO 得到了很高的性能(在 HDD 上尤其显著)。但是同一份数据会在生命周期中写入多次,随之带来高额的写放大。

以 LevelDB 为例,数据写入的整个流程为:

  1. 数据首先会被写入 memtable 和 WAL
  2. 当 memtable 达到上限后,会转换为 immutable memtable,之后持久化到 L0(称为 flush),L0 中每个文件都是一个持久化的 immutable memtable,多个文件间可以有相互重叠的 Key
  3. 当 L0 中的文件达到一定数量时,便会和 L1 中的文件进行合并(称为 compaction)
  4. 自 L1 开始所有文件都不会再有相互重叠的 Key,并且每个文件都会按照 Key 的顺序存储。每一层通常是上一层大小的 10 倍,当一层的大小超过限制时,就会挑选一部分文件合并到下一层

由此可以计算出 LevelDB 的写放大比率:

  1. 由于每一层是上一层大小的 10 倍,所以在最坏情况下,上一层的一个文件可能需要和下一层的十个文件进行合并,所以合并的写放大是 10
  2. 假设每行数据经过一系列 compaction 最终都会落入最终层,每层都需要重新写一次,那么从 L1 到 L6 的写放大为 5
  3. 加上 WAL 和 L0,最终写放大可能会超过 50

另一方面,由于数据在 LevelDB 中的每一层(memtable/L0/L1~L6)都有可能存在,所以对于读请求,也会有一定的读放大:

  1. 由于 L0 的多个文件允许有数据重叠,所以最坏情况需要查询所有文件
  2. 对于 L1 到 L6,因为数据有序且不重叠,所以每层需要查询一个文件
  3. 为了确认 Key 是否存在,对于每个文件都需要读取 index block、bloom-filter blocks 和 data block

论文中提供了一个实际的数据:

WiscKey 介绍

设计目标

WiscKey 的核心思想是将数据中的 Key 和 Value 分离,只在 LSM-Tree 中有序存储 Key,而将 Value 存放在单独的 Log 中。这样带来了两点好处:

  1. 当 LSM-Tree 进行 compaction 时,只会对 Key 进行排序和重写,不会影响到没有改变的 Value,也就显著降低了写放大
  2. 将 Value 分离后,LSM-Tree 本身会大幅减小,所以对应磁盘中的层级会更少,可以减少查询时从磁盘读取的次数,并且可以更好的利用缓存的效果

另外,WiscKey 的设计很大一部分还建立在 SSD 的普及上,相比 HDD,SSD 有一些变化:

  1. SSD 的随机 IO 和顺序 IO 的性能差距并不像 HDD 那么大,所以 LSM-Tree 为了避免随机 IO 而采用了大量的顺序 IO,反而可能会造成了带宽浪费
  2. SSD 拥有内部并行性,但 LSM-Tree 并没有利用到该特性
  3. SSD 会因为大量的重复写入而产生硬件损耗,LSM-Tree 的高写入放大率会降低设备的寿命

下图展示了在不同请求大小和并发度时,随机读和顺序读的吞吐量,可以看到在请求大于 16KB 时,32 线程的随机读已经接近了顺序读的吞吐:

实现

在 LSM-Tree 的基础上,WiscKey 引入了一个额外的存储用于存储分离出的值,称为 Value Log。整体的读写路径为:

  1. 当用户添加一个 KV 时,WiscKey 会先将 Value 写入到 Value-Log 中(顺序写),然后将 Key 和 Value 在 Value Log 中的地址写入 LSM-Tree
  2. 当用户删除一个 Key 时,仅在 LSM-Tree 中删除 Key 的记录,之后通过 GC 清理掉 Value Log 中的数据
  3. 当用户查询一个 Key 时,会先从 LSM-Tree 中查询到 Value 的地址,再根据地址将 Value 真正从 Value-Log 中读取出来(随机读)

假设 Key 的大小为 16 Bytes,Value 的大小为 1KB,优化后的效果为:

  1. 如果在 LSM-Tree 中的单层的写放大率是 10,那么使用 WiscKey 后单层的写放大率将变为 ((16 x 10) + (1024 x 1)) / (16 + 1024) = 1.14,远小于之前的 10 倍
  2. 如果一个标准的 LSM-Tree 的大小为 100G,那么将 Value 分离后 LSM-Tree 本身的大小将会减少到 2G,层级会减少 1~2 级,并且缓存到内存中的比例会更高,从而降低读放大

看上去实现很简单,效果也很好,但是背后也存在了一些挑战和优化。

挑战一:范围查询

在标准的 LSM-Tree 中,由于 Key 和 Value 是按照顺序存储在一起的,所以范围查询只需要顺序读即可遍历整个 SSTable 的所有数据。但是在 WiscKey 中,每个 Key 都需要额外的一次随机读才能读取到对应的 Value,因此效率会很差。

论文中的解决方案是利用上文中所提到的 SSD 内部的并行能力。WiscKey 内部会有一个 32 线程的线程池,当用户使用迭代器迭代一行时,迭代器会预先取出多个 Key,并放入到一个队列中,线程池会从队列中读取 Key 并行的查找对应的 Value。

疑问:

  1. 预取在某些场景是否会有浪费?(用户不准备迭代完所有数据的场景,例如 Limit 或是 Filter)
  2. 为什么用线程池 + 队列,而不是直接用异步 IO?

挑战二:垃圾收集(GC)

上文中提到了,当用户删除一个 Key 时,WiscKey 只会将 LSM-Tree 中的 Key 删除掉,所以需要一个额外的方式清理 Value-Log 中的值。

最简单的方法是定期扫描整个 LSM-Tree,获得所有还有引用的 Value 地址,然后将没有引用的 Value 删除,但是这个逻辑非常重。

论文中介绍的方式是通过维护一个 Value Log 的有效区间(由 head 和 tail 两个地址组成),通过不断地搬运有效数据来达到淘汰无效数据。整个流程为:

  1. 对于 Value-Log 中的每个值,需要额外存储 Key,为了方便从 LSM-Tree 中进行反查(相对 Value,Key 会比较小,所以写入放大不会增加太多)
  2. 从 tail 的位置读取 KV,通过 Key 在 LSM-Tree 中查询 Value 是否还在被引用
  3. 如果 Value 还在被引用,则将 Value 写入到 head,并将新的 Value 地址写回 LSM-Tree 中
  4. 如果 Value 已经没有被引用,则跳过这行数据,接着读取下一个 KV
  5. 当已经确认数据写入 head 之后,就可以将 tail 之后的数据都删除掉了

因为需要重新写入一次 Value,并且需要将 Key 回填到 LSM-Tree 中,所以这个 GC 策略会造成额外的写放大。并且即使不做 GC,也只会影响到空间放大(删除的数据没有真正清理),所以感觉可以配置一些策略:

  1. 根据磁盘负载和 LSM-Tree 的负载计算,仅在低峰期执行
  2. 计算每一段数据中被删除的比例有多少,当空洞变得比较大的时候才触发 GC

挑战三:崩溃一致性

当系统崩溃时,LSM-Tree 可以保证数据写入的原子性和恢复的有序性,所以 WiscKey 也需要保证这两点。

WiscKey 通过查询时的容错机制保证 Key 和 Value 的原子性:

  1. 当用户查询时,如果在 LSM-Tree 中找不到 Key,则返回 Key 不存在
  2. 如果在 LSM-Tree 中可以找到 Key,但是通过地址在 Value-Log 中无法找到匹配的 Value,则说明 Value 在写入时丢失了,同样返回不存在

这个前提建立在于 WiscKey 通过一个 Write Buffer 批量提交 Value Log(下面有详细介绍),所以才会出现 Key 写入成功后 Value 丢失的场景,用户也可以通过设置同步写入,这样在刷新 Value Log 之后,才会将 Key 写入 LSM-Tree 中。

另外,WiscKey 通过现代的文件系统的特性保证了写入的有序性,即写入一个字节序列 b1, b2, b3…bn,如果 b3 在写入时丢失了,那么 b3 之后的所有值也一定会丢失。

优化一:Write Buffer

为了提高写入效率,WiscKey 首先会将 Value 写入到 Write Buffer 中,等待 Write Buffer 达到一定大小再一起刷新到文件中。所以查询时首先也要先从 WriteBuffer 中查询。当崩溃时,Write Buffer 中的数据会丢失,此时的行为就是上文中的崩溃一致性。

疑问:

  1. 根据这个描述,Value-Log 似乎是异步写入?结合上文中崩溃一致性的介绍,会有给用户返回成功但是数据丢失的情况?

优化二:WAL 优化

LSM-Tree 通过 WAL 保证了在系统崩溃时 memtable 中的数据可恢复,但是也带来了额外的一倍写放大。

而在 WiscKey 中,Value-Log 和 WAL 都是基于用户的写入顺序进行存储的,并且也具备了恢复数据的所有内容(前提是基于上文中的 GC 实现,Value Log 里存有 Key),所以理论上 Value-Log 是可以同时作为 WAL 的,从而减少 WAL 的写放大。

由于 Value Log 的 GC 比 WAL 更加低频,并且包含了大量已经持久化的数据,直接通过 Value-Log 进行恢复的话可能会导致回放大量已经持久化到 SST 的数据。所以 WiscKey 会定期将已经持久化到 SST 的 head 写入到 LSM-Tree 中,这样当恢复时只需要从最新持久化的 head 开始恢复即可。

疑问:

  1. Delete 操作只需要写 LSM-Tree,但如果需要 Value Log 作为 WAL,则 Delete 也需要写入到 Value Log 中
  2. 如果不应用这个优化,则可以做到只将大 Value 分离出 LSM-Tree,应用此优化后,小 Value 也必须要额外存到 Value Log 中了
  3. 与其说是用 Value Log 替代 WAL,不如说是让 WAL 支持读 Value…

效果

说完实现再看看效果,论文中有 db_bench 和 YCSB 的数据,为了节约篇幅,只贴一部分 db_bench 的数据。

db_bench 的场景分两种,一种是所有 Key 按顺序写入(这样写放大会更低,数据在每一层会更紧凑),另一种是随机写入(写放大更高,数据在每一层分布更均匀)。

顺序写入

效果应该来自两部分:

  1. WAL 没了直接省了一倍写放大
  2. 顺序写入,每一层合并可以认为没有写放大,但是数据依旧要在每一层写一次,100 G 可能是 4~5 次(对应 L5 的大小)

随机写入

效果对比顺序写入,如果说为什么差距会这么大,只有可能是每一层合并造成的写放大了。

点查

  1. 当 Value 比较小时,WiscKey 的劣势在于额外的一次随机读,而 LevelDB 的劣势在于读放大。当 Value 变得更大时,基于 SSD 内部的并行能力,随机读依旧能读满带宽,但是 LevelDB 读放大造成的带宽浪费却没有改善。
  2. 另外这个测试场景是数据库大小为 100G,对于 LevelDB 来说,层级和 KV 大小挂钩,对于 WiscKey 来说,层级和 Key 大小挂钩,所以当 Value 越大,WiscKey 中的 LSM-Tree 反而更小,层级也就更低,甚至可能仅在内存中 (例如 Value 为 256KB 时,Key 加起来才 100G / (16 + 256 1024) 16 ~= 610KB)

这个有点看不懂…:

  1. 在数据集是顺序写的场景下,LevelDB 的性能随着 Value 的增大反而降低了,这个不太理解原因(理论上读放大不会很大,而且是顺序读,很容易就能读满带宽),WiscKey 因为是随机读,并且有上文中提到的 LSM-Tree 本身很小,随着 Value 变大性能越高是符合预期的
  2. 在数据集是随机写的场景下,一开始 WiscKey 性能低是因为随机读的延迟,随着 Value 增大,优势应该和点查类似

GC

上文提到了 GC 会重写 Value 以及写回 LSM-Tree,造成额外的写入。当空余空间的占比越高时(大部分数据都已经被删了),回写的数据越少,对性能的影响也就越小。

Titan 的实现

BlobDB 和 Badger 的实现都和论文比较接近,并且也都是玩具。反而 TiKV 的 Titan 有一些独特的设计可以学习和讨论,所以下面只介绍这一案例。

核心实现

和 WiscKey 的主要区别在于:Titan 在 flush/compaction 时才开始分离键值,并且用于存储分离后 Value 的文件(BlobFile)会按照 Key 的顺序存储,而不是写入的顺序(其实在这个阶段,已经没有写入顺序了)。

因此导致实现上的差异有:

  1. 范围查询:由于 Value-Log 没有按照 Key 排序,所以 WiscKey 需要将一个范围查询拆解为多个随机读。而 Titan 保证了局部有序,在单个 BlobFile 内部可以顺序读,但是会有多个 BlobFile 的范围有重叠,需要额外做归并。另外对于预取策略,WiscKey 建立在 SSD 并行的优势上,可以靠增加并发预取增加吞吐,而 Titan 暂时没有如此激进的预取策略
  2. WAL 优化:在 WiscKey 的实现中通过 Value Log 替代 WAL 减少了一倍写放大,而 Titan 在 flush/compaction 时才进行键值分离,肯定是没办法做这个优化的,不过这一点在 Titan 的设计文档里也提到了:「假设 LSM-tree 的 max level 是 5,放大因子为 10,则 LSM-tree 总的写放大大概为 1 + 1 + 10 + 10 + 10 + 10,其中 Flush 的写放大是 1,其比值是 42 : 1,因此 Flush 的写放大相比于整个 LSM-tree 的写放大可以忽略不计。」,个人觉得还是还比较信服的
  3. GC 策略:Titan 目前有两个版本的 GC 策略,会在下面详细介绍

GC 策略

第一种策略(传统 GC):

  1. 首先挑选一些需要合并的 BlobFile,在 flush/compaction 时可以统计出每个 BlobFile 中已经删除的数据大小,从而挑选出空洞较大的文件
  2. 迭代这些 BlobFile,通过 Key 查询 LSM-Tree,判断 Value 是否还在被引用
  3. 如果 Value 还在被引用就写到新的 BlobFile 中,并把更新后的地址回填到 LSM-Tree 中

这个实现和论文中的 GC 方案类似,只不过论文为了 WAL 需要写入一条完整的 Value Log,所以需要维护 head 和 tail。Titan 的实现只需要每次都生成新的 BlobFile 即可。

不同点在于:WiscKey 是随机读,Value Log 的大小不会影响到读 Value 的成本。GC 策略在于写放大和空间放大的权衡,所以 GC 可以更加低频。而 BlobFile 是顺序读,如果 BlobFile 中的无效数据太多,会影响到预取的效率,间接也会影响到读的性能。

第二种策略(Level-Merge):

  1. 不存在单独的 GC,由 LSM-Tree 的 compaction 触发
  2. compaction 时,如果遍历到的值已经是一个 BlobIndex(代表值已经写入了某个 BlobFile),依旧将其读出来重新写入新的 BlobFile,也就是说每次 compaction 都会生成一批与新的 SST 完全对应的 BlobFile
  3. 目前 Level Merge 只在最后两层开启

开启 Level Merge 后相当于 GC 频率和 compaction 频率持平了(GC 频率最多也只能和 compaction 持平),并且在这个基础上,直接在 compaction 里做 GC,可以减少一次回写 LSM-Tree 的成本(因为在 compaction 的过程中就能将老的 Value 地址替换掉)。

这种策略的优点在于 BlobFile 中不再有无效数据,可以用更加激进的预取策略提高范围查询的性能,缺点是写放大肯定会比之前更大(个人觉得开启后,写放大就和标准 LSM-Tree 完全一样了吧(一次 compaction 需要合并的 Key 和 Value 都需要重写一遍)?),所以只在最后两层开启。

效果

Titan 的性能测试结果摘自官网的文章,大部分结论都和 WiscKey 类似,并且文章中也分析了原因,就不在此赘述了。

因为文章是 19 年初的,所以还没有上文中的 Level Merge GC,不过 GC 策略理论上只影响范围查询的性能,所以在此贴一下范围查询的性能:

在实现 Level Merge GC 的策略之前,Titan 的范围查询只有 RocksDB 的 40%,主要原因应该还是分离后需要额外读一次 Value,以及没办法并行预取增加吞吐。 这点文章最后也提到了:

我们通过测试发现,目前使用 Titan 做范围查询时 IO Util 很低,这也是为什么其性能会比 RocksDB 差的重要原因之一。因此我们认为 Titan 的 Iterator 还存在着巨大的优化空间,最简单的方法是可以通过更加激进的 prefetch 和并行 prefetch 等手段来达到提升 Iterator 性能的目的。

另外在 TiDB in Action 也提到了 Level Merge GC 可以「大幅提升 Titan 的范围查询性能」,不知道除了完全去掉无效数据之外,是否还有其他的优化,还需要再看下代码。

感受

个人认为 WiscKey 的核心思想还是比较有意义的,毕竟适用的场景很典型而且还比较常见:大 Value、写多读少、点查多范围查询少,只要业务场景命中一个特点,效果应该就会非常显著了。

对于论文中的具体实现是否能套用在一个真实的工业实现中,我觉得大部分实现还是简单有效的,但是也有一些设计个人不太喜欢,例如使用 Value Log 替代 WAL 的方案,感觉有些过于追求减少写放大了,可能反而会引入其他问题,以及默认的 GC 策略还要写回 LSM-Tree 也有些别扭。

在和其他同事讨论内部项目的实现时,也畅想过一些其他玩法,例如只将 Value 中的一部分分离出来单独存储,或是一个分布式的 WAL 是否也能转换为 Value Log,会有哪些问题。包括看到 Titan 的实现时,我也很好奇设计成 BlobFile 这种顺序读的方式是否有什么深意(毕竟论文都把利用 SSD 写到标题里了),或者只是因为从 compaction 才开始分离键值最简单的做法就是按顺序存储 KV。

总之,期待将来能有更多工业实现落地,看到更多有趣的案例。

参考资料

  • WiscKey: Separating Keys from Values in SSD-conscious Storage
  • https://nan01ab.github.io/2018/07/WiscKey.html
  • Titan 的设计与实现
  • Titan 原理介绍

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

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

相关文章

week1-[循环嵌套]画正方形

week1-[循环嵌套]画正方形 题目描述 输入一个正整数 nnn,请使用数字 000 到 999 拼成一个这样的正方形图案(参考样例输入输出):由上至下、由左至右依次由数字 000 到 999 填充。每次使用数字 999 填充后,将从头使用数字…

在 Vue2 中使用 pdf.js + pdf-lib 实现 PDF 预览、手写签名、文字批注与高保真导出

本文演示如何在前端(Vue.js)中结合 pdf.js、pdf-lib 与 Canvas 技术实现 PDF 预览、图片签名、手写批注、文字标注,并导出高保真 PDF。 先上demo截图,后续会附上代码仓库地址(目前还有部分问题暂未进行优化&#xff0…

tomcat 定时重启

tomcat 定时重启 定时重启的目的是:修复内存泄漏等问题,tomcat 长时间未重启,导致页面卡顿,卡死,无法访问,影响用户访问 1.编写脚本 su - tomcat [tomcat@u1abomap02 ~]$ ls restart_tomcat_gosi.sh tomcat_gosi.log vi restart_tomcat_gosi.sh #!/bin/bash# 定义日志目…

WinForm 简单用户登录记录器实现教程

目录 功能概述 实现思路 一、程序入口(Program.cs) 二、登录用户控件(Login.cs) 2.1 控件初始化与密码显示逻辑 2.2 登录控件设计器(Login.Designer.cs) 三、主窗体(Form1.cs&#xff09…

docker 安装 使用

Docker安装 一键安装命令 sudo curl -fsSL https://get.docker.com| bash -s docker --mirror Aliyun启动docker sudo service docker startpull镜像加速配置 sudo vi /etc/docker/daemon.json输入下列内容,最后按ESC,输入 :wq! 保存退出。 {"regis…

无人机探测器技术解析

一、工作模式 无人机探测器通过多模式协同实现全流程防御闭环: 1. 主动扫描模式 雷达主动探测:发射电磁波(如Ka/Ku波段),通过回波时差与多普勒频移计算目标距离、速度及航向,适用于广域扫描(…

Linux学习-软件编程(进程与线程)

进程回收wait原型:pid_t wait(int *wstatus); 功能:回收子进程空间 参数:wstatus:存放子进程结束状态空间的首地址 返回值:成功返回回收到的子进程的PID失败返回-1WIFEXITED(wstatus):测试进程是否正常结束…

大模型微调分布式训练-大模型压缩训练(知识蒸馏)-大模型推理部署(分布式推理与量化部署)-大模型评估测试(OpenCompass)

大模型微调分布式训练 LLama Factory与Xtuner分布式微调大模型 大模型分布式微调训练的基本概念 为什么需要分布式训练? 模型规模爆炸:现代大模型(如GPT-3、LLaMA等)参数量达千亿级别,单卡GPU无法存储完整模型。 …

物联网、大数据与云计算持续发展,楼宇自控系统应用日益广泛

在深圳某智慧园区的控制中心,管理人员通过云端平台实时监控着5公里外园区内每台空调的运行参数、每盏路灯的开关状态和每个区域的能耗数据。当系统检测到某栋楼宇的电梯运行振动异常时,大数据算法自动预判可能的故障点并推送维修建议;物联网传…

在实验室连接地下车库工控机及其数据采集设备

在实验室连接地下车库工控机及其数据采集设备 我们小组为项目的数据采集组,目前在车顶集成了一个工控机、两个激光雷达、两个摄像头、一个户外电源 由于地下车库蚊子太多了,我们可受不了这个苦,所以想坐在实验室吹着空调就能连接工控机来修改…

icmpsh、PingTunnel--安装、使用

用途限制声明,本文仅用于网络安全技术研究、教育与知识分享。文中涉及的渗透测试方法与工具,严禁用于未经授权的网络攻击、数据窃取或任何违法活动。任何因不当使用本文内容导致的法律后果,作者及发布平台不承担任何责任。渗透测试涉及复杂技…

系统思考:情绪内耗与思维模式

我们正在努力解决的问题,很多时候,根源就在我们自己。 在日常的工作和生活中,我们常常感到焦虑、内耗和失控。这些情绪和状态,似乎总是在不断循环。但如果停下来仔细思考,会发现,问题的背后,并不…

详解grafana k6 中stage的核心概念与作用

在Grafana k6中,​​Stage(阶段)​​ 是负载测试脚本的核心配置概念,用于动态控制虚拟用户(VUs)的数量随时间的变化。通过定义多个阶段,用户可以模拟真实场景中的流量波动(如用户逐步…

JS 和 JSX 的区别

JS 和 JSX 是两种不同的概念,尽管它们都与 JavaScript 密切相关,尤其是在 React 开发中。以下是它们的主要区别:1. 定义JS (JavaScript): 一种通用的编程语言,用于开发动态网页、服务器端应用程序等。它是标准的 ECMAScript 语言。…

Linux软件编程-进程(2)及线程(1)

1.进程回收资源空间&#xff08;1&#xff09;wait函数头文件&#xff1a;#include <sys/types.h>#include <sys/wait.h>函数接口&#xff1a;pid_t wait(int *wstatus);功能&#xff1a;阻塞等待回收子进程的资源空间参数&#xff1a;wstatus &#xff1a;保存子进…

java 集合 之 集合工具类Collections

前言早期开发者经常需要对集合进行各种操作比如排序、查找最大最小值等等但是当时没有统一的工具类来处理所以导致代码重复且容易出错java.util.Collections 工具类的引入为开发者提供了大量 静态方法 来操作集合它就像一个经验丰富的助手和数组工具类 Arrays 一样避免了我们重…

2025 年电赛 C 题 发挥部分 1:多正方形 / 重叠正方形高精度识别与最小边长测量

2025 年全国大学生电子设计竞赛 C 题 发挥部分 1&#xff1a;多正方形 / 重叠正方形高精度识别与最小边长测量 香橙派 OpenCV C 全流程解析 目录 赛题背景与需求技术难点全景图系统总体架构硬件平台与接线软件架构与线程模型算法流水线逐帧拆解 6.1 图像预处理6.2 轮廓提取与…

【自动驾驶】自动驾驶概述 ② ( 自动驾驶技术路径 | L0 ~ L5 级别自动驾驶 )

文章目录一、自动驾驶技术路径1、L0 级别 自动驾驶2、L1 级别 自动驾驶3、L2 级别 自动驾驶4、L3 级别 自动驾驶5、L4 级别 自动驾驶6、L5 级别 自动驾驶一、自动驾驶技术路径 美国汽车工程师学会 ( SAE ) 将 自动驾驶 分为 L0 ~ L5 六个级别 : 其中 L0 级别 是 完全手动 , L5…

C++少儿编程(二十二)—条件结构

1.理解条件结构小朋友们&#xff0c;今天让我们一起来探索一个神奇而有趣的知识——程序的条件结构&#xff01;首先&#xff0c;让我们来想象一个有趣的场景。比如说&#xff0c;你们正在准备去公园玩耍。在出发之前&#xff0c;你们会看看天气怎么样。如果天气晴朗&#xff0…

Ubuntu20.04下Px4使用UORB发布消息

1 .msg文件夹定义数据类型及 变量名文件位置如图&#xff0c;在PX4-Autopilot/msg文件夹下&#xff0c;笔者创建的文件名为gps_msg.msggps_msg.msg内容如下 uint64 timestamp # 时间戳 float32 latitude float32 longitude float32 altitude 同时&#xff0c;在CM…