跳转至

Vertica COPY 小文件加载优化

作者:JiangChong | 撰写时间:2026-05-22

适用场景: ETL 作业向 Vertica 批量加载时,源端产生大量小文件(每文件 < 100MB),导致 COPY 变慢、ROS 容器爆炸、Tuple Mover 合并积压。

关联文章:

  • 在Vertica使用COPY加载数据 — COPY 基础概念与三种加载方式(AUTO/DIRECT/TRICKLE)
  • ROS Pushback 故障排查 — ROS 容器数逼近 1024 上限的根本原因与解决
  • Tuple Mover 最佳实践完全指南 — Mergeout 触发条件、阈值调优、ROS 容器生命周期管理
  • 某运营商 Vertica 数据仓库装载慢问题分析 — HDFS 加载变慢的真实排查案例

理解全文脉络: 如果你刚遇到「COPY 变慢了」但不确定原因,从第 2 节(监控)和第 3 节(定位)开始;如果你已经确认是小文件导致的性能问题,直接跳到第 4 节(解决方案);如果你想看完整的排查思路和案例,从第 5 节(案例)和第 6 节(完整演练)入手。


1. 原理理解

1.1 COPY DIRECT 做了什么

COPY DIRECT 将数据直接写入 ROS 容器(Read Optimized Storage),跳过了 WOS(Write Optimized Storage)。ROS 容器是数据被排序、编码并写入磁盘后的最小存储单元。

传统数据库中,INSERT 写入的是行式页面(page),一个页面可以容纳多行。Vertica 是列式存储,每一次 COPY DIRECT 调用(即使只加载了一个 1MB 的小文件)也会产生至少一个 ROS 容器——每个 ROS 容器包含该次加载的全部列数据。

比喻:把普通数据库的 INSERT 想象成往笔记本上写一行字,写很多行也只是在同一页上。Vertica 的 COPY DIRECT 则像每次打印一张新纸放到文件夹里——即使纸上只有一行字,也得占一个文件夹格子。

1.2 小文件加载的连锁反应

当你有大量小文件(如每天 500 个 10MB 文件)并用 COPY DIRECT 逐个加载时,会发生三级放大效应:

每个 COPY DIRECT → 1+ 个 ROS 容器
500 个小文件 → 500+ 个 ROS 容器
N 个节点 × K-safety=1 → 每个节点 500 × 2(buddy)× N(如有unsegmented)

这导致三个问题同时发生:

层次 问题 影响
存储层 ROS 容器数爆炸 每个节点每投影最多 1024 个 ROS,逼近上限触发 ROS Pushback
合并层 Tuple Mover 积压 TM 需要不停合并碎片化的 ROS,Mergeout 持续消耗 CPU 和 I/O
查询层 查询变慢 每个 ROS 容器都有独立的元数据、位置索引,查询时要扫描更多 ROS

1.3 问题发生的条件

条件 阈值 说明
单个文件大小 < 100MB/节点 低于 COPY DIRECT 推荐的 100MB 下限
单次 ETL 批次的文件数 > 50 个 批次内大量小文件
单个 COPY 加载的文件数 = 1 每个文件一个 COPY 语句(最坏情况)
COPY 频率 > 每小时 1 次 频繁的 trickle load
加载方式 DIRECT WOS 资源池 9.3.0 弃用 / 10.0.0 移除,所有 COPY 均为 DIRECT;关键不在关键字而在逐文件独立调用

触发机制: 核心矛盾在于 COPY DIRECT 的设计目标是「大块数据直接落盘」,牺牲了少量文件的容器碎片化代价,换来大文件的性能收益。当文件大小不满足 DIRECT 的使用条件时,代价远大于收益。


2. 系统级监控(从宏观入手)

在深入排查具体 COPY 问题之前,先通过宏观监控确定范围和严重程度。

2.1 检查 ROS 容器数

SELECT node_name,
       projection_schema,
       anchor_table_name,
       SUM(ros_count) AS total_ros,
       MAX(ros_count) AS max_ros_per_projection,
       COUNT(DISTINCT projection_name) AS projection_count
FROM v_monitor.projection_storage
GROUP BY node_name, projection_schema, anchor_table_name
ORDER BY total_ros DESC, projection_schema, anchor_table_name, node_name
LIMIT 20;

如何解读:

  • max_ros_per_projection > 500:说明某些投影的 ROS 容器数已经偏高,Tuple Mover 合并压力大
  • max_ros_per_projection > 900:接近 1024 上限,随时可能触发 ROS Pushback,需要立即干预
  • total_rosprojection_count 的比例:如果总 ROS 数接近投影数 × 节点数,说明容器数在合理范围;如果远超,说明存在大量未合并的小 ROS
  • 重点关注锚表(anchor_table_name)——这是数据的最终归属表,也是 COPY 的目标

2.2 检查 Tuple Mover 合并效率

-- 查询最近 1 小时内的 TM mergeout 事件
SELECT time,
       node_name,
       schema_name,
       table_name,
       projection_name,
       event,
       plan_type,
       container_count,
       total_size_in_bytes
