Skip to content

02. 存储与元数据 ​

目标:有可用的本地存储、PostgreSQL 和 S3,Iceberg 能建库建表。
所有命令默认在 node1 执行。

为什么先做存储,不先装计算 ​

计算组件都是“无状态或半有状态”的,真正不能丢的是:

  • 元数据:Iceberg 表指向哪个 metadata、DS 工作流定义、OM 实体;
  • 对象存储:Iceberg 的 Parquet 数据文件;
  • PVC:Kafka/Doris/PG 的本地数据。

如果先装 Flink/Spark,它们启动后就会去找 PostgreSQL、S3 和 PVC;存储没就绪,计算只会反复 CrashLoop,排障时无法区分“组件问题”还是“依赖没准备好”。
所以顺序是:PVC → PostgreSQL → S3 → Iceberg 读写验证 → 再装计算。

1. 创建 namespace ​

bash
kubectl create namespace data --dry-run=client -o yaml | kubectl apply -f -
kubectl create namespace storage --dry-run=client -o yaml | kubectl apply -f -

2. local-path ​

local-path 不是“生产存储”,它只是把 PVC 落到节点本地目录,解决测试环境“有 PVC 可用”的问题。
优点:零依赖、读写快;缺点:Pod 漂到别的节点后数据不在,节点挂数据就丢。
本教程用它跑 PG/Kafka/Doris 的测试数据;生产要换 Ceph/云盘等带副本的 StorageClass。

bash
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.30/deploy/local-path-storage.yaml

把默认目录改到 /data/local-path:

bash
kubectl -n local-path-storage edit configmap local-path-config

找到 config.json,把 nodePathMap 改成:

json
{
  "nodePathMap": [
    {
      "node": "DEFAULT_PATH_FOR_NON_LISTED_NODES",
      "paths": ["/data/local-path"]
    }
  ]
}

保存退出后,ConfigMap 会自动更新;不需要再 kubectl apply 旧文件。

设默认 StorageClass:

bash
kubectl patch storageclass local-path \
  -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl get storageclass

验证 PVC:

bash
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-test
  namespace: data
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 1Gi
EOF

kubectl -n data get pvc pvc-test
kubectl -n data delete pvc pvc-test

预期 Bound。

3. PostgreSQL ​

为什么用 PostgreSQL 而不是每个组件自带数据库:

  • Iceberg JDBC catalog、DolphinScheduler、OpenMetadata 都需要关系型元数据;
  • 集中一个 PG 实例,备份、监控、账号管理只做一遍;
  • 测试期单实例足够,生产再换 Patroni/云数据库。
    四个库分开(polaris/iceberg/openmetadata/dolphinscheduler)是为了隔离故障和权限,不要把所有表塞进一个库。

3.1 完整 manifest ​

保存为 10-postgres.yaml,把密码换成自己的:

yaml
apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  namespace: data
type: Opaque
stringData:
  password: "<PG_PASS>"
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-init
  namespace: data
data:
  init.sql: |
    CREATE USER polaris WITH PASSWORD '<PG_PASS>';
    CREATE DATABASE polaris OWNER polaris;
    CREATE DATABASE iceberg OWNER polaris;
    CREATE DATABASE openmetadata OWNER polaris;
    CREATE DATABASE dolphinscheduler OWNER polaris;
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: data
spec:
  selector:
    app: postgres
  ports:
    - name: pg
      port: 5432
      targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: data
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: docker.io/library/postgres:16-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              cpu: "2"
              memory: 4Gi
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
            - name: init
              mountPath: /docker-entrypoint-initdb.d
      volumes:
        - name: init
          configMap:
            name: postgres-init
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: local-path
        resources:
          requests:
            storage: 10Gi

字段说明:

字段作用
Secret.stringData.passwordPG 超级用户密码;init 脚本和连接串都用它
ConfigMap.init.sql首次启动自动执行的建库脚本
Service.selector.app=postgres选中带 app: postgres 的 Pod
Service.ports.port/targetPort集群内 5432 映射到容器 5432
StatefulSet.serviceName给 Pod 提供稳定 DNS:postgres-0.postgres
replicas: 1测试期单实例;生产用 Patroni/主备
env.POSTGRES_PASSWORD从 Secret 注入,不把密码写进镜像
env.PGDATA数据目录子路径,避免 PVC 根目录权限问题
resources请求 0.5C1G,限制 2C4G,防止占满节点
volumeMounts.data挂 PVC 到 PG 数据目录
volumeClaimTemplates每个副本自动创建 10Gi PVC

应用:

