界面已本地化;技能介绍与说明文档按所选语言由 AI 自动翻译,代码、命令、路径和专业术语保留原文,以原作者文档为准。
探索技能
数据与研究 · SKILL.md

data-pipeline-architecture

data-pipeline-architecture

【数据与研究】Implements data pipeline architectures (batch ETL/ELT, streaming, medallion bronze-silver-gold layers, data quality gates, schema evolution handling) for reliable data processing at scale.

独立 GitHub 快照 · 自动分类 · 以原作者文档为准

技能说明

GitHub

name: data-pipeline-architecture description: Implements data pipeline architectures (batch ETL/ELT, streaming, medallion bronze-silver-gold layers, data quality gates, schema evolution handling) for reliable data processing at scale. license: MIT compatibility: opencode archetypes:

  • tactical
  • orchestration anti_triggers:
  • brainstorming
  • vague ideation
  • long-form architecture response_profile: verbosity: low directive_strength: high abstraction_level: operational metadata: version: "1.0.0" domain: coding triggers: data pipeline, ETL architecture, batch processing, stream processing, data quality gates, schema evolution, Spark pipeline, how do i build a data pipeline role: implementation scope: implementation output-format: code content-types: [code, guidance, config, do-dont, examples] related-skills: event-driven-architecture, system-reliability-architecture, distributed-systems-architecture, database-design-patterns

Data Pipeline Architecture

Implements data pipeline architectures that reliably ingest, transform, and serve data across batch and streaming workloads. This skill makes the model design fault-tolerant pipelines with proper data quality enforcement, schema evolution handling, and observable failure recovery.

TL;DR Checklist

  • Choose batch, streaming, or hybrid based on freshness requirements (not technology preference)
  • Implement medallion architecture: bronze (raw) → silver (cleaned/validated) → gold (business aggregates)
  • Add data quality gates at every stage with explicit pass/fail handling
  • Handle schema evolution explicitly — never assume the source schema stays static
  • Design idempotent transformations for exactly-once semantics on reprocessing
  • Implement dead letter queue for records that fail validation
  • Add lineage tracking to record which transforms produced each output record

When to Use

  • Designing a new data platform or analytics pipeline from scratch
  • Migrating a legacy ETL system (cron scripts, SQL-only) to a modern batch/streaming pipeline
  • Implementing medallion architecture (bronze/silver/gold layers) for structured data governance
  • Adding data quality enforcement to an existing pipeline that processes untrusted sources
  • Handling schema changes from upstream systems without breaking downstream consumers
  • Building real-time or near-real-time streaming pipelines with Kafka, Flink, or Spark Structured Streaming

When NOT to Use

  • Simple one-off data extraction scripts — use a single Spark job or SQL query instead
  • OLTP database design — this covers analytical/data platform architecture, not transactional systems
  • Real-time application serving (user-facing APIs) — use api-architecture or cloud-native-architecture instead
  • Machine learning model training pipeline orchestration — focus only on data movement/transform, not model lifecycle

Core Workflow

  1. Define Data Contract — Identify source systems, ingestion frequency, schema expectations, and SLA requirements for freshness. Checkpoint: Document the schema as a typed contract (field names, types, nullability, constraints) that both producer and consumer agree on.

  2. Select Processing Mode — Choose batch, streaming, or hybrid based on data volume velocity, and freshness requirements:

    • Volume > 1TB/day or T+1 reporting → batch-first (Spark, dbt)
    • Latency < 5 minutes required → streaming (Kafka Streams, Flink)
    • Mixed workloads with different SLAs → hybrid (batch for historical, streaming for real-time) Checkpoint: Do not select streaming because it is "modern" — batch is simpler and cheaper for most workloads.
  3. Design Medallion Layers — Implement the three-tier architecture:

    • Bronze: Raw append-only storage, exact source fidelity, partitioned by ingestion time
    • Silver: Cleaned, deduplicated, typed data with quality gates applied; conformed to common schemas
    • Gold: Business-level aggregates optimized for analytics, denormalized where needed Checkpoint: Each layer must have its own schema registry and quality rules — never skip a layer.
  4. Implement Quality Gates — Add validation at bronze→silver and silver→gold transitions:

    • Not-null constraints on key fields
    • Type enforcement with explicit casting or rejection
    • Range checks (dates in the future, negative quantities)
    • Referential integrity against reference datasets Checkpoint: Define explicit policies for failed records — quarantine to dead letter queue vs. auto-fix with defaults.
  5. Handle Schema Evolution — Implement forward/backward compatibility:

    • Always add fields as nullable (forward compatible)
    • Never remove fields that downstream consumers may read (backward compatible)
    • Use schema registry with explicit versioning
    • Maintain a field deprecation timeline before dropping columns Checkpoint: Test every schema migration against existing consumer pipelines before promoting to production.
  6. Add Observability — Implement monitoring at every stage:

    • Row count comparison between layers (bronze count ≥ silver count after quality filtering)
    • Late data handling with watermark configuration for streaming
    • Alert on quality gate failure rate exceeding threshold
    • End-to-end latency tracking from source to gold

