ETL基础

第02课:ETL管道设计

数据工程实战课程 · ETL基础阶段

🔧 ETL管道设计

ETL(Extract-Transform-Load)是数据工程最基础的模式。一个设计良好的ETL管道是可靠数据流的基石。

ETL vs ELT

维度ETLELT
转换时机加载前转换加载后转换
计算资源ETL服务器数仓弹性计算
适用场景脱敏/复杂清洗云数仓/dbt

🐍 Python实战:核心实战

# 构建可扩展的ETL管道框架
import time, random
from datetime import datetime
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from collections import defaultdict

@dataclass
class PipelineContext:
    data: list = field(default_factory=list)
    metadata: dict = field(default_factory=dict)
    metrics: dict = field(default_factory=dict)

class PipelineStep(ABC):
    def __init__(self, name): self.name = name
    @abstractmethod
    def execute(self, context): pass

class ExtractStep(PipelineStep):
    def __init__(self, name, count=200):
        super().__init__(name)
        self.count = count
    def execute(self, context):
        records = []
        for i in range(self.count):
            records.append({'id':i+1,'value':round(random.uniform(10,1000),2),
                'category':random.choice(['A','B','C','D']),
                'status':random.choice(['active','inactive','pending'])})
        context.data.extend(records)
        context.metrics['extracted'] = len(records)
        return context

class CleanStep(PipelineStep):
    def __init__(self, name, rules):
        super().__init__(name)
        self.rules = rules
    def execute(self, context):
        cleaned = []
        removed = defaultdict(int)
        for r in context.data:
            skip = False
            for fld, rule in self.rules.items():
                if fld in r:
                    val = r[fld]
                    if rule.get('min') is not None and isinstance(val,(int,float)) and val < rule['min']:
                        removed[f'{fld}_low'] += 1; skip = True; break
                    if rule.get('valid') and val not in rule['valid']:
                        removed[f'{fld}_invalid'] += 1; skip = True; break
            if not skip: cleaned.append(r)
        context.data = cleaned
        context.metrics['cleaned'] = len(cleaned)
        context.metadata['removed'] = dict(removed)
        return context

class AggregateStep(PipelineStep):
    def __init__(self, name, group_by, agg_field):
        super().__init__(name)
        self.group_by = group_by
        self.agg_field = agg_field
    def execute(self, context):
        groups = defaultdict(list)
        for r in context.data:
            groups[r.get(self.group_by,'unknown')].append(r.get(self.agg_field,0))
        context.data = [{self.group_by:k,'total':round(sum(v),2),'count':len(v),'avg':round(sum(v)/len(v),2)}
                       for k,v in groups.items()]
        context.metrics['groups'] = len(context.data)
        return context

class ETLPipeline:
    def __init__(self, name):
        self.name = name
        self.steps = []
        self.context = PipelineContext()
    def add_step(self, step): self.steps.append(step); return self
    def run(self):
        print(f"启动管道: {self.name} ({len(self.steps)}步骤)")
        for step in self.steps:
            start = time.perf_counter()
            inp = len(self.context.data)
            self.context = step.execute(self.context)
            out = len(self.context.data)
            ms = (time.perf_counter()-start)*1000
            print(f"  ✅ {step.name}: {inp}→{out} ({ms:.1f}ms)")
        return self.context

pipeline = ETLPipeline("订单处理管道")
pipeline.add_step(ExtractStep("提取订单", 200))
pipeline.add_step(CleanStep("数据清洗", {'value':{'min':0},'status':{'valid':['active','inactive','pending']}}))
pipeline.add_step(AggregateStep("分类聚合", 'category', 'value'))
result = pipeline.run()
print(f"最终: {len(result.data)}组")
for r in result.data[:4]:
    print(f"  {r}")
print("✅ 验证通过 - ETL管道框架运行正常")

📐 管道设计模式

模式图示场景
线性A→B→C→D简单顺序
分支A→[B,C]→D同源不同处理
汇聚[A,B]→C→D多源Join

🐍 管道可靠性:重试、幂等、断点续传

# 管道可靠性:重试、幂等、断点续传
import time, random
from functools import wraps