FROM v_internal.dc_tuple_mover_events
WHERE time > CURRENT_TIMESTAMP - INTERVAL '1 hour'
  AND event IN ('Start', 'Complete')
ORDER BY time DESC
LIMIT 20;

此外也可以通过 projection_storage 的时间维度间接判断 TM 工作量:

-- 比较两次快照的 ROS 容器数变化,评估 TM 合并速度
-- 注:需要手动记录两次快照,间隔 5-10 分钟
SELECT node_name,
       projection_schema,
       anchor_table_name,
       SUM(ros_count) AS total_ros
FROM v_monitor.projection_storage
GROUP BY node_name, projection_schema, anchor_table_name
ORDER BY total_ros DESC
LIMIT 20;

如何解读:

  • 执行两次(间隔 5-10 分钟),如果特定表的 total_ros 数没有明显下降,说明 TM 合并跟不上生成速度
  • 如果 total_ros 持续增长 → 典型的小文件加载问题

2.3 检查 COPY 加载流

-- 查询最近 1 小时内活跃和已完成的加载流
SELECT stream_name,
       schema_name,
       table_name,
       load_start,
       load_duration_ms,
       accepted_row_count,
       rejected_row_count,
       input_file_size_bytes,
       parse_complete_percent,
       sort_complete_percent,
       unsorted_row_count,
       sorted_row_count
FROM v_monitor.load_streams
WHERE load_start::TIMESTAMPTZ > CURRENT_TIMESTAMP - INTERVAL '1 hour'
ORDER BY load_start::TIMESTAMPTZ DESC;

注:load_start 在系统表中为 VARCHAR 类型,比较时需 ::TIMESTAMPTZ 显式转型。

补充:如需追踪单个 COPY 的事件生命周期(PARSEBEGIN → SOURCEBEGIN → SOURCEDONE → PARSEDONE),可查询 v_internal.dc_load_events

-- 追踪某个 transaction 的 COPY 事件生命周期
SELECT time, event_type, event_description,
       rows_accepted, rows_rejected, uri
FROM v_internal.dc_load_events
WHERE transaction_id = :t_id
ORDER BY time;

如何解读:

  • input_file_size_bytes 很小但 accepted_row_count 也很小:典型的小文件加载模式。每个文件产生的数据行很少,但每个 COPY 都建立了一个新的 stream
  • load_duration_ms 远大于预期:如果一个 10MB 的文件加载耗时超过 30 秒,说明加载管道中有瓶颈(可能是 IO、网络或解析)
  • parse_complete_percentsort_complete_percent 差异大:解析完成但排序缓慢 → sort 阶段是瓶颈,通常与 CPU 或内存不够有关
  • rejected_row_count > 0:有被拒绝的行,检查 REJECTED DATA 进一步分析
  • stream 数量很多:在短时间内有大量 stream → 说明 COPY 请求频繁,每个都是独立的事务

2.4 检查节点级别的加载分布

-- 检查各节点当前加载分布是否均匀
SELECT node_name,
       COUNT(*) AS source_count,
       SUM(input_size) AS total_input_size,
       SUM(rows_produced) AS total_rows,
       SUM(read_bytes) AS total_read_bytes,
       MAX(peak_cooperating_threads) AS max_threads
FROM v_monitor.load_sources
GROUP BY node_name
ORDER BY total_input_size DESC;

如何解读:

  • 某个节点的 source_count 远高于其他节点:说明 Apportioned Load 没有生效(见 3.3 节),数据加载倾斜到单节点
  • peak_cooperating_threads 很低:协作解析未启用或数据格式不支持
  • total_input_size 在节点间差异 > 3:1:加载严重倾斜,部分节点空闲,部分节点过载

2.5 检查系统资源

SELECT node_name,
       end_time,
       average_cpu_usage_percent,
       average_memory_usage_percent,
       net_rx_kbytes_per_second,
       net_tx_kbytes_per_second,
       io_read_kbytes_per_second,
       io_written_kbytes_per_second
FROM v_monitor.system_resource_usage
WHERE end_time > CURRENT_TIMESTAMP - INTERVAL '1 hour'
ORDER BY end_time DESC;

如何解读:

  • average_cpu_usage_percent > 80% 且加载任务仍在排队:CPU 是瓶颈,检查是否大量逐文件独立 COPY 导致解析线程过多,或考虑拓展节点
  • io_written_kbytes_per_second 非常高:大量 ROS 容器写入磁盘,可能是小文件 DIRECT 加载的直接信号
  • net_rx_kbytes_per_second 非常高(从 HDFS/S3 加载时):网络可能是瓶颈,检查是否使用了 REJECTMAXERROR TOLERANCE(会禁用 Apportioned Load,迫使单节点加载)[^1]

3. 逐步定位根因(从宏观到微观)

3.1 步骤 1:确认文件基数

做什么: 确认 ETL 作业到底产生了多少文件,每个文件的大小分布。