Implementation Patterns

Pattern 1: Medallion Architecture with Spark Structured Streaming

"""Medallion architecture layers — bronze (raw) → silver (cleaned) → gold (aggregated).

Each layer has explicit quality gates and idempotent processing.
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import (
    col, current_timestamp, to_timestamp, when, lit, countDistinct
)
from pyspark.sql.types import StructType, StructField, StringType, DoubleType, TimestampType


def read_bronze(spark: SparkSession, table_name: str) -> "pyspark.DataFrame":
    """Read raw data from bronze layer — append-only, exact source fidelity."""
    return spark.read.table(f"bronze.{table_name}")


def apply_quality_gates(
    df: "pyspark.DataFrame",
    required_fields: list[str],
    quality_threshold: float = 0.95,
) -> tuple["pyspark.DataFrame", int]:
    """Validate records and return clean dataframe + count of rejected records.

    Args:
        df: Input dataframe (typically from bronze layer).
        required_fields: Field names that must not be null for a record to pass.
        quality_threshold: Minimum fraction of records that must pass quality gates.

    Returns:
        Tuple of (cleaned_df, rejected_count). Raises if rejection rate exceeds threshold.
    """
    # Mark each record as passing or failing quality checks
    df_with_flag = df.withColumn(
        "_quality_status",
        when(
            all(col(f).isNotNull() for f in required_fields),
            lit("pass"),
        ).otherwise(lit("fail"))
    )

    total_count = df_with_flag.count()
    rejected_count = df_with_flag.filter(col("_quality_status") == "fail").count()
    pass_rate = (total_count - rejected_count) / max(total_count, 1)

    if pass_rate < quality_threshold:
        raise ValueError(
            f"Quality gate failed: {pass_rate:.2%} pass rate "
            f"(rejected {rejected_count}/{total_count}). "
            f"Investigate source data before reprocessing."
        )

    cleaned = df_with_flag.filter(col("_quality_status") == "pass").drop("_quality_status")
    return cleaned, rejected_count


def write_silver(
    spark: SparkSession,
    table_name: str,
    df: "pyspark.DataFrame",
) -> None:
    """Write cleaned data to silver layer with idempotent overwrite by partition."""
    df.write.mode("overwrite").partitionBy("ingestion_date").saveAsTable(f"silver.{table_name}")


def build_gold_aggregates(
    silver_df: "pyspark.DataFrame",
    group_keys: list[str],
    metrics: dict[str, str],
) -> "pyspark.DataFrame":
    """Build gold layer aggregates from silver data.

    Args:
        silver_df: Cleaned silver-layer dataframe.
        group_keys: Columns to group by for aggregation.
        metrics: Mapping of output column name → aggregate expression string.
            Example: {"total_revenue": "SUM(revenue)", "avg_order_value": "AVG(order_value)"}

    Returns:
        Aggregated gold-layer dataframe.
    """
    from pyspark.sql import functions as F

    agg_exprs = [F.expr(expr).alias(col_name) for col_name, expr in metrics.items()]
    return silver_df.groupBy(*group_keys).agg(*agg_exprs)


def process_pipeline(
    spark: SparkSession,
    source_table: str,
    required_fields: list[str],
    group_keys: list[str],
    metrics: dict[str, str],
) -> None:
    """End-to-end medallion pipeline: bronze → silver → gold.

    Demonstrates the full data quality and transformation flow.
    """
    # Bronze to Silver: read raw, apply quality gates, write cleaned
    bronze_df = read_bronze(spark, source_table)
    silver_df, rejected_count = apply_quality_gates(
        bronze_df, required_fields=required_fields, quality_threshold=0.95
    )

    if rejected_count > 0:
        # In production, write rejected records to a dead letter queue table
        print(f"Rejected {rejected_count} records — check dead_letter.{source_table}")

    write_silver(spark, source_table, silver_df)

    # Silver to Gold: aggregate for analytics
    gold_df = build_gold_aggregates(silver_df, group_keys=group_keys, metrics=metrics)
    gold_df.write.mode("overwrite").saveAsTable(f"gold.{source_table}_daily")

Pattern 2: Schema Evolution Handler (BAD vs. GOOD)

"""Schema evolution patterns — forward and backward compatibility."""

# ❌ BAD: Fragile schema — breaking changes cascade downstream
def process_raw_data(raw_row: dict) -> dict:
    """Assumes exact source schema; breaks on any field change."""
    return {
        "user_id": raw_row["userId"],          # Renamed → KeyError
        "email": raw_row["email_address"],     # Removed → KeyError
        "timestamp": raw_row["created_at"],    # Type changed (str → datetime) → TypeError
        "amount": float(raw_row["amount"]),    # No null safety
    }

# ✅ GOOD: Tolerant schema parser with explicit versioning and safe defaults
from typing import Any, Optional
from dataclasses import dataclass, field


@dataclass
class SchemaVersion:
    """Tracks schema version and maps source fields to canonical names."""
    version: int
    field_mappings: dict[str, str]       # canonical_name → source_field_name
    required_fields: list[str]
    type_coercions: dict[str, type]      # canonical_name → expected_type
    default_values: dict[str, Any] = field(default_factory=dict)

    def resolve(self, raw_row: dict) -> Optional[dict]:
        """Safely map raw row to canonical schema with error handling."""
        result = {}
        for canonical_name, source_field in self.field_mappings.items():
            if source_field not in raw_row:
                # Field was removed — use default or skip
                if canonical_name in self.required_fields:
                    return None  # Required field missing → reject
                result[canonical_name] = self.default_values.get(
                    canonical_name, None
                )
            else:
                value = raw_row[source_field]
                try:
                    if value is not None and self.type_coercions.get(canonical_name):
                        result[canonical_name] = self.type_coercions[
                            canonical_name
                        ](value)
                    else:
                        result[canonical_name] = value
                except (TypeError, ValueError):
                    # Type coercion failed — reject this record
                    return None
        return result


# Example usage with schema v1 → v2 migration
SCHEMA_V1 = SchemaVersion(
    version=1,
    field_mappings={
        "user_id": "userId",
        "email": "email_address",
        "created_at": "created_at",
        "amount": "amount",
    },
    required_fields=["user_id", "amount"],
    type_coercions={"user_id": int, "amount": float},
)

# V2 adds optional fields; V1 schema still works (backward compatible)
SCHEMA_V2 = SchemaVersion(
    version=2,
    field_mappings={
        "user_id": "userId",              # Same as V1
        "email": "email_address",         # Same as V1
        "created_at": "created_at",       # Same as V1
        "amount": "amount",               # Same as V1
        "country_code": "country_code",   # New optional field
        "plan_tier": "plan_tier",         # New optional field
    },
    required_fields=["user_id", "amount"],  # No change — no breaking impact on V1 consumers
    type_coercions={"user_id": int, "amount": float},
    default_values={"country_code": "US", "plan_tier": "free"},
)


def process_with_schema_evolution(raw_row: dict, version: int) -> Optional[dict]:
    """Process a record using the appropriate schema version.

    Args:
        raw_row: Raw data from upstream source.
        version: Schema version embedded in the source metadata.

    Returns:
        Canonical data record, or None if validation fails.
    """
    schemas = {1: SCHEMA_V1, 2: SCHEMA_V2}
    schema = schemas.get(version)
    if schema is None:
        # Unknown schema version — send to dead letter queue for investigation
        raise ValueError(f"Unsupported schema version: {version}")

    resolved = schema.resolve(raw_row)
    return resolved

Pattern 3: Streaming Pipeline with Kafka and Spark Structured Streaming

"""Streaming pipeline pattern — Kafka source → transformation → sink.

