拾穗数据

Back

数据开发工程师 L3:架构演进#

[!quote] 写在前面 如果你正在读这篇文档,说明你已经在数据开发领域摸爬滚打了几年。你对数仓建模、Hive/Spark 已经相当熟练,日常工作得心应手。但你开始感到某种瓶颈:业务方要实时数据,现有的 T+1 架构满足不了;数据量越来越大,以前的优化手段不够用了;新技术层出不穷,Flink、数据湖、流批一体…你不确定该往哪个方向发力。

L3 阶段是一个分水岭。从这里开始,你不再只是”写代码的”,而是要开始思考”为什么这么做”、“有没有更好的架构”。这篇文档会帮助你理清这个阶段的学习重点,以及如何从”熟练工”进化为”架构师”。


这个阶段的你,可能是这样的#

画像一:业务要实时数据,但你只会离线#

老板说:“竞对的数据大屏是实时的,我们也要。“产品说:“用户下单后,5秒内就要在 APP 里看到状态更新。“你慌了——你的技能树全点在离线数仓上,Flink 只听过没用过,Kafka 只知道是个消息队列。

给你的建议:实时计算是 L3 阶段最重要的技能跃迁。好消息是,实时和离线的思维方式有很多相通之处。你在 Spark SQL 上的经验,可以快速迁移到 Flink SQL。建议从 Flink SQL 入手,先跑通一个简单的实时 ETL,再慢慢深入 DataStream API 和状态管理。

画像二:Spark 任务越来越慢,调参调不动了#

你负责的 Spark 任务,数据量翻了一倍,运行时间从 2 小时变成了 8 小时。你试了各种参数调优——增加 executor 数量、调整内存配比、调整 shuffle 分区数——但效果有限。你意识到,可能不是参数的问题,而是架构的问题。

给你的建议:到了 L3 阶段,“调参”已经不是主要手段了。你需要深入理解 Spark 的执行原理——Stage 是怎么划分的?Shuffle 数据是怎么落盘的?内存是怎么管理的?搞清楚这些,你才能从根本上解决问题,而不是在参数上碰运气。

画像三:想往架构师方向发展,但不知道从哪开始#

你听说高级别的岗位叫”数据架构师”,薪资很高,也很有技术含量。但你不知道架构师具体做什么,也不确定自己是否具备那些能力。你想往这个方向发展,但没有明确的路径。

给你的建议:架构师不是突然”升级”的,而是在日常工作中逐渐培养出来的。你可以从以下几个方面开始:

  1. 每次接需求时,多想想”有没有更好的架构方案”
  2. 主动参与系统设计评审,学习别人的设计思路
  3. 尝试写技术方案文档,把你的设计思考落到纸面上
  4. 关注业界的架构演进,了解为什么别人要这么设计

画像四:对数据治理没什么概念,感觉是”虚的”#

你听过数据质量、元数据管理、数据血缘这些词,但觉得这些是”管理层的事”,和写代码没什么关系。你的关注点一直在技术实现上,对治理体系不太上心。

给你的建议:数据治理绝对不是”虚的”。当你半夜被叫起来排查”数据怎么又错了”,当你花了三天才搞清楚一个字段的口径,当你的任务因为上游变更突然挂掉——这些都是缺乏治理的后果。L3 阶段,你需要开始建立治理思维:写代码的同时,思考如何让这套系统更可控、更可追溯、更少出问题。


L3 阶段的核心目标#

用一句话概括:

能够设计和落地复杂的数据架构,解决性能、时效、质量方面的核心挑战。

具体来说:

  • 掌握实时计算技术,能构建秒级延迟的数据链路
  • 深入理解计算引擎原理,能进行深度性能优化
  • 能进行架构选型和设计,权衡各种方案的利弊
  • 具备数据治理意识,能建立质量保障体系

L2 阶段你学会了”构建系统”,L3 阶段你要学会”设计架构”。构建是执行,架构是决策。


必须掌握的核心技能#

1. 实时计算 —— 从 T+1 到 T+0#

这是 L3 阶段最重要的能力跃迁。离线计算和实时计算是两种完全不同的思维方式。

离线 vs 实时的本质区别

