公开笔记

CSS

覆盖基于 Elasticsearch/OpenSearch 的托管式分布式搜索服务核心能力,包含存算分离、流量控制、向量检索、数据迁移、存储与分词优化,详解读写性能调优、分片规划、集群运维及常见故障排查实战方法。

发布于 更新于

CSS 是基于 Elasticsearch、OpenSearch 且完全托管的在线分布式搜索服务,包含以下:

  • Elasticsearch
  • OpenSearch
  • 向量数据库
  • logstash 基于开源 Logstash 提供的服务器端数据处理管道服务,专注于数据采集、解析、转换与输出,是 ELK 生态中数据接入的核心组件。支持从多种数据源采集数据,经过数据处理输出至目的端。适用于日志管理、数据集成和实时数据处理场景。Logstash 工具的深入介绍请参见 《Logstash Documentation》
  • Kibana
  • OpenSearch Dashboards
  • Cerebro Cerebro 是 Elasticsearch / OpenSearch 管理的专业工具,适用于需要实时监控和高效运维的场景。CSS 服务的 Elasticsearch 和 OpenSearch 集群默认集成开箱即用的 Cerebro 工具,无需安装部署即可一键启动。Cerebro 可以快速查看集群的健康状态、节点分布、索引详情等关键信息。对于日常运维场景(如分片调整、索引管理、性能监控等),推荐选择 Cerebro,其直观的集群拓扑视图和丰富的管理功能可显著提升运维效率,同时降低操作复杂度,适用于开发、运维及数据分析等多角色用户。

RAG_1764524987547

RAG_1764525896997

ES 能力增强总览

存算分离:大幅降低存储成本

痛点: 随着时间推移,历史数据(冷数据)查询频率低,但长期占用昂贵的本地 SSD 存储,且多副本机制导致成本倍增。

解决方案:

  • 冷热数据分层: 新写入的“热数据”存放在本地 SSD 以保证速度;历史“冷数据”自动转储至低成本的对象存储(OBS)。
  • 消除副本开销: 冷数据在 OBS 中存储,无需维护多副本,大幅降低存储空间占用。
  • 智能缓存策略:
    • 首次查询直接访问 OBS。
    • 数据会被缓存至集群内存或本地磁盘。
    • 后续查询直接命中缓存,配合 OBS 的高并发 IO 能力,实现与本地 SATA 盘相当的性能。

RAG_1764527136905

流量控制:提升写入稳定性

痛点: 突发流量洪峰容易导致节点内存溢出(OOM),进而引发集群崩溃。

解决方案:

  • 多维流控: 提供基于节点级别的流控,支持黑白名单限制、HTTPS 并发连接数限制等。
  • 客户端反压(Backpressure): 基于节点内存使用情况,开启客户端写入流量反压控制。
  • 熔断保护: 当节点堆内存过高时,自动阻止大请求涌入,牺牲少量请求以保全整个节点的稳定性。

RAG_1764527414578

大查询隔离:保障读取稳定性

痛点: 少数几个资源消耗极大的“大查询”可能耗尽内存,导致整个集群卡死,影响其他正常的小查询。

解决方案:

  • 独立管理: 对高内存、长耗时的查询请求进行独立识别和管理。
  • 自动中断: 当节点堆内存使用率达到警戒线时,系统会根据策略自动中断正在运行的大查询任务。
  • 资源保护: 避免大查询阻塞全局请求,确保集群在复杂查询场景下的高可用性。

(示意图:展示了 Task 任务在内存池中的隔离与熔断逻辑)

RAG_1764527221762

聚合增强:加速统计分析性能

痛点:在大规模数据集上进行聚合分析(如排序、统计)时,计算开销大,响应慢。

解决方案:

  • 向量化执行: 利用向量化技术批量处理数据,提升处理效率。
  • 聚簇键(Cluster Key): 将具有相同键值的数据物理存储在一起,大幅减少倒排索引的查找和计算开销。
  • 排序优化: 高聚簇数据时间局部有序,大幅减少时间维度上的搜索的聚合计算开销。
  • 剪枝:控制扇出度,抑制“水桶效应”,保证在大规模分片下的搜索性能。

(示意图:展示了通过聚簇和排序优化索引结构的原理)RAG_1764527531681

读写分离:降低延迟与抖动

痛点: 写入和查询混合在同一个集群中,写入高峰会抢占资源,导致查询慢;反之亦然。

解决方案:

  • 物理隔离架构: 建立 1 个写集群 + N 个读集群。写集群专注数据摄入,读集群专注检索。
  • 文件级同步: 采用底层文件差异化同步技术,而非传统的日志重放(Replay),减少了同步对 CPU 的消耗。
  • 故障切换: 如果写集群故障,可临时切换主从,利用读集群提供写入服务,大大提升容灾能力。

(示意图:展示了写集群向多个读集群同步 ECS 文件的架构)

RAG_1764527864626

导入性能增强:提升数据摄入性能

痛点: 在日志、文本索引量大或分片数多的场景下,数据写入速度成为瓶颈。

解决方案:

  • Bulk 路由优化: 将 bulk 请求的数据 rerouting 到某一个节点,减少请求分发次数,进而降低队列排队情况。在 shard 数较多的场景下,该方案能够有效提升写入性能以及减少写入拒绝。 通常不是在 ES 配置里点个开关就能实现的,而是需要在客户端代码中实现
    1. 获取分片分布图:客户端(如 Java 客户端)通过 _cluster/state 接口获取当前索引的分片分布情况。
    2. 计算 Hash:客户端模拟 ES 的路由算法(hash(_id) % number_of_shards),算出每条数据该去哪个分片。
    3. 内存分桶:在内存里开启多个“桶”,每个分片一个桶,把数据投进去。
    4. 按桶发送:当某个桶满了,直接发往该分片所在的具体节点 IP。
  • Bulk 聚合优化: 通过批量接口优化将 Bulk 请求中的 doc 从单个依次写入变为批量写入,该方案可以有效减少内存申请、锁申请、及其他调用开销,从而提升数据导入性能。(小 Bulk -> 大 Bulk)
  • 文本索引加速:针对日志场景,优化掉词性分析、大小写转换等逻辑,构建极简日志分词器。对于文本字段(text、keyword)能够极大提升索引构建的性能。
  • Merge 优化: 提升 merge 线程数,优化 Lucene 两阶段的构建索引逻辑,降低索引 merge 任务开销对导入性能的影响,减少合并对数据导入的限流。

(示意图:列举了 Lucene 层面的多项优化技术)

RAG_1764528122571

数据迁移

迁移方式

ES API

Logstash

备份与恢复

读写分离插件

云迁移工具

  • 华为云 CDM :将存储在 Oracle 数据库或对象存储服务 OBS 中的数据导入到 Elasticsearch 集群中,数据文件支持 JSON 格式
  • DataArts Studio Migration

导入性能优化

场景:索引分片较多、文本索引量大、导入吞吐量高

Bulk 路由优化

根据 Elasticsearch 默认的路由规则,Bulk 请求中的每一条数据会被路由到不同的 shard,当索引分片较多时,会产生大量的内部转发请求,在大规模写入场景下容易触发写拒绝。同时,在大规模集群中,长尾效(大多数操作处理速度较快,但少数尤其是分布到“慢分片”或“卡住节点”上的操作导致整个 Bulk 请求的响应时间被拉长)应会导致 Bulk 请求时延较高。

通过指定配置项“index.bulk_routing”可以开启集群的 Bulk 路由优化,该优化可以减少内部转发的请求数量,在 shard 数较多的场景下,能够有效提升写入性能以及减少写入拒绝。

执行如下命令,开启 Bulk 路由优化。

PUT my_index 
{
  "settings": { 
    "index.bulk_routing": "local_pack"
  } 
}

配置项“index.bulk_routing”的取值范围如下所示。

  • “default”:缺省值,使用集群默认的路由机制,Bulk 请求中的每一条记录会拆分后独立路由。
  • “pack”:单个 Bulk 请求的数据会被随机路由到同一个 shard 中。
  • “local_pack”:单个 Bulk 请求的数据会被路由到接收该 Bulk 请求的数据节点的本地 shard 中,如果该节点不包含对应 index 的 shard,则会进行随机路由到其他包含该索引 shard 节点上。该方案依赖客户端 Bulk 请求的随机打散和主 shard 的均衡分布。

⚠️ 注意:

开启 Bulk 路由优化后(即“index.bulk_routing”设置为“pack”或“local_pack”),数据写入不再根据“_id”进行路由,与路由的相关功能使用会受限,例如根据“_id”进行文档 GET 请求可能失败。

默认路由行为:ES 将文档路由到特定分片(shard)的哈希公式为 shard = hash(_routing) % num_shards。其中,_routing 的默认值为文档的 _id。这意味着:

  • 索引时,如果不指定自定义 routing,文档基于 _id 哈希决定分片。
  • 检索时(如 GET /index/_doc/{_id}),ES 自动使用该 _id 作为 routing 值,直接定位分片,无需扫描所有分片。这确保了高效的单文档操作。

Bulk 聚合优化