检测方法: 这里需要从数据源端检查(HDFS、S3、本地 NFS),而不是在 Vertica 内。

Linux 命令(源端检查):

# 列出目录下文件数量
ls /data/etl/input/ | wc -l

# 统计文件大小分布(按 10MB 分段)
find /data/etl/input/ -type f -exec ls -l {} \; | \
  awk '{sum[$5 < 10485760 ? "<10MB" : $5 < 104857600 ? "10-100MB" : $5 < 1073741824 ? "100MB-1GB" : ">1GB"]++} END {for (k in sum) print k, sum[k]}'

# 统计总文件数
find /data/etl/input/ -type f | wc -l

检出标准: 如果 > 50% 的文件 < 100MB 且全部使用 COPY DIRECT 加载,则本文的诊断完全适用。如果大部分文件 > 500MB,问题出在其他地方(网络、解析器、资源竞争)。

3.2 步骤 2:确认加载方式

做什么: 确认 ETL 是否使用了 COPY DIRECT

背景:WOS 资源池(WOSDATA)从 Vertica 9.3.0 开始弃用(Deprecated),10.0.0 完全移除(Removed)。此后所有 COPY 均为 DIRECT 方式加载数据,不存在 AUTO vs DIRECT 的选择——这就是 DIRECT。以下检测是为了确认 ETL 是否逐文件独立调用 COPY(最坏模式)。

检测方法(检查 SQL 文本):

-- 从 query_requests 查看最近 COPY 语句是否包含 DIRECT 关键字
SELECT request_id,
       start_timestamp,
       LEFT(request, 200) AS request_preview
FROM v_monitor.query_requests
WHERE request_type = 'LOAD'
  AND start_timestamp > CURRENT_TIMESTAMP - INTERVAL '24 hours'
ORDER BY start_timestamp DESC
LIMIT 50;

如果 request 文本中包含 DIRECT,即为 COPY DIRECT

3.3 步骤 3:确认 Apportioned Load 是否生效

做什么: 确认多个节点是否参与了加载。如果只有一个节点在干活,那即使文件很大也会慢。

检测方法:

-- 按 stream 查看各节点的参与情况
SELECT ls.stream_name,
       ls.table_name,
       ls.input_file_size_bytes,
       COUNT(DISTINCT lso.node_name) AS participating_nodes,
       SUM(lso.rows_produced) AS total_rows,
       SUM(lso.read_bytes) AS total_read_bytes
FROM v_monitor.load_streams ls
JOIN v_monitor.load_sources lso
  ON ls.session_id = lso.session_id
 AND ls.transaction_id = lso.transaction_id
 AND ls.statement_id = lso.statement_id
WHERE ls.load_start::TIMESTAMPTZ > CURRENT_TIMESTAMP - INTERVAL '1 hour'
GROUP BY ls.stream_name, ls.table_name, ls.input_file_size_bytes
ORDER BY ls.load_start::TIMESTAMPTZ DESC;

如何解读:

  • participating_nodes < 总节点数 且文件 > 100MB → Apportioned Load 未生效。可能原因:
    • 使用了 FILTER(如 SearchAndReplace),这会禁用 Apportioned Load
    • 数据格式不支持(ORC/PARQUET 在 24.4 之前)
    • REJECTMAX 被设置,这也禁用 Apportioned Load
    • 数据源路径不是所有节点都能访问
  • participating_nodes = 1只有单节点加载,这是最坏的情况,所有数据由一个节点处理,性能完全受限于单节点带宽

3.4 步骤 4:确认 ROS 容器增长速度

做什么: 通过对比两个时间点的 projection_storage 快照,量化 ROS 容器的增长速度。

-- 快照 1:当前 ROS 容器数
-- 5 分钟后执行快照 2,对比 ros_count 差值
-- 注:以下为快照查询,需手动记录两次结果
SELECT node_name,
       projection_schema,
       anchor_table_name,
       SUM(ros_count) AS total_ros,
       MAX(ros_count) AS max_ros
FROM v_monitor.projection_storage
GROUP BY node_name, projection_schema, anchor_table_name
ORDER BY total_ros DESC
LIMIT 30;

如何解读:

  • 5 分钟内 total_ros 增长 > 10 而实际行数增长不多 → 碎片化加载在发生
  • 特定表的 max_ros 接近 1024 → 该表可能很快触发 ROS Pushback

3.5 步骤 5:检查资源池配置

做什么: 确认 COPY 使用的资源池是否有足够的内存和并发能力。

SELECT node_name,
       pool_name,
       memory_size_kb,
       max_memory_size_kb,
       memory_inuse_kb,
       query_budget_kb,
       planned_concurrency,
       max_concurrency,
       execution_parallelism,
       running_query_count
FROM v_monitor.resource_pool_status
WHERE pool_name NOT IN ('sysquery', 'tm', 'recovery', 'refresh', 'jvm', 'metadata', 'blobdata')
ORDER BY pool_name, node_name;

