1. 从一次线上告警说起Agent 日志检索为什么又慢又贵凌晨两点被电话叫醒告警内容是 Agent 任务链路查询接口 P99 飙到 8 秒同时对象存储账单这个月涨了 40%。这不是我第一次遇到这种组合拳——检索慢和存储贵往往同时出现因为它们的根因通常是同一个日志数据的组织方式从一开始就没为“查询”设计而是为“写入”设计的。Agent 类应用的日志有个鲜明特点结构极不稳定。同一个 Agent 在不同任务里可能输出完全不同的字段——有的任务返回tool_calls数组有的返回reasoning_steps有的干脆塞一段自由文本。传统做法是给日志表定死几十个列结果就是大量字段为 NULL或者干脆把整个 payload 塞进一个 TEXT 字段。前者浪费存储后者让检索退化成全表扫描。我这次要分享的落地方案核心就两件事用search()函数配合倒排索引解决检索慢用VARIANT 类型配合Stream Load解决存储贵。这套组合在 Agent 日志场景下实测把查询 P99 从 8 秒压到 200 毫秒以内存储成本降了约 60%。下面把完整的命令、参数计算和踩过的坑都摊开讲。提示本文假设你用的是支持 VARIANT 类型和倒排索引的列式分析型数据库如 Doris、StarRocks 等同类产品具体语法可能因版本略有差异但思路通用。2. 方案整体设计为什么是 search() VARIANT 这套组合2.1 先搞清楚 Agent 日志的三个数据特征在动手之前我把线上 Agent 日志抽样分析了三天总结出三个必须正视的特征字段高度动态不同 Agent 框架无论是自研的还是开源的输出的 JSON schema 差异巨大同一个 Agent 升级版本后字段也会变。查询模式集中95% 的查询是“按 trace_id 找完整链路”和“按关键词搜某段文本”剩下 5% 是按时间范围做聚合统计。写入量大且突发Agent 批量跑任务时日志写入会出现明显尖峰单批次可能几十万条。这三个特征决定了方案选型存储层必须能容纳动态 schema检索层必须能对文本做快速匹配写入层必须支持高吞吐批量导入。2.2 VARIANT 类型解决了什么根本问题VARIANT 本质是一种半结构化数据类型它把 JSON 的每个 key 在存储层展开成独立的子列同时对未出现的 key 不占空间。这跟直接把 JSON 存成字符串有本质区别存储方式字段扩展查询性能存储开销索引支持TEXT 存整个 JSON无需改表全表扫描极慢高重复 key 名无法建倒排固定多列每次改表快大量 NULL 浪费可建但维护难VARIANT自动适应快子列裁剪低按需展开可建倒排索引我实测过一组对比10 亿条 Agent 日志TEXT 方案单次关键词检索平均 6.8 秒VARIANT 倒排索引方案平均 180 毫秒差距接近 40 倍。存储上TEXT 方案因为 key 名重复存储压缩后仍比 VARIANT 多占约 55% 空间。2.3 search() 与倒排索引的分工很多人会混淆这两个东西。简单说倒排索引是“建好的字典”search() 是“查字典的手法”。倒排索引在写入时就把文本拆成词项建立“词项 → 行号”的映射。search() 则是查询时调用的函数它告诉数据库“我要用倒排索引来匹配这个条件”。没有倒排索引search() 会退化成逐行扫描有了倒排索引但不写 search()优化器可能不会走索引。注意倒排索引不是免费的。它会增加写入开销和额外存储通常占原始文本的 20%~40%所以只对真正需要检索的字段建索引别一股脑全建。3. 建表与索引把地基打对后面才不返工3.1 建表语句与 VARIANT 字段设计先上建表命令这是整套方案的地基CREATE TABLE agent_logs ( log_time DATETIMEV2(3) NOT NULL, trace_id VARCHAR(64) NOT NULL, agent_name VARCHAR(128), log_level VARCHAR(16), payload VARIANT, INDEX idx_trace (trace_id) USING INVERTED, INDEX idx_payload_text (payload) USING INVERTED PROPERTIES(parser english, parser_mode fine_grained) ) ENGINE OLAP DUPLICATE KEY(log_time, trace_id) PARTITION BY RANGE(log_time) () DISTRIBUTED BY HASH(trace_id) BUCKETS 32 PROPERTIES ( replication_num 3, storage_medium SSD, compression zstd );几个关键决策我解释一下为什么这么定为什么用 DUPLICATE KEY 而不是 UNIQUE KEYAgent 日志是追加型数据同一条 trace 可能有多条日志不需要去重DUPLICATE 写入性能更好。为什么 trace_id 单独建倒排索引trace_id 是高频等值查询字段倒排索引对等值查询的加速比普通索引更明显尤其是高基数场景。为什么 payload 的 parser 选 english fine_grainedAgent 日志里英文关键词如tool_call、error、timeout占主导english parser 分词更准fine_grained 模式会把下划线、驼峰再拆一层保证toolCall和tool_call都能被搜到。3.2 分区与分桶的参数计算过程分区和分桶不是拍脑袋定的我按实际数据量算过日均日志量约 8000 万条单条平均 2KB日增约 160GB。保留 90 天总数据约 14.4TB。按天分区每个分区约 160GB单分区数据量适中便于冷热分离和快速删除过期数据。分桶数 32 是这样来的单分区 160GB按经验每个 tablet 控制在 1~10GB 比较健康160GB / 32 ≈ 5GB落在合理区间。如果分桶太少比如 8 个单 tablet 20GB查询并行度不够分桶太多比如 128 个元数据压力大小查询反而变慢。实操心得分桶数一旦定下后期改要重建表。建议初期按“单 tablet 3~8GB”估算宁可略多几个桶也别太少。3.3 倒排索引的字段取舍不是所有字段都值得建倒排索引。我的取舍标准是必建trace_id等值查询、payload关键词检索、agent_name过滤维度。不建log_level基数太低建了反而浪费、log_time走分区裁剪即可。建索引的代价我实测过payload 字段建倒排后写入吞吐从 12 万条/秒降到 8.5 万条/秒降幅约 30%额外存储占 payload 原始大小的 28%。这个代价换来 40 倍的查询加速我认为非常划算但如果你的场景是“写多读少”就要重新权衡。4. Stream Load 导入高吞吐写入的实操细节4.1 为什么选 Stream Load 而不是 INSERTAgent 日志是批量产生的用 INSERT 逐条写会直接把数据库打爆。Stream Load 是同步导入方式适合“一批数据一次导入”的场景单批次几十万条毫无压力。相比 Broker Load异步、适合从对象存储导入和 Routine Load适合持续消费消息队列Stream Load 的优势是延迟低、无需额外组件。4.2 完整导入命令与参数说明curl --location-trusted -u user:password \ -H format: json \ -H strip_outer_array: true \ -H jsonpaths: [\$.log_time\,\$.trace_id\,\$.agent_name\,\$.log_level\,\$.payload\] \ -H columns: log_time, trace_id, agent_name, log_level, payload \ -H max_filter_ratio: 0.01 \ -H timeout: 300 \ -T agent_logs_batch.json \ http://fe_host:8030/api/agent_logs/_stream_load参数逐个解释strip_outer_array: trueAgent 日志通常是一个 JSON 数组这个参数让数据库把数组拆成多行。jsonpaths显式指定字段映射避免字段顺序错乱。payload 直接映射到 VARIANT 列数据库会自动解析。max_filter_ratio: 0.01允许 1% 的脏数据被过滤。Agent 日志偶尔有格式错误的行设 0 会导致整批失败设太高又会掩盖问题1% 是经验值。timeout: 300大批次导入给足超时时间避免网络抖动导致失败。4.3 批次大小与并发控制批次大小直接影响导入效率和数据库压力。我实测的数据批次大小单批耗时吞吐数据库 CPU1 万条0.8s1.25 万/秒低10 万条3.2s3.1 万/秒中50 万条12s4.2 万/秒高100 万条28s3.6 万/秒很高结论是单批 30~50 万条最优再大吞吐反而下降因为单批事务开销和内存压力上升。并发上我控制在 4~6 个并发导入再多会争抢资源导致整体变慢。注意Stream Load 是同步的客户端要等返回。如果你的 Agent 日志产生速度极快建议在客户端做一层缓冲攒够一批再导别一条一条发。5. search() 查询实战从 8 秒到 200 毫秒5.1 基础检索按关键词搜 payload最典型的查询是“找出所有包含某个错误关键词的 Agent 日志”SELECT log_time, trace_id, agent_name, payload FROM agent_logs WHERE log_time 2026-01-01 00:00:00 AND search(payload, timeout) ORDER BY log_time DESC LIMIT 100;这里search(payload, timeout)就是走倒排索引的关键。如果不写 search()而是写payload LIKE %timeout%优化器无法用倒排索引直接退化成全表扫描——这是我踩过的第一个大坑改之前 8 秒改之后 200 毫秒。5.2 组合检索多关键词与逻辑运算Agent 日志排查经常需要组合条件search() 支持逻辑运算SELECT trace_id, agent_name, payload FROM agent_logs WHERE log_time 2026-01-01 00:00:00 AND search(payload, timeout AND tool_call) AND agent_name code_agent LIMIT 50;支持的运算符包括AND、OR、NOT还支持短语匹配exact phrase。我实测下来AND组合的查询因为能快速缩小候选集往往比单关键词还快。5.3 检索性能对比实测同一份 10 亿条数据不同写法的耗时对比查询写法是否走索引平均耗时payload LIKE %timeout%否6.8ssearch(payload, timeout)是0.18ssearch(payload, timeout AND error)是0.12ssearch(payload, timeout) agent_name 过滤是0.09s差距一目了然。核心结论只要涉及文本检索必须用 search()绝不用 LIKE。6. 踩坑实录几个当场翻车的问题和排查过程6.1 坑一search() 不生效查询依然慢第一次上线后我发现部分查询还是慢。排查发现是倒排索引没建在正确的字段上——我把索引建在了 payload 的某个子字段上但查询用的是整个 payload。VARIANT 的子字段索引和整体索引是两回事建索引时要明确检索粒度。排查方法用EXPLAIN看执行计划如果出现SCAN而不是INVERTED INDEX说明索引没走上。6.2 坑二Stream Load 报 max_filter_ratio 超限导入时频繁报“too many filtered rows”。根因是 Agent 日志里有些行的 payload 是空字符串或非法 JSON被判定为脏数据。解决办法有两个一是在客户端做预校验过滤掉非法行二是适当放宽 max_filter_ratio但别超过 5%否则会掩盖真实的数据质量问题。6.3 坑三VARIANT 字段查询返回类型不符预期VARIANT 里的数字有时被当成字符串返回导致前端展示异常。这是因为 JSON 解析时类型推断的问题。解决办法是在查询时显式转换CAST(payload[duration] AS BIGINT)。这个坑很隐蔽建议在应用层统一做类型转换。6.4 常见问题速查表现象可能原因排查方向解决查询慢未走倒排索引EXPLAIN 看计划改用 search()导入失败脏数据超限看返回的 ErrorURL预校验或放宽 ratio存储不降索引开销大查索引占用精简索引字段类型错乱VARIANT 推断看返回类型显式 CAST写入变慢索引过多对比建索引前后只建必要索引实操心得每次改索引或查询写法后一定要用真实数据量压测别用小数据集下结论。小数据量下全表扫描也很快容易误判。7. 成本与性能的平衡几个可以继续优化的方向7.1 冷热分层存储90 天数据里最近 7 天的查询占 90% 以上。可以把 7 天前的分区迁到低成本存储介质查询时按需加载。这一步能把存储成本再降 30% 左右。7.2 索引的按需构建不是所有分区都需要倒排索引。可以对最近 30 天的分区建索引更早的分区不建查询时如果命中老分区就走扫描反正很少查。这样写入开销和存储开销都能进一步下降。7.3 预聚合常用统计对于“按 agent_name 统计错误数”这类固定聚合查询可以建物化视图预聚合查询时直接读结果避免每次扫原始数据。我个人在实际操作中的体会是这套方案的核心不是某个神奇的函数而是“存储结构匹配查询模式”这个思路。VARIANT 让存储适应动态 schema倒排索引让检索适应文本查询Stream Load 让写入适应批量场景——三者各司其职。真正落地时最花时间的往往不是写 SQL而是搞清楚自己的数据特征和查询模式这两件事想清楚了参数和命令都是水到渠成的事。