通过指定配置项“index.aggr_perf_batch_size”可以开启集群的 Bulk 聚合优化。Bulk 聚合优化是通过批量导入将 Bulk 请求中的 doc 从单个依次写入变为批量写入,该方案可以有效减少内存申请、锁申请、及其他调用开销,从而提升数据导入性能。

执行如下命令,开启 Bulk 聚合优化。

PUT my_index 
{
  "settings": { 
    "index.aggr_perf_batch_size": "128"
  } 
}

配置项“index.aggr_perf_batch_size”的取值范围为 [1, Integer.MAX_VALUE]。缺省值为 1,表示关闭 Bulk 聚合优化。当取值大于 1 时,表示打开 Bulk 聚合优化且批量取值为 MIN(bulk_doc_size, aggr_perf_batch_size)。

⚠️ 注意:

普通 bulk 请求也是批量写入,Elasticsearch 服务端收到 bulk 请求后:

  1. 解析整个 payload,把所有 index/update/delete/create 操作拆成列表
  2. 按目标 shard 分组(routing 相同 + 相同 shard 的操作会放在一起)
  3. 每个 shard 批量执行这些操作,一次性写入一个 Lucene segment(而不是每条 doc 写一次 segment)
  4. 最后一次性调用 refresh(可选,由 refresh_interval 控制)使文档可见

实际上 bulk 只是“单次请求”层面的批量,而常说的 Bulk 聚合优化(也叫 Bulk Buffer / Bulk 池化) 是“跨多次请求”层面的二次批量,所以还能再提升 3~50 倍性能。它在客户端(或中间件)再加一层缓冲池,把成千上万次“小 bulk”合并成几次“超级大 bulk”。

为什么普通 bulk 还不够极致?服务端 bulk 本身已经批量了,但只要你“频繁”地调用它,它仍然会频繁触发 refresh、fsync、产生大量小 segment,这就是性能瓶颈的根源。

文本索引加速

通过指定配置项“index.native_speed_up”可以开启文本索引加速。索引加速功能通过优化索引流程以及内存使用等方式实现,对于文本字段(text、keyword)能够极大提升索引构建的性能。当开启文本索引加速时,支持通过指定配置项“index.native_analyzer”同时开启分词加速。对于需要分词的文本字段(text),当无特殊分词需求时可以开启分词加速提升分词性能。

执行如下命令,开启文本索引加速:

PUT my_index 
{
  "settings": {
    "index.native_speed_up": true,
    "index.native_analyzer": true
  }
}

⚠️ 注意:

  • 仅当开启文本索引加速(即“index.native_speed_up”设置为“true”)时,才支持开启分词加速(即“index.native_analyzer”设置为“true”),否则分词加速不生效。
  • 包含“nested”字段的索引不支持开启文本索引加速。

索引 merge 任务优化

开启以上三种数据导入性能优化后,集群的索引 merge 任务会增加,通过指定配置项“index.merge.scheduler.max_thread_count”可以降低索引 merge 任务开销对导入性能的影响。索引 merge 任务优化可以增加 shard 的合并线程数,减少合并对数据导入的限流。

PUT my_index 
{
  "settings": {
    "index.merge.scheduler.max_thread_count": 8
  }
}
// 配置项“index.merge.scheduler.max_thread_count”的取值范围是[1,node.processors/2],缺省值是4,建议设置为8。

搜索能力增强

向量检索

向量检索支持对图像、视频、语料等非结构化数据提取的特征向量数据进行最近邻或近似近邻检索。支持多种索引算法及相似度度量方式,如暴力检索、图索引(HNSW)、乘积量化、IVF-HNSW 等,并兼容多种相似度计算方式,包括欧式、内积、余弦、汉明等。

CSS 向量检索的原理基于近似最近邻(ANN) 搜索,旨在高效解决 K 近邻(KNN)问题(即找出与查询向量最相似的 K 个结果),避免计算量巨大的精确 KNN。其关键在于优化检索效率与精度:

  • 减少候选集:传统文本检索通过倒排索引过滤无关文档,而向量检索通过构建索引结构(如 HNSW 图或 IVF-PQ)快速定位潜在相关向量,避免全量遍历。例如,HNSW 索引通过多层图结构实现快速跳转,显著缩短搜索路径。
  • 降低计算复杂度:漏斗模型先对向量进行粗粒度量化(如 IVF-PQ),快速筛选候选集;再对候选集进行精粒度计算(如余弦相似度),平衡性能与精度。量化压缩是通过乘积量化(PQ)将向量编码为低维码本,减少存储和计算开销。
  • 性能与精度平衡:支持动态调整索引参数(如 HNSW 的层级数、IVF 的聚类数),在召回率与响应时间之间灵活权衡。

Elasticsearch 向量集群通过将非结构化数据转换为高维向量,并结合向量索引技术(如 HNSW 图索引、乘积量化等),可实现近似最近邻检索(ANN),在保证高召回率的同时显著降低计算复杂度。

  • 检索索引分类
大类核心原理核心优势核心劣势适用场景算法
图类索引构建近邻图(每个向量作为节点,边连接近邻向量),通过图遍历快速找邻居检索速度快、召回率高建图耗时长、高维数据适配一般大规模数据(百万 - 亿级)、追求高召回HNSW、NSG
IVF 类索引聚类分桶(将向量分成多个簇),先找目标向量所在簇,再在簇内精细检索平衡速度与精度、建索引快簇内检索仍有开销中大规模数据(十万 - 千万级)IVF-FLAT、IVF-PQ、IVF-SQ、IVF-RESIDUAL、IVF-HNSW
量化类索引对向量 “降维 / 压缩”(用少量字节表示向量),通过压缩后的编码快速匹配内存占用极低、检索极快精度有损失(可调节)超大规模数据(亿级 +)、低内存场景PQ、SQ、LSH、RQ
扁平类索引无预处理,全量遍历对比(暴力搜索)精度 100%、无建索引开销检索速度极慢小规模数据(万级以下)、追求绝对精度Flat、FAISS-Flat
混合/复合索引多技术组合(实际生产主流)兼顾召回率、速度、内存、磁盘的终极方案HNSW+PQ、IVF+HNSW

集群构建

内存规划

在创建 Elasticsearch 向量集群时,需根据数据规模、向量维度和索引类型,合理规划集群内存规格。

  • CSS 向量检索引擎依赖较高的内存计算,向量索引依赖堆外内存,建议选择“内存优化型”的节点规格。
  • 内存计算公式 根据索引算法类型、向量维度和数据量,通过公式预估堆外内存需求:
    • FLAT 索引:mem_size = dim * dim_size * num + delta
    • GRAPH 索引:mem_size = (dim * dim_size + neighbors * 4) * num + delta
    • GRAPH_PQ 索引:mem_size = (fragment_num + neighbors * 4) * num + delta
    • GRAPH_SQ8 索引:mem_size = (dim * 2 + neighbors * 4) * num + delta
    • GRAPH_SQ4 索引:mem_size = (dim + neighbors * 4) * num + delta
    • IVF_GRAPH、IVF_GRAPH_PQ 索引无需常驻内存,不涉及估算。
参数说明
mem_size向量索引所需的堆外内存大小(不含副本,当索引需要副本时,则随副本数翻倍)。
dim向量维度。
dim_size每一维度值所需的字节数,默认为 float 类型,需要 4 字节。
neighbors图索引中每个向量的邻居数,默认值为 64。
num向量总条数。
fragment_num使用 PQ 量化时的向量分段数。
如果创建索引时没有显示配置 fragment_num,则由向量维度“dim”决定。
RAG_1764585904975
delta元数据大小,该项通常可以忽略。

在选择节点规格时,还需考虑每个节点的堆内存开销。节点的堆内存分配策略:每个节点的堆内存大小为节点物理内存的一半,最大不超过 31GB。

示例:

基于 SIFT10M 数据集(128 维向量,1000 万条数据)创建 GRAPH 索引时,假设 neighbors 设置为 32,则堆外内存需求约为“mem_size = (128 x 4 + 32 x 4)x 10000000 + delta ≈ 6GB”。当索引副本数设置为 1 时,则至少需要堆外内存 6*2=12GB,此时建议集群规格选择 1 台 8U32G 或 2 台 8U16G。

配置熔断线

为避免集群节点的堆外内存过载并保障向量查询性能,系统在堆外内存使用率超过阈值时会触发写入熔断机制,暂停向量数据的写入操作。堆外内存熔断机制的目的:

  • 防止内存过载:通过限制写入,降低堆外内存的消耗。
  • 维持查询性能:避免因内存压力导致向量查询性能下降。

堆外内存熔断默认启用,支持根据业务需要调整熔断开关和熔断线,命令参考如下:

PUT _cluster/settings
{
  "persistent": {
    "native.cache.circuit_breaker.enabled": "true", // 控制是否启用堆外内存熔断功能
    "native.cache.circuit_breaker.cpu.limit": "80%" // 指定向量索引的堆外内存的使用上限(熔断线)
  }
}

索引构建

基本操作

  • 创建向量索引

创建一个名为“my_index”的索引,该索引包含一个名为“my_vector”的向量字段和一个名为“my_label”的文本字段,其中,向量字段创建了 GRAPH 图索引,并使用欧式距离作为相似度度量。

