普通视图

发现新文章,点击刷新页面。
今天 — 2025年4月17日掘金专栏-得物技术

得物自研DGraph4.0推荐核心引擎升级之路

作者 得物技术
2025年4月17日 09:44

一、前言

DGraph是得物自主研发的新一代推荐系统核心引擎,基于C++语言构建,自2021年启动以来,经过持续迭代已全面支撑得物社区内容分发、电商交易等核心业务的推荐场景。DGraph在推荐链路中主要承担数据海选和粗排序功能,为上层精排提供高质量候选集。

核心技术特性:

  • 索引层 - 支持KV(键值)、KVV(键-多值)、INVERT(倒排)、DENSE-KV(稠密键值)等。索引存储支持磁盘 & 内存两种模式,在预发等延迟压力低场景,通过磁盘索引使用低规格服务器提供基本服务。线上场景使用内存索引保证服务稳定性,提供毫秒级延迟响应。索引更新支持双buff热更新【内存足够】、服务下线滚动更新【内存受限】、Kafka流式数据实时更新等三种模式。

  • 查询层 - 支持向量检索IVF & HNSW、键值(KV)查询、倒排检索、X2I关联查询、图查询。对外提供JavaSDK & C++ SDK。

系统依赖架构:

  • 索引全生命周期管理由得物索引平台DIP统一管控。
  • 服务发现基于ZooKeeper(zk)。
  • 集群资源调度基于得物容器平台,目前已经支持HPA。

服务规模:

目前在线100+集群,2024年双11在线突破了100W qps。

本文主要介绍DGraph系统在2024年的一些重要改进点。主要包括两次架构调整 + 性能优化 + 用户体验提升方面的一些工作。

二、架构升级

2.1 垂直拆分业务集群支持

在2023年前,DGraph系统始终采用单一集群架构提供服务。该架构模式在平台发展初期展现出良好的经济性和运维便利性,但随着业务规模扩张,单集群架构在系统层面逐渐显露出三重刚性约束:

  1. 存储容量瓶颈 - 单节点内存上限导致数据规模受限;
  2. 网络带宽瓶颈 - 单物理机Pod共享10Gbps带宽,实际可用带宽持续承压,推荐引擎业务中部分核心集群200余张数据表(单表需20分钟级更新)的实时处理需求已遭遇传输瓶颈;
  3. 计算能力瓶颈 - 单实例最大64核的算力天花板,难以支撑复杂策略的快速迭代,核心场景响应时效与算法复杂度形成显著冲突;
  4. 稳定性 - 大规格集群对于容器调度平台不友好,在扩容、集群故障、集群发布时耗时较久;基于得物平台推荐数据量增长和算法迭代需求,我们实施业务垂直拆分的多集群架构升级,通过资源解耦与负载分离,有效突破了单节点资源约束,为复杂算法策略的部署预留出充足的技术演进空间。

系统改进点是在DGraph中增加了访问了其他DGraph集群 & FeatureStore特征集群的能力(图1)。为了成本考虑,我们复用了之前系统的传输协议flatbuffers,服务发现仍基于ZooKeeper。

图 1 DGraph 访问架构改进

改造的难点在图化集群!

目前推荐业务的核心场景都进行了图化改造,图化查询是把多路召回、打散、融合、粗排等策略打包到一个DAG图中一次发送到DGraph,DGraph的算子调度模块根据DAG的描述查询索引数据 & 执行算子最终把结果返回给业务系统,但这些DAG图规模都很大,部分业务DAG图涉及300+算子,因此如何在垂直拆分业务中把这些DAG图拆分到不同的DGraph集群中是一个非常复杂的问题,我们主要做了三方面改进:

  1. DAG管理 - 集群分主集群和从集群【多个】,DAG图部署在存在主集群中,DIP平台会分析DAG的拓步结构并把属于从集群的部分复制出来分发给从集群,为了保证DAG的一致性,只允许从主集群修改DAG图;

  2. 集群划分 - 通常按召回划分,比如Embedding召回、X2I召回、实验召回可以分别部署在不同的集群,另外也可以把粗排等算力需求大的部分单独放在一个集群,具体根据业务场景调整;

  3. 性能优化 - 核心表多个集群存放,减少主集群和从集群间数据交换量。

