news 2026/9/13 7:19:20

Vector File Source 重构深度解析:从 RFC 3480 看 file source 的设计演进与实现落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vector File Source 重构深度解析:从 RFC 3480 看 file source 的设计演进与实现落地

Vector File Source 重构深度解析:从 RFC 3480 看 file source 的设计演进与实现落地

【免费下载链接】vectorA high-performance observability data pipeline.项目地址: https://gitcode.com/GitHub_Trending/vect/vector

导读

本文以 Vector 仓库中的设计文档 RFC 3480 - File Source Rework 为主体,深入剖析 Vector 最古老、使用最广泛的组件之一——filesource 的内部结构、历史痛点与重构方案。你将理解其"读取调度、文件身份识别、文件起始位置"三大核心关注点如何被拆分为可独立演进、配置和扩展的子组件,并通过当前仓库的源码(src/sources/file.rs、lib/file-source、lib/file-source-common)验证这些设计如何落地为真实的FingerprinterFileWatcherCheckpointer等模块。读完本文,你将掌握 file source 的完整配置模型、指纹识别策略的原理与取舍,以及批处理(batch)、顺序读取(tail_sequential)等模式的配置方法。

背景:为什么需要对 file source 动一次"手术"

filesource 诞生于 Vector 早期,接口和实现最初都相对简单。但随着时间推移,它积累了大量配置项、历史包袱和通用问题,导致该组件无论是用户使用还是开发者维护都日益吃力。RFC 3480 的核心判断是:与其推倒重写,不如在保留既有正确行为(历次 bug 修复累积的经验)的前提下,对内部结构做一次整体性重构(general overhaul),把用户关心的行为拆解为尽可能正交(orthogonal)的能力,让每个问题都能被独立、低成本地解决。

该 RFC 的 Scope 明确了两点:

  • 聚焦filesource 的内部实现用户侧配置的整体检修;
  • 重点是迁移到更好的结构,而非从零重写。

从 issue 列表中提炼的痛点清单

RFC 中收集了当时仓库中标记为source: file的数页 issue,并按类别归纳为以下几组核心问题:

问题类别代表 issue 与描述
大量文件导致的性能下降#763、#1466、#4434:文件源在磁盘上保留大量文件、EBS 上数百万文件时变慢
校验和(Checksum)令人困惑#828、#1065:校验和对小文件不生效、只警告小文件而不警告空文件、可观测性差
配置行为与用户预期不符#1020(start_at_beginning困惑)、#3567(ignore_older困惑)、#4382(只追尾新数据)
读取正确性#1125、#2992:读行被拆分导致正确性问题
新读取模式需求#1198(周期性读取非文件)、#3216(读完即停的 backfill 模式)、#4271(批处理模式)
检查点(Checkpoint)问题#1427:清理检查点文件
文件身份识别#1948:基于路径的指纹识别、inode 指纹识别不可靠
性能瓶颈#3379:tail 仅限单核;#3440:因落后而未释放文件描述符
可观测性#2420:日志权限问题;#3662:悬空符号链接;#4048:便于内部复用

RFC 明确指出,目标不是一口气修完所有问题,而是通过重组让这些行为彼此正交,从而能够独立、容易地逐个解决。

现状解剖:重构前的单体主循环

要理解重构的必要性,先看重构前filesource 的顶层结构。RFC 用近似伪代码还原了当时的实现骨架,其核心是一个单体主循环(monolithic main loop)

let checkpoints = load_checkpoints(); // 找出配置为要监视的文件 let file_list = look_for_files(); // 对启动时已存在的文件进行优先级排序 sort(file_list); loop { // 偶尔执行这些操作以避免烧 CPU if its_time() { checkpoints.persist(); let current_file_list = look_for_files(); reconcile(&mut file_list, current_file_list); } for file in file_list { // 不频繁检查非活跃文件 if !should_read(file) { continue; } // 尝试从文件中读取新数据 while let Some(line) = file.read_line() { output.push(line) // 但不要在单次读取无限量 if limit_reached { break } } // 若配置了,则在处理完后删除文件 maybe_rm(file) // 继续读下一个文件,或中断回到(已排序的)列表头部 if should_not_read_next_file() { break } } // 丢弃已删除且已读完文件的句柄 unwatch_dead(&mut file_list); // 将收集的数据发往下游 emit(output); // 若没有新数据,退避以避免烧 CPU maybe_backoff(); // 若 Vector 正在关闭,停止处理 maybe_shutdown(); }