PUT my_index 
{
  "settings": {
    "index": {
      "vector": true,
      "number_of_shards": 1,
      "number_of_replicas": 1
    }
  },
  "mappings": {
    "properties": {
      "my_vector": {
        "type": "vector",
        "dimension": 2,
        "indexing": true,
        "algorithm": "GRAPH",
        "metric": "euclidean"
      },
      "my_label": {
        "type": "keyword"
      }
    }
  }
}

向量索引算法

定义了向量怎么在 ES 中构建索引,决定了寻找目标向量(被搜索向量)的过程和方式。

仅当“indexing”为“true”时生效。当选择“IVF_GRAPH”或“IVF_GRAPH_PQ”算法类型时,需要预构建与注册中心点向量。

  • FLAT:暴力计算,目标向量依次和所有向量进行距离计算,此方法计算量大,召回率 100%。适用于对召回准确率要求极高的场景。
  • GRAPH(默认值):图索引,内嵌深度优化的 HNSW 算法,主要应用在对性能和精度均有较高要求且单分片文档数量在千万量级的场景。
  • GRAPH_PQ:将 HNSW 算法与 PQ 算法进行了结合,通过 PQ 降低原始向量的存储开销,能够使 HNSW 轻松支撑上亿规模的检索场景。
  • GRAPH_SQ8:将 HNSW 算法与 SQ 量化算法进行了结合,将 float32 数值类型量化为 int8,降低原始向量的存储开销,并提升构建和查询效率,但会带来一定的召回率下降。仅 Elasticsearch 7.10.2 版本的集群支持。
  • GRAPH_SQ4:将 HNSW 算法与 SQ 量化算法进行了结合,将 float32 数值类型量化为 int4,降低原始向量的存储开销,并提升构建和查询效率,但会带来一定的召回率损失。SQ4 量化压缩率高于 SQ8,且计算效率更高,但召回率下降也会更多。仅 Elasticsearch 7.10.2 版本的集群支持。
  • IVF_GRAPH:算法将 IVF 与 HNSW 结合,对全量空间进行划分,每一个聚类中心向量代表了一个子空间,极大地提升检索效率,同时会带来微小的检索精度损失。适用于数据量在上亿以上同时对检索性能要求较高的场景。
  • IVF_GRAPH_PQ:PQ 算法与 IVF-HNSW 的结合,PQ 可以通过配置选择与 HNSW 结合和 IVF 结合,进一步提升系统的容量并降低系统开销,适用于 shard 中文档数量在十亿级别以上同时对检索性能要求较高的场景。

向量距离度量方式

定义向量之间相似度或距离的计算方法。

  • euclidean(默认值):欧式距离。
  • inner_product:内积距离。
  • cosine:余弦距离。
  • hamming:汉明距离,仅支持“dim_type”为“binary”时使用。

数据导入

在向量数据库中,向索引写入向量数据时,需通过指定向量字段名称(如 my_vector)和对应的数据格式才能完成数据结构化存储。CSS 向量数据库支持两种常见格式:浮点数组格式和 Base64 编码格式。

  • 浮点数组格式:直接传输可读的数值数组,适用于常规向量数据。
  • Base64 编码格式:将向量(小端字节序)编码为字符串,减少网络传输开销,提升高维/二值向量处理效率。 数据导入命令:
    // 浮点数组格式
    POST my_index/_doc
    {
      "my_vector": [1.0, 2.0]
    }
    
    // Base64编码格式
    POST my_index/_doc
    {
      "my_vector": "AACAPwAAAEA="
    }
    
    // Bulk批量导入
    POST my_index/_bulk
    {"index": {}}
    {"my_vector": [1.0, 2.0], "my_label": "red"}
    {"index": {}}
    {"my_vector": [2.0, 2.0], "my_label": "green"}
    {"index": {}}
    {"my_vector": [2.0, 3.0], "my_label": "red"}

离线构建

Elasticsearch 使用类 LSM-Tree 写入模型,数据持续写入和更新的过程中会生成大量小的索引段,并通过后台合并任务不断合并成大的索引段,以提供更优的查询性能。由于向量索引的构建是计算密集型的,向量数据写入过程频繁的合并任务会消耗更多的 CPU 资源。因此,在数据实时性要求不高的场景,建议设置向量字段的“lazy_indexing”参数为“true”开启索引延迟构建,当全量数据写入完成后,再调用离线构建 API 完成向量索引的最终构建。使用离线构建功能可以有效减少合并过程中向量索引的重复构建,能够端到端提升写入和索引合并的性能。

离线构建的执行流程包含两个步骤:

  1. 合并索引段。
  2. 基于最终的索引段构建向量索引。

离线构建 API 的接口格式如下:

POST _vector/indexing/{index_name}
{
  "field": "{field_name}"
}

其中,{index_name}为需要构建索引的索引名称,{field_name}为向量字段名称且该字段必须已经配置了“lazy_indexing”为“true”。

检索

标准查询

标准查询用于检索与查询向量最相似的文档。

下述查询命令将会返回所有数据中与查询向量最相似的 size(topk)条数据。

POST my_index/_search
{
  "size":2,
  "_source": false,  // 是否返回文档的源数据
  "query": {
    "vector": {
      "my_vector": {
        "vector": [1, 1],
        "topk":2
      }
    }
  }
}

复合查询

  • 前置过滤查询

先执行过滤条件检索,筛选出符合条件的结果,再对筛选结果进行向量相似度检索,以找出最相似的结果:

POST my_index/_search
{
  "size": 10,
  "query": {
    "vector": {
      "my_vector": {
        "vector": [1, 2],
        "topk": 10,
        "filter": {
          "term": { "my_label": "red" }
        }
      }
    }
  }
}
  • 布尔查询 实际上是后置过滤查询方式。过滤条件与向量相似度检索分别独立执行,执行完成后对两者的检索结果进行布尔逻辑合并,合并逻辑由 must、should、filter 等谓词决定:
POST my_index/_search
{
  "size": 10,
  "query": {
    "bool": {
      "must": {
        "vector": {
          "my_vector": {
            "vector": [1, 2],
            "topk": 10
          }
        }
      },
      "filter": {
        "term": { "my_label": "red" }
      }
    }
  }
}

ScriptScore 查询

ScriptScore 查询允许使用自定义脚本计算向量相似度。查询语法如下:

前置过滤条件可以为任意查询,script_score 仅针对前置过滤的结果进行遍历,计算向量相似度并排序返回。此种查询方式不使用向量索引算法,性能取决于前置过滤后中间结果集的大小,当前置过滤条件为 “match_all” 时,相当于全局暴力检索:

POST my_index/_search 
 { 
   "size":2, 
   "query": { 
   "script_score": { 
       "query": { 
         "match_all": {} 
       }, 
       "script": { 
         "source": "vector_score",   // 脚本名称
         "lang": "vector",     // 脚本语言类型
         "params": { 
           "field": "my_vector", 
           "vector": [1.0, 2.0], 
           "metric": "euclidean"   // 向量距离计算公式
         } 
       } 
     } 
   } 
 }

重打分查询

重打分查询用于对初始查询结果进行精排,提升召回率。

当使用 GRAPH_PQ 索引或者 IVF_GRAPH_PQ 索引时,查询结果是根据 PQ 计算的非对称距离进行排序,通过 Rescore 方式可以对查询结果进行重打分精排。

假设 my_index 是 PQ 类型的索引,重打分查询示例如下:

GET my_index/_search 
 { 
   "size": 10, 
   "query": { 
     "vector": { 
       "my_vector": { 
         "vector": [1.0, 2.0], 
         "topk": 100 
       } 
     } 
   }, 
   "rescore": {         // 精排定义
     "window_size": 100,    // 精排窗口大小
     "vector_rescore": { 
       "field": "my_vector", 
       "vector": [1.0, 2.0], 
       "metric": "euclidean" 
     } 
   } 
 }

Painless 语法扩展

Painless 语法扩展允许在自定义脚本中使用向量距离计算函数。CSS 服务扩展实现了多种向量距离计算函数,可在自定义的 Painless 脚本中直接使用,用以构建灵活的重打分公式:

POST my_index/_search
{
  "size": 10,
  "query": {
    "script_score": {
      "query": {
        "match_all": {}
      },
      "script": {
        "source": "1 / (1 + euclidean(params.vector, doc[params.field]))",
        "params": {
          "field": "my_vector",
          "vector": [1, 2]
        }
      }
    }
  }
}

嵌套字段

使用嵌套字段可以实现在单条文档中存储多条向量数据,比如在 RAG 场景中,文档数据通常需要按段落或按长度进行切分,分别进行向量化得到多条语义向量,通过嵌套字段(Nested)可以将这些向量写入同一条 ES 的文档中。对于包含多条向量数据的文档,查询时任意一条向量数据与查询向量相似便会返回该条文档。

  • 创建一个带有嵌套字段的向量索引,该索引包含一个 id 字段,类型为 keyword,包含一个 embedding 字段,类型为 nested。embedding 嵌套字段包含两个子字段 chunk 和 emb,其中 chunk 为 keyword 类型,emb 为 vector 类型
