数据工程设计模式——Lambda架构

101 阅读15分钟

引言(Introduction)

本章将深入探讨 Lambda 架构这一数据工程模式,并使读者熟悉如何用该模式构建解决方案。内容将涵盖 Lambda 模式能够解决的用例;还将基于开源与云技术讲解如何设计采用 Lambda 的系统,并通过示例应用与代码片段加以演示。此外,我们也会介绍 Lambda 模式在真实场景中的应用实例。

结构(Structure)

本章涵盖以下主题:

  • Lambda 架构模式的用例
  • 基于 Lambda 模式的系统设计
  • 构建 Lambda 架构系统的技术选型
  • 真实案例

目标(Objectives)

在本章结束时,你将对 Lambda 架构模式有深入理解;也能够基于该模式设计并构建数据管道,并编写代码落地该设计。你将了解应选择的技术栈,以及如何将各组件“缝合”起来,构建端到端的 Lambda 架构管道。最后,你还将理解哪些真实业务场景适合采用该模式,并看到来自 金融科技(Fintech) 的示例。

Lambda 架构模式的用例(Use cases for Lambda architecture pattern)

Lambda 架构是一种复杂的数据工程模式,应当谨慎、少量地使用。它可以同时结合实时系统批处理系统的优势,但实现成本较高。适用于那些“既要实时又要批处理”的投入产出比(ROI)可以被证明合理的领域。该模式包含 速度层(speed) 、**批处理层(batch)服务层(serving)**三层。

机器学习模型的创建与打分(Machine learning model creation and scoring)

Lambda 架构可用于同时满足“离线构建 ML 模型”与“基于实时数据进行打分”的系统需求。训练模型的关键在于干净且定义明确的历史数据训练框架。历史数据由 批处理层提供;速度层使用批处理层产出的模型进行实时打分服务层综合实时与批量打分,为业务决策提供洞察。下面详细说明该系统的构建方式。

批处理层(Batch layer)

为批量训练机器学习模型,需要先将事务源系统的数据汇入数据湖:可按周期从事务系统卸数到 S3HDFS 等廉价存储。数据入湖后,由批量 Spark 作业进行清洗与特征提取(即将原始数据中有价值的字段加工成模型输入所需的形式)。可见,Lambda 的批处理层并非单一作业,而是一组流水线作业:抽取 → 清洗 → 特征工程。

特征准备好后,即可使用 TensorFlow / PyTorch 等训练框架进行批量训练:在某个时间点抽取一批特征值送入训练;模型以增量方式构建,最终得到版本化的可用模型。随着数据演进,抽取/清洗/特征/训练这一批式流程可周期性重复,产出不同版本,使模型能够反映数据分布的变化。下图展示:通过批量抽取把源数据入湖、用 Spark 清洗并抽取特征、再用训练框架进行训练(图 6.1)。

image.png

图 6.1:用于特征工程的批量抽取(Batch extraction for feature engineering)

速度层(Speed layer)

速度层利用批处理层产出的模型进行实时打分。在打分前,需要对事务系统生成的数据进行实时处理并提取特征。通常做法是:在源库上使用 CDC(变更数据捕获)Kafka Connect + Kafka 等消息技术实时抽取数据;通过 SMT(Single Message Transforms) 对抵达 Kafka 的每条消息应用业务转换,提取打分所需特征,并将其发送给模型进行推断。随着批处理层持续产出新模型版本,速度层需跟随并使用最新版本进行打分。下图展示了速度层如何从源系统持续获取数据进行实时打分(图 6.2)。

image.png

图 6.2:实时推理(Real-time inferencing)

服务层(Serving layer)

服务层将批处理层的历史视图速度层的实时分值相结合,对外提供统一视图,以支持业务决策。通过融合两者,服务层帮助用户做出更为准确的预测与判断。

具有历史偏好的实时数据分析(Real-time data analysis with historical bias)

