跳转至

Ch 17 Landing→Raw→Redshift 开发实战

面包屑

本书主页Part III 数据工程实践 › Ch 17

项目第 1-2 年 · 核心建设期——湖仓两跳开发(演进自三层 ETL)


本章你将学到

  • Landing→Raw(Glue)与 Raw→Redshift(SQL ELT)两跳职责与产物
  • 数据质量校验:约束声明与阈值治理(可在入仓前门禁)
  • 代理键生成与行数对账(源 / Landing / Raw / Redshift)
  • Schema 演进:防御式 Crawler diff vs 自适应 mergeSchema 的取舍

17.1 两跳开发的职责与产物

第 1 年我写过完整的 Landing→Raw→Enriched→COPY 三层 PySpark。第 1–2 年砍掉 S3 Enriched 后,开发面缩成两跳:湖上只标准化,仓内做业务变换(见 Ch 7/Ch 8)。

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
flowchart LR
    subgraph 两跳["湖仓两跳"]
        L2R@{ icon: "logos:aws-s3", form: "rounded", label: "Landing → Raw<br/>Glue 标准化", pos: "b", h: 40 }
        R2RS@{ icon: "logos:aws-redshift", form: "rounded", label: "Raw → enriched_*<br/>Spectrum SQL ELT", pos: "b", h: 40 }
    end

    L2R -->|产物| R_OUT[Parquet + Catalog<br/>字段名/类型标准化]
    R2RS -->|产物| RS_OUT[清洗/关联/质检<br/>脱敏/代理键 · 仓内 Gold]
classDef bpProcess  fill:#edf5ff,stroke:#0f62fe,stroke-width:2px,color:#161616
classDef bpData     fill:#d9fbfb,stroke:#007d79,stroke-width:2px,color:#161616
classDef bpSuccess  fill:#defbe6,stroke:#198038,stroke-width:2px,color:#161616

class L2R bpData
class R2RS bpProcess
class R_OUT bpData
class RS_OUT bpSuccess
linkStyle default stroke:#697077,stroke-width:2px

图 17-1 两跳开发的职责与产物

层间转换 职责 不做什么
Landing → Raw 格式标准化、字段名/类型统一、编码处理、更新 Catalog 不做业务清洗、不做关联
Raw → Redshift Spectrum 读 Raw;SQL 清洗/关联/质检/脱敏/代理键;写入 enriched_* 不在 S3 再写金层;不默认 COPY

表 17-1 两跳开发的职责与产物

Trade-off

早期三层的好处是"每层职责单一、可独立重跑",代价是数据写三次(含可重建的 Enriched)。现行两跳是湖写两次 + 仓内一次:合规追溯仍靠 Landing/Raw,Gold 只在 Redshift。湖侧其他引擎拿不到现成金层 Parquet;换来的是少一套 Glue 作业和一份对象存储。某域变换特别重时,我仍会局部开 Glue 特例,默认路径则是 SQL ELT。

落地示意如下。第一跳仍是 PySpark,第二跳是交给 Redshift Data API 的 SQL:

# 示意:Landing→Raw(Glue)+ SQL ELT 入仓
def run_lake_standardize(spark, config):
    # ① Landing → Raw:只做格式标准化
    raw = (spark.read.parquet(f"s3://aurora-cdp-landing/{config['domain']}/{config['table']}/")
               .withColumnRenamed("col_a", "hospital_id")
               .withColumn("biz_date", F.to_date("raw_ts")))
    (raw.write.mode("overwrite").partitionBy("batch_id")
         .parquet(f"s3://aurora-cdp-raw/{config['domain']}/{config['table']}/"))
    # Crawler / enableUpdateCatalog 更新 Glue Data Catalog

def run_sql_elt(redshift_data_api, config):
    # ② Raw(external) → enriched_*:清洗、脱敏、代理键、入仓
    sql = f"""
    INSERT INTO enriched_{config['domain']}.dim_hospital
    SELECT DISTINCT hospital_id,
           f_mask(patient_name) AS patient_name_masked,
           sha2(hospital_id || '||' || source_system, 256) AS sk,
           biz_date
    FROM ext_raw.{config['table']}
    WHERE batch_id = '{config['batch_id']}';
    """
    redshift_data_api.execute(sql)   # Glue Python Shell + boto3 Data API

17.2 数据质量校验框架:约束声明与阈值治理

质量校验架构