def retry(max_attempts=3, backoff_base=2):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts+1):
                try: return fn(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts: raise
                    wait = backoff_base ** attempt + random.uniform(0,1)
                    print(f"  第{attempt}次失败,{wait:.1f}s后重试: {e}")
                    time.sleep(wait)
        return wrapper
    return decorator

class IdempotentLoader:
    def __init__(self): self.loaded_ids = set(); self.dedup = 0
    def load(self, records, id_field='id'):
        new = []
        for r in records:
            rid = r.get(id_field)
            if rid not in self.loaded_ids: self.loaded_ids.add(rid); new.append(r)
            else: self.dedup += 1
        print(f"  幂等加载: {len(records)}输入, {len(new)}新增, {self.dedup}去重")
        return new

class Checkpoint:
    def __init__(self): self.done = set(); self.state = {}
    def mark(self, name, state=None): self.done.add(name); self.state[name] = state
    def is_done(self, name): return name in self.done

@retry(max_attempts=3, backoff_base=1)
def unreliable_api():
    if random.random() < 0.5: raise ConnectionError("API超时")
    return [{"id":1,"data":"ok"}]

print("可靠性机制测试")
print("1. 重试:")
try: print(f"  结果: {unreliable_api()}")
except: print("  最终失败")
print("2. 幂等:")
loader = IdempotentLoader()
loader.load([{'id':i,'v':i*10} for i in range(1,11)])
loader.load([{'id':i,'v':i*10} for i in range(1,6)])
print("3. 断点续传:")
ckpt = Checkpoint()
ckpt.mark('extract', {'offset':1000})
print(f"  extract完成: {ckpt.is_done('extract')}")
print("✅ 验证通过 - 可靠性机制运行正常")
💡 实战建议:动手实现本课代码示例,修改参数观察输出变化。生产环境还需考虑错误处理、监控告警和数据安全。

🎯 构建多源管道

  1. 实现MergeStep
  2. 实现BranchStep
  3. DAG验证

🎯 设计电商ETL

  1. 多源:订单+用户+商品
  2. 清洗→关联→聚合

🏆 第2课成就解锁

🎁 下一课预告:数据提取技术!

📖 原理深入:ETL设计核心原则

数据管道设计五原则

  1. 幂等性原则:同一输入无论执行多少次,输出结果完全一致。这是重试机制的基础,也是管道可恢复性的保证。
  2. 原子性原则:一个批次的数据要么全部成功,要么全部回滚。使用事务或两阶段提交确保原子性。
  3. 可观测性原则:每个步骤都有明确的输入输出指标、执行日志和错误追踪。当管道出现问题时,能在5分钟内定位到故障点。
  4. 可扩展性原则:管道能水平扩展以应对数据量增长。避免单点瓶颈,使用分区并行处理,设计无状态的计算节点。
  5. 自愈性原则:管道能自动处理常见故障,无需人工干预即可恢复。

ETL性能优化清单

优化项方法预期效果
I/O优化批量读写、列式存储、压缩3-10x
计算优化向量化执行、谓词下推、列裁剪2-5x
并行化分区并行、流水线并行线性扩展
缓存维度表缓存、结果缓存2-10x
增量处理只处理变更数据10-100x

🏆 最佳实践:生产环境ETL检查清单

🔬 深度案例:生产环境实战经验

踩坑与解决方案

在生产环境中,我们经常遇到教科书不会告诉你的问题。以下是常见的实战经验和解决方案:

监控与运维指标

指标告警阈值监控方式
管道延迟超过SLA 120%端到端时间戳追踪
数据量偏差日均偏差超过30%与历史同期对比
错误率超过0.1%错误计数/总记录数
数据新鲜度超过SLA 150%最新数据时间戳
资源利用率CPU持续>80%系统监控
⚠️ 常见反模式
💡 架构演进建议:不要一开始就追求完美架构。从MVP开始,随业务增长逐步演进。每次演进都要回答三个问题:当前瓶颈是什么?解决它的投入产出比如何?是否为后续扩展留了空间?

📚 延伸阅读与参考

推荐资源