Appearance
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.password | PG 超级用户密码;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;
SQL4. 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: 3 | 3 个 master 做 quorum,挂 1 个还能选主 |
master.defaultReplication: "001" | 测试期只存 1 份;生产改 "010"/"100" 等 |
master.data | master 元数据落 PVC,重启不丢拓扑 |
volume.replicas: 3 | 3 个 volume 节点,分散数据 |
volume.dataDirs | 每个 volume 的 PVC 大小和 StorageClass |
volume.maxVolumes: 0 | 不限制单盘 volume 数,测试期够用 |
filer.replicas: 2 | filer 无状态,多副本避免单点 |
filer.data | filer 元数据/日志落 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-... Running4.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/password | catalog 连 PG 的账号 |
catalog.lake.warehouse | 表数据根路径,必须是 S3 URI |
io-impl=S3FileIO | Iceberg 用 AWS S3 客户端读写对象存储 |
s3.endpoint | 指向 SeaweedFS S3,不是 AWS |
path-style-access | SeaweedFS 不支持 virtual-host 风格,必须 path-style |
access-key-id/secret-access-key | S3 凭证,从 Secret 取,不要写死 |
client.region | S3 客户端要求有 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,
warehousebucket 可访问; - Spark 能建
lake.ods并读写 Iceberg。
下一步:消息与实时。