<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>自学笔记</title><description>数据 · 数据湖 · 云原生</description><link>https://notes.ezworker.cc/</link><language>zh_CN</language><item><title>在 kind 上自建 Lakehouse(六):Flink CDC 实时入湖 —— MariaDB / PostgreSQL → Doris 存算分离</title><link>https://notes.ezworker.cc/posts/lakehouse-on-kind-6-cdc/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/lakehouse-on-kind-6-cdc/</guid><description>前五篇都在查,这篇来写:用 Flink CDC 3.6 把 MariaDB(mariadb-operator)和 PostgreSQL(CNPG)实时同步进 Doris 存算分离的 UNIQUE 内部表。跑通增删改 / upsert / schema 演进,量化延迟与 FE/BE 节点数的影响。结论:延迟被 Doris sink 刷新间隔主导(与源类型、节点数都无关);BE 扩容增吞吐、对延迟无效;schema 演进是 MySQL 与 PostgreSQL 唯一的分叉点。附 5 个真实工程坑。</description><pubDate>Wed, 10 Jun 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是「自建 Lakehouse 实战」系列第六篇。前五篇都在&lt;strong&gt;查&lt;/strong&gt;(搭建、互操作、性能、联邦、扩容),这一篇换到&lt;strong&gt;写&lt;/strong&gt;:数据怎么&lt;strong&gt;实时&lt;/strong&gt;进湖?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;业务库(MySQL/PostgreSQL)里的增删改,怎么以秒级延迟同步进数仓,还要能自动跟上源表的 schema 变更?这正是 CDC(Change Data Capture)要解决的问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这次的链路:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MariaDB (mariadb-operator)  ─┐
                             ├─ Flink CDC 3.6 YAML pipeline ──→ Doris 存算分离 UNIQUE 内部表