当用于分析的数据恰到好处时,分析才真正有力。根据用例不同,所需数据可能偏历史,也可能偏当前。历史数据一般通过批处理提供,当前数据则由实时系统提供;但两者并非互斥。在某些用例中,同时具备整理好的历史数据实时数据会显著增强分析能力。
一个简单例子是银行联络中心坐席的数据服务:当客户因借记交易失败致电时,拥有该笔交易的实时数据可以立刻解决客户当下问题;同时掌握客户历史交易信息则可借机提供定制化产品(如定期存款、保险)。

要构建该系统的速度层、批处理层与服务层,做法如下。首先看速度层:在目标分析系统上建立两张表——speed 表batch 表,分别存储实时与批量数据。batch 表每日装载一次,speed 表则随着源事务系统的变化持续更新

正如“实时数据工程模式”一章所述,从事务系统搬运数据到实时分析系统通常使用CDC:事务系统的每次变更写入变更日志,源端 Kafka 连接器读取该日志并写入 Kafka topic;分析系统的汇端连接器(sink)消费消息,转换为相应的 INSERT/UPDATE/DELETE,并应用到目标 speed 表(图 6.3)。

image.png

图 6.3:使用 CDC 的速度层(Speed layer using CDC)

batch 表可在日终由 speed 表派生而来。通常,batch 表会在处理后更为规范与整洁,并非简单拷贝。每天结束时,可由 Spark 作业读取 speed 表、整备数据并写入 batch 表,随后截断 speed 表以避免在服务层合并时出现重复数据。下图展示了如何在日终用速度层作为批处理层的数据来源(图 6.4)。

image.png

图 6.4:批处理层从速度层摄取(Batch layer ingestion from speed layer)

接着定义服务层:其职责是合并 batchspeed 两层的数据并对外提供分析能力,构建方式有两种:

  • 逻辑服务层:在查询时通过**视图(view)**联结现有表(适用于对服务时延不苛刻的场景)。
  • 物理服务层:预先将数据**合并(必要时预聚合)**到服务表中(适用于对查询时延要求很低的场景)。

下图分别给出两种服务层的示意:

image.png

图 6.5:物理数据服务层(Physical data serving layer)

image.png

图 6.6:逻辑数据服务层(Logical data serving layer)

采用 Lambda 模式设计系统(Designing system with a Lambda pattern)

本节我们将设计并构建一个 Lambda 系统,通过将历史数据实时数据相结合来完成数据分析。演示中以 MySQL 为源系统,ClickHouse 为目标系统。

速度层(Speed layer)

速度层通过 Kafka 源连接器从 MySQL 实时抽取数据(读取 CDC 变更日志),并把数据推送到 Kafka topics;随后由 ClickHouse 汇(sink)连接器消费这些消息,将其转换为 INSERT / UPDATE / DELETE 操作,写入 ClickHouse 的 speed 表。下图展示了从 MySQL 到 ClickHouse 的基于 Kafka 的实时装载(图 6.7):

image.png

图 6.7:基于 CDC 的速度层(Speed layer using CDC)

本章末尾 “Setup instructions” 小节给出了配置 Kafka 连接器以搬运数据的步骤。

批处理层(Batch layer)

批处理层按从 MySQL 表抽取数据,在 Apache Spark 作业中进行整理与加工(curate),并写入 ClickHouse 的 batch 表。Spark 分别通过 JDBC 连接器读取 MySQL、写入 ClickHouse。可用 cron 调度每日的批处理任务。下图演示了将数据从 MySQL 搬运到 ClickHouse 的批处理作业(图 6.8):

image.png

图 6.8:带 ETL 的批处理层(Batch layer with ETL)

系统包含一张名为 sales_table 的 MySQL 表,存放当月销售数据。表中 transaction_ts 列保存交易时间戳。Spark 程序以天为批读取、处理,并加载到 ClickHouse 目标表以供分析。

该程序需要 MySQLClickHouseJDBC 驱动(可从各自的下载页获取)。

还需在系统中安装 Spark,可执行:

brew install apache-spark