维度离线计算实时计算
数据特点有界数据集无界数据流
计算模式批处理(一次处理所有)流处理(逐条/微批处理)
时效性T+1 或更长秒级/分钟级
容错方式任务失败重跑Checkpoint + 状态恢复
核心挑战数据量、计算效率延迟、乱序、状态管理

为什么实时计算这么难?

离线计算处理的是”已经发生完”的数据,可以反复计算、校验。实时计算处理的是”正在发生”的数据,你不知道后面还有什么,而且必须快速响应。

几个核心挑战:

  • 乱序问题:用户 10:00 的行为,可能 10:05 才到达系统。你该按发生时间算还是到达时间算?
  • 状态管理:要算用户的累计消费额,必须存储历史状态。状态存在哪?多大?崩溃了怎么恢复?
  • Exactly-Once:消息来了处理一半系统挂了,重启后怎么保证不丢不重?

Flink 核心概念

  1. 时间语义
// Event Time:事件发生时间(最常用,但需要处理乱序)
// Processing Time:处理时间(最简单,但结果不可复现)
// Ingestion Time:进入 Flink 的时间(折中方案)

env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
java
  1. Watermark(水位线)

Watermark 是处理乱序数据的核心机制。它告诉系统:“我认为时间戳小于这个值的数据都已经到齐了。”

// 假设数据最多乱序 5 秒
WatermarkStrategy
    .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(5))
    .withTimestampAssigner((event, timestamp) -> event.getTimestamp());
java
  1. 窗口(Window)
// 滚动窗口:每 5 分钟一个窗口,窗口不重叠
stream.keyBy(e -> e.userId)
      .window(TumblingEventTimeWindows.of(Time.minutes(5)))
      .sum("amount");

// 滑动窗口:窗口大小 10 分钟,每 5 分钟滑动一次
stream.keyBy(e -> e.userId)
      .window(SlidingEventTimeWindows.of(Time.minutes(10), Time.minutes(5)))
      .sum("amount");

// 会话窗口:不活跃超过 30 分钟,窗口关闭
stream.keyBy(e -> e.userId)
      .window(EventTimeSessionWindows.withGap(Time.minutes(30)))
      .sum("amount");
java
  1. 状态(State)
// Keyed State:每个 Key 独立的状态
public class CountFunction extends KeyedProcessFunction<String, Event, Result> {
    // 值状态:存储一个值
    private ValueState<Long> countState;

    // 列表状态:存储一个列表
    private ListState<Event> historyState;

    // Map状态:存储一个Map
    private MapState<String, Long> detailState;

    @Override
    public void open(Configuration parameters) {
        countState = getRuntimeContext().getState(
            new ValueStateDescriptor<>("count", Long.class));
    }

    @Override
    public void processElement(Event event, Context ctx, Collector<Result> out) {
        Long count = countState.value();
        if (count == null) count = 0L;
        count++;
        countState.update(count);
        // ...
    }
}
java
  1. Checkpoint

Flink 通过定期做快照(Checkpoint)来保证容错。任务崩溃后可以从最近的 Checkpoint 恢复。

// 启用 Checkpoint,每 60 秒一次
env.enableCheckpointing(60000);

// Exactly-Once 语义(更安全,但更慢)
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);

// At-Least-Once 语义(更快,但可能重复)
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.AT_LEAST_ONCE);
java

Flink SQL —— 快速入门实时计算

如果你已经熟悉 SQL,Flink SQL 是最快的入门方式。

-- 创建 Kafka 源表
CREATE TABLE order_source (
    order_id STRING,
    user_id STRING,
    amount DECIMAL(10,2),
    order_time TIMESTAMP(3),
    WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND
) WITH (
    'connector' = 'kafka',
    'topic' = 'orders',
    'properties.bootstrap.servers' = 'localhost:9092',
    'format' = 'json'
);

-- 实时聚合:每分钟的订单统计
SELECT
    TUMBLE_START(order_time, INTERVAL '1' MINUTE) as window_start,
    COUNT(*) as order_cnt,
    SUM(amount) as total_amount
FROM order_source
GROUP BY TUMBLE(order_time, INTERVAL '1' MINUTE);
sql

推荐学习实时数据架构