图 2 DGraph业务垂直拆分集群

2.2 分布式能力支持

垂直拆分集群,虽然把推荐N路召回分散到了M个集群,但是每个集群中每个表依然是全量。随着得物业务的发展,扩类目、扩商品,部分业务单表的数据量级已经接近单集群的存储瓶颈。因此需要DGraph中引入数据水平拆分的能力。

图 3 DGraph 分布式集群架构图

在DGraph分布式架构设计中,重点考虑了部署成本优化与业务迁移工作量:

  1. 分布式集群采用【分片数2】×【双活节点2】×【数据副本数2】的最小拓扑结构,理论上需要8台物理节点保障滚动更新与异常容灾时的稳定性。针对CPU负载较轻的场景,为避免独立Proxy集群带来的额外资源开销,DGraph将Proxy模块和DGraph引擎以对称架构部署到所有节点,通过本地优先的智能路由策略(本地节点轮询优先于跨节点访问)实现资源利用率与访问效率的平衡;

  2. 在业务兼容性方面,基础查询接口(KV检索、倒排索引、X2I关联查询)保持完全兼容以降低迁移成本,而DAG图查询需业务侧在查询链路中明确指定Proxy聚合算子的位置以发挥分布式性能优势。数据链路层面,通过DIP平台实现索引无缝适配,支持DataWorks原有任务无需改造即可对接分布式集群,同时增量处理模块内置分片过滤机制,可直接复用现有Flink实时计算集群进行数据同步。

三、性能优化

3.1 算子执行框架优化

在DGraph中,基于DGraph DAG图(参考图9)的一次查询就是图查询,内部简称graphSearch。在一个DAG图中,每个节点都是一个算子(简称Op),算子通过有向边连接其他算子,构成一个有向无环图,算子执行引擎按DAG描述的关系选择串行或者并发执行所有算子,通过组合不同算子DAG图能在推荐场景中灵活高效的完成各种复杂任务。

在实际应用场景中受DAG图规模 & 超时时间(需要控制在100ms内)限制,算子执行框架的效率非常重要。在最开始的版本中我们使用过Omp & 单队列线程池,集群在CPU负载低于30%时表现尚可,但在集群CPU负载超过30%后,rt99表现糟糕。在降本增效的背景下,我们重点对算子执行框架进行了优化,引入了更高效的线程池 & 减少了调度过程中锁的使用。优化后目前DGraph 在CPU压力超过60%依然可以提供稳定服务。

图4 DGraph算子执行框架优化

线程池优化:将原1:N 的线程池-队列架构调整为M:N 分组模式。具体实现为将N个工作线程划分为M个执行组(每组N/M线程),各组配备独立任务队列。任务提交采用轮询分发机制至对应组队列,通过资源分区有效降低线程调度时的锁竞争强度。

调度器优化:在DAG调度过程中存在两个典型多写场景

  1. 前驱算子节点完成时需并行更新后继节点标记;

  2. DAG全局任务计数器归零判断。原方案通过全局锁(Graph锁+Node锁)保障原子性,但在高负载场景引发显著锁竞争开销,影响线程执行效率。经分析发现这两个状态变更操作符合特定并发模式:所有写操作均为单调增减操作,因此可将锁机制替换为原子变量操作。针对状态标记和任务计数场景,分别采用原子变量的FetchAdd和FetchSub指令即可实现无锁化同步,无需引入CAS机制即满足线程安全要求。

3.2 传输协议编码解码优化

优化JavaSDK - DGraph数据传输过程:在DGraph部分场景,由于请求引擎返回的数据量很大,解码编码耗时占整个请求20%以上。分析已有的解码编码模块,引擎在编码阶段会把待传输数据编码到一个FlatBuffer中,然后通过rpc协议发送到业务侧的JavaSDK,sdk 解码FlatBuffer封装成List 返回给业务代码,业务代码再把List 转化成 List<业务Object>。过程中没有并发 & sdk侧多了一层冗余转换。

