【大数据】《传统Hadoop大数据技术入门:补充与进阶——从数据格式到智能决策的问答实录》

《传统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能极大提升查询性能?

  1. 列裁剪(Projection Pushdown)SELECT user_id, amount 只需读取这两个列的文件片段,跳过其他列。
  2. 谓词下推(Predicate Pushdown)WHERE dt = '2026-07-16' 通过Footer中的统计信息(min/max),直接跳过不包含该日期的行组。
  3. 向量化读取:同一列的数据连续存放,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执行操作?"

  1. 不能"基于":ClickHouse不能架设在HBase之上,因为两者存储引擎完全不兼容(HBase = HFile on HDFS,ClickHouse = MergeTree on 本地盘)。

  2. 但可以"联合" :ClickHouse支持联邦查询(Federated Query) ,可以在一个SQL中同时查询ClickHouse本地表和HBase外部表,并进行JOIN操作。但这需要借助hbase表函数或JDBC桥接,且性能通常不如直接查询本地表。

  3. 生产最佳实践不推荐实时联合查询,而是通过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升级为动态决策的"智能参谋"

愿你带着这两本书的知识,从容应对大数据架构的设计、面试与生产实践。


全书完

相关推荐
吾AI科技2 小时前
基于Tez引擎的 Hive SQL 性能优化
大数据·hive·性能优化·tez
ThanksGive2 小时前
TurboLock:从 v1 Redis 分布式锁到 v2 Go 并发控制中间件
分布式·后端
百胜软件@百胜软件2 小时前
零点零一×百胜E3+业务智能中台项目启动,中台赋能客制化机械键盘品牌“加数”发展
大数据·云计算
鼎艺创新科技2 小时前
WAIC 2026观察:应急AI三维沙盘避坑 拆解3DGIS+AI真落地的3个核心技术门槛
大数据·人工智能·国产化·电子沙盘·三维电子沙盘
2503_931712483 小时前
2026年国内广东树脂骰子/合成树脂配件/树脂化妆刷/树脂胡须刷/树脂按摩手柄工厂盘点
大数据·人工智能
小小放舟、3 小时前
Windows 本地安装 Elasticsearch 8.10.0 与 IK 分词器(2026)
java·大数据·windows·spring boot·elasticsearch·搜索引擎·全文检索
F20226974863 小时前
西门子 PLC 与 C# 通信_项目实战
大数据·c#
合众恒跃3 小时前
RK3588 + RK182X 双芯异构边缘计算方案:部署流程与实测性能解析
大数据·人工智能·科技·编辑器·etl工程师
百胜软件@百胜软件4 小时前
AI大促之后:618的三大启示与双11的三大预判
大数据·人工智能·零售