1. 多源目录这件事,到底解决了谁的痛
Doris 的Multi Catalog 多源数据目录,说白了就是把"读外部数据源"变成了数仓内部的一等公民。以前你的数据分散在 MySQL 业务库、对象存储上的 Iceberg 表、日志集群的 Elasticsearch 索引里,想做一次联合分析,常规做法是先用同步工具把这些数据搬到数仓内部再算。Doris 的多源数据目录换了个思路:不搬数据,只登记"数据在哪、怎么读",然后在 SQL 里直接用catalog.db.table的写法访问。这个能力适合的人群其实挺明确——手里已经有一个跑起来的 Doris 集群、日常被"这张表能不能拉进来一起查"折磨的取数和分析同学,以及需要为多个业务线提供统一查询入口的数据平台同学。它对基础的要求不高,会写CREATE CATALOG就能起步,但想跑得稳、跑得快,需要弄清楚的东西比想象中多。
我接触这套功能的时间不短,从最早的 Hive 外表方案一路换到 Catalog 体系,中间踩过的坑足够写好几页纸。这篇文章不打算把官方文档翻译一遍,而是按照我实际落地的顺序,把为什么这么设计、参数为什么这么配、出问题去哪儿看讲清楚。如果你正在评估要不要上这套方案,或者已经建了几个 Catalog 但查询慢得没法用,下面的内容应该能省你不少试错时间。
1.1 一个二手数据需求的真实形状
先说个具体场景,我手上一个项目,业务侧提的需求是"给运营做一张订单全链路宽表"。听起来简单,实际取数的时候发现数据是碎的:订单主表在 MySQL 业务库里,只保留最近三个月的热数据;历史归档在数据湖的 Iceberg 表上,按天分区;物流轨迹和用户行为日志在 Elasticsearch 里,一条订单可能对应几十条轨迹。这三份数据分属三个团队维护,同步周期不同,字段命名风格也不同。
如果是老办法,我得先申请把 MySQL 的三张表同步进 Doris 的 ODS 层,把 Iceberg 的归档表也同步一份,ES 的轨迹数据要么导出来落成明细表,要么在应用层做拼接。这套流程走下来,快的话三天,慢的话一周,而且每加一个数据源就要重走一遍。更麻烦的是,运营的需求是会变的,今天要加个退款金额,明天要看优惠券核销,每次变更都得动同步任务。Multi Catalog 在这个场景里的价值就很直接了:MySQL 那几张表建一个 JDBC Catalog,Iceberg 建一个湖表 Catalog,ES 建一个检索 Catalog,写 SQL 的时候三段catalog.db.table拼在一起,Doris 自己去规划执行。需求变更的时候,只要改 SQL,不动任何同步链路。
1.2 先把数据搬进来再算,成本高在哪
很多人会觉得,同步一份数据而已,能有多贵。真正做过的人知道,贵的不是存储,是链路维护和时效对齐。同步任务本身要开发、要调度、要监控失败重试,源端表结构一变(加字段、改类型),同步任务就得跟着改,改完还要回刷历史数据。数据量大一点的表,全量初始化跑一晚上,增量同步延迟十几分钟到几小时不等,业务侧看到的数据永远比源端慢半拍。
还有一个隐性成本是存储放大。业务库里的表往往只存当前状态,同步进数仓为了做拉链会存多版本,一份数据变成三份五份很正常。归档层的 Iceberg 表本来就压得很紧,再同步一份到 Doris,等于把成本翻倍。多源数据目录的做法是把这份成本转移掉——数据留在原处,Doris 只负责算。代价是查询时要去远端拉数据,网络和源端压力会上升,但这是个可以用谓词下推、分区裁剪、结果缓存来优化的可控成本,比无脑同步划算得多。
1.3 Catalog 这一层抽象改变了什么
从架构上看,Catalog 引入了三个关键变化,理解了这三点基本就理解了这个功能。
第一是统一命名空间。任何数据源,无论底层是 HDFS 上的 Parquet、MySQL 的 InnoDB 页还是 ES 的倒排索引,在 Doris 眼里都变成catalog.database.table的三级路径。SQL 解析器不需要知道底层是什么,优化器只需要根据 Catalog 类型选择对应的扫描算子。这个抽象让"跨源 JOIN"在语法层面变得毫无特殊性,跟查两张内部表没区别。
第二是元数据与数据分离。Catalog 本身只持有连接信息和元数据缓存,不持有业务数据。这带来一个很重要的运维特性:删掉一个 Catalog 不会丢任何数据,重建一次只要参数对,几十秒就能恢复。反过来也意味着,Catalog 的状态完全依赖远端元数据服务的可用性,Hive Metastore 一旦抖动,所有依赖它的查询都会受影响。
第三是执行下推的边界由 Catalog 类型决定。这是最容易被忽略的一点。JDBC Catalog 能把WHERE条件下推成源端的 SQL,Hive Catalog 能做分区裁剪但过滤条件不一定能下推到存储层,ES Catalog 会把条件下推成 query DSL。同样一条 SQL,走不同 Catalog,性能可能差十倍。后面的性能调优章节会专门拆这块。
2. 三级命名空间与元数据缓存的底层逻辑
2.1catalog.db.table这套命名怎么生效
Doris 内部其实一直有 Catalog 概念,只是早期只有一个隐式的internal,你写db.table的时候默认就在这里面找。引入多源目录之后,命名空间显式化了:完整写法是catalog_name.database_name.table_name,省略 catalog 前缀时默认走internal,省略 database 前缀时会用当前会话的current database。
这个默认规则会带来一个很常见的困惑:同一个会话里,先USE hive_prod.ods,然后写SELECT * FROM orders,报错说表不存在。原因是USE切库只切换了当前 database 的上下文,而查询时如果没带 catalog 前缀,Doris 依然在internal里找。正确的做法是查询时写全hive_prod.ods.orders,或者用SWITCH hive_prod切换当前 Catalog(这个语法在较新版本支持,切换后默认库也一起变)。
我个人的习惯是永远写全路径,尤其在跨源 JOIN 的场景里。写过一次hive_prod和iceberg_lake里都有同名orders表的项目之后,你会明白显式写全路径能省掉多少排查时间。踩过这个坑的人都懂,那种"数据明明在那儿但就是查不到"的困惑,八成是命名空间没对上。
2.2internal与external的边界
internal是 Doris 自建的存储,数据落在自己的 BE 上,支持完整的写入、更新、物化视图、索引能力。外部 Catalog 是只读或有限写入的,能力边界取决于底层格式。这个边界不是随便划的,它决定了你的分层架构应该怎么设计。
一个典型的分层做法是:高频访问、需要做聚合加速的结果数据落在internal,原始的、明细的、变化频繁的数据留在外部 Catalog 直接查。比如把 ODS 层的明细表通过 Catalog 直接读 Hive/Iceberg,把 DWD/DWS 层的宽表和聚合结果物化到 Doris 内部。这样既避免了全量同步,又保证了 BI 看板的响应速度。
外部 Catalog 的写入能力这几年在逐步增强,湖表格式(Iceberg、Hudi、Paimon)在较新版本里支持INSERT INTO和部分更新操作,Hive 表也支持INSERT OVERWRITE。但我的建议是不要把外部 Catalog 当主存储用。原因很实际:写入链路涉及远端事务、小文件合并、元数据提交,任何一环出问题排查起来都比写内部表复杂得多。除非你的架构本来就是"湖仓一体、Doris 只做计算引擎",否则写入还是老老实实走内部表。
2.3 元数据缓存:整个功能最容易翻车的地方
外部 Catalog 的元数据不是实时拉的,Doris 的 FE 会在内存里缓存一份。这个设计是必须的——如果每次查询都去问一遍 Hive Metastore,几万张表的集群光元数据请求就能把 HMS 打挂。但缓存带来了一致性延迟,这是多源目录落地过程中最高频的故障来源。
缓存的行为由几个属性控制,不同 Catalog 类型的参数名略有差异,但逻辑是通的。以 Hive Catalog 为例,创建时可以指定:
CREATE CATALOG hive_prod PROPERTIES ( "type" = "hive", "hive.metastore.uris" = "thrift://hms-01:9083", "hive.metastore.username" = "doris_ro", "hadoop.username" = "doris", "metadata_refresh_interval_sec" = "600", "enable.metadata.refresh" = "true" );metadata_refresh_interval_sec控制后台自动刷新的周期,默认值是 86400 秒,也就是一天。这个默认值在生产环境基本不能用——上游表结构变更、新增分区,你不可能等一天。enable.metadata.refresh打开后,FE 会起后台线程按周期拉取。
但调小周期也不是万能药。周期调到 60 秒,几万张表的集群每分钟就要向 HMS 发起一轮全量拉取,HMS 的压力会明显上升,FE 的 GC 也会变频繁。我一般的做法是分级设置:核心业务表所在的 Catalog 周期设短一些(300~600 秒),历史归档类 Catalog 设长一些(3600 秒以上),同时在调度系统里挂一个任务,在 ETL 跑完之后主动执行REFRESH CATALOG。
手动刷新的语法有三档粒度,按影响范围从小到大:
REFRESH TABLE hive_prod.ods.order_main; -- 只刷单表,最快最轻 REFRESH DATABASE hive_prod.ods; -- 刷整个库 REFRESH CATALOG hive_prod; -- 刷整个 Catalog,最重注意:
REFRESH CATALOG在大规模集群上是重操作,几万张表的情况下可能耗时几十秒到几分钟,期间 FE 内存会有一个峰值。不要在业务高峰期批量调这个命令,更不要写成定时任务每分钟跑一次。
还有一个容易被忽略的细节:分区信息的缓存和表结构的缓存是两回事。表结构(字段名、类型)更新相对少见,分区列表在按天分区的场景下每天都在变。如果发现"表能查到但新分区查不到数据",八成是分区缓存没刷,执行一次REFRESH TABLE通常就能解决。
3. 五类数据源的接入参数详解
3.1 Hive Catalog:绝大多数离线表的入口
Hive Catalog 是使用频率最高的一类,绝大多数离线数仓的表都在 Hive Metastore 里登记着——不管底层文件是 Parquet、ORC 还是 Text,只要 HMS 里能查到表定义,Doris 就能读。
创建语句的核心是hive.metastore.uris,这是 Thrift 协议的地址,格式是thrift://host:port,多个地址用逗号分隔。这里有个细节:HMS 的多地址不是负载均衡,是故障转移,Doris 会按顺序尝试,第一个不通才用第二个。所以顺序上有讲究,把最稳定的那个放前面。
认证方式主要有两种。简单认证下,如果 HMS 没开权限校验,hive.metastore.username随便填一个就行;如果开了,要保证这个用户名在 HMS 侧有对应的读权限。Kerberos 环境下需要额外配置hive.metastore.authentication.type和相关的 principal、keytab 路径,这块配置项比较多,建议直接用hadoop.config.resources指定一个完整的core-site.xml和hdfs-site.xml路径,让 Doris 复用现成的 Hadoop 配置,比自己一项项填靠谱得多。
数据读取走的是 HDFS 或对象存储,所以hadoop.username也要配对。对象存储场景(S3、OSS、COS 之类)需要在 Catalog 属性里额外配置 access key 和 endpoint,或者通过hadoop.config.resources引入配置好的core-site.xml。这块最容易出的问题是路径权限不一致——HMS 里登记的表 location 和实际文件路径的权限主体不是同一个,表现为"表能 describe 但 select 报权限错误"。
3.2 湖表 Catalog:Iceberg、Hudi、Paimon 的差异
湖表格式这三种,接入方式看着像,内部机制差别挺大。
Iceberg 的参数里最关键的是iceberg.catalog.type,它决定了元数据从哪儿读。常见取值有hms(走 Hive Metastore)、hadoop(直接读文件系统上的 metadata 目录)、rest(走 REST Catalog 服务)。走hms是最省事的,因为大多数团队的 Iceberg 表本来就是登记在 HMS 里的:
CREATE CATALOG iceberg_lake PROPERTIES ( "type" = "iceberg", "iceberg.catalog.type" = "hms", "hive.metastore.uris" = "thrift://hms-01:9083", "hadoop.username" = "doris" );Hudi 的接入最简单,参数基本就是 HMS 地址,因为 Hudi 表的元数据主要通过 HMS 暴露。Paimon 需要区分paimon.catalog.type是hms还是filesystem,选filesystem时要额外指定warehouse路径。
这三种格式在 Doris 里的能力支持度不完全一样。Iceberg 的支持最完整,快照读取、时间旅行、增量读取这些特性都有对应语法;Hudi 主要支持 COW 表,MOR 表需要额外的读优化配置;Paimon 的支持在逐步完善中。选型的时候别只看"能不能读",要看"你的表是什么类型、需不需要读最新快照"。
提示:湖表 Catalog 建好之后,如果发现查出来的数据比预期"旧",先确认读的是不是最新快照。Iceberg 的元数据有多层缓存(Catalog 层、表元数据层、manifest 层),任何一层没刷新都会导致读到旧数据。
3.3 JDBC Catalog:业务库直查的正确姿势
JDBC Catalog 是我用得最多的一类,因为它解决的是"业务库数据不能实时同步但又要查"这个刚需。创建语句看着简单,参数却最讲究:
CREATE CATALOG mysql_biz PROPERTIES ( "type" = "jdbc", "user" = "doris_reader", "password" = "******", "jdbc_url" = "jdbc:mysql://10.0.0.11:3306?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true", "driver_url" = "mysql-connector-java-8.0.30.jar", "driver_class" = "com.mysql.cj.jdbc.Driver", "only_specified_database" = "true", "include_database_list" = "order_db,user_db", "connection_pool_min_size" = "5", "connection_pool_max_size" = "30", "connection_pool_max_wait_time" = "5000", "connection_pool_max_life_time" = "1800000", "connection_pool_keep_alive" = "true" );jdbc_url里几个参数值得说一下。serverTimezone必须显式和 Doris 侧对齐,否则DATETIME字段会整体偏几个小时,这个坑我见过不止一次,表现为"两边查同一张表,时间对不上"。allowPublicKeyRetrieval=true在 MySQL 8 默认的caching_sha2_password认证下是必须的,不加会报认证失败。useSSL按实际网络环境配,内网一般关掉省一次握手。
driver_url有三种写法:文件名(需要提前把 jar 放到 FE 的jdbc_drivers目录)、HTTP 地址(FE 会自动下载到本地缓存)、本地绝对路径(加file://前缀)。生产环境我推荐用 HTTP 地址或统一放置 jar 包,别指望每台 FE 都手动拷贝。驱动版本也要注意,连 MySQL 8 必须用 8.x 的驱动,用 5.1 的老驱动在caching_sha2_password下连不上。
only_specified_database这个参数很关键。默认false,意味着 Doris 会去拉取源端所有的库和表元数据。业务库如果库表数量多,一次初始化可能要几十秒甚至更久,而且会持续占用连接。设置成true再用include_database_list精确指定要暴露的库,初始化速度和后续查询的元数据开销都会明显下降。反过来,如果是黑名单思路,就用exclude_database_list。
连接池参数别照抄默认值。默认的连接池偏小,并发查询多了会出现"等待连接超时"。我的经验是按"峰值并发查询数 × 1.5"来设connection_pool_max_size,connection_pool_max_wait_time设在 3000~5000 毫秒,太长会让查询卡住不明显,排查时看不出问题。connection_pool_max_life_time默认 30 分钟,建议保留,它能自动清理僵死连接。
3.4 Elasticsearch Catalog:日志检索的捷径
ES Catalog 的接入相对简单,主要是地址和认证:
CREATE CATALOG es_log PROPERTIES ( "type" = "es", "hosts" = "http://es-01:9200,http://es-02:9200", "user" = "doris_reader", "password" = "******", "http_ssl_enabled" = "false" );用了 HTTPS 的场景要把http_ssl_enabled设成true,走自签证书的话还需要额外配置信任。索引在 Doris 里映射成表,_source里的字段映射成列,_id也会暴露成一个特殊列。
ES Catalog 最大的问题是类型语义的错位。ES 是搜索引擎,字段类型体系和 SQL 差得远。text类型的字段在 ES 里默认不支持聚合和排序(需要fielddata),Doris 生成查询计划时如果对text字段做了GROUP BY或ORDER BY,源端会直接报错。解决办法是在建索引时给这些字段配keyword子字段,查询时用子字段名;或者接受在 Doris 侧不在text字段上做聚合。
另一个坑是nested类型的字段。ES 的nested是一个独立文档结构,SQL 里没有直接对应物,Doris 会把整个nested对象映射成一个 JSON 字符串列。想在nested内部做过滤,得配合 JSON 函数,性能不会太好。如果你的查询强依赖nested字段,建议在 ES 侧先做预处理,把关键字段拍平成顶层字段。
3.5 各类 Catalog 关键参数对照
| Catalog 类型 | 必填参数 | 高频调优参数 | 主要限制 |
|---|---|---|---|
| Hive | hive.metastore.uris | metadata_refresh_interval_sec、hadoop.config.resources | 过滤条件下推有限 |
| Iceberg | iceberg.catalog.type、对应连接信息 | refresh_table_interval、快照相关属性 | 部分格式版本不支持 |
| Hudi | hive.metastore.uris | 读优化相关配置 | MOR 表支持有限 |
| Paimon | paimon.catalog.type、warehouse(filesystem 时) | 同 Hive | 特性覆盖度随版本变化 |
| JDBC | jdbc_url、driver_url、driver_class、user、password | connection_pool_*、include_database_list | 复杂 SQL 下推不完整 |
| ES | hosts | http_ssl_enabled、索引字段映射 | text字段聚合受限 |
4. 从建目录到跑通一条跨源 JOIN
4.1 建 Catalog 前的三项确认
建之前先花十分钟做三件事,能省掉后面大半的排查时间。
确认网络连通性。从 FE 节点上直接telnet或nc一下源端端口,HMS 的 9083、MySQL 的 3306、ES 的 9200。这一步很重要,因为 Doris 报出来的连接错误信息往往被层层包装,实际原因可能就是一个防火墙策略没放行。我遇到过一次排查了两小时的"连接超时",最后发现是新扩容的 FE 节点没加进安全组白名单。
确认账号权限。JDBC 和 ES 需要显式账号,Hive 侧要确认hadoop.username对表目录有读权限。这里有个易错点:HMS 元数据的读权限和底层文件的读权限是两回事,前者靠hive.metastore.username,后者靠hadoop.username,两个账号不一定要是同一个,但都要配到位。
确认驱动版本匹配。JDBC Catalog 尤其要注意,源端是 MySQL 8 就用 8.x 驱动,是 PostgreSQL 就用对应大版本的驱动,驱动版本和数据库版本差太远会出现各种莫名其妙的协议错误。
4.2 语句逐行拆解
拿一个混合场景举例,假设要同时接入 Hive 数仓和 MySQL 业务库:
-- 第一个:离线数仓 CREATE CATALOG hive_prod PROPERTIES ( "type" = "hive", "hive.metastore.uris" = "thrift://hms-01:9083,thrift://hms-02:9083", "hive.metastore.username" = "doris_ro", "hadoop.username" = "doris", "hadoop.config.resources" = "/opt/doris/conf/hadoop/core-site.xml,/opt/doris/conf/hadoop/hdfs-site.xml", "metadata_refresh_interval_sec" = "600" ); -- 第二个:业务库 CREATE CATALOG mysql_biz PROPERTIES ( "type" = "jdbc", "user" = "doris_reader", "password" = "******", "jdbc_url" = "jdbc:mysql://10.0.0.11:3306?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true", "driver_url" = "http://artifact-host/mysql-connector-java-8.0.30.jar", "driver_class" = "com.mysql.cj.jdbc.Driver", "only_specified_database" = "true", "include_database_list" = "order_db", "connection_pool_max_size" = "30" );hadoop.config.resources这个属性值得单独说。它接受一个逗号分隔的路径列表,Doris 会加载这些 XML 作为 Hadoop 客户端配置。用它能一次性把 HDFS 的fs.defaultFS、HA 配置、对象存储的 access key 全带进来,比在 Catalog 属性里一项项写更不容易出错。路径要保证所有 FE 节点上都能访问到,放在统一的配置目录挂载是最省事的做法。
driver_url用 HTTP 地址的好处是 FE 只需要能访问到那个地址就行,jar 包会自动下载到本地缓存目录,不用逐台机器部署。缺点是首次建 Catalog 会有一次下载耗时,网络不通时会报下载失败。
4.3 验证链路
建完之后别急着写复杂查询,按下面的顺序逐层验证,出问题能快速定位在哪一层:
-- 第一层:Catalog 是否建成功 SHOW CATALOGS; -- 第二层:元数据能否拉到 SHOW DATABASES FROM hive_prod; SHOW TABLES FROM hive_prod.ods; -- 第三层:表结构是否正确 DESC hive_prod.ods.order_main; -- 第四层:能否真正读到数据(先加 limit) SELECT * FROM hive_prod.ods.order_main LIMIT 10;这四步每一层的报错含义完全不同。第一步失败说明语法或权限有问题;第二步失败说明网络或认证有问题;第三步失败通常是元数据缓存脏了,REFRESH一下;第四步失败才是数据读取层的问题,可能是文件权限、格式不兼容、分区路径不对。
提示:第四步用
LIMIT 10而不是直接COUNT(*)。LIMIT会触发少量文件读取,能快速验证链路是否通;COUNT(*)可能扫全表,一旦出问题你等的时间会很长。
4.4 跨源 JOIN 的执行计划怎么读
链路通了之后,写一条跨源 JOIN:
SELECT o.order_id, o.create_time, m.user_id, m.pay_amount FROM hive_prod.ods.order_main o JOIN mysql_biz.order_db.user_order m ON o.order_id = m.order_id WHERE o.dt = '2025-01-15' AND m.status = 1;执行前先加EXPLAIN看计划,重点看两件事:扫描算子类型和谓词有没有下推。计划里会出现HIVE_SCAN_NODE和JDBC_SCAN_NODE两个节点,如果JDBC_SCAN_NODE的predicates里能看到status = 1,说明条件下推成功了;如果predicates是空的,条件全部拉回 Doris 侧做过滤,源端会返回全量数据,性能会差很多。
JOIN 的执行方式也值得关注。跨源 JOIN 没有本地化的数据分布,Doris 一般会选 Broadcast Join,把小表广播到各个节点,大表做流式读取。如果两边都是大表,执行计划里出现 Shuffle 就要小心了,中间数据的落盘和网络传输会成为瓶颈。
我一般会先让业务方在源端做一次预聚合,把 MySQL 侧的数据量压到几十万行以内,再参与 JOIN。这样即使走 Broadcast,广播的数据量也可控。如果实在压不下来,考虑把这部分数据先落到internal的临时表里,做一次物化再关联,反而比直连快。
5. 性能调优:把跨源查询从"能跑"推到"能用"
5.1 谓词下推能救多少
谓词下推是跨源查询性能的第一杠杆,没有之一。原理很简单:把WHERE条件尽可能推到源端执行,让源端只返回过滤后的数据,减少网络传输和 Doris 侧的 CPU 消耗。
不同 Catalog 的下推能力差别很大。JDBC Catalog 的下推能力最强,因为它本质上是把 SQL 片段拼到源端的查询里,=、>、<、IN、BETWEEN这些常规条件基本都能推,部分版本还支持下推LIMIT和聚合函数。Hive/Iceberg 类的下推主要靠分区裁剪和存储层统计信息跳过,非分区字段的过滤条件通常推不到文件层,只能推到 Doris 侧做。ES Catalog 会把条件下推成 query DSL,term、range、bool这些能转换,但LIKE '%xxx%'这种前缀模糊匹配转换成 ES 的wildcard查询性能也不好。
实际操作里有个很有效的技巧:把分区字段条件写在最显眼的位置,并且保证它是常量表达式。像dt = '2025-01-15'能触发分区裁剪,写成dt = DATE_FORMAT(NOW(), '%Y-%m-%d')就不一定了,因为优化器可能无法在执行计划阶段求值。这个差别在按天分区的表上非常明显,一个扫一天,一个扫全部。
5.2 缓存刷新频率的取舍
前面提过元数据缓存的刷新周期,这里补充一个实践中的平衡点。我一般按"表的变更频率"分三档:
| 场景 | 建议周期 | 说明 |
|---|---|---|
| 核心业务表,每小时有新分区 | 300~600 秒 | 配合 ETL 完成后主动 REFRESH |
| 常规离线表,每天一次 | 1800~3600 秒 | 默认值可用 |
| 归档冷数据,几乎不变 | 86400 秒或关闭自动刷新 | 需要时手动刷新 |
需要注意的是,自动刷新是 FE 全量拉取,表越多压力越大。如果集群里 Catalog 数量超过十个,总表数上万,建议关掉自动刷新(enable.metadata.refresh设为false),改成在调度系统里按需触发。这样刷新时机可控,FE 的内存抖动也可控。实际案例中,我们曾经把自动刷新全部关掉,改成 ETL 完成后触发,FE 的 Full GC 频率从每天几次降到基本没有。
5.3 连接池与并发控制
JDBC Catalog 的连接池是最容易成为瓶颈的地方。源端数据库的连接数是有限的,Doris 这边并发一高,连接池打满,查询就会卡在等连接。表现是查询不报错但一直不返回,SHOW PROCESSLIST看到状态是WAITING。
我的配置思路是源端连接数倒推。假设源端 MySQL 允许 200 个连接,业务自身占用 50,留给 Doris 的额度是 150,那么按每个 Catalog 50、connection_pool_max_size设 50 是安全的。如果多个 Doris 集群共用同一个源端,还要再除以集群数。这个算法很土但很有效,比拍脑袋设个数靠谱。
connection_pool_keep_alive建议打开,它会让连接池定期做探活,避免拿到已经断开的连接。这个参数在源端有wait_timeout限制的场景下特别重要,MySQL 默认 8 小时断开空闲连接,不探活的话第二天早上第一批查询必然报错。
5.4 资源隔离兜底
多源查询的一个隐患是不可控的资源消耗。一条写得很随意的跨源 JOIN 可能拉取几十 GB 数据,把 BE 的内存打满,影响同集群其他查询。资源组是兜底手段:
CREATE RESOURCE GROUP rg_bi TO (user = 'bi_user') WITH ( 'cpu_share' = '20', 'memory_limit' = '30%', 'concurrent_limit' = '20' );把 BI 类用户绑到资源组上,限制 CPU 份额、内存上限和并发数。这样即使有人写了一条灾难性的查询,最多把自己那个组打满,不会影响核心报表链路。这个配置在建 Catalog 之前就该规划好,等出了问题再补,往往已经造成了影响。
另外还有个全局开关值得关注:单条查询的内存上限。通过会话变量控制,SET exec_mem_limit = 8G;这类设置能防止单条查询无限膨胀。跨源查询的内存消耗主要在 JOIN 的 build 侧和网络缓冲上,设一个合理的上限能让查询快速失败而不是拖垮整个节点。
6. 六类典型故障的排查路径
6.1 认证与驱动类故障
这类问题的特征很统一:Catalog 建成功了,但SHOW DATABASES或者查询时直接报错。最常见的三种情况。
驱动类找不到。报错信息里会出现ClassNotFoundException或No suitable driver。原因要么是driver_url指向的 jar 不在 FE 能访问的位置,要么是driver_class写错了全限定名。MySQL 用com.mysql.cj.jdbc.Driver(8.x)或com.mysql.jdbc.Driver(5.x),PostgreSQL 用org.postgresql.Driver,Oracle 用oracle.jdbc.OracleDriver。名字写错一个字母就会失败。
认证失败。MySQL 8 的caching_sha2_password是高频原因,jdbc_url里不带allowPublicKeyRetrieval=true就会报Public Key Retrieval is not allowed。另外账号的 host 白名单也要确认,doris_reader@%和doris_reader@10.0.0.%是两个不同的账号,权限可能不一样。
证书或协议问题。ES 走 HTTPS 时如果证书是自签的,Doris 侧会报证书校验失败。要么把证书导入信任链,要么在测试环境临时关掉校验(生产不建议)。
6.2 元数据不一致类故障
这类问题最磨人,因为往往不报错,只是数据不对。典型表现是新加的表查不到、新分区查不到数据、字段改了但 DESC 还是老的。
排查顺序很固定:先SHOW TABLES看表在不在,不在就REFRESH DATABASE再试;表在但字段不对,REFRESH TABLE;分区不对,同样REFRESH TABLE,如果还不行,去 HMS 或源端确认元数据本身有没有更新。有一次我排查了半小时,最后发现是上游的建表任务失败了,元数据压根没提交,Doris 这边刷一百遍也没用。
还有一种更隐蔽的情况:表被删除后重建,Doris 缓存的还是老的表 ID。这时查询报"表不存在"但SHOW TABLES能看到。REFRESH TABLE加上等待下一次自动刷新周期,通常能恢复;如果不行,DROP CATALOG再重建是最彻底的解法。这操作不丢数据,代价只是重新初始化元数据。
6.3 类型映射类故障
外部数据源的类型体系和 Doris 不是一一对应的,映射过程中会出现精度丢失、语义变化。几个典型例子:
MySQL 的BIGINT UNSIGNED映射到 Doris 时会变成LARGEINT,如果应用层按BIGINT解析结果,可能会溢出。TINYINT(1)在很多场景下被当作布尔值用,Doris 侧的映射跟 JDBC 驱动的tinyint1isBit参数有关,表现可能是0/1也可能是true/false。DECIMAL的精度如果超过 Doris 支持的上限,会被截断或者报错。
ES 的类型映射问题更突出。ES 的date类型支持多种格式,Doris 侧统一按DATETIME处理,格式不匹配就会解析失败。text字段做聚合会直接报Fielddata is disabled,需要在 ES 侧用keyword子字段。
排查这类问题的通用方法是:两边各查一条样本数据,逐字段比对。别只看DESC的输出,实际数据的表现比类型定义更能说明问题。
6.4 查询报错速查表
| 报错关键词 | 大概率原因 | 处理方向 |
|---|---|---|
Unknown catalog | Catalog 名拼错或未创建 | SHOW CATALOGS核对名称 |
Table not found/Unknown table | 元数据缓存过期或库表路径错 | REFRESH TABLE,核对全路径 |
Connection refused/timeout | 网络不通或源端未监听 | 从 FE 节点 telnet 目标端口 |
Public Key Retrieval is not allowed | MySQL 8 认证参数缺失 | jdbc_url加allowPublicKeyRetrieval=true |
No suitable driver | 驱动版本或类名不匹配 | 核对driver_class和 jar 版本 |
Fielddata is disabled | ES 的text字段做聚合 | 改用keyword子字段 |
Out of memory/Memory limit exceeded | 单查询内存超限 | 调exec_mem_limit或优化 JOIN 顺序 |
Access denied | 账号权限或文件权限不足 | 分别核对元数据账号和数据账号 |
Partition not found | 分区缓存未刷新 | REFRESH TABLE后重试 |
7. 落地时的架构取舍:哪些源该接,哪些源该同步
7.1 判断一个数据源该不该直接接
我的判断标准是三条,满足两条以上就适合用 Catalog 直连。
第一,数据量可控或者能有效裁剪。如果一条查询必然要扫几十 GB 的源端数据,直接接过来查会很痛苦,不如同步。反过来,如果表不大,或者查询总能带上分区条件,直连就很划算。
第二,源端能承受额外压力。Doris 的查询对业务库来说是额外的读负载。如果源端本身就是高负载的业务数据库,直连会把风险传导过去。这时候要么加只读从库,要么老老实实同步。
第三,时效要求高但变更不频繁。业务库的数据实时性要求高(今天下的单要能马上看到),但表结构相对稳定,这是 JDBC Catalog 的甜蜜点。如果表结构三天两头改,维护 Catalog 映射的成本会超过收益。
反过来,高频访问的热数据、需要复杂聚合加速的宽表、要做物化视图的数据,还是应该落到internal里做。这是成本账,不是能力账。
7.2 一种可复用的分层做法
我目前比较常用的结构是三层:
最底层是原始层直连。Hive/Iceberg 的 ODS 表不做同步,直接通过 Catalog 访问。这层的查询频率不高,主要是排查问题和临时取数,直连完全够用。
中间层是轻度加工后落内部。把 ODS 里需要频繁访问的字段抽出来,在 Doris 的internal里建 DWD 宽表。加工的触发方式是定时任务,用INSERT INTO ... SELECT ... FROM hive_prod.ods.xxx这种跨源写入,Doris 自己就能做,不需要额外的同步工具。这一步把"跨源查询的成本"用"定时物化"的方式摊平了。
最上层是结果层加速。面向 BI 的聚合结果、指标表,全部在internal里,配上物化视图和 Rollup,保证看板响应在秒级。
这套结构的核心思想是:跨源直连解决"有没有",内部物化解决"快不快"。两者不是替代关系,是分层关系。我见过一些团队试图把所有查询都做成直连,结果是被源端数据库的 DBA 投诉;也见过把所有数据都同步的,结果同步链路维护成本高到放弃。分开用才对。
提示:跨源写入(
INSERT INTO internal_table SELECT ... FROM external_catalog.db.table)是个很实用的减法。它比独立的同步工具简单,因为不需要额外的组件和调度,一条 SQL 就是一个同步任务;但要注意它对源端的读取压力是集中式的,跑大批量的时候建议限速或者放到低峰期。
我在实际使用中的一个体会是,多源数据目录最大的价值不是"省掉了同步",而是把"要不要同步"这件事从架构决策降级成了 SQL 决策。以前加一个数据源要立项、排期、开发、上线;现在建个 Catalog 试一下,好用就接着用,不好用再考虑同步,试错成本几乎为零。这种灵活性在业务需求变化快的时候特别值钱。踩过几次"同步任务改到怀疑人生"的坑之后,我对这个能力的评价还会再高一档。