优化方案如下: 

  1. 串行编码调整为根据文档数量动态调整编码块数量。各子编码块可以并发编码解码,加快编码&解码速度,提升整体传输性能;

  2. sdk 侧由 Doc -> Map -> JavaObject 的转化方式调整为 Doc -> JavaObject,减少解码端算力开销。

图5 DGraph 传输编码解码过程优化

四、用户体验优化

4.1 DAG图调试功能优化

目前我们已经把DGraph DAG图查询的调试能力集成到DIP平台。其原理是:DGraph 的算子基类实现了执行结果输出,由于算子的中间结果数据量极大,当调试模块发现调试标志后会先把当前算子的中间结果写入日志中,数据按TraceID + DAGID+ NodeID 组织,最终这些数据被采集到SLS日志平台。

图6 DGraph DAG图查询调试

从DIP平台调试DAG图请求,首先通过DGraph JavaSDK的调试入口拿到DAG图请求json,填入DIP平台图请求调试入口,发起请求。索引平台会根据请求体自动关联DAG图并结合最终执行结果通过页面的方式展示。DIP平台拿到结果后,在DAG图中成功的算子节点标记为绿色,失败的节点标记为红色(图6)。点击任意节点可以跳转到日志平台查看该节点的中间结果输出。可用于分析DAG图执行过程中的各种细节,提升业务排查业务问题效率。

4.2 DAG图支持TimeLine分析

基于Chrome浏览器中的TimeLine构建,用于DGraph DAG图查询时算子性能分析优化工作。TimeLine功能集成在算子基类中,启动时会记录每个算子的启动时间、等待时间、完成时间、执行线程pid等信息,这些信息首先输出到日志,然后被SLS日志平台采集。用户可以使用查询时的TraceID在日志平台搜索相关的TimeLine信息。

图7 DGraph DAG图例子

图8 使用浏览器查看DGraph DAG图 TimeLine

当我们拿到请求的TimeLine信息后,通过浏览器加载可以通过图形化的方式分析DAG执行过程中耗时分布。图7是一个DAG 请求,它有9个算子节点,图8是它的一次请求的TimeLine。通过分析这些算子的耗时,可以帮助我们定位当前DAG图查询的瓶颈点在哪里,从而精准去解决性能方面的问题。

4.3 DAG图支持动态子图

在DAG图召回中,业务的召回通常都带有一些固定模式,比如一个业务在一个DAG图召回中有N路召回,每一路召回都是:① 查找数据;② 关联可推池;③ 打散; 它们之间的区别可能仅仅是召回数据表名不同或者传递的参数不同。通常我们业务调整或者算法实验调整只需要增加或者减少部分召回,原有模式下这些操作需要去新增或者修改DAG图,加上算法实验很多,业务维护DAG图的成本会非常高。

DAG动态子图的引入就是为了解决这类问题,首先我们在DAG图中配置一个模板子图,它仅仅描述一个行为模式,代表会涉及几个算子,算子之间的关系如何,实际的参数以及召回路的数量则由业务方在发起请求时动态决定。子图的执行和主图的执行共用同一套调度框架,共享运行时资源以降低运行开销。

图9 DGraph 子图

图9是一个DAG召回使用DAG子图后的变化,它有8路召回,一个Merge节点,这些召回分为两类,一类是基于KV表(ForwardSearch)触发的向量召回,另外一类是基于KVV表(IvtSearch)触发的向量召回。引入DAG子图后,在主图中节点数量由17个降为3个。

五、展望未来

过去四年,DGraph聚焦于实现得物推荐引擎体系从0到1的突破,重点完成了核心系统架构搭建、算法策略支持及业务迭代空间拓展,取得多项基础性成果。基于2024年底的用户调研反馈结合DGraph当前的发展,后续将重点提升产品易用性、开发与运维效能及用户体验,同时在系统稳定性、可扩展架构和平台化建设方面持续深化。

算法团队大量HC,欢迎加入我们:得物技术大量算法岗位多地上线,“职”等你来!

文 / 寻风

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

昨天以前掘金专栏-得物技术

如何合理规划Elasticsearch的索引|得物技术

作者 得物技术
2025年4月15日 14:38

一、背景

