跳转至

Ch 11 配置与状态管理

面包屑

本书主页Part II 架构设计 › Ch 11

项目第 0-1 年 · 架构设计期→核心建设期——配置驱动奠基


本章你将学到

  • 配置驱动架构的核心理念:运行时配置存"做什么",部署配置存"在哪跑"
  • 批次标识与增量水位管理——数据可追溯性的基石
  • 运行状态追踪与审计日志体系的设计

11.1 配置驱动架构:运行时配置存"做什么",部署配置存"在哪跑"

这是平台最重要的设计决策之一——如果全书只能保留一个设计思想,我会选这一个。

这个思想的萌芽来自企业征信项目。当时每接一个新数据源(工商/司法/税务),数据团队就要写一套新的 ETL 脚本——读源、清洗、转换、写目标。十个数据源就是十套脚本,虽然逻辑大同小异,但因为硬编码了表名、字段映射和连接信息,每套都得单独维护。数据源增加到二十个时,维护成本已经失控。

到了 Aurora,我发誓不再走老路。核心思路是:把"做什么"(业务逻辑:字段映射、加载模式、脱敏规则)从代码中提取出来,变成数据(配置);代码变成"通用引擎",按配置执行。 这样加新数据源不需要改代码,只需要加配置。但配置怎么管?放 Terraform 里?那每次加数据源都要走 plan/apply——太重了。最终方案是"双配置体系":运行时配置存 DynamoDB(热更新),部署配置存 Terraform(静态注入)。

这是平台最重要的设计决策之一:把"做什么任务"和"在哪跑任务"分成两个配置体系

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
flowchart TB
    subgraph 运行时配置["运行时配置(DynamoDB)—— 存'做什么'"]
        D1[数据源是什么]
        D2[字段映射规则]
        D3[同步策略:全量/增量]
        D4[目标表与 Schema]
        D5[脱敏规则]
    end

    subgraph 部署配置["部署配置(Terraform)—— 存'在哪跑'"]
        T1[哪个环境:dev/qa/prod]
        T2[哪个 Glue Job 脚本版本]
        T3[用什么 IAM Role]
        T4[分配多少 DPU]
        T5[调度时间表达式]
    end

    D1 -.->|运行时动态读取| RUNTIME@{ icon: "codicon:gear", form: "rounded", label: "Glue / Lambda 运行时", pos: "b", h: 40 }
    T1 -.->|部署时静态注入| INFRA@{ icon: "logos:aws", form: "rounded", label: "AWS 资源", pos: "b", h: 40 }

classDef bpProcess  fill:#edf5ff,stroke:#0f62fe,stroke-width:2px,color:#161616
classDef bpData     fill:#d9fbfb,stroke:#007d79,stroke-width:2px,color:#161616
classDef bpDecision fill:#fcf4d6,stroke:#f1c21b,stroke-width:2px,color:#161616
classDef bpInfo     fill:#f6f2ff,stroke:#8a3ffc,stroke-width:2px,color:#161616

class D1,D2,D3,D4,D5 bpData
class T1,T2,T3,T4,T5 bpInfo
class RUNTIME bpProcess
class INFRA bpInfo

linkStyle default stroke:#697077,stroke-width:2px

图 11-1 配置驱动架构:运行时配置存"做什么",部署配置存"在哪跑"

维度 运行时配置(DynamoDB) 部署配置(Terraform)
回答的问题 "做什么任务、什么映射、什么策略" "在哪个环境、跑哪个脚本版本、用什么资源"
读取时机 运行时动态读取(每次执行) 部署时静态注入(Terraform apply)
变更方式 配置发布流(热更新,无需重建资源) Terraform 发布流(需 plan/apply)
存储形式 DynamoDB JSON 文档 Terraform tfvars

表 11-1 配置驱动架构:运行时配置存"做什么",部署配置存"在哪跑"

为什么要分两套

Trade-off

如果只用 Terraform 管所有配置,那么"加一个数据源"就需要走 Terraform plan/apply 流程——审批、变更基础设施、等待部署。这太重了。把"做什么"放进 DynamoDB 后,加数据源只需要加一条 JSON 配置并发布——热更新、秒级生效、不碰基础设施。

配置注入链路

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
sequenceDiagram
    participant DDB as DynamoDB
    participant L as Lambda(参数处理)
    participant SF as Step Functions
    participant G as Glue Job

    L->>DDB: 按 domain+entity 读取配置
    activate DDB
    DDB-->>L: 返回任务配置 JSON
    deactivate DDB
    activate L
    L->>L: 解析配置,构造运行参数
    L->>SF: 触发状态机,注入参数
    activate SF
    SF->>G: 启动 Glue Job,传递参数
    activate G
    G->>G: 按配置执行 ETL
    deactivate G
    deactivate SF
    deactivate L