PUT my_index
{
  "settings": {
    "index.vector": true
  },
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "embedding": {
        "type": "nested",
        "properties": {
          "chunk": {
            "type": "keyword"
          },
          "emb": {
            "type": "vector",
            "dimension": 2,
            "indexing": true,
            "algorithm": "GRAPH",
            "metric": "euclidean"
          }
        }
      }
    }
  }
}
  • 使用 Bulk 操作,以数组形式写入数据,每条文档包含了 2 条向量数据。
POST my_index/_bulk
{"index":{}}
{"id": 1, "embedding": [{"chunk":1,"emb": [1, 1]}, {"chunk":2,"emb": [2, 2]}]}
{"index":{}}
{"id": 2, "embedding": [{"chunk":1,"emb": [2, 2]}, {"chunk":2,"emb": [3, 3]}]}
{"index":{}}
{"id": 3, "embedding": [{"chunk":1,"emb": [3, 3]}, {"chunk":2,"emb": [4, 4]}]}
  • Nested 字段需要使用 nested 查询,查询时需要指定 path 参数以指明要查询的嵌套路径,以及必须设置 score_mode 为 max,表示文档的得分为该文档中所有向量与查询向量相似度的最大值。
// 1.标准查询:查询与向量[1, 1]最相似的Top10文档。
GET my_index/_search
{
  "_source": {"excludes": ["embedding"]},
  "query": {
    "nested": {
      "path": "embedding",
      "score_mode": "max",
      "query": {
        "vector": {
          "embedding.emb": {
            "vector": [1, 1],
            "topk": 10
          }
        }
      }
    }
  }
}
// hit
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "Hc4Vc5QBSxCnghau22AE",
        "_score" : 1.0,
        "_source" : {
          "id" : 1
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "Hs4Vc5QBSxCnghau22AE",
        "_score" : 0.33333334,
        "_source" : {
          "id" : 2
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "H84Vc5QBSxCnghau22AE",
        "_score" : 0.11111111,
        "_source" : {
          "id" : 3
        }
      }
    ]
  }
}

// 2.前置过滤查询:先筛选出id取值为["2", "3"]的文档,再返回与查询向量[1, 1]最相似的Top10文档。
GET my_index/_search
{
  "query": {
    "nested": {
      "path": "embedding",
      "score_mode": "max",
      "query": {
        "vector": {
          "embedding.emb": {
            "vector": [1, 1],
            "topk": 10,
            "filter": {
              "terms": {"id": ["2", "3"]}
            }
          }
        }
      }
    }
  }
}
// hit
{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.33333334,
    "hits" : [
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "3t0ZypcB-Tff59gMTZO2",
        "_score" : 0.33333334,
        "_source" : {
          "id" : 2,
          "embedding" : [
            {
              "chunk" : 1,
              "emb" : [
                2,
                2
              ]
            },
            {
              "chunk" : 2,
              "emb" : [
                3,
                3
              ]
            }
          ]
        }
      },
      {
        "_index" : "my_index",
        "_type" : "_doc",
        "_id" : "390ZypcB-Tff59gMTZO2",
        "_score" : 0.11111111,
        "_source" : {
          "id" : 3,
          "embedding" : [
            {
              "chunk" : 1,
              "emb" : [
                3,
                3
              ]
            },
            {
              "chunk" : 2,
              "emb" : [
                4,
                4
              ]
            }
          ]
        }
      }
    ]
  }
}

优化集群性能

写入性能优化

向量数据写入时涉及副本同步、索引刷新(refresh)和段合并(merge)三大开销。索引实时写入时,频繁的索引刷新会生成大量小 segment,触发额外的向量索引构建和合并操作,消耗大量 CPU/IO 资源。因此,写入性能优化,主要从这几方面入手。

  • 关闭临时副本 在数据导入期间临时关闭副本,待数据导入完成后再重新开启。适用于批量导入历史数据或全量更新场景(如初始化向量数据库)。
PUT my_index/_settings
{
    "number_of_replicas": 0
}

效果:避免副本节点的实时向量索引构建开销,提升写入性能。

  • 调整刷新间隔 将索引的刷新间隔设置为 120s 或更大,以减少频繁刷新索引生成的小 segment 数量,同时降低 segment 合并带来的向量索引构建开销。也可以直接关闭自动刷新索引,即将索引的刷新间隔设置为“-1”。适用于高吞吐写入场景(如日志型向量数据流)。
PUT my_index/_settings
{
    "refresh_interval": "120s"
}

效果:减少 refresh 次数,避免生成大量小 segment,降低 segment 合并开销,提升写入性能。

  • 增加索引线程 适当增加向量索引构建的线程数可以加速索引构建过程,但是不建议设置过大,避免产生过多的构建线程抢占查询资源。适用于 CPU 资源充足但写入延迟高的场景(如 GPU 服务器环境)。
PUT _cluster/settings
{
  "persistent": {
    "native.vector.index_threads": 8
  }
}

效果:并行加速向量索引构建,提升写入并发能力。

查询性能优化

查询性能受 segment 数量、内存熔断机制及字段召回方式影响。大量 segment 会降低搜索效率;堆外内存不足时向量索引频繁换入换出;召回全字段会加重 fetch 阶段负载。因此,查询性能优化,主要从这几方面入手。

  • 强制段合并(forcemerge) 在批量导入数据完成后,执行 forcemerge 操作强制段合并可以减少 segment 数量。该操作一般在写入后、查询前执行(如定时批量导入后的检索准备)。
POST my_index/_forcemerge?max_num_segments=1

效果:将多个 segment 合并为 1,减少搜索时文件扫描开销,提升查询速度。

  • 调整 segment 大小上限 在批量写入时,系统自动合并生成的 segment 上限值是 5GB,通过提升 segment 大小上限可以减少自动合并后的 segment 数量,该操作一般在写入数据前执行。
PUT my_index/_settings
{
  "index.merge.policy.max_merged_segment": "10gb"
}

效果:有效减少 segment 数量,减少查询计算开销,提升查询速度。

  • 调整堆外内存熔断线 如果向量索引所需的堆外内存超过熔断线,查询时索引的缓存管理器会频繁进行索引的换进换出操作,导致查询变慢。通过适当调大熔断线的配置,可以避免因内存不足导致的查询频繁触发熔断(如日志提示“CircuitBreakingException”)。堆外内存熔断线的默认值是 80%,可以根据实际需求进行调整:
PUT _cluster/settings
{
  "persistent": {
    "native.cache.circuit_breaker.cpu.limit": "85%"
  }
}

效果:避免向量索引被换出,减少查询抖动。

  • 优化字段召回 如果查询结果需要返回的字段较少且均为 keyword 或数值类型字段,可以通过 docvalue_fields 配置来召回必要字段。这种方式适用于仅需返回数值/枚举类元数据(如商品 ID、分类标签)的场景,可以有效降低 fetch 阶段的开销。
POST my_index/_search
{
  "size": 2,
  "stored_fields": ["_none_"], // 不获取stored类型的字段
  "docvalue_fields": ["my_label"],
  "query": {
    "vector": {
      "my_vector": {
        "vector": [1, 1],
        "topk": 2
      }
    }
  }
}

通常文档的原始内容存在 _source 里,而 stored_fields 是单独存储的字段;docvalues 是 Elasticsearch 等系统为字段建立的列式存储结构(默认对数值、关键字等字段开启),比 _source 的行式存储更适合批量读取,性能更高。

效果:跳过 _source 解析,利用列式存储(docvalues)降低 fetch 阶段开销,提升查询性能。

缓存超时清理

当集群内存资源紧张、数据更新频繁或对数据新鲜度要求较高时,可以启用缓存超时自动清除功能,该功能可以优化系统性能、保障数据一致性和提升稳定性,适用于数据动态变化或内存资源敏感的场景。

PUT _cluster/settings
{
  "persistent": {
    "native.cache.expiry.enabled": "true",
    "native.cache.expiry.time": "30m"
  }
}

管理索引缓存

CSS 向量检索引擎基于 C++ 构建,采用堆外内存技术以提升性能和效率。为更好地管理和优化向量索引缓存,CSS 向量数据库提供了一系列专用接口,使用户能够灵活地监控和调整缓存使用情况,从而确保查询性能的稳定性和可靠性。

查看缓存统计信息

通过下面的 API 检索堆外内存的使用情况,返回当前堆外内存的使用量、缓存命中次数、加载次数等关键指标。这些数据有助于用户了解缓存的运行状态,并根据实际使用情况进行调整。在向量检索插件的实现中,每个 segment 都会构造并存储一份索引文件,查询时这些文件会被加载到堆外内存中,而插件通过缓存机制对这些内存进行有效管理。

GET /_vector/stats