[!warning] 实时计算的坑 实时任务一旦上线,就是 7x24 小时运行的。和离线任务不同,你没法说”今晚重跑一下就好了”。所以:

  1. 一定要做好监控和报警
  2. 状态不能无限增长,要设置 TTL
  3. 要考虑好 Schema 变更怎么处理
  4. 要有回溯方案(从某个时间点重新消费 Kafka)

2. 数据湖与湖仓一体 —— 架构的下一站#

传统数据仓库有一些固有的问题:

  • 不支持 ACID 事务,数据更新只能全量覆盖
  • 只能存储结构化数据,非结构化数据没法处理
  • Schema 强绑定,修改表结构很痛苦

数据湖技术(Hudi、Iceberg、Delta Lake)就是为了解决这些问题。

核心能力对比

特性传统 Hive数据湖(Hudi/Iceberg)
ACID 事务不支持支持
增量更新INSERT OVERWRITEUPSERT/DELETE
Schema 演进困难支持
时间旅行不支持支持(查历史快照)
存储格式Parquet/ORCParquet + 元数据

Hudi 核心概念

Copy-on-Write (COW):
- 写入时复制整个文件
- 读取性能好(直接读 Parquet)
- 写入性能差(要重写文件)
- 适合读多写少的场景

Merge-on-Read (MOR):
- 写入时只追加 Delta 文件
- 写入性能好
- 读取时需要合并(读性能略差)
- 适合写多读少的场景
plaintext

实际应用场景

-- Hudi 表创建示例
CREATE TABLE hudi_order (
    order_id STRING,
    user_id STRING,
    amount DECIMAL(10,2),
    status STRING,
    update_time TIMESTAMP
) USING hudi
OPTIONS (
    'primaryKey' = 'order_id',
    'type' = 'cow',
    'preCombineField' = 'update_time'
);

-- 支持 UPSERT(有则更新,无则插入)
MERGE INTO hudi_order target
USING source_data source
ON target.order_id = source.order_id
WHEN MATCHED THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;

-- 时间旅行:查询昨天的数据快照
SELECT * FROM hudi_order TIMESTAMP AS OF '2024-06-14';
sql

湖仓一体架构

传统架构:
数据源 → 数据湖(原始存储) → 数据仓库(分析)
         ↑ 两套系统,数据要搬来搬去

湖仓一体:
数据源 → 数据湖 + 仓库能力(一套系统搞定)
         ↑ 存储和计算分离,同一份数据支持批/流/交互式分析
plaintext

推荐学习数据仓库与数据湖建模云原生数据架构

3. 深度性能优化 —— 从调参到调架构#

L2 阶段的优化主要是”调参”,L3 阶段要深入到原理层面。

Spark 执行原理深度解析

一个 Spark SQL 的执行过程:

SQL 语句
    ↓ 解析
逻辑计划(Logical Plan)
    ↓ 优化器(Catalyst)
优化后的逻辑计划
    ↓ 物理计划生成
物理计划(Physical Plan)
    ↓ 代码生成(Codegen)
RDD 执行图
    ↓ DAGScheduler
Stage 划分(以 Shuffle 为边界)
    ↓ TaskScheduler
Task 分发到 Executor 执行
plaintext

几个关键优化点

  1. 减少 Shuffle

Shuffle 是分布式计算中最昂贵的操作。数据要写磁盘、通过网络传输、再读出来合并。

-- 不好的写法:两次 Shuffle
SELECT a.user_id, b.order_cnt, c.pay_amount
FROM users a
JOIN (
    SELECT user_id, COUNT(*) as order_cnt
    FROM orders
    GROUP BY user_id
) b ON a.user_id = b.user_id
JOIN (
    SELECT user_id, SUM(amount) as pay_amount
    FROM payments
    GROUP BY user_id
) c ON a.user_id = c.user_id;

-- 优化后:合并子查询,减少 Shuffle
SELECT
    a.user_id,
    COUNT(DISTINCT o.order_id) as order_cnt,
    SUM(p.amount) as pay_amount
FROM users a
LEFT JOIN orders o ON a.user_id = o.user_id
LEFT JOIN payments p ON a.user_id = p.user_id
GROUP BY a.user_id;
sql
  1. 利用分区裁剪
-- 不好的写法:全表扫描
SELECT * FROM orders WHERE order_date >= '2024-06-01';