质检门禁可以挂在两处:Raw 写完后用 Glue/PyDeequ 扫一遍(大数据量时更适合 Spark 侧),或入仓 SQL 前后用 Redshift 约束/断言。我这边默认是入仓前门禁:不合格就不写 enriched_*

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
flowchart TB
    subgraph 质量校验框架["数据质量校验框架"]
        DECLARE[声明式约束<br/>在配置中声明质量规则]
        CHECK[校验引擎<br/>PyDeequ 或 SQL 断言]
        THRESHOLD[阈值治理<br/>通过率低于阈值则告警/阻断]
        ACTION[处置动作<br/>告警 / 隔离 / 阻断入仓]
    end

    DECLARE --> CHECK --> THRESHOLD --> ACTION
classDef bpProcess  fill:#edf5ff,stroke:#0f62fe,stroke-width:2px,color:#161616
class ACTION,CHECK,DECLARE,THRESHOLD bpProcess
linkStyle default stroke:#697077,stroke-width:2px

图 17-2 质量校验架构

# 示意:PyDeequ 声明式质量校验(也可改写为 Redshift SQL 断言)
from pydeequ.checks import Check, CheckLevel
from pydeequ.verification import VerificationSuite

def run_quality_checks(df, config):
    check = Check(spark, CheckLevel.Error, "raw_quality_gate")
    (check.isComplete("prescription_id")
          .isUnique("prescription_id")
          .isInRange("quantity", (1, 10000))
          .satisfies("end_date >= start_date", "date_consistency"))
    result = VerificationSuite(spark).onData(df).addCheck(check).run()
    pass_rate = result.checkMetrics.filter("constraint_status='Success'").count() / result.checkMetrics.count()
    if pass_rate < config["block_threshold"]:
        raise QualityGateError(f"质检通过率 {pass_rate:.2%} 低于阻断阈值,隔离数据")

质量约束类型

约束类型 说明 举例
完整性 字段非空 prescription_id IS NOT NULL
唯一性 主键不重复 COUNT(prescription_id) = COUNT(DISTINCT prescription_id)
范围性 值在合理范围内 quantity BETWEEN 1 AND 10000
一致性 跨字段逻辑一致 end_date >= start_date
引用性 外键引用有效 hospital_id EXISTS IN dim_hospital

表 17-2 质量约束类型

阈值治理

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
flowchart TD
    CHECK[执行质量校验] --> RESULT{通过率}
    RESULT -->|≥ 99%| PASS[通过,继续入仓]
    RESULT -->|95% - 99%| WARN[告警,继续入仓,记录异常]
    RESULT -->|< 95%| BLOCK[阻断入仓,隔离数据]

    classDef bpProcess  fill:#edf5ff,stroke:#0f62fe,stroke-width:2px,color:#161616
    classDef bpDecision fill:#fcf4d6,stroke:#f1c21b,stroke-width:2px,color:#161616
    classDef bpSuccess  fill:#defbe6,stroke:#198038,stroke-width:2px,color:#161616
    classDef bpError    fill:#fff1f1,stroke:#da1e28,stroke-width:2px,color:#161616
    classDef bpInfo     fill:#f6f2ff,stroke:#8a3ffc,stroke-width:2px,color:#161616

    class CHECK bpProcess
    class RESULT bpDecision
    class PASS bpSuccess
    class WARN bpInfo
    class BLOCK bpError
    linkStyle default stroke:#697077,stroke-width:2px

图 17-3 阈值治理

引申

质量校验框架可用 Amazon PyDeequ(Spark)或 Redshift SQL 断言。"约束即声明"比散落在代码里的手写校验好维护。若今天重新选型,Great Expectations 也是常见的开源替代。


17.3 代理键生成与行数对账

代理键生成

代理键生成从早期 Enriched 里的 Spark UDF,迁到了 SQL ELT 表达式sha2 / UDF)。同自然键得到同代理键,跨源才能对上。

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
flowchart LR
    subgraph 代理键生成策略["代理键生成策略"]
        NAT[自然键<br/>业务系统原有 ID] --> HASH[哈希函数<br/>SHA-256]
        HASH --> SUR[代理键<br/>统一格式 Surrogate Key]
        CONCAT[复合自然键<br/>多字段拼接] --> HASH
    end
classDef bpProcess  fill:#edf5ff,stroke:#0f62fe,stroke-width:2px,color:#161616
class CONCAT,HASH,NAT,SUR bpProcess
linkStyle default stroke:#697077,stroke-width:2px

图 17-4 代理键生成

