1551 字
8 分钟
在 kind 上自建 Lakehouse(一):Lakekeeper + GCS + 五大查询引擎接入实战

这是「自建 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 一定要预先创建并妥善保存 用它加密存储在 PG 里的存储凭据,丢了就等于丢了所有 warehouse 的凭据。 服务起来后(GET /healthGET /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=iceberg
iceberg.catalog.type=rest
iceberg.rest-catalog.uri=http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalog
iceberg.rest-catalog.warehouse=iceberg
iceberg.rest-catalog.security=NONE
fs.gcs.enabled=true # 注意是 fs.gcs.enabled,不是 fs.native-gcs.enabled
gcs.project-id=…
gcs.json-key-file-path=/secrets/gcs-key.json
fs.native-s3.enabled=true # 必须!因为 Doris 把文件路径写成 s3://(GCS S3 互操作)
s3.endpoint=https://storage.googleapis.com
s3.path-style-access=true
s3.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。这个引擎踩了四个坑,每个都让一次任务失败:

  1. iceberg-gcp 的 vended credentials 报 NotSerializableException(OAuth2RefreshCredentialsHandler),即便序列化过了,凭据派发还会导致”Failed to load committed snapshot”——写成功但读回为空。解法:在 Lakekeeper warehouse 上关掉凭据派发(storage-profile.sts-enabled=false),让 Spark 用挂载的 SA key 走纯 ADC。Doris/Trino 不受影响,它们本来就用自己的静态凭据。
  2. 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 镜像。
  3. 同一会话里连续 DML 报 Found conflicting files(可串行化隔离 + 快照缓存)。解法:spark.sql.catalog.lk.cache-enabled=false + 表属性把 delete/update/merge 的隔离级别设成 snapshot
  4. 失败残留表状态 → 用 DROP TABLE IF EXISTSCREATE 保证可重跑。

4) StarRocks 4.1.1#

最快路径是单 Pod allin1 镜像。catalog 用 gcp.gcs.* 凭据。GCS 认证有个隐蔽坑:

type=iceberg
iceberg.catalog.type=rest
iceberg.catalog.uri=http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalog
iceberg.catalog.warehouse=iceberg
vended-credentials-enabled=false
gcp.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.impl
fs.gs.auth.type = SERVICE_ACCOUNT_JSON_KEYFILE
google.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/v1alpha1KeeperCluster + 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 篇):

  1. 搭建篇(本文) —— Lakekeeper + GCS + 五大引擎接入
  2. 互操作篇 —— 跨引擎读写、MERGE 与 positional delete 合规性实测
  3. 性能篇 —— Iceberg 读性能横评 vs Doris vs StarRocks
  4. 联邦篇 —— Trino 联邦查询 vs 专用 Doris:冷数据 Iceberg ⋈ 关系表的代价
  5. 扩容篇 —— Doris 存算分离 BE 扩容量化:加算力到底快多少
  6. 实时入湖篇 —— Flink CDC / PostgreSQL → Doris 存算分离,延迟与节点数的真相
在 kind 上自建 Lakehouse(一):Lakekeeper + GCS + 五大查询引擎接入实战
https://notes.ezworker.cc/posts/lakehouse-on-kind-1-setup/
作者
jayzhu
发布于
2026-06-07
许可协议
CC BY-NC-SA 4.0