-- 好的写法:如果 dt 是分区字段,只扫描需要的分区
SELECT * FROM orders WHERE dt >= '2024-06-01';
sql
  1. 避免数据膨胀
-- 危险的写法:笛卡尔积
SELECT a.*, b.*
FROM table_a a
JOIN table_b b
ON a.key = b.key AND a.key IS NULL;
-- 如果 a.key 有很多 NULL,会产生笛卡尔积

-- 更危险的写法:CROSS JOIN
SELECT * FROM table_a CROSS JOIN table_b;
-- 1万行 x 1万行 = 1亿行
sql
  1. AQE(Adaptive Query Execution)

Spark 3.0 引入的自适应查询执行,可以在运行时动态调整执行计划。

-- 启用 AQE
SET spark.sql.adaptive.enabled = true;

-- 自动合并小分区(避免大量小文件)
SET spark.sql.adaptive.coalescePartitions.enabled = true;

-- 自动处理数据倾斜
SET spark.sql.adaptive.skewJoin.enabled = true;
sql

JVM 层面的优化

# Executor 内存配置
--executor-memory 8g
--conf spark.executor.memoryOverhead=2g

# 内存管理
--conf spark.memory.fraction=0.6      # 执行+存储内存占比
--conf spark.memory.storageFraction=0.5  # 存储内存占比

# GC 优化
--conf spark.executor.extraJavaOptions="-XX:+UseG1GC -XX:MaxGCPauseMillis=200"
bash

推荐学习性能优化

[!tip] 性能优化的正确姿势 不要盲目优化。正确的流程是:

  1. 定位瓶颈:看 Spark UI,找出最慢的 Stage
  2. 分析原因:是数据倾斜?是 Shuffle 太多?是内存不够?
  3. 针对性优化:根据原因选择合适的优化手段
  4. 验证效果:对比优化前后的执行时间和资源消耗

4. 数据治理 —— 从混乱到有序#

L3 阶段,你要开始建立治理思维。这不是管理层的事,而是架构设计的一部分。

数据质量管理

数据质量问题的代价是巨大的。我见过因为一个字段口径错误,导致财务报表偏差几百万;见过因为数据延迟,导致运营活动失败。

质量检查的几个维度

维度含义检查方法
完整性数据是否缺失NULL 值比例、行数波动
准确性数据是否正确业务规则校验、交叉验证
一致性不同数据源是否一致核对关键指标
时效性数据是否及时监控任务延迟
唯一性是否有重复数据主键去重检查
-- 数据质量检查示例

-- 完整性检查:关键字段 NULL 比例
SELECT
    COUNT(*) as total,
    SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) as null_cnt,
    SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) / COUNT(*) as null_ratio
FROM dwd_order_detail
WHERE dt = '${bizdate}';

-- 一致性检查:订单金额和支付金额是否匹配
SELECT
    SUM(order_amount) as order_sum,
    SUM(pay_amount) as pay_sum,
    ABS(SUM(order_amount) - SUM(pay_amount)) / SUM(order_amount) as diff_ratio
FROM ads_daily_summary
WHERE dt = '${bizdate}';

-- 唯一性检查:主键是否重复
SELECT order_id, COUNT(*) as cnt
FROM dwd_order_detail
WHERE dt = '${bizdate}'
GROUP BY order_id
HAVING cnt > 1;
sql

元数据管理与数据血缘

当你有几千张表时,“这个字段是从哪里来的”就成了一个大问题。

数据血缘的价值:
1. 影响分析:修改一张表前,知道会影响哪些下游
2. 问题追溯:数据错了,能快速定位是哪个环节出问题
3. 口径统一:知道每个指标是怎么算出来的
plaintext

成本治理

大数据计算资源很贵。L3 工程师要有成本意识。

成本优化的几个方向:
1. 资源利用率:任务申请 100G 内存,实际只用 20G
2. 存储优化:历史数据压缩、冷热分层
3. 计算优化:避免重复计算,合理设置任务周期
4. 淘汰无用数据:很多表几个月没人用了,占着资源
plaintext

推荐学习数据质量管理体系与实践数据开发文档管理

5. 云原生与容器化 —— 需要学吗?#

