Apache Doris 2.1.10 集群部署与 Paimon 数据湖集成实战文档

目录

[1. 环境概述](#1. 环境概述)

[1.1 硬件与系统信息](#1.1 硬件与系统信息)

[1.2 节点规划](#1.2 节点规划)

[1.3 依赖组件 (CDH)](#1.3 依赖组件 (CDH))

[2. 基础环境准备 (所有节点)](#2. 基础环境准备 (所有节点))

[2.1 检查 CPU AVX2 支持](#2.1 检查 CPU AVX2 支持)

[2.2 操作系统优化](#2.2 操作系统优化)

[2.3 配置 Hosts 映射](#2.3 配置 Hosts 映射)

[2.4 创建目录与授权](#2.4 创建目录与授权)

[3. FE (Frontend) 部署](#3. FE (Frontend) 部署)

[3.1 安装与配置](#3.1 安装与配置)

[3.2 启动 FE 集群](#3.2 启动 FE 集群)

[4. BE (Backend) 部署](#4. BE (Backend) 部署)

[4.1 修改配置 be.conf](#4.1 修改配置 be.conf)

[4.2 启动 BE](#4.2 启动 BE)

[4.3 注册 BE](#4.3 注册 BE)

[5. Paimon 数据湖集成](#5. Paimon 数据湖集成)

[5.1 准备 Hadoop 配置文件](#5.1 准备 Hadoop 配置文件)

[5.2 创建 Catalog](#5.2 创建 Catalog)

[5.3 验证查询](#5.3 验证查询)

[6. 问题排查与避坑总结 (Troubleshooting)](#6. 问题排查与避坑总结 (Troubleshooting))

[7. 基础测试(增删改查)](#7. 基础测试(增删改查))

[7.1 插入数据](#7.1 插入数据)

[7.2 更新数据](#7.2 更新数据)

[7.3 删除数据](#7.3 删除数据)

[8. Paimon外表数据写入Doris内表基础测试](#8. Paimon外表数据写入Doris内表基础测试)

[8.1 数据准备](#8.1 数据准备)

[8.2 测试代码](#8.2 测试代码)

[8.3 数据验证](#8.3 数据验证)


1. 项目背景与技术选型 当前项目正在构建基于 Paimon+OLAP 的流批一体数仓架构。在数据链路规划中,原定将治理后的数据汇入 StarRocks 【从StarRocks3.x版本开始,BINARY/VARBINARY支持的最大长度与 VARCHAR 类型相同,Varchar:[1, 1048576],网址:BINARY/VARBINARY | StarRocks】进行查询服务。然而在实际业务场景验证中发现,部分业务核心表包含超长文本或复杂 JSON 字段,受限于原选型对大字段(Large Field)存储的支撑能力,无法满足业务需求。

鉴于 Apache Doris 2.1.10 【当时最新稳定版】在大字段存储(支持 String/Text/Variant 类型)【Doris的String类型默认支持 1048576 字节(1MB),可调大到 2147483643 字节(2GB),网址:数据类型 - Apache Doris】以及湖仓一体(Lakehouse)集成方面的显著优势,项目组决定调整技术选型,引入 Doris 作为新的 OLAP 引擎及批处理计算单元。

2. 测试目标 为了验证新架构的可行性,需对 Doris 2.1.10 + Paimon 1.1.1 进行深度集成测试。本次测试的核心目标是评估 Doris 挂载 Paimon 外表的性能表现,具体包括:

  • 读取性能:Doris 读取 Paimon ODS 层海量原始数据的速度。

  • 计算与写入性能:Doris 执行 ETL 清洗逻辑(数据治理)并将结果回写至 Paimon DWD 层的效率。

3.补充说明

Paimon官网支持Doris 2.0.6 及以上版本。对应网址:Doris | Apache Paimon

Doirs官网支持对Paimon的查询,使用 Doris 的分布式计算引擎直接访问 Paimon 数据以实现查询加速;数据集成,读取Paimon数据并将其写入Doris内部表,或使用Doris计算引擎执行ZeroETL。对应网址:Paimon Catalog - Apache Doris

支持Paimon版本为1.0.0

调研架构图如下:

1. 环境概述

1.1 硬件与系统信息

  • 操作系统: CentOS 7 (CDH 6.3.2 环境混合部署)

  • 节点配置:

    • CPU: 10核

    • 内存: 14GB (资源紧缺,需精细调优)

    • 存储: 400GB SSD

  • 部署用户 : bigdata

  • Java 环境 : /usr/java/jdk1.8.0_181-cloudera

1.2 节点规划

前置组件分配

IP 主机名 角色 版本
10.x.xx.201-10.x.xx.205 10.x.xx.215 10.x.xx.149 10.x.xx.151 10.x.xx.156 10.x.xx.157 10.x.xx.167 10.x.xx.206 nd1-nd5 nd6 nd11 nd12 nd13 nd14 nd15 nd16 CDH 6.3.2
10.x.xx.201-10.x.xx.205 nd1-nd5 Paimon 1.1.1

采用 FE (Frontend) + BE (Backend) 混合部署 模式,共 3 个节点。

IP 主机名 角色 端口规划 (FE/BE) 备注
10.x.xx.149 nd11 FE (Leader) + BE FE: 8030, 9030, 9020, 9010 BE: 18040, 9060, 9050, 8060 此时 8040 被 YARN 占用,BE Web 端口改为 18040
10.x.xx.151 nd12 FE (Follower) + BE 同上
10.x.xx.156 nd13 FE (Follower) + BE 同上

1.3 依赖组件 (CDH)

  • Hive Metastore : 10.x.xx.201 (nd1), 10.x.xx.203 (nd3)

  • HDFS NameNode : 10.x.xx.201 (nd1) (端口 8020)

2. 基础环境准备 (所有节点)

nd11, nd12, nd13 上执行以下操作。

2.1 检查 CPU AVX2 支持

Doris 2.0+ 默认依赖 AVX2 指令集。

bash 复制代码
cat /proc/cpuinfo | grep avx2
  • 有输出:继续下一步。

  • 无输出 :您需要下载 Doris 的 x64-noavx2 版本安装包,否则 BE 启动会报错 Illegal instruction

2.2 操作系统优化

bash 复制代码
# 1. 临时关闭 Swap
sudo swapoff -a
​
# 2. 修改 sysctl.conf
sudo vi /etc/sysctl.conf
# 添加:
vm.max_map_count=2000000
vm.swappiness=0
# 生效:
sudo sysctl -p
bash 复制代码
# 3. 修改文件句柄限制
sudo vi /etc/security/limits.conf
# 添加:
* soft nofile 65536
* hard nofile 65536
* soft nproc 65536
* hard nproc 65536

注意:修改 limits 后需重新登录 SSH 生效,可用 ulimit -n 检查。

注意: 由于原始的配置均为65535,Doris 官方推荐 65536 是为了取个整(2的16次方),但实际上 65535 对于 Doris 来说没有任何区别。只要这个数值大于 60000,Doris 就能非常稳定地运行。因此上述配置可以不用进行配置。

2.3 配置 Hosts 映射

确保 Doris 节点能解析彼此及 CDH 组件。由于这里我是在CDH集群节点上选择的节点搭建,相应的配置均有,因此也可不用进行配置。

bash 复制代码
sudo vi /etc/hosts
​
# Doris 节点
10.x.xx.149 nd11
10.x.xx.151 nd12
10.x.xx.156 nd13
​
# CDH 依赖节点
10.x.xx.201 nd1
10.x.xx.203 nd3

2.4 创建目录与授权

bash 复制代码
mkdir -p /home/bigdata/doris
mkdir -p /home/bigdata/data/doris-meta     # FE 元数据
mkdir -p /home/bigdata/data/doris-storage  # BE 数据存储
​
# 确保所有权为 bigdata
sudo chown -R bigdata:bigdata /home/bigdata/doris
sudo chown -R bigdata:bigdata /home/bigdata/data

3. FE (Frontend) 部署

3.1 安装与配置

上传 apache-doris-2.1.10-bin-x64.tar.gz/home/bigdata/doris 并解压。由于我这里选择的是nd11、nd12、nd13,因此每个节点都要进行如下的配置。

bash 复制代码
cd /home/bigdata/doris/apache-doris-2.1.10-bin-x64
mv fe /home/bigdata/doris/fe
mv be /home/bigdata/doris/be

修改配置文件 fe.conf

bash 复制代码
vi /home/bigdata/doris/fe/conf/fe.conf

关键配置项:

bash 复制代码
# 1. 指定 Java 环境 (解决找不到 JDK 问题)
# ls -ld /usr/java
JAVA_HOME=/usr/java/jdk1.8.0_181-cloudera
​
# 2. 元数据目录
meta_dir = /home/bigdata/data/doris-meta
​
# 3. 绑定网段 (必须指定,防止抓错网卡)
priority_networks = 10.8.15.0/24
​
# 4. 内存限制 (14G 内存机器,FE 给 4G)
JAVA_OPTS="-Xmx4096m -Xms4096m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:log/fe.gc.log"

3.2 启动 FE 集群

步骤 1:启动 Leader (在 nd11 执行)

bash 复制代码
/home/bigdata/doris/fe/bin/start_fe.sh --daemon
# 停止服务
# /home/bigdata/doris/fe/bin/stop_fe.sh --daemon

步骤 2:启动 Follower (在 nd12, nd13 执行) 注意:第一次启动必须加 --helper 指向 Leader。

bash 复制代码
/home/bigdata/doris/fe/bin/start_fe.sh --helper 10.x.xx.149:9010 --daemon

查看日志确认是否启动成功:

bash 复制代码
tail -f /home/bigdata/doris/fe/log/fe.INFO

nd11端口:

上述截图解析可以看出:

  1. 心跳广播成功 (关键信号)

    复制代码
    [topic_publish]publish topic info to be 10.x.xx.149 success ...
    [topic_publish]publish topic info to be 10.x.xx.151 success ...
    [topic_publish]publish topic info to be 10.x.xx.156 success ...
    • 含义:Master FE 正在将元数据更新广播给所有的 BE 节点。

    • 状态 :全部 success 。这意味着 FE 能够顺利连接到 nd11, nd12, nd13 三台机器的 BE 服务。

  2. 接收汇报成功

    复制代码
    receive report from be 10425 ...
    receive report from be 10334 ...
    • 含义:BE 正在主动向 FE 汇报自己的负载和任务状态。

    • 状态:正常接收。

步骤 3:注册 Followernd11 上登录 MySQL:

bash 复制代码
mysql -h 10.x.xx.149 -P 9030 -uroot

执行 SQL:

sql 复制代码
ALTER SYSTEM ADD FOLLOWER "10.x.xx.151:9010";
ALTER SYSTEM ADD FOLLOWER "10.x.xx.156:9010";

-- 验证
SHOW PROC '/frontends';

预期结果:3 个节点,Alive 均为 true。

4. BE (Backend) 部署

4.1 修改配置 be.conf

所有 BE 节点需保持一致。

bash 复制代码
vi /home/bigdata/doris/be/conf/be.conf

关键配置项:

bash 复制代码
# 1. 指定 Java 环境 (Paimon/HDFS 访问必需)
JAVA_HOME=/usr/java/jdk1.8.0_181-cloudera

# 2. 网段配置
priority_networks = 10.8.15.0/24

# 3. 数据存储路径
storage_root_path = /home/bigdata/data/doris-storage

# 4. 修改 Web 端口 (解决 8040 被 YARN NodeManager 占用问题)
webserver_port = 18040

4.2 启动 BE

所有节点 执行:

bash 复制代码
/home/bigdata/doris/be/bin/start_be.sh --daemon
# 停止服务
# /home/bigdata/doris/be/bin/stop_be.sh --daemon

查看日志确认是否启动成功:

bash 复制代码
tail -f /home/bigdata/doris/be/log/be.INFO

nd11端口:

nd12端口:

nd13端口:

4.3 注册 BE

nd11 (MySQL) 中执行:

bash 复制代码
mysql -h 10.x.xx.149 -P 9030 -uroot

然后执行下述语句:

sql 复制代码
ALTER SYSTEM ADD BACKEND "10.x.xx.149:9050";
ALTER SYSTEM ADD BACKEND "10.x.xx.151:9050";
ALTER SYSTEM ADD BACKEND "10.x.xx.156:9050";

-- 验证
SHOW PROC '/backends';

预期结果:3 个节点,Alive 均为 true,TotalCapacity 显示磁盘容量。

5. Paimon 数据湖集成

5.1 准备 Hadoop 配置文件

nd11 上操作,将 nd1上CDH 集群的配置文件拉取到 Doris 配置目录。

bash 复制代码
mkdir -p /home/bigdata/doris/conf/cdh_conf/

# 从 CDH 节点 (201) 拷贝
# 拷贝 Hadoop 配置文件 (core-site.xml 和 hdfs-site.xml)
scp root@10.x.xx.201:/etc/hadoop/conf/core-site.xml /home/bigdata/doris/conf/cdh_conf/
scp root@10.x.xx.201:/etc/hadoop/conf/hdfs-site.xml /home/bigdata/doris/conf/cdh_conf/
# 拷贝 Hive 配置文件 (hive-site.xml)
scp root@10.x.xx.201:/etc/hive/conf/hive-site.xml /home/bigdata/doris/conf/cdh_conf/

验证文件

bash 复制代码
ls -l /home/bigdata/doris/conf/cdh_conf/

分发到其他节点 (nd12, nd13)

将这 3 个文件放到 Doris 2台机器(另外2台)的统一目录,例如 /home/bigdata/doris/conf/cdh_conf/

bash 复制代码
# 1. 确保目标机器也有这个目录
ssh bigdata@nd12 "mkdir -p /home/bigdata/doris/conf/cdh_conf/"
ssh bigdata@nd13 "mkdir -p /home/bigdata/doris/conf/cdh_conf/"
复制代码
# 2. 发送文件给 nd12
bash 复制代码
scp /home/bigdata/doris/conf/cdh_conf/* bigdata@nd12:/home/bigdata/doris/conf/cdh_conf/
复制代码
# 3. 发送文件给 nd13
bash 复制代码
scp /home/bigdata/doris/conf/cdh_conf/* bigdata@nd13:/home/bigdata/doris/conf/cdh_conf/

5.2 创建 Catalog

由于 CDH 6.3.2 (Hive 2.1.1) 与 Doris 内置的高版本 Hive 客户端存在 API 兼容性问题 (报错 Invalid method name: 'get_table_objects_by_name_req'),可以使用 Filesystem 类型 Catalog【方案三】 ,直接绕过 Hive Metastore 读取 HDFS 数据。但是为了走Hive元数据,推荐使用方案四。对于API兼容性报错问题,对应执行的语句为

sql 复制代码
-- 方案一
CREATE CATALOG paimon_catalog PROPERTIES (
    "type" = "paimon",
    "paimon.catalog.type" = "hms",
    "hive.metastore.uris" = "thrift://nd1:9083,thrift://nd3:9083",
    "warehouse" = "hdfs://nd1:8020/user/hive/warehouse",
    "hadoop.conf.dir" = "/home/bigdata/doris/conf/cdh_conf/",
    "hadoop.username" = "hdfs",
    
    -- 【关键修复】显式指定 Hive 版本,禁止调用 Hive 3 的新 API
    "hive.version" = "2.1.1"
);

--方案二
--这里我还尝试了如下sql语句,仍然报错Invalid method name: 'get_table_objects_by_name_req'
CREATE CATALOG paimon_catalog PROPERTIES (
    "type" = "paimon",
    "paimon.catalog.type" = "hms",
    "hive.metastore.uris" = "thrift://nd1:9083,thrift://nd3:9083",
    "warehouse" = "hdfs://nd1:8020/user/hive/warehouse",
    "hadoop.conf.dir" = "/home/bigdata/doris/conf/cdh_conf/",
    "hadoop.username" = "hdfs",
    
    -- 【核心修改】即使你是 2.1.1,也请填 1.1.0
    -- 这会强制 Doris 使用旧版 API (get_table) 而不是新版 API (get_table_objects_by_name_req)
    "hive.version" = "1.1.0"
);

上述sql语句执行成功之后会在show tables查看paimon表的时候会报错Invalid method name: 'get_table_objects_by_name_req'。对应可以使用下述方案进行解决:

方案三:

在 Doris MySQL 客户端执行:

sql 复制代码
DROP CATALOG IF EXISTS paimon_catalog;

CREATE CATALOG paimon_catalog PROPERTIES (
    "type" = "paimon",
    "paimon.catalog.type" = "filesystem",
    
    -- 直接指向 HDFS 上的数仓根目录 (注意:如果 nameservice 未解析,直接写 active namenode 地址)
    "warehouse" = "hdfs://nd1:8020/user/hive/warehouse",
    
    "hadoop.conf.dir" = "/home/bigdata/doris/conf/cdh_conf/",
    "hadoop.username" = "hdfs"
);

方案四:

在 Doris MySQL 客户端执行:

sql 复制代码
DROP CATALOG IF EXISTS paimon_catalog;

CREATE CATALOG paimon_catalog PROPERTIES (
    "type" = "paimon",
    -- 【这里是修改点】:将 'hive' 改为 'hms'
    "paimon.catalog.type" = "hms",
    -- 指定 HMS 地址
    "hive.metastore.uris" = "thrift://nd1:9083",
    -- 【核心兼容配置】保持不变,解决 CDH 兼容性
    "hive.version" = "2.1.1",
    -- 数仓路径
    "warehouse" = "hdfs://nd1:8020/user/hive/warehouse",
    -- 复用配置文件
    "hadoop.conf.dir" = "/home/bigdata/doris/conf/cdh_conf/",
    "hadoop.username" = "hdfs"
);

四种方案的特点总结

方案 核心配置 (catalog.type) HMS 地址配置 hive.version 结果 评价
方案一 hms (走 Hive 元数据) 双节点 HA (nd1, nd3) 2.1.1 失败 (Invalid method) 配置被忽略:HA 模式下,版本降级配置似乎失效了。
方案二 hms (走 Hive 元数据) 双节点 HA (nd1, nd3) 1.1.0 失败 (Invalid method) 配置被忽略:即使强制写 1.1.0,HA 模式下依然顽固地用 Hive 3 协议。
方案三 filesystem (走文件系统) 无 (直接读 HDFS) 成功 绕过问题:不走 Hive 协议,就没有协议冲突,但丧失了元数据管理能力。
方案四 hms (走 Hive 元数据) 单节点 (仅 nd1) 2.1.1 成功 最佳实践:单点模式下,版本降级配置生效,成功解决了兼容性问题。

1.为什么方案三 (Filesystem) 可以成功?

原因:它完全绕过了"案发现场"。

  • 原理filesystem 类型的 Catalog,Doris/Paimon 不会去连接 9083 端口的 Hive Metastore。它直接去 HDFS (8020 端口) 扫描目录 user/hive/warehouse/ods.db/... 下的 Paimon 元数据文件。

  • 为什么没报错 :报错的 get_table_objects_by_name_req 是一个 Thrift RPC 请求,只有在连接 Hive Metastore 时才会发送。既然不连 HMS,自然不会报错。

  • 代价:这属于"降级方案",你失去了利用 HMS 进行权限控制、统一视图的能力,且无法与其他 Hive 工具互通。

2.为什么方案一和方案二 (HA 模式) 失败了?

原因:Doris/Paimon 插件在处理 HA URI 列表时存在缺陷,导致 hive.version 参数失效。

  • 现象 :错误 Invalid method name: 'get_table_objects_by_name_req' 是典型的 版本不兼容

    • 客户端 (Doris Paimon):发出了一个 Hive 3.0+ 才有的"批量获取表对象"请求。

    • 服务端 (CDH Hive):我是 Hive 2.1.1,我没听说过这个方法,报错!

  • 深层逻辑 :虽然你写了 "hive.version" = "2.1.1",但在 Scenario 1 & 2 中,你配置了多个 URI (thrift://nd1:9083,thrift://nd3:9083)。

    • 在 Doris 内部初始化 Paimon HiveCatalog 时,当检测到 URI 是列表(HA模式)时,内部的初始化逻辑可能走了一条不同的代码路径,或者在传递参数时发生了丢失 ,导致 hive.version 配置没有被正确应用到底层的 Hive Client。

    • 结果就是:客户端"无视"了你的降级指令,依然默认使用编译时的最高版本(Hive 3.x)去请求,导致撞墙。

3. 为什么方案四 (单节点 HMS) 成功了?

原因:单点连接模式下,配置参数传递正常,成功触发了兼容模式。

  • 原理:方案四与方案一唯一的区别就是去掉了逗号后的第二个地址。

  • 成功逻辑 :当 hive.metastore.uris 只有一个地址时,Doris 正确地将 "hive.version" = "2.1.1" 传递给了底层的 Paimon Hive Shim。

  • 效果 :客户端收到了指令"对方是老版本",于是它自动禁用了 get_table_objects_by_name_req 这种高级 API,转而使用老掉牙但兼容性好的 get_table API。

  • 结论:这就是你一直在寻找的"既能用 HMS 管理元数据,又不会报错"的完美形态。

最终建议与优化

既然 方案四 验证通过,它是目前最适合的方案。

关于 HA (高可用) 的补充建议: 如果你非常介意生产环境存在单点故障(怕 nd1 挂了导致查询失败),而方案一(写死两个 URI)又会报错。你可以尝试以下变通方法来实现 HA:

使用 core-site.xml 实现 Metastore 发现(高级) 不要在 SQL 里写死 URI,而是通过配置文件让 Client 自己去发现。

sql 复制代码
DROP CATALOG IF EXISTS paimon_catalog;

CREATE CATALOG paimon_catalog PROPERTIES (
    "type" = "paimon",
    "paimon.catalog.type" = "hms",
    
    -- 【关键点1】注意:我故意删除了 "hive.metastore.uris" 这一行
    -- 我们赌一把:让 Paimon 插件自己从下面的 xml 文件里读这个配置
    
    -- 【关键点2】版本修复必须保留
    "hive.version" = "2.1.1",
    
    "warehouse" = "hdfs://nd1:8020/user/hive/warehouse",
    
    -- 【关键点3】这里指向你截图的文件夹,必须包含 hive-site.xml
    "hadoop.conf.dir" = "/home/bigdata/doris/conf/cdh_conf/",
    "hadoop.username" = "hdfs"
);

由截图可以得出:

Doris 的 Catalog 创建语法存在强校验 。它强制要求在 SQL 中显式写出 hive.metastore.uris,不能从配置文件"偷懒"。因此只能使用方案四,它虽然在 SQL 层面只写了一个 Metastore 地址,但对于解决 CDH 6.3.2 的兼容性问题,它是目前唯一能打通 Paimon + HMS + Doris 的路径。

运维指南

由于为了兼容性牺牲了 Metastore 的自动 HA 配置,建议采用以下手动 HA 方案

故障切换流程

当主 Metastore 节点 (nd1) 宕机时,无需删除重建 Catalog,只需执行 ALTER CATALOG 命令即可毫秒级切换到备用节点 (nd3)。

执行 SQL:

sql 复制代码
-- 将连接地址切换到 nd3
ALTER CATALOG paimon_catalog SET PROPERTIES (
    "hive.metastore.uris" = "thrift://nd3:9083"
);

5.3 验证查询

sql 复制代码
SWITCH paimon_catalog;
-- 注意:filesystem 模式下,库名通常对应 HDFS 目录名 (可能带有 .db 后缀)
SHOW DATABASES;
sql 复制代码
-- ods数据库存储的为paimon表
USE ods; 
SHOW TABLES;
SELECT * FROM t_admin_division_code LIMIT 5;

6. 问题排查与避坑总结 (Troubleshooting)

在本次部署过程中,我们遇到了以下关键问题并成功解决:

问题现象 报错信息关键词 原因分析 解决方案
Java 环境缺失 JAVA_HOME environment variable is not defined 启动脚本未找到 CDH 自带的 JDK 路径。 fe.confbe.conf 第一行添加 JAVA_HOME=/usr/java/jdk1.8.0_181-cloudera
FE 启动权限错误 fe.out: Permission deniedStop it first 首次使用了 sudo 启动,导致文件归属变为 root,后续 bigdata 用户无法写入。 1. 停止 root 进程。 2. chown -R bigdata:bigdata 修复目录权限。 3. 清空 doris-metalog 目录。 4. 使用 bigdata 用户重新启动并初始化。
集群无法组成 System has no available disk capacity 这是一个新集群的正常报错,原因是 FE 启动后还未注册 BE 节点。 启动 BE 并在 FE 中执行 ALTER SYSTEM ADD BACKEND,报错会自动消失。
端口冲突 tcp listen failed, errno=98 (端口 8040) 端口 8040 被 CDH 的 YARN NodeManager 占用。 修改 be.conf,设置 webserver_port = 18040
BE 启动失败 (权限) failed to create file .../.read_write_test_file: Permission denied BE 数据存储目录 (doris-storage) 是用 root 创建的。 执行 sudo chown -R bigdata:bigdata /home/bigdata/data 修复权限。
Paimon Catalog 类型错误 Unknown paimon.catalog.type value: hive-catalog hive-catalog 是 Flink 的写法,Doris 中应使用 hms 将配置改为 "paimon.catalog.type" = "hms"
Hive API 版本不兼容 Invalid method name: 'get_table_objects_by_name_req' Doris 客户端尝试使用 Hive 3.0 的批量 API,但 CDH 6.3.2 (Hive 2.1.1) 不支持。 放弃 hms 模式,改用 filesystem 模式的 Catalog,直接读取 HDFS 文件。
HDFS 路径无法解析 Incomplete HDFS URI, no host 配置文件中未正确解析 Nameservice,或未指定具体 Namenode。 warehouse 路径改为明确的 NameNode 地址:hdfs://nd1:8020/user/hive/warehouse

7. 基础测试(增删改查)

将下述测试代码保存为doris2_test.py【python解释器:3.8.20、windows系统:11】

python 复制代码
# -*- coding: utf-8 -*-
import pymysql
import random
import time
import logging
import functools
from sshtunnel import SSHTunnelForwarder # 需要: pip install sshtunnel

# ================= 配置信息 =================

# 1. SSH 连接信息 (参考你提供的 test_flink2paimon.py)
SSH_HOST = '10.x.xx.149'       # Doris FE Leader IP (nd11)
SSH_PORT = 22
SSH_USER = 'xxxxx'
SSH_PASSWORD = 'xxxxxxxxxxxxxxxxx' # 来自你的参考代码

# 2. Doris 数据库信息
DORIS_LOCAL_HOST = '127.0.0.1' # 在服务器看来,Doris是跑在本地的
DORIS_QUERY_PORT = 9030        # Doris FE 查询端口
DORIS_DB_USER = 'root'
DORIS_DB_PWD = ''              # 初始部署默认为空,如果有设置请填写
DB_NAME = 'python_perf_test'
TABLE_NAME = 'student_scores_perf'

LOG_FILE = 'doris2_test_report.log'

# ================= 日志与工具模块 (保持不变) =================
def setup_logger():
    logger = logging.getLogger("DorisTester")
    logger.setLevel(logging.INFO)
    if logger.hasHandlers():
        logger.handlers.clear()
    file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8')
    console_handler = logging.StreamHandler()
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    return logger

logger = setup_logger()

def measure_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        logger.info(f"正在执行: [{func.__name__}] ...")
        try:
            result = func(*args, **kwargs)
            duration = time.time() - start_time
            logger.info(f"执行完成: [{func.__name__}] | 耗时: {duration:.4f} 秒")
            return result
        except Exception as e:
            duration = time.time() - start_time
            logger.error(f"执行失败: [{func.__name__}] | 耗时: {duration:.4f} 秒 | 错误: {e}")
            raise e
    return wrapper

# ================= 业务逻辑 (保持不变) =================

@measure_time
def init_db_and_table(cursor):
    cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DB_NAME}")
    cursor.execute(f"USE {DB_NAME}")
    create_sql = f"""
    CREATE TABLE IF NOT EXISTS {TABLE_NAME} (
        id INT COMMENT "用户ID",
        name VARCHAR(50) COMMENT "姓名",
        age INT COMMENT "年龄",
        score INT COMMENT "分数",
        update_time DATETIME COMMENT "更新时间"
    )
    UNIQUE KEY(id)
    DISTRIBUTED BY HASH(id) BUCKETS 1
    PROPERTIES (
        "replication_num" = "1", 
        "enable_unique_key_merge_on_write" = "true"
    );
    """
    # 注意:测试环境副本数改为1,防止BE节点不够报错
    cursor.execute(create_sql)
    cursor.execute(f"TRUNCATE TABLE {TABLE_NAME}")
    logger.info(f"数据库 {DB_NAME} 和表 {TABLE_NAME} 已初始化")

@measure_time
def insert_data_batch(cursor, count=10):
    data = []
    for i in range(1, count + 1):
        name = f"User_{i:03d}"
        age = random.randint(18, 30)
        score = random.randint(50, 100)
        data.append((i, name, age, score))
    sql = f"INSERT INTO {TABLE_NAME} (id, name, age, score, update_time) VALUES (%s, %s, %s, %s, NOW())"
    cursor.executemany(sql, data)
    logger.info(f"成功插入 {count} 条数据")

@measure_time
def query_and_log(cursor, stage_name):
    sql = f"SELECT * FROM {TABLE_NAME} ORDER BY id"
    cursor.execute(sql)
    results = cursor.fetchall()
    logger.info(f"--- [{stage_name}] 当前总行数: {len(results)} ---")
    if results:
        for row in results[:3]: logger.info(f"Row: {row}")

@measure_time
def update_random_data(cursor, update_count=3):
    cursor.execute(f"SELECT id FROM {TABLE_NAME}")
    all_ids = [row['id'] for row in cursor.fetchall()]
    if not all_ids: return
    target_ids = random.sample(all_ids, min(len(all_ids), update_count))
    for uid in target_ids:
        new_score = random.randint(95, 100)
        sql = f"UPDATE {TABLE_NAME} SET score = %s, update_time = NOW() WHERE id = %s"
        cursor.execute(sql, (new_score, uid))
        logger.info(f" -> 更新 ID={uid}, New Score={new_score}")

@measure_time
def delete_random_data(cursor, delete_count=2):
    cursor.execute(f"SELECT id FROM {TABLE_NAME}")
    all_ids = [row['id'] for row in cursor.fetchall()]
    if not all_ids: return
    target_ids = random.sample(all_ids, min(len(all_ids), delete_count))
    for uid in target_ids:
        sql = f"DELETE FROM {TABLE_NAME} WHERE id = %s"
        cursor.execute(sql, (uid,))
        logger.info(f" -> 删除 ID={uid}")

# ================= 主流程 (修改为使用 SSH Tunnel) =================

def main_process():
    server = None
    conn = None
    try:
        logger.info(">>> 1. 正在建立 SSH 隧道 ...")
        # 建立 SSH 隧道
        server = SSHTunnelForwarder(
            (SSH_HOST, SSH_PORT),
            ssh_username=SSH_USER,
            ssh_password=SSH_PASSWORD,
            # 将远程 Doris 的 9030 映射到本地随机端口
            remote_bind_address=(DORIS_LOCAL_HOST, DORIS_QUERY_PORT) 
        )
        server.start()
        logger.info(f">>> SSH 隧道建立成功! 本地端口: {server.local_bind_port}")

        # 2. 连接数据库 (连接本地端口,流量会被转发)
        logger.info(">>> 正在连接 Doris ...")
        conn = pymysql.connect(
            host='127.0.0.1',             # 连接本机
            port=server.local_bind_port,  # 使用隧道映射的端口
            user=DORIS_DB_USER,
            password=DORIS_DB_PWD,
            charset='utf8mb4',
            cursorclass=pymysql.cursors.DictCursor,
            autocommit=True
        )
        cursor = conn.cursor()
        
        # 3. 执行测试逻辑
        init_db_and_table(cursor)
        
        insert_data_batch(cursor, count=100) # 加大一点测试量
        time.sleep(1) # Doris 写入可能有微小延迟,sleep一下
        query_and_log(cursor, "插入后")
        
        update_random_data(cursor, update_count=5)
        time.sleep(1)
        query_and_log(cursor, "更新后")
        
        delete_random_data(cursor, delete_count=5)
        time.sleep(1)
        query_and_log(cursor, "删除后")
        
        logger.info(">>> 测试全部通过 <<<")

    except Exception as e:
        logger.error(f"主流程发生错误: {e}")
    finally:
        # 清理资源
        if conn:
            conn.close()
            logger.info("数据库连接已关闭")
        if server:
            server.stop()
            logger.info("SSH 隧道已关闭")

if __name__ == "__main__":
    main_process()

对应的log文件内容如下:

bash 复制代码
2025-12-09 11:21:04,416 - INFO - >>> 1. 正在建立 SSH 隧道 ...
2025-12-09 11:21:04,766 - INFO - >>> SSH 隧道建立成功! 本地端口: 53725
2025-12-09 11:21:04,767 - INFO - >>> 正在连接 Doris ...
2025-12-09 11:21:04,985 - INFO - 正在执行: [init_db_and_table] ...
2025-12-09 11:21:05,251 - INFO - 数据库 python_perf_test 和表 student_scores_perf 已初始化
2025-12-09 11:21:05,251 - INFO - 执行完成: [init_db_and_table] | 耗时: 0.2653 秒
2025-12-09 11:21:05,251 - INFO - 正在执行: [insert_data_batch] ...
2025-12-09 11:21:10,981 - INFO - 成功插入 100 条数据
2025-12-09 11:21:10,984 - INFO - 执行完成: [insert_data_batch] | 耗时: 5.7328 秒
2025-12-09 11:21:11,985 - INFO - 正在执行: [query_and_log] ...
2025-12-09 11:21:12,621 - INFO - --- [插入后] 当前总行数: 100 ---
2025-12-09 11:21:12,629 - INFO - Row: {'id': 1, 'name': 'User_001', 'age': 30, 'score': 52, 'update_time': datetime.datetime(2025, 12, 9, 11, 21, 5)}
2025-12-09 11:21:12,629 - INFO - Row: {'id': 2, 'name': 'User_002', 'age': 25, 'score': 72, 'update_time': datetime.datetime(2025, 12, 9, 11, 21, 6)}
2025-12-09 11:21:12,629 - INFO - Row: {'id': 3, 'name': 'User_003', 'age': 28, 'score': 64, 'update_time': datetime.datetime(2025, 12, 9, 11, 21, 6)}
2025-12-09 11:21:12,630 - INFO - 执行完成: [query_and_log] | 耗时: 0.6451 秒
2025-12-09 11:21:12,630 - INFO - 正在执行: [update_random_data] ...
2025-12-09 11:21:12,950 - INFO -  -> 更新 ID=94, New Score=95
2025-12-09 11:21:13,003 - INFO -  -> 更新 ID=98, New Score=98
2025-12-09 11:21:13,072 - INFO -  -> 更新 ID=6, New Score=95
2025-12-09 11:21:13,125 - INFO -  -> 更新 ID=71, New Score=97
2025-12-09 11:21:13,173 - INFO -  -> 更新 ID=64, New Score=99
2025-12-09 11:21:13,173 - INFO - 执行完成: [update_random_data] | 耗时: 0.5435 秒
2025-12-09 11:21:14,174 - INFO - 正在执行: [query_and_log] ...
2025-12-09 11:21:14,227 - INFO - --- [更新后] 当前总行数: 100 ---
2025-12-09 11:21:14,228 - INFO - Row: {'id': 1, 'name': 'User_001', 'age': 30, 'score': 52, 'update_time': datetime.datetime(2025, 12, 9, 11, 21, 5)}
2025-12-09 11:21:14,228 - INFO - Row: {'id': 2, 'name': 'User_002', 'age': 25, 'score': 72, 'update_time': datetime.datetime(2025, 12, 9, 11, 21, 6)}
2025-12-09 11:21:14,228 - INFO - Row: {'id': 3, 'name': 'User_003', 'age': 28, 'score': 64, 'update_time': datetime.datetime(2025, 12, 9, 11, 21, 6)}
2025-12-09 11:21:14,228 - INFO - 执行完成: [query_and_log] | 耗时: 0.0545 秒
2025-12-09 11:21:14,228 - INFO - 正在执行: [delete_random_data] ...
2025-12-09 11:21:14,311 - INFO -  -> 删除 ID=24
2025-12-09 11:21:14,361 - INFO -  -> 删除 ID=99
2025-12-09 11:21:14,412 - INFO -  -> 删除 ID=13
2025-12-09 11:21:14,456 - INFO -  -> 删除 ID=22
2025-12-09 11:21:14,514 - INFO -  -> 删除 ID=64
2025-12-09 11:21:14,514 - INFO - 执行完成: [delete_random_data] | 耗时: 0.2859 秒
2025-12-09 11:21:15,515 - INFO - 正在执行: [query_and_log] ...
2025-12-09 11:21:15,573 - INFO - --- [删除后] 当前总行数: 95 ---
2025-12-09 11:21:15,573 - INFO - Row: {'id': 1, 'name': 'User_001', 'age': 30, 'score': 52, 'update_time': datetime.datetime(2025, 12, 9, 11, 21, 5)}
2025-12-09 11:21:15,574 - INFO - Row: {'id': 2, 'name': 'User_002', 'age': 25, 'score': 72, 'update_time': datetime.datetime(2025, 12, 9, 11, 21, 6)}
2025-12-09 11:21:15,574 - INFO - Row: {'id': 3, 'name': 'User_003', 'age': 28, 'score': 64, 'update_time': datetime.datetime(2025, 12, 9, 11, 21, 6)}
2025-12-09 11:21:15,574 - INFO - 执行完成: [query_and_log] | 耗时: 0.0594 秒
2025-12-09 11:21:15,575 - INFO - >>> 测试全部通过 <<<
2025-12-09 11:21:15,575 - INFO - 数据库连接已关闭
2025-12-09 11:21:15,653 - INFO - SSH 隧道已关闭

去nd11执行下述语句进行查看:

bash 复制代码
mysql -h 10.x.xx.149 -P 9030 -uroot
sql 复制代码
show databases;
use python_perf_test;
show tables;
bash 复制代码
select * from student_scores_perf;

7.1 插入数据

这里展示部分数据,可以看出插入了100条测试数据:

7.2 更新数据

控制台打印结果如下:

终端验证,部分数据展示如下:

7.3 删除数据

控制台打印结果如下:

终端验证,部分数据展示如下:

8. Paimon外表数据写入Doris内表基础测试

8.1 数据准备

对于Paimon外表数据写入Doris内表基础测试,需要提前在Flink SQL会话里面创建Paimon表,并插入测试数据

sql 复制代码
-- 1. 创建 Flink 端的 Paimon Catalog
CREATE CATALOG paimon_catalog WITH (
  'type'        = 'paimon',
  'warehouse'   = 'hdfs:///user/hive/warehouse',
  'metastore'   = 'hive',
  'hive-conf-dir' = '/etc/hive/conf.cloudera.hive'
);

-- 2. 切换 Catalog 和 Database
USE CATALOG my_paimon;
CREATE DATABASE IF NOT EXISTS ods;
USE ods;

-- 3. 创建 Paimon 表 (源表)
-- 这是一个记录用户行为的日志表
CREATE TABLE IF NOT EXISTS paimon_source_event (
    user_id INT,
    item_id INT,
    behavior STRING,
    dt STRING,
    ts TIMESTAMP(3),
    PRIMARY KEY (dt, user_id, item_id) NOT ENFORCED
) PARTITIONED BY (dt) WITH (
    'bucket' = '1',
    'file.format' = 'parquet'
);

-- 4. 写入测试数据 (Batch 模式写入)
INSERT INTO paimon_source_event VALUES
(1001, 501, 'click', '2025-12-12', TIMESTAMP '2025-12-12 10:00:00.123'),
(1002, 502, 'view',  '2025-12-12', TIMESTAMP '2025-12-12 10:05:00.456'),
(1003, 501, 'buy',   '2025-12-12', TIMESTAMP '2025-12-12 10:10:00.789'),
(1001, 503, 'view',  '2025-12-13', TIMESTAMP '2025-12-13 11:00:00.000'),
(1004, 501, 'click', '2025-12-13', TIMESTAMP '2025-12-13 11:05:00.000');

8.2 测试代码

将下述测试代码保存为doris2_paimon_etl_test.py【python解释器:3.8.20、windows系统:11】

python 复制代码
# -*- coding: utf-8 -*-
import pymysql
import time
import logging
import functools
from sshtunnel import SSHTunnelForwarder

# ================= 配置信息 =================

# 1. SSH 连接信息
SSH_HOST = '10.x.xx.149'
SSH_PORT = 22
SSH_USER = 'xxxx'           
SSH_PASSWORD = 'xxxxxxxxxxxxxxxxx' 

# 2. Doris 数据库连接信息
DORIS_LOCAL_HOST = '127.0.0.1' 
DORIS_QUERY_PORT = 9030        
DORIS_DB_USER = 'root'
DORIS_DB_PWD = ''              

# 3. Paimon Catalog 配置
CATALOG_PROPS = {
    "type": "paimon",
    "paimon.catalog.type": "hms",
    "hive.metastore.uris": "thrift://nd1:9083",
    "hive.version": "2.1.1",
    "warehouse": "hdfs://nd1:8020/user/hive/warehouse",
    "hadoop.conf.dir": "/home/bigdata/doris/conf/cdh_conf/",
    "hadoop.username": "hdfs"
}

# 4. 业务配置
PAIMON_CATALOG_NAME = 'paimon_catalog' 
PAIMON_DB = 'ods'
PAIMON_TABLE = 'paimon_source_event'

DORIS_TEST_DB = 'python_perf_test'
DORIS_TARGET_TABLE = 'doris_target_event_sink'

LOG_FILE = 'doris_paimon_etl_report.log'

# ================= 日志与工具模块 =================
def setup_logger():
    logger = logging.getLogger("DorisPaimonTester")
    logger.setLevel(logging.INFO)
    if logger.hasHandlers():
        logger.handlers.clear()
    file_handler = logging.FileHandler(LOG_FILE, mode='w', encoding='utf-8')
    console_handler = logging.StreamHandler()
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    return logger

logger = setup_logger()

def measure_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        logger.info(f"正在执行: [{func.__name__}] ...")
        try:
            result = func(*args, **kwargs)
            duration = time.time() - start_time
            logger.info(f"执行完成: [{func.__name__}] | 耗时: {duration:.4f} 秒")
            return result
        except Exception as e:
            duration = time.time() - start_time
            logger.error(f"执行失败: [{func.__name__}] | 耗时: {duration:.4f} 秒 | 错误: {e}")
            raise e
    return wrapper

# ================= 核心测试逻辑 =================

@measure_time
def init_doris_catalog(cursor):
    """在 Doris 中自动创建 Paimon Catalog"""
    logger.info(f"正在初始化 Doris Catalog: {PAIMON_CATALOG_NAME} ...")
    cursor.execute(f"DROP CATALOG IF EXISTS {PAIMON_CATALOG_NAME}")
    props_str = ",\n".join([f'"{k}" = "{v}"' for k, v in CATALOG_PROPS.items()])
    create_sql = f"""
    CREATE CATALOG {PAIMON_CATALOG_NAME} PROPERTIES (
        {props_str}
    );
    """
    cursor.execute(create_sql)
    logger.info("Catalog 创建成功!")
    time.sleep(1)

@measure_time
def check_paimon_source(cursor):
    """验证 Doris 是否能通过 Catalog 读取 Paimon 数据"""
    logger.info(f"检查 Paimon 数据源: {PAIMON_CATALOG_NAME}.{PAIMON_DB}.{PAIMON_TABLE}")
    cursor.execute(f"SWITCH {PAIMON_CATALOG_NAME}")
    cursor.execute(f"USE {PAIMON_DB}")
    cursor.execute("SHOW TABLES")
    tables = [list(row.values())[0] for row in cursor.fetchall()] 
    
    if PAIMON_TABLE not in tables:
        raise Exception(f"Paimon 表 {PAIMON_TABLE} 未找到!")
    
    sql = f"SELECT * FROM {PAIMON_TABLE} ORDER BY dt, user_id LIMIT 5"
    cursor.execute(sql)
    results = cursor.fetchall()
    logger.info(f"Paimon 数据预览 (前5条):")
    for row in results:
        logger.info(row)
    
    if not results:
        raise Exception("Paimon 表为空!")
    return len(results)

@measure_time
def create_doris_target_table(cursor):
    """创建 Doris 内部表 (已修正字段顺序)"""
    cursor.execute("SWITCH internal")
    cursor.execute(f"CREATE DATABASE IF NOT EXISTS {DORIS_TEST_DB}")
    cursor.execute(f"USE {DORIS_TEST_DB}")
    
    # 【注意】dt 是 Key,必须紧跟在 item_id 后面
    create_sql = f"""
    CREATE TABLE IF NOT EXISTS {DORIS_TARGET_TABLE} (
        user_id INT COMMENT "用户ID",
        item_id INT COMMENT "商品ID",
        dt VARCHAR(20) COMMENT "日期分区",
        behavior VARCHAR(50) COMMENT "行为类型",
        ts DATETIME(3) COMMENT "时间戳"
    )
    UNIQUE KEY(user_id, item_id, dt)
    PARTITION BY LIST(dt) (
        PARTITION p20251212 VALUES IN ("2025-12-12"),
        PARTITION p20251213 VALUES IN ("2025-12-13")
    )
    DISTRIBUTED BY HASH(user_id) BUCKETS 1
    PROPERTIES (
        "replication_num" = "1",
        "enable_unique_key_merge_on_write" = "true"
    );
    """
    cursor.execute(create_sql)
    cursor.execute(f"TRUNCATE TABLE {DORIS_TARGET_TABLE}")
    logger.info(f"Doris 内表 {DORIS_TARGET_TABLE} 已准备就绪")

@measure_time
def execute_etl_paimon_to_doris(cursor):
    """执行 INSERT INTO ... SELECT ..."""
    logger.info(">>> 开始执行从 Paimon 到 Doris 的数据导入 (ETL) <<<")
    
    # 显式指定插入字段顺序,确保与建表顺序一致
    etl_sql = f"""
    INSERT INTO internal.{DORIS_TEST_DB}.{DORIS_TARGET_TABLE}
    (user_id, item_id, dt, behavior, ts)
    SELECT user_id, item_id, dt, behavior, ts 
    FROM {PAIMON_CATALOG_NAME}.{PAIMON_DB}.{PAIMON_TABLE}
    """
    cursor.execute(etl_sql)
    logger.info("ETL SQL 提交完毕")

@measure_time
def verify_data_consistency(cursor):
    """验证两边数据是否一致"""
    logger.info(">>> 开始数据一致性校验 <<<")
    
    cursor.execute(f"SELECT count(*) as cnt FROM {PAIMON_CATALOG_NAME}.{PAIMON_DB}.{PAIMON_TABLE}")
    paimon_count = cursor.fetchone()['cnt']
    
    cursor.execute(f"SELECT count(*) as cnt FROM internal.{DORIS_TEST_DB}.{DORIS_TARGET_TABLE}")
    doris_count = cursor.fetchone()['cnt']
    
    logger.info(f"Paimon 源表行数: {paimon_count}")
    logger.info(f"Doris 目标表行数: {doris_count}")
    
    if paimon_count == doris_count:
        logger.info("✅ 数据条数一致,集成测试通过!")
    else:
        logger.error("❌ 数据条数不一致。")
        
    cursor.execute(f"SELECT * FROM internal.{DORIS_TEST_DB}.{DORIS_TARGET_TABLE} ORDER BY dt, user_id LIMIT 3")
    rows = cursor.fetchall()
    logger.info("Doris 内表数据抽样:")
    for row in rows:
        logger.info(row)

# ================= 主流程 =================

def main_process():
    server = None
    conn = None
    try:
        logger.info(">>> 1. 正在建立 SSH 隧道 ...")
        server = SSHTunnelForwarder(
            (SSH_HOST, SSH_PORT),
            ssh_username=SSH_USER,
            ssh_password=SSH_PASSWORD,
            remote_bind_address=(DORIS_LOCAL_HOST, DORIS_QUERY_PORT) 
        )
        server.start()
        logger.info(f">>> SSH 隧道建立成功! 本地端口: {server.local_bind_port}")

        logger.info(">>> 2. 连接 Doris ...")
        conn = pymysql.connect(
            host='127.0.0.1',
            port=server.local_bind_port,
            user=DORIS_DB_USER,
            password=DORIS_DB_PWD,
            charset='utf8mb4',
            cursorclass=pymysql.cursors.DictCursor,
            autocommit=True
        )
        cursor = conn.cursor()
        
        init_doris_catalog(cursor)
        check_paimon_source(cursor)
        create_doris_target_table(cursor)
        execute_etl_paimon_to_doris(cursor)
        time.sleep(2)
        verify_data_consistency(cursor)
        
        logger.info(">>> 所有测试步骤执行完毕 <<<")

    except Exception as e:
        logger.error(f"主流程发生错误: {e}")
        import traceback
        logger.error(traceback.format_exc())
    finally:
        if conn:
            conn.close()
        if server:
            server.stop()

if __name__ == "__main__":
    main_process()

对应的log文件内容如下:

bash 复制代码
2025-12-12 15:19:45,098 - INFO - >>> 1. 正在建立 SSH 隧道 ...
2025-12-12 15:19:45,395 - INFO - >>> SSH 隧道建立成功! 本地端口: 50437
2025-12-12 15:19:45,395 - INFO - >>> 2. 连接 Doris ...
2025-12-12 15:19:45,573 - INFO - 正在执行: [init_doris_catalog] ...
2025-12-12 15:19:45,573 - INFO - 正在初始化 Doris Catalog: paimon_catalog ...
2025-12-12 15:19:45,588 - INFO - Catalog 创建成功!
2025-12-12 15:19:46,588 - INFO - 执行完成: [init_doris_catalog] | 耗时: 1.0155 秒
2025-12-12 15:19:46,589 - INFO - 正在执行: [check_paimon_source] ...
2025-12-12 15:19:46,589 - INFO - 检查 Paimon 数据源: paimon_catalog.ods.paimon_source_event
2025-12-12 15:19:46,870 - INFO - Paimon 数据预览 (前5条):
2025-12-12 15:19:46,870 - INFO - {'user_id': 1001, 'item_id': 501, 'behavior': 'click', 'dt': '2025-12-12', 'ts': datetime.datetime(2025, 12, 12, 10, 0, 0, 123000)}
2025-12-12 15:19:46,871 - INFO - {'user_id': 1002, 'item_id': 502, 'behavior': 'view', 'dt': '2025-12-12', 'ts': datetime.datetime(2025, 12, 12, 10, 5, 0, 456000)}
2025-12-12 15:19:46,871 - INFO - {'user_id': 1003, 'item_id': 501, 'behavior': 'buy', 'dt': '2025-12-12', 'ts': datetime.datetime(2025, 12, 12, 10, 10, 0, 789000)}
2025-12-12 15:19:46,871 - INFO - {'user_id': 1001, 'item_id': 503, 'behavior': 'view', 'dt': '2025-12-13', 'ts': datetime.datetime(2025, 12, 13, 11, 0)}
2025-12-12 15:19:46,871 - INFO - {'user_id': 1004, 'item_id': 501, 'behavior': 'click', 'dt': '2025-12-13', 'ts': datetime.datetime(2025, 12, 13, 11, 5)}
2025-12-12 15:19:46,872 - INFO - 执行完成: [check_paimon_source] | 耗时: 0.2830 秒
2025-12-12 15:19:46,872 - INFO - 正在执行: [create_doris_target_table] ...
2025-12-12 15:19:46,974 - INFO - Doris 内表 doris_target_event_sink 已准备就绪
2025-12-12 15:19:46,974 - INFO - 执行完成: [create_doris_target_table] | 耗时: 0.1024 秒
2025-12-12 15:19:46,974 - INFO - 正在执行: [execute_etl_paimon_to_doris] ...
2025-12-12 15:19:46,975 - INFO - >>> 开始执行从 Paimon 到 Doris 的数据导入 (ETL) <<<
2025-12-12 15:19:51,286 - INFO - ETL SQL 提交完毕
2025-12-12 15:19:51,286 - INFO - 执行完成: [execute_etl_paimon_to_doris] | 耗时: 4.3116 秒
2025-12-12 15:19:53,286 - INFO - 正在执行: [verify_data_consistency] ...
2025-12-12 15:19:53,287 - INFO - >>> 开始数据一致性校验 <<<
2025-12-12 15:19:53,579 - INFO - Paimon 源表行数: 5
2025-12-12 15:19:53,579 - INFO - Doris 目标表行数: 5
2025-12-12 15:19:53,579 - INFO - ✅ 数据条数一致,集成测试通过!
2025-12-12 15:19:53,605 - INFO - Doris 内表数据抽样:
2025-12-12 15:19:53,606 - INFO - {'user_id': 1001, 'item_id': 501, 'dt': '2025-12-12', 'behavior': 'click', 'ts': datetime.datetime(2025, 12, 12, 10, 0, 0, 123000)}
2025-12-12 15:19:53,606 - INFO - {'user_id': 1002, 'item_id': 502, 'dt': '2025-12-12', 'behavior': 'view', 'ts': datetime.datetime(2025, 12, 12, 10, 5, 0, 456000)}
2025-12-12 15:19:53,606 - INFO - {'user_id': 1003, 'item_id': 501, 'dt': '2025-12-12', 'behavior': 'buy', 'ts': datetime.datetime(2025, 12, 12, 10, 10, 0, 789000)}
2025-12-12 15:19:53,606 - INFO - 执行完成: [verify_data_consistency] | 耗时: 0.3198 秒
2025-12-12 15:19:53,606 - INFO - >>> 所有测试步骤执行完毕 <<<

8.3 数据验证

去Doris终端验证数据结果如下:

bash 复制代码
mysql -h 10.x.xx.149 -P 9030 -uroot

执行下述sql

sql 复制代码
SWITCH internal;
SHOW DATABASES;
USE python_perf_test;
SHOW TABLES;
sql 复制代码
SELECT * FROM doris_target_event_sink ORDER BY user_id;

也可以在同一个查询窗口中直接对比两边的数量(不需要反复 SWITCH):

sql 复制代码
-- 这里的 internal 和 paimon_catalog 是 Catalog 名称
SELECT 
    (SELECT count(*) FROM internal.python_perf_test.doris_target_event_sink) as doris_count,
    (SELECT count(*) FROM paimon_catalog.ods.paimon_source_event) as paimon_count;
相关推荐
鹿衔`4 小时前
StarRocks 4.0.2 (CDH 环境)与Paimon数据湖集成混合部署文档
linux·硬件架构·paimon·starroks
鹿衔`6 小时前
Apache Doris 4.0.1 集群部署与 Paimon 数据湖集成实战文档
flink·apache·doris·paimon
SelectDB技术团队1 天前
面向 Agent 的高并发分析:Doris vs. Snowflake vs. ClickHouse
数据仓库·人工智能·科技·apache·知识图谱
初願致夕霞1 天前
C++文件压缩及解压缩小程序的实现
c++·小程序·apache
小小8程序员1 天前
Apache Doris的部署
apache
Rover.x2 天前
head table is mandatory
java·apache
SelectDB2 天前
面向 Agent 的高并发分析:Doris vs. Snowflake vs. ClickHouse
数据库·apache·agent
微学AI2 天前
时序数据库的核心概念与使用指南:Apache IoTDB 深度剖析与部署实践
apache·时序数据库·iotdb
写代码的【黑咖啡】3 天前
Apache Flink SQL 入门与常见问题解析
sql·flink·apache