图 11-2 配置注入链路

这个配置注入链路,是我从企业征信的"硬编码注入"教训改良来的。企业征信时,ETL 脚本的参数(源表名、字段映射)直接写在 Python 脚本里——改参数要改代码、打包、部署,周期半天。到 Aurora 我把参数提取到 DynamoDB,Lambda 运行时动态读取注入——改参数只需改 DynamoDB 一条 JSON,秒级生效,不用动代码。这个"从硬编码到动态注入"的转变,让"加数据源"的周期从"半天"降到"分钟级"——这是配置驱动架构(M1)最直接的效率收益。

链路里有一个设计细节值得点出——配置是在 Lambda 层读取并注入的,不是在 Glue 层读取的。最初我让 Glue job 自己读 DynamoDB 配置——结果发现 Glue job 启动后第一件事是读配置,如果配置读失败,整个 job 白启动了(浪费 30-60 秒的 Spark 启动开销)。后来我改成 Lambda 先读配置、校验完整性,确认无误再触发 Glue job——配置错误在 Lambda 层(秒级、低成本)就拦住了,不会浪费 Glue 的启动开销。配置校验要在最轻量的层做——这是控制面/数据面分离(M6)在配置注入上的精细应用。

引申

配置驱动架构的本质是"数据化"——把原本硬编码在脚本里的业务逻辑(字段映射、同步策略、脱敏规则)提取为数据(JSON 配置),让代码变成"通用引擎",配置变成"业务声明"。这样加新数据源不需要改代码,只需要加配置。这是"开闭原则"(对扩展开放、对修改关闭)在数据工程中的体现。


11.2 批次标识与增量水位管理:数据可追溯性的基石

批次标识

每次数据加载生成一个唯一批次标识(时间戳),贯穿全链路:

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
flowchart LR
    GEN@{ icon: "codicon:calendar", form: "rounded", label: "批次标识生成<br/>时间戳", pos: "b", h: 40 } --> LANDING@{ icon: "logos:aws-s3", form: "rounded", label: "Landing 层路径", pos: "b", h: 40 }
    GEN --> RAW@{ icon: "logos:aws-s3", form: "rounded", label: "Raw 层分区", pos: "b", h: 40 }
    GEN --> ENR@{ icon: "logos:aws-s3", form: "rounded", label: "Enriched 层分区", pos: "b", h: 40 }
    GEN --> META@{ icon: "logos:aws-redshift", form: "rounded", label: "Redshift 元数据表<br/>加载历史", pos: "b", h: 40 }
    GEN --> DDB@{ icon: "logos:aws-dynamodb", form: "rounded", label: "DynamoDB<br/>任务状态", pos: "b", h: 40 }

classDef bpProcess  fill:#edf5ff,stroke:#0f62fe,stroke-width:2px,color:#161616
classDef bpData     fill:#d9fbfb,stroke:#007d79,stroke-width:2px,color:#161616
classDef bpInfo     fill:#f6f2ff,stroke:#8a3ffc,stroke-width:2px,color:#161616

class GEN bpProcess
class LANDING,RAW,ENR,DDB bpData
class META bpInfo

linkStyle default stroke:#697077,stroke-width:2px

图 11-3 批次标识

批次标识解决两个问题: 1. 版本管理:同一张表每次加载是独立版本,可按时间回溯 2. 故障恢复:失败后可以从指定批次重跑

批次标识用时间戳(如 20260618-001500)而非 UUID,这个选择是有考量的。UUID 唯一性好但"不可读"——排障时看到 batch_id=a3f7b2c1 完全不知道是哪次加载。时间戳既唯一(精确到分钟)又可读——20260618-001500 一看就知道是 6 月 18 日 00:15 的加载。我在企业征信时用过 UUID,排障时要在 DynamoDB 里反查"这个 UUID 对应哪次加载",多一步。到 Aurora 换成时间戳,排障时看 batch_id 就知道时间——可读性是可运维性的基础

但时间戳有一个边界情况——同一分钟内多次加载会冲突。我在第一年遇到过:某次重跑和正常调度在同一分钟触发,两个 batch_id 相同,数据混在一起。解决方案是在时间戳后加序号(20260618-001500-01/-02),同一分钟内的多次加载用序号区分。唯一性靠"时间戳+序号"组合保证,可读性靠时间戳保证——两全其美。