你可能听说”现在都上 K8s 了”、“不会云原生找不到工作”。这里帮你理清。

什么情况下需要学 Kubernetes?

你的情况K8s 是否必要建议
公司数据平台部署在 K8s 上需要至少能看懂 YAML、会用 kubectl
公司还是传统 YARN 集群暂不必要先把当前技术栈学精
想做数据平台架构师必须学云原生是未来趋势
只做 ETL 开发不必要平台运维有专人负责

L3 阶段需要了解的程度

基本概念(必须知道):
- Pod:K8s 最小调度单位
- Deployment:管理 Pod 副本
- Service:服务发现和负载均衡
- ConfigMap/Secret:配置管理

实操技能(按需学习):
- 能看懂 Spark/Flink on K8s 的 YAML 配置
- 能用 kubectl 查看日志、排查问题
- 理解 Spark on K8s 和 Spark on YARN 的区别
plaintext

云原生 vs 传统方案对比

组件传统方案云原生方案
计算引擎Spark on YARNSpark on K8s
实时引擎Flink on YARNFlink Kubernetes Operator
消息队列自建 Kafka 集群Kafka on K8s / 云托管
存储HDFSS3 / OSS / MinIO

[!tip] 务实建议 不要为了学 K8s 而学 K8s。如果你当前工作用不到,先把实时计算、架构设计这些核心技能学好。当公司开始做云原生转型时,再深入也不迟。

6. AI 时代对 L3 工程师的影响#

L3 阶段,你需要思考 AI 对数据工程的影响——不是焦虑”会不会被取代”,而是思考”如何利用”。

AI 能帮 L3 工程师做什么?

场景AI 能做你必须做
架构设计列出方案选项、分析优缺点结合公司情况做最终决策
技术选型比较 Flink vs Spark 特点考虑团队能力、运维成本
性能调优分析执行计划、建议方向验证效果、处理边界情况
代码编写生成 Flink/Spark 代码框架Review 逻辑、处理异常

AI 替代不了什么?

  • 架构决策:需要结合公司实际情况权衡
  • 深度调优:复杂问题需要深入理解原理
  • 业务理解:数据模型设计需要理解业务
  • 故障处理:线上问题需要快速判断和决策

关于 MLOps / 特征工程

L3 阶段你可能开始接触 ML 相关需求(特征计算、数据集准备)。了解基本概念有帮助,但不是必须——除非你的工作方向明确是 ML 平台开发。

[!note] 核心观点 AI 时代,L3 工程师的价值在于:架构决策能力 + 深度问题解决能力 + 业务理解能力。这些恰恰是 AI 做不好的。把 AI 当高效工具用,同时深耕这些核心能力。


架构选型的思考框架#

L3 阶段,你经常要做架构选型。这里提供一个思考框架:

Lambda 架构 vs Kappa 架构#

Lambda 架构:
     数据源

   ┌────┴────┐
批处理层   实时处理层
   └────┬────┘

     服务层

优点:批处理保证准确性,实时满足时效性
缺点:两套代码,维护成本高

Kappa 架构:
数据源 → 消息队列 → 实时处理 → 服务层

        重放(回溯)

优点:一套代码,架构简单
缺点:对实时引擎要求高,历史重算成本高
plaintext

如何选择?

  • 如果团队实时能力强,数据量不是特别大,Kappa 更简单
  • 如果需要复杂的批处理逻辑,或者需要经常回算历史,Lambda 更稳妥
  • 很多公司采用”伪 Lambda”:实时链路用 Flink,每天跑批任务修正数据

选型决策清单#

每次做技术选型时,问自己这些问题:

  1. 业务需求:时效性要求多高?数据量有多大?准确性要求多高?
  2. 团队能力:团队熟悉什么技术栈?能否支撑新技术的运维?
  3. 运维成本:这个技术生态是否成熟?出了问题能否快速定位?
  4. 可扩展性:未来数据量增长 10 倍,这个架构还能撑住吗?
  5. 成本:计算资源、存储资源、人力成本各是多少?

[!warning] 技术选型的陷阱 不要为了用新技术而用新技术。我见过很多团队,业务场景明明用 Hive 就够了,非要上 Flink;数据量明明不大,非要搞分布式。结果运维成本大增,效率反而下降。选型要基于问题,而不是基于技术流行度。