随着ES在业务场景中的使用逐渐增多,平台对ES集群的稳定性、管理、运维的压力逐渐增大,通过日常的运维情况来看,发现用户对ES的了解熟悉程度参差不齐,经常性的遇到索引创建不规范,或者参考别人索引的创建脚本进行创建索引,对索引没有一个比较清晰的认知,对索引结构的规划也寥寥无几,为此,平台使用了一些列手段来帮助用户提前合理规划模板,比如索引、模板的创建接入飞书审批流,平台侧会逐一结合业务场景和ES集群情况详细沟通确定索引或者模板结构;又比如ES内核增加业务不停服的动态扩分片能力,旨在进行不合理索引的治理提升ES集群稳定性(索引一旦创建分片是不能修改的),我们内部改动ES源码实现了不停服动态扩分片。

因此有必要从ES的索引讲起,让大家对ES的索引从概念、原理到使用有一个清晰的认知,希望日常业务场景中用到ES的同学能够抽时间读一下。当然文章避免不了存在主观的分析,大家可以在文章底部进行评论或者私聊我们,一起探讨。好了废话不多说了,现在开始介绍。

二、什么是index(索引)

下面会针对索引的组成和基本结构结合官方文档逐一介绍。

基本概念

index(索引)是索引是具有相似特征的文档(Document)集合,类似于关系型数据库中的表。每个索引都具有自己唯一的名称与_id。并且可以进行不同的参数配置与mapping映射。以适应不同的业务场景。索引中的最小单位是文档。每一条文档(doc)都是一个json格式的数据对象。包含了实际的具体数据以及该数据所对应的元数据。文档可以是结构化,半结构化或非结构化的数据。索引在elasticsearch中被用于存储,检索与分析数据。通过对索引进行搜索与聚合操作可以快速地找到相关的文档。

官方描述:The index is the fundamental unit of storage in Elasticsearch, a logical namespace for storing data that share similar characteristics. After you have Elasticsearch deployed, you’ll get started by creating an index to store your data.

翻译:索引是Elasticsearch中存储数据的基本单位,是一个逻辑命名空间,用于存储具有相似特性的数据。在部署Elasticsearch后,您将通过创建索引来存储数据。

An index is a collection of documents uniquely identified by a name or an alias. This unique name is important because it’s used to target the index in search queries and other operations.

翻译:索引是一种文档集合,通过名称或别名唯一标识。这个唯一名称非常重要,因为它用于在搜索查询和其他操作中定位索引。

三、索引结构详解

索引结构详解

图片

创建索引结构
PUT /index_demo
{
  "aliases" : {
    "index_demo_alias" : { }
  },
  "mappings" : {
    "properties" : {
      "id" : {
        "type" : "long"
      },
      "name" : {
        "type" : "text",
        "fields" : {
          "keyword" : {
            "type" : "keyword",
            "ignore_above" : 256
          }
        }
      },
      "status" : {
        "type" : "keyword"
      },
      "createDate" : {
        "type" : "long"
      }
    }
  },
  "settings" : {
    "index" : {
      "refresh_interval" : "5s",
      "number_of_shards" : "3",
      "number_of_replicas" : "1"
    }
  }
}

ignore_above属性说明:

- ignore_above的默认值通常为256个字符,这意味着任何超过256个字符的字符串将不会被索引或存储。

- 该参数仅适用于keyword类型的字段,因为这些字段主要用于过滤、排序和聚合操作,不需要进行全文搜索。

- ignore_above的值以字符为单位计算,包括英文字符和汉字。例如,一个汉字和一个英文字符都算作一个字符。

- 性能优化:通过限制字段长度,可以减少索引大小和查询时间,从而提高性能。

- 避免资源浪费:对于包含大量数据的字段,如日志文件中的长字符串,可以通过ignore_above避免不必要的存储和索引。

官方描述:Strings longer than the ignore_above setting will not be indexed or stored. For arrays of strings, ignore_above will be applied for each array element separately and string elements longer than ignore_above will not be indexed or stored.

3.1 别名

别名将其生命置于群集状态内,由主节点(master node) 管理; 这意味着如果你有一个名为 xiaoming 的别名指向一个名为 potato 的索引,那么开销就是群集状态映射中的一个额外键,它将名称 xiaoming 映射到具体的索引字符串。这意味着与其他指数相比,别名的重量要轻得多; 可以维护数千个而不会对集群产生负面影响。

