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_ros与projection_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 都建立了一个新的 streamload_duration_ms远大于预期:如果一个 10MB 的文件加载耗时超过 30 秒,说明加载管道中有瓶颈(可能是 IO、网络或解析)parse_complete_percent和sort_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 加载时):网络可能是瓶颈,检查是否使用了REJECTMAX或ERROR 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- 数据源路径不是所有节点都能访问
- 使用了 FILTER(如
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;
或者使用通配符:
为什么有效: 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等排序- 管道方式的吞吐量受限于
cat和vsql之间的单线程传输。对于极大数据量,优先用 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;
为什么这样设: PLANNEDCONCURRENCY 和 MAXCONCURRENCY 从默认的 3 增加到 5,让 TM 同时能处理更多投影的 mergeout。绝不要超过 6,超过后 TM 自身会成为 CPU 负担。MergeoutInterval 从 600 秒降到 300 秒,TM 每 5 分钟(而非 10 分钟)检查一次是否要合并。
⚠️ 增加 TM 并发和缩短 mergeout 间隔会增加系统负载。仅当确认 ROS 容器增长迅速时使用,并在问题解决后恢复默认值。
4.2.2 确保 Apportioned Load 生效¶
排查并移除阻止 Apportioned Load 的因素:
- 去掉 COPY 中的 FILTER:
SearchAndReplace等 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;
- 去掉
REJECTMAX和ERROR 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 表后置校验。
-
对于 ORC/PARQUET 格式,生成多个文件:24.4 版本之后 ORC/PARQUET 可以通过将 row groups 拆分到多个源实现多线程加载,但仍建议上游生成多个文件让更多节点参与。
-
确保数据文件对全部节点可访问:如果文件存放在单节点的本地磁盘上,其他节点无法访问,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 容器。本质上等同于逐小批次加载。
常见 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 budgetEXECUTIONPARALLELISM 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,而非文件数本身。
修复方案:
- 将 800 个文件用通配符合并到一个 COPY 语句加载
效果对比:
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 加载时间 | 4 小时 | 22 分钟 |
| ROS 容器数(每节点) | ~890 | ~12 |
| 文件数/COPY | 1 | ~800 |
> 📋 真实案例 · 来源:某运营商 Vertica 数据仓库装载慢问题分析¶
客户背景: 某运营商省级数据仓库,93 节点 Vertica 集群,从 Hadoop 集群加载数据到 Vertica 集群。
故障现象: 2019 年 12 月 10 日,部分数据装载程序耗时从分钟级变成小时级。进一步分析发现,加载 Hadoop 上单个文件时,只有 1 个 Vertica 节点参与取数(正常应 90 个节点并行加载)。
关键诊断:
- 本地加载测试(排除 Vertica 写入问题):17 节点本地加载 154GB 文件,耗时 24 分钟(正常)。同样文件从 HDFS 加载耗时 105 分钟。结论:慢的不是 Vertica 写入环节。
- 网络测试(排除网络带宽问题):群对群 SCP 总带宽 105Gbps,主干链路峰值 40Gbps(利用率仅 13%)。结论:网络不是瓶颈。
- curl 对比测试:用
curl从同一个 DataNode 取文件只需约 1 分钟,而 VerticaCOPY从同一个 DN 取相同文件需要 20 分钟到 1 小时 → 问题出在 Vertica 的 HDFS 连接层,不是数据源或网络。 - Vertica debug 日志分析:发现大量
No fds to select和Waited 5 ms等待事件。最严重的 DataNode 触发了 8,081 次 wait 事件,每次 5ms,累积约 40 秒。结合其他 DN,总等待时间以分钟计。指向 I/O 多路复用select()函数的文件描述符限制(fd < 1024)。 - 文件描述符泄漏分析: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 文件)。
修复:
- 设置
export KRB5RCACHETYPE=none禁用 Kerberos replay cache,杜绝 fd 泄漏 - 调整
HadoopFSShortPauseTimeout = 0,消除不必要的等待 - 去掉 COPY 中的
FILTER SearchAndReplace,在上游 Hadoop 侧进行数据清洗 - 将 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 — 确认根因¶
根因总结:
- 30 个独立
COPY DIRECT,每个文件 15-30MB → 每天产生 30 批 ROS 容器 - 运行 30 天后,累积 30 × 30 = 900 个 ROS 容器(分布在各节点)→ 接近 1024 上限
- Tuple Mover 需要合并 900 个碎片,mergeout 任务排队积压
- 每个 COPY 本身也需要扫描/检查更多的 ROS 元数据(因为碎片太多),进一步拖慢加载
T+10min — 执行修复¶
立即措施:
-- 用通配符合并 30 个文件到单个 COPY,各节点并发加载
COPY insurance.policy_daily FROM '/data/policy/province_*.csv' DIRECT;
短期优化(加速 TM 消化历史碎片):
⚠️ 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 超限前就能发现碎片化累积问题 |
扩展阅读¶
- ROS Pushback 故障排查 — ROS 容器数逼近上限时的处理
- Tuple Mover 最佳实践完全指南 — Mergeout 调优与 ROS 生命周期管理
- 在 Vertica 使用 COPY 加载数据 — COPY 命令基础与三种加载方式
- Vertica 错误日志解读与常见错误处理 — COPY 相关错误的排查思路
[^1]: Vertica 官方文档:"REJECTMAX disables apportioned load. Using this parameter disables apportioned load." 同样 ERROR TOLERANCE 也会触发此行为。原因是跨节点并行时无法准确维护累计拒绝计数,Vertica 退化为单节点加载以确保计数准确。参见 COPY Parameters.