可以看出,读取调度、文件身份、文件起始点这三类逻辑全部纠缠在一个循环里,由sortshould_readlimit_reachedshould_not_read_next_filemaybe_backoff等零散控制点共同决定行为。这正是问题的根源——任何一处改动都会牵动全局。

在 lib/file-source/src/file_server.rs 中,我们仍能看到这个主循环的当代形态:FileServer::run维护fp_map: IndexMap<FileFingerprint, FileWatcher>,以固定的glob_minimum_cooldown间隔执行文件发现(glob)、指纹匹配与重命名判定(file_server.rs#L167-L240),随后轮询每个FileWatchershould_read()并循环read_line()收集行数据(file_server.rs#L282-L309)。RFC 中描述的"公平(fair)调度"担忧在源码注释中亦有体现:FileServer 的文档说明其协作式调度"力求公平,繁忙的文件不会淹没安静的文件,但系统若激进地轮转日志文件,极快的文件仍可能丢失"。

三大核心关注点与子组件拆分

RFC 将用户侧的主要关切归纳为三点,并据此确定实现中"倾向于一起变化"的部分:

  1. 读取调度(Read scheduling)——读哪些文件、以什么顺序、每次读多少;
  2. 文件身份(File identity)——如何唯一识别一个文件,以及识别后如何维护监视列表;
  3. 文件起始点(File starting point)——从文件的哪个位置开始读取。

拆分的第一目标,是让这些变更区域彼此隔离,构造能够有效封装复杂度的子组件;接缝(seams)一旦就位,每个组件的内部改进就变得简单。

读取调度(Read scheduling)

在伪代码中,读取调度由sortshould_readlimit_reachedshould_not_read_next_filemaybe_backoff控制。一个完整的调度组件需要回答五个问题:

  1. 以什么顺序读取可用的文件?
  2. 是否读完一个文件再转向下一个?
  3. 是否对某个文件退避读取?
  4. 是否对所有文件退避(即休眠)?
  5. 在单个文件上花费多长时间?

这些问题的答案由配置、文件元数据、收集到的统计信息共同决定。RFC 给出了两种实现路线:

  • 结构体方案:实现一个包含配置、暴露类似上述方法的 struct,保持"一个主读循环 + 多个控制点"的现有结构;
  • Trait 方案:引入表示读循环逻辑的 trait,可能带来一定代码重复,但能让不同用例更简单地分离,也让代码阅读者只需理解更少的微妙控制点。

RFC 的倾向是先采用更简单的结构体整合方案(设计决策更少),待批处理(batch)模式等功能完成后,再重新评估是否值得进一步简化读循环。从当代源码看,调度逻辑仍然内聚在FileServer中:should_read()以"EOF 退避 + 10 秒活跃窗口"决定是否轮询某个文件(file_watcher/mod.rs#L339-L347),而oldest_firstmax_read_bytes等配置直接控制读取顺序与单文件读量上限。

文件身份:Fingerprinter 的演进与统一

身份识别是 RFC 认为已经具备模块化雏形但尚不完整的部分——即Fingerprinter抽象。在伪代码中,身份逻辑同时参与look_for_filesreconcile,它不仅是计算一个"魔法标识符",更包含"根据标识符更新被监视文件列表"的逻辑。它需要回答:

  1. 给定一个可见路径,它是否包含我见过的文件?
  2. 若见过,它是否已被重命名?
  3. 若见过,我现在是否在多个位置看到它?
  4. 若出现重复,我应选择跟随哪一个?

RFC 提出三个方向的改进:

  • 用路径式指纹替代设备+inode 指纹:为不需要担心传统日志轮转的用例提供最简单的选项;
  • 统一两种校验和策略:将 checksum 与 first line checksum 融合为一种兼顾两者的简单算法;
  • 让指纹携带"如何被确定"的信息:为未来演进/组合策略保留灵活性。

RFC 提出的统一算法草案如下:

  1. 从文件ignored_header_bytes处开始,读取最多max_line_length字节;
  2. 若返回的字节中没有换行符,则不返回指纹;
  3. 否则,返回截至第一个换行符之前字节的校验和。
源码中的指纹实现

在 lib/file-source-common/src/fingerprinter.rs 中,该设计已被完整落地。FingerprintStrategy枚举仅保留两种策略:

pub enum FingerprintStrategy { FirstLinesChecksum { ignored_header_bytes: usize, lines: usize, }, DevInode, }

对应的FileFingerprint枚举(可序列化、可哈希)为:

pub enum FileFingerprint { FirstLinesChecksum(u64), DevInode(u64, u64), }

FirstLinesChecksum使用CRC_64_ECMA_182算法(FINGERPRINT_CRC),并感知压缩UncompressedReaderImpl::reader会检查 gzip 魔数头,若匹配则通过gzip_multiple_decoder解压后再读取(fingerprinter.rs#L86-L137),而skip_first_n_bytes明确指出"不能直接 seek n 字节,因为文件可能被压缩,必须解压到 n 字节并丢弃输出"(fingerprinter.rs#L139-L153)。这正是 RFC 中"先统一读路径,让校验和正确处理压缩文件"前提的直接实现。

fingerprint_or_emit还实现了"小文件"的宽容处理:当读到UnexpectedEof(文件内容不足以形成完整行)时,会将路径记入known_small_files并发出emit_file_checksum_failed,待下次轮询再试,避免对空文件/小文件刷错误日志(fingerprinter.rs#L199-L244)。

单元测试对上述设计给出了直接验证(fingerprinter.rs#L307-L618):

  • test_checksum_fingerprint:空文件与无换行的文件指纹失败,内容相同的文件指纹相等;
  • test_first_line_checksum_fingerprinttest_first_two_lines_checksum_fingerprint压缩文件与未压缩文件得到相同的指纹(这是 RFC 中压缩感知指纹的核心诉求);恰好等于max_line_length与超过max_line_length的文件指纹一致,而少于一个换行符的"半行"文件指纹失败;
  • test_first_two_lines_checksum_fingerprint_with_headersignored_header_bytes可跳过公共头部,头部字节数相同但内容不同时指纹仍相等(因为被忽略),头部字节数不同则指纹不同;
  • test_inode_fingerprint:DevInode 策略对小文件同样有效(这是 RFC 所说 inode "简单且对小文件有效"的优点),但对内容相同、inode 不同的文件会给出不同指纹(其缺陷)。
用户侧指纹配置

在 src/sources/file.rs#L274-L343 中,FingerprintConfig将上述策略暴露为用户可配置项:

  • strategy: checksum:从文件开头读取若干行计算校验和,支持ignored_header_bytes(跳过的头部字节数,文件压缩时指解压后内容的头部)与lines(参与校验的行数,文件行数不足则完全不被读取);
  • strategy: device_and_inode:使用设备号与 inode 作为标识(inode 说明)。

默认策略为Checksum { ignored_header_bytes: 0, lines: 1 }parse_config测试(src/sources/file.rs#L879-L962)验证了strategy: device_and_inodestrategy: checksum(含bytesignored_header_bytes等历史字段)的解析兼容性。

文件起始点:新配置模型

文件起始点决策发生在look_for_files构建 watcher 时,需要综合已存检查点、文件元数据(如 mtime)、文件是启动时还是运行中发现、source 配置四方面。RFC 指出这一决策只发生在一处,难点主要在提供可理解的配置 UI,且应由真实用例驱动,例如:

  1. 忽略既有检查点;
  2. 从已有文件的开头或结尾开始,可选地考虑 mtime 等因素;
  3. 对监视期间新增的文件,从开头或结尾开始(这可能很棘手);
  4. 对上述关注点的优先级排序。

RFC 提议的配置模型如下:

  • ignore_checkpoints = true|false——忽略已存在的检查点(但仍照常写入);
  • read_from = beginning|end——在没有检查点或检查点被忽略时,从哪里开始读;
  • skip_older_than = duration——当read_from = beginning时,根据 mtime 跳过较旧文件(seek 到末尾);
  • 监视期间新增的文件总是从头开始——因为难以区分mv与"创建后写入",不能依赖先看到空文件来保证拿到全部数据。
源码中的起点决策

该配置模型在 src/sources/file.rs 中落地为:

  • start_at_beginning已被标记为deprecated,文档注释明确提示"使用ignore_checkpoints/read_from代替"(file.rs#L87-L93);
  • ignore_checkpoints: Option<bool>(file.rs#L95-L99);
  • read_from: ReadFromConfig,取值beginning/end(lib/file-source-common/src/lib.rs#L21-L39);
  • ignore_older_secs保留了ignore_older作为别名(file.rs#L104-L109),并由calculate_ignore_before换算为Option<DateTime<Utc>>(file_server.rs#L517-L519)。

reconcile_position_options(file.rs#L694-L715)实现了新旧配置的兼容与优先级:使用旧参数start_at_beginning时发出弃用警告;start_at_beginning: true等价于"忽略检查点并从开头读",否则回退到ignore_checkpointsread_from的显式设置。ReadFrom内部枚举还包含Checkpoint(FilePosition)变体,用于表达"从检查点位置继续"。

FileWatcher:统一文件访问的通用包装

RFC 观察到FingerprinterFileWatcher都在读文件——一个为校验和、一个为返回行——但只有FileWatcher处理压缩,因此文件被轮转并压缩后指纹会被搞混。解决方案是把FileWatcher演进为通用文件句柄包装结构,将所有直接文件访问封装在结构体内,统一处理压缩等关注点。

当代源码中FileWatcher位于 lib/file-source/src/file_watcher/mod.rs,除了压缩感知读取(is_gzipped通过检查GZIP_MAGIC魔数决定是否走 gzip 解码路径,见 file_watcher/mod.rs#L360-L365),还承载了 RFC 提议的无句柄跟踪状态雏形:should_read()依据reached_eoflast_read_success/last_read_attempt决定是否值得轮询,从而避免对长时间无数据文件频繁发起读取(file_watcher/mod.rs#L339-L347);read_retry_delay在反复 EOF 时指数退避(EOF_READ_BACKOFF_MIN/MAX之间倍增,见 file_watcher/mod.rs#L321-L332)。

通用优化(General tweaks)

在抽取子组件之外,RFC 还提出了若干相对简单的通用改进,均能在当代源码中找到对应实现。

文件发现与检查点持久化:拆出主循环

RFC 指出当时用过时的glob_minimum_cooldown同时控制文件发现与检查点持久化的频率,应改为各自独立的配置项,并允许禁用(如 batch 用例无需持续发现新文件)。两者还应搬入独立的后台周期任务,避免在主循环中执行昂贵的操作(例如在 EBS 上发现数百万文件)时引发性能问题。

源码中:

  • glob_minimum_cooldown_ms保留了glob_minimum_cooldown作为别名(file.rs#L150-L162),默认值 1000ms;
  • FileServer::run在启动后即spawn一个独立的checkpoint writer 任务checkpoint_writer),以glob_minimum_cooldown为周期独立运行(file_server.rs#L150-L156),文件发现则仍由主循环按next_glob_time节制地周期性执行(file_server.rs#L167-L240);
  • 检查点持久化采用临时文件 + 原子重命名策略:先写tmp文件并sync_all刷盘,再fs::rename覆盖稳定文件,崩溃时仍能保留一份完整有效文件用于恢复(checkpointer.rs#L185-L227);读取时优先尝试 tmp 文件(说明上次进程被中断),再回退到稳定文件(checkpointer.rs#L229-L272)。这与 RFC 中"迁移到 JSON 文件式检查点"的设想(issue #1779)一致——当前检查点即以 JSON 格式存储。

读取并发:从线程池到 iouring 的取舍

把其他关注点抽走后,主读循环可以腾出精力支持并发读取。RFC 列出了三种可能路线:

  1. 将读取分发给显式线程池(threadpool);
  2. 生成有限数量的阻塞 tokio 任务;
  3. 基于iouring实现。

前两种都要回答线程数量问题,且依赖底层文件系统能否真正从并发访问中获得性能提升,需要广泛场景测试。iouring更有趣但受限——仅现代 Linux 可用,不能作为唯一实现;不过现代 Linux 占 Vector 使用的绝大多数,足以覆盖最苛刻的用例,还能把并发问题交给内核去利用硬件。

RFC 的结论是暂时等待:本 RFC 中其他改动已经能带来正向性能影响,届时对上述方案的性价比会有更清晰判断;进入该阶段时建议先从tokio 文件系统接口入手,因为未来的iouring改进更可能匹配 async 接口。

压缩感知指纹(Compression-aware fingerprints)

如上一节所述,FingerprinterFileWatcher双双演进为统一读取路径后,指纹计算可正确处理 gzip 压缩文件——fingerprinter.rs#L418-L433 的测试直接断言one_line.log(未压缩)与one_line_duplicate_compressed.log(gzip)指纹相等,这正是本小节设计意图的验收标准。

批处理模式(Batch mode):读完即停

RFC 提议在既有关闭逻辑之外,增加一个配置项及对应条件:当所有文件都到达 EOF 后退出 source。配合禁用的文件发现,即可简洁地实现呼声很高的 batch 模式。在 Doc-level Proposal 中,这体现为用mode = tail|tail_sequential|batch取代oldest_first

可观测性改进

RFC 列出若干可观测性方向("没有宏大设计,就是逐项落实"):

  • 文件被删除但必须保持打开时记录日志;
  • 不为小文件/空文件刷噪声日志;
  • 可选地静默悬空符号链接导致的错误;
  • 暴露正在读取哪些文件以及读取进度。

最后一项最有价值:将检查点从"奇怪的基于文件名的系统"迁移到JSON 文件方案(issue #1779)。当代Checkpointer即采用data_dir下固定文件名(CHECKPOINT_FILE_NAMETMP_FILE_NAME)的 JSON 持久化,CheckpointsViewFileFingerprint -> FilePosition映射维护进度(checkpointer.rs#L44-L76),并定期清理 60 秒前已删除文件的检查点以控制工作集大小(checkpointer.rs#L189-L193)。此外,FileServer内置TimingStats在 debug 级别输出"发现/读取/检查点"各段耗时占比与事件、字节吞吐,用于定位性能热点(file_server.rs#L529-L564)。

Doc-level Proposal:用户侧配置的迁移路径

RFC 提出的用户侧配置变更方案如下:

旧配置新配置说明
ignore_olderskip_older_than重命名
start_from_beginningignore_checkpoints+read_from拆分语义
oldest_firstmode = tail\|tail_sequential\|batch用模式取代布尔开关
glob_minimum_cooldowndiscovery_intervalmode = batch时禁用重命名并语义化

权衡与备选方案

Rationale:为什么不直接重写

RFC 论证重构(而非重写)的核心理由:

  • file source 已严重超出原始设计,对用户和开发者都造成痛苦,值得投入时间改善可用性与可维护性,否则维护与用户支持将持续消耗大量人力;
  • 通过模块化与简单改进,可以保留组件长期积累的"好部分"(历次 bug 修复),也降低了激进重写必然带来的新 bug 风险;
  • 模块化为未来铺路:鼓励用户侧配置与实现级配置分离,与 config composition RFC 相契合,可为特定文件使用场景构建简洁的配置门面(facade)。

Drawbacks:不兼容变更的代价

这是对最广泛使用的 source 之一的向后不兼容配置变更,会打扰大量现有用户;重构过程中也始终存在引入新 bug 的风险(尽管设计上已尽力最小化)。

Alternatives:另两条路

  • 从零重写:代码可能更易维护,但会丢失现有实现积累的领域知识,耗时更长、迁移计划更难;
  • 只改进文档与配置 UI、基本不动实现:能带来明显收益,但大量重要 issue 无法解决,且无助于未来的扩展能力。

攻击计划(Plan of Attack):分阶段落地路径

RFC 按"投入/回报比"给出了明确的实施顺序,也是理解 file source 演进脉络的路线图:

第一优先:校验和(回报最高)

  • FileWatcher迁移为带指纹能力的通用文件包装器;
  • 重构检查点持久化,使其能区分并迁移不同类型;
  • 合并 checksum 与 first line checksum 指纹策略;
  • 新增路径式指纹策略;
  • 弃用 device/inode 指纹策略。

第二优先:把多余工作移出读循环(显著改善部分边界场景性能)

  • 将路径发现移至独立任务与独立间隔;
  • 将检查点持久化移至独立任务与独立间隔。

第三优先:通用重组(为配置改进铺路)

  • 抽取调度组件(scheduler);
  • 抽取文件身份组件(依赖FileWatcher工作);
  • 抽取文件起始点组件(依赖FileWatcher工作)。

第四优先:新版配置

  • 实现version = 2的 file source 配置并带弃用警告;
  • ignore_olderskip_older_than
  • start_from_beginningignore_checkpoints+read_from
  • oldest_firstmode = tail|tail_sequential
  • glob_minimum_cooldowndiscovery_interval

随后:批处理模式

  • 实现 batch 模式关闭条件与新配置mode

文件包装器工作完成后:

  • 为闲置一定时间的文件增加"无打开句柄跟踪"状态,停止持有不必要的文件句柄。

并行进行:可观测性打磨

  • 文件不再可找到但 watcher 未死亡时记录日志;
  • 空文件不记录日志;
  • 增加禁用悬空符号链接日志的选项。

遗留问题(Outstanding Questions)

RFC 在最后留下两个开放问题,供实施时决策:

  • 用户侧配置应与实现同时变更,还是分两步推进?
  • 如何帮助现有用户平滑迁移到新接口?

总结

RFC 3480 为 Vector 的filesource 描绘了一条"模块化演进而非重写"的路径:以读取调度、文件身份、文件起始点三个正交子组件为骨架,配合文件发现/检查点独立化、并发读取、压缩感知指纹、batch 模式与可观测性改进,系统性化解数页 issue 积累的历史包袱。对照当代仓库源码可以确认,该 RFC 的大部分构想已经落地:FingerprintStrategy/FileFingerprint统一了校验和策略并支持压缩感知指纹(lib/file-source-common/src/fingerprinter.rs),Checkpointer采用 JSON + 原子重命名实现可靠检查点(lib/file-source-common/src/checkpointer.rs),FileWatcher成为封装压缩与读取退避的统一文件访问层(lib/file-source/src/file_watcher/mod.rs),而ignore_checkpointsread_fromignore_older_secs等新配置与start_at_beginning的弃用迁移逻辑也已就位(src/sources/file.rs)。阅读 RFC 原文与这些实现代码,可以完整理解 Vector 最核心输入组件从"单体怪物"走向"正交模块"的设计哲学与工程取舍。

【免费下载链接】vectorA high-performance observability data pipeline.项目地址: https://gitcode.com/GitHub_Trending/vect/vector

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 7:19:16

STM32CubeMX驱动ST7789实战:突破默认配置的时序陷阱

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 7:16:30

GPT-Image-2实战指南:从API选型到批量出图工作流全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 7:16:09

Doris + Paimon 构建 Agentic AI 数据闭环

最近在帮团队搭一个 Agentic AI 项目的数据底座&#xff0c;老板上来第一句就是“把大模型 API 接上、工具链配上&#xff0c;是不是就完事了&#xff1f;”我直接打断他&#xff1a;还差最重要的一环——数据闭环。Agent 不是简单的“模型调用”&#xff0c;它每次感知、决策、…

作者头像 李华
网站建设 2026/9/13 7:16:06

AR远程协助核心技术解析与应用实践

1. AR远程协助行业概述AR&#xff08;增强现实&#xff09;远程协助技术正在重塑全球企业的服务模式。这项技术通过将数字信息叠加到真实世界场景中&#xff0c;使专家能够跨越地理限制指导现场人员。根据市场研究数据&#xff0c;全球AR远程协助市场规模预计将从2022年的25亿美…

作者头像 李华