如何解读:

  • query_budget_kb 过小(< 256MB):每个 COPY 能用的内存太少,Sort 阶段可能被迫 spill 到磁盘
  • max_concurrency 为 NULL(无限制):如果同时提交了 50 个 COPY,它们全部争抢 CPU 和内存,反而互相拖慢
  • running_query_count 非常高:并发 COPY 数超出了系统承载能力

4. 解决方案(从快速见效到根本治理)

4.1 立即措施(当天可执行)

4.1.1 合并 COPY 语句:在同一个 COPY 中列出所有文件

改进前(最坏写法):

-- 每个文件一个 COPY 语句 → 500 个文件 = 500 次 COPY
COPY schema.table FROM '/data/file_001.csv' DIRECT;
COPY schema.table FROM '/data/file_002.csv' DIRECT;
-- ... 重复 500 次

改进后:

-- 同一个 COPY 加载多个文件 → 1 次 COPY = 1 个 ROS 容器
COPY schema.table FROM '/data/file_001.csv',
                       '/data/file_002.csv',
                       -- ...
                       '/data/file_500.csv' DIRECT;

或者使用通配符:

COPY schema.table FROM '/data/file_*.csv' DIRECT;

为什么有效: Vertica 在一个 COPY 语句中(即使加载多个文件)最终只产生一批 ROS 容器(每节点每投影),而不是每个文件一批。这从根本上减少了 ROS 容器的创建数量。500 个小文件合并到一个 COPY → ROS 容器数从 500 降到约 1(每投影)。通配符或逗号列表均支持任意数量的文件,且 Vertica 会在各节点间并发解析和加载。

4.1.2 用 Linux 管道合并文件再加载

当无法修改批处理逻辑时,可以在加载前合并文件:

# 将 500 个小文件合并为一个大的管道流,喂给 COPY LOCAL
cat /data/etl/input/file_*.csv | vsql -c "COPY schema.table FROM STDIN DIRECT;"

为什么有效: Vertica 看到的是一个连续的数据流,作为单一数据源处理,生成一批 ROS 容器。STDIN 方式是让 COPY 从标准输入读取,这种方式天然避免了多文件多 ROS 的问题。

注意事项:

  • cat 默认按字母顺序排列文件,如果文件顺序有意义(如按时间),需要用 ls -t 等排序
  • 管道方式的吞吐量受限于 catvsql 之间的单线程传输。对于极大数据量,优先用 4.1.1 的通配符方案,Vertica 会在各节点间并发加载

4.2 短期优化(当周执行)

4.2.1 调优 Tuple Mover 合并频率

如果小文件已经产生了很多 ROS 容器,需要让 Tuple Mover 更积极地合并:

-- 增加 TM 资源池的并发能力
ALTER RESOURCE POOL tm PLANNEDCONCURRENCY 5;
ALTER RESOURCE POOL tm MAXCONCURRENCY 5;
-- 缩短 Mergeout 间隔(默认 600 秒)
ALTER DATABASE DEFAULT SET MergeoutInterval = 300;

为什么这样设: PLANNEDCONCURRENCYMAXCONCURRENCY 从默认的 3 增加到 5,让 TM 同时能处理更多投影的 mergeout。绝不要超过 6,超过后 TM 自身会成为 CPU 负担。MergeoutInterval 从 600 秒降到 300 秒,TM 每 5 分钟(而非 10 分钟)检查一次是否要合并。

⚠️ 增加 TM 并发和缩短 mergeout 间隔会增加系统负载。仅当确认 ROS 容器增长迅速时使用,并在问题解决后恢复默认值。

4.2.2 确保 Apportioned Load 生效

排查并移除阻止 Apportioned Load 的因素:

  1. 去掉 COPY 中的 FILTERSearchAndReplace 等 FILTER 会阻止 Apportioned Load,导致所有数据由单节点加载
-- 不好:FILTER 阻止 Apportioned Load
COPY schema.table FROM 'webhdfs://...' FILTER SearchAndReplace(PATTERN='$$', REPLACE_WITH=E'\x11');

-- 好:去掉 FILTER,在 ETL 上游清洗数据
COPY schema.table FROM 'webhdfs://...' DIRECT;
  1. 去掉 REJECTMAXERROR TOLERANCE(如果不需要):两者都会禁用 Apportioned Load [^1]。原因是跨节点并行时难以维护准确的累计拒绝计数,Vertica 选择退化为单节点加载以确保计数准确
-- 不好:REJECTMAX / ERROR TOLERANCE 阻止 Apportioned Load
COPY schema.table FROM '/data/file.csv' REJECTMAX 100;
COPY schema.table FROM '/data/file.csv' ERROR TOLERANCE;

如果确实需要拒绝控制,可以不用这些参数先加载,再通过查询 REJECTED DATA 表后置校验。

  1. 对于 ORC/PARQUET 格式,生成多个文件:24.4 版本之后 ORC/PARQUET 可以通过将 row groups 拆分到多个源实现多线程加载,但仍建议上游生成多个文件让更多节点参与。

  2. 确保数据文件对全部节点可访问:如果文件存放在单节点的本地磁盘上,其他节点无法访问,Apportioned Load 自然无法生效。将文件放在 NFS、HDFS 或 S3 上。