// 返回结果:
{
  "_nodes" : {  		# 节点信息
    "total" : 1, 		# 总节点数
    "successful" : 1,  	# 成功返回节点数
    "failed" : 0  		# 执行失败节点数
  },
  "cluster_name" : "css-d3a7", 			    # 集群名称
  "cpu_circuit_breaker_triggered" : false, 	# 是否已触发写入熔断
  "nodes" : {
    "cAHmVUZTR9ON7t6jxcDCkg" : {  		      # 节点UUID
      "cpu_cache_capacity_reached" : false,   # 当前节点的堆外内存是否达到使用上限
      "cpu_eviction_count" : 0,  		      # 当前节点segment级别缓存换出次数
      "cpu_hit_count" : 0,  			      # 当前节点segment级别缓存命中次数
      "cpu_load_exception_count" : 0,  		  # 当前节点segment级别索引加载异常次数
      "cpu_load_success_count" : 0,  		  # 当前节点segment级别索引加载正常次数
      "cpu_miss_count" : 0,   			      # 当前节点segment级别索引缓存未命中次数
      "cpu_query_memory_usage" : 0,  		  # 当前节点堆外内存使用量,单位为KB
      "cpu_total_load_time" : 0  		# 当前节点segment级别索引加载总耗时,单位为ms
    }
  }
}

预加向量缓存

通过下面的 API 可以将指定索引的向量索引预先加载到堆外内存中。这一操作能够确保在后续查询中,这些索引可以被快速访问和使用,从而提高查询效率,减少查询延迟。

PUT /_vector/warmup/{index_name}

// 返回结果:
{
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  }
}

清除缓存

当向量索引的总大小超过缓存限制时,系统会自动执行索引项的换进换出操作。然而,这种频繁的换进换出可能会影响集群的查询性能。通过下面的 API 可以手动清除不再使用的索引缓存,有助于释放堆外内存资源,确保热数据索引的查询性能不受影响。

  • 清除全量索引缓存
PUT /_vector/clear/cache
  • 清除指定索引缓存
PUT /_vector/clear/cache/{index_name}

存算分离

Elasticsearch 存算分离将冷热数据分离存储:高频访问的热数据存储于高性能存储介质,低频访问的冷数据迁移至低成本存储介质(对象存储服务 OBS)。该方案既能保障实时查询性能,又能大幅降低长期存储成本。

存算分离和切换冷热数据比,更适用于对搜索性能要求不高的场景,冷数据存储在 OBS 中,存储成本更低一些。

冻结索引

RAG_1764611221264

Elasticsearch 存算分离的核心业务流程:

  1. 数据写入 热数据直接写入本地高性能磁盘(SSD),确保实时查询性能。
  2. 冷热转换 低频数据从 SSD 迁移至 OBS(该 OBS 桶对用户不可见),从热数据转为冷数据。可通过调用冻结索引接口或生命周期策略(如数据超过 90 天)自动触发。冷数据索引的元数据和日期字段数据保留在本地磁盘,用于快速定位 OBS 中的冷数据。
  3. 查询处理
    • 热数据查询可直接从本地磁盘读取,系统响应延迟为毫秒级。
    • 冷数据查询通过保留的元数据快速定位 OBS 中的冷数据,按需加载数据后返回结果。

CSS 服务通过生命周期管理实现索引数据不同状态之间的转换:

  • 热索引(open):支持数据写入和毫秒级检索。
  • 冻结索引(freeze):将低频访问的索引数据转储至 OBS,索引变为只读,检索速率降低到秒/分级别。
  • 删除索引:冻结索引支持定期删除,释放存储资源。

配置 OBS 中缓存优化

冻结索引后,索引数据转储到 OBS,为了尽可能地减少对 OBS 的访问请求,提升集群的查询性能,系统会缓存部分数据。第一次查询冷数据时,集群会直接访问 OBS,获取到的数据会被缓存在集群内存中,后续查询时会先检查是否有缓存数据。CSS 服务支持查询和重置存储在 OBS 桶中的冻结索引的缓存状态,以及修改缓存配置。

提升冻结索引的查询性能

在 Kibana 的 Discover 页面首次查询冻结索引时,由于此时无任何缓存,导致所有数据均需要从 OBS 上获取,当命中的数据较多时,需要耗费大量的时间从 OBS 上获取对应的时间字段以及文件元数据。如果将这一部分数据直接缓存在本地,即可大幅提升查询性能,解决 Discover 页面首次查询慢的问题。CSS 服务就是通过冷数据的本地缓存,实现冻结索引的查询性能提升。本地缓存配置是预置的,用户可以基于业务场景修改配置,也可以查询、了解本地缓存信息。

流量控制 2.0

流量控制策略通过限制客户端访问、反压写入流量、统计分析流量行为,实现集群资源的合理分配和风险防控,保障集群稳定性并防止异常流量冲击。

在 Elasticsearch 集群中,突发流量、恶意访问或资源竞争可能导致节点过载甚至崩溃。流量控制策略通过限制客户端访问、反压写入流量、统计分析流量行为,实现集群资源的合理分配和风险防控,适用于以下场景:

  • 高并发写入场景:避免大请求涌入导致节点内存溢出。
  • 安全防护场景:通过黑白名单限制非法 IP 访问。
  • 流量异常响应场景:一键断流快速应对突发流量冲击。
  • 性能调优场景:通过统计分析优化流控配置阈值

原理策略:

  • 开启 HTTP/HTTPS 节点流控
  • 开启内存流控
  • 开启请求采样统计
  • 开启一键断流
  • 查看流量控制信息
  • 开启并查看访问日志
  • 开启访问日志记录到文

流量控制 1.0

流量控制 1.0 提供节点级别的流量控制功能,可提供单个节点基于黑白名单的访问限制、HTTP 并发连接数限制、HTTP 最大连接数限制、基于请求 Path 的堆内存最大使用量流控能力、基于 CPU 最大占用率流控能力,一键断流能力,同时也提供节点访问 IP 统计和 URL 的采样统计能力。开启流控功能会使请求在入口处直接阻塞,可以缓解节点高并发场景下的集群压力,降低 P99 时延,减少节点不可用的风险。

大查询隔离

大查询隔离支持对查询请求进行独立管理,将高内存、长耗时的查询请求进行隔离,保证节点内存安全。在节点堆内存使用率过高时,触发中断控制程序,根据选择的中断策略将其中一条大查询请求进行中断,取消其正在运行的查询任务。大查询隔离同时支持全局查询超时配置,用户可实时配置所有查询请求的超时时间,中断超时查询请求。其中,中断能力采用的是 Elasticsearch 原生 cancel 接口。

大查询隔离可以有效解决以下问题,提升集群的搜索能力。

  • 部分查询占用了很高的节点堆内存,导致机器频繁 Garbage Collect,甚至引发 OOM 异常。
  • 频繁 Garbage Collect 导致节点脱离,查询迟迟无法响应甚至失败。
  • 查询量过大导致 CPU 爆满,线上业务受到影响。

聚合增强

CSS 服务的 Elasticsearch 集群聚合增强能力通过向量化技术和数据聚簇优化,显著提升大规模数据的聚合分析性能,帮助用户在复杂数据场景中实现高效分析和提升业务决策效率。

其核心原理是通过合理设置排序键(Sort Key)和聚簇键(Clustering Key)对数据进行预排序和物理聚簇,从而减少聚合过程中的数据扫描和计算开销。

  • 排序键:数据按指定字段(如时间戳)顺序存储,确保相同值或相近值的文档在磁盘上连续分布。
  • 聚簇键:作为排序键的前缀子集(如各城市订单数),进一步将数据聚簇,使聚合操作可批量处理连续数据块,而非逐条遍历。
场景描述
低基字段聚合字段值种类较少,适合分组聚合。例如,统计各城市订单数量时,数据聚簇后可快速定位并批量处理。
高基字段聚合字段值种类繁多,适合直方图聚合。例如,按小时统计访问量时,聚簇键可加速区间数据的聚合。
低基和高基字段混合聚合先对低基字段分组(如各城市订单),再对高基字段直方图聚合(如时间),通过多级聚簇提升混合查询效率。

读写分离

随着业务规模扩大,数据量和访问量呈指数级增长,单一集群同时处理数据写入和查询请求的“读写一体”架构逐渐暴露出资源争抢、集群负载过载等问题。为了解决这些问题,CSS 服务提供了 Elasticsearch 读写分离功能。

Elasticsearch 读写分离架构通过主集群和从集群的协作实现。具备如下特点与优势:

  • 解耦读写压力:主集群专注写入,提升写入效率;从集群专注查询。支持高并发查询扩展。消除资源争抢,降低峰值负载。
  • 弹性扩展能力:独立横向扩展,读/写集群可单独扩容。支持跨地域部署能力。
  • 数据一致性保障:低时延的实时同步机制。支持仅同步增量数据。

RAG_1764614938438

切换冷热数据

冷热数据切换是指通过将数据按使用频率分配到不同性能节点的策略,以优化存储成本和查询性能。使用高性能硬件(如 SSD)存储高频访问的实时数据(热数据),使用低成本硬件(如 HDD)存储低频访问的历史数据(冷数据)。冷热数据切换可以减低存储成本,提升搜索效率。

切换冷热数据和存算分离比,更适用于对搜索性能要求高的场景,冷数据存储在集群本地的冷数据节点中,存储的数据量大小依赖冷数据节点数和磁盘容量,存储成本也会比 OBS 高一些。

