3. 环境搭建:Docker Compose 部署 Hadoop、Spark、Flink、Kafka 集群

说实话,做实时数仓最头疼的往往不是写代码,而是搭环境。我见过太多人花了两周装集群,最后发现版本不兼容,又得重来。所以这一章,我带你用 Docker Compose 一把梭搞定。

你想想看,本地开发环境要是搞不定,后面所有实战都是空中楼阁。我个人习惯是把基础设施容器化,这样团队新同学拉起来就能跑,不用在每个人的电脑上折腾 JDK 版本、SSH 免密这些破事。

本章目标:用 Docker Compose 编排一套完整的 Lakehouse 底座,包含 HDFS、YARN、Hive Metastore、Spark、Flink、Kafka、ZooKeeper。全部容器化,一键启动。

3.1 整体架构与组件选型

先看整体拓扑。我画了一张图,帮你理清每个组件的位置和关系。

Lakehouse 实时数仓 Docker 集群架构 Lakehouse 实时数仓 Docker 集群架构 存储层 HDFS NameNode HDFS DataNode × 2 Hive Metastore PostgreSQL (Metastore 后端) 计算层 Spark Master Spark Worker × 2 Flink JobManager Flink TaskManager × 2 YARN ResourceManager + NodeManager 消息层 Kafka Broker × 3 ZooKeeper × 3 Schema Registry Kafka Connect (可选) 接入与调度层 Nginx (UI 反向代理) Airflow (任务调度) Grafana + Prometheus (监控)

这张图分四层:存储层(HDFS + Hive Metastore)、计算层(Spark + Flink + YARN)、消息层(Kafka + ZooKeeper)、接入与调度层。每层之间通过内部网络通信,端口映射到宿主机方便调试。

3.2 版本选择与兼容性

版本选型是个坑。我踩过最大的坑是 Flink 1.14 和 Hadoop 3.3 的 Shaded Guava 冲突,折腾了两天。后来我总结了一套经过验证的组合:

组件 版本 说明
Hadoop 3.3.6 支持 EC 纠删码,适合 Lakehouse 存储
Spark 3.5.1 内置 Iceberg 支持,Hive 兼容性好
Flink 1.19.0 最新稳定版,CDC 和 Paimon 集成最佳
Kafka 3.7.0 KRaft 模式可选,但这里用 ZooKeeper 模式更稳
ZooKeeper 3.9.1 Kafka 和 HBase 共用
Hive Metastore 3.1.3 独立部署,后端用 PostgreSQL

我的建议:别追最新版。比如 Flink 1.20 刚出时我试过,和 Paimon 0.8 有序列化兼容问题。选经过社区验证的组合,省心。

3.3 目录结构与配置文件

我习惯把每个组件的配置分开,再用一个根 docker-compose.yml 聚合。这样改某个组件时不会影响其他。

lakehouse-docker/
├── docker-compose.yml          # 主编排文件
├── .env                        # 环境变量
├── config/
│   ├── hadoop/
│   │   ├── core-site.xml
│   │   ├── hdfs-site.xml
│   │   └── yarn-site.xml
│   ├── spark/
│   │   └── spark-defaults.conf
│   ├── flink/
│   │   └── flink-conf.yaml
│   └── kafka/
│       └── server.properties
├── data/                       # 持久化数据目录
│   ├── hdfs/
│   ├── kafka/
│   └── postgres/
└── scripts/
    ├── init-hive.sh            # 初始化 Hive Metastore
    └── create-topics.sh        # 创建 Kafka Topic

嗯,这里要注意:data/ 目录要提前创建好,否则 Docker 会以 root 权限创建,后面 HDFS DataNode 写权限会出问题。我曾经因为这个排查了半天。

3.4 核心 Docker Compose 配置

直接上干货。这是 docker-compose.yml 的核心片段,我删掉了重复的服务定义,保留了关键配置。

version: '3.8'