增量水位管理

对于增量加载的数据源,平台维护一个水位表,记录"上次加载到哪里":

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
flowchart TD
    LAST[上次水位<br/>last_sync_timestamp] --> QUERY[本次查询<br/>WHERE update_time > 上次水位]
    QUERY --> NEW[获取增量数据]
    NEW --> UPDATE[更新水位<br/>本次最大 update_time]
    UPDATE --> NEXT[下次查询从新水位开始]

classDef bpData     fill:#d9fbfb,stroke:#007d79,stroke-width:2px,color:#161616
classDef bpProcess  fill:#edf5ff,stroke:#0f62fe,stroke-width:2px,color:#161616
classDef bpSuccess  fill:#defbe6,stroke:#198038,stroke-width:2px,color:#161616
classDef bpDecision fill:#fcf4d6,stroke:#f1c21b,stroke-width:2px,color:#161616
classDef bpInfo     fill:#f6f2ff,stroke:#8a3ffc,stroke-width:2px,color:#161616

class LAST bpData
class QUERY bpProcess
class NEW bpSuccess
class UPDATE bpProcess
class NEXT bpInfo

linkStyle default stroke:#697077,stroke-width:2px

图 11-4 增量水位管理

加载模式 水位管理 适合场景
全量加载 不需要水位 数据量小、或源表每次全量覆盖
增量加载 按时间戳/自增 ID 追踪水位 数据量大、有可靠变更标识
自定义加载 按业务逻辑定义 特殊场景(如按状态变更)

表 11-2 增量水位管理

增量水位管理看似简单——"记住上次加载到哪,下次从那继续"——但我在第一年踩过两个水位坑。第一个坑是水位更新时机:最初我在"数据加载后"立即更新水位,结果有次 Glue job 在加载后、更新水位前崩溃了——数据进了 Landing 但水位没更新,下次重跑时又拉了一遍重复数据。修复方案是"水位更新放在整个事务最后",且加幂等校验(重跑时检查目标表是否已有该批次数据)。水位更新必须是事务的最后一步,且要幂等——否则崩溃恢复时会产生重复数据。

第二个坑是水位回退:有次源系统的 update_time 因为时区配置错误,新数据的 update_time 比旧数据小——水位"倒退"了,增量查询 WHERE update_time > 水位 漏掉了那些"时间戳变小"的数据。这个坑花了两周才发现(报表数字对不上,排查到源系统时区问题)。修复方案是"定期全量校准"——每周跑一次全量加载对比增量,发现差异告警。增量加载不能只信水位,要定期用全量校验兜底——这是"增量为主+全量校准"双保险策略的由来。

Trade-off

增量加载性能好但正确性依赖"可靠的水位标识"。如果源表的 update_time 不可靠(比如批量更新时时间戳相同),可能导致数据遗漏。全量加载简单可靠但性能差。对于关键业务数据,建议"增量加载为主 + 定期全量校准"的双保险策略。


11.3 运行状态追踪与审计日志体系

状态追踪

每个任务的执行状态实时回写到 DynamoDB,形成"可观测的执行面板":

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
flowchart LR
    SF[Step Functions 执行] -->|每步完成| LAM[Lambda 状态回写]
    LAM --> DDB[DynamoDB 状态表]
    DDB --> |查询| DASH[运维状态面板]

    subgraph 状态表字段["状态表记录的关键信息"]
        F1[任务标识]
        F2[批次标识]
        F3[执行状态:running/success/failed]
        F4[开始时间/结束时间]
        F5[行数统计]
        F6[错误信息]
    end

classDef bpProcess  fill:#edf5ff,stroke:#0f62fe,stroke-width:2px,color:#161616
classDef bpData     fill:#d9fbfb,stroke:#007d79,stroke-width:2px,color:#161616
classDef bpInfo     fill:#f6f2ff,stroke:#8a3ffc,stroke-width:2px,color:#161616

class SF bpProcess
class LAM bpProcess
class DDB bpData
class DASH bpInfo
class F1,F2,F3,F4,F5,F6 bpData

linkStyle default stroke:#697077,stroke-width:2px

图 11-5 状态追踪