你可能会遇到的困难#

你的公司可能还是以离线为主,没有实时业务场景。

解决方案

  1. 主动找实时场景——实时监控大屏、实时推荐、实时风控,很多业务其实有需求,只是没人做
  2. 如果公司确实没有,可以考虑换一个有实时业务的平台历练
  3. 至少保持学习,技术储备在,机会来了才能抓住

”感觉自己只会 CRUD,没有架构能力”#

架构能力不是天生的,是在实践中培养出来的。

培养方法

  1. 每次设计前,先画架构图,和团队讨论
  2. 多看别人的系统是怎么设计的(开源项目、技术博客、架构书籍)
  3. 主动参与系统重构,这是最好的架构训练
  4. 复盘出过的问题,思考”如果重新设计,怎么避免这个问题"

"数据治理不知道从哪开始”#

数据治理是一个体系工程,不要指望一步到位。

建议的起步方式

  1. 从数据质量开始——先把关键表的质量检查做起来
  2. 建立基本的监控告警——任务失败、数据异常要能及时发现
  3. 梳理核心链路的血缘——至少知道核心报表是从哪些表算出来的
  4. 逐步完善,不要追求完美

”不确定要不要深入源码”#

源码阅读是一个争议话题。有人觉得必须读,有人觉得没必要。

我的建议

  • 不需要通读全部源码,那是不可能的任务
  • 但关键模块要理解——比如 Spark 的 Shuffle 实现、Flink 的 Checkpoint 机制
  • 遇到诡异问题时,源码是最终的答案
  • 如果想往架构师方向发展,源码阅读能力是必备的

L3 阶段可以胜任的岗位#

完成 L3 阶段的学习后,你可以胜任:

高级数据开发工程师

  • 主要工作:核心数据系统开发、性能优化、架构设计
  • 薪资参考:一线城市 35-55K,二线城市 25-40K
  • 面试重点:实时计算、性能调优、架构设计能力

实时计算工程师

  • 主要工作:实时数据链路建设、Flink/Kafka 集群运维
  • 特点:专注实时领域,技术深度要求高

数据架构师(初级)

  • 主要工作:数据平台架构设计、技术选型、标准制定
  • 特点:从执行转向规划,需要更广的技术视野

[!note] L3 的瓶颈 L3 是一个比较难突破的阶段。很多人会在这个阶段停留很长时间。突破的关键是:

  1. 不要只做自己熟悉的事,要主动接触新领域
  2. 培养系统性思维,从全局看问题
  3. 提升表达和沟通能力,好的架构需要”卖出去”

给 L3 学习者的真诚建议#

1. 深度和广度要平衡#

L3 阶段容易走两个极端:要么只钻一个方向,要么什么都想学。正确的做法是:在某一个领域(比如实时计算)建立深度,同时保持对其他领域的了解。

2. 从”解决问题”到”预防问题”#

L2 阶段你学会了解决问题,L3 阶段要学会预防问题。设计架构时,要思考:这个系统可能出什么问题?如何提前规避?

3. 开始建立影响力#

L3 阶段,你应该开始在团队内建立技术影响力:

  • 做技术分享,把你的经验传播出去
  • 写技术文档,让后来者少走弯路
  • 参与招聘,帮助团队识别人才
  • 指导新人,在教的过程中深化理解

4. 保持对业务的敏感度#

技术最终是为业务服务的。不要只顾着研究技术,要理解业务目标是什么、数据是如何产生价值的。能用技术解决业务问题的人,永远比只会技术的人更有价值。


接下来#

当你能够独立设计复杂的数据架构,有这样的困惑时:

  • “我应该如何规划整个公司的数据平台?”
  • “团队该怎么组建?流程该怎么设计?”
  • “数据平台的 ROI 应该怎么衡量?”
  • “新技术那么多,应该投入多少资源跟进?”

恭喜你,你已经准备好进入下一个阶段了。

➡️ L4:技术战略 —— 技术管理、平台规划、组织建设


相关资源

数据开发 L3:架构演进
https://blog.ss-data.cc/blog/data-engineer-l3-architecture
Author 石头
Published at 2025年1月5日
Comment seems to stuck. Try to refresh?✨