这是「自建 Lakehouse 实战」系列的第一篇。整个系列围绕一个目标:在本地 kind(Kubernetes in Docker / podman)集群上,用开源组件搭一套以 Apache Iceberg 为表格式、对象存储为底座的 Lakehouse,然后把市面上主流的查询/计算引擎都接到同一张 Iceberg 目录上,做跨引擎互操作与读性能的横向对比。 本篇先把地基打好:目录服务(Lakekeeper)、元数据后端(CloudNativePG)、数据仓(GCS),以及五大引擎各自如何接入。后两篇分别讲跨引擎互操作性和读性能横评(见文末系列导航)。
架构总览
- 编排:本地 kind 集群(3 节点,podman provider)。
- 表格式
Iceberg(format-version 2,支持 merge-on-read 行级删除)。 - 目录:Lakekeeper —— 一个 Iceberg REST Catalog 实现(本次用 v0.12.2)。
- 目录后端:CloudNativePG 管理的 PostgreSQL(1 主 1 从)。
- 数据仓
Cloud Storage 专用桶 gs://your-bucket。 - 引擎
Doris 4.1.1、Trino 481、Apache Spark 3.5.8、StarRocks 4.1.1、ClickHouse 26.5 —— 全部读写/查询同一张 Lakekeeper 目录。
一、目录后端
Lakekeeper 的元数据(命名空间、表、快照指针等)存在 PostgreSQL 里。用 CloudNativePG operator(v1.29.1)起一个 2 实例(1 主 1 从)的 PG 17 集群:
kubectl apply --server-side -f \ https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.29/releases/cnpg-1.29.1.yaml# 然后 apply 一个 Cluster: 2 实例、storageClass standard、bootstrap.initdb 建库 lakekeeper它会自动生成读写分离的 Service:pg-lakekeeper-rw(主/写)、pg-lakekeeper-ro(从/读),应用凭据放在 secret pg-lakekeeper-app 里。
二、目录服务
用官方 Helm chart 部署,关掉内置 PG、指向上面的外部 PG:
helm repo add lakekeeper https://lakekeeper.github.io/lakekeeper-charts/# values 关键项:# postgresql.enabled: false# externalDatabase.host_read: pg-lakekeeper-ro# externalDatabase.host_write: pg-lakekeeper-rw# secretBackend.postgres.encryptionKeySecret: lakekeeper-encryption-key⚠️ 那把加密 key 一定要预先创建并妥善保存GET /health、GET /management/v1/info 检查),它没配 OpenID,authz 是 allow-all 单租户模式,调用 API 不需要 token。Iceberg REST 的基础地址是:
http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalog挂上 GCS warehouse
专门开一个 GCS 桶和一个服务账号(授 roles/storage.objectAdmin),然后两步把 warehouse 建出来:
# 1) 初始化POST /management/v1/bootstrap {"accept-terms-of-use": true} → 204# 2) 建 warehouse(名字就叫 iceberg)POST /management/v1/warehouse storage-profile: {type: gcs, bucket: …, key-prefix: warehouse} storage-credential: {type: gcs, credential-type: service-account-key, key: [SA-JSON]}注意:和 Doris 用 HMAC(S3 互操作)不同,Lakekeeper 这里走的是 原生 GCS 凭据(service-account-key,把整段 SA JSON 嵌进 storage-credential)。后面各引擎读数据文件时,是各自直连 GCS,而不一定走 Lakekeeper 的 vended credentials。
三、五大引擎接入
所有引擎连的都是上面那串 REST 地址,warehouse 参数填的是 warehouse 的名字(iceberg),不是 gs:// 路径。难点几乎都在 GCS 认证怎么喂给引擎。逐个说。
1) Apache Doris 4.1.1(存算分离)
Doris 通过 REST 连目录,数据文件用 HMAC S3 互操作密钥(gs.* 属性)访问 GCS:
CREATE CATALOG iceberg PROPERTIES( 'type'='iceberg', 'iceberg.catalog.type'='rest', 'iceberg.rest.uri'='http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalog', 'warehouse'='iceberg', 'gs.endpoint'='https://storage.googleapis.com', 'gs.region'='us-central1', 'gs.access_key'='[HMAC-ak]', 'gs.secret_key'='[HMAC-sk]');⚠️ Iceberg 行级 DML(UPDATE/DELETE/MERGE INTO)需要 Doris 4.1.0+;3.1.x 只能 INSERT/append + 读。这正是把集群从 3.1.4 升到 4.1.1 的原因。建表带 'format-version'='2' 才有 merge-on-read 删除。
2) Trino 481
Helm chart 部署,catalog 配置:
connector.name=icebergiceberg.catalog.type=resticeberg.rest-catalog.uri=http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalogiceberg.rest-catalog.warehouse=icebergiceberg.rest-catalog.security=NONEfs.gcs.enabled=true # 注意是 fs.gcs.enabled,不是 fs.native-gcs.enabledgcs.project-id=…gcs.json-key-file-path=/secrets/gcs-key.jsonfs.native-s3.enabled=true # 必须!因为 Doris 把文件路径写成 s3://(GCS S3 互操作)s3.endpoint=https://storage.googleapis.coms3.path-style-access=trues3.aws-access-key=[HMAC-ak] s3.aws-secret-key=[HMAC-sk]坑点gs://,但要读 Doris 写的表得同时打开原生 S3 文件系统(fs.native-s3.enabled),因为 Doris 的路径是 s3://。
3) Apache Spark 3.5.8(Spark K8s Operator)
通过 Spark Kubernetes Operator 提交 SparkApplication。catalog 配置走 SparkCatalog + REST + GCSFileIO。这个引擎踩了四个坑,每个都让一次任务失败:
- iceberg-gcp 的 vended credentials 报 NotSerializableException(
OAuth2RefreshCredentialsHandler),即便序列化过了,凭据派发还会导致”Failed to load committed snapshot”——写成功但读回为空。解法:在 Lakekeeper warehouse 上关掉凭据派发(storage-profile.sts-enabled=false),让 Spark 用挂载的 SA key 走纯 ADC。Doris/Trino 不受影响,它们本来就用自己的静态凭据。 - iceberg 1.10+/1.11 需要 Java 17(默认
apache/spark:3.5.x是 Java 11,报 class file version 61.0)。换apache/spark:3.5.8-java17-python3镜像。 - 同一会话里连续 DML 报
Found conflicting files(可串行化隔离 + 快照缓存)。解法:spark.sql.catalog.lk.cache-enabled=false+ 表属性把 delete/update/merge 的隔离级别设成snapshot。 - 失败残留表状态 → 用
DROP TABLE IF EXISTS再CREATE保证可重跑。
4) StarRocks 4.1.1
最快路径是单 Pod allin1 镜像。catalog 用 gcp.gcs.* 凭据。GCS 认证有个隐蔽坑:
type=icebergiceberg.catalog.type=resticeberg.catalog.uri=http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalogiceberg.catalog.warehouse=icebergvended-credentials-enabled=falsegcp.gcs.service_account_email / _private_key_id / _private_key = …gcp.gcs.* 能覆盖 BE 的数据读取,但 FE 读元数据(snapshot/manifest 的 avro)走的是内置 Hadoop gcs-connector,因为 fs.gs.auth.type 没被这些属性赋值,直接 NullPointerException。解法是非侵入式地往 FE 和 BE 的 core-site.xml 追加 GCS 认证:
fs.gs.impl / fs.AbstractFileSystem.gs.implfs.gs.auth.type = SERVICE_ACCOUNT_JSON_KEYFILEgoogle.cloud.auth.service.account.json.keyfile = /opt/gcs-key.json# 然后 supervisorctl restart feservice beservice(后面性能篇里又用官方 operator 把它重部成了 1 FE + 2 BE,同样的 core-site.xml 坑要在 FE 和 BE 都补。)
5) ClickHouse 26.5
用官方 operator(不是 Altinity),CRD 为 clickhouse.com/v1alpha1 的 KeeperCluster + ClickHouseCluster。连 Iceberg 用 DataLakeCatalog 引擎:
SET allow_experimental_database_iceberg=1;CREATE DATABASE lake ENGINE=DataLakeCatalog('http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalog')SETTINGS catalog_type='rest', warehouse='iceberg', vended_credentials=false, storage_endpoint='https://storage.googleapis.com/your-bucket', storage_uri_style='path';三个坑:
DataLakeCatalog默认vended_credentials=true,而 Lakekeeper 没开 STS → 变匿名访问 → 403。必须设false。- 那些
aws_access_key_id/secret设置只对 Glue 生效,REST 存储用不到;S3 客户端的凭据来自AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY环境变量(默认凭据链)→ 用 GCS HMAC 互操作密钥,通过容器 env 注入。 - 这个 kind/podman runtime 上单文件 subPath 挂载会失败(“not a directory”),所以配置只能走 env,不能挂 config.d 文件。
另外 operator 默认容器配额只有 1Gi/1cpu,务必调大;icebergGCS() 表函数不存在(只有 iceberg()/icebergS3())。
小结
到这里,五个引擎都能查同一张 Lakekeeper/GCS 上的 Iceberg 表了。最大的共性痛点是 GCS 认证:有的走 HMAC S3 互操作(Doris、Trino、ClickHouse),有的走原生 SA key(Lakekeeper、Spark、StarRocks),而 StarRocks 还要分别照顾 FE 元数据和 BE 数据两条路径。地基搭好后,接下来就能问两个有意思的问题:这些引擎写出来的表,彼此读得动吗?谁读得快?
📚 本文是「自建 Lakehouse 实战」系列(共 6 篇):