官方原话:An alias points to one or more indices or data streams. Most Elasticsearch APIs accept an alias in place of a data stream or index name.

Aliases enable you to:

- Query multiple indices/data streams together with a single name

- Change which indices/data streams your application uses in real time

- Reindex data without downtime

翻译:别名(Alias)可以指向一个或多个索引或数据流。大多数Elasticsearch API接受别名代替数据流或索引名称。别名的功能包括:

- 使用单一名称查询多个索引/数据流;

- 实时更改应用程序使用的索引/数据流;

- 在不中断服务的情况下进行扩分片。

可以看到索引有上面三个作用,平台建议为每个索引添加别名(动态扩分片依赖别名)。添加别名可以在索引创建时和创建后再添加,即索引可以随时添加,但是平台还是建议你在创建索引时候指定别名,避免动态扩分片时候再去修改代码重新部署应用。

添加别名的几种方式

1. 创建索引时指定别名

PUT /test_index
{
    "settings" : {
        "number_of_shards" : 1,
        "number_of_replicas" : 1
    },
    "aliases":{"test_alias":{}},
    "mappings" : {
        "properties" : {
            "field1" : { 
                "type" : "text" 
            },
            "createdAt": {
                "type""date",
                "format""yyyy-MM-dd HH:mm:ss"
           }
        }
    }
}

2. 已存在的索引添加别名

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index""test_index"# 索引名
        "alias""test_alias" # 别名
      }
    }
  ]
}

3. 别名更换

别名更换可以零停机进行动态扩分片。

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index""existing_index",
        "alias""test_alias" # 别名
      },
      {
        "remove": {
          "index""old_index",
          "alias""old_test_alias" # 别名
        }
      }
    }
  ]
}

3.2 映射

建立索引时需要定义文档的数据结构,这种结构叫作映射。在映射中,文档的字段类型一旦设定后就不能更改。因为字段类型在定义后,elasticsearch已经针对定义的类型建立了特定的索引结构,这种结构不能更改。借助映射可以给文档新增字段。另外,elasticsearch还提供了自动映射功能,即在添加数据时,如果该字段没有定义类型,elasticsearch会根据用户提供的该字段的真实数据来猜测可能的类型,从而自动进行字段类型的定义。

3.3 字段类型

字段类型(Field Type)是定义数据格式和索引方式的重要概念,它决定了字段在索引中的存储、搜索和聚合行为。下面针对日常用到最多的三个字段类型进行解释,text、keyword、Numeric(Integer、Long)。

Text

text字段类型是Elasticsearch中用于全文搜索的核心字段类型。它通过分析器将文本拆分为单个词,并存储为倒排索引,适用于非结构化文本的搜索和分析。然而,由于其经过分析器处理,不适用于排序和聚合操作。

1. 特点

  • 全文搜索: text字段类型主要用于存储和索引可读的文本内容,例如邮件正文、产品描述、新闻文章等。这些字段会被分析器(analyzer)处理,将字符串拆分为单个词(term),以便进行全文搜索。
  • 分词处理: text字段支持分词器(tokenizer),可以根据语言和需求选择不同的分词策略(如标准分词器、正则表达式分词器等)。分词后的结果会存储为倒排索引,便于快速检索。
  • 不适用于排序和聚合: 由于text字段经过分析器处理,其原始字符串无法直接用于排序或聚合操作。如果需要排序或聚合,通常需要结合keyword字段类型。
  • 支持多字段映射: 可以通过多字段(multi-field)映射同时使用text和keyword类型,以满足全文搜索和精确匹配的需求。

2. 使用场景

  • 全文搜索: 适用于需要对文本内容进行模糊搜索的场景,例如搜索引擎、新闻网站、商品搜索等。
  • 文本分析: 可以结合分析器(如TF-IDF、BM25等)进行文本相似性搜索或评分计算。
  • 日志分析: 用于分析和搜索日志文件中的文本内容,提取关键信息。
  • 内容管理: 在内容管理系统中,用于存储和搜索文档、文章等内容。

