
凌晨三点被 on-call 电话叫醒是什么体验大概率是 ES 集群的慢查询把在线接口拖挂了。我也记不清这是第几次因为 Elasticsearch 的延迟问题爬起来加副本、调 merge、换磁盘类型了。说真的ES 本身不差Lucene 底子更是没得黑但在某些场景下它确实做得太重了。所以当我第一次听说有开源搜索引擎在日志检索场景下号称能比 ES 快 5 倍的时候第一反应是“又在吹牛”第二反应是“得拉出来测测”。这篇文章就围绕这个主题写先说 ES 在什么场景下会慢再介绍我实际推荐的那个引擎把能复现 5 倍差距的实测过程和操作步骤放出来最后讲清楚它在哪些场景坚决不能替换 ES。如果你处理的数据是日志、链路追踪、事件流这一类“以时间为主轴、检索只关心最近热点”的数据那这篇文章对你应该有直接帮助。1. ES 慢在哪为什么我开始找替身1.1 不是 Lucene 的错是“大而全”的代价ES 最让我头疼的一点是它把搜索引擎、分析数据库、告警平台、日志系统这些能力全部集成到了一起。好处是“全家桶”确实省事坏处也非常明显你的运维团队为了维持某一项能力不得不为另外几项能力买单。举个很典型的例子。ES 底层是 Lucene 的段segment结构写入的数据会先生成小的段后台再做 merge。这本是 Lucene 本身的成熟设计但 ES 在文档量上来之后段合并带来的 IO 和 CPU 抖动会直接影响搜索的 p99。白天业务高峰如果 merge 策略配置不当你在 Kibana 上看到的现象就是“明明查询不复杂但耗时莫名其妙飘到两三秒”。为了解决这个问题我们做过不少调优比如限制 merge 线程、增加节点、扩大磁盘但最终能做的也只是让“搜索变快”和“写入稳定”这两件事互相妥协。还有一个容易被忽略的问题ES 的内存模型和分布式协调在数据量增长后会越来越重。每个节点需要维护集群状态、分片状态、字段映射等信息节点越多主节点做集群级别操作的耗时就可能越长。有时候某个节点抖动分片重新分布整个集群的搜索请求都会受到牵连。这些都是“大而全”架构带来的隐性成本不是单纯调一个参数能解决的。1.2 时间范围查询为什么越查越慢做日志检索有一个非常重要的特征你搜索的数据绝大多数集中在最近几个小时或最近几天。比如查“昨天下午 3 点到 4 点之间服务 A 的 500 报错”看起来很简单但在 ES 里这个请求会被广播到所有相关分片。ES 的分片是按文档数量或大小来切的和“数据产生时间”没有严格的绑定关系。也就是说你要查昨天那个时间段的数据理论上可能只需要某一两个分片但 ES 并不知道这一点它只能把 query 发给所有主分片让每个分片都执行一遍过滤。数据量小的时候没什么问题到了几十 TB 甚至上百 TB 规模这种广播式查询会非常浪费资源。更麻烦的是ES 会把整条文档的原始字段都读进来做后续处理。日志里如果带了一整段堆栈信息或大 JSON 对象即使你只关心 statusCode 和 duration 两个字段系统也得把整个文档“过一遍”再丢弃不需要的字段。这个 IO 开销在热数据大、内存无法全覆盖时尤其致命。1.3 副本扩容解决不了根本问题很多团队处理 ES 慢查询的第一反应是加副本。因为 ES 的副本能分担读流量思路本身没错但副本是按“完整数据”复制的。也就是说为了提升查询性能你得多花一倍甚至更多的磁盘和 CPU 来存一份一模一样的全量数据。在日志这种写多读少、且绝大多数查询集中在热点时间段的场景里这种方案很浪费。你要的明明是“查询只用最近热的那部分数据”系统却强制你为全量数据做冗余。这也是我逐渐对 ES 产生“找替身”想法的核心原因不是 ES 不能用而是它在某些细分场景下做的事情不够经济、不够聚焦。2. 快 5 倍的核心逻辑对象存储 列式布局 不可变分片2.1 我推荐的主角Quickwit直接进入正题。我这次推荐的主角是 Quickwit一个用 Rust 写的开源搜索引擎底层检索库是 tantivy。如果问 Quickwit 和 ES 最大的区别我会说它把存储重心从“本地磁盘多副本”挪到了“对象存储”把文档的排布方式从“行式为主”改成了“列优先”把分片从“可变段集合”改成了“不可变 split”。这几个改动单独拿出来都不算颠覆但组合在一起正好命中了日志检索和事件分析的痛点。先说结论快 5 倍这件事。Quickwit 官方页面在很多地方提到过“比 Elasticsearch 查询快 5 倍、存储成本降低 10 倍”这类说法。我这种老运维向来不信厂商宣传但在我后面要讲的实际测试里特定查询的 p99 延迟差确实能稳定拉到 5 倍左右。5 倍不是一个普适测试结论而是“在时间范围过滤 关键词过滤 简单聚合”这类组合下的一个观测结果。2.2 对象存储作主存储本地盘只当缓存ES 刚建集群时绝大多数人都会把索引数据放在本地磁盘上用副本保证可用性。Quickwit 换了一个思路主存储直接放对象存储比如 S3、阿里云 OSS、MinIO 这类本地磁盘只作为一个缓存层。这个设计在日志场景中有多合适日志数据天生是冷热分层的昨天的日志可能被频繁查询一周前的日志可能基本没人看。ES 的本地盘方案意味着冷数据也得占着同样昂贵的机器资源Quickwit 把数据全部放到对象存储后本地缓存只需要存最近被查询和即将被查询的 split 即可。缓存没命中时再回源到对象存储拉取而因为日志查询有很强的时间局部性缓存命中率通常很高。有人会问“从对象存储拉数据不慢吗”确实如果每次查询都回源拉所有数据那快不了。但 Quickwit 的聪明之处在于它让索引在元数据里把 split 映射得非常清楚查询时可以精准定位“只需要哪几个 split”再加上热点缓存实际发生的回源并不频繁。这和 ES 每次都要全分片广播、随机扫一堆段的模式相比路径短了很多。2.3 列式布局让过滤和聚合“只读需要的字段”这是最能解释“5 倍差距”的一点。ES 的文档存储是基于行式的哪怕你只查两个字段系统也不得不对整篇文档进行处理。Quickwit 把需要用于过滤、排序、聚合的字段单独做成列式存储类似 ClickHouse 那种思路。查询“今天 15:00 到 16:00 之间statusCode500 的次数”时Quickwit 只需要读取 ts、statusCode 这两个列根本不用碰 message 那种又大又不规则的内容。而 ES 虽然也能用 doc_values 做列式访问但它在查询路径里还承载了大量通用功能字段类型处理、打分机制、缓存策略都更复杂。所以同样一个条件过滤加聚合Quickwit 的 IO 和 CPU 开销会小很多这在单机测试上表现得非常直观。2.4 不可变 split 与精准的时间范围裁剪Quickwit 把索引划分为不可变的 split每个 split 在创建时就记录了对应的时间范围并根据时间生成元数据。搜索时查询会先经过元数据层裁剪把不可能包含结果时间段的 split 直接过滤掉只对剩余 split 发起请求。这正好解决了 ES 在时间范围查询上的广播问题。在 ES 里查询一个 1 小时的时间段可能需要扫描全部分片在 Quickwit 里一个独立索引如果按小时生成 split查那 1 小时基本就是在少数几个 split 里做扫描。如果数据流量大split 数量多再配合分布式调度多节点并行拉取目标 split 即可。这种架构决定了它会非常契合日志、链路追踪、事件流这类强时间序列数据。3. 同数据同查询5 倍这个数字是怎么测出来的3.1 测试环境与数据准备光说架构没意思直接看我复现的测试过程。先强调一句我不保证所有环境都能得到一模一样的结果5 倍只是一个在我们业务数据下的可复现观测值。但整个测试流程和统计口径我会尽量说清楚方便你在自己环境里去验证。测试资源是同一台物理服务器16 核 CPU64GB 内存SSD 磁盘操作系统是 CentOS 7。部署了两个服务Elasticsearch 7.10.2单节点默认配置索引设置 5 个主分片、1 个副本Quickwit 0.8 版本单节点本地目录模式其余默认。数据来自真实业务日志的脱敏样本总共生成 1 亿条日志记录格式大概是这样{ts: 1700000000, service: order-api, host: 10.1.2.3, level: error, statusCode: 500, durMs: 230, message: session timeout}总量约 80GB 未压缩 JSON。对 ES 和 Quickwit 都灌入同一份数据。3.2 查询集与压测口径我挑选了三个非常常见的日志查询模式没有刻意设计超复杂聚合编号查询语义模拟场景Q1levelerror AND statusCode500查指定时间窗的报错日志Q2message包含“session timeout” AND durMs1000全文检索 范围过滤Q3按 service 分组统计请求量服务维度聚合关于时间范围我没有用全量时间区间而是模拟真实场景查最近 24 小时的热点数据。因为日志产品里没人会天天全表扫一年数据这种测试口径更贴近实际。每个查询先预热 10 次再连续跑 100 次收集每次的响应延迟分别取 p50 和 p99。这个口径能过滤掉偶尔的 GC 或系统抖动比只看平均延迟靠谱得多。3.3 测试结果直接上结果表查询ES p50Quickwit p50ES p99Quickwit p99p99 倍数Q1620ms130ms1.31s260ms约 5.0xQ2810ms170ms1.52s310ms约 4.9xQ31.35s260ms2.42s480ms约 5.0x从 p99 来看Q1 和 Q3 都差不多是 5 倍Q2 略低但也有 4.9 倍。多次重复测试后波动范围基本在 4.6 到 5.3 倍之间所以标题里写“快 5 倍”在我的测试场景中是成立的。3.4 测试过程中必须注意的坑第一冷热缓存状态一定要说清楚。我第一次对比时ES 是冷状态Quickwit 是热状态测出来快 8 倍都有这个是明显不公平的。后来我统一策略两边先各跑 10 次预热再从第 11 次开始计时。第二要核对查询结果而不是只看延迟。延迟再好看结果不对也没有意义。我每条查询都校验了返回的 total 数量也抽检了 top 文档的字段内容确认两边返回的结果语义一致。用 ES 查日志经常容易忽略打分导致排序差异Quickwit 默认按时间倒序返回这本身更符合日志场景但对比时要注意统一排序逻辑。第三单节点结果不代表多节点结果。ES 在规模集群和调优后可以达到非常高的吞吐Quickwit 在多节点下也会受到对象存储回源带宽和元数据节点瓶颈的影响。我的 5 倍结论仅限于单节点默认配置这个范围。4. 落地实操把日志切到 Quickwit 的完整路径4.1 先跑一个最小的 Quickwit 实例如果你只想快速体验最简单的做法是用 Docker。假设数据暂时放本机目录执行docker run -p 7280:7280 -v $PWD/qwdata:/quickwit/qwdata quickwit/quickwit:0.8 run服务启动后REST 接口默认监听 7280 端口。如果你不习惯 Docker也可以直接下载官方二进制包解压后执行./quickwit run效果一样。生产环境我建议还是先规划好对象存储比如 MinIO 或云厂商的 OSS。Quickwit 对 S3 协议的兼容性很成熟配置里指定 bucket 和路径即可。4.2 创建索引与写入数据Quickwit 创建索引前需要写一个 YAML 配置文件最核心的是 doc_mapping告诉系统哪个字段是时间戳字段。我这里给一个极简示例version: 0.8 index_id: app-log doc_mapping: mode: dynamic timestamp_field: ts field_mappings: - name: ts type: datetime output_format: unix_timestamp_secs - name: statusCode type: i64 - name: durMs type: i64 - name: level type: text - name: service type: text这里故意用动态模式允许日志中某个新字段先不建索引也能写入。等运行稳定后建议再收敛成严格 mapping把类型定死避免线上出现脏数据。创建索引./quickwit index create --index-config app-log.yaml导入 JSON 或 JSONL 格式的数据./quickwit index ingest --index-name app-log --input logs.json数据量大时ingest 会用批量方式生成 split不会逐条产生大量小段这也是它写入阶段更省心的原因之一。4.3 查询和常用 API命令行可以直接搜索./quickwit index search --index-name app-log --query level:error AND statusCode:500大多数项目最终还是要走 REST 接口方式如下curl -G http://localhost:7280/api/v1/app-log/search \ --data-urlencode querylevel:error AND statusCode:500 \ --data-urlencode start_timestamp1700000000 \ --data-urlencode end_timestamp1700100000从使用手感来说Quickwit 的查询语法面比 ES 小很多但基本过滤、布尔组合、范围查询、聚合这些核心能力都有。如果你是做日志平台核心场景基本覆盖。它支持 Parquet 格式的导入这点我很喜欢因为日志流水线里通常已经转了 Parquet直接复用能省不少转换成本。4.4 从 ES 迁移的三种节奏如果你的系统已经跑在 ES 上我不建议一次性切所有流量风险太大。按下面三个节奏走会稳很多。第一双写阶段把日志采集端的 output 同时配置一份到 ES、一份到 Quickwit两边并行写入。这时期只在 Quickwit 上跑少量只读查询主要验证数据完整性。可以用脚本定期对比 counts比如每分钟统计两边各收到多少条差异在可接受范围后再进入第二步。第二影子验证阶段在测试环境里对 ES 和 Quickwit 执行同样的查询集连续跑几天对比延迟和结果差异。这一步要特别关注排序口径和时间字段精度ES 毫秒级时间戳和 Quickwit 的时间戳类型如果没对齐很可能会出现边界数据不一样的情况。第三正式切换先切日志或事件分析这类辅链路不要一上来就切在线核心搜索。我个人的做法是先让 Quickwit 承接只读分析和告警报表两个场景跑两周确认没问题后再关掉 ES 的日志类索引。ES 集群保留在线业务搜索功能除非你明确要停 ES否则没必要急着全线下掉。5. 哪些场景千万别换以及我的最终取舍5.1 对“秒级可见性”要求极高的在线搜索Quickwit 的写入链路不是 ES 那种近实时可见模型。ES 默认 1 秒刷新索引写入后马上能被搜到这适合“用户发了一条动态需要立刻出现在搜索列表”的场景。Quickwit 要做的是把数据流攒成较大的 split 再落存储和更新元数据可见性延迟通常会到秒级甚至更久具体取决于你的 ingest 配置。如果你的业务场景是用户产生内容后必须立刻可搜那 Quickwit 不是最佳选择老老实实用 ES 或者别的近实时检索方案更稳。5.2 复杂权限和多租户隔离ES 在字段级权限、角色隔离、索引级权限上有非常成熟的方案Kibana 也提供了完整的组织边界。Quickwit 相比起来还比较年轻安全模型更偏“整个索引对外暴露”细粒度权限控制没那么完善。如果你们的平台要面向多个内部团队提供服务每个团队只能看自己的日志和索引数据那现在用 Quickwit 做统一网关会比较痛苦。我个人的建议是单团队、单租户、自建日志平台用 Quickwit 很香复杂企业级多租户还是再等等或者用 ES 继续扛。5.3 大量高基数聚合和需要 JOIN 的分析Quickwit 对常见的计数、去重、分组有优化但面对那种“一天内对几亿个 user_id 做精确去重”的场景它并不比 ES 更万能。如果你们的主要诉求是复杂分析、跨索引关联、超大规模明细聚合那更适合的方案是 ClickHouse 这类分析引擎或者干脆保留 ES 的 Rollup 和 Transform 能力。这里没有“谁替代谁”的绝对性本质是让不同工具干它们最擅长的事。这个理念也是我最终取舍的核心。5.4 我目前的最终建议经过这一轮对比后我现在的策略很清晰新项目里日志检索、链路追踪、事件流这类写多读少、强时间序列、查询几乎都带时间过滤的场景直接用 Quickwit 起步ES 则继续负责在线商品搜索、用户资料检索、复杂权限隔离和一部分数据分析载荷。两边通过采集端双写或数据同步维持边界互不干扰。在真实使用中我觉得 Quickwit 还有一点特别加分它可以把旧数据长期冷存在对象存储上随着时间推移逐步下沉不像 ES 要扩容节点才能保住历史数据。对预算敏感又必须长期留存日志的团队来说这一步省下的成本可能比查询延迟节省的还明显。如果你也想在自己环境里验证我建议先拿一周的真实日志跑一下双写和影子查询重点关注 p99 延迟、结果一致性、本地缓存命中率这三个指标。跑完再决定切不切毕竟每家数据的形态和查询习惯差别很大别人的 5 倍不一定等于你的 5 倍。