在创建 MySQL 表之前,需要安装并启动 MySQL 服务(详见官方文档)。同理,创建 ClickHouse 数据库与表之前,也需要在本地或云端完成 ClickHouse 的安装/开通。

以下步骤与代码演示如何为批处理层将数据从 MySQL 搬运到 ClickHouse:

1)创建 MySQL 数据库

CREATE DATABASE sales_db;

2)连接并创建月度交易数据表

USE sales_db;
CREATE TABLE sales_table (
    transaction_id INT AUTO_INCREMENT PRIMARY KEY,
    transaction_amount DECIMAL(10, 2) NOT NULL,
    product_id INT NOT NULL,
    location_id INT NOT NULL,
    transaction_ts TIMESTAMP NOT NULL
);

3)向 MySQL 载入示例数据

LOAD DATA INFILE '/tmp/sales_table_january_2024.csv' INTO TABLE sales_table
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 LINES
(transaction_id, transaction_amount, product_id, location_id, transaction_ts);

4)在 ClickHouse 上创建数据库

CREATE DATABASE sales_db;

5)连接并创建用于分析的 ClickHouse 表

USE sales_db;
CREATE TABLE sales_table (
    transaction_id UInt32,
    transaction_amount Decimal(10, 2),
    product_id UInt32,
    location_id UInt32,
    transaction_ts Timestamp
)
ENGINE = MergeTree()
ORDER BY transaction_id;

6)按日搬运数据的 Spark 作业

from pyspark.sql import SparkSession

# Create a SparkSession
spark = SparkSession.builder \
    .appName("MySQL to Clickhouse") \
    .getOrCreate()

# Setup the MySQL JDBC connection properties
mysql_url = "jdbc:mysql://localhost:3306/sales_db"
mysql_properties = {
    "user": "root",
    "password": "MyPassword",
    "driver": "com.mysql.cj.jdbc.Driver"
}

# Query to batch query the day's data in MySQL
query = "(SELECT transaction_id, transaction_amount, product_id, location_id, transaction_date FROM sales_table where  transaction_ts> DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 10 SECOND)

# Read from MySQL daily data table into DataFrame
mysql_df = spark.read.jdbc(url=mysql_url, table=query, properties=mysql_properties)

# Setup ClickHouse JDBC connection properties
clickhouse_url = "jdbc:clickhouse://url:port/default"
clickhouse_properties = {
    "user": "default",
    "password": "MyPassword",
    "ssl": "true",
    "driver": "com.clickhouse.jdbc.ClickHouseDriver"
}

# Write daily data from MySQL to Clickhouse in a batch
mysql_df.write \
    .mode("append") \
    .jdbc(url=clickhouse_url, table="sales_table", properties=clickhouse_properties)

# Stop the SparkSession
spark.stop()

7)用 cron 调度 Spark 作业

crontab -e
0 22 * * * spark-submit --jars "/tmp/jdbc_dir/mysql-connector-j-9.0.0.jar,/tmp/jdbc_dir/clickhouse-jdbc-0.7.0.jar" spark_program.py

服务层(Serving layer)

在 ClickHouse 中,可直接对 speed 表batch 表创建视图进行合并(如 UNION),对外呈现为单一实体。若查询时延敏感,可将两张表合并落到一张物理表,以避免在视图中做合并开销。最终选择逻辑视图还是物化/物理表,取决于应用的可接受时延:

  • 低时延(如实时反欺诈、实时推荐)通常需要物化
  • 传统批量 BI / 线下会员忠诚度等场景可用逻辑视图

在 ClickHouse 中创建 Serving 视图:

create view serving_view as
select * from batch_table
union
select * from speed_table;

Lambda 系统的技术选型(Technologies for Lambda systems)

