《传统Hadoop大数据技术入门:补充与进阶------从数据格式到智能决策的问答实录》
------写给已读《传统Hadoop大数据技术入门:从数据源到用户界面的全链路实践指南》的读者
封面语
第一本书带你走完了"从数据源到用户界面"的全链路。但真正的生产环境,远不止一条笔直的管道。
本书以一问一答的方式,深入剖析了第一本书中"一笔带过"或"简化处理"的21个真实问题:
- 原始输入真的只是文本日志吗?Protobuf数据如何处理?
- Logstash采集到巨大日志后,分布式计算如何保证每一行数据的完整性?
- Spark SQL到底能查什么、不能查什么?
- HBase和ClickHouse是平行概念吗?大屏展示时先查谁、后查谁?
- LLM时代,传统BI如何升级为"智能决策"?
这是一本"问题驱动"的进阶指南。每一个问题都源自真实的架构设计纠结,每一个答案都经过一线生产实践的检验。
前言:为什么还需要一本"补充图书"?
第一本书《传统Hadoop大数据技术入门:从数据源到用户界面的全链路实践指南》的使命是:用一条清晰的数据流,串起传统大数据技术栈的全部组件。 这本书完成了它的使命------你知道了Kafka、Spark Streaming、HDFS、Hive Metastore、Spark SQL、HBase、ClickHouse、Airflow、Spring Boot、React之间是如何协作的。
但第一本书为了保持"链路清晰",有意或无意地简化/省略了许多真实生产环境中的关键细节:
- 原始输入真的只是JSON/文本日志吗?Protobuf、Avro等二进制格式怎么处理?
- 写入Kafka的内容,真的需要自己写Java Producer吗?Logstash是干什么的?
- 当Spark并行读取HDFS上的大文件时,如何保证每一行数据不被截断?
- HDFS上的
.parquet文件到底长什么样?扩展名有意义吗? - Spark SQL真的只查原始数据吗?ODS/DWD/DWS/ADS是什么意思?
- HBase和ClickHouse在架构层级上是什么关系?大屏场景下先查谁?
- 商业智能中的"主动感知"应该由ETL负责吗?LLM来了之后怎么变?
本书就是为解决这些问题而生的。
阅读方式:本书完全采用"一问一答"的形式。每一个问题都是独立的,你可以跳过已熟悉的内容,直接跳到你感兴趣的章节。但如果你想彻底通透,建议按顺序阅读------因为后面的问题往往建立在前面的答案之上。
第1章:数据源与采集格式的真相
问题1:原始输入真的只是文本日志(JSON/XML)吗?Protobuf数据怎么处理?
背景:第一本书假定数据源是JSON格式的文本日志,但这在真实生产环境中只是"被转换后"的结果。真正的原始二进制流量远比这复杂。
答案:
真实情况是:来自终端的原始物理输入(如Protobuf、Thrift、Avro二进制流),在进入大数据管道之前,大部分系统会通过API服务器或网关,主动将其"降级"为JSON文本日志。
但这并非唯一路径。实际上存在三条并行路径:
| 路径 | 处理方式 | 适用场景 | 占比 |
|---|---|---|---|
| 路径A(主流) | 终端(Protobuf) → API服务器 → 打印JSON日志 → Filebeat采集 → Kafka | 业务数据(订单/用户行为),追求稳定性和可调试性 | ~70% |
| 路径B(高性能) | 终端(Protobuf) → 网关直接序列化为Avro → Kafka | 实时性要求极高的场景,跳过落盘环节 | ~20% |
| 路径C(埋点直传) | App/小程序SDK缓存 → 批量HTTP上报 → 收集服务 → Kafka/HDFS | 前端埋点(点击、曝光),不经过业务API | ~10% |
关键认知 :公司内部职责划分决定了格式的多样性------通讯协议(Protobuf)是后端开发负责的,目的是快;数据仓库的原始日志(JSON)是大数据开发负责的,目的是稳、全、可读。 两者在API服务器层完成了"翻译"。
关于Protobuf入湖的处理:
如果你的Kafka中直接流入了Protobuf二进制数据(路径B),在Spark Streaming/Flink消费时,不能像JSON那样用get_json_object解析,而是需要使用对应的Protobuf Deserializer。
伪代码示例(Spark Streaming消费Protobuf):
java
// 导入Protobuf定义(编译自 .proto 文件)
import com.example.SensorLog.SensorData;
// 自定义反序列化函数
public static class ProtobufDeserializer implements DeserializationFunction<byte[], SensorData> {
@Override
public SensorData deserialize(byte[] bytes) throws Exception {
return SensorData.parseFrom(bytes); // Protobuf 原生 parseFrom
}
}
// Spark Streaming 中使用
Dataset<Row> df = spark.readStream()
.format("kafka")
.option("kafka.bootstrap.servers", "kafka-cluster:9092")
.option("subscribe", "sensor-protobuf-topic")
.load()
.selectExpr("CAST(value AS BINARY) as proto_bytes") // 读取为二进制
.map((Row row) -> {
byte[] bytes = row.getAs("proto_bytes");
SensorData data = SensorData.parseFrom(bytes); // 解析Protobuf
// 提取字段并转为Row
return RowFactory.create(data.getSensorId(), data.getValue(), data.getTimestamp());
}, ...);
但当数据最终写入HDFS作为离线存储时,强烈建议转换为Parquet格式(路径A的优势在于JSON可以直接通过Hive SerDe读取,而Protobuf需要额外处理)。
总结一句话:Protobuf是高效的"传输格式",JSON是便于调试的"中间格式",Parquet是高性能的"存储格式"。三者各司其职,通过API服务器或网关完成转换。
问题2:写入Kafka的内容,主流方案是自写Java代码还是Logstash等框架?
背景 :第一本书的1.3节直接写了Java版本的KafkaProducer,这容易让读者误以为生产环境都是这样做的。
答案:
你的理解完全正确。在生产环境中,直接写Java代码往Kafka生产日志的做法极为罕见。主流方案是使用专业日志采集框架(Logstash、Fluentd、Filebeat等)。
为什么不用自写Java代码?
| 痛点 | 自写Java Producer的问题 | 专业框架的解决方案 |
|---|---|---|
| 多数据源 | 每个业务系统都要写一套Producer代码 | Filebeat/Fluentd以DaemonSet运行在每台服务器,统一采集 |
| 容错机制 | 需要自己实现重试、指数退避、磁盘缓冲 | 框架内置,开箱即用 |
| 资源消耗 | 每个JVM进程开销大 | Filebeat/FluentBit内存占用仅~10MB |
| 运维成本 | 每次改配置要重新编译发布 | 只需修改YAML配置文件,热加载 |
| 生态集成 | 需要手动对接Kafka的认证、压缩、分区策略 | 框架已支持全部Kafka特性 |
生产环境的典型分工(以ELK/EFK栈为例):
┌─────────────────────────────────────────────────────────────────────────┐
│ 边缘采集层 (Edge Collectors) --- 轻量级,运行在每台业务服务器 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Filebeat / Fluent Bit │ │
│ │ - 监听 /var/log/app/*.log 文件变化 │ │
│ │ - 只做一件事:读取新行,发送到Kafka(或直接发送到Logstash) │ │
│ │ - 内存占用约10MB,CPU占用<5% │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────┬───────────────────────────────────────┘
│ ① 发送原始日志
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 消息缓冲层 (Message Broker) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Kafka (可选,如果Logstash直接消费则不经过Kafka) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────┬───────────────────────────────────────┘
│ ② 消费日志(或直接从Filebeat接收)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 中心处理层 (Central Processors) --- 强大的数据加工工厂 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Logstash │ │
│ │ - 职责:复杂的日志解析(Grok)、字段提取、类型转换、数据脱敏 │ │
│ │ - 示例配置: │ │
│ │ filter { │ │
│ │ grok { match => { "message" => "%{TIMESTAMP_ISO8601:ts}" }│ │
│ │ mutate { convert => { "amount" => "float" } } │ │
│ │ drop { if => [message] !~ /INFO/ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────┬───────────────────────────────────────┘
│ ③ 输出结构化数据
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 存储层 │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Elasticsearch (用于检索) / HDFS (用于归档) / ClickHouse (分析) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Filebeat的配置文件示例(filebeat.yml):
yaml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/app/*.log
multiline.pattern: '^\d{4}-\d{2}-\d{2}' # 合并多行日志(如Java Stack Trace)
multiline.negate: true
multiline.match: after
output.kafka:
hosts: ["kafka1:9092", "kafka2:9092"]
topic: "app-log-topic"
partition.round_robin:
reachable_only: false
compression: gzip
max_message_bytes: 1000000
# 不需写任何Java代码!运维只需维护此YAML文件
总结:第一本书的Java Producer仅用于本地开发和测试。生产环境的日志采集,由Filebeat/Fluentd(边缘采集)+ Logstash(中心处理)+ Kafka(缓冲)组成的管道完成,全程不需要写一行Java代码。
第2章:分布式存储与计算的底层细节
问题3:Logstash采集到一个巨大的日志文件,ETL时能否分布式加速?行截断问题怎么解决?
背景:第一本书假设Spark可以并行读取HDFS上的文件,但没有解释其底层机制。读者容易疑惑:如果文件在本地磁盘,单节点读取岂不成了瓶颈?
答案:
首先明确角色定位 :Logstash是采集端,运行在单台服务器上,确实只能由一个进程读取本地磁盘的日志文件(无法分布式)。
你问的"分布式ETL加速"发生在下一个阶段:当文件已经从本地磁盘上传到HDFS或S3之后,由Spark/Hive/MapReduce读取时发生。
HDFS/Spark解决"行截断"问题的机制如下:
核心问题:HDFS将大文件物理切割为128MB的Block,分布在多台DataNode上。Spark启动多个Task并行读取这些Block。但Block的物理边界恰好切在了一行日志的中间------如果Task直接读取,就会得到不完整的行。
解决方案:逻辑分片(Logical Split)+ 边界溢出(Read-ahead)+ 起始跳过(Skip)
text
【物理存储】HDFS Block 边界
文件物理布局:
┌─────────────────────────────────────────────────────────────────────────┐
│ Block 1 (物理位置: 0 ~ 128MB) │
│ 第1行 (完整) │
│ 第2行 (完整) │
│ ... │
│ 第N行 (完整) │
│ 第N+1行 的数据只写了 50% (这部分在 Block 1 的末尾) ←截断点 │
└─────────────────────────────────────────────────────────────────────────┘
│
│ 真正的分割线在此
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Block 2 (物理位置: 128MB ~ 256MB) │
│ (Block 1末尾被截断的第N+1行的剩余50%数据) ←被截断的部分 │
│ 第N+2行 (完整) │
│ 第N+3行 (完整) │
└─────────────────────────────────────────────────────────────────────────┘
═══════════════════════════════════════════════════════════════════════════
【Spark/Hadoop InputFormat 的处理】
═══════════════════════════════════════════════════════════════════════════
Task 1 (处理 Block 1,物理位置 0~128MB):
① 创建输入流,读取 0 ~ 128MB 的数据。
② 开始逐行解析,遇到 \n 就切分一行。
③ 读到 Block 1 末尾时,发现最后一行(第N+1行)没有换行符。
④ 🛡️ 【越界读取】不截断!继续向 Block 2 读取额外数据,
直到遇到第一个 \n,把第N+1行的后半部分(在Block2里的内容)也读过来。
⑤ 将完整的第N+1行作为 Task 1 的最后一条记录输出。
⑥ 记录:实际读取了 128MB + X 字节(X = 第N+1行在Block2的占用量)。
Task 2 (处理 Block 2,物理位置 128MB~256MB):
① 创建输入流,从物理位置 128MB 开始读取。
② 🛡️ 【跳过开头】它不会直接把第1个字节当作新行的开始。
③ 先跳过 (Skip) Block 2 开头的不完整行(即第N+1行后半部分)。
④ 寻找 Block 2 中的第一个换行符 \n,从这个 \n 之后的下一个字节开始正式处理
(即从第N+2行开始)。
⑤ 这样,Task 1 和 Task 2 处理的行数据,既没有重复,也没有遗漏。
通俗比喻:切西瓜的"保险边界"
想象一个超长的竹简,上面写满了连续的汉字(没有标点)。
- 物理切块(HDFS):用菜刀强行把竹简每隔1米砍断(Block边界)。
- 逻辑分片(Spark) :
- Executor 1 拿到第一段。它发现切口的最后一个字只写了一半(被截断),就"越过"切口,从第二段竹简上借来剩下的半句话,拼成一句完整的话,然后归自己处理。
- Executor 2 拿到第二段。它发现开头有几个字已经被Executor 1借走了,于是它从这几个字后面的第一个句号(即换行符)开始读,剩下的作为自己的范围。
通过这种 "多借一点 + 跳过开头" 的协作机制,两个Executor完美地瓜分了所有完整的内容。
特殊情况:如果日志不是按"行"分隔,而是二进制无分隔符(如Protobuf原始流)怎么办?
| 文件格式 | 是否可分割(Splittable)? | 说明 |
|---|---|---|
| Text/JSON/CSV(按\n分隔) | ✅ 是 | 依赖换行符,InputFormat自动处理边界溢出 |
| Avro(含同步标记Sync Marker) | ✅ 是 | Avro文件内部有固定的同步标记,可从任意位置找到下一个完整Block |
| Parquet(含内嵌元数据) | ✅ 是 | Parquet文件包含Row Group边界,Spark可并行读取不同Row Group |
| 纯Protobuf二进制流(无分隔符) | ❌ 否 | 无分隔符,无法确定行的边界,Spark只能启动1个Task顺序读取 |
总结 :分布式并行读取是完全可行的,且行截断问题由Spark底层的TextInputFormat自动处理,对开发者完全透明。但前提是数据格式必须是"可分割"的(如Text/JSON/Avro/Parquet)。如果是纯二进制无分隔符流,则丧失了并行能力。
问题4:HDFS的"默认存储格式"是什么?扩展名(如.parquet)有什么作用?
背景:第一本书多处提到写入Parquet格式,但读者容易误解:是不是HDFS本身有"默认格式"这个概念?
答案:
HDFS本身没有任何"默认存储格式"。它只是一个文件系统,存的是纯粹的二进制字节流,它不关心你存的是文本、图片还是视频。
"存储格式"是由写入数据的工具(Spark、Hive、Flume)决定的,"扩展名"在HDFS上几乎没有实际作用,仅仅是一个"给人看的标签"。
各工具写入HDFS时的默认存储格式:
| 写入工具 | 默认格式 | 说明 |
|---|---|---|
| Hive(老版本) | TextFile(纯文本) | 若不指定STORED AS,默认纯文本,每行一条记录 |
| Hive(新版本/ICF) | ORC | 列式存储,优化查询性能 |
| Spark SQL(默认) | Parquet | 通过spark.sql.sources.default配置,绝大多数生产环境为Parquet |
| Spark Streaming | 需显式指定 | 若不指定format(),可能是JSON/CSV(视DataFrame类型而定) |
扩展名(如.parquet、.log、.avro)的实际作用:
在Windows上,扩展名决定用什么软件打开。但在**HDFS(Linux生态)**上,扩展名只是一个约定俗成的字符串,操作系统和HDFS本身完全忽略它。
- 对HDFS :完全没有意义,你把Parquet文件命名为
.txt,HDFS照样存储。 - 对人 :看到
.parquet就知道是列式存储,看到.log就知道是纯文本,方便人工排查。 - 对工具(有限制) :某些API(如
spark.read.load("path"))会根据扩展名自动选择解析器。但生产代码中通常显式指定.format("parquet"),此时扩展名可有可无。
一个颠覆认知的事实 :Spark写入HDFS时,生成的文件名默认是part-00000-xxx.snappy.parquet。但如果把.parquet后缀删掉,Spark依然能正常读取(只要代码中声明了格式)。
生产环境的文件命名规范(最佳实践):
part-{taskId}-{uuid}.{compressionCodec}.{format}
示例:part-00000-8f9a2b3c.snappy.parquet
part:表示计算任务输出的分片{taskId}:哪个Task生成的(00000=第一个任务){uuid}:唯一标识,防止重名.snappy:使用Snappy压缩算法.parquet:存储格式为Parquet
最重要的区别:存储格式 ≠ 文件扩展名
| 概念 | 定义 | 示例 |
|---|---|---|
| 存储格式(Format) | 数据在磁盘上的物理排列方式(行式/列式/有无索引/有无统计) | Parquet(列式+内嵌Schema)、ORC(列式+索引)、TextFile(行式纯文本) |
| 文件扩展名(Extension) | 一个字符标签,无实际语义 | .parquet、.log、.avro |
终极总结 :写代码时永远显式指定format(),不要依赖扩展名或系统默认,这是防止数据沼泽的第一步。
java
// ✅ 正确做法
df.write().format("parquet").save("/path/table")
// ❌ 错误做法(依赖Spark默认配置,不同环境可能不同)
df.write().save("/path/table")
问题5:Parquet文件到底是什么?它的发音是什么?
背景:第一本书频繁提及Parquet,但没有详细解释其内部结构。
答案:
发音 :/pɑːrˈkeɪ/ ("帕-凯" )。重音在第二音节"凯"上,像"par " + "kay"。源自法语,原意"镶木地板",形象地比喻其数据存储"结构紧凑"的特点。
Parquet是什么 :
Apache Parquet是一种开源的列式存储文件格式。是Apache顶级项目,也是大数据领域事实上的标准格式之一。
三大核心特征:
| 特征 | 说明 | 带来的收益 |
|---|---|---|
| 列式存储 | 数据按列(而非按行)组织和存储 | 查询时只需读取相关列,I/O减少10~100倍 |
| 自描述结构 | 每个文件内嵌Schema(数据结构元数据) | 独立于外部系统,可被任何支持Parquet的工具解析 |
| 高效压缩 | 同一列数据类型相同,可针对性压缩 | 存储空间节省50%~80%(相比文本格式) |
Parquet文件内部结构:
┌─────────────────────────────────────────────────────────────────────────┐
│ Parquet 文件结构 │
├─────────────────────────────────────────────────────────────────────────┤
│ 文件头 (Magic Number: "PAR1") │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 行组 1 (Row Group 1) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 列块: col1 │ │ 列块: col2 │ │ 列块: col3 │ ← 按列重组 │ │
│ │ │ (含页Page) │ │ (含页Page) │ │ (含页Page) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ ├─────────────────────────────────────────────────────────────────┤ │
│ │ 行组 2 (Row Group 2) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ 列块: col1 │ │ 列块: col2 │ │ 列块: col3 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ 文件尾 (Footer) │
│ - 包含完整Schema定义 │
│ - 包含每个行组/列块的元数据(行数、压缩类型、统计信息:min/max) │
│ - 包含文件长度和Magic Number │
└─────────────────────────────────────────────────────────────────────────┘
为什么Parquet能极大提升查询性能?
- 列裁剪(Projection Pushdown) :
SELECT user_id, amount只需读取这两个列的文件片段,跳过其他列。 - 谓词下推(Predicate Pushdown) :
WHERE dt = '2026-07-16'通过Footer中的统计信息(min/max),直接跳过不包含该日期的行组。 - 向量化读取:同一列的数据连续存放,CPU缓存命中率高,适合SIMD指令批量处理。
总结 :Parquet是一个"为分析而生"的文件格式。它通过列式存储+内嵌元数据+高效压缩,让Spark SQL查询速度比文本格式快10~100倍,同时节省大量存储空间。这也是为什么ETL后一定要转换为Parquet格式的根本原因。
第3章:Spark SQL的完整职能与数仓分层
问题6:Spark SQL是不是只查原始数据?ODS/DWD/DWS/ADS是什么意思?
背景:第一本书的讨论集中在"Spark SQL读取原始JSON做ETL",容易让读者误以为Spark SQL只在这一环节起作用。
答案:
你的误解完全合理------因为第一本书为了让你快速理解"入湖"过程,确实把焦点聚焦在了"从原始JSON解析字段"这一步。但实际上,Spark SQL是整个离线数仓的"通用计算引擎",它贯穿了数据加工的每一个环节。
Spark SQL的完整职能:
Spark SQL的核心职责是作为"离线数据加工引擎(ETL)",它负责读取ODS原始数据并逐层转化为DWD/DWS/ADS,最终导出至HBase/ClickHouse供业务查询。
数仓分层的全称与含义:
| 层级 | 全称 | 中文译名 | 数据特征 | 存储格式 |
|---|---|---|---|---|
| ODS | O perational D ata Store | 操作数据存储层(贴源层) | 最原始的日志,未做任何处理,可能有脏数据 | 文本/JSON(原始格式) |
| DWD | D ata W arehouse Detail | 数据仓库明细层 | 清洗后的明细数据,字段已解析、类型已转换、脏数据已过滤 | Parquet(列式) |
| DWS | D ata W arehouse Summary | 数据仓库汇总层(宽表层) | 按主题聚合的数据(如按用户/按天/按小时统计) | Parquet(列式,高度压缩) |
| ADS | A pplication D ata Service | 应用数据服务层 | 为具体业务场景定制的数据(极少,通常直接导出到HBase/CK/MySQL) | HBase/CK/MySQL |
Spark SQL在各层级的读取/写入角色:
text
【Spark SQL 贯穿数仓全链路】
ODS (原始文本) DWD (Parquet明细) DWS (Parquet汇总) ADS (导出服务)
│ │ │ │
│ ① 读取 │ ② 读取 │ ③ 读取 │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Spark SQL │ │ Spark SQL │ │ Spark SQL │ │ Spark SQL │
│ 解析JSON │───▶│ 过滤/去重 │───▶│ GROUP BY │───▶│ foreach │
│ 提取字段 │ │ 类型转换 │ │ 窗口函数 │ │ Partition │
│ │ │ 维度退化 │ │ 多表JOIN │ │ → HBase/CK │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │ │
│ 写入Parquet │ 写入Parquet │ 写入Parquet │
▼ ▼ ▼ ▼
DWD (Parquet) DWS (Parquet) ADS (Parquet) HBase/ClickHouse
伪代码:Spark SQL在不同层级的实际应用:
java
// ============ 阶段一:ODS → DWD(清洗明细) ============
// 读取原始JSON,解析字段,过滤脏数据,写入Parquet
public class ODSToDWDJob {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().enableHiveSupport().getOrCreate();
Dataset<Row> dwdDF = spark.sql(
"SELECT " +
" get_json_object(raw, '$.order_id') as order_id, " +
" get_json_object(raw, '$.user_id') as user_id, " +
" CAST(get_json_object(raw, '$.amount') AS DOUBLE) as amount, " +
" CAST(get_json_object(raw, '$.ts') AS TIMESTAMP) as event_time, " +
" dt, hour " +
"FROM ods_game_logs " +
"WHERE dt = '2026-07-16' AND amount IS NOT NULL AND amount > 0" // 过滤脏数据
);
dwdDF.write()
.mode(SaveMode.Overwrite)
.format("parquet")
.partitionBy("dt", "hour")
.saveAsTable("dwd_order_detail");
}
}
// ============ 阶段二:DWD → DWS(聚合汇总) ============
// 读取Parquet明细表,按用户汇总,产生宽表
public class DWDToDWSJob {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().enableHiveSupport().getOrCreate();
Dataset<Row> dwsDF = spark.sql(
"SELECT " +
" user_id, " +
" SUM(amount) as total_amount, " +
" COUNT(1) as order_cnt, " +
" AVG(amount) as avg_amount, " +
" dt " +
"FROM dwd_order_detail " + // ← 读的是Parquet表(列式,快速)
"WHERE dt = '2026-07-16' " +
"GROUP BY user_id, dt"
);
dwsDF.write()
.mode(SaveMode.Overwrite)
.format("parquet")
.saveAsTable("dws_user_daily_stats");
}
}
// ============ 阶段三:DWS → ADS(导出服务) ============
// 读取DWS汇总表,导出到HBase供业务查询
public class DWSToADSJob {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().enableHiveSupport().getOrCreate();
// 读取DWS(聚合后数据量已经很小,只有几千行)
Dataset<Row> adsDF = spark.sql(
"SELECT * FROM dws_user_daily_stats WHERE dt='2026-07-16'"
);
// 导出到HBase
adsDF.foreachPartition((Iterator<Row> rows) -> {
Connection hbConn = HBaseConnectionPool.getConnection();
Table table = hbConn.getTable(TableName.valueOf("user_stats"));
List<Put> puts = new ArrayList<>();
while (rows.hasNext()) {
Row row = rows.next();
Put put = new Put(Bytes.toBytes(row.getString(0))); // RowKey = user_id
put.addColumn(Bytes.toBytes("f"), Bytes.toBytes("total_amount"),
Bytes.toBytes(row.getDouble(1).toString()));
puts.add(put);
if (puts.size() >= 1000) { table.put(puts); puts.clear(); }
}
if (!puts.isEmpty()) table.put(puts);
table.close();
});
}
}
总结:Spark SQL不仅查原始数据,还要查**中间层(DWD/DWS)**来做"层层递进"的ETL。它是一套完整的SQL执行引擎,贯穿整个离线数仓的加工链路。
问题7:DWS → ADS导出时,能否跳过DWS直接从DWD导出到HBase/ClickHouse?
背景:这个问题触及了数仓分层的核心矛盾------"分层原则"与"成本/性能权衡"。
答案:
对于HBase:99%的场景都是DWD → HBase(直接导出明细),这是标准实践。
对于ClickHouse:强烈不建议从DWD直接导出,必须经过DWS预聚合(或至少是轻度汇总),否则存储成本和查询性能都会失控。
为什么HBase欢迎DWD明细?
| 维度 | HBase接收DWD明细的合理性 |
|---|---|
| 本质定位 | HBase是"面向海量数据的Key-Value随机读写数据库",天生存储明细记录 |
| 数据粒度 | DWD就是最细粒度的明细,HBase按RowKey存取,无需聚合 |
| 业务场景 | 客服查询"订单1001详情",查的就是单条明细,不需要SUM/GROUP BY |
| 存储成本 | HBase存储于HDFS,成本低廉,不怕明细数据量大 |
为什么ClickHouse畏惧DWD明细?
| 维度 | 问题 | 解决方案 |
|---|---|---|
| 存储成本 | ClickHouse通常使用昂贵的NVMe本地盘,存海量明细会爆炸 | 必须预聚合,将数据量缩小100~1000倍 |
| 查询性能 | 每次大屏刷新都要扫描几亿行做SUM,极慢 | DWS层已将聚合结果算好,CK只需读取几千行 |
| 并发能力 | CK虽快,但扫描几十亿行仍然消耗CPU | 预聚合后,CK可以支撑1000+ QPS |
两种路径对比:
| 路径 | 数据量 | 存储成本 | 查询响应 | 适用场景 |
|---|---|---|---|---|
| DWD → HBase(直接) | 极大(全量明细) | 低(HDFS廉价) | 毫秒级点查 | ✅ 推荐(HBase本职工作) |
| DWD → DWS → ClickHouse(标准) | 小(聚合后缩小1000倍) | 低(存得少) | 亚秒级聚合 | ✅ 推荐(CK本职工作) |
| DWD → ClickHouse(直接) | 极大(全量明细) | 极高(NVMe昂贵) | 极慢(每次都做全量SUM) | ❌ 强烈不推荐 |
但有一个20%的例外场景:如果业务需要"任意维度下钻的Ad-Hoc即席查询"(数据分析师在BI工具中随意拖拽维度),且要求看到最新明细,那么可以将DWD直接导入ClickHouse。但必须配合TTL(数据生命周期),如只保留最近3天明细。
伪代码:选择性导出:
java
public class SelectiveExportJob {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().enableHiveSupport().getOrCreate();
// ===== 第一步:公共清洗(ODS → DWD) =====
Dataset<Row> dwdDF = spark.sql(
"SELECT order_id, user_id, amount, status, dt, hour " +
"FROM ods_logs WHERE dt='2026-07-16'"
);
dwdDF.write().format("parquet").saveAsTable("dwd_order_detail");
// ===== 第二步:导出到HBase(直接使用DWD明细) =====
System.out.println("导出DWD明细到HBase(点查服务)...");
dwdDF.foreachPartition(...); // RowKey = order_id
// ===== 第三步:先做DWS聚合,再导出到ClickHouse =====
System.out.println("先做DWS预聚合...");
Dataset<Row> dwsDF = spark.sql(
"SELECT hour, SUM(amount) as gmv, COUNT(1) as order_cnt " +
"FROM dwd_order_detail GROUP BY hour"
);
dwsDF.write().format("parquet").saveAsTable("dws_hourly_gmv");
System.out.println("导出DWS聚合结果到ClickHouse(大屏服务)...");
dwsDF.foreachPartition(...); // JDBC批量INSERT到CK
}
}
总结 :HBase欢迎"大而全"的明细数据(DWD),因为它靠RowKey吃饭;ClickHouse畏惧"大而全"的明细,只喜欢"小而精"的聚合数据(DWS)。 这就是为什么导出到HBase时可以跳过DWS,导出到ClickHouse时必须经过DWS层。
第4章:HBase与ClickHouse的架构关系
问题8:HBase和ClickHouse在软件架构层级上是平行的概念吗?ClickHouse能否基于HBase执行操作?
背景:读者可能会疑惑:两者都叫"数据库",是不是可以叠加或替代?
答案:
HBase和ClickHouse在架构层级上是平行的(同属数据存储层),但各自解决完全不同的问题。ClickHouse不能"基于"HBase执行操作,但可以通过"联邦查询"能力"联合"HBase进行查询。
两者在架构层级上的位置:
text
┌─────────────────────────────────────────────────────────────────────────┐
│ 应用层 (Spring Boot / BI工具 / 数据产品) │
└──────────────────────────────┬──────────────────────────────────────────┘
│
┌──────────────────────────────┴──────────────────────────────────────────┐
│ 数据服务层 (Serving Layer) ← HBase 和 ClickHouse 都位于这一层 │
│ ┌─────────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ HBase │ │ ClickHouse │ │
│ │ (NoSQL 列族数据库) │ │ (OLAP 列式分析数据库) │ │
│ │ │ │ │ │
│ │ 擅长: 毫秒级点查 │ │ 擅长: 亚秒级聚合/BI报表 │ │
│ │ 存储: HDFS │ │ 存储: 本地NVMe磁盘 │ │
│ │ 架构: Master-Slave │ │ 架构: Multi-Master │ │
│ │ SQL: 需Phoenix插件 │ │ SQL: 原生支持 │ │
│ └─────────────────────────┘ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
┌──────────────────────────────┴──────────────────────────────────────────┐
│ 离线数仓层 (Hive + HDFS Parquet) │
└─────────────────────────────────────────────────────────────────────────┘
核心差异:
| 对比维度 | HBase | ClickHouse |
|---|---|---|
| 核心定位 | 海量数据的随机实时读写(点查/范围扫描) | 海量数据的在线分析查询(BI报表/聚合统计) |
| 数据模型 | 稀疏多维Map(RowKey + 列族 + 列) | 标准二维关系表(支持SQL) |
| 存储依赖 | 强依赖HDFS | 数据存储在本地磁盘,不依赖HDFS |
| 集群架构 | Master-Slave(HMaster + RegionServer) | Multi-Master 对等架构(无中心节点) |
| SQL支持 | 不原生支持,需Phoenix插件 | 原生支持标准SQL |
| 更新/删除 | 支持行级更新(实际是插入新版本) | 极弱,不支持标准UPDATE/DELETE,仅异步Mutation |
关于"ClickHouse能否基于HBase执行操作?"
-
不能"基于":ClickHouse不能架设在HBase之上,因为两者存储引擎完全不兼容(HBase = HFile on HDFS,ClickHouse = MergeTree on 本地盘)。
-
但可以"联合" :ClickHouse支持联邦查询(Federated Query) ,可以在一个SQL中同时查询ClickHouse本地表和HBase外部表,并进行JOIN操作。但这需要借助
hbase表函数或JDBC桥接,且性能通常不如直接查询本地表。 -
生产最佳实践 :不推荐实时联合查询,而是通过ETL定时将HBase中的维表数据同步到ClickHouse本地表,然后用ClickHouse内部的JOIN完成分析。这样性能最优、稳定性最高。
text
【生产实践】HBase → (ETL同步) → ClickHouse
HBase (维表/明细数据)
↓ 每天凌晨批量导出 (Spark SQL)
HDFS 中间文件 (Parquet)
↓ 导入
ClickHouse (本地表)
↓ 查询
BI工具 / 业务系统
总结:HBase管"海量数据的实时存取",ClickHouse管"海量数据的极速分析"。它们是平起平坐的兄弟,各管一摊,通过ETL管道协同工作。
问题9:大屏展示场景下,第一时间查ClickHouse,第二时间查HBase,我的理解正确吗?
背景:读者在理解了大屏业务后,自然会产生"查什么、何时查"的疑问。
答案:
你的理解完全正确,而且极为精准!这恰恰是大数据架构中"大屏 + 明细下钻"最经典、最高效的标准交互模式(Dashboard + Drill-down Pattern)。
"第一时间"必须查ClickHouse(宏观概览):
| 维度 | 说明 |
|---|---|
| 业务需求 | 大屏加载时需要瞬间看到"今日GMV"、"各省份订单量"、"近24小时趋势"等全局聚合数据 |
| 技术匹配 | ClickHouse的向量化执行引擎+稀疏索引,能在500ms内扫描数十亿行返回SUM/GROUP BY |
| 数据来源 | DWS/ADS层的预聚合表(按小时/按天汇总),数据量极小,CK处理无压力 |
"第二时间"必须切到HBase(微观明细):
| 维度 | 说明 |
|---|---|
| 业务需求 | 运营看到"广东省订单异常飙升",点击查看该省份的具体订单列表------这叫"下钻(Drill-down)" |
| 技术匹配 | ClickHouse的LIMIT 10 OFFSET 10000极低效;HBase基于RowKey前缀扫描,毫秒级返回 |
【大屏→明细 数据流向图】
text
用户/运营 交互动作
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 👁️ 大屏页面(宏观报表) │
│ 今日 GMV: 500万 广东省: 120万 (点击广东省柱子) │
└─────────────────────────────────────────────────────────────────────────┘
│ ① 第一时间请求(聚合查询)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ ClickHouse (DWS聚合表) │
│ SELECT province, SUM(gmv) FROM dws_gmv_by_province │
│ WHERE dt = today() GROUP BY province │
│ → 返回广东省等省份汇总数据(500ms) │
└─────────────────────────────────────────────────────────────────────────┘
│ ② 用户点击后,第二时间请求(明细下钻)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ HBase (DWD明细表) │
│ Scan with RowKey Prefix = 'GD_2026-07-16_' │
│ → 返回广东省今天的全部订单列表(20ms) │
└─────────────────────────────────────────────────────────────────────────┘
关于"翻页查询"的重要补充:
在HBase中,传统OFFSET翻页是不存在的。正确实现是基于RowKey的书签(Marker)翻页:
text
【HBase 翻页模式】
第1页查询:SCAN START_ROW = 'GD_2026-07-16_',LIMIT 10
→ 返回前10条,最后一条的RowKey = 'GD_2026-07-16_0010'
第2页查询:SCAN START_ROW = 'GD_2026-07-16_0010',LIMIT 10
→ 从上次的结尾开始继续扫描
关键:后端需要把"上一页最后一条Key"返回给前端,由前端作为参数传回。
总结 :你的架构认知完全准确------"宏观聚合靠ClickHouse(DWS/ADS),微观明细靠HBase(DWD)"。这也是一线大厂大屏系统(如双十一实时大屏)的标准实践。
第5章:商业智能与未来演进
问题10:主动感知决策应该在ETL阶段执行吗?是否违背单一职责?
背景:商业智能需要"主动发现异常",读者可能自然认为应在数据清洗完成后立即执行规则判断。
答案:
你的"时机"理解完全正确------数据清洗完成(DWD/DWS产出)确实是最佳感知窗口。但"职责"理解有偏差------ETL的职责止于"数据变干净",规则判断必须交给独立的数据质量监控或规则引擎模块。
为什么不能把规则写在ETL代码里?
| 后果 | 详细说明 |
|---|---|
| 计算资源互相抢占 | ETL是大规模批处理,异常检测是低延迟逻辑,混跑会导致ETL变慢或流式任务反压 |
| 逻辑耦合导致维护地狱 | 告警规则频繁变化(波动率10%→15%),每改一次都要重新编译ETL Jar包,风险极高 |
| 数据回刷灾难 | 重新跑历史ETL时,告警规则也会被重跑,导致凌晨系统狂发上百条误报 |
正确的架构:三大独立职责("清洗"、"质检"、"感知"):
text
【正确架构】数据流经的三道关口
┌─────────────────────────────────────────────────────────────────────────┐
│ 第一关:ETL 清洗层 (职责:只做格式转换、类型映射、去重) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Spark SQL: 读取 ODS → 清洗(JSON拍平) → 写入 DWD (Parquet) │ │
│ │ 特征:纯碎的"搬运工"+"格式转换器",不掺杂任何业务规则 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────┬───────────────────────────────────────┘
│ (清洗完成,触发下一个任务)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 第二关:数据质量监控层 (职责:统计指标是否在合理阈值内) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Apache Griffin / Great Expectations (独立Spark任务) │ │
│ │ 执行SQL: SELECT COUNT(*) FROM DWD WHERE sensor_value IS NULL │ │
│ │ 规则:如果丢失率 > 5%,触发"数据质量告警"(发邮件给数据团队) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────┬───────────────────────────────────────┘
│ (数据质量确认无误,才触发下一步)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 第三关:业务规则感知层 (职责:基于业务语义判断是否影响商业决策) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 独立 Rule Engine / Flink CEP (复杂事件处理) │ │
│ │ 规则A: 5分钟内无心跳 → 发送"设备离线"企微告警 │ │
│ │ 规则B: 环比波动 > 30% → 发送"业务异动"钉钉 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
伪代码:独立于ETL的监控任务:
java
// 这是凌晨 2:00 由 Airflow 触发的独立监控任务,和 ETL 完全解耦
public class DataQualityMonitorJob {
public static void main(String[] args) {
SparkSession spark = SparkSession.builder().enableHiveSupport().getOrCreate();
// 1. 检查昨日数据量(DWD层)
long count = spark.sql("SELECT COUNT(1) FROM dwd_sensors WHERE dt='2026-07-16'")
.collectAsList().get(0).getLong(0);
if (count < expectedThreshold) {
sendAlert("传感器数据量异常下降!只有 " + count + " 条");
}
// 2. 检查昨日波动(DWS层)
double todayAvg = spark.sql("SELECT AVG(value) FROM dws_sensor_hourly WHERE dt='2026-07-16'")
.collectAsList().get(0).getDouble(0);
double yestAvg = spark.sql("SELECT AVG(value) FROM dws_sensor_hourly WHERE dt='2026-07-15'")
.collectAsList().get(0).getDouble(0);
if (Math.abs((todayAvg - yestAvg) / yestAvg) > 0.3) {
sendAlert("传感器数值发生剧烈波动!");
}
}
}
总结 :ETL的职责止于"数据变干净"。一旦涉及"判断是否异常",就必须交由专门的DQC(数据质量中心)或CEP(复杂事件处理)独立模块处理。ETL模块永远是稳定、纯净、不随业务规则变化而频繁改动的"磐石"。
问题11:LLM时代,传统BI会如何进化?静态规则能否升级为动态决策?
背景:随着大模型技术的普及,商业智能正在从"基于预定义规则"向"基于LLM推理的动态决策"演进。
答案:
你的理解完全正确,且极具前瞻性。这正是一线大厂(字节跳动、阿里巴巴、微软)正在积极探索的"LLM驱动的智能分析(Insight-as-a-Code)"方向。
传统静态BI vs LLM动态BI:
| 维度 | 传统静态BI(基于规则) | LLM动态BI(基于推理) |
|---|---|---|
| SQL来源 | 程序员提前写死在.java/.sql文件中 |
实时生成 。LLM根据问题现场编写GROUP BY/JOIN/窗口函数 |
| 异常处理 | 只能告警(发钉钉),无法自动判断根因 | 自主根因分析。发现CK聚合异常后,自动调用HBase查明细交叉验证 |
| 数据源选择 | 人工决定查CK还是HBase | 自动路由。LLM判断宏观趋势→查CK,微观证据→查HBase,历史归档→查HDFS |
| 行动闭环 | 告警后需人等处理 | 自主决策。LLM可生成Python脚本,调用K8s API自动重启故障Pod |
核心架构:LLM处于"超级智能中间件"层:
text
【升级后的全链路架构】
用户/业务方 (提问):"为什么今天华南区传感器数据丢了这么多?"
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 第0层:LLM 智能决策层 (Python + LangChain + ReAct Agent) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 1. 理解意图:用户想查"华南区"+"传感器丢失率"+"今日对比昨日" │ │
│ │ 2. 生成策略:决定先去ClickHouse查DWS聚合指标是否存在异常 │ │
│ │ 3. 生成代码:自动编写PySpark/SQL/HBase Scan逻辑 │ │
│ │ 4. 解释结果:将返回的200行数字,总结为"14:00 丢失率80%" │ │
│ │ 5. 自主决策:发现异常,决定下发指令给运维API自动重启 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────┬─────────────────────────────────────────┘
│ (调用执行引擎,动态生成查询)
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 原有底座 (第1~7章) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │
│ │ ClickHouse │ │ HBase │ │ Spark SQL (Hive) │ │
│ │ (查宏观趋势) │ │ (查明细下钻)│ │ (查历史离线数据) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Python伪代码(实现"智能感知与决策"):
python
# 基于 LangChain 的智能分析 Agent
# 拥有"思维链(CoT)"能力,能自主决定查CK还是HBase
from langchain.agents import Tool, initialize_agent
from langchain.llms import OpenAI
from clickhouse_driver import Client
import happybase
# 1. 定义基础工具(让LLM知道它能调用哪些数据源)
def query_clickhouse(sql: str) -> str:
"""执行ClickHouse SQL聚合查询,适合查趋势、SUM、AVG"""
client = Client(host='ck-host', user='default')
result = client.execute(sql)
return str(result) # 返回给LLM进行推理
def scan_hbase_by_prefix(rowkey_prefix: str) -> str:
"""在HBase中根据RowKey前缀扫描明细数据"""
connection = happybase.Connection('hbase-host')
table = connection.table('sensor_detail')
scan_data = []
for key, data in table.scan(row_prefix=rowkey_prefix.encode()):
scan_data.append(data)
return str(scan_data[:10]) # 只返回前10条给LLM节省token
# 2. 将工具绑定给LLM
tools = [
Tool(name="ClickHouseAgg", func=query_clickhouse, description="查宏观聚合指标"),
Tool(name="HBaseDetail", func=scan_hbase_by_prefix, description="查微观明细记录")
]
agent = initialize_agent(tools, OpenAI(model="gpt-4"), agent="zero-shot-react-description", verbose=True)
# 3. 用户提出业务问题(传统静态BI无法自动完成)
user_question = "排查为什么2026-07-16华南区传感器丢失率飙升?请查明原因并给出是否需自动重启的建议。"
# 4. LLM的"思考与执行"链条(ReAct模式)
# Step 1: LLM决定查ClickHouse(宏观)
# 生成SQL -> SELECT hour, lost_rate FROM dws_sensor WHERE region='华南' AND dt='2026-07-16'
# 返回: 发现14:00丢失率80%,15:00恢复。
# Step 2: LLM决定查HBase(微观下钻)
# 生成RowKey前缀 -> 'SC_2026-07-16_14:'
# 返回: 发现14:00时刻对应网关IP 10.0.3.5的传感器集体掉线。
# Step 3: LLM决策 -> 输出"检测到网关10.0.3.5故障,正在生成自动重启脚本发送至运维API。"
response = agent.run(user_question)
print(response)
# 输出: "分析完成。故障根因为网关10.0.3.5离线。已自动触发告警通知网络组。"
必须遵守的架构红线(防止LLM闯祸):
| 红线约束 | 在Python代码中的实现方式 |
|---|---|
| 禁止全表扫描 | 在query_clickhouse中强制校验SQL是否包含WHERE dt = today(),否则拒绝 |
| 限制返回行数 | 强制在SQL末尾拼接LIMIT 1000,防止几亿条数据拉入内存 |
| 只读权限 | LLM连接的ClickHouse用户必须是只读账号,禁止DROP/ALTER/INSERT |
| Token费用控制 | 只返回聚合后的数字给LLM,绝不把1GB原始日志塞给GPT-4 |
总结:传统大数据组件(HDFS/Spark/HBase/CK)是高度专业化的"肌肉和骨骼",负责极速存取和计算;LLM是刚接入的"大脑皮层"决策中枢,通过Python胶水代码实时编写SQL/Scan指令,实现了从"被动报表"到"主动参谋"的质变。
附录:问题索引
| 序号 | 问题 | 章节 |
|---|---|---|
| 1 | 原始输入真的只是文本日志吗?Protobuf数据怎么处理? | 第1章 |
| 2 | 写入Kafka的内容,主流方案是自写Java代码还是Logstash? | 第1章 |
| 3 | Logstash采集到巨大日志,ETL时能否分布式加速?行截断怎么解决? | 第2章 |
| 4 | HDFS的"默认存储格式"是什么?扩展名(如.parquet)有什么作用? | 第2章 |
| 5 | Parquet文件到底是什么?它的发音是什么? | 第2章 |
| 6 | Spark SQL是不是只查原始数据?ODS/DWD/DWS/ADS是什么意思? | 第3章 |
| 7 | DWS→ADS导出时,能否跳过DWS直接从DWD导出到HBase/ClickHouse? | 第3章 |
| 8 | HBase和ClickHouse在架构层级上是平行的吗?能否相互基于? | 第4章 |
| 9 | 大屏展示场景下,第一时间查ClickHouse,第二时间查HBase正确吗? | 第4章 |
| 10 | 主动感知决策应该在ETL阶段执行吗?是否违背单一职责? | 第5章 |
| 11 | LLM时代,传统BI会如何进化?静态规则能否升级为动态决策? | 第5章 |
结语
第一本书带你走完了"从数据源到用户界面"的完整链路------你知道了Kafka、Spark Streaming、HDFS、Hive Metastore、Spark SQL、HBase、ClickHouse、Airflow、Spring Boot、React是如何协作的。
这本补充图书则带你深入了每一个环节的"阴影面"------那些第一本书为了保持链路清晰而简化或省略的细节。现在你知道了:
- 原始输入不只是JSON,Protobuf/Avro如何入湖
- 生产环境不用自己写Kafka Producer,Filebeat/Logstash才是主流
- 分布式并行读取时,HDFS/Spark如何保证每一行数据的完整性
.parquet扩展名只是个标签,真正的核心是列式存储格式- Spark SQL贯穿ODS→DWD→DWS→ADS全链路,不仅仅是查原始数据
- HBase欢迎DWD明细,ClickHouse必须吃DWS聚合
- 大屏场景下,先查CK看宏观,再下钻到HBase看明细
- ETL只负责清洗,规则感知必须交给独立的DQC模块
- LLM正在将静态BI升级为动态决策的"智能参谋"
愿你带着这两本书的知识,从容应对大数据架构的设计、面试与生产实践。
全书完