bash
kubectl apply -f 10-postgres.yaml
kubectl -n data get pods -l app=postgres
kubectl -n data logs postgres-0 --tail=20

预期 postgres-0 1/1 Running。

3.2 验证 ​

bash
kubectl -n data exec -it postgres-0 -- psql -U postgres -c '\l'

应该看到:

text
polaris
iceberg
openmetadata
dolphinscheduler

建 demo 库和 CDC 用户:

bash
kubectl -n data exec -i postgres-0 -- psql -U postgres <<'SQL'
CREATE DATABASE demo;
CREATE USER flink_pg WITH REPLICATION LOGIN PASSWORD '<PG_PASS>';
GRANT ALL PRIVILEGES ON DATABASE demo TO flink_pg;
SQL

进入 demo 建表:

bash
kubectl -n data exec -i postgres-0 -- psql -U postgres -d demo <<'SQL'
CREATE TABLE IF NOT EXISTS pg_orders (
  id BIGINT PRIMARY KEY,
  user_id BIGINT,
  amount DECIMAL(10,2),
  status VARCHAR(20),
  updated_at TIMESTAMP DEFAULT now()
);
GRANT SELECT ON pg_orders TO flink_pg;
SQL

4. SeaweedFS S3 ​

为什么 Iceberg 不直接用本地盘:

  • 存算分离:Spark/Flink/Doris 可以随时重建,数据不绑在某个 Pod;
  • S3 协议是 Iceberg 的通用后端,换存储只改 endpoint;
  • SeaweedFS 比 Ceph 轻,3 节点能快速跑起来。
    3 master 是为了 quorum,3 volume 是为了副本和容量;测试期 defaultReplication: "001" 只保证一份,生产要调高。

4.1 values ​

安装 Helm repo:

bash
helm repo add seaweedfs https://seaweedfs.github.io/seaweedfs/helm
helm repo update

保存 values-seaweedfs.yaml:

yaml
master:
  replicas: 3
  defaultReplication: "001"
  data:
    type: persistentVolumeClaim
    size: 5Gi
    storageClass: local-path

volume:
  replicas: 3
  dataDirs:
    - name: data1
      type: persistentVolumeClaim
      size: 50Gi
      storageClass: local-path
      maxVolumes: 0

filer:
  replicas: 2
  data:
    type: persistentVolumeClaim
    size: 10Gi
    storageClass: local-path

s3:
  enabled: true
  replicas: 2
  enableAuth: true
  createBuckets:
    - name: warehouse

字段说明:

字段作用
master.replicas: 33 个 master 做 quorum,挂 1 个还能选主
master.defaultReplication: "001"测试期只存 1 份;生产改 "010"/"100" 等
master.datamaster 元数据落 PVC,重启不丢拓扑
volume.replicas: 33 个 volume 节点,分散数据
volume.dataDirs每个 volume 的 PVC 大小和 StorageClass
volume.maxVolumes: 0不限制单盘 volume 数,测试期够用
filer.replicas: 2filer 无状态,多副本避免单点
filer.datafiler 元数据/日志落 PVC
s3.enabled开启 S3 网关,Iceberg 通过它读写
s3.enableAuth开启 AK/SK 鉴权,不能用匿名
s3.createBuckets自动创建 warehouse bucket

4.2 安装 ​

bash
helm upgrade --install seaweedfs seaweedfs/seaweedfs \
  -n storage --create-namespace \
  -f values-seaweedfs.yaml

验证:

bash
kubectl -n storage get pods
kubectl -n storage get svc

预期:

text
seaweedfs-master-0/1/2   Running
seaweedfs-volume-0/1/2   Running
seaweedfs-filer-0/1      Running
seaweedfs-s3-...         Running

4.3 取 S3 凭证 ​

bash
kubectl -n storage get secret seaweedfs-s3-secret -o jsonpath='{.data}' ; echo

解码:

bash
kubectl -n storage get secret seaweedfs-s3-secret \
  -o jsonpath='{.data.admin_access_key_id}' | base64 -d; echo
kubectl -n storage get secret seaweedfs-s3-secret \
  -o jsonpath='{.data.admin_secret_access_key}' | base64 -d; echo

保存到变量:

bash
export S3_ENDPOINT=http://seaweedfs-s3.storage.svc.cluster.local:8333
export S3_AK=<access-key>
export S3_SK=<secret-key>

验证 S3:

bash
kubectl -n data run s3check --rm -it --image=amazon/aws-cli:latest --command -- \
  aws --endpoint-url $S3_ENDPOINT s3 ls

预期能看到 warehouse bucket。