状态存 DynamoDB 而非 Redshift,这个选择是性能特性决定的。状态回写是"高频低量"——每个任务每步都写一次状态,一天可能几千次写入;而 Redshift 是列式 MPP 数仓,擅长"低频大量"的分析查询,不擅长高频单行写入(每行写入都要 commit,IO 开销大)。DynamoDB 是键值数据库,单行写入毫秒级,天然适合高频状态回写。我在企业征信时犯过这个错——把状态写进 PostgreSQL(也是行式数据库),高频写入导致锁竞争,状态回写反而成了性能瓶颈。到 Aurora 我把状态放 DynamoDB、审计历史放 Redshift——热状态用 KV 存储,冷审计用列式存储,各取所长(M6 治理与执行分离在存储层的体现)。

状态表的六个字段(任务标识/批次标识/执行状态/时间/行数/错误信息)不是随便选的,每个都对应一个排障场景。"执行状态"让运维一眼看到"哪个在跑、哪个失败了";"行数统计"让排障时能判断"这次加载的数据量是否正常"——如果平时 10 万行突然变成 100 行,即使状态是 success 也值得警觉;"错误信息"让排障不用翻 CloudWatch 日志就能看到失败原因。状态表的设计目标是"运维查一个表就能完成 80% 的排障"——剩下的 20% 才需要翻详细日志。

审计日志体系

Redshift 中维护一套元数据表,记录全链路审计信息:

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
erDiagram
    LOAD_HISTORY ||--o{ AUDIT_LOG : "关联批次"
    LOAD_HISTORY ||--o{ DDL_LOG : "关联表"

    LOAD_HISTORY {
        string batch_id PK
        string domain
        string entity
        timestamp load_time
        int row_count
        string status
    }
    AUDIT_LOG {
        string log_id PK
        string batch_id FK
        string event_type
        timestamp event_time
        string detail
    }
    DDL_LOG {
        string log_id PK
        string table_name
        string ddl_type
        timestamp change_time
        string sql_text
    }

图 11-6 审计日志体系

元数据表 记录内容 用途
加载历史 每次数据加载的批次、域、实体、行数、状态 追溯数据来源
审计日志 全链路事件(开始/完成/失败/告警) 排障与合规
DDL 变更日志 Schema 变更记录(建表/改列/删表) Schema 演进追溯

表 11-3 审计日志体系

这三张元数据表(加载历史/审计日志/DDL 变更日志)是我在企业征信"无审计"教训后建的。企业征信时没有 DDL 变更日志——有次某张表的列被改了(加了列、改了类型),但没人知道是谁改的、什么时候改的,下游 ETL 全崩,排障花了一天才定位到是 DDL 变更导致。到 Aurora 我把 DDL 变更日志作为一等公民——任何 ALTER TABLE/CREATE TABLE 自动记录到 ddl_log 表,包含操作者、时间、SQL 文本。这个设计在第二年的一次排障中救了我们:某张表突然多了一列,下游 ETL 报错——查 ddl_log 一分钟就定位到"是某个开发者在调试时手动 ALTER 了表",恢复 DDL 后五分钟修复。没有 DDL 日志,排障靠猜;有 DDL 日志,排障靠查

审计日志还有一个我在第四年才充分体会到的价值——它是 GxP 审计的核心交付物。GxP ALCOA+ 要求"数据可追溯到产生者"(见 Ch 1 表 1-2),审计员要看的就是"谁在什么时候对哪张表做了什么操作"。audit_logddl_log 正好提供了这些——操作者、时间、操作类型、详情。如果当时没有建这套日志,第四年 GxP 审计时要么通不过,要么要花几个月"补造"日志。审计日志不是为了排障建的,它是合规的基础设施——排障是短期收益,合规是长期收益(M10 合规从第一天嵌入)。

引申

审计日志是"被动血缘"——它记录"发生了什么",但不记录"数据的流向关系"。主动血缘(如 OpenLineage/DataHub)会在任务执行时主动收集"输入表→输出表"的映射。我们在 Ch 20 会详细对比这两种方案。


本章小结

  • 配置驱动架构:运行时配置(DynamoDB)存"做什么",部署配置(Terraform)存"在哪跑"——加数据源只需加配置,无需改代码或重建基础设施
  • 批次标识贯穿全链路,实现版本管理与故障恢复;增量水位管理实现高效增量加载
  • 运行状态实时回写 DynamoDB 实现可观测;Redshift 元数据表(加载历史/审计日志/DDL 变更)实现合规追溯
  • 审计日志是"被动血缘",与主动血缘(OpenLineage/DataHub)各有优劣

下一部分

Part III 数据工程实践:连接器与流水线 —— 架构骨架搭完了,接下来进入"怎么开发":从配置驱动的任务模型,到五类连接器,到三层 ETL 开发实战。

评论