Lambda 系统的技术组合 = 批处理 + 实时。由于同时利用批处理层与速度层,其所需技术也更为多样;服务层则根据逻辑/物理形态采用不同技术。

  • 批处理层:一次处理大量数据,要求能在无瓶颈下横向扩展。历史上常用 Hadoop MapReduce(HDFS 作为临时/持久存储),但其资源利用效率不佳;现代系统更多采用 Apache Spark(可从失败点重启,性能与可靠性更优)。存储侧,对象存储正逐步成为默认选择,取代 HDFS。
  • 批作业调度/编排Apache Airflow 以跨系统适配性与 DAG 建模能力成为强选择;简单场景可用 Linux Cron
  • 速度层:需要高吞吐 + 低延迟的消息投递与流处理,常见技术包括 Apache Kafka、Apache Flink、Spark Streaming。其中 Kafka 因连接器生态丰富而最为常用,提供至少一次投递语义,显著简化实时应用开发。
  • 速度层数据库:需高吞吐、低延迟访问,常见选择有 Redis、Couchbase、Aerospike。小规模、可完全驻内存的场景优先 Redis;数据规模更大且既需内存级服务又需持久化时,倾向 Couchbase(持久化存储 + 集成缓存)。
  • 服务层:可为逻辑(基于视图,无需额外技术)或物理(需要把 batch 与 speed 数据合并,通常需 SparkKafka 协同)。

下图给出了 Lambda 架构的数据层与各层适配的技术词云(图 6.9):

image.png

图 6.9:Lambda 架构中的数据层与适用技术

真实世界示例(Real-world examples)

本节将讨论一个在 金融科技(Fintech) 领域中利用 Lambda 架构 的真实案例。该方案同时利用 Lambda 的速度层批处理层,为银行支持分析人员提供客户交易数据的当前视图历史视图。这一独特架构使同一系统既能支撑客服职能,也能承载交叉销售等能力。

金融科技(Fintech)

在 Fintech 中,可以用 Lambda 模式构建复杂的客户支持系统,同时兼顾会员忠诚/营销等玩法。其中一个例子是“客户交易可视化系统”,由银行联络中心(Contact Center)的支持人员使用,为客户提供及时支持。由于该支持系统为银行员工提供了与客户交流的机会,因此还能基于客户历史交易信息提供定制化的银行解决方案,以创造更好的体验。Lambda 架构非常契合该问题:速度层为联络中心人员提供实时交易信息批处理层提供客户交易的历史视图

首先看如何为此类客服系统构建速度层。银行交易通常运行在大型机系统上(以保证可靠性与可用性)。需要将这些大型机系统中的数据实时抽取到 Oracle 或 DB2 LUW 等二级系统,供联络中心人员查询。这可以通过 Kafka 与主机 CDC 连接器实现,或使用 IBM 提供的主机数据抽取工具(如 IBM Change Data Capture)。IBM CDC 是 IBM 的商用标准软件,能从包括主机上的 DB2 在内的多种数据源抽取数据。通过读取主机系统的变更日志抽取数据后,再以 Insert/Update/Delete 的方式写入 Oracle 或 DB2 LUW。

下图展示速度层架构(图 6.10):

image.png

图 6.10:主机卸载的速度层(Speed layer for mainframe offload)

接下来构建批处理层:每日从实时速度层抽取数据,导入批处理表,随后截断速度层表。注意,这里并不是再次从主机抽取来构建批处理层,而是使用速度层对批处理层进行增量填充。这种做法称为对表的前向填充(forward population) 。仅在需要加载历史数据时,才会再次使用主机进行所谓的回填(back population) ,可借助 IBM InfoSphere DataStage 等工具实现。回填即将历史数据装载到表中的过程。

不在前向填充时直接读取主机的原因是:主机成本通常按 MIPS(百万指令/秒)计费,每次从主机读取数据都会增加 MIPS 成本。利用速度层作为批处理层的数据来源可以降低整体系统成本。MIPS 成本往往占比显著,采用下述架构可为项目带来可观的成本节省。

下图展示批处理层架构(图 6.11):

image.png

图 6.11:主机卸载的批处理层(Batch layer for mainframe offload)