3. 官方建议

Use a field as both text and keyword

Sometimes it is useful to have both a full text (text) and a keyword (keyword) version of the same field: one for full text search and the other for aggregations and sorting. This can be achieved with multi-fields.

通过多字段映射同时使用text和keyword类型,可以实现全文搜索和精确匹配的双重需求。

4. 平台建议

  • 明确业务使用场景,如果不需要进行模糊搜索的话,设置为keyword类型,来避免分词带来的存储开销,增加系统压力。

Keyword

keyword字段类型是一种用于存储和索引结构化数据的字段类型。

1. 特点

  • 不进行分词: keyword字段类型不会对字段值进行分词处理,而是将其作为整体存储。这意味着字段值会被原样存储到倒排索引中,不会被拆分成单独的单词或短语。
  • 精确匹配: 由于字段值不进行分词,keyword字段类型非常适合用于精确匹配查询,例如查找特定的电子邮件地址、身份证号或状态码等。
  • tips: 在term查询中可以结合case_insensitive属性,忽略大小写对值进行搜索,但不支持terms查询。
  • 支持排序和聚合: keyword字段类型可以用于排序和聚合操作,例如按状态码统计数量或按用户ID进行分组。
  • 存储效率高: 由于不需要分词,keyword字段类型的存储开销较低,适合存储大量具有唯一性或固定值的字段。

2. 使用场景

  • 精确查询: 适用于需要精确匹配的场景,例如查找特定的电子邮件地址、身份证号、状态码等。
  • 排序和聚合: 当需要对数据进行排序或聚合时,keyword字段类型是理想选择。例如,按用户ID排序或按状态统计数量。
  • 标签和分类: 用于存储标签、分类等结构化数据,例如用户画像标签(学生、IT、教师等)。
  • 唯一性字符串: 适用于存储具有唯一性的字符串,如SpuId、货号、得物订单号等。

Numeric

数值类型,包含long、interger、short、byte、double、float等数字类型。

1. 特点

  • 整数类型: 适用于范围查询、排序和聚合操作。由于整数类型占用空间较小,推荐优先使用范围较小的类型(如 integer 或 long)以提高索引和搜索效率。
  • 浮点类型: 适用于需要高精度的计算场景。如果数据范围较大或精度要求不高,可以使用 scaled_float 类型并设置合适的 scale 值。
  • 选择合适的类型: 在满足需求的前提下,尽量选择范围较小的类型以节约存储空间和提升性能。

tips

如果确定业务使用场景,建议keyword代替数值类型字段,如果不确定则采用多字段,keyword在term查询中性能更佳。

图片

3.4 针对字段类型选择的几条建议

  1. 针对Text和数值类型场景的字段,尽量改成keyword字段类型,来提升查询速度。

  2. 在不确定业务查询有哪些需求的情况下,设置多字段类型keyword。

  3. 枚举字段没有特殊业务场景下,统一使用keyword字段类型。

  4. 业务不需要范围查询的话,使用keyword字段类型(支持聚合和排序的)。

  5. 对keyword字段类型进行模糊查询会性能较差,使用多字段类型wildcard来模糊查询性能更高。

  6. 尽量不要使用聚合查询,text的fielddata会加大对内存的占用,如有需求使用,建议使用keyword。

  7. 需要中文分词的话,不要使用默认分词器,推荐使用ik_smart,ik_max_word会生成更多的分词,其中含有重复的内容,需谨慎使用。

  8. 时间字段不要使用keyword,除非点查,推荐使用date/long类型,支持范围查询,建议精确到分钟,会提高查询效率。

  9. keyword字段类型不适用于模糊wildcard查询,建议使用wildcard字段类型。

    图片

  10. 日期的查询条件为now时,并不能有效利用缓存,尽量换成绝对时间值。

  11. ES默认字段个数最大1000,但建议不要超过100,对于不需要建立索引的字段,不写入ES。

  12. 将不需要建立索引的字段index数据设置为false,对字段不分词,不索引可以减少很多运算操作。

  13. 不建议或者禁止每次写入后立马进行显示的refresh,refresh会带来较高的磁盘IO,和CPU消耗,甚至有可能导致ES宕机。

  14. 持续补充......