4.3 根本治理(长期方案)

4.3.1 上游数据生产端优化

最根本的解决方案是在数据产出的环节减少小文件数量:

  • Hadoop/Spark ETL:调整输出参数,让每个 task 输出更大的文件。例如 spark.sql.files.maxRecordsPerFile 设大一些,或者显式 repartition() 减少输出分区数。
  • Kafka consumer:增加 batch size,减少 flush 频率。
  • 日志收集:在 Flume/Filebeat 端增加 batch size,减少写入文件系统的频率。

📝 这是一项「治本」的工作,需要协调上游数据工程团队。在根本治理到位之前,4.1 和 4.2 的方案可以临时保护 Vertica 免受小文件伤害。

4.3.2 ETL 工具批量提交大小调优

如果使用 ETL 工具(Kettle、DataX、Informatica、自研 Spark 程序等)直接写入 Vertica,工具的 batch size / commit size 设置过小也会制造大量小文件。

问题机制: ETL 工具的 batch size 控制「每处理多少条数据提交一次」。例如源端有 10 万条数据,batch size 设为 1000,则 ETL 工具会拆成 100 次提交——每次 1000 条触发一次 COPY,产生 100 批 ROS 容器。本质上等同于逐小批次加载。

源端 10万条数据
  batch_size=1000 → 100 次 COPY → 100 批 ROS 容器
  batch_size=50000 → 2 次 COPY   → 2 批 ROS 容器

常见 ETL 工具的配置项:

ETL 工具 配置参数 建议值
Kettle (PDI) Commit size(表输出步骤) 50000 ~ 100000
DataX batchSize(verticawriter) 50000 ~ 100000
Spark DataFrame batchsize(JDBC write option) 50000 ~ 100000
Informatica Commit interval(目标定义) 50000 ~ 100000

排查方法: 如果无法直接查看 ETL 工具的 batch size 配置,可以通过 query_profiles 反推。ETL 工具按固定 batch size 提交时,每次 COPY 加载的行数几乎相同——如果同一时段、对同一张表出现大量 processed_row_count 完全相同的 LOAD 记录,就是批量提交过小的明确信号。

SELECT user_name,
       query_start,
       processed_row_count,
       query_duration_us / 1000 AS query_duration_ms,
       LEFT(query, 200) AS query_snippet
FROM v_monitor.query_profiles
WHERE query_type = 'LOAD'
  AND query_start_epoch > EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - INTERVAL '1 day'))
ORDER BY query_start_epoch DESC
LIMIT 1000;

注:query_profiles.query_start 为 VARCHAR 类型,不能直接与 TIMESTAMPTZ 比较,需用 query_start_epoch(epoch 秒数)做时间过滤。

如何解读: 观察结果集中同一目标表的记录。如果出现大量 processed_row_count 相同(如都 = 1000)的 LOAD 记录,几乎可以确定 ETL 工具的 batch size / commit size 设为了该值。正常的批量加载每次处理的行数会有自然波动,固定值就是批量切分的指纹。

为什么这样配: 一次 COPY 处理更大批量,生成更少的 ROS 容器,同时减少事务开启/元数据写入的固定开销。对于 10 万条(约几十 MB)的数据,一次 COPY 完成是最优的,拆分毫无必要。

4.3.3 创建专用 ETL 资源池

为 COPY 加载创建独立的资源池,与其业务查询隔离:

-- 创建 ETL 专用池
CREATE RESOURCE POOL etl_pool
    MEMORYSIZE '4G'
    PLANNEDCONCURRENCY 4
    EXECUTIONPARALLELISM AUTO;

-- 将 ETL 用户绑定到该池
ALTER USER etl_user RESOURCE POOL etl_pool;

为什么这样配:

  • MEMORYSIZE '4G':给 COPY 足够的 sort buffer 内存,假设 PLANNEDCONCURRENCY = 4,每个 COPY 获得 4G / 4 = 1G 的 query budget
  • EXECUTIONPARALLELISM AUTO:让 Vertica 根据 CPU 核数自动决定并行线程数

4.3.4 使用 Staging 表 + 批量 MERGE 模式

对于一些无法在上游合并文件的场景,可以引入 staging 表模式:

-- 1. 小文件先加载到 staging 表(用通配符合并到单个 COPY)
COPY staging_table FROM '/data/daily_small_files/*.csv' DIRECT;

-- 2. 积累到一定量后,批量 INSERT 到目标表
INSERT INTO target_table SELECT * FROM staging_table;
COMMIT;

-- 3. 清空 staging 表
TRUNCATE TABLE staging_table;

为什么有效: Staging 表作为中间缓冲区吸收小文件。通配符 *.csv 让所有小文件合并到一次 COPY,生成一批 ROS 容器,之后通过一次大批量 INSERT ... SELECT 写入目标表。INSERT ... SELECT 产生的 ROS 容器很少,且是优化的批量写入。