5. Iceberg JDBC catalog ​

Iceberg 需要 catalog 回答“表的最新 metadata 在哪”。
测试期用 JDBC catalog:元数据直接存 PostgreSQL,不额外部署 Polaris/Nessie,少一个故障点。
代价:不是多租户 REST catalog,权限和并发能力弱;生产可换 Polaris/Nessie,表格式和数据文件不变。
S3FileIO + path-style-access 是为了适配 SeaweedFS 这类非 AWS S3。

测试环境不依赖 Polaris,直接用 JDBC catalog。

5.1 元数据库 ​

iceberg 库已在 PostgreSQL init 中创建。

5.2 Spark 配置 ​

后续 Spark 任务统一带:

text
spark.sql.catalog.lake=org.apache.iceberg.spark.SparkCatalog
spark.sql.catalog.lake.type=jdbc
spark.sql.catalog.lake.uri=jdbc:postgresql://postgres.data:5432/iceberg
spark.sql.catalog.lake.jdbc.user=polaris
spark.sql.catalog.lake.jdbc.password=<PG_PASS>
spark.sql.catalog.lake.warehouse=s3://warehouse/lake
spark.sql.catalog.lake.io-impl=org.apache.iceberg.aws.s3.S3FileIO
spark.sql.catalog.lake.s3.endpoint=http://seaweedfs-s3.storage:8333
spark.sql.catalog.lake.s3.path-style-access=true
spark.sql.catalog.lake.s3.access-key-id=<S3_AK>
spark.sql.catalog.lake.s3.secret-access-key=<S3_SK>
spark.sql.catalog.lake.client.region=us-east-1

逐项说明:

配置作用
catalog.lake声明一个名为 lake 的 Iceberg catalog
catalog.lake.type=jdbc用 JDBC 存元数据,不用 REST/Hive
catalog.lake.uri指向 PostgreSQL 的 iceberg 库
jdbc.user/passwordcatalog 连 PG 的账号
catalog.lake.warehouse表数据根路径,必须是 S3 URI
io-impl=S3FileIOIceberg 用 AWS S3 客户端读写对象存储
s3.endpoint指向 SeaweedFS S3,不是 AWS
path-style-accessSeaweedFS 不支持 virtual-host 风格,必须 path-style
access-key-id/secret-access-keyS3 凭证,从 Secret 取,不要写死
client.regionS3 客户端要求有 region,测试填 us-east-1

5.3 验证 ​

用 Spark 镜像启动一个临时 Pod:

bash
kubectl -n data run spark-client --image=<REGISTRY>/spark:3.5.9-iceberg \
  --restart=Never --command -- sleep 3600

进入 Pod:

bash
kubectl -n data exec -it spark-client -- bash

执行:

bash
/opt/spark/bin/spark-sql \
  --conf spark.sql.catalog.lake=org.apache.iceberg.spark.SparkCatalog \
  --conf spark.sql.catalog.lake.type=jdbc \
  --conf spark.sql.catalog.lake.uri=jdbc:postgresql://postgres.data:5432/iceberg \
  --conf spark.sql.catalog.lake.jdbc.user=polaris \
  --conf spark.sql.catalog.lake.jdbc.password='<PG_PASS>' \
  --conf spark.sql.catalog.lake.warehouse=s3://warehouse/lake \
  --conf spark.sql.catalog.lake.io-impl=org.apache.iceberg.aws.s3.S3FileIO \
  --conf spark.sql.catalog.lake.s3.endpoint=http://seaweedfs-s3.storage:8333 \
  --conf spark.sql.catalog.lake.s3.path-style-access=true \
  --conf spark.sql.catalog.lake.s3.access-key-id='<S3_AK>' \
  --conf spark.sql.catalog.lake.s3.secret-access-key='<S3_SK>' \
  -e "CREATE NAMESPACE IF NOT EXISTS lake.ods;"

再建表:

sql
CREATE TABLE IF NOT EXISTS lake.ods.orders (
  id BIGINT,
  amount DECIMAL(10,2)
) USING iceberg;

INSERT INTO lake.ods.orders VALUES (1, 10.00);
SELECT count(*) FROM lake.ods.orders;

预期 1。

6. 本阶段验收 ​

bash
kubectl -n data get pods
kubectl -n storage get pods
kubectl -n data exec postgres-0 -- psql -U postgres -c '\l'

要求:

  • PostgreSQL Running,四个元数据库存在;
  • SeaweedFS 全部 Running,warehouse bucket 可访问;
  • Spark 能建 lake.ods 并读写 Iceberg。

下一步:消息与实时。