策略 机制 优势 劣势
哈希代理键 SHA256(natural_key) 确定性、跨源可关联 哈希碰撞(概率极低)
自增序列 数据库自增 简单 跨系统不可关联
UUID 随机生成 无碰撞 不可追溯、索引差

表 17-3 代理键生成

平台用的是哈希代理键:对自然键(可能是复合键)做 SHA-256,输出确定。同一实体落在不同源系统里,只要自然键一致,代理键就一致。

行数对账

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#edf5ff','primaryTextColor':'#161616','primaryBorderColor':'#0f62fe','lineColor':'#697077','secondaryColor':'#d9fbfb','tertiaryColor':'#f2f4f8','fontSize':'14px'}}}%%
flowchart TB
    SRC_COUNT@{ icon: "devicon:microsoftsqlserver", form: "rounded", label: "源系统行数", pos: "b", h: 36 } --> COMPARE{对账}
    LANDING_COUNT@{ icon: "logos:aws-s3", form: "rounded", label: "Landing 行数", pos: "b", h: 36 } --> COMPARE
    RAW_COUNT@{ icon: "logos:aws-s3", form: "rounded", label: "Raw 行数", pos: "b", h: 36 } --> COMPARE
    RS_COUNT@{ icon: "logos:aws-redshift", form: "rounded", label: "Redshift 行数", pos: "b", h: 36 } --> COMPARE

    COMPARE -->|一致或符合预期去重| OK[对账通过]
    COMPARE -->|异常不一致| ALERT[告警<br/>定位丢失环节]
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 bpSuccess  fill:#defbe6,stroke:#198038,stroke-width:2px,color:#161616
classDef bpError    fill:#fff1f1,stroke:#da1e28,stroke-width:2px,color:#161616

class SRC_COUNT,LANDING_COUNT,RAW_COUNT,RS_COUNT bpData
class COMPARE bpDecision
class OK bpSuccess
class ALERT bpError
linkStyle default stroke:#697077,stroke-width:2px

图 17-5 行数对账

行数对账不花哨,但管用:每层记行数,层间比一下。Landing 1000、Raw 998?标准化丢了 2 行。Raw 与 Redshift 因去重对不上时,得对着配置看是不是预期行为。

# 示意:两跳行数对账(无 Enriched)
def reconcile(config, counts: dict):
    ddb.put_item(Item={"table": config["table"], "batch_id": config["batch_id"], **counts})
    if counts["landing"] != counts["raw"]:
        alert(f"{config['table']} Landing→Raw 丢失 {counts['landing']-counts['raw']} 行")
    if counts["raw"] != counts["redshift"] and not config.get("dedup_enabled"):
        alert(f"{config['table']} Raw→Redshift 行数变化,检查是否预期去重")

Trade-off

行数对账查不出"行数没变、内容却错了"。对"数据丢失"这类高频事故,性价比却很高。跟质量校验框架搭着用,各补一块盲区。

Schema 演进处理

数据源会变:加字段、改类型、删列。两跳 pipeline 若对 schema 变化没预案,常见结果是上游加了列被 ELT 静默丢掉,或类型一改入仓直接失败。

方案 机制 优势 劣势
A 防御式(Crawler diff) Glue Crawler 检测 DDL 变更 → diff → 人工确认 → 更新 target / ELT SQL 可控、变更可审查 人工介入,延迟上线
B 自适应(mergeSchema) Raw 读启用 mergeSchema=true;Catalog 同步后 ELT 跟列 自动化、无延迟 静默合并可能掩盖非预期变更

表 17-4 Schema 演进处理

Trade-off

平台默认走方案 A(防御式)。医药合规要求"任何 schema 变更可审查"。方案 B 我只放在非合规敏感域。删除列和类型变更必须告警:它们会直接打挂 Spectrum/ELT 或质检门禁。


本章小结

  • 两跳开发:Landing→Raw(Glue 标准化)/ Raw→Redshift(Spectrum SQL ELT);早期三层含 Enriched→COPY 已演进淘汰为默认路径
  • 质量校验:声明式约束 + 阈值治理;门禁挡在入仓前
  • 代理键用哈希策略;行数对账覆盖源 / Landing / Raw / Redshift
  • Schema 演进默认防御式 Crawler diff;破坏性变更必须告警

下一章

Ch 18 数据脱敏与隐私治理:数据入仓了,敏感字段怎么保护?下一章看脱敏怎么落在 SQL ELT 与仓内表上。

评论