Uses watermarking for late data handling and checkpointing for fault tolerance.
"""
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, window, to_timestamp


def create_streaming_pipeline(
    spark: SparkSession,
    bootstrap_servers: str,
    topic: str,
    group_id: str,
    checkpoint_dir: str,
) -> None:
    """Build a streaming pipeline with proper watermarking and fault tolerance.

    Args:
        bootstrap_servers: Kafka broker connection string.
        topic: Source Kafka topic to consume from.
        group_id: Consumer group for exactly-once semantics.
        checkpoint_dir: HDFS/S3 path for Spark checkpoint state.
    """
    # Read stream from Kafka with schema inference disabled for safety
    streaming_df = (
        spark.readStream.format("kafka")
        .option("kafka.bootstrap.servers", bootstrap_servers)
        .option("subscribe", topic)
        .option("group.id", group_id)
        .option("startingOffsets", "latest")
        .option("maxOffsetsPerTrigger", "100000")  # Backpressure control
        .load()
    )

    # Parse JSON value and add watermark for late event handling (30-minute window)
    parsed = streaming_df.selectExpr("CAST(key AS STRING)", "CAST(value AS STRING)")

    from pyspark.sql.functions import from_json, col as spark_col
    from pyspark.sql.types import StructType, StructField, StringType, DoubleType, TimestampType

    value_schema = StructType([
        StructField("event_type", StringType(), True),
        StructField("timestamp", StringType(), True),
        StructField("amount", DoubleType(), True),
        StructField("user_id", StringType(), True),
    ])

    enriched = parsed.withColumn(
        "parsed",
        from_json(spark_col("value"), value_schema)
    ).select(
        spark_col("parsed.*"),
        spark_col("key").alias("partition_key"),
    ).withColumn(
        "event_time",
        to_timestamp(col("timestamp"))
    )

    # Apply watermark: drop events older than 30 minutes from the latest event seen
    watermarked = enriched.withWatermark("event_time", "30 minutes")

    # Aggregate in 5-minute windows with output mode that tracks state changes
    windowed_agg = watermarked.groupBy(
        window(col("event_time"), "5 minutes"),
        col("event_type"),
    ).agg(
        spark_col("COUNT(*)").alias("event_count"),
        spark_col("SUM(col('amount'))").alias("total_amount"),
    )

    # Write to sink (Delta Lake table with upsert mode)
    query = (
        windowed_agg.writeStream
        .format("delta")
        .outputMode("update")
        .option("checkpointLocation", checkpoint_dir)
        .trigger(processingTime="1 minute")
        .start("silver.streaming_aggregates")
    )

    # Block until streaming query terminates
    query.awaitTermination()

Constraints

MUST DO

  • Implement idempotent transformations so reprocessing produces identical results
  • Use append-only storage at the bronze layer — never overwrite raw source data
  • Maintain a schema registry with versioned schemas and explicit compatibility rules
  • Add quality gates before every silver→gold transition with configurable thresholds
  • Configure watermarks in streaming pipelines to handle late-arriving data
  • Implement dead letter queues for records that fail validation — never silently drop them
  • Track lineage: record which input files/records produced each output partition

MUST NOT DO

  • Store processed/transformed data as the source of truth — bronze layer is always the source of truth
  • Apply business logic at the bronze layer — keep raw data untouched and append-only
  • Use streaming for all workloads — batch is simpler, cheaper, and sufficient for most analytical queries
  • Bypass quality gates to "fix things faster" — a single bad record can corrupt downstream aggregations
  • Remove or modify records in the silver layer after they pass quality gates — always add corrective transformations as new records
  • Store schemas inline in code without versioning — schema drift will cause silent data corruption

Output Template

When designing a data pipeline, produce:

  1. Pipeline Architecture Diagram — ASCII diagram showing sources → bronze → silver → gold → consumers, with processing mode (batch/stream/hybrid) labeled on each stage
  2. Data Contract Specification — Table of source fields, canonical names, types, nullability, and constraints for the first 5 key tables
  3. Quality Gate Definition — List of validation rules per layer with rejection handling policy
  4. Schema Evolution Plan — Migration steps from current schema to target schema with backward/forward compatibility verification
  5. Implementation Code — Python/PySpark code for the primary transformation logic

Related Skills

SkillPurpose
event-driven-architectureEvent streaming infrastructure (Kafka, RabbitMQ) that powers data pipeline messaging
system-reliability-architectureResilience patterns (circuit breakers, retry strategies) for pipeline fault tolerance
distributed-systems-architectureConsistency models and partitioning strategies for distributed data storage
database-design-patternsDelta Lake/Iceberg/Hudi table formats that serve as the silver/gold storage layer

Live References

Authoritative documentation for data pipeline architecture patterns and tools. The model follows markdown links at load time to resolve external references and inline content.

我的收藏