为什么叫"测试夹具":Fixture 的词源与跨领域漫游
测试夹具这个中文词带着工业感,让第一次听到它的开发者往往去搜索"是不是翻译错了"。词源确实如此:fixture 来自机械加工,后来进入电子测试领域,再由 Kent Beck 引入软件测试框架里。这个词在软件工程里覆盖的范围,也远比"单元测试"要宽。 从车间到代码 fixture 这个词在机械加工里有精确含义:在 CNC 车床或焊接台上,fixture 是一种工装,把工件固定在已知位置,让每次加工操作都从相同的起点开始。与 jig(引导刀具路径的夹具)不同,fixture 的职责只有一件事——把工件钳牢,消除定位误差。 词源上,fixture 来自拉丁语 figere(刺入、固定),经 fixura(固定物)演变为英语词汇,约在 16 世纪末进入法律语言,最初指"永久附着于不动产的动产"——嵌入墙体的管道、固定在地板上的书架,那些不能随意拆走的东西。 电子测试领域用法相似:一块 PCB 插入测试夹具后被固定在已知电气状态,探针才能重复施加相同的激励、在相同节点读取信号,排除接触不稳定的干扰。 三种用法的共同语义:将某个对象...
Spark 与 Flink:两种计算引擎的设计选择
上一篇解决了 Spark 的性能诊断方法。这一篇进入设计对比——Spark 和 Flink 两种计算引擎的架构选择。 这不是一个"谁更好"的问题。Spark 和 Flink 从不同的起点出发,做了不同的设计取舍。Spark 以批处理引擎起家,Structured Streaming 默认使用 micro-batch,当前版本也提供受限定的 Real-Time Mode;Flink 以持续流处理为基础,同时为有界输入提供专门的批执行模式。两条路线在 API 层逐步趋同,但执行方式、状态后端和运行时能力仍然需要按具体版本与负载评估。 本文只抓一个问题:两个引擎在执行模型、状态管理、容错机制和延迟特性上的具体差异及其设计原因。 执行模型对比 Spark 的执行模型是 Stage-based DAG:把计算图按 Shuffle 边界切成 Stage,每个 Stage 包含一组可以 pipeline 执行的窄依赖变换。Stage 之间是全局同步点——上游 Stage 的所有 Task 必须全部完成后,下游 Stage 才能启动。 1234567891011Spark 执...
性能诊断:Spark UI、Event Log 与调优思路
上一篇解决了动态资源分配和调度策略。这一篇进入性能诊断——当 Spark 作业慢了,怎么找到瓶颈。 性能调优的第一步不是改配置,是定位问题。Spark UI 提供了五个关键页面(Jobs、Stages、Storage、Environment、Executors),每个页面指向不同类型的性能问题。Event Log 提供了离线分析的完整数据。 本文只抓一个问题:面对一个慢作业,如何从 Spark UI 的指标出发,沿着诊断路径找到根因。 Spark UI 五个页面 1234567891011121314151617181920212223Jobs 页面 → 每个 Action 对应一个 Job → 关注: Job Duration、Failed Tasks 数量 → 入口: 找到最慢的 Job,点击进入 Stage 详情Stages 页面 → 每个 Stage 的 Task 统计 → 关注: Task Duration 分布、Shuffle Read/Write、GC Time → 核心: 这里是定位性能问题的主战场Storage 页面 → 缓存的 RDD/DataF...
动态资源分配与作业调度:多租户集群的资源博弈
上一篇解决了 Spark 在 YARN 和 K8s 上的部署模式。这一篇进入资源调度——在多个作业共享集群时,Spark 如何动态分配和回收资源。 默认情况下,一个 Spark 应用在启动时申请固定数量的 Executor,作业结束前一直占用。这在单用户开发环境下可以接受,在多租户生产集群中会造成严重的资源浪费——一个空闲的 Spark 应用占着 100 个 Executor 不释放,其他作业排队等待。 本文只抓一个问题:动态资源分配如何按需申请和释放 Executor,以及 FAIR 调度器如何在多个作业之间公平分配资源。 图 1:动态资源分配与两级调度的责任边界 flowchart TB Q[集群队列或 namespace 配额] --> A[应用可申请的资源上限] B[等待中的 Task 与空闲超时] --> D[Dynamic Allocation] D -->|请求增减| E[应用专属 Executor] A --> E F[FIFO 或 FAIR:应用内部] -->|选择待运行 Task| E 图 1 中 FAIR 只...
Spark on YARN / K8s:资源管理与部署模式
Spark 应用由 Driver 和专属 Executor 组成。集群管理器分配进程所需资源,Driver 决定运行哪些 Task,Executor 读写业务数据。部署时要分别确认进程位置、控制连接、数据路径和故障后的恢复责任。 本文采用 Apache Spark 当前部署文档描述的 Standalone、YARN 和 Kubernetes 原生运行方式;具体参数应与实际安装版本对齐。Mesos 已在 Spark 4.0 移除,不属于当前可选部署后端。 生产部署拓扑与组件职责 Driver 保存 SparkContext、调度器和运行时元数据。Executor 执行 Task、缓存分区,并提供 Shuffle 数据。集群管理器既不理解 SQL 算子,也不代替 Driver 调度 Task。 图 1:资源分配、任务调度与业务数据的三条路径 flowchart TB CM[集群管理器] -->|分配资源| E[Executor 进程] D[Driver 与 SparkContext] -->|Task 与控制消息| E E -->|状态与任务结果| D ...
水位线与状态管理:流处理的时间语义
上一篇解决了 Structured Streaming 的基本模型。这一篇进入时间语义——事件时间、水位线和状态管理。 流处理中的"时间"不是一个简单的概念。一条消息有两个时间戳:事件时间(Event Time,事件实际发生的时间)和处理时间(Processing Time,Spark 收到消息的时间)。两者可能差距很大——网络延迟、消费积压、跨时区传输都会导致消息乱序到达。 本文只抓一个问题:Watermark 如何告诉 Spark “多晚的数据还值得等”,以及 State Store 如何持久化窗口状态。 事件时间 vs 处理时间 123456789101112事件发生 网络传输 Spark 收到 t=10:00:01 ~~~延迟 3s~~~ t=10:00:04 (Processing Time) t=10:00:02 ~~~延迟 1s~~~ t=10:00:03 t=10:00:03 ~~~延迟 8s...
Structured Streaming:把流当成无界表
上一篇解决了数据源 API 和谓词下推。这一篇进入流处理——Structured Streaming。 Spark 的流处理经历了两代:DStream(基于 RDD 的离散化流)和 Structured Streaming(基于 DataFrame/Dataset 的结构化流)。DStream 已经停止演进,Structured Streaming 是当前唯一推荐的流处理 API。 Structured Streaming 的核心模型可以用一句话概括:把流数据看成一张不断追加新行的无界表,复用 Spark SQL 的 Catalyst 优化器和 Tungsten 执行引擎来处理。 本文只抓一个问题:无界表模型如何工作,以及 MicroBatch 执行引擎如何把连续的流切成离散的批次。 无界表模型 12345678传统流处理的心智模型: 消息 → 处理函数 → 输出 (一条一条处理,每条消息触发一次计算)Structured Streaming 的心智模型: 数据源不断追加行到一张"输入表" 查询在整张表上持续运行 结果写入一张"输出表&qu...
数据源 API 与谓词下推:让存储层帮忙过滤
上一篇解决了 AQE 的运行时优化。这一篇进入数据源层——Spark 如何把计算推到存储层执行。 查询优化不只发生在执行引擎内部。如果存储层能在读取数据时就过滤掉不需要的行和列,引擎需要处理的数据量会大幅减少。Spark 通过 DataSource API 和 Catalyst 的优化规则把这个能力标准化了:谓词下推(Predicate Pushdown)让存储层只返回满足条件的行,列裁剪(Column Pruning)让存储层只返回查询需要的列。 本文只抓一个问题:谓词下推和列裁剪如何从 Catalyst 的逻辑计划传递到数据源实现。 DataSource API 的两代演进 Spark 的数据源 API 经历了两代: 12345678910DataSource V1 (Spark 1.3+): 接口: InputFormat / OutputFormat + createRelation 谓词下推: 通过 PrunedFilteredScan trait 可选实现 局限: 批与流使用不同接口,能力与优化协商较分散DataSource V2 (Spark 2.3+, 重构...
Adaptive Query Execution:运行时改写执行计划
上一篇解决了 DataFrame 和 Dataset 的 API 演进。这一篇进入运行时优化——Adaptive Query Execution(AQE)。 传统查询优化器的一个根本问题是:优化发生在执行之前,依赖的统计信息可能不准确或完全缺失。AQE 把优化推迟到执行过程中——在每个 Shuffle 边界,收集实际的数据统计信息,然后用这些精确的统计重新优化后续 Stage 的执行计划。 本文只抓一个问题:AQE 的三个核心优化分别解决什么问题,以及运行时优化的反馈循环如何工作。 图 1:AQE 使用已物化统计调整后续读取与 Join flowchart TB P[初始物理计划] --> S[执行 Exchange 并物化数据] S --> M[实际分区大小与行数统计] M --> C[合并小分区] M --> J[调整适用的 Join 策略] M --> K[拆分符合条件的倾斜分区] C --> R[后续 Task 读取] J --> R K --> R 图 1 的优化发生在已有物化结果之上。已经...
DataFrame 与 Dataset:类型安全与性能的折中
上一篇解决了 Catalyst 优化器的四阶段流水线。这一篇进入用户 API 层——RDD、DataFrame、Dataset 三代 API 的演进逻辑。 三者的区别容易被简化为"RDD 是低级 API,DataFrame 是高级 API"。更准确的说法是:三代 API 在类型安全和执行优化之间做了不同的取舍。RDD 完全类型安全但无法被 Catalyst 优化;DataFrame 完全被 Catalyst 优化但放弃了编译期类型检查;Dataset 试图兼顾两者,但付出了 Encoder 序列化/反序列化的代价。 本文只抓一个问题:三代 API 在内部表示、优化路径和序列化机制上的具体差异。 三代 API 的内部表示 1234567891011121314RDD[Person] └─ 内部存储: JVM 堆上的 Java/Scala 对象 └─ 优化路径: 无(用户代码是黑盒,Spark 无法查看函数内部) └─ 类型信息: 编译期完整保留(泛型参数 T = Person)DataFrame (= Dataset[Row]) └─ 内部存储: Uns...