适用条件:

  • 数据允许在 staging 表暂留(不需要秒级实时可见)
  • ETL 流程有批次窗口(如每小时一次 MERGE)

5. 深入案例

> 📝 虚构案例 1 · ETL 批量加载碎片化

场景: 某电商公司的日常 ETL 作业每天凌晨从上游数仓拉取数据,生成 800 个 8.5MB 左右的 CSV 文件(总计约 6.5GB),通过 COPY DIRECT 逐个加载到 3 节点 Vertica 集群的 order_fact 表中。加载耗时从最初的 30 分钟逐渐增长到 4 小时。

诊断过程:

-- 查看加载流数量和文件大小
SELECT COUNT(*) AS stream_count,
       AVG(input_file_size_bytes) / 1048576 AS avg_file_size_mb,
       SUM(input_file_size_bytes) / 1073741824 AS total_data_size_gb,
       SUM(load_duration_ms) / 1000 AS total_load_seconds
FROM v_monitor.load_streams
WHERE table_name = 'order_fact'
  AND load_start::TIMESTAMPTZ > CURRENT_TIMESTAMP - INTERVAL '1 hour';
stream_count avg_file_size_mb total_data_size_gb total_load_seconds
802 8.5 6.5 14,400

解读:802 个 stream、平均文件 8.5MB、总数据约 6.5GB、总耗时 4 小时。平均每个文件 18 秒——对于 8.5MB 的数据来说太高了。

根因分析: 800+ 个小文件使用 COPY DIRECT,每次 COPY 产生一批 ROS 容器。802 次 COPY × 每个节点 1+ ROS = 每个节点 800+ ROS。Tuple Mover 需要不停合并 800+ 个微小的 ROS 容器,导致 mergeout 积压。根本原因是每个文件独立调用一次 COPY,而非文件数本身。

修复方案:

  1. 将 800 个文件用通配符合并到一个 COPY 语句加载

效果对比:

指标 修复前 修复后
加载时间 4 小时 22 分钟
ROS 容器数(每节点) ~890 ~12
文件数/COPY 1 ~800

> 📋 真实案例 · 来源:某运营商 Vertica 数据仓库装载慢问题分析

客户背景: 某运营商省级数据仓库,93 节点 Vertica 集群,从 Hadoop 集群加载数据到 Vertica 集群。

故障现象: 2019 年 12 月 10 日,部分数据装载程序耗时从分钟级变成小时级。进一步分析发现,加载 Hadoop 上单个文件时,只有 1 个 Vertica 节点参与取数(正常应 90 个节点并行加载)。

关键诊断:

  1. 本地加载测试(排除 Vertica 写入问题):17 节点本地加载 154GB 文件,耗时 24 分钟(正常)。同样文件从 HDFS 加载耗时 105 分钟。结论:慢的不是 Vertica 写入环节。
  2. 网络测试(排除网络带宽问题):群对群 SCP 总带宽 105Gbps,主干链路峰值 40Gbps(利用率仅 13%)。结论:网络不是瓶颈。
  3. curl 对比测试:用 curl 从同一个 DataNode 取文件只需约 1 分钟,而 Vertica COPY 从同一个 DN 取相同文件需要 20 分钟到 1 小时 → 问题出在 Vertica 的 HDFS 连接层,不是数据源或网络。
  4. Vertica debug 日志分析:发现大量 No fds to selectWaited 5 ms 等待事件。最严重的 DataNode 触发了 8,081 次 wait 事件,每次 5ms,累积约 40 秒。结合其他 DN,总等待时间以分钟计。指向 I/O 多路复用 select() 函数的文件描述符限制(fd < 1024)。
  5. 文件描述符泄漏分析:Vertica 进程打开了 2,326 个文件描述符,其中 2,063 个 是 Kerberos replay cache 文件(/var/tmp/krb5_RCxxxxxx),未释放。

根因:

  • 根因 1(文件描述符不足):select() I/O 多路复用函数只能使用编号小于 1024 的文件描述符。Kerberos replay cache 泄漏导致 fd 资源耗尽,系统没有 < 1024 的 fd 可用,触发 HadoopFSShortPauseTimeout = 5ms 等待。
  • 根因 2(不能并发装载):COPY 语句中带有 FILTER SearchAndReplace,这会阻止 Apportioned Load,导致只有 1 个节点参与加载(而不是预期的 90 个节点)。

相关 Bug: VER-70261(WebHDFS select/curl_multi_fdset 在 fd > 1024 时不能正常工作)、VER-65314(Kerberos 认证时 Vertica 不释放 Kerberos cache 文件)。

修复:

  1. 设置 export KRB5RCACHETYPE=none 禁用 Kerberos replay cache,杜绝 fd 泄漏
  2. 调整 HadoopFSShortPauseTimeout = 0,消除不必要的等待
  3. 去掉 COPY 中的 FILTER SearchAndReplace,在上游 Hadoop 侧进行数据清洗
  4. 将 HDFS 输出文件拆分为 > 90 个文件,确保所有节点参与加载