services:
  # ========== 存储层 ==========
  namenode:
    image: bde2020/hadoop-namenode:3.3.6
    hostname: namenode
    environment:
      - CLUSTER_NAME=lakehouse
      - CORE_CONF_fs_defaultFS=hdfs://namenode:8020
    ports:
      - "9870:9870"   # HDFS Web UI
    volumes:
      - ./data/hdfs/namenode:/hadoop/dfs/name
    networks:
      - lakehouse-net

  datanode1:
    image: bde2020/hadoop-datanode:3.3.6
    hostname: datanode1
    environment:
      - CORE_CONF_fs_defaultFS=hdfs://namenode:8020
    volumes:
      - ./data/hdfs/datanode1:/hadoop/dfs/data
    depends_on:
      - namenode
    networks:
      - lakehouse-net

  # ========== 计算层 ==========
  spark-master:
    image: bitnami/spark:3.5.1
    hostname: spark-master
    environment:
      - SPARK_MODE=master
      - SPARK_MASTER_HOST=spark-master
    ports:
      - "8080:8080"   # Spark Web UI
      - "7077:7077"   # Spark Master RPC
    networks:
      - lakehouse-net

  flink-jobmanager:
    image: flink:1.19.0-scala_2.12
    hostname: flink-jobmanager
    ports:
      - "8081:8081"   # Flink Web UI
    command: jobmanager
    environment:
      - JOB_MANAGER_RPC_ADDRESS=flink-jobmanager
    networks:
      - lakehouse-net

  # ========== 消息层 ==========
  zookeeper1:
    image: confluentinc/cp-zookeeper:7.6.0
    hostname: zookeeper1
    environment:
      - ZOOKEEPER_SERVER_ID=1
      - ZOOKEEPER_CLIENT_PORT=2181
    volumes:
      - ./data/zookeeper/1:/var/lib/zookeeper/data
    networks:
      - lakehouse-net

  kafka1:
    image: confluentinc/cp-kafka:7.6.0
    hostname: kafka1
    depends_on:
      - zookeeper1
    environment:
      - KAFKA_BROKER_ID=1
      - KAFKA_ZOOKEEPER_CONNECT=zookeeper1:2181
      - KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
    ports:
      - "9092:9092"
    volumes:
      - ./data/kafka/1:/var/lib/kafka/data
    networks:
      - lakehouse-net

networks:
  lakehouse-net:
    driver: bridge

避坑指南:Kafka 的 ADVERTISED_LISTENERS 一定要配成宿主机地址或 localhost,否则客户端连不上。我曾经配成容器名,结果 Flink 消费时一直报连接超时。

3.5 启动与验证

配置写好了,怎么启动?我一般分三步走:

  1. 拉取镜像:docker compose pull,提前下好所有镜像,避免启动时超时。
  2. 启动集群:docker compose up -d,后台运行。
  3. 检查状态:docker compose ps,确保所有服务都是 Up 状态。

启动后,访问这些地址验证:

组件 Web UI 地址 验证方法
HDFS http://localhost:9870 查看 DataNode 是否存活
Spark http://localhost:8080 查看 Worker 是否注册
Flink http://localhost:8081 查看 TaskManager 数量
Kafka 无 UI docker exec kafka1 kafka-topics --list --bootstrap-server localhost:9092

小技巧:我习惯在 .env 文件里定义版本号,这样升级时只改一个文件。比如 FLINK_VERSION=1.19.0,然后在 docker-compose.yml 里引用 ${FLINK_VERSION}。

3.6 资源限制与调优

本地开发环境资源有限,不加限制的话,Flink 和 Spark 会抢光你的内存。我建议在 docker-compose.yml 里加上资源限制:

services:
  flink-jobmanager:
    deploy:
      resources:
        limits:
          memory: 2g
          cpus: '1.0'
    environment:
      - JOB_MANAGER_MEMORY=1024m

  flink-taskmanager:
    deploy:
      resources:
        limits:
          memory: 4g
          cpus: '2.0'
    environment:
      - TASK_MANAGER_MEMORY=2048m

  spark-worker:
    deploy:
      resources:
        limits:
          memory: 4g
          cpus: '2.0'
    environment:
      - SPARK_WORKER_MEMORY=2g
      - SPARK_WORKER_CORES=2