索引回收站

索引回收站支持将删除的索引存放到回收站中,且支持从回收站中还原索引,防止误操作导致数据被删除,进而提升集群的数据可靠性。

存储优化

冷热数据

在 Elasticsearch 集群中,切换冷热数据是一种通过将数据按使用频率分配到不同性能节点的策略,以优化存储成本和查询性能。

  • 热数据:存储高频访问的实时数据,使用高性能硬件(如 SSD),确保快速读写和检索。
  • 冷数据:存储低频访问的历史数据,使用低成本硬件(如 HDD),降低存储成本。

切换冷热数据适用于日志分析、监控数据归档等场景,通过动态分配数据节点,平衡性能与成本。

集群存算分离

CSS 支持存算分离,即将索引冻结到 OBS 来降低冷数据的存储成本,同时可以使用索引生命周期管理,在特定的时间自动冻结索引,实现存算分离。

词库

分词器

CSS 服务使用的分词器包括 IK 分词器和同义词分词器。IK 分词器配备主词词库和停词词库;同义词分词器配备同义词词库。其中,IK 分词器包含 ik_max_word 和 ik_smart 分词策略。同义词分词器使用的是 ik_synonym 分词策略。

  • ik_max_word:会将文本做最细粒度的拆分,比如会将“昨夜西风吹折千林梢”拆分为“昨夜西风,昨夜,西风,吹折千林梢,吹折,千林梢,千,林,折千林,千林,吹”,会穷尽各种可能的分词组合。
  • ik_smart:会做最粗粒度的拆分,比如会将“昨夜西风吹折千林梢”拆分为“昨夜西风,吹折千林梢”。

CSS 服务给集群预置了静态主词词库、静态停词词库、Extra 主词词库和 Extra 停词词库这四个词库。

  • 当这些预置词库已满足集群业务的分词需求时,则集群无需配置自定义词库即可直接实现关键词搜索。
  • 当预置词库不满足集群业务分词需求时,可以给集群添加主词词库、停词词库或同义词词库,亦或者是修改预置的四个词库,使集群能够实现关键词或同义词搜索。

查看预置词库:

analysis-ik/config at master · infinilabs/analysis-ik

词库热更新

最佳实践

CSS最佳实践汇总

优化集群性能

写入性能优化

写入流程

图中的 P 表示主分片 Primary,R 表示副本分片 Replica,主副分片在数据节点 Node 里是随机分配的,但是不能在同一个节点里。

RAG_1764668489561

  1. 客户端向 Node1 发送写数据请求,此时 Node1 为协调节点。
  2. 节点 Node1 根据数据的 _id 将数据路由到分片 2,此时请求会被转发到 Node3,并执行写操作。
  3. 当主分片写入成功后,它将请求转发到 Node2 的副本分片上。当副本写入成功后,Node3 将向协调节点报告写入成功,协调节点向客户端报告写入成功。

Elasticsearch 中的单个索引由一个或多个分片 (shard) 组成,每个分片包含多个段(Segment),每一个 Segment 都是一个倒排索引:

RAG_1764668529498

将文档插入 Elasticsearch 时,文档首先会被写入缓冲区 Buffer 中,同时写入日志 Translog 中,然后在刷新时定期从该缓冲区刷新文档到 Segment 中。刷新频率由 refresh_interval 参数控制,默认 1 秒刷新一次。更多写入性能相关的介绍请参见 Elasticsearch 的官方介绍 Near Real-Time Search

RAG_1764668724114

优化方案

  1. 使用 SSD 盘或升级集群配置:

使用 SSD 盘可以大幅提升数据写入与 merge 操作的速度,对应到 CSS 服务,建议选择“超高 IO 型”存储,或者超高 IO 型主机。

  1. 采用 Bulk API

客户端采用批量数据的写入方式,每次批量写入的数据建议在 1~10MB 之间。

  1. 随机生成 _id

如果采用指定 _id 的写入方式,数据写入时会先触发一次查询操作,进而影响数据写入性能。对于不需要通过 _id 检索数据的场景,建议使用随机生成的 _id。

  1. 设置合适的分片数

分片数建议设置为集群数据节点的倍数,且分片的大小控制在 50GB 以内。

分片数是节点数的倍数,本质上是为了消除“木桶效应”。只有让每个节点承载的分片数量完全一致,集群的整体吞吐量才能达到物理上限。

  1. 关闭副本

数据写入与查询错峰执行,在数据写入时关闭数据副本,待数据写入完成后再开启副本:

PUT {index}/_settings
{
  "number_of_replicas": 0
}
  1. 调整索引的刷新频率

数据批量写入时,可以将索引的刷新频率“refresh_interval”设置为更大的值或者设置为“-1”(表示不刷新),通过减少分片刷新次数提高写入性能。

PUT {index}/_settings
{
  "refresh_interval": "15s"
}
  1. 优化写入线程数与写入队列大小

为应对突发流量,可以适当地提升写入线程数与写入队列的大小,防止突发流量导致出现错误状态码为 429 的情况。

Elasticsearch 7.x 版本中,可以修改如下自定义参数实现写入优化:thread_pool.write.size,thread_pool.write.queue_size。

  1. 设置合适的字段类型

指定索引中各字段的类型,防止 Elasticsearch 默认将字段猜测为 keyword 和 text 的组合类型,增加不必要的数据量。其中 keyword 用于关键词搜索,text 用于全文搜索。对于不需要索引的字段,建议“index”设置为“false”。

Elasticsearch 7.x 版本中,将字段“field1”设置为不建构索引的命令如下:

PUT {index}
{
  "mappings": {
    "properties": {
      "field1":{
        "type": "text",
        "index": false
      }
    }
  }
}
  1. 优化 shard 均衡策略

Elasticsearch 默认采用基于磁盘容量大小的 Load balance 策略,在多节点场景下,尤其是在新扩容的节点上,可能出现 shard 在各节点上分配不均的问题。为避免这类问题,可以通过设置索引级别的参数“routing.allocation.total_shards_per_node”控制索引分片在各节点的分布情况。此参数可以在索引模板中配置,也可以修改已有索引的 setting 生效。修改已有索引的 setting 的命令如下:

PUT {index}/_settings
{
	"index": {
		"routing.allocation.total_shards_per_node": 2
	}
}

查询性能优化

查询流程

RAG_1764687836349

图中的 P 表示主分片 Primary,R 表示副本分片 Replica,主副分片在数据节点 Node 里是随机分配的,但是不能在同一个节点里。

  1. 客户端向 Node1 发送查询请求,此时 Node1 为协调节点。
  2. 节点 Node1 根据查询请求的索引以及其分片分布,进行分片选择;然后将请求转发到 Node1、Node2、Node3。
  3. 各分片分别执行查询任务;当各分片查询成功后,将查询结果汇聚到 Node1,然后协调节点向客户端返回查询结果。

对于某个查询请求,其在节点上默认可并行查询 5 个分片,多于 5 个分片时将分批进行查询;在单个分片内,通过逐个遍历各个 Segment 的方式进行查询。

RAG_1764687918912

优化方案

  1. 通过 routing 减少检索扫描的分片数

在数据入库时指定 routing 值,将数据路由到某个特定的分片,查询时通过该 routing 值将请求转发到某个特定的分片,而不是相关索引的所有分片,进而提升集群整体的吞吐能力:

PUT /{index}/_doc/1?routing=user1
{
  "title": "This is a document"
}

// 根据routing值去查询数据
GET /{index}/_doc/1?routing=user1
  1. 采用 index sorting 减少检索扫描的 Segments 数

当请求落到某个分片时,会逐个遍历其 Segments,通过使用 index sorting,可以使得范围查询、或者排序查询在段内提前终止 (early-terminate)。

在默认情况下,ES 的文档是按进入系统的先后顺序存储在 Lucene 段(Segments)中的。 开启 Index Sorting 后,当 ES 将内存中的数据刷写到磁盘(Flush)或者进行段合并(Merge)时,Lucene 会强制对段内的文档进行物理排序。

//假设需要频繁使用字段date做范围查询。
PUT {index}
{
  "settings": {
    "index": {
      "sort.field": "date", 
      "sort.order": "desc"  
    }
  },
  "mappings": {
    "properties": {
      "date": {
        "type": "date"
      }
    }
  }
}
  1. 增加 query cache 提升缓存命中的概率

当 filter 请求在段内执行时,会通过 bitset 保留其刷选结果,当下一个类似的查询过来时,就可以复用之前查询的结果,以此减少重复查询。

增加 query cache 可以通过修改集群的参数配置实现,将自定义缓存参数“indices.queries.cache.size”设置为更大的值,修改参数配置后一定要重启集群使参数生效。

  1. 提前 Forcemerge,减小需要扫描的 Segments 数

对于定期滚动后的只读索引,可以定期执行 forcemerge,将小的 Segments 合并为大的 Segments,并将标记为“deleted”状态的索引彻底删除,提升查询效率。

//假设配置索引forcemerge后segments数量为10个。
POST /{index}/_forcemerge?max_num_segments=10

使用 Elasticsearch Pipeline 实现数据增量迁移