与本主题的关联: 这个案例展示了 COPY 性能问题的复杂性——不是所有「慢」都是小文件导致的。排查 COPY 问题时,文件描述符泄漏、Kerberos 认证、Apportioned Load 失效都是必须优先排除的因素。如果这些基础设施层面的问题没解决,即使文件大小和数量合理,性能依然会严重退化。


6. 完整诊断流程实战

📝 虚构场景 · 完整演练

场景设定: 某保险公司需要每天凌晨 3:00 将 30 个省份的保单数据(每省 1 个 CSV 文件,约 15-30MB/文件,总计约 600MB)加载到 3 节点 Vertica 集群。ETL 脚本是用 COPY DIRECT 逐个加载 30 个文件。初期运行正常(约 8 分钟),一个月后加载时间增至 35 分钟。

时间线:

T+0min — 接到告警

运维人员收到告警:ETL 作业 SLA 超时(SLA = 20 分钟,实际 = 35 分钟)。

T+2min — 宏观检查

-- 检查 ROS 容器数
SELECT node_name, anchor_table_name,
       SUM(ros_count) AS total_ros,
       MAX(ros_count) AS max_ros
FROM v_monitor.projection_storage
WHERE anchor_table_name = 'policy_daily'
GROUP BY node_name, anchor_table_name
ORDER BY total_ros DESC;
node_name anchor_table_name total_ros max_ros
node001 policy_daily 423 423
node002 policy_daily 398 398
node003 policy_daily 441 441

判断: 每节点 400+ ROS 容器(表总行数约 1.35 亿,此处不通过 projection_storage 直接 SUM 计算) → ROS 容器数与数据量严重不匹配。小文件碎片化加载的典型信号。

T+4min — 确认加载方式

-- 检查最近的加载流
SELECT COUNT(*) AS stream_count,
       AVG(input_file_size_bytes) / 1048576 AS avg_file_size_mb,
       SUM(load_duration_ms) / 1000 AS total_seconds
FROM v_monitor.load_streams
WHERE table_name = 'policy_daily'
  AND load_start::TIMESTAMPTZ > CURRENT_TIMESTAMP - INTERVAL '1 hour';
stream_count avg_file_size_mb total_seconds
30 20.5 2,100

判断: 30 个 stream、每个约 20.5MB、总耗时 2100 秒(35 分钟)。平均每个文件 70 秒,但对于 20MB 的文件来说太慢了。确认是 30 个独立的 COPY DIRECT。

T+6min — 检查节点参与情况

-- 检查加载分布
SELECT COUNT(DISTINCT node_name) AS node_count
FROM v_monitor.load_sources
WHERE stream_name IN (
    SELECT stream_name FROM v_monitor.load_streams
    WHERE table_name = 'policy_daily'
      AND load_start::TIMESTAMPTZ > CURRENT_TIMESTAMP - INTERVAL '1 hour'
);
node_count
3

判断: 3 个节点都参与了加载,Apportioned Load 正常。排除单节点瓶颈。

T+8min — 确认根因

根因总结:

  1. 30 个独立 COPY DIRECT,每个文件 15-30MB → 每天产生 30 批 ROS 容器
  2. 运行 30 天后,累积 30 × 30 = 900 个 ROS 容器(分布在各节点)→ 接近 1024 上限
  3. Tuple Mover 需要合并 900 个碎片,mergeout 任务排队积压
  4. 每个 COPY 本身也需要扫描/检查更多的 ROS 元数据(因为碎片太多),进一步拖慢加载

T+10min — 执行修复

立即措施:

-- 用通配符合并 30 个文件到单个 COPY,各节点并发加载
COPY insurance.policy_daily FROM '/data/policy/province_*.csv' DIRECT;

短期优化(加速 TM 消化历史碎片):

-- 加速 TM 合并
ALTER RESOURCE POOL tm PLANNEDCONCURRENCY 5;
ALTER RESOURCE POOL tm MAXCONCURRENCY 5;

⚠️ TM 调优是应急手段,碎片清完后应恢复默认值。

T+30min — 效果验证

指标 修复前 修复后
COPY 次数 30 1
加载时间 35 分钟 4.5 分钟
ROS 容器数(修复后首次) ~440 ~3
Mergeout 耗时(次日) 12 分钟 < 1 分钟

修复后首次运行需等待 Tuple Mover 将历史碎片合并(约 15 分钟),次日运行时 ROS 容器已从 440 降至 3,整个链路恢复正常。


7. 快速诊断 SQL 工具箱