3.5 索引结构与关系性数据库对比

图片

四、索引(Shard)结构-分片与副本

4.1 什么是Shard

基本概念

分片是管理文档的一个数据单元,分片是Elasticsearch中逻辑概念。ES内部把索引中文档进行按照一定路由规则(文档_id的hash值与分片数取余)进行路由到不同的存储数据单元,存储数据单元就是分片。你可以理解为MySQL的分表。

ElS的逻辑分片就是一个Lucene索引,一个ES索引是分哦的集合,当ES在索引中搜索的时候,他发送查询到每一个属于索引的分片(Lucene索引)进行检索,最后合并每个分片的结果得到一个全局的结果集。

分片划分

分片分为primary shard(主分片)replicate shard(副本分片)

  • 主分片: 索引的基本数据存储单元,每个索引被水平拆分为多个主分片,每个分片都是互相独立的。包含一部分索引的数据与索引的结构(segement)。每个分片都可以在集群中不同的节点上进行移动与复制。以提高数据的可用性与容错性。

  • 副本分片: 主分片的完整拷贝,用于冗余存储和容灾,副本分片和主分片在ES节点数足够的情况下不会同时存在一个ES节点。

注意:单分片的记录条数不要超过上限2,147,483,519。

  • 主副分片分布示意图

图片

分片的功能

1. 主分片

  • 数据存储与写入: 所有文档通过路由算法(如 hash(_id) % num_primary_shards(主分片数))分配到主分片,主分片负责处理索引、更新、删除等写操作。
  • 扩展性: 通过增加节点和分片分布,实现数据的水平扩展。
  • 不可变性: 主分片数量在索引创建时通过 number_of_shards 参数设定,创建后无法修改(需重建索引)。

2. 副本分片

  • 高可用性: 当主分片所在节点宕机时,副本分片自动升级为主分片(和对应的主分片不在一个节点),避免数据丢失和服务中断。
  • 读取负载均衡: 副本分片可并行处理查询请求,提升读吞吐量。
  • 动态调整: 副本分片数量通过 number_of_replicas 参数动态配置,支持按需扩展或缩减。

4.2 分片数规划

分片的基本概念和功能咱们咱们已经了解,在日常ES运维过程中发现不少同学对分片和数量的设置没有什么概念,照搬其他同学的比较多,这是严重错误的。咱们在实际的业务场景中也要做好分片(主副)数量的规划,来避免慢查、数据倾斜、磁盘容量浪费等问题。

当索引分片数量过多时,可能会对ES性能产生不利影响。因为每个分片都需要一定量的内存来存储索引数据和缓存,从而导致内存消耗增加。另外当查询或写入数据涉及多个分片时,ES需要在节点之间进行传输和协调数据,从而增加网络开销,这也会导致查询和写入性能的降低。可见分片数量的选择需要慎重考虑。

索引在不同场景中,其分片分设置是不一样的,接下来咱们会在下面四个场景中来进行阐述。

读场景

索引单分片20g~40g,尽量减少分片数,可以降低热点,因为当分片数过多时,就容易出现长尾子请求,即有可能部分子请求因ES集群节点异常、Old GC、网络抖动等延迟响应,导致整个请求响应缓慢。另一方面,拆分过多的子请求无法提升数据节点请求吞吐,不能充分利用 CPU。在尽量减少主分片数的情况下,同时也可以适当增加副本数,从而提升查询吞吐。

写场景

索引单分片10g~20g,小分片更有利于数据写入。小分片维护的segment数量远低于大分片,在数据刷新落盘与段合并上更有优势。由于单分片数据量更少,在写入时数据可以更快地缓存至内存中并通过refresh参数更快的持久化至磁盘中。

日志存储场景

  • 需要考虑每日写入集群的数据总量大小。通过过数据量与数据节点数评估索引分片数量。
  • 在日志存储后是否需要兼顾查询与聚合性能。合理大小的分片数据量能够提高查询效率。
  • 根据日志持久化策略,采用按月/周/天的策略生成索引。并使用ILM(索引生命周期管理策略)动态对日志索引进行完整生命周期的管理。
  • 建议副本数设置为0来减少磁盘容量成本。