通过 Elasticsearch Pipeline 的自动字段更新能力,解决了增量迁移中增量字段识别困难的问题,实现了可靠的增量数据迁移方案。

当使用 Logstash、ESM 等工具进行 Elasticsearch 集群数据的增量迁移时,如果遇到以下问题,可采用本方案:

  • 索引未配置增量时间字段,无法识别增量数据。
  • 业务逻辑复杂,无法可靠识别增量字段。
  • 不同索引的增量字段不一致,需统一增量方式。
  • 数据更新未同步更新增量字段。

本方案通过 Elasticsearch Pipeline 功能,在数据写入时自动为文档添加增量时间字段,实现灵活的数据增量迁移:

  1. 在源 Elasticsearch 集群中配置 Pipeline,自动添加增量时间字段。
  2. 配置索引关联 Pipeline,实现数据写入时自动添加增量时间字段。
  3. 通过迁移工具实现增量数据迁移。

优势:

  • 简化操作:无需分析索引的增量字段,通过 Pipeline 统一生成增量字段,降低配置复杂度。
  • 灵活性:适用于不同索引和业务场景,无需修改索引结构即可实现增量迁移。
  • 兼容性:可与 Logstash、ESM、Reindex 等多种迁移工具结合使用。

使用前提:

  • 打通源 Elasticsearch 集群和目标 Elasticsearch 集群网络
  • 确认集群的索引已开启“_source”:集群索引的“_source”默认是开启的。执行命令 GET {index}/_search,当返回的索引信息里有“\source”信息时表示已开启。

操作步骤:

  1. 在源端集群中为索引添加增量时间字段:
PUT /{index_name}/_mapping
{
  "properties": {
    "@migrate_update_time": {
      "type": "date"
    }
  }
}
  1. 创建 Pipeline,自动添加增量时间字段:
PUT _ingest/pipeline/migrate_update_time
{
  "description": "Adds update_time timestamp to documents",
  "processors": [
    {
      "set": {
        "field": "_source.@migrate_update_time",
        "value": "{{_ingest.timestamp}}"
      }
    }
  ]
}

processors 配置会读取当前机器的时间写入到索引的增量时间字段(如@migrate_update_time)中

  1. 配置索引关联 Pipeline,在数据写入时自动添加增量时间字段:
PUT {index_name}/_settings
{
  "index.default_pipeline": "migrate_update_time"
}

配置索引的默认 Pipeline,在索引数据增加和更新时,都会经过 Pipeline 更新增量时间字段@migrate_update_time。

  1. 查询增量数据,通过查询增量时间字段@migrate_update_time 获取增量数据:
GET /{index_name}/_search
{
  "query": {
    "range": {
      "@migrate_update_time": {
        "gte": "2025-01-01T00:00:00"
      }
    }
  }
}
  1. 使用迁移工具进行增量迁移:

以 ESM 为例,执行增量迁移命令:

./migrator-linux-amd6 -s http://source:9200 -d http://dest:9200 -x {index_name} -m admin:password -n admin:password -w 5 -b 10 -q "@migrate_update_time:[\"2025-04-08T00:00:00\" TO \"2030-01-01T 00:00:00\"]"
  1. 检查数据一致性:

数据迁移完成后,分别在源集群和目标集群的 Kibana 执行命令 GET {index_name}/_count,对比两者的索引信息是否一致。

  1. 迁移过程中出现 Pipeline 不存在错误

原因:源 Elasticsearch 集群创建了 Pipeline,迁移索引结构的时候会把索引的 Pipeline 也一并迁移到目标 Elasticsearch。但是目标端集群并没有创建 Pipeline,导致出现该报错。

解决方案:需要在目标 Elasticsearch 集群取消索引的 Pipeline。登录目标 Elasticsearch 集群的 Kibana,进入“Dev Tools”执行如下命令:

PUT {index_name}/_settings
{
    "index.default_pipeline":  null
}

向量检索性能测试

https://support.huaweicloud.com/bestpractice-css/css_07_0050.html

使用 IVF_GRAPH_PQ 算法实现向量检索

https://support.huaweicloud.com/bestpractice-css/css_07_0067.html

使用 Elasticsearch 和 Logstash 构建日志管理平台

https://support.huaweicloud.com/bestpractice-css/css_07_0025.html

使用 Elasticsearch 自定义规则排序搜索结果

https://support.huaweicloud.com/bestpractice-css/css_07_0033.html

Kibana 实现数据可视化

通过Kibana Discover实现数据可视化时序展示

常见问题

如何规划集群索引的分片数?

在使用集群的过程时,特别是在进行数据导入操作之前,建议根据具体的业务需求,提前对集群的数据结构和分布进行规划。这包括合理设计索引和确定分片数量。为了确保集群在性能和可扩展性方面达到最佳状态,以下是一些规划建议:

  • 单个分片大小:建议将每个分片的大小控制在 10GB 到 50GB 之间。这有助于在存储效率和查询性能之间取得平衡。
  • 内存与分片比例:在资源分配上,建议每 1GB 的内存空间放置 20 到 30 个分片。这样可以确保每个分片都有足够的内存资源进行索引和查询操作。
  • 单节点分片数:为了避免单点过载,建议每个节点上的分片数量不超过 1000 个。这有助于避免节点资源竞争,确保节点的稳定运行。
  • 索引分片与节点数的关系:对于单个索引,建议其分片数与集群的数据节点数和冷数据节点数之和保持整数倍关系。这有助于实现负载均衡,优化查询和索引的性能。
  • 集群总分片数量:为了管理方便和避免过度扩展,建议将集群的总分片数量控制在 3 万以内。这有助于保持集群的稳定性和响应速度。

集群平均已用内存比例达到 98% 怎么办?

  • 问题现象

查看集群监控发现,Elasticsearch 集群“平均已用内存比例”一直处于 98%,用户担心内存比例过高是否对集群有影响。

  • 问题原因

在 Elasticsearch 集群中,Elasticsearch 会占用 50% 内存,另外 50% 内存会被 Lucene 用于缓存文件,因此节点内存占用会一直很高,平均已用内存比例达到 98% 是正常现象,请您放心使用。

  • 解决方案

您可以关注“最大 JVM 堆使用率”和“平均 JVM 堆使用率”这两个指标来监控集群内存使用情况。

为什么新创建的索引分片集中分配到单节点上

  1. 原因分析:

新建索引分片被集中分配于一个 node 节点上可能有以下原因:

  • 之前索引的分配导致某个节点上的 shards 数量过少,新建索引 shards 分配被 balance.shard 参数主导,为了平衡所有索引的全部分片,将 shards 集中分配在数量过少的节点上。
  • 节点扩容,当新节点加入时新节点上的 shards 数量为 0,此时集群会自动进行 rebalance,但是 rebalance 需要时间,此时新建索引很容易会被 balance.shard 参数主导,平衡所有索引的分片,即都分配在新节点上看起来更加平衡。

涉及集群平衡性 shard 分配主要有两个配置参数:

cluster.routing.allocation.balance.index(默认值 0.45f)
cluster.routing.allocation.balance.shard(默认值 0.55f)

“balance.index”:值越大,shard 分配越倾向于使得每个索引的所有分片在节点上均匀分布;
“balance.shard”:值越大,shard 分配越倾向于使得所有分片(所有索引的)在节点上平衡
  1. 解决方案

当新建的索引分片被全部分配在一个 node 节点上时,有以下 2 种解决办法:

  • 扩容集群需要新建索引时,按照如下所示设置对应参数。
PUT INDEX_NAME/_settings
{
  "index.routing.allocation.total_shards_per_node": 2
}

即单个索引在每个节点上最多分配 2 个 shards。其中,具体每个节点最多分配多少个 shards,请根据集群数据节点个数、索引分片(主、副)的数量自行决定。

  • 如果是 shards 集中分配在数量过少的节点上导致索引 shards 分配到同一个节点上,可以使用 POST _cluster/reroute 的 move 命令迁移分片到其他节点,rebalance 模块会自动分配其他更合适的分片与其交换节点。根据具体业务使用场景可以适当调节 balance.index,balance.shard 配置。

Elasticsearch 集群分片过多会有哪些影响

  • 集群创建分片的速度随着集群分片数量增多而逐渐减低。
  • 触发 Elasticsearch 自动创建 index 时,创建速度变慢会导致大量写入请求堆积在内存中,严重时可导致集群崩溃。
  • 分片过多时,如果不能及时掌控业务的变化,可能经常遇到单分片记录超限、写入拒绝等问题。

CSS 服务中如何清理 Elasticsearch 缓存?

  • 清理 fielddata
// 查询fielddata缓存情况
GET /_cat/nodes?v&h=name,fielddataMemory

// 当fielddata占用内存过高时,可以执行如下命令清理指定索引的fielddata cache或者所有索引的fielddata cache
POST /{indexName}/_cache/clear?fielddata=true
POST /_cache/clear?fielddata=true  
  • 清理 segment

每个 segment 的 FST 结构都会被加载到内存中,并且这些内存是不会被垃圾回收的。因此如果索引的 segment 数量过大,会导致内存使用率较高,建议定期进行清理。