PostgreSQL (CNPG, wal=logical)─┘   (Flink standalone session 集群)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接给三个结论:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CDC 端到端延迟几乎只由 Doris sink 的 &lt;code&gt;buffer-flush.interval&lt;/code&gt; 决定&lt;/strong&gt;,跟源是 MySQL 还是 PG、跟你加几个节点,都没关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加 BE 能提吞吐(sub-linear),但对延迟完全无效&lt;/strong&gt;;加 FE 对 CDC 基本没用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;增删改、upsert、全量快照两边都开箱即用;唯一的分叉点是 schema 演进&lt;/strong&gt; —— MySQL 开箱,PostgreSQL 必须显式开开关,否则直接挂。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;一、为什么这套选型&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MariaDB&lt;/strong&gt;:用官方 &lt;code&gt;mariadb-operator&lt;/code&gt;(CRD &lt;code&gt;k8s.mariadb.com&lt;/code&gt;),声明式管理实例 + binlog 配置。版本特意选 &lt;strong&gt;10.11 LTS&lt;/strong&gt; 而非 11.x —— 新版 MariaDB 的 GTID 实现和 Debezium 的 MySQL 连接器有兼容雷,10.11 的 binlog 与 Flink CDC 的 mysql 连接器完全兼容。CDC 必须开 &lt;code&gt;binlog_format=ROW&lt;/code&gt; + &lt;code&gt;binlog_row_image=FULL&lt;/code&gt;(UPDATE/DELETE 才带完整前像)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;:复用前几篇就在的 CloudNativePG operator,新建一个专用库,&lt;code&gt;wal_level=logical&lt;/code&gt;,逻辑解码插件用内置的 &lt;strong&gt;&lt;code&gt;pgoutput&lt;/code&gt;&lt;/strong&gt;(PG 10+ 自带,不用装 wal2json/decoderbufs)。表要设 &lt;code&gt;REPLICA IDENTITY FULL&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flink CDC 3.6&lt;/strong&gt;:关键是用 &lt;strong&gt;YAML pipeline 模式&lt;/strong&gt;而不是老的 SQL connector —— 只有 pipeline 模式的 Doris sink 支持&lt;strong&gt;自动 schema 演进&lt;/strong&gt;。值得一提:&lt;strong&gt;PostgreSQL 的 pipeline 连接器从 3.5.0 才有&lt;/strong&gt;,3.6.0 起按 Flink 版本分叉(&lt;code&gt;3.6.0-1.20&lt;/code&gt; 对应 Flink 1.20)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Doris 内部表&lt;/strong&gt;:存算分离模式,数据落对象存储 storage vault。CDC 目标表用 &lt;strong&gt;&lt;code&gt;UNIQUE KEY&lt;/code&gt; 模型&lt;/strong&gt; —— 这正好天然实现 upsert(主键重复即更新),配合 sink 的 delete sign 处理删除。Flink CDC 的 Doris sink 会&lt;strong&gt;自动建表&lt;/strong&gt;:&lt;code&gt;UNIQUE KEY(主键) DISTRIBUTED BY HASH(主键) BUCKETS AUTO&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、场景:增删改 / upsert / schema 演进&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/cdc-scenario-matrix.png&quot; alt=&quot;Flink CDC → Doris 场景能力矩阵&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在两个源上分别跑了一整套:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;全量快照&lt;/strong&gt;:pipeline 一提交就把存量数据灌进 Doris,✓。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;INSERT / UPDATE / DELETE&lt;/strong&gt;:源表改完,Doris 侧 UNIQUE 表对应行增/改/删,✓。删除走 Doris 的 &lt;code&gt;__DORIS_DELETE_SIGN__&lt;/code&gt;,merge-on-write 生效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;upsert&lt;/strong&gt;:MySQL 的 &lt;code&gt;INSERT ... ON DUPLICATE KEY UPDATE&lt;/code&gt;、PostgreSQL 的 &lt;code&gt;INSERT ... ON CONFLICT DO UPDATE&lt;/code&gt;,命中改、未命中插,都正确落到 UNIQUE 表,✓。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;唯一两边&lt;strong&gt;不一样&lt;/strong&gt;的,是 schema 演进(给源表 &lt;code&gt;ALTER TABLE ADD COLUMN&lt;/code&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MariaDB:开箱即用。&lt;/strong&gt; binlog 里原生带 DDL,Flink CDC 捕获后自动给 Doris 表 &lt;code&gt;ALTER TABLE ADD COLUMN&lt;/code&gt;,新列 + 新数据无缝跟上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL:默认直接挂。&lt;/strong&gt; &lt;code&gt;pgoutput&lt;/code&gt; &lt;strong&gt;不复制 DDL&lt;/strong&gt;。你加完列再插一条带新列的数据,这条 6 字段的记录撞上 pipeline 里登记的 5 字段 schema,直接抛 &lt;code&gt;IllegalStateException: Column size does not match the data size&lt;/code&gt;,整个 job 失败。&lt;strong&gt;解法是显式给 postgres 源加 &lt;code&gt;schema-change.enabled: true&lt;/code&gt;&lt;/strong&gt; —— 开了之后,它靠 pgoutput 在结构变化时发的 relation 消息把新列带出来,就能正常演进了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是从 MySQL 迁 CDC 经验到 PostgreSQL 时最容易栽的一个坑:&lt;strong&gt;别假设 PG 的 schema 演进跟 MySQL 一样自动&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;三、延迟:被 sink 刷新间隔主导,跟源、跟节点都无关&lt;/h2&gt;
&lt;p&gt;测法:源表插一行(记录提交时刻),紧轮询 Doris 直到可见,算差值。一开始测出来 MariaDB 和 PostgreSQL &lt;strong&gt;都是 ~9.6 秒&lt;/strong&gt;,高得反常,而且两个源几乎一模一样。&lt;/p&gt;
&lt;p&gt;排查发现:延迟既不是源的解码延迟,也不是 checkpoint 间隔,而是 &lt;strong&gt;Doris sink 的 &lt;code&gt;sink.buffer-flush.interval&lt;/code&gt;(默认 10 秒)&lt;/strong&gt; 在卡。日志里那一串 &lt;code&gt;bufferMap is empty, no need to flush&lt;/code&gt; 每 10 秒一次,就是它。把它调到 1 秒,延迟立刻掉到 ~2.5 秒:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/cdc-latency-flush.png&quot; alt=&quot;CDC 延迟被 Doris sink 刷新间隔主导&quot; /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;flush interval&lt;/th&gt;
&lt;th&gt;MariaDB&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10s(默认)&lt;/td&gt;
&lt;td&gt;9.76s&lt;/td&gt;
&lt;td&gt;9.49s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1s&lt;/td&gt;
&lt;td&gt;2.59s&lt;/td&gt;
&lt;td&gt;2.51s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两个源在同一档下几乎重合,&lt;strong&gt;证明延迟是 sink 侧的地板,与源类型无关&lt;/strong&gt;。残留的 ~2.5s 是 1s flush + checkpoint 交互 + stream load 可见性 + 轮询粒度叠起来的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;要低延迟,调小 &lt;code&gt;sink.buffer-flush.interval&lt;/code&gt;,而不是加机器。&lt;/strong&gt; 代价是更频繁的 stream load(更多小事务、更碎的版本),要在延迟和 Doris compaction 压力之间权衡。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;四、FE / BE 节点数的影响:BE 提吞吐,延迟全平&lt;/h2&gt;
&lt;p&gt;存算分离的卖点是算力独立伸缩。那对 CDC &lt;strong&gt;摄入&lt;/strong&gt;来说,加 FE、加 BE 各有什么用?把 Doris 的 FE 从 1 扩到 3、BE 从 2 扩到 4,在同一条 pipeline(parallelism=4、flush=1s)上测吞吐(30k 行突发 drain)和延迟:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/cdc-fe-be-scaling.png&quot; alt=&quot;FE/BE 节点数对 CDC 摄入的影响&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;吞吐(左):BE 2→4 暖跑约 +35%&lt;/strong&gt;(3323→4495 rows/s)。这是 sub-linear 的 —— 受单条 binlog reader(源侧串行读)+ 目标表 bucket 数 + sink 并行度共同封顶。CDC 增量的天花板往往在&lt;strong&gt;源侧的单线程 binlog/WAL 读&lt;/strong&gt;,不在 Doris 写。图里我特意把两轮的散点也画上:&lt;strong&gt;轮间噪声有 ±40%&lt;/strong&gt;(flush/checkpoint 周期相位对齐导致),所以 FE×3 那点 3821 落在噪声内,算不上提升。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟(右):三档配置 2210 / 2201 / 2206ms,一条直线。&lt;/strong&gt; 加 FE、加 BE 对单行延迟&lt;strong&gt;完全无效&lt;/strong&gt; —— 因为延迟被 sink flush 地板卡死,跟有多少算力无关。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FE 对 CDC 基本没用&lt;/strong&gt;:FE 只做 stream load 的协调和重定向(很轻),根本不是瓶颈。BE 才是真正落盘的。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话:&lt;strong&gt;要低延迟调 flush;要高吞吐加 BE(且记得给目标表多分 bucket);FE 别为 CDC 加。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;顺带一个韧性观察:整个扩缩容过程(BE 2→4→2、FE 1→3→1)里,两条 pipeline 一直 RUNNING,Doris sink 在节点变动时自动重连,数据不丢 —— 测完源表 24 万行和 Doris 精确一致。&lt;/p&gt;
&lt;h2&gt;五、五个真实工程坑&lt;/h2&gt;
&lt;p&gt;这套链路不是一把跑通的,记下几个值得避的坑:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;MySQL 驱动 GPL 不打包,而且要放对地方。&lt;/strong&gt; Flink CDC 因许可证不带 &lt;code&gt;mysql-connector-j&lt;/code&gt;,报 &lt;code&gt;NoClassDefFoundError: com.mysql.cj.jdbc.Driver&lt;/code&gt;。更阴的是:JDBC 的 &lt;code&gt;DriverManager&lt;/code&gt; 在 &lt;strong&gt;JobManager 的 source coordinator&lt;/strong&gt; 里用父类加载器找驱动,光把驱动 ship 到 TaskManager 的 child-first 类加载器&lt;strong&gt;没用&lt;/strong&gt; —— 必须把驱动塞进 Flink 的&lt;strong&gt;系统 &lt;code&gt;lib/&lt;/code&gt;&lt;/strong&gt;(我直接 &lt;code&gt;FROM flink:1.20.4&lt;/code&gt; + &lt;code&gt;COPY 驱动&lt;/code&gt; 重打了个镜像)。(MariaDB 走 MySQL 协议,用 mysql 驱动即可。)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;checkpoint 默认没开,MySQL 增量永远不启动。&lt;/strong&gt; 现象很迷惑:全量快照进来了,job 也 RUNNING,但源表后续的 binlog 一行都不同步。根因:&lt;strong&gt;MySQL 增量源在快照完成后,要等一次成功的 checkpoint 才会切到 binlog 读取&lt;/strong&gt;。而 session 集群在 JobManager 上设的 checkpoint 间隔&lt;strong&gt;不会&lt;/strong&gt;应用到通过 REST 提交的 job(job 的配置在客户端构建 JobGraph 时就定了)。解法:在&lt;strong&gt;客户端&lt;/strong&gt;的 Flink 配置里显式开 &lt;code&gt;execution.checkpointing.interval&lt;/code&gt; + &lt;code&gt;checkpoints-after-tasks-finish.enabled: true&lt;/code&gt;(parallelism&amp;gt;1 时部分 source 子任务空闲,后一个标志必需)。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 的 &lt;code&gt;DEFAULT now()&lt;/code&gt; 会让建表失败。&lt;/strong&gt; CreateTableEvent 把源列的 &lt;code&gt;updated_at DEFAULT now()&lt;/code&gt; 原样带给 Doris,Doris 不认 &lt;code&gt;now()&lt;/code&gt; 这个字面量,报 &lt;code&gt;date literal [now()] is invalid&lt;/code&gt;。去掉源列的 server 默认值即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;route 的 TableId 段数 MySQL ≠ PostgreSQL。&lt;/strong&gt; mysql 源的 TableId 是两段 &lt;code&gt;库.表&lt;/code&gt;;&lt;strong&gt;postgres 源的 &lt;code&gt;tables&lt;/code&gt; 要写三段 &lt;code&gt;库.schema.表&lt;/code&gt;,但它发出的 TableId 却是两段 &lt;code&gt;schema.表&lt;/code&gt;&lt;/strong&gt;(库名不进 TableId)。所以 route 的 &lt;code&gt;source-table&lt;/code&gt; 必须按两段写,否则 sink 会拿源 schema 名当库名乱建库。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;存算分离 FE 缩容要手动收尾。&lt;/strong&gt; 实验完把 FE 从 3 缩回 1,operator 报 &lt;code&gt;ScaleDownFailed&lt;/code&gt;、集群变 yellow —— 因为它不会自动把多余 FE 从选举组里摘掉(扩容时加的还是 &lt;strong&gt;OBSERVER&lt;/strong&gt;,不是 follower)。手动 &lt;code&gt;ALTER SYSTEM DROP OBSERVER &apos;host:9010&apos;&lt;/code&gt; 之后,operator 立刻完成缩容、回 green。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(还有一个纯环境坑:kind 节点的 containerd 解析不了镜像仓库 DNS,所有镜像都得宿主机 &lt;code&gt;podman pull&lt;/code&gt; + &lt;code&gt;kind load&lt;/code&gt; 进节点 —— 这个跟 CDC 无关,是本地 kind/podman 环境的老问题了。)&lt;/p&gt;
&lt;h2&gt;六、小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flink CDC 3.6 的 YAML pipeline + Doris UNIQUE 内部表&lt;/strong&gt;,是一套完整能用的实时入湖方案:增删改、upsert、schema 演进都覆盖,Doris 自动建表、自动演进。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;延迟的旋钮是 &lt;code&gt;sink.buffer-flush.interval&lt;/code&gt;,不是节点数&lt;/strong&gt;:默认 10s,要秒级就往下调。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;节点伸缩对 CDC 摄入:BE 提吞吐(且受源侧单线程读封顶)、对延迟无效;FE 没用。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL → PostgreSQL 迁移时最大的认知差是 schema 演进&lt;/strong&gt;:MySQL binlog 带 DDL 开箱即用,PG pgoutput 不带 DDL,必须显式 &lt;code&gt;schema-change.enabled&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;至此,这个在 kind 上自建的 Lakehouse 从&lt;strong&gt;搭建&lt;/strong&gt;、&lt;strong&gt;跨引擎互操作&lt;/strong&gt;、&lt;strong&gt;读性能横评&lt;/strong&gt;、&lt;strong&gt;联邦查询&lt;/strong&gt;、&lt;strong&gt;弹性扩容&lt;/strong&gt;,一路写到了&lt;strong&gt;实时入湖&lt;/strong&gt;。六篇下来,它已经是一个能查、能写、能实时同步的完整数据平台了。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;📚 本文是「自建 Lakehouse 实战」系列(共 6 篇):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-1-setup/&quot;&gt;搭建篇&lt;/a&gt; —— Lakekeeper + GCS + 五大引擎接入&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-2-interop/&quot;&gt;互操作篇&lt;/a&gt; —— 跨引擎读写、MERGE 与 positional delete 合规性&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-3-benchmark/&quot;&gt;性能篇&lt;/a&gt; —— Iceberg 读性能横评:ClickHouse vs Doris vs StarRocks&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-4-federation/&quot;&gt;联邦篇&lt;/a&gt; —— Trino 联邦查询 vs 专用 Doris:冷数据 Iceberg ⋈ 关系表的代价&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-5-be-scaling/&quot;&gt;扩容篇&lt;/a&gt; —— Doris 存算分离 BE 扩容量化:加算力到底快多少&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时入湖篇(本文)&lt;/strong&gt; —— Flink CDC:MariaDB / PostgreSQL → Doris 存算分离,延迟与节点数的真相&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>在 kind 上自建 Lakehouse(五):Doris 存算分离 BE 扩容量化 —— 加算力到底能快多少?</title><link>https://notes.ezworker.cc/posts/lakehouse-on-kind-5-be-scaling/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/lakehouse-on-kind-5-be-scaling/</guid><description>存算分离的好处之一是算力可独立伸缩。把 Doris 的 compute group 从 2 一路扩到 8 个 BE,跑 TPC-H SF10,量化「加 BE 到底快多少」。结论:收益强烈双峰——重聚合/重 shuffle 近线性(TPC-H Q1 8BE 4.3×),轻量亚秒查询几乎不动甚至变慢;存在 ~300ms 协调地板加 BE 打不破;扫描受 bucket 数封顶。还补了 3/5/6/7 这些非 2^n 的点,验证不会有奇怪现象。</description><pubDate>Tue, 09 Jun 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是「自建 Lakehouse 实战」系列第五篇。前四篇都在横向比引擎(&lt;a href=&quot;/posts/lakehouse-on-kind-1-setup/&quot;&gt;搭建&lt;/a&gt;、&lt;a href=&quot;/posts/lakehouse-on-kind-2-interop/&quot;&gt;互操作&lt;/a&gt;、&lt;a href=&quot;/posts/lakehouse-on-kind-3-benchmark/&quot;&gt;性能&lt;/a&gt;、&lt;a href=&quot;/posts/lakehouse-on-kind-4-federation/&quot;&gt;联邦&lt;/a&gt;),这一篇换个角度,问一个&lt;strong&gt;调优&lt;/strong&gt;问题:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;存算分离最大的卖点之一,是&lt;strong&gt;算力可以独立伸缩&lt;/strong&gt;。那么把 Doris 的 compute group 从 2 个 BE 扩到 8 个,查询到底能快多少?是线性加速,还是很快就撞墙?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;直接给结论:&lt;strong&gt;加 BE 的收益强烈&quot;双峰&quot;——重计算/重 shuffle 的查询近线性加速,轻量亚秒查询几乎不动甚至变慢。该堆 BE 就堆,但要堆在对的负载上。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;一、设置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Doris 存算分离(compute-storage 分离),数据在 GCS storage vault,计算节点(BE)无状态、可秒级伸缩。扩容只改一个数:&lt;code&gt;computeGroups[0].replicas&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;数据:TPC-H SF10 原生表(lineitem 6000 万行,16 bucket)。每个 BE 限 8 核 / 16GB。&lt;/li&gt;
&lt;li&gt;7 个查询,覆盖不同负载形态:纯扫描、TPC-H Q1(重聚合)、2/3/5 表 join、高基数 group by(6000 万行 join 后按 150 万 custkey 分组,重 shuffle)。&lt;/li&gt;
&lt;li&gt;口径:&lt;strong&gt;热缓存&lt;/strong&gt;(预热 2 轮,关掉 SQL/结果缓存只留数据缓存),所以测的是&lt;strong&gt;计算并行度&lt;/strong&gt;而非 GCS 扫描带宽;每查询 best-of-3,再&lt;strong&gt;减去连接开销基线&lt;/strong&gt;(本环境 &lt;code&gt;SELECT 1&lt;/code&gt; 往返 ~290ms,不减会严重压缩快查询的加速比)。&lt;/li&gt;
&lt;li&gt;BE 数从 2 一路扫到 8(含 3/5/6/7 这些非 2 的幂)。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;二、双峰:重查询近线性,轻查询不动&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/doris-be-scaling-speedup-1.png&quot; alt=&quot;Doris BE 2→8 全扫描加速比&quot; /&gt;&lt;/p&gt;
&lt;p&gt;净耗时(ms)与相对 2BE 的加速比:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;查询&lt;/th&gt;
&lt;th&gt;2&lt;/th&gt;
&lt;th&gt;4&lt;/th&gt;
&lt;th&gt;8&lt;/th&gt;
&lt;th&gt;8BE 加速比&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S3 TPC-H Q1&lt;/strong&gt;(重聚合)&lt;/td&gt;
&lt;td&gt;1370&lt;/td&gt;
&lt;td&gt;675&lt;/td&gt;
&lt;td&gt;319&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.3×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;S7 高基数 group by&lt;/strong&gt;(重 shuffle)&lt;/td&gt;
&lt;td&gt;3195&lt;/td&gt;
&lt;td&gt;1124&lt;/td&gt;
&lt;td&gt;810&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.9×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S4 join2&lt;/td&gt;
&lt;td&gt;599&lt;/td&gt;
&lt;td&gt;488&lt;/td&gt;
&lt;td&gt;280&lt;/td&gt;
&lt;td&gt;2.1×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S5 join3&lt;/td&gt;
&lt;td&gt;434&lt;/td&gt;
&lt;td&gt;348&lt;/td&gt;
&lt;td&gt;290&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S6 join5&lt;/td&gt;
&lt;td&gt;511&lt;/td&gt;
&lt;td&gt;622&lt;/td&gt;
&lt;td&gt;304&lt;/td&gt;
&lt;td&gt;1.7×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S2 Q6 过滤扫描&lt;/td&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;107&lt;/td&gt;
&lt;td&gt;1.3×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;重计算/重 shuffle 近线性甚至超线性&lt;/strong&gt;:TPC-H Q1 在 8 BE 跑出 &lt;strong&gt;4.3×&lt;/strong&gt;(超过理想的 4×,多半来自热集分散到更多 BE 后单 BE 缓存命中更好);高基数 group by 3.9×。这类&quot;可并行的活够多&quot;的查询,堆 BE 直接见效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;轻量/亚秒查询几乎不动&lt;/strong&gt;:join3 / join5 / 过滤扫描只有 1.3–1.7×,join5 在 4 BE 甚至&lt;strong&gt;变慢&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;几何平均:4BE 1.42×、8BE 2.23×(理想 2×/4×)——但这个均值掩盖了上面强烈的分化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三、为什么轻查询不动:协调地板&lt;/h2&gt;
&lt;p&gt;把所有点(2–8 BE)画出来看,会发现轻查询都压在一条&lt;strong&gt;水平线&lt;/strong&gt;附近——本环境约 &lt;strong&gt;300ms&lt;/strong&gt;。这是每个查询的固定成本:查询规划、fragment 派发、跨 BE 的数据交换(exchange)建立。这部分&lt;strong&gt;不随 BE 增多而减少,反而略增&lt;/strong&gt;(BE 越多,exchange 的连接对越多)。&lt;/p&gt;
&lt;p&gt;所以:&lt;strong&gt;亚秒级查询有一个加 BE 也打不破的延迟地板&lt;/strong&gt;。当一个查询的&quot;真正计算&quot;只要一两百毫秒时,它整体耗时被这层固定开销主导,堆再多 BE 也只是让一群 BE 围着一点点活空转。要优化这类查询,该提升的是并发吞吐和缓存命中,不是 BE 数。&lt;/p&gt;
&lt;h2&gt;四、扫描受 bucket 数封顶&lt;/h2&gt;
&lt;p&gt;纯扫描类(全表 count+sum)在热缓存下已是 sub-150ms,加 BE 基本无感。一个结构性原因:lineitem 是 &lt;code&gt;BUCKETS 16&lt;/code&gt;,扫描并行度最多 16 路;8 BE 时每个 BE 只摊到 ~2 个 lineitem tablet,8 核根本喂不饱。&lt;strong&gt;要让大扫描吃满更多 BE,得先把表分更多 bucket&lt;/strong&gt;——否则桶数就是扫描并行度的天花板。(join/agg 的 shuffle 不受此限,它按核数重新分区。)&lt;/p&gt;
&lt;h2&gt;五、非 2 的幂会不会有奇怪现象?&lt;/h2&gt;
&lt;p&gt;我特意补了 3、5、6、7 这些非 2 的幂的点,担心 16 个 bucket 除不尽会造成分配失衡、出现 straggler。结果是:&lt;strong&gt;没有任何意外。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/doris-be-scaling-nonpow2-1.png&quot; alt=&quot;非 2^n BE 无异常:重查询耗时随 BE 平滑下降&quot; /&gt;&lt;/p&gt;
&lt;p&gt;实心圆是 2 的幂(2/4/8),空心方块是非 2 的幂(3/5/6/7)——它们&lt;strong&gt;精确落在同一条平滑曲线上&lt;/strong&gt;。原因是 Doris 存算分离的 tablet 缓存亲和性重平衡器,在&lt;strong&gt;任意 BE 数都把总 tablet 均分&lt;/strong&gt;(3BE=31/30/30、5BE=19/18×4、7BE=13×7)。尽管单张 lineitem 的 16 桶除不尽 7,但跨所有表的总 tablet 仍然均衡,加上 shuffle 本就按核数均分,所以奇数 BE 不会失衡。&lt;/p&gt;
&lt;p&gt;轻查询那几条线确实有上下抖动(比如 join2 在某个点突然偏快),但那是耗时贴着 300ms 地板时 best-of-3 的正常噪声,跟&quot;是不是 2 的幂&quot;无关。&lt;strong&gt;结论:BE 想配几个配几个,不必凑 2 的幂。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;六、调优小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;按主力查询的&quot;重量&quot;决定 BE 数,而不是某个魔法数字。&lt;/strong&gt; 负载是大聚合 / 大 join / 高基数 group by → 堆 BE 近线性划算,一路扩到 8 都在降;负载是大量亚秒级点查 / 小 join → 扩 BE 几乎白花钱,该优化并发与缓存。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每查询有协调延迟地板&lt;/strong&gt;(本环境 ~300ms),小查询别指望靠 BE 数突破。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扫描并行度被 bucket 数封顶&lt;/strong&gt;,大扫描想吃满算力要先加桶。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不同查询的&quot;拐点&quot;不同也不在 2 的幂上&lt;/strong&gt;:高基数 group by 的收益在 2→4 就基本吃完,TPC-H Q1 能一路扩到 8。先认识你的负载,再定规模。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存算分离让&quot;按需加算力&quot;变得很轻(改一个 replicas 数、秒级生效),但&quot;加了就一定快&quot;是个美好的误会——&lt;strong&gt;算力只对吃得下它的查询有意义&lt;/strong&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;📚 本文是「自建 Lakehouse 实战」系列(共 6 篇):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-1-setup/&quot;&gt;搭建篇&lt;/a&gt; —— Lakekeeper + GCS + 五大引擎接入&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-2-interop/&quot;&gt;互操作篇&lt;/a&gt; —— 跨引擎读写、MERGE 与 positional delete 合规性&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-3-benchmark/&quot;&gt;性能篇&lt;/a&gt; —— Iceberg 读性能横评:ClickHouse vs Doris vs StarRocks&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-4-federation/&quot;&gt;联邦篇&lt;/a&gt; —— Trino 联邦查询 vs 专用 Doris:冷数据 Iceberg ⋈ 关系表的代价&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-5-be-scaling/&quot;&gt;扩容篇&lt;/a&gt; —— Doris 存算分离 BE 扩容量化:加算力到底快多少&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-6-cdc/&quot;&gt;实时入湖篇&lt;/a&gt; —— Flink CDC:MariaDB / PostgreSQL → Doris 存算分离,延迟与节点数的真相&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>在 kind 上自建 Lakehouse(四):Trino 联邦查询到底有多糟?—— 冷数据 Iceberg ⋈ Doris 关系表</title><link>https://notes.ezworker.cc/posts/lakehouse-on-kind-4-federation/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/lakehouse-on-kind-4-federation/</guid><description>把冷数据(Iceberg/GCS 上的事实表)和关系维表(Doris 原生表)用 Trino 联邦 join,性能有多糟?该不该为冷数据单独立一个 Doris?实测三种跑法:Trino 联邦、Trino 全 Iceberg、专用 Doris 直读冷 Iceberg。结论:单表聚合能整段下推时联邦不亏,但 join 一重,单条 MySQL 流串行拉维表让联邦比专用 Doris 慢 3–4 倍——而且 Doris 只用了 1/7 的 CPU。</description><pubDate>Mon, 08 Jun 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是「自建 Lakehouse 实战」系列第四篇。前三篇(&lt;a href=&quot;/posts/lakehouse-on-kind-1-setup/&quot;&gt;搭建&lt;/a&gt;、&lt;a href=&quot;/posts/lakehouse-on-kind-2-interop/&quot;&gt;互操作&lt;/a&gt;、&lt;a href=&quot;/posts/lakehouse-on-kind-3-benchmark/&quot;&gt;性能&lt;/a&gt;)解决了&quot;能用、能互通、谁读得快&quot;。这一篇回答一个很实际的架构问题:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我有一份&lt;strong&gt;冷数据&lt;/strong&gt;躺在 Iceberg/GCS 上(事实表),关系维表在 &lt;strong&gt;Doris&lt;/strong&gt; 里。如果用 &lt;strong&gt;Trino 做联邦查询&lt;/strong&gt;把两边 join 起来,性能会有多糟?&lt;strong&gt;如果太糟,是不是不如单独立一个专门查冷数据的 Doris?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;直接给结论:&lt;strong&gt;冷数据 + 需要 join → 专用 Doris 直读 Iceberg 是更优解。&lt;/strong&gt; Trino 联邦只在&quot;能整段下推的单表查询&quot;上不亏;一旦把 Doris 的关系数据当 join 的一边,它的代价会随 join 复杂度迅速恶化。&lt;/p&gt;
&lt;h2&gt;一、三种跑法&lt;/h2&gt;
&lt;p&gt;给 Trino 加一个走 &lt;strong&gt;MySQL connector&lt;/strong&gt; 的 &lt;code&gt;doris&lt;/code&gt; catalog——Doris FE 本就讲 MySQL 协议(9030 端口),Trino 直接当它是 MySQL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connector.name=mysql
connection-url=jdbc:mysql://&amp;lt;doris-fe-host&amp;gt;:9030?useSSL=false
connection-user=root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后对同一逻辑查询跑三种方式,数据集是 TPC-H SF10(lineitem 6000 万行,事实表在 Iceberg/GCS 上;orders/customer/nation/region 关系维表):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FED(Trino 联邦)&lt;/strong&gt;:Trino 读 Iceberg 事实表 + 经 MySQL 拉 Doris 维表,在 Trino 里 join。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TRI(Trino 全 Iceberg)&lt;/strong&gt;:维表也放 Iceberg,纯 Trino。用来隔离&quot;MySQL 拉取&quot;这一步的成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;专用 Doris&lt;/strong&gt;:Doris 用外部 catalog 直读&lt;strong&gt;同一份&lt;/strong&gt;冷 Iceberg 事实表 + 自身原生维表,全在 Doris MPP 里 join。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;冷读口径&lt;/strong&gt;:Trino 默认无数据缓存,读 Iceberg 每次都打 GCS = 天然冷;Doris 侧关掉 file cache / page cache / segment cache,强制每次走对象存储。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;一个对结论很关键的资源不对称&lt;/strong&gt;:这套 Doris 计算节点被限制在 &lt;strong&gt;8 核 / 16GB ×2&lt;/strong&gt;,而 Trino 我特意放开——&lt;strong&gt;不限 CPU(可吃满 56 核)+ 32GB 堆 ×2&lt;/strong&gt;。记住这点,因为下面 Doris 是&lt;strong&gt;用 1/7 的 CPU&lt;/strong&gt; 在赢。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;二、下推决定一切&lt;/h2&gt;
&lt;p&gt;联邦快不快,只看一件事:&lt;strong&gt;对 Doris 表的操作能不能被 Trino 整段下推过去。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单表聚合 / 过滤:完全下推。&lt;/strong&gt; &lt;code&gt;SELECT count(*), sum(totalprice) FROM orders WHERE orderdate &amp;lt; …&lt;/code&gt; 会被 Trino 整句改写成一条 SQL 丢给 Doris 执行,Trino 只收回 &lt;strong&gt;1 行结果&lt;/strong&gt;。这种查询联邦毫无损耗,甚至比 Trino 自己扫 GCS 还快。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;join:不能跨 catalog 下推。&lt;/strong&gt; Trino 没法把 join 推给 Doris,只能把(谓词下推、列裁剪之后的)&lt;strong&gt;维表行经 MySQL 协议拉回自己这边再 join&lt;/strong&gt;。更要命的是,MySQL connector 对单表只生成 &lt;strong&gt;一个 split = 单连接、单线程串行拉取&lt;/strong&gt;,没有任何并行度。这是 connector 的结构性限制。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/iceberg-federation-mysql-pull-1.png&quot; alt=&quot;联邦的代价:join 不下推,维表行经单条 MySQL 流串行拉回 Trino&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看这张图就懂了:Q1 整段下推,只回 1 行,联邦零成本;Q2 要从 Doris 拉 228 万行 orderkey(单 split);Q3 则要单线程拖 &lt;strong&gt;729 万行&lt;/strong&gt; orders——拉取量一上来,Trino 联邦耗时(红线)直接飙到 12.9 秒,而专用 Doris(蓝线)稳在 3.6 秒。&lt;/p&gt;
&lt;h2&gt;三、冷读横评&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/iceberg-federation-cold-elapsed-1.png&quot; alt=&quot;冷数据查询:Trino 联邦 vs 专用 Doris 读 Iceberg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;四个查询,join 由简到繁(对数轴,越低越好;灰色横杠是&quot;数据已入库 Doris 原生·热&quot;的地板):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;查询&lt;/th&gt;
&lt;th&gt;Trino 联邦&lt;/th&gt;
&lt;th&gt;Trino 全 Iceberg&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;专用 Doris 读冷 Iceberg&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;谁赢&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q1 单表聚合&lt;/td&gt;
&lt;td&gt;0.6&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Doris&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q2 li+ord(冷扫描主导)&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;td&gt;6.2&lt;/td&gt;
&lt;td&gt;5.7&lt;/td&gt;
&lt;td&gt;平&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q3 li+ord+cust&lt;/td&gt;
&lt;td&gt;报错→&lt;strong&gt;12.9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8.8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Doris 快 3.6 倍&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q4 5 表宽 join&lt;/td&gt;
&lt;td&gt;6.8&lt;/td&gt;
&lt;td&gt;8.2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Doris&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;逐条看:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Q2 三家打平(~5.6–6.2s)&lt;/strong&gt;:因为冷扫描 6000 万行 lineitem(1.5GB)从 GCS 拉回来是&lt;strong&gt;所有引擎的共同地板&lt;/strong&gt;——物理带宽决定,谁都逃不掉。Q2 只从 Doris 拉 228 万行,被这个地板掩盖了。所以&quot;冷数据查询&quot;的下限,本质是对象存储的扫描速度;引擎之争,拼的是&lt;strong&gt;扫描之上的 join 与计算&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q3 是分水岭&lt;/strong&gt;:要从 Doris 单线程拖 729 万行 orders,Trino 联邦 12.9 秒,而且冷读首次直接 &lt;code&gt;RpcException&lt;/code&gt; 报错、重试才出数——&lt;strong&gt;又慢又脆&lt;/strong&gt;。专用 Doris 自己 MPP 读同一份冷 Iceberg + 本地维表,只要 3.6 秒。连 Trino 全 Iceberg(8.8s)都比联邦快,反证 MySQL 拉取那一步是纯负担。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q4 宽 join&lt;/strong&gt;:Trino 默认配置(&lt;code&gt;query.max-memory-per-node=1GB&lt;/code&gt;)下 5 表 join &lt;strong&gt;直接 OOM&lt;/strong&gt;,把 per-node 调到 20GB 才跑通(6.8s);专用 Doris 5.2s 拿下。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;四、为什么专用 Doris 赢&lt;/h2&gt;
&lt;p&gt;两个原因叠加:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;省掉跨引擎拉数。&lt;/strong&gt; 联邦的本质是把维表数据&quot;搬&quot;到 Trino,而搬运通道是单条 MySQL 流。专用 Doris 直接在数据所在的引擎里 join,没有这趟搬运。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源效率碾压。&lt;/strong&gt; 别忘了 Doris 只有 8 核 ×2,Trino 不限 CPU(56 核)+ 32GB 堆 ×2。&lt;strong&gt;Doris 用 1/7 的 CPU 还全面更快&lt;/strong&gt;——正经给它配资源会更夸张。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而&quot;数据已入库 Doris 原生&quot;的地板更是低到 0.3–0.8 秒(图里灰杠)。也就是说,如果冷数据值得反复查,&lt;strong&gt;把它 ingest 进 Doris&lt;/strong&gt; 比任何 Iceberg 直读都快一个数量级。&lt;/p&gt;
&lt;h2&gt;五、那联邦什么时候还能用?&lt;/h2&gt;
&lt;p&gt;公平地说,Trino 联邦不是一无是处:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;下推友好的查询&lt;/strong&gt;(单表聚合、过滤、点查):整段推给 Doris,联邦零损耗,甚至更快(Q1)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冷扫描完全主导、join 可忽略&lt;/strong&gt;:大事实表全表扫,小维表点缀(Q2),联邦和专用引擎打平——反正瓶颈在 GCS 带宽。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;真正的价值是&quot;一个 SQL 跨多个数据源&quot;的便利性&lt;/strong&gt;,而不是性能。当你只是偶尔关联一下、数据量不大时,省去搬数据建表的功夫很香。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但只要满足&quot;&lt;strong&gt;把另一个引擎里的大块关系数据当 join 的一边&lt;/strong&gt;&quot;,联邦就会被单线程 MySQL 拉取拖垮。&lt;/p&gt;
&lt;h2&gt;六、选型小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;冷数据 + 需要 join + 在意延迟&lt;/strong&gt; → &lt;strong&gt;专用 Doris 直读 Iceberg&lt;/strong&gt;(外部 catalog)。省掉跨引擎搬运,MPP 并行扫对象存储,本地维表零成本。这就是开头那个问题的答案:&lt;strong&gt;值得为冷数据单独立一个 Doris。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冷数据值得反复查&lt;/strong&gt; → 干脆 ingest 进 Doris 原生表,亚秒级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trino 联邦&lt;/strong&gt; → 留给&quot;跨源便利性 &amp;gt; 性能&quot;的探索性查询,或能整段下推的单表分析。别拿它扛大维表 join。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个反直觉但重要的点&lt;/strong&gt;:冷数据查询的天花板是对象存储扫描速度,不是引擎。选型真正影响的是&lt;strong&gt;扫描之上&lt;/strong&gt;的部分——而那恰恰是联邦最吃亏的地方。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有数字都是在&lt;strong&gt;同一份 Lakekeeper/GCS 上的 Iceberg 表&lt;/strong&gt;上跑出来的——开放表格式让&quot;一份冷数据、多引擎按需取用&quot;成为可能;而这一篇说明:取用的&lt;strong&gt;方式&lt;/strong&gt;(联邦拉取 vs 引擎直读),对冷数据查询的体验差出好几倍。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;📚 本文是「自建 Lakehouse 实战」系列(共 6 篇):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-1-setup/&quot;&gt;搭建篇&lt;/a&gt; —— Lakekeeper + GCS + 五大引擎接入&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-2-interop/&quot;&gt;互操作篇&lt;/a&gt; —— 跨引擎读写、MERGE 与 positional delete 合规性&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-3-benchmark/&quot;&gt;性能篇&lt;/a&gt; —— Iceberg 读性能横评:ClickHouse vs Doris vs StarRocks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;联邦篇(本文)&lt;/strong&gt; —— Trino 联邦查询 vs 专用 Doris:冷数据 Iceberg ⋈ 关系表的代价&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-5-be-scaling/&quot;&gt;扩容篇&lt;/a&gt; —— Doris 存算分离 BE 扩容量化:加算力到底快多少&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-6-cdc/&quot;&gt;实时入湖篇&lt;/a&gt; —— Flink CDC:MariaDB / PostgreSQL → Doris 存算分离,延迟与节点数的真相&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>在 kind 上自建 Lakehouse(三):Iceberg 读性能横评 —— ClickHouse vs Doris vs StarRocks(TPC-H SF10)</title><link>https://notes.ezworker.cc/posts/lakehouse-on-kind-3-benchmark/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/lakehouse-on-kind-3-benchmark/</guid><description>在同一张 Lakekeeper/GCS 上的 Iceberg 表(TPC-H SF10)上,横评 ClickHouse 26.5、Doris 4.1.1、StarRocks 4.1.1 的读性能:冷读 vs 热读、Trino 写 vs Spark 写、缓存深挖。StarRocks 热读最快但强依赖缓存;Doris 最稳、几乎不吃缓存;ClickHouse…</description><pubDate>Sun, 07 Jun 2026 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是「自建 Lakehouse 实战」系列第三篇,也是最硬核的一篇。前两篇(&lt;a href=&quot;/posts/lakehouse-on-kind-1-setup/&quot;&gt;搭建&lt;/a&gt;、&lt;a href=&quot;/posts/lakehouse-on-kind-2-interop/&quot;&gt;互操作&lt;/a&gt;)解决了&quot;能不能用、能不能互通&quot;;本篇回答&quot;谁读得快&quot;。
对象:&lt;strong&gt;ClickHouse 26.5、Doris 4.1.1、StarRocks 4.1.1&lt;/strong&gt;,都读同一张 Lakekeeper/GCS 上的 Iceberg 表。(Trino 和 Spark 在这里是&lt;strong&gt;数据写入器&lt;/strong&gt;,用来对照&quot;写入器是否影响读性能&quot;。)&lt;/p&gt;
&lt;h2&gt;一、基准设置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据集&lt;/strong&gt;:TPC-H SF10。用 Trino 的 &lt;code&gt;tpch&lt;/code&gt; connector CTAS 写成 Iceberg &lt;code&gt;bench_trino.*&lt;/code&gt;(lineitem 5999 万行 = &lt;strong&gt;1.53GB / 84 个月度分区&lt;/strong&gt;;orders 1500 万、customer 150 万、part 200 万…),再用 Spark CTAS 复制成 &lt;code&gt;bench_spark.*&lt;/code&gt;(同内容、不同写入器)。实测 Trino 与 Spark 的文件布局几乎一致(都约每分区 1 文件)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;join 测试&lt;/strong&gt;:每个引擎各自加载一份本地原生表(&lt;code&gt;lineitem/orders/customer/nation/region&lt;/code&gt;),以测&quot;本地表 + Iceberg 表&quot;混合 join。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冷读 vs 热读&lt;/strong&gt;:冷读 = 关闭引擎数据缓存跑 1 次(纯 GCS 扫描);热读 = 开缓存、预热后取 3 次平均。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拓扑(重要前提)&lt;/strong&gt;:Doris = 2 BE;StarRocks 后期用官方 operator 部成 1 FE + 2 BE;ClickHouse 的 &lt;code&gt;DataLakeCatalog&lt;/code&gt; + 本地表为&lt;strong&gt;单节点&lt;/strong&gt;。所以下面的对比并非严格同拓扑,解读时请带上这个上下文。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查询场景:&lt;strong&gt;S1&lt;/strong&gt; 单 Iceberg 表(全表扫描 / 分区裁剪 / TPC-H Q1 聚合);&lt;strong&gt;S2&lt;/strong&gt; Iceberg 表之间 2→3→4 路 join;&lt;strong&gt;S3/S4&lt;/strong&gt; 本地表 + Iceberg 表混合 join(small/big 指结果集大小)。&lt;/p&gt;
&lt;h2&gt;二、总览:热读耗时&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/iceberg-read-warm-by-engine-1.avif&quot; alt=&quot;各引擎热读耗时对比,对数轴&quot; /&gt;&lt;/p&gt;
&lt;p&gt;热读耗时(Trino 写入数据,对数轴)。越低越好。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;ClickHouse&lt;/th&gt;
&lt;th&gt;Doris&lt;/th&gt;
&lt;th&gt;StarRocks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;S1 全表扫描&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;4.1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.9&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S1 分区裁剪&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S1 group by(Q1)&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S2 2 表 join&lt;/td&gt;
&lt;td&gt;10.4&lt;/td&gt;
&lt;td&gt;6.3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S2 3 表 join&lt;/td&gt;
&lt;td&gt;17.3&lt;/td&gt;
&lt;td&gt;9.6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S2 4 表 join&lt;/td&gt;
&lt;td&gt;17.0&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 小(3本地+1iceberg)&lt;/td&gt;
&lt;td&gt;2.1&lt;/td&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.6&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S3 大&lt;/td&gt;
&lt;td&gt;3.6&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.3&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S4 小(3本地+3iceberg)&lt;/td&gt;
&lt;td&gt;93.0&lt;/td&gt;
&lt;td&gt;1.9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;S4 大&lt;/td&gt;
&lt;td&gt;96.5&lt;/td&gt;
&lt;td&gt;3.8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;热读状态下 &lt;strong&gt;StarRocks 一骑绝尘&lt;/strong&gt;(简单查询亚秒级),Doris 居中,ClickHouse 在简单扫描上接近、但一到宽 join(S4)就崩到 90 秒级——这点单独说。&lt;/p&gt;
&lt;h2&gt;三、join 随表数扩展:ClickHouse 的单节点断崖&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/iceberg-join-scaling-cold-1.avif&quot; alt=&quot;多表 join 冷读随表数扩展&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Iceberg 多表 join 冷读随表数扩展(2→3→4 表)。&lt;/p&gt;
&lt;p&gt;2/3/4 表 join 时,Doris、StarRocks 基本平稳在 10 秒级以内,而 &lt;strong&gt;ClickHouse 从 3 表的 18 秒陡增到 4 表的 38 秒&lt;/strong&gt;。更极端的是 S4(6 张表的混合 join):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ClickHouse:S4 热读 90–100 秒&lt;/strong&gt;,而 Doris/StarRocks 只要 2–6 秒。&lt;/li&gt;
&lt;li&gt;而且即便给 ClickHouse 喂 20GB 缓存,S4 仍停在 ~80 秒——这是&lt;strong&gt;计算瓶颈,缓存救不了&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;连 S4-small(结果集很小)也要 90 秒,因为 6000 万行的本地扫描 + 多路 hash build 主导了耗时,与结果大小无关。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根因是 ClickHouse 这套是&lt;strong&gt;单节点&lt;/strong&gt;跑宽多路 join,缺少分布式 shuffle 的并行优势。它真正有竞争力的场景是简单扫描和分区裁剪。&lt;/p&gt;
&lt;h2&gt;四、写入器影响:Trino 写 vs Spark 写&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/iceberg-writer-effect-spark-vs-trino-1.avif&quot; alt=&quot;Spark 写与 Trino 写的冷读耗时比值&quot; /&gt;&lt;/p&gt;
&lt;p&gt;写入器影响:Spark/Trino 冷读耗时比值,&amp;gt;1 表示读 Spark 写的数据更慢。&lt;/p&gt;
&lt;p&gt;这是个常被问到的问题:同样的数据,用 Trino 写还是 Spark 写,会不会影响下游读性能?结论:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Doris、StarRocks:基本没差别&lt;/strong&gt;(在噪声范围内,有时 Spark 写的还更快)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ClickHouse:读 Spark 写的数据冷读稳定慢 20–30%&lt;/strong&gt;(S1 5.3→6.7、S2 join2 12.3→16.0、join3 18.2→22.6);热读差距变小。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于两者文件布局几乎一致,ClickHouse 这点残留的冷读惩罚,推测来自 parquet row-group/统计/编码的细微差异与 CH 的 GCS reader 的交互。&lt;strong&gt;净结论:选 Trino 还是 Spark 当写入器,对读性能影响不大,只有 ClickHouse 冷读对 Spark 文件有轻微惩罚。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;五、缓存深挖:StarRocks 的快是不是全靠缓存?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/starrocks-cache-deep-dive-1.avif&quot; alt=&quot;StarRocks 不同缓存配置对比 Doris&quot; /&gt;&lt;/p&gt;
&lt;p&gt;StarRocks 全量缓存 vs ~1GB vs 关闭,对比 Doris(对数轴)。&lt;/p&gt;
&lt;p&gt;StarRocks 热读那么快,很大程度来自它的 &lt;strong&gt;datacache&lt;/strong&gt;。于是专门压了三档:全量缓存、缩到 ~1GB、完全关闭。发现:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;当缓存小于工作集,StarRocks 的优势会崩塌&lt;/strong&gt;:全量缓存下 1–2 秒的查询,在 1GB 缓存下变成 5–10 秒(≈ 裸 GCS 扫描 ≈ Doris);S2 join2 慢了 5 倍(2.0→10.1 秒),此时&lt;strong&gt;反而输给 Doris&lt;/strong&gt;(6.3 秒)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Doris 几乎不吃缓存&lt;/strong&gt;:热读 ≈ 关缓存(如 join4 是 9.96 vs 9.14 秒)——它在这些聚合上是计算瓶颈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;StarRocks 完全关缓存、直连 GCS 的路径在本环境不稳定&lt;/strong&gt;:join/groupby 会报错(连接超时、parquet streaming reader 的 &quot;two dictionary page&quot; / &quot;io ranges overlapped&quot;),只有简单扫描能跑。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以一句话:&lt;strong&gt;StarRocks 是&quot;缓存命中&quot;的赌注&lt;/strong&gt;——热集装得下缓存时碾压全场,装不下就跌回 GCS 速度;&lt;strong&gt;Doris 则是可预测、与缓存无关&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;六、ClickHouse 缓存修正:并不是不缓存,而是默认没配&lt;/h2&gt;
&lt;p&gt;一开始 ClickHouse 看起来&quot;又慢又不缓存&quot;,后来发现这是&lt;strong&gt;配置缺口&lt;/strong&gt;:&lt;code&gt;DataLakeCatalog&lt;/code&gt; 默认&lt;strong&gt;没有文件系统缓存&lt;/strong&gt;。补上具名缓存(&lt;code&gt;filesystem_caches&lt;/code&gt; 配 path + max_size,查询时 &lt;code&gt;SET enable_filesystem_cache=1&lt;/code&gt;)后,ClickHouse 会缓存对象存储数据,热读快 &lt;strong&gt;2–4.5 倍&lt;/strong&gt;(S1 扫描 5.5→1.2 秒、S2 join2 12.5→2.8 秒)。给足 20GB 缓存后,它在简单查询上甚至&lt;strong&gt;反超 Doris&lt;/strong&gt;(仅次于 StarRocks)。
&lt;img src=&quot;/wp-images/iceberg-fair-comparison-full-cache-1.avif&quot; alt=&quot;三引擎都给足缓存的公平对比&quot; /&gt;&lt;/p&gt;
&lt;p&gt;公平对比:三引擎都给足缓存(ClickHouse 20GB / Doris 热 / StarRocks 全量,对数轴)。&lt;/p&gt;
&lt;p&gt;这张&quot;都给足缓存&quot;的公平对比里能清楚看到:简单扫描/聚合 ClickHouse 与 Doris 接近、StarRocks 最快;但 &lt;strong&gt;S4 宽 join,ClickHouse 仍是 80 秒的断崖&lt;/strong&gt;——再次印证那是单节点计算瓶颈,缓存无能为力。&lt;/p&gt;
&lt;h2&gt;七、StarRocks 拓扑:单 BE vs 1 FE + 2 BE&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/wp-images/starrocks-1be-vs-2be-vs-doris-1.avif&quot; alt=&quot;StarRocks 单BE与1FE2BE对比 Doris&quot; /&gt;&lt;/p&gt;
&lt;p&gt;StarRocks allin1(1 BE)vs 1 FE + 2 BE vs Doris(2 BE),热读对数轴。缺失的浅红柱 = allin1 在大 join 上报错。&lt;/p&gt;
&lt;p&gt;为了和 Doris 同口径(都 2 BE),又用官方 operator 把 StarRocks 重部成 1 FE + 2 BE,有两个发现:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;之前那个 &lt;code&gt;invalid pos&lt;/code&gt; 大 join 报错是&quot;单 BE 特有&quot;的&lt;/strong&gt;:在 1 FE + 2 BE 上,S3-big / S4-big 干净跑过(1.3 秒 / 2.7 秒热读)。&lt;/li&gt;
&lt;li&gt;但&lt;strong&gt;对比自己的 allin1(1 BE),2 BE 在简单/中等查询上反而略慢&lt;/strong&gt;(S1 group by 1.8 vs 0.8、S2 4 表 join 4.7 vs 3.4):在单机 ~6000 万行这个量级,跨 BE 的 exchange/shuffle + 拆分的 datacache 开销,比一个&quot;胖 BE&quot;省下的更多。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即便如此,1 FE + 2 BE 的 StarRocks 仍在各场景全面快于 Doris(2 BE)。&lt;/p&gt;
&lt;h2&gt;八、选型小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;StarRocks&lt;/strong&gt;:热读最快,简单查询亚秒级——前提是&lt;strong&gt;热数据装得进缓存&lt;/strong&gt;;否则回落到 GCS 速度。关缓存直连对象存储在本环境还不稳。适合&quot;热集明确、缓存喂得饱&quot;的交互式分析。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Doris&lt;/strong&gt;:最稳、最可预测,几乎不依赖缓存(计算瓶颈型),宽 join 表现扎实、无失败。适合&quot;工作集大、对延迟稳定性要求高&quot;的场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ClickHouse&lt;/strong&gt;:简单扫描/分区裁剪有竞争力(配好文件系统缓存后更佳),但&lt;strong&gt;单节点跑宽多路 join 会断崖&lt;/strong&gt;(S4 达 80–100 秒),且缓存救不了。它的强项不在这种多表 join 工作负载。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入器&lt;/strong&gt;(Trino vs Spark):对读性能影响可忽略,仅 ClickHouse 冷读对 Spark 文件有约 20–30% 的轻微惩罚。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有数字都是在&lt;strong&gt;同一张 Lakekeeper/GCS Iceberg 表&lt;/strong&gt;上跑出来的——这正是开放表格式的价值:一份数据,多引擎按需取用,各取所长。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;📚 本文是「自建 Lakehouse 实战」系列(共 6 篇):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-1-setup/&quot;&gt;搭建篇&lt;/a&gt; —— Lakekeeper + GCS + 五大引擎接入&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-2-interop/&quot;&gt;互操作篇&lt;/a&gt; —— 跨引擎读写、MERGE 与 positional delete 合规性&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能篇(本文)&lt;/strong&gt; —— Iceberg 读性能横评:ClickHouse vs Doris vs StarRocks&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-4-federation/&quot;&gt;联邦篇&lt;/a&gt; —— Trino 联邦查询 vs 专用 Doris:冷数据 Iceberg ⋈ 关系表的代价&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-5-be-scaling/&quot;&gt;扩容篇&lt;/a&gt; —— Doris 存算分离 BE 扩容量化:加算力到底快多少&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-6-cdc/&quot;&gt;实时入湖篇&lt;/a&gt; —— Flink CDC:MariaDB / PostgreSQL → Doris 存算分离,延迟与节点数的真相&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>在 kind 上自建 Lakehouse(二):五引擎共写一张 Iceberg 表,跨引擎读写与 positional delete 合规性实测</title><link>https://notes.ezworker.cc/posts/lakehouse-on-kind-2-interop/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/lakehouse-on-kind-2-interop/</guid><description>让 Doris / Trino / Spark / StarRocks 读写同一张 Iceberg 表,实测各自的 CRUD + MERGE 能力,以及&quot;谁读得动谁的表&quot;。核心发现:Doris 写出的 positional delete 文件缺少 Iceberg 保留 field-id,导致 Trino 等严格引擎报 position is…</description><pubDate>Sun, 07 Jun 2026 11:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是「自建 Lakehouse 实战」系列第二篇。&lt;a href=&quot;/posts/lakehouse-on-kind-1-setup/&quot;&gt;第一篇&lt;/a&gt;把五大引擎都接到了同一张 Lakekeeper/GCS 上的 Iceberg 目录。本篇问一个直接的问题:**它们写出来的表,彼此读得动吗?**尤其是涉及行级删除(merge-on-read)时。
这正是 Iceberg &quot;开放表格式、引擎无关&quot;承诺的试金石。结论先放这儿:&lt;strong&gt;绝大多数组合都能互读,唯独 Doris 在做了 UPDATE/DELETE/MERGE 之后写出的表,Trino 等严格引擎读不了&lt;/strong&gt;——根因是 Doris 的 positional delete 文件不符合 Iceberg 规范。下面是完整过程。&lt;/p&gt;
&lt;h2&gt;一、各引擎的原生 CRUD + MERGE&lt;/h2&gt;
&lt;p&gt;四个引擎都建 &lt;code&gt;format-version=2&lt;/code&gt; 的 Iceberg 表,跑 INSERT / SELECT / UPDATE / DELETE / 三分支 MERGE INTO:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;引擎&lt;/th&gt;
&lt;th&gt;INSERT/SELECT&lt;/th&gt;
&lt;th&gt;UPDATE/DELETE&lt;/th&gt;
&lt;th&gt;MERGE INTO&lt;/th&gt;
&lt;th&gt;路径风格&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trino 481&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅(三分支全过)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gs://&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spark 3.5.8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅(需第一篇的 4 个修复)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gs://&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Doris 4.1.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅(需 4.1.0+)&lt;/td&gt;
&lt;td&gt;✅(三分支全过)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;s3://&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;StarRocks 4.1.1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;DELETE ✅ / UPDATE 受限&lt;/td&gt;
&lt;td&gt;❌(V2 MERGE 仍在开发,#73684)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Trino、Spark、Doris 的行级 DML 都完整可用;StarRocks 的 DELETE 能产出 merge-on-read 删除文件(这就够验证合规性了),但 UPDATE 在 4.1.1 上报&quot;does not support update&quot;,MERGE INTO 还没合并。&lt;/p&gt;
&lt;h2&gt;二、跨引擎读矩阵&lt;/h2&gt;
&lt;p&gt;把上面写出来的表互相读一遍,得到下表(同一张 Lakekeeper 目录、同一个 GCS 桶):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;表的来源&lt;/th&gt;
&lt;th&gt;Trino 读&lt;/th&gt;
&lt;th&gt;Doris 读&lt;/th&gt;
&lt;th&gt;Spark 读&lt;/th&gt;
&lt;th&gt;StarRocks 读&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trino(含 MERGE 删除)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spark(含 MERGE 删除)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;StarRocks(含 DELETE 删除)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doris —— 纯 INSERT&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Doris —— 含 UPDATE/DELETE/MERGE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ &lt;code&gt;position is null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌*&lt;/td&gt;
&lt;td&gt;❌*&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;* Trino 的失败是实测确认的;Spark、StarRocks 同样按 field-id 读取删除文件,机理一致,预期同样失败。Doris 能读自己的表,是因为它按列名而非 field-id 读取。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;三、根因:Doris 的 positional delete 文件缺少保留 field-id&lt;/h2&gt;
&lt;p&gt;这个 bug 值得展开,因为它非常隐蔽。把出问题的删除文件从 GCS 下载下来,用 pyarrow 检查 parquet 的 field-id,真相才浮出水面:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Iceberg 规范规定,positional delete 文件的两列必须带&lt;strong&gt;保留 field-id&lt;/strong&gt;:&lt;code&gt;file_path = 2147483546&lt;/code&gt;、&lt;code&gt;pos = 2147483545&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;引擎读删除文件时,是&lt;strong&gt;按 field-id 定位列,而不是按列名&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trino 的删除文件&lt;/strong&gt;:field-id 正确。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;StarRocks 的删除文件&lt;/strong&gt;:field-id 也正确(&lt;code&gt;2147483546&lt;/code&gt; / &lt;code&gt;2147483545&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Doris 的删除文件&lt;/strong&gt;:这两列的 &lt;code&gt;PARQUET:field_id&lt;/code&gt; 都是 &lt;code&gt;None&lt;/code&gt;——Doris 没写保留 field-id。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是 Trino 按 id &lt;code&gt;2147483545&lt;/code&gt; 去找 &lt;code&gt;pos&lt;/code&gt; 列,找不到,返回 null → 报错 &lt;code&gt;position is null&lt;/code&gt;。这&lt;strong&gt;不是 Trino 的能力缺陷&lt;/strong&gt;(它能完整读写自己和 Spark 的 merge-on-read 删除),而是 &lt;strong&gt;Doris 写出了不符合规范的删除文件&lt;/strong&gt;。
需要澄清的一点:Trino 并不是 copy-on-write。用 &lt;code&gt;SELECT content, file_path FROM &quot;表名$files&quot;&lt;/code&gt; 可以确认 Trino 和 Doris 都是 merge-on-read(&lt;code&gt;content=1&lt;/code&gt; 即 position delete)。两者唯一的差别就是那对 field-id。
(次要差别:Doris 把路径写成 &lt;code&gt;s3://&lt;/code&gt;(GCS S3 互操作),Trino 是 &lt;code&gt;gs://&lt;/code&gt;——这个靠在 Trino 里也打开 &lt;code&gt;fs.native-s3.enabled&lt;/code&gt; 能解决;但 field-id 缺失才是真正的硬障碍。)&lt;/p&gt;
&lt;h2&gt;四、实践建议&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;行级 DML 尽量放在 Trino 或 Spark 做&lt;/strong&gt;:它们的删除文件是通用可读的。&lt;/li&gt;
&lt;li&gt;如果一张 Doris 表需要被其它引擎读,要么&lt;strong&gt;保持纯 INSERT&lt;/strong&gt;(无删除文件),要么让 Doris 走 &lt;strong&gt;copy-on-write&lt;/strong&gt;(&lt;code&gt;write.update.mode&lt;/code&gt;/&lt;code&gt;write.delete.mode&lt;/code&gt;/&lt;code&gt;write.merge.mode&lt;/code&gt; 设为 &lt;code&gt;copy_on_write&lt;/code&gt;),这样不产生删除文件。&lt;/li&gt;
&lt;li&gt;StarRocks 在合规性上是干净的——它写的删除文件 Trino 读得动;只是它自身的 UPDATE/MERGE 能力当前还有限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;Iceberg &quot;引擎无关&quot;的承诺在这套环境里基本兑现了:四个引擎共用一张表、互相读取大多顺畅。唯一的破绽是 Doris 的 positional delete 不写保留 field-id,导致严格按规范读取的引擎失败——一个仅凭 SELECT 报错很难定位、必须下载文件看 field-id 才能坐实的细节。下一篇换个维度:在读性能上,这些引擎差多少?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;📚 本文是「自建 Lakehouse 实战」系列(共 6 篇):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-1-setup/&quot;&gt;搭建篇&lt;/a&gt; —— Lakekeeper + GCS + 五大引擎接入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互操作篇(本文)&lt;/strong&gt; —— 跨引擎读写、MERGE 与 positional delete 合规性&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-3-benchmark/&quot;&gt;性能篇&lt;/a&gt; —— Iceberg 读性能横评:ClickHouse vs Doris vs StarRocks&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-4-federation/&quot;&gt;联邦篇&lt;/a&gt; —— Trino 联邦查询 vs 专用 Doris:冷数据 Iceberg ⋈ 关系表的代价&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-5-be-scaling/&quot;&gt;扩容篇&lt;/a&gt; —— Doris 存算分离 BE 扩容量化:加算力到底快多少&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-6-cdc/&quot;&gt;实时入湖篇&lt;/a&gt; —— Flink CDC:MariaDB / PostgreSQL → Doris 存算分离,延迟与节点数的真相&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>在 kind 上自建 Lakehouse(一):Lakekeeper + GCS + 五大查询引擎接入实战</title><link>https://notes.ezworker.cc/posts/lakehouse-on-kind-1-setup/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/lakehouse-on-kind-1-setup/</guid><description>用 Lakekeeper 作为 Iceberg REST Catalog、GCS 作数据仓、CloudNativePG 作元数据后端,在本地 kind 集群上搭一套 Lakehouse,并把 Doris / Trino / Spark / StarRocks / ClickHouse 五大引擎接到同一张目录上——逐个记录各引擎的接入配置与 GCS 认证踩坑。</description><pubDate>Sun, 07 Jun 2026 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是「自建 Lakehouse 实战」系列的第一篇。整个系列围绕一个目标:在本地 &lt;strong&gt;kind&lt;/strong&gt;(Kubernetes in Docker / podman)集群上,用开源组件搭一套以 &lt;strong&gt;Apache Iceberg&lt;/strong&gt; 为表格式、对象存储为底座的 Lakehouse,然后把市面上主流的查询/计算引擎都接到&lt;strong&gt;同一张 Iceberg 目录&lt;/strong&gt;上,做跨引擎互操作与读性能的横向对比。
本篇先把地基打好:目录服务(Lakekeeper)、元数据后端(CloudNativePG)、数据仓(GCS),以及五大引擎各自如何接入。后两篇分别讲跨引擎互操作性和读性能横评(见文末系列导航)。&lt;/p&gt;
&lt;h2&gt;架构总览&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编排&lt;/strong&gt;:本地 kind 集群(3 节点,podman provider)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;表格式&lt;/strong&gt;:Apache Iceberg(format-version 2,支持 merge-on-read 行级删除)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录&lt;/strong&gt;:&lt;strong&gt;Lakekeeper&lt;/strong&gt; —— 一个 Iceberg REST Catalog 实现(本次用 v0.12.2)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目录后端&lt;/strong&gt;:&lt;strong&gt;CloudNativePG&lt;/strong&gt; 管理的 PostgreSQL(1 主 1 从)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据仓&lt;/strong&gt;:Google Cloud Storage 专用桶 &lt;code&gt;gs://your-bucket&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引擎&lt;/strong&gt;:Apache Doris 4.1.1、Trino 481、Apache Spark 3.5.8、StarRocks 4.1.1、ClickHouse 26.5 —— 全部读写/查询同一张 Lakekeeper 目录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;一、目录后端:CloudNativePG&lt;/h2&gt;
&lt;p&gt;Lakekeeper 的元数据(命名空间、表、快照指针等)存在 PostgreSQL 里。用 CloudNativePG operator(v1.29.1)起一个 2 实例(1 主 1 从)的 PG 17 集群:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会自动生成读写分离的 Service:&lt;code&gt;pg-lakekeeper-rw&lt;/code&gt;(主/写)、&lt;code&gt;pg-lakekeeper-ro&lt;/code&gt;(从/读),应用凭据放在 secret &lt;code&gt;pg-lakekeeper-app&lt;/code&gt; 里。&lt;/p&gt;
&lt;h2&gt;二、目录服务:Lakekeeper&lt;/h2&gt;
&lt;p&gt;用官方 Helm chart 部署,关掉内置 PG、指向上面的外部 PG:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ &lt;strong&gt;那把加密 key 一定要预先创建并妥善保存&lt;/strong&gt;:Lakekeeper 用它加密存储在 PG 里的存储凭据,丢了就等于丢了所有 warehouse 的凭据。
服务起来后(&lt;code&gt;GET /health&lt;/code&gt;、&lt;code&gt;GET /management/v1/info&lt;/code&gt; 检查),它没配 OpenID,authz 是 allow-all 单租户模式,调用 API 不需要 token。Iceberg REST 的基础地址是:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalog
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;挂上 GCS warehouse&lt;/h3&gt;
&lt;p&gt;专门开一个 GCS 桶和一个服务账号(授 &lt;code&gt;roles/storage.objectAdmin&lt;/code&gt;),然后两步把 warehouse 建出来:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1) 初始化
POST /management/v1/bootstrap  {&quot;accept-terms-of-use&quot;: 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]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意:和 Doris 用 HMAC(S3 互操作)不同,Lakekeeper 这里走的是 &lt;strong&gt;原生 GCS 凭据&lt;/strong&gt;(&lt;code&gt;service-account-key&lt;/code&gt;,把整段 SA JSON 嵌进 storage-credential)。后面各引擎读数据文件时,是各自直连 GCS,而不一定走 Lakekeeper 的 vended credentials。&lt;/p&gt;
&lt;h2&gt;三、五大引擎接入&lt;/h2&gt;
&lt;p&gt;所有引擎连的都是上面那串 REST 地址,&lt;code&gt;warehouse&lt;/code&gt; 参数填的是 warehouse 的&lt;strong&gt;名字&lt;/strong&gt;(&lt;code&gt;iceberg&lt;/code&gt;),不是 &lt;code&gt;gs://&lt;/code&gt; 路径。难点几乎都在 &lt;strong&gt;GCS 认证怎么喂给引擎&lt;/strong&gt;。逐个说。&lt;/p&gt;
&lt;h3&gt;1) Apache Doris 4.1.1(存算分离)&lt;/h3&gt;
&lt;p&gt;Doris 通过 REST 连目录,数据文件用 &lt;strong&gt;HMAC S3 互操作密钥&lt;/strong&gt;(&lt;code&gt;gs.*&lt;/code&gt; 属性)访问 GCS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE CATALOG iceberg PROPERTIES(
  &apos;type&apos;=&apos;iceberg&apos;,
  &apos;iceberg.catalog.type&apos;=&apos;rest&apos;,
  &apos;iceberg.rest.uri&apos;=&apos;http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalog&apos;,
  &apos;warehouse&apos;=&apos;iceberg&apos;,
  &apos;gs.endpoint&apos;=&apos;https://storage.googleapis.com&apos;,
  &apos;gs.region&apos;=&apos;us-central1&apos;,
  &apos;gs.access_key&apos;=&apos;[HMAC-ak]&apos;, &apos;gs.secret_key&apos;=&apos;[HMAC-sk]&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Iceberg 行级 DML(UPDATE/DELETE/MERGE INTO)需要 Doris 4.1.0+&lt;/strong&gt;;3.1.x 只能 INSERT/append + 读。这正是把集群从 3.1.4 升到 4.1.1 的原因。建表带 &lt;code&gt;&apos;format-version&apos;=&apos;2&apos;&lt;/code&gt; 才有 merge-on-read 删除。&lt;/p&gt;
&lt;h3&gt;2) Trino 481&lt;/h3&gt;
&lt;p&gt;Helm chart 部署,catalog 配置:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;坑点:Trino 自己把路径写成 &lt;code&gt;gs://&lt;/code&gt;,但要读 Doris 写的表得&lt;strong&gt;同时&lt;/strong&gt;打开原生 S3 文件系统(&lt;code&gt;fs.native-s3.enabled&lt;/code&gt;),因为 Doris 的路径是 &lt;code&gt;s3://&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;3) Apache Spark 3.5.8(Spark K8s Operator)&lt;/h3&gt;
&lt;p&gt;通过 Spark Kubernetes Operator 提交 &lt;code&gt;SparkApplication&lt;/code&gt;。catalog 配置走 &lt;code&gt;SparkCatalog&lt;/code&gt; + REST + &lt;code&gt;GCSFileIO&lt;/code&gt;。这个引擎踩了&lt;strong&gt;四个&lt;/strong&gt;坑,每个都让一次任务失败:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;iceberg-gcp 的 vended credentials 报 NotSerializableException&lt;/strong&gt;(&lt;code&gt;OAuth2RefreshCredentialsHandler&lt;/code&gt;),即便序列化过了,凭据派发还会导致&quot;Failed to load committed snapshot&quot;——写成功但读回为空。&lt;strong&gt;解法:在 Lakekeeper warehouse 上关掉凭据派发&lt;/strong&gt;(&lt;code&gt;storage-profile.sts-enabled=false&lt;/code&gt;),让 Spark 用挂载的 SA key 走纯 ADC。Doris/Trino 不受影响,它们本来就用自己的静态凭据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;iceberg 1.10+/1.11 需要 Java 17&lt;/strong&gt;(默认 &lt;code&gt;apache/spark:3.5.x&lt;/code&gt; 是 Java 11,报 class file version 61.0)。换 &lt;code&gt;apache/spark:3.5.8-java17-python3&lt;/code&gt; 镜像。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同一会话里连续 DML 报 &lt;code&gt;Found conflicting files&lt;/code&gt;&lt;/strong&gt;(可串行化隔离 + 快照缓存)。解法:&lt;code&gt;spark.sql.catalog.lk.cache-enabled=false&lt;/code&gt; + 表属性把 delete/update/merge 的隔离级别设成 &lt;code&gt;snapshot&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;失败残留表状态 → 用 &lt;code&gt;DROP TABLE IF EXISTS&lt;/code&gt; 再 &lt;code&gt;CREATE&lt;/code&gt; 保证可重跑。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4) StarRocks 4.1.1&lt;/h3&gt;
&lt;p&gt;最快路径是单 Pod &lt;code&gt;allin1&lt;/code&gt; 镜像。catalog 用 &lt;code&gt;gcp.gcs.*&lt;/code&gt; 凭据。&lt;strong&gt;GCS 认证有个隐蔽坑&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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 = …
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;gcp.gcs.*&lt;/code&gt; 能覆盖 &lt;strong&gt;BE&lt;/strong&gt; 的数据读取,但 &lt;strong&gt;FE 读元数据(snapshot/manifest 的 avro)走的是内置 Hadoop gcs-connector&lt;/strong&gt;,因为 &lt;code&gt;fs.gs.auth.type&lt;/code&gt; 没被这些属性赋值,直接 &lt;code&gt;NullPointerException&lt;/code&gt;。解法是&lt;strong&gt;非侵入式地&lt;/strong&gt;往 FE 和 BE 的 &lt;code&gt;core-site.xml&lt;/code&gt; 追加 GCS 认证:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(后面性能篇里又用官方 operator 把它重部成了 1 FE + 2 BE,同样的 &lt;code&gt;core-site.xml&lt;/code&gt; 坑要在 FE 和 BE 都补。)&lt;/p&gt;
&lt;h3&gt;5) ClickHouse 26.5&lt;/h3&gt;
&lt;p&gt;用&lt;strong&gt;官方&lt;/strong&gt; operator(不是 Altinity),CRD 为 &lt;code&gt;clickhouse.com/v1alpha1&lt;/code&gt; 的 &lt;code&gt;KeeperCluster&lt;/code&gt; + &lt;code&gt;ClickHouseCluster&lt;/code&gt;。连 Iceberg 用 &lt;code&gt;DataLakeCatalog&lt;/code&gt; 引擎:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SET allow_experimental_database_iceberg=1;
CREATE DATABASE lake ENGINE=DataLakeCatalog(&apos;http://lakekeeper.lakekeeper.svc.cluster.local:8181/catalog&apos;)
SETTINGS catalog_type=&apos;rest&apos;, warehouse=&apos;iceberg&apos;, vended_credentials=false,
  storage_endpoint=&apos;https://storage.googleapis.com/your-bucket&apos;,
  storage_uri_style=&apos;path&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个坑:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DataLakeCatalog&lt;/code&gt; 默认 &lt;code&gt;vended_credentials=true&lt;/code&gt;,而 Lakekeeper 没开 STS → 变匿名访问 → 403。必须设 &lt;strong&gt;&lt;code&gt;false&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;那些 &lt;code&gt;aws_access_key_id/secret&lt;/code&gt; 设置只对 Glue 生效,REST 存储用不到;S3 客户端的凭据来自 &lt;strong&gt;&lt;code&gt;AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY&lt;/code&gt; 环境变量&lt;/strong&gt;(默认凭据链)→ 用 GCS HMAC 互操作密钥,通过容器 env 注入。&lt;/li&gt;
&lt;li&gt;这个 kind/podman runtime 上单文件 subPath 挂载会失败(&quot;not a directory&quot;),所以配置只能走 env,不能挂 config.d 文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外 operator 默认容器配额只有 1Gi/1cpu,务必调大;&lt;code&gt;icebergGCS()&lt;/code&gt; 表函数不存在(只有 &lt;code&gt;iceberg()&lt;/code&gt;/&lt;code&gt;icebergS3()&lt;/code&gt;)。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;到这里,五个引擎都能查同一张 Lakekeeper/GCS 上的 Iceberg 表了。最大的共性痛点是 &lt;strong&gt;GCS 认证&lt;/strong&gt;:有的走 HMAC S3 互操作(Doris、Trino、ClickHouse),有的走原生 SA key(Lakekeeper、Spark、StarRocks),而 StarRocks 还要分别照顾 FE 元数据和 BE 数据两条路径。地基搭好后,接下来就能问两个有意思的问题:这些引擎写出来的表,彼此读得动吗?谁读得快?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;📚 本文是「自建 Lakehouse 实战」系列(共 6 篇):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;搭建篇(本文)&lt;/strong&gt; —— Lakekeeper + GCS + 五大引擎接入&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-2-interop/&quot;&gt;互操作篇&lt;/a&gt; —— 跨引擎读写、MERGE 与 positional delete 合规性实测&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-3-benchmark/&quot;&gt;性能篇&lt;/a&gt; —— Iceberg 读性能横评:ClickHouse vs Doris vs StarRocks&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-4-federation/&quot;&gt;联邦篇&lt;/a&gt; —— Trino 联邦查询 vs 专用 Doris:冷数据 Iceberg ⋈ 关系表的代价&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-5-be-scaling/&quot;&gt;扩容篇&lt;/a&gt; —— Doris 存算分离 BE 扩容量化:加算力到底快多少&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/lakehouse-on-kind-6-cdc/&quot;&gt;实时入湖篇&lt;/a&gt; —— Flink CDC:MariaDB / PostgreSQL → Doris 存算分离,延迟与节点数的真相&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Add user into sudoer list</title><link>https://notes.ezworker.cc/posts/add-user-into-sudoer-list/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/add-user-into-sudoer-list/</guid><description>新安装的debian 13 server系统默认不带sudo 使用 root 用户 apt install sudo -y 之后， sudo adduser [USERNAME] sudo 再用 [USERNAME] 用户登录就可以使用sudo了</description><pubDate>Sun, 28 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;新安装的debian 13 server系统默认不带sudo&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;root&lt;/code&gt;用户&lt;code&gt;apt install sudo -y&lt;/code&gt;之后，&lt;code&gt;sudo adduser [USERNAME] sudo&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;再用&lt;code&gt;[USERNAME]&lt;/code&gt;用户登录就可以使用sudo了&lt;/p&gt;
</content:encoded></item><item><title>Correclty move WSL2 distro from default drive to another drive</title><link>https://notes.ezworker.cc/posts/correclty-move-wsl2-distro-from-default-drive-to-another-drive/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/correclty-move-wsl2-distro-from-default-drive-to-another-drive/</guid><description>DO NOT USE THE MOVE FUNCTION IN SETTINGS&gt;APPLICATION it will move into encrypted WindowsApps folder and cause a lot of WSL2 failure. exmpale the correct way to do it is export…</description><pubDate>Sat, 12 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;DO NOT USE THE MOVE FUNCTION IN SETTINGS&amp;gt;APPLICATION&lt;/p&gt;
&lt;p&gt;it will move into encrypted &lt;code&gt;WindowsApps&lt;/code&gt; folder and cause a lot of WSL2 failure. &lt;a href=&quot;https://github.com/microsoft/WSL/issues/10324&quot;&gt;exmpale&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;the correct way to do it is export then import.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;check your distro name&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wsl -l
适用于 Linux 的 Windows 子系统分发:
Ubuntu (默认)
docker-desktop
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;export your distro&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wsl --export Ubuntu X:\wslbackup.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;import your distro&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wsl --import Ubuntu X:\WSL2\ X:\wslbackup.tar --version 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;get into your wsl distro and validate see if everything is working fine. then delete the backup tar.&lt;/p&gt;
</content:encoded></item><item><title>zpool replace cache disk</title><link>https://notes.ezworker.cc/posts/zpool-replace-cache-disk/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/zpool-replace-cache-disk/</guid><description>zpool cache disk需要替换，执行以下命令 zpool remove zmain sdj5 zpool remove zmain sdl5 zpool add zmain cache /dev/sdm5 zpool add zmain cache /dev/sdn5</description><pubDate>Mon, 27 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;zpool cache disk需要替换，执行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zpool remove zmain sdj5
zpool remove zmain sdl5
zpool add zmain cache /dev/sdm5
zpool add zmain cache /dev/sdn5
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>zpool replace fault disk</title><link>https://notes.ezworker.cc/posts/zpool-replace-fault-disk/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/zpool-replace-fault-disk/</guid><description>zpool有一个disk不是按label加入pool的，因各种原因导致disk的label变换之后somehow zpool报错fault disk 重新将该盘加入zpool需先 zpool labelclear -f /dev/sdg1 清除原始zpool记录 然后 zpool replace zmain 1938936576781299960…</description><pubDate>Mon, 27 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;zpool有一个disk不是按label加入pool的，因各种原因导致disk的label变换之后somehow zpool报错fault disk&lt;/p&gt;
&lt;p&gt;重新将该盘加入zpool需先 &lt;code&gt;zpool labelclear -f /dev/sdg1&lt;/code&gt;清除原始zpool记录&lt;/p&gt;
&lt;p&gt;然后&lt;/p&gt;
&lt;p&gt;&lt;code&gt;zpool replace zmain 1938936576781299960 /dev/sdg&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;此处忘了用label。。。下次再换&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;果然又出问题了，这次换成正确的label&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;zpool replace zmain 8092770932184895643 scsi-SHGST_HUH728080AL4200_R5GG9LHV
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Delete argoworkflow jobs when argoworkflow server or controller are down</title><link>https://notes.ezworker.cc/posts/delete-argoworkflow-jobs-when-argoworkflow-server-or-controller-are-down/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/delete-argoworkflow-jobs-when-argoworkflow-server-or-controller-are-down/</guid><description>当argo workflow遇到大量event堆积的时候，会有很多WorkFlow资源处于无响应的状态。本次遇到的情况是大约500-650个workflow会处于Running状态，其余的处于没有任何label的状态。 当这些事件触发的Workflow很多的时候，controller可能会因为查询workflow状态超时而变成unhealthy状态，甚至无法…</description><pubDate>Mon, 21 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;当argo workflow遇到大量event堆积的时候，会有很多WorkFlow资源处于无响应的状态。本次遇到的情况是大约500-650个workflow会处于Running状态，其余的处于没有任何label的状态。&lt;/p&gt;
&lt;p&gt;当这些事件触发的Workflow很多的时候，controller可能会因为查询workflow状态超时而变成unhealthy状态，甚至无法响应。 此时需要删除这些无label的workflow需要通过kubectl来筛选Workflow并删除&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl -n argo-events get Workflow --selector=&apos;events.argoproj.io/sensor=YOUR_WEBHOOK_LABEL,!workflows.argoproj.io/phase&apos; &amp;gt; cleanup_argo_events.txt
tail -n +2 cleanup_argo_events.txt | awk &apos;{ print $1 }&apos; | xargs -P 5 -I{} kubectl -n argo-events delete Workflow/{}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Clickhouse select count of parquet files from s3 not utilizing metadata when file is small</title><link>https://notes.ezworker.cc/posts/clickhouse-select-count-of-parquet-files-from-s3-not-utilizing-metadata-when-file-is-small/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/clickhouse-select-count-of-parquet-files-from-s3-not-utilizing-metadata-when-file-is-small/</guid><description>在clickhouse中select count(*) from s3(..., format=&apos;Parquet&apos;)如果是很小的文件会出现拉取完整文件而不是获取parquet metadata来计算行数。在我的测试中这个阈值大约是1MB，然而根据默认设置 max_download_buffer_size 应该是10MB。…</description><pubDate>Wed, 04 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在clickhouse中select count(*) from s3(..., format=&apos;Parquet&apos;)如果是很小的文件会出现拉取完整文件而不是获取parquet metadata来计算行数。在我的测试中这个阈值大约是1MB，然而根据默认设置 &lt;code&gt;max_download_buffer_size&lt;/code&gt; 应该是10MB。&lt;/p&gt;
&lt;p&gt;虽不清楚为什么在我的环境中&amp;gt;1MB的文件就会只读取metadata，但是经过和clickhouse member的讨论学习到了一些settings能强制停止prefetch行为。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注：关闭prefetch总体来说会导致查询性能下降。之所以在我的use case中需要关闭prefetch是因为存在gcs inter region data transfer cost。为了减少select count(*)的费用需要关闭prefetch&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;方法一：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SETTINGS max_download_buffer_size = 1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;方法二:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SETTINGS remote_filesystem_read_prefetch = 0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;另外补充一点，在clickhouse client中调试时可通过添加 &lt;code&gt;send_logs_level = &apos;trace&apos;&lt;/code&gt;来获取query对应的trace log&lt;/p&gt;
</content:encoded></item><item><title>rocky linux podman container permission issue</title><link>https://notes.ezworker.cc/posts/rocky-linux-podman-container-permission-issue/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/rocky-linux-podman-container-permission-issue/</guid><description>通过dockge部署wordpress+mysql遇到权限问题，debug发现是selinux禁止了启动时修改权限的操作 解决方案: cd /zmain chcon -R -t container_file_t wordpress chcon -R -t container_file_t mysql</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;通过dockge部署wordpress+mysql遇到权限问题，debug发现是selinux禁止了启动时修改权限的操作&lt;/p&gt;
&lt;p&gt;解决方案:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /zmain
chcon -R -t container_file_t wordpress
chcon -R -t container_file_t mysql
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Deploy Dragonfly with crontab backup with Docker compose</title><link>https://notes.ezworker.cc/posts/deploy-dragonfly-with-crontab-backup-with-docker-compose/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/deploy-dragonfly-with-crontab-backup-with-docker-compose/</guid><description>使用docker compose部署dragonfly并设置定时任务备份至本地磁盘 compose file: version: &quot;3.8&quot; services: dragonfly: image: docker.dragonflydb.io/dragonflydb/dragonfly ulimits: memlock: -1 ports: -…</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;使用docker compose部署dragonfly并设置定时任务备份至本地磁盘&lt;/p&gt;
&lt;p&gt;compose file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.8&quot;
services:
  dragonfly:
    image: docker.dragonflydb.io/dragonflydb/dragonfly
    ulimits:
      memlock: -1
    ports:
      - TAILSCALE_IP:16379:6379
    # For better performance, consider `host` mode instead `port` to avoid docker NAT.
    # `host` mode is NOT currently supported in Swarm Mode.
    # https://docs.docker.com/compose/compose-file/compose-file-v3/#network_mode
    # network_mode: &quot;host&quot;
    volumes:
      - /zmain/df/data:/data:rwz
    networks:
      - dockge_default
    command:
      - dragonfly
      - --logtostderr
      - --snapshot_cron
      - &quot;*/30 * * * *&quot;
networks:
  dockge_default:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意坑点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dragonfly运行时使用的用户是dfly， 如果你进入容器使用id查看可以看到uid:gid是999:999&lt;/li&gt;
&lt;li&gt;如果遇到save权限问题，在宿主机内对目标目录进行chown -R 999:999 TARGET_FOLDER这样挂载后能识别为dfly:dfly并成功保存&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Docker compose with nginx reverse proxy</title><link>https://notes.ezworker.cc/posts/docker-compose-with-nginx-reverse-proxy/</link><guid isPermaLink="true">https://notes.ezworker.cc/posts/docker-compose-with-nginx-reverse-proxy/</guid><description>背景： Dockge部署了多个docker compose，之前暴露端口全部绑定为tailscale的内网ip，现增加nginx proxy manager反向代理来暴露部分应用至公网 wordpress docker compose: version: &quot;3.1&quot; services: wordpress: image: wordpress…</description><pubDate>Mon, 02 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;背景： Dockge部署了多个docker compose，之前暴露端口全部绑定为tailscale的内网ip，现增加nginx proxy manager反向代理来暴露部分应用至公网&lt;/p&gt;
&lt;p&gt;wordpress docker compose:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.1&quot;
services:
  wordpress:
    image: wordpress
    restart: always
    ports:
      - TAILSCALE_IP:8080:80
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: *********
      WORDPRESS_DB_PASSWORD: *********
      WORDPRESS_DB_NAME: *********
      WP_REDIS_HOST: *********
      WP_REDIS_PORT: *********
    volumes:
      - /zmain/wordpress/var/www/html:/var/www/html:rwz
    networks:
      - dockge_default
  db:
    image: mysql
    restart: always
    environment:
      MYSQL_DATABASE: *********
      MYSQL_USER: *********
      MYSQL_PASSWORD: *********
      MYSQL_RANDOM_ROOT_PASSWORD: *********
    volumes:
      - /zmain/mysql/var/lib/mysql:/var/lib/mysql:rwz
    networks:
      - dockge_default
networks:
  dockge_default:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;nginx proxy manager docker compose:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.8&quot;
services:
  nginx:
    image: jc21/nginx-proxy-manager:latest
    restart: unless-stopped
    ports:
      # These ports are in format &amp;lt;host-port&amp;gt;:&amp;lt;container-port&amp;gt;
      - 80:80 # Public HTTP Port
      - 443:443 # Public HTTPS Port
      - TAILSCALE_IP:81:81 # Admin Web Port
      - TAILSCALE_IP:21:21 # FTP
      # Uncomment the next line if you uncomment anything in the section
      # environment:
      # Uncomment this if you want to change the location of
      # the SQLite DB file within the container
      # DB_SQLITE_FILE: &quot;/data/database.sqlite&quot;

      # Uncomment this if IPv6 is not enabled on your host
      # DISABLE_IPV6: &apos;true&apos;

    volumes:
      - /zmain/nginx_proxy_manager/data:/data
      - /zmain/nginx_proxy_manager/letsencrypt:/etc/letsencrypt
    networks:
      - dockge_default
networks:
  dockge_default:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;需要共享网络的docker compose添加同一个networks配置，此处使用dockge默认的dockge_default。也可以自己创建一个network。&lt;/li&gt;
&lt;li&gt;注意修改关联应用中对于访问地址的设置，例如wordperess中修改站点地址为nginx proxy manager中配置的域名&lt;/li&gt;
&lt;li&gt;配合cloudflare使用时如果遇到奇怪的502报错，可尝试修改SSL/TLS 加密为 &lt;strong&gt;完全&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item></channel></rss>