诊断目标 SQL 说明
ROS 容器数检查 SELECT node_name, anchor_table_name, SUM(ros_count) AS total_ros FROM v_monitor.projection_storage GROUP BY 1,2 ORDER BY 3 DESC; > 500 偏高,> 900 危险
加载流概览 SELECT stream_name, table_name, input_file_size_bytes, load_duration_ms, accepted_row_count FROM v_monitor.load_streams WHERE load_start::TIMESTAMPTZ > CURRENT_TIMESTAMP - INTERVAL '1 hour' ORDER BY load_start::TIMESTAMPTZ DESC; 查看 1 小时内所有加载流的关键指标
加载流文件大小分布 SELECT CASE WHEN input_file_size_bytes < 104857600 THEN '<100MB' WHEN input_file_size_bytes < 1073741824 THEN '100MB-1GB' ELSE '>1GB' END AS size_cat, COUNT(*) FROM v_monitor.load_streams WHERE load_start::TIMESTAMPTZ > CURRENT_TIMESTAMP - INTERVAL '24 hours' GROUP BY 1; 统计小文件占比
节点级加载分布 SELECT node_name, COUNT(*) AS sources, SUM(input_size) AS total_bytes FROM v_monitor.load_sources GROUP BY 1 ORDER BY 3 DESC; 检查加载是否倾斜到单节点
资源池配置 SELECT pool_name, query_budget_kb, planned_concurrency, max_concurrency, running_query_count FROM v_monitor.resource_pool_status WHERE pool_name NOT IN ('sysquery','tm','recovery','refresh','jvm','metadata','blobdata'); 检查 ETL 池配置和当前负载
系统资源(CPU/IO/NET) SELECT node_name, end_time, average_cpu_usage_percent, average_memory_usage_percent, net_rx_kbytes_per_second, io_written_kbytes_per_second FROM v_monitor.system_resource_usage WHERE end_time > CURRENT_TIMESTAMP - INTERVAL '1 hour' ORDER BY end_time DESC; 宏观监控 COPY 时期的资源消耗
每投影 ROS 详情 SELECT node_name, projection_name, ros_count, row_count, used_bytes FROM v_monitor.projection_storage WHERE anchor_table_name = :table_name ORDER BY ros_count DESC; 替换 :table_name 为目标表名
数据加载器事件(24.1+) SELECT data_loader_name, file_name, load_status, rows_loaded, failure_reason, file_size_bytes FROM v_monitor.data_loader_events WHERE time_stamp > CURRENT_TIMESTAMP - INTERVAL '24 hours' ORDER BY time_stamp DESC; v26 中追踪每次文件加载的成败
COPY 事件生命周期 SELECT time, event_type, event_description, rows_accepted, rows_rejected FROM v_internal.dc_load_events WHERE transaction_id = :t_id ORDER BY time; 替换 :t_id 为目标事务 ID,追踪 COPY 的完整事件流程
TM Mergeout 事件 SELECT time, schema_name, table_name, projection_name, event, plan_type, container_count FROM v_internal.dc_tuple_mover_events WHERE time > CURRENT_TIMESTAMP - INTERVAL '1 hour' ORDER BY time DESC; dc_tuple_mover_events 全版本均在 v_internal schema

8. 最佳实践清单

# 实践 为什么
1 不要逐文件独立调用 COPY 每个文件独立 COPY 产生一批 ROS 容器,碎片化成本远超数据传输收益。将多个小文件合并到同一个 COPY 语句中(通配符或逗号列表)
2 合并小文件到一个 COPY 中 同一个 COPY 语句(用逗号分隔多个文件或用通配符)只产一批 ROS 容器,500 个小文件 → 1 个 COPY → ~1 个 ROS
3 用通配符或管道替代逐文件 COPY COPY t FROM '/data/*.csv' 或用 cat files | vsql -c "COPY ... FROM STDIN",一次 COPY 只产生一批 ROS 容器
4 检查并确保 Apportioned Load 生效 FILTER、REJECTMAX、单节点可访问的数据路径都会阻止多节点并行加载。90 节点集群只有 1 节点加载 = 浪费 89 个节点
5 监控 ROS 容器数并设置告警 定期查询 projection_storage.ros_count,在接近 512 时预警,接近 900 时紧急处理。ROS Pushback 会导致 COPY 直接被拒绝
6 在 ETL 上游解决小文件问题 治本之策。Spark repartition() 减少输出分区、Kafka batch size 增大、日志收集器合并批次
7 对 ORC/PARQUET 加载生成多文件 24.4 以前不支持 Apportioned Load 和 Cooperative Parse,多文件是多节点参与的唯一方式。24.4+ 有改进但仍建议多文件
8 TM 调优是应急手段,不是常态 增加 TM 并发或缩短 mergeout 间隔是为了消化历史碎片。碎片清完后应恢复默认值,否则 TM 本身会消耗过多资源
9 建立加载 SLA 基线和趋势监控 记录每天 COPY 的耗时、文件数、文件大小分布。当耗时出现持续增长(如每周增加 5%),在 SLA 超限前就能发现碎片化累积问题

扩展阅读

[^1]: Vertica 官方文档:"REJECTMAX disables apportioned load. Using this parameter disables apportioned load." 同样 ERROR TOLERANCE 也会触发此行为。原因是跨节点并行时无法准确维护累计拒绝计数,Vertica 退化为单节点加载以确保计数准确。参见 COPY Parameters.