最后,服务层可以是在 Oracle 或 DB2 LUW 中对速度表批处理表创建的简单视图,联络中心人员通过该视图即可同时查看历史数据实时交易数据

Kafka 部署与连接器设置(Kafka setup instructions)

以下是为 MySQLClickHouse 分别设置 Kafka 源/汇连接器的步骤:

下载并安装 Kafka 集群:

wget https://dlcdn.apache.org/kafka/3.9.0/kafka_2.12-3.9.0.tgz
tar -xvf kafka_2.12-3.9.0.tgz
mv kafka_2.12-3.9.0 /opt/kafka
cd /opt/kafka

启动 Zookeeper 与 Kafka Broker:

/opt/kafka/bin/zookeeper-server-start.sh /opt/kafka/config/zookeeper.properties
/opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/server.properties

创建 Kafka topic:

/opt/kafka/bin/kafka-topics.sh \
  --create --topic <topic> \
  --bootstrap-server <broker-node-list> \
  --replication-factor <num-replicas> \
  --partitions <partition-count>

下载 MySQL Kafka 连接器并配置:

wget https://repo1.maven.org/maven2/io/debezium/debezium-connector-mysql/3.0.6.Final/debezium-connector-mysql-3.0.6.Final-plugin.tar.gz
mkdir -p /opt/kafka/plugins/mysql
tar -xvzf debezium-connector-mysql-3.0.6.Final-plugin.tar.gz -C /opt/kafka/plugins/mysql

(注:原文给的是 .jartar -xvzf 组合,此处按 Debezium 官方发布的打包形式示例为 .tar.gz。)

启动 Kafka Connect:

/opt/kafka/bin/connect-standalone.sh connector.properties

创建 MySQL 源连接器配置文件(示例 JSON):

{
  "name": "debezium-mysql-connector",
  "config": {
    "connector.class": "io.debezium.connector.mysql.MySqlConnector",
    "tasks.max": "1",
    "database.hostname": "host",
    "database.port": "port",
    "database.user": "debezium",
    "database.password": "password",
    "database.server.id": "1",
    "database.server.name": "mysql_server",
    "database.include.list": "database-name",
    "table.include.list": "table-name",
    "database.history.kafka.bootstrap.servers": "ip:port",
    "database.history.kafka.topic": "topic-name",
    "include.schema.changes": "true",
    "snapshot.mode": "initial"
  }
}

部署 MySQL 源连接器:

curl -X POST -H "Content-Type: application/json" \
  --data @<mysql-config-file> \
  http://ip:port/connectors

创建 ClickHouse 汇连接器配置文件:

{
  "name": "clickhouse-sink-connector",
  "config": {
    "connector.class": "com.altinity.clickhouse.sink.connector.ClickHouseSinkConnector",
    "tasks.max": "1",
    "topics": "<kafka_topic_name>",
    "clickhouse.url": "http://host:port",
    "clickhouse.database": "database-name",
    "clickhouse.username": "username",
    "clickhouse.password": "password",
    "clickhouse.table.name": "clickhouse-table",
    "key.converter": "org.apache.kafka.connect.storage.StringConverter",
    "value.converter": "org.apache.kafka.connect.json.JsonConverter",
    "value.converter.schemas.enable": "false",
    "batch.size": "1000",
    "insert.mode": "insert"
  }
}

部署 ClickHouse 汇连接器:

curl -X POST -H "Content-Type: application/json" \
  --data @<clickhouse-config-file> \
  http://ip:port/connectors

结语(Conclusion)

本章介绍了基于 Lambda 架构 的常见用例,并设计了一个方案:将批处理层的历史数据与速度层的实时数据相结合以进行数据分析。我们还看到 Lambda 系统如何将批处理技术栈实时技术栈组合落地;并了解了 Fintech 领域中相关系统的架构示例,看到 Lambda 架构如何在该领域构建差异化解决方案。

下一章将介绍 ETL 与 ELT 设计模式(最基础的数据工程模式),并回顾其在不同行业中的用例与实践。