为什么这么配?Flink JobManager 主要做调度,1GB 足够;TaskManager 要跑算子,给 2GB。Spark Worker 同理。你想想看,要是笔记本只有 16GB 内存,不限制的话,三个 Kafka Broker 就能吃掉 6GB。

3.7 网络与端口规划

端口冲突是家常便饭。我整理了一张端口映射表,方便你对照:

端口 组件 说明
9870 HDFS NameNode Web UI
9864 HDFS DataNode 数据传输
8088 YARN ResourceManager Web UI
8080 Spark Master Web UI
8081 Flink JobManager Web UI
9092 Kafka Broker 1 客户端连接
9093 Kafka Broker 2 客户端连接
2181 ZooKeeper 客户端连接

注意:如果你本地已经跑了 Kafka 或 ZooKeeper,端口会冲突。我建议要么停掉本地服务,要么改映射端口,比如 Kafka 映射到 19092。

3.8 持久化与数据安全

容器重启后数据会丢,这肯定不行。所以我把所有重要数据都挂载到宿主机目录:

  • HDFS 数据:挂载到 ./data/hdfs/,NameNode 和 DataNode 分开目录。
  • Kafka 数据:挂载到 ./data/kafka/,每个 Broker 独立目录。
  • PostgreSQL 数据:挂载到 ./data/postgres/,存 Hive Metastore 的元数据。
  • ZooKeeper 数据:挂载到 ./data/zookeeper/,避免选举状态丢失。

另外,我建议把 data/ 目录加到 .gitignore 里,否则 git 会追踪这些二进制文件,导致仓库臃肿。

3.9 常见问题与排查

环境搭建总会遇到问题。我列几个高频坑:

问题 1:HDFS DataNode 启动失败

原因:DataNode 的 dfs.datanode.data.dir 权限不对。容器内 UID 是 1000,但宿主机目录是 root 所有。

解决:sudo chown -R 1000:1000 ./data/hdfs/datanode1

问题 2:Flink 连不上 Kafka

原因:Kafka 的 ADVERTISED_LISTENERS 配成了容器名,Flink 在容器外无法解析。

解决:改成 PLAINTEXT://localhost:9092,或者用宿主机 IP。

问题 3:Spark 提交任务报 ClassNotFoundException

原因:Spark 和 Hadoop 的 Guava 版本冲突。

解决:在 spark-defaults.conf 里加 spark.driver.userClassPathFirst=true。

嗯,这些问题我基本都遇到过。尤其是 Guava 冲突,几乎每个版本升级都要折腾一次。后来我干脆把 Spark 的 spark.driver.userClassPathFirst 和 spark.executor.userClassPathFirst 都设成 true,省心。

3.10 一键启动脚本

最后,我写了个启动脚本,把初始化工作自动化:

#!/bin/bash
# start-lakehouse.sh

echo "🚀 启动 Lakehouse 集群..."

# 1. 创建数据目录
mkdir -p data/{hdfs/{namenode,datanode1,datanode2},kafka/{1,2,3},postgres,zookeeper/{1,2,3}}

# 2. 设置权限
sudo chown -R 1000:1000 data/hdfs data/kafka data/postgres data/zookeeper

# 3. 启动所有服务
docker compose up -d

# 4. 等待服务就绪
echo "⏳ 等待服务就绪..."
sleep 30

# 5. 初始化 Hive Metastore
docker exec -it hive-metastore /opt/hive/bin/schematool -dbType postgres -initSchema

# 6. 创建 Kafka Topic
docker exec kafka1 kafka-topics --create \
  --topic lakehouse-orders \
  --partitions 3 \
  --replication-factor 2 \
  --bootstrap-server localhost:9092

echo "✅ 集群启动完成!"
echo "HDFS: http://localhost:9870"
echo "Spark: http://localhost:8080"
echo "Flink: http://localhost:8081"

把这个脚本放到项目根目录,以后启动集群就一行命令:./start-lakehouse.sh。干净利落。

我的习惯:我还会写一个 stop-lakehouse.sh,里面执行 docker compose down -v。注意 -v 会删除 volume,如果你不想丢数据,就别加。