小数据量索引业务场景

对于数据量比较小的索引,增加索引分片数并不一定会带来性能提升,反而可能会带来一些负面影响。

首先,增加索引分片数会增加集群的管理开销,包括维护分片的状态、备份和恢复分片等。如果索引数据量比较小,这种开销可能会超过性能提升带来的收益。

其次,增加索引分片数可能会导致数据分布不均衡,从而影响查询性能。具体来说,如果某些分片中的数据量过小,可能会导致这些分片的查询性能比其他分片差。此外,如果查询涉及到多个分片,数据的合并操作也会增加查询时间。

因此,对于数据量比较小的索引,在查询场景下,通常建议将分片数设置为1或2,以避免不必要的开销和性能问题。如果需要提高查询性能,可以考虑配置索引副本,优化查询语句或使用缓存。

通用场景

  • 根据实际业务场景提前规划预算索引数据量,做好分片数量规划(索引一旦创建无法修改分片数)。

  • 分片数量:推荐公式:主分片数 ≈ 总数据量 / 单分片容量上限(官方建议单分片10-50GB,单个分片文档数在1亿条以内,日志场景可放宽至50-100GB)。

    注意:分片数量平台强烈建议或者要求设置为ES data节点角色的整数倍。

  • 副本数量:增加副本数可提升读性能,但会降低写入速度(需同步更多副本),因此在读场景可以酌情考虑。

  • 如果索引是时序类,或者数据过大,单分片几百G,可以结合生命周期和索引模板进行索引滚动管理。

  • 平台不建议使用自动移routing值进行分片,默认使用文档_id就好。

    原因:使用自定义routing值进行路由分片的话很容易产生数据倾斜,另外ES内部会多一些计算逻辑来如何进行分片路由,在写入较高的场景下也会有一定的性能损耗。

  • 控制分片数量,分片数不是越多越好,过多分分片,也会造成ES集群元数据管理的压力,降低系统的性能损耗。

  • 设置total_shards_per_node,将索引压力分摊至多个节点。

  • index.routing.allocation.total_shards_per_node参数可以限制每个节点上的shard数量,从而将索引的压力分摊到多个节点,这样可以提高集群性能和可用性,避免某个节点过载导致整个集群出现问题。

  • index.routing.allocation.total_shards_per_node是一个索引级别设置(创建索引和对已有索引进行设置),语法如下:

PUT <index_name>/_settings
{
    "index.routing.allocation.total_shards_per_node":<number_of_shards>
}
<index_name>为索引名字,<number_of_shards>表示每个节点上该索引的分片数量。

持续调整索分片

对于集群分片的调整,通常不是一蹴而就的。随着业务的发展,不断新增的子业务 或 原有子业务规模发生突变,都需要持续调整分片数量。

4.3 索引与资源消耗的关系**

分片数量与内存消耗

每个分片都是独立的Lucene索引,需要维护倒排索引、缓存等内存结构。分片数量过多会导致以下问题:

  • 内存占用激增: 每个分片默认占用约10-30MB内存(含元数据),数千分片可能消耗数十GB内存。
  • 文件句柄耗尽: 集群总分片数过多会占用大量文件描述符,可能触发"too many open files"错误。
  • CPU热点问题: 分片分配不均会导致部分节点负载过高。

Segment碎片化

分片由多个segment组成,segment数量过多会:

  • 增加IO压力: 查询需遍历多个segment文件。
  • 占用堆内存: 每个segment需加载部分元数据到内存,百万级segment可能消耗数GB内存。
  • 影响GC效率: 频繁的segment合并会触发Full GC。

五、总结

创建一个索引需要结合业务使用场景考量字段类型选择和是否需要索引分词,按照数据规模和业务增长速度来确定分片和副本的数量的大小。索引的结构直接影响集群的稳定性,因此我们在创建索引的时候要养成习惯,作为技术方案的一环去仔细打磨这样才能保证线上的稳定性。

大家工作中遇到的一些稳定性问题,和使用上的一些问题都可以找我们一起探讨,寻找最优解。

文 / 阳光

关注得物技术,每周一、三更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

❌
❌