// 查看各节点的segment数量和占用内存大小
GET /_cat/nodes?v&h=segments.count,segments.memory&s=segments.memory:desc

如果 segment 占用内存过高时,可以通过删除部分不用的索引、关闭索引或定期合并不再更新的索引等方式释放内存

  • 清理 cache
POST /_cache/clear

使用 delete_by_query 命令删除 Elasticsearch 集群数据后,为什么磁盘使用率反而增加?

  • delete_by_query:按查询条件批量删除文档的核心操作

使用 delete_by_query 命令删除数据并不是真正意义上的物理删除,它只是对数据增加了删除标记。当再次搜索时,会搜索全部数据后再过滤掉带有删除标记的数据。

因此,该索引所占的空间并不会因为执行磁盘删除命令后马上释放掉,只有等到下一次段合并时才真正的被物理删除,这个时候磁盘空间才会释放。

相反,在查询带有删除数据时需要占用磁盘空间,这时执行磁盘删除命令不但没有被释放磁盘空间,反而磁盘使用率上升了。

CSS 服务中单节点的磁盘使用率过高是否会影响集群的业务?

  1. 问题现象

查看集群监控发现,Elasticsearch 集群“磁盘使用率”达到 80% 以上,用户担心单节点磁盘使用率过高会对集群业务产生影响。

  1. 业务影响
  • 单节点磁盘使用率超过 85%:无法为新副本分配空间,但是新的主分片仍然可以被分配,从而确保业务操作的连续性不受影响,然而,集群的高可用性上有风险。
  • 单节点磁盘使用率超过 90%:系统将自动触发分片迁移机制,将该节点上的分片重新分配至磁盘使用率较低的其他数据节点。这一过程可能导致集群暂时无法分配新的分片,进而影响到业务的正常运行,因为分片的迁移和重新分配可能会导致查询延迟增加或临时的服务中断,从而对业务连续性造成影响。
  • 单节点磁盘使用率超过 95%:系统会对 Elasticsearch 集群中对应节点里每个索引强制设置“read_only_allow_delete”属性,此时该节点上的所有索引将无法写入数据,只能读取和删除对应索引。

单节点磁盘使用率过高,可通过扩容 Elasticsearch 集群操作动态调整集群节点的数量和容量。如果是增加节点数量,则扩容成功后会重新分配索引分片,使分片在新旧节点之间均衡分布,可打开 Cerebro 查看索引分配情况,也可以修改“indices.recovery.max_bytes_per_sec”和“cluster.routing.allocation.cluster_concurrent_rebalance”两个参数值增加索引分配速度。

集群出现写入拒绝“Bulk Reject”,如何解决?

引起 bulk reject 的大多原因是 shard 容量过大或 shard 分配不均,具体可通过以下方法进行定位分析:

  1. 检查分片(shard)数据量是否过大。

单个分片数据量过大,可能引起 Bulk Reject,建议单个分片大小控制在 20GB - 50GB 左右。可在 kibana 控制台,通过如下命令查看索引各个分片的大小。

GET _cat/shards?index=index_name&v
  1. 检查分片数是否分布不均匀。

提供如下两种方式查看:

方式一:在控制台查看集群的监控指标,确认节点分片相关的监控数据。

方式二:使用命令行查看节点分片。

例如,在客户端执行 curl 命令,查看集群各个节点的分片个数。

curl "$p:$port/_cat/shards?index={index_name}&s=node,store:desc" | awk '{print $8}' | sort | uniq -c | sort

云搜索服务_1764720893972

返回结果中,第一列为分片个数,第二列为节点 ID。图中,有的节点分片为 1,有的为 8,分布极不均匀。

解决方案:

  • 如果问题是由分片数据量过大导致。

分片大小可以通过 index 模板下的“number_of_shards”参数进行配置。模板创建完成后,新建索引立即生效,已有索引不能调整。

  • 如果问题是由分片数分布不均匀导致。

临时解决方案:

  1. 可以通过如下命令设置“routing.allocation.total_shards_per_node”参数,动态调整某个 index 解决。
PUT <index_name>/_settings
{
    "settings": {
        "index": {
            "routing": {
                "allocation": {
                    "total_shards_per_node": "3"
                }
            }
        }
    }
}
// “total_shards_per_node”要留有一定的buffer,防止机器故障导致分片无法分配(例如10台机器,索引有20个分片,则total_shards_per_node设置要大于2,可以取3)。
// 在生产环境中,我们永远不能假设机器 100% 不宕机。设置 `total_shards_per_node` 为 `3`,本质上是给集群留了 10 个“空座位”(10 节点 * 3 上限 - 20 实际分片),这样当某台机器倒下时,它的两个分片才可以挪到其他节点上。
  1. 在索引创建前,可以通过索引模板设置索引在每个节点上的分片个数。
PUT _template/<template_name>
{
    "order": 0,
    "template": "{index_prefix@}*",  //要调整的index前缀
    "settings": {
        "index": {
            "number_of_shards": "30", //指定index分配的shard数,可以根据一个shard 30GB左右的空间来分配
            "routing.allocation.total_shards_per_node": 3 //指定一个节点最多容纳的shards数
        }
    },
    "aliases": {}
}

集群负载过高导致集群不可用

问题现象

“集群状态”为“不可用”,单击集群名称进入集群基本信息页面,选择“日志管理”,单击“日志查询”页签,可见日志内容存在报错“OutOfMemoryError”和警告“[gc][xxxxx] overhead spent [x.xs] collecting in the last [x.xs]”:频繁 GC 导致 OOM

云搜索服务_1764721587871

原因分析

集群负载过高,可能是有大量查询或写入任务堆积。当堆内存不足时,任务无法分配,将频繁触发 GC,导致 Elasticsearch 进程异常退出。

处理步骤

  1. 查询集群是否存在任务堆积。
  • 方式一:在 Kibana 的“Dev Tools”页面,分别执行以下命令查询是否存在任务堆积。
GET /_cat/thread_pool/write?v
GET /_cat/thread_pool/search?v

如下所示“queue”的值为非 0,表示存在任务堆积。

node_name                    name   active queue rejected
css-0323-ess-esn-2-1         write       2   200     7662
css-0323-ess-esn-1-1         write       2   188     7660
css-0323-ess-esn-5-1         write       2   200     7350
css-0323-ess-esn-3-1         write       2   196     8000
css-0323-ess-esn-4-1         write       2   189     7753
  • 方式二:在集群管理列表,单击集群操作列的“监控指标”,在集群监控指标页面查看集群的“Search 队列中总排队任务数”和“Write 队列中总排队任务数”,如果排队任务数值非 0 表示存在任务堆积。

云搜索服务_1764721704586

如果集群存在大量的任务堆积,则参考如下步骤优化集群。

  • 在集群的“日志管理 > 日志查询”页面,查看节点在 OOM 前是否存在大量慢查询日志记录,分析查询是否会对节点造成压力导致节点内存不足,如果存在则根据业务实际情况优化查询语句。(大查询)
  • 在集群的“日志管理 > 日志查询”页面,查看节点日志是否有“Inflight circuit break”或“segment can’t keep up”的报错信息,如果存在则可能是写入压力过大,对集群造成较大的压力导致熔断。需要查看监控信息,排查近期数据写入量(写入速率)是否存在激增,如果存在则根据业务实际情况合理安排写入高峰时间窗。(大导入)

如果集群不存在任务堆积或者集群优化完依旧不可用,则执行下一步,查看集群是否压力过大。

  1. 查看集群是否压力过大

在集群管理列表,单击集群操作列的“监控指标”,在监控指标页面查看 CPU 和堆内存相关指标,如“平均 CPU 使用率”和“平均 JVM 堆使用率”。如“平均 CPU 使用率”超过 80% 或“平均 JVM 堆使用率”高于 70%,则说明集群当前压力较大。

云搜索服务_1764721808668

  • 如果集群压力过大,请降低客户端的请求发送速率或扩容集群。
  • 如果集群压力正常或降低发送请求速率后集群依旧不可用,则执行下一步,查看集群是否存在大量缓存。
  1. 在 Kibana 的“Dev Tools”页面,执行以下命令查询集群是否存在大量缓存。
GET /_cat/nodes?v&h=name,queryCacheMemory,fielddataMemory,requestCacheMemory

name                         queryCacheMemory fielddataMemory requestCacheMemory 
css-0323-ess-esn-1-1                    200mb           1.6gb              200mb

如果返回结果中 queryCacheMemory、fielddataMemory 或 requestCacheMemory 的数值超过堆内存的 20%,则表示缓存过大,可执行命令 POST _cache/clear 清除缓存。这些缓存数据是在数据查询时生成的,目的是为了加快查询速度,当缓存清除则可能使查询时延增加。

每个节点的最大堆内存可以执行如下命令查询:

GET _cat/nodes?v&h=name,ip,heapMax

其中,name 为节点名称,ip 为节点的 IP 地址。

集群快照怎么生成

← 返回 Notes

Contact

Contact Me

Leave a message here. The form sends directly from the browser to a form delivery service and then to my email.

Messages are delivered to lzx744008464@gmail.com.