资讯动态

ES迁移Quickwit实战:日志检索性能提升5倍,存储成本下降62%

发布时间:2026/9/18 9:59:37 来源:尧图企业网站定制
做日志平台做了五年多最大的体会ES好用但性能和成本都是真金白银堆出来的。去年我们做了一次搜索组件选型对比把内部日志检索和向量召回场景从 Elasticsearch 迁移到 Quickwit 之后查询 P99 从 800 多毫秒降到了 150 毫秒上下存储体量缩到原来的三分之一索引写入吞吐翻了不止一个量级。文章发出来之后不少人私信问细节这里把当时的对比过程、迁移路径和踩坑记录整理成文给正在被 ES 查询延迟和存储成本困扰的团队一个参考。这不是一篇否定 ES 的文章。ES 依然是事务型搜索场景里很可靠的选择但当数据规模上来之后你会发现很多“慢”和“贵”来自架构本身不是调几个参数就能救回来的。我先讲 ES 到底在哪些地方卡脖子再拆 Quickwit 为什么能快 5 倍最后给出可以直接照着做的迁移方案。1. ES 真正让我下决心替换的几个场景1.1 向量检索时间被段合并和内存放大拖垮去年我们给日志平台加了一个“语义搜索”入口想用向量召回把错误日志按相似度聚类。当时按标准姿势在 ES 里加了dense_vector字段索引用HNSWmapping 里指定了dims和相似度函数。demo 阶段一切正常几十万条文档怎么查都很快但等全量日志灌进去之后问题开始集中爆发。ES 的向量检索是基于 Lucene 的 HNSW 图实现的每个 segment 都要维护自己的图结构。查询时每个 segment 都得从头遍历一遍候选节点最后再做归并排序。当索引有几十个 segment 时查询延迟会被放大几十倍而段合并又非常吃 CPU 和内存合并过程中查询抖动非常明显。我们线上高峰期 P99 一度到 8 秒多那已经不是“优化一下”能解决的量级了。更麻烦的是过滤条件无法高效下推。我们很多向量查询是“先按时间范围过滤再在剩余数据里做相似度召回”ES 的处理方式是把时间过滤和向量检索分开算最后再合并结果。数据量大时这两个操作都很重内存和 CPU 双高节点 OOM 的情况一个月能遇到两三次。1.2 存储空间和写入链路都是按“最贵方案”设计的ES 的高可用模型是主分片加副本分片这在数据库场景没问题但在日志和事件流场景非常奢侈。写入 1TB 数据如果配 1 副本实际存储就是 2TB 起步如果担心热点再分几个索引、多加几个分片成本直接翻倍。我们内部算过一笔账在同等压缩配置下ES 的存储开销大约是原始日志体积的 1.5 到 2 倍含副本虽然可以用冷热分离和索引生命周期管理压低一部分但运维复杂度也跟着上去了。写入链路的问题同样卡脖子。ES 为了保证数据不丢每次写入都要写 translog这个写盘操作在高吞吐下就是瓶颈。想快就得调大refresh_interval、批量提交代价是查询实时性变差而且一旦节点宕机丢失数据的窗口会变大。很多人用异步写入 Java 客户端来缓解这个问题但异步写本质上是把“丢数据”的风险从服务端挪到了客户端内存真要挂掉一批还没提交的请求数据照样找不回来。我们在日志平台里试过异步批量写吞吐上去了但排查问题时发现缺日志的时间段恰好就是客户端重启前后的时间段这种“性能换可靠性”的账很难接受。顺便说一句网上很多“ES 存储空间优化”的教程核心思路无非是关_source、调压缩算法、分冷热节点、定期强制合并。这些我都试过每一种都能省一点但省出来的空间很快又被新增数据吃掉。问题根子不在参数而在整个索引模型愿意为通用性付出多大代价。1.3 查询语法和生态连接让人又爱又恨ES 的 Query DSL 能力确实强嵌套查询、布尔组合、聚合分析什么都能做。但用久了你会觉得它在“熵增”——一个简单的“查某个字段等于某值”的需求写成 DSL 就是三层嵌套要再加个聚合、再加个排序就没法看了。搜索热词里常年挂着“es 查询语法”“es 数据库连接工具”说明这不是我一个人的感受新同学上手 ES 的第一道坎就在这里。ES 生态里数据库连接工具、可视化面板、告警组件都挺成熟这是它的优势。但成熟生态也有代价版本兼容性是个大坑客户端、插件、服务端各有一套版本矩阵升级一次像拆炸弹。在很多只需要“写入、查询、按时间过滤、按关键词召回”的场景里ES 提供的能力明显超配我们付出的却是完整的集群运维、堆内存调优、分片规划和冷热迁移成本。2. Quickwit 凭什么快 5 倍先看它和 ES 的本质差异2.1 它的内核是 Tantivy一个 Rust 写的全文搜索引擎库Quickwit 的底层全文索引不是 Lucene而是 Tantivy。Tantivy 是纯 Rust 实现的搜索引擎库API 设计和 Lucene 有相似之处但内存模型更直接。ES 跑在 JVM 上堆内存要留、GC 要调、老年代和新生代要配节点一多光内存参数就够运维团队喝一壶。Tantivy 没有这几层包袱内存分配由 Rust 的所有权机制管理没有 GC 停顿这在长尾查询上体现得非常明显。不是说 Java 做不到高性能而是在“日志检索”这种读多写少、数据量极大、对成本敏感的场景里Rust 和 C 这一类系统级语言天然有优势。Quickwit 的官方定位是“cloud-native search engine”它从设计之初就没有走 ES 的“通用搜索数据库”路线而是把日志、追踪、向量检索这几个高频场景做到极致。2.2 列式存储和 min/max 索引让“大面积扫描”变“局部扫描”这是 Quickwit 能快 5 倍的最核心原因。ES 的倒排索引针对“匹配哪些文档”做了优化但查询时还是要打开相关 segment、加载词项字典、遍历候选文档。Quickwit 在倒排索引之上还做了一层 Parquet 列式存储每一列都维护 min/max 统计信息。查询进来时Quickwit 会先按时间字段、ID 字段这类高基数列的 min/max 把数据块切掉一大半剩下可能命中的 Split 才会真正加载。你可以理解为翻一本一千页的词典找词ES 的做法是把整本词典从头翻到尾Quickwit 是先看每页的页眉把明显不含目标词的所有页直接扔掉只精读剩下几页。日志场景里 90% 的查询都带时间范围这个优势会被无限放大。2.3 不用主分片加副本那套模型数据文件扔对象存储ES 的索引是“每个节点持有部分主分片和副本分片”查询要跨节点汇总。Quickwit 的逻辑完全不同底层索引文件以 Split 为单位存放在对象存储或本地磁盘查询节点不持有全部数据。需要查询时节点只需要拉取相关的 Split 元数据和小块数据而不是像 ES 那样把整台机器的分片都激活。这个模型带来的好处是扩容不再需要重新分片和复制数据。ES 加一个节点做集群扩容时要经历分片迁移和副本分配CPU 和网络都有明显波动Quickwit 要扩展查询能力只需要把查询节点从一个加到三个数据文件仍然共用同一份存储没有副本倍数放大成本。这也是它能做到“便宜”的原因之一同一份日志数据物理上只存一份但可以被任意多个查询节点并发读。2.4 写入模型更简单先攒批、再合并没有 translog 抄后路Quickwit 写入端支持从 Kafka、Kinesis 或本地文件连续摄取数据内部会先把数据攒成一定大小的批次再分批元数据和索引文件。这个“攒批合并”的模型天然适合日志和事件流它不追求单条延迟而是追求吞吐稳定和最终一致性。对比 ES 的写入过程单条数据进来先写 translog再进内存 bufferrefresh 之后才可见从写入到可查询之间有窗口Quickwit 是攒够一定条数或大小直接生成一个 Split分片元数据更新后即可查询。少了 translog 落盘这个串行瓶颈写入吞吐自然能拉开差距。我们测试的结果是同样数据量ES 单机写入在三万条每秒上下抖动Quickwit 稳定在三十万条每秒以上而且 CPU 占用还低不少。3. 从 ES 迁到 Quickwit 的三条实操路径3.1 路径 A用 API 兼容层先只换写入端Quickwit 提供了 ES 的兼容 API支持一部分 Elasticsearch 的索引和查询接口。最简单的试点方式不动现有查询逻辑先写一个消费程序把数据从线上日志管道投递到 Quickwit。比如你有 Java 服务往 Kafka 里写日志原来由 Logstash 或自研消费者写入 ES现在只需要改消费者目标地址。Quickwit 兼容 ES 的 bulk 写入格式投递接口大概长这样curl -XPOST http://127.0.0.1:7280/api/v1/es-bulk/ -H Content-Type: application/x-ndjson --data-binary logs.ndjson注意这里不需要像 ES 那样先建 indexQuickwit 支持动态模式推断也可以提前定义索引配置。如果只是做 POC用动态推断最快数据进去之后直接查询即可。路径 A 适合“先不拆老系统只是把新数据流切过去试试”的场景。3.2 路径 B全量导出 ES 旧数据再导入 Quickwit如果想把历史数据也迁过去最稳妥的方式是走 ES 的 scan API 导出再转成 Quickwit 的 ingest 格式。导出时建议用大 scroll 窗口批量取回文档按行写 Ndjsoncurl -s -XPOST http://es-host:9200/logs-2024/_search?scroll10m -H Content-Type: application/json -d { query: {range: {timestamp: {gte: 2024-01-01, lt: 2024-02-01}}}, sort: [_doc], size: 5000 }拿到数据后用 Quickwit CLI 直接灌入quickwit index ingest --index logs-2024 --input logs-2024.ndjson这块最大的坑在字段映射ES 里keyword类型和text类型的行为差异很大导出后如果 Quickwit 没有提前定义 schema动态推断可能把某些数字字段识别成文本导致范围查询失效。建议在正式迁移前先用一个月的数据做一次导流测试把 mapping 字段逐一对齐再跑全量任务。3.3 路径 C双写加校验平滑切换流量双写是风险最低的迁移方式。保留线上 ES 不变同时写一份数据到 Quickwit持续跑两个星期。期间要做三件事对比两边文档计数、抽检关键查询结果、监控写入延迟和存储增长。文档计数对比要按时间窗口拆开比如按小时对比可以及时发现丢数据或重复写。抽检查询要覆盖三类精确词查询、范围查询、聚合查询。纯文档计数一致不代表查询结果一致因为分词器和字段类型可能不同有些查询在两边返回结果的顺序不完全一样。双写稳定后用一个七层代理按流量比例逐步切流比如先切 10%观察查询错误率和延迟再逐步放大到 100%。整个过程里 ES 集群先不缩容等新系统稳定跑两周再下线。我给一个比较保守但实操性最强的时间表双写两周、切流一周、观察两周、下线 ES。4. 实测对比同样的数据快在哪里、快多少4.1 测试环境与数据集说明测试机配置是 24 核 CPU、128GB 内存、NVMe 磁盘单机部署。数据来自线上日志平台的一个月日志原始 JSON 体积约 860GB总文档数约 12 亿条平均单条 200 字节左右。ES 版本是 7.10.2Quickwit 版本是 1.2。两边都使用单索引ES 设置 3 主分片 1 副本Quickwit 默认配置关闭了实时写入场景模拟回溯查询。需要提前说明这不是官方基准只是我们业务场景下的真实数据。你的数据分布、字段数量、查询模式不同结果肯定有差异但方向是可靠的。4.2 索引写入吞吐对比指标ES 7.10.2Quickwit 1.2单条写入模式吞吐约 3.2 万条/s约 31 万条/s批量写入模式吞吐约 9.5 万条/s约 68 万条/s写入后 1 秒内可查是受 refresh 影响否需等待分片合并CPU 峰值占用约 92%约 71%ES 写入跑高之后translog 写盘和 refresh 线程轮询会频繁抢占 CPU整个节点响应变慢查询和写入相互影响。Quickwit 的情况是写入线程和合并线程分离查询基本不受写入影响。另外Quickwit 数据从写入到可查询之间会有秒级到十几秒的延迟这个在实时日志场景通常可以接受但如果你的业务对“写入后立即查到”有硬性要求需要留意。4.3 查询延迟对比我们构造了三组有代表性的查询。第一组是“按错误码精确查 最近 5 分钟时间过滤”第二组是“关键词模糊搜索 一小时时间范围”第三组是“按服务名聚合统计”每组都跑 1000 次记录 P50 和 P99。查询场景ES P50 / P99Quickwit P50 / P99提升倍数按 P99精确词 时间范围320ms / 860ms51ms / 142ms约 6 倍关键词 时间范围480ms / 1250ms88ms / 230ms约 5.4 倍聚合统计 时间范围1200ms / 2600ms210ms / 540ms约 4.8 倍查询延迟提升最大的是“带时间范围的精确词查询”因为 Quickwit 的 min/max 剪枝直接跳过了绝大多数据块。聚合类查询提升相对小但也远快于 ES原因是 Parquet 列式存储对只读少数列做聚合非常友好ES 聚合则需要逐文档收集数据。4.4 存储占用对比项目数值原始 JSON 日志体积860GBES 单副本落地后体积约 1.1TBES 配 1 副本后的总体积约 2.2TBQuickwit 落地后体积约 420GBQuickwit 存储节省比例对比 ES 单副本约 62%这里有个容易被忽略的点ES 的_source字段默认保存原始文档占空间非常大。Quickwit 在配置索引时可以按需决定是否保留原始 JSON如果只是做检索不需要回放原始内容存储还能进一步压缩。我们保留了原始字段因为排查日志时常需要看完整上下文。4.5 为什么“5 倍”不是一个普适结论上面数据更像是在“日志检索时间过滤”这个场景下的真实差异。换到另一类场景比如全文搜索一个产品库每条文档都很长、字段类型复杂、大量 nested 对象和 join 查询Quickwit 和 ES 的差距会明显缩小部分聚合场景甚至可能倒退。我的建议是不要拿“快 5 倍”这个数字去说服所有人而是把它当成一个选型线索如果你的业务和日志、事件流、向量召回这类 Append-Only 场景接近那么值得做一轮 POC如果你的核心是复杂的在线事务查询那 ES 依然是更稳妥的选择。5. Quickwit 不是银弹哪些场景我不建议无脑切换5.1 高频更新和逐条删除场景要想清楚Quickwit 的设计明显倾向于 Append-Only 数据模型。文档更新在实现上基本是先标记旧文档删除、再写入新文档删除不是物理立即消失而是通过 tombstone 标记异步合并。如果你业务里大量存在“高频更新某条文档状态”“按 ID 反复 upsert”的需求Quickwit 的效率会明显不如 ES。举例订单表、用户状态表这类数据一条记录可能一天改十几次。在 Quickwit 里每次更新都会生成一条新版本记录和一条删除标记后台合并时要处理大量 tombstone查询时也要跳过这些已删除标记长期跑下来索引膨胀会抵消存储优势。这类场景我建议继续用 ES 或干脆换 OLAP 数据库别硬上搜索组件。5.2 复杂嵌套对象、父子关系和深度聚合ES 的 nested 对象和join父子关系在电商和内容系统里用得很多。Quickwit 支持的对象模型偏扁平复杂嵌套查询写起来很别扭。另外ES 的时间日期直方图聚合、嵌套桶聚合这类能力在 Quickwit 里要么还没实现要么实现得比较基础。我们的经验是日志检索几乎不需要这些高级聚合但你在迁移前要先盘点自己真实用到了 ES 的哪些能力别安装完才发现某个关键聚合在 Quickwit 里不支持。建议的做法是列一个“必须保留的 ES 特性清单”逐项到 Quickwit 文档里找对应能力找不到就标记为阻塞项。我见过一个团队因为某个 edge case 聚合在 Quickwit 里行为不一致整体迁移计划延后了一个月这种事提前排查比事后补救省钱。5.3 生态成熟度Kibana 和各种连接器不能直接拿来就用如果你们团队重度依赖 Kibana 做可视化迁移前要有心理准备Quickwit 官方不提供和 Kibana 完全等价的面板能力很多运维观测点得自己搭 Grafana、自写查询面板。连接工具生态也比 ES 少JDBC 驱动、可视化 BI 工具的支持度要看具体版本不能假设“能和 ES 一样全兼容”。这不是说 Quickwit 不能用于生产而是说你要么有自研能力补齐观测和运维面板要么团队能接受用命令行和 API 处理日常任务。对成本极度敏感、想省机器的团队这反而可能是优点——少一堆 JVM 节点运维工具自己做也不复杂。6. 迁移过程中踩过的几个坑6.1 时间字段格式没统一min/max 剪枝直接失效刚开始迁移时我们发现同样一条查询在 ES 里 300ms 返回到 Quickwit 居然要 1.2 秒一度以为是性能吹过头了。后来查了日志才知道源数据里的timestamp字段有的带时区偏移、有的是yyyy-MM-dd HH:mm:ss字符串Quickwit 在推断 schema 时把它当成了普通字符串而不是时间戳。字符串列不会被 min/max 索引用在时间范围剪枝上等于把 Quickwit 最大的加速机制废掉了。解决方法是统一字段格式最好在写入前就转成 RFC3339 格式{timestamp: 2025-01-12T10:00:00Z, message: connection refused}同时在建索引配置里显式声明时间戳字段不要依赖动态推断。踩过一次之后我后来所有接入的数据源都会先过一层 schema 校验时间字段统一交给日志采集端做格式化。6.2 中文分词结果和 ES 不一样导致查询命不中ES 里最常用的中文分词是 IK 分词器停用词、扩展词、同义词都维护在 IK 的字典里。Quickwit 默认的分词器是simple和raw它不能直接加载 IK 的 Java 词典中文检索想达到同样的召回效果需要自己配置。我们内部踩了个不大不小的坑日志里大量中文异常信息ES 里搜“数据库连接超时”能命中Quickwit 默认分词后是按单字或者整词索引的结果就是搜“连接超时”时返回结果一下子少了一半。后来我们把索引配置里的 tokenizer 调成按常用中文词词典切分并保留部分 IK 自定义词库同步过来召回率才对齐。这里提醒一下迁移后做结果对比时除了比较耗时和延迟一定要抽一批真实用户查询对比召回率和准确率。性能快 5 倍但最终结果少返回几条这种偏差比查询速度问题更隐蔽。6.3 删除语义和 ES 不一样误删恢复很麻烦ES 里调用 delete API 删除某条文档后立即生效从副本读不到了。Quickwit 的删除操作是异步的底层会记录删除条件并应用到对应 Split但如果你删除的是某个时间范围内的数据新的写入又带着旧时间戳进来可能会被再次匹配到删除条件造成数据“复发”。我们的做法是删除操作尽量限定到确定性的范围字段比如按索引名称切分日期需要清理历史数据时直接删除整个索引而不是在索引内部做条件删除。另外删除操作执行完不代表物理空间立即释放后台合并 Split 需要时间监控里看到的存储量下降是滞后的别误判成删除没生效。6.4 首次查询延迟高预热之后才达到表中数字Quickwit 在冷启动时访问对象存储拉取 Split 元数据这个阶段查询延迟会明显偏高。我们在压测时也遇到过第一次查询跑了 900ms第二三次才掉到 100ms 左右。生产环境里如果节点频繁重启、索引数量又很大冷查询多体验会被拉低。缓解方法是在部署完索引后主动跑一轮“预热查询”把常用时间窗口的 Split 元数据和热数据块提前拉进本地缓存。对于日志平台每天会有固定时段的高频查询窗口可以在上班前定时跑一轮预热任务。这一点官方文档写得很简略但实际对线上体验影响很大。6.5 原本依赖 ES 的部分高级特性需要提前找替代方案我们早期没排查到的一个阻塞点是滑动窗口聚合。ES 里做一个“每分钟错误数趋势”可以用 date_histogram 很直观地完成Quickwit 在当时的版本里对这类聚合参数支持有限最后只能改由查询端拉取原始数据后自己用分组逻辑做。这类问题不会在导入数据时暴露只有上线用户开始用的时候才发现体验对不上。所以建迁移清单时除了数据量、查询延迟、存储成本这三个常规项建议把“用户真实在用的查询模式”当成最重要的评估维度。我在迁移前让团队从查询日志里拉出 Top 50 的真实用户查询逐条在 Quickwit 上做回归这一步能提前规避绝大部分兼容性坑。7. 最后再给几个落地层面的建议如果你也想做一轮快速 POC我的建议是不要一上来就搭大数据集群、搞全量迁移。先拿一台普通机器装好 Quickwit导两天的真实日志把线上最常见的二三十条查询语句跑一遍对比结果和延迟。这一步最多花三天基本就能判断值不值得继续投入。内部如果同时有多个搜索场景优先选日志检索或链路追踪来试点因为这类场景是 Append-Only、查询条件简单、时间范围过滤占比极高是 Quickwit 优势最大的地方。向量检索可以放到第二梯队先验证文本召回效果再逐步切流量。真正的在线交易搜索、订单查询这类场景我的个人看法是继续保持 ES没必要为了快而牺牲生态成熟度。还有一个小技巧迁移期间把 ES 索引的保留周期调短让旧数据慢慢过期新数据全部直接进 Quickwit用“自然淘汰”代替“全量双写”能大大减轻运维压力。我们的集群就是这样在不停服的情况下完成切换的整个过程中用户感知不到任何异常。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价