把一套 Elasticsearch(简称 ES)集群想象成一座 7×24 小时运转的“数字仓储物流中心”。文档数据是入库的货物,分片是把货物拆成的箱子,副本是每个箱子在另一间仓库里的备用品,而节点就是一间间实体仓库。主节点是总调度台,负责指挥全局;数据节点是仓管员,负责把箱子搬进搬出;协调节点是前台接待,把客户订单派给正确的仓管员。索引生命周期管理,像货物从热销期到清仓下架的自动流转;集群健康监控,像中控大屏上的红绿灯;性能调优,则是调整搬运路线、货架位置和叉车数量。当指示灯从绿色变成黄色甚至红色,别慌——这是系统在提醒你:仓库该扩建、货物该分流、防火演练该安排了。本篇文章用这条“数字仓储”的故事线,带你掌握 ES 集群的节点角色、分片副本、索引生命周期、健康监控、性能调优和高可用架构,让你成为能兜住千万级搜索请求的运维操盘手。

岗位关联

学完这部分,你将具备独立运维 ES 集群、诊断分片异常、规划容量与设计高可用架构的实战能力,这是搜索工程师与 SRE 岗位的核心技能。

目录
  1. 可视化演示
  2. 节点角色划分
  3. 分片副本策略
  4. 索引生命周期
  5. 集群健康监控
  6. 性能调优实践
  7. 高可用架构
  8. 运维实战经验
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示ES集群管理的核心机制,通过入门/进阶/实战/优化四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

ES集群管理:集群节点 集群节点角色与职责分工 分片与副本策略 核心机制可视化 ES集群管理整体架构与关键组件

节点角色划分

建一家物流园,第一步不是买叉车,而是先确定“谁管调度、谁管仓库、谁做接待”。如果每个人都想说了算,货就会全堆在同一个门口,谁都进不去。ES 集群也一样:节点是物理机器,但角色决定了它们的“岗位”。

ES 中常见三类角色。主节点(master)是总调度台,负责管理集群元数据——比如创建或删除索引、记录节点上下线、决定分片怎么分配。数据节点(data)是真正的仓管员,分片(shard)就存放在这些节点上,读写请求最终都在这里执行。协调节点(coordinating)是前台接待,接收客户端请求、解析语义,再转发给正确的数据节点。

生产环境建议把角色拆开部署:比如 3 个 master 候选节点不存数据,10 个数据节点只开 data 角色,再加 2 个协调节点专门扛请求压力。这样某个数据节点抖动时,不影响集群元数据管理;协调节点压力大时,也不会拖累磁盘 IO。

防脑裂的关键是投票法定人数。所谓“脑裂”,就是网络抖动把集群分成两半,两边都想当领导。用一个公式规定:候选节点超过半数才能选出主节点。

把“谁说了算”的规则定死,园区永远只有一个总调度台,风暴再大也不会乱。

quorum=⌊Nmaster2⌋+1\text{quorum} = \left\lfloor \frac{N_{\text{master}}}{2} \right\rfloor + 1
通俗理解:3 个候选节点时,法定人数是 2;只有拿到 2 票以上的人才能成为总调度台。
  1. 候选节点总数先统计所有配置了 master 角色的节点个数,这是投票池的规模。
    NmasterN_{\text{master}}
  2. 向下取整取一半并向下取整,避免出现小数票。
    ⌊Nmaster2⌋\left\lfloor \frac{N_{\text{master}}}{2} \right\rfloor
  3. 加一构成多数派加 1 得到严格超过半数的票数,任何时刻集群只会有一个主节点。
    quorum=⌊Nmaster2⌋+1\text{quorum} = \left\lfloor \frac{N_{\text{master}}}{2} \right\rfloor + 1

分片副本策略

同一批货物,不整箱堆在同一个角落,而是拆成多个箱子分放不同仓库;每个箱子再复制一份,寄存到对面的仓库。这样丢了任何一箱,都能用备份补上,还能两间仓库同时出货。ES 的分片(shard)和副本(replica)就是这个思路。

一个索引(index)在物理上被划分为多个主分片(primary shard),写入操作只发生在主分片上;每个主分片可以配置若干副本分片(replica shard),副本负责容灾和分流读请求。假设索引有 3 个主分片、每个主分片 1 个副本,那么集群里一共有 6 个分片在物理节点上分布。副本永远不会和它的主分片落在同一台机器上,就像备用箱不会放在同一间仓库。

分片数一旦确定,不能直接修改,只能通过 reindex 重建索引来调整,所以规划时要想清楚。判断一条文档进哪个分片,靠的是下面的路由公式:

仓库分区加备份加路由编号,整个园区才能既防丢货,又让每个仓管员都忙而不乱。

shard_id=hash(routing) mod primary_count\text{shard\_id} = \text{hash}(\text{routing}) \bmod \text{primary\_count}
通俗理解:把文档 ID 丢进哈希函数搅一遍,得到一个整数,再除以主分片总数取余数,余数就是箱子编号。
  1. 路由值 routing通常取文档 ID,也可以指定用户 ID,让同一用户的文档聚到同一分片。
    routing=doc_id\text{routing} = \text{doc\_id}
  2. 哈希函数 hash把任意长度的字符串压缩成一个整数,让数据均匀散开。
    hash(routing)⇒integer\text{hash}(\text{routing}) \Rightarrow \text{integer}
  3. 取模运算 mod整数除以主分片总数得到的余数,就是目标分片编号。
    shard_id=integer mod primary_count\text{shard\_id} = \text{integer} \bmod \text{primary\_count}

索引生命周期

一件商品在仓库里有自己的旅程:热销期放门口“快进快出”,过季转常规仓,再老转冷库,最后清仓报废。整个过程不需要人肉盯守,货运系统按规则自动流转。ES 的索引生命周期管理(ILM)就是这套自动滑轨。

ILM 把索引切分为四个阶段:hot(热)阶段存放最新数据,读写都很快;warm(温)阶段索引变为只读,可以收缩分片、强制合并段;cold(冷)阶段数据迁移到冷节点,只保留低频搜索能力;delete(删除)阶段直接物理清理,释放磁盘。以日志索引为例,通常用索引模板配合滚动策略:

货架区、常规仓、冷库和回收站之间铺上自动滑轨,运维人员就从重复劳动里解放出来,只需要设定规则、观察状态。

rollover=(max_size≥50GB)∨(max_age≥30d)\text{rollover} = (\text{max\_size} \geq 50\text{GB}) \lor (\text{max\_age} \geq 30\text{d})
通俗理解:当前索引要么超过 50GB、要么超过 30 天,任一个条件满足,就自动生成一个新索引,老索引转入下一阶段。
  1. 体积阈值 max\_size索引主分片总大小达到 50GB,说明箱子满了。
    max_size≥50GB\text{max\_size} \geq 50\text{GB}
  2. 时间阈值 max\_age索引创建超过 30 天,说明这箱货放得太久。
    max_age≥30d\text{max\_age} \geq 30\text{d}
  3. 逻辑或运算两个条件只要命中任意一个就触发滚动。
    (max_size≥50GB)∨(max_age≥30d)(\text{max\_size} \geq 50\text{GB}) \lor (\text{max\_age} \geq 30\text{d})

集群健康监控

物流园的中控大屏上,每个仓库、每辆叉车都有指示灯:绿色代表一切正常,黄色代表备份没到位,红色代表关键货物失联。值班员一抬眼就知道该干什么。ES 集群也有这么一块“大屏”——GET _cluster/health。

返回结果里最重要的字段是 status,它用三种颜色概括全局状态:green 表示所有主分片和副本分片都已分配;yellow 表示所有主分片已分配,但存在尚未分配的副本分片(通常是节点数不足或磁盘紧张);red 表示存在未分配的主分片,意味着数据可能有丢失风险。监控大盘上除了看颜色,还要盯 CPU、内存、磁盘使用率、JVM 老年代 GC 耗时、查询 p99 延迟等核心指标。

要算清楚一个索引需要多少物理分片,可以用这条公式:

红绿灯只是提醒,真正重要的是看懂哪条线、哪个仓库出了问题,然后快速行动。

total_shards=primary_shards×(1+replica_count)\text{total\_shards} = \text{primary\_shards} \times (1 + \text{replica\_count})
通俗理解:一份货的标准箱数是主分片数,每箱再额外复制 replica 份,总箱数就是标准箱 ×(1 + 复制份数)。
  1. 主分片数 primary\_shards索引最初拆分出的箱子数量。
    primary_shards\text{primary\_shards}
  2. 副本系数 (1 + replica\_count)1 表示主分片本身,replica 表示复制份数。
    1+replica_count1 + \text{replica\_count}
  3. 相乘得总箱数两者相乘,得到所有主副本分片在物理节点上占用的位置总数。
    primary_shards×(1+replica_count)\text{primary\_shards} \times (1 + \text{replica\_count})

性能调优实践

仓库出货慢,有经验的调度员不会盲目加人,而是先测叉车车速、货架距离、搬运路径。很多时候瓶颈不在力气,而在“路绕”。ES 性能调优也是同样的逻辑——先定位瓶颈,再用工具优化。

写入优化:调大 refresh_interval,让数据少刷几次盘;用 bulk 批量接口,一次写入几千条,像集装箱装货而不是单件搬运。查询优化:多用 filter 上下文,让结果进入节点缓存;避免深分页(from+size 翻到上万条),改用 search_after 滚动遍历。内存优化:JVM 堆大小按下面的经验公式设置,不要贪心设太大。磁盘规划:用 SSD 替换机械硬盘,多块磁盘用条带化或 data path 分散写入,别用 NFS 共享存储。

调优不是使劲加资源,而是把搬运路线、货架高度、叉车数量一起调顺,让每一份力气都花在刀刃上。

heap=min⁡(32GB, physical_memory×50%)\text{heap} = \min(32\text{GB},\ \text{physical\_memory} \times 50\%)
通俗理解:堆内存最多给物理内存的一半,但最多不超过 32GB,超过这个上限反而会导致垃圾回收变慢。
  1. 物理内存的一半在总内存里切一半给 JVM 堆,剩下留给操作系统页缓存。
    physical_memory×50%\text{physical\_memory} \times 50\%
  2. 32GB 封顶超过 32GB 后,JVM 无法使用压缩指针,同样的对象要白占更多内存。
    min⁡(32GB,…)\min(32\text{GB}, \ldots)
  3. 取最小值两个值里选小的那个,保证堆既够用又不越界。
    heap=min⁡(32GB, physical_memory×50%)\text{heap} = \min(32\text{GB},\ \text{physical\_memory} \times 50\%)

高可用架构

园区再大也可能遇到台风,所以总调度台的旁边还有备用调度室。关键物资的清单在两座城市各存一份——这就是高可用。ES 集群的可用性,靠的是跨可用区部署、故障转移、容量规划和滚动升级四件事。

跨机房部署:master 候选节点分布在至少两个可用区,数据节点和副本也分散布置,避免单机房故障导致数据全灭。故障转移:当某个节点宕机,master 会自动把该节点上的副本提升为主分片,并重新分配缺失的副本。容量规划:根据线上 QPS 增长曲线和存储增速,提前几个月估算需要加多少节点。滚动升级:一轮一轮重启节点,每次只停一个,配合分片分配开关,让集群全程零停机升级。

系统整体可用率可以用概率公式估算。假设单个机房可用率是 A,有 n 个机房互为备份,只要其中任意一个还活着,服务就不会中断:

多一个备用中心,台风季节也能睡个安稳觉。

Atotal=1−(1−Asingle)nA_{\text{total}} = 1 - (1 - A_{\text{single}})^n
通俗理解:每个机房都挂掉的概率是 (1-A) 的 n 次方,用 1 减去这个值,就是至少有一个机房活着的概率。
  1. 单机房可用率一个机房在统计周期内正常服务的时间占比,比如 99.9%。
    AsingleA_{\text{single}}
  2. 所有机房同时故障每个机房挂掉的概率相乘,得到全挂概率。
    (1−Asingle)n(1 - A_{\text{single}})^n
  3. 取补集得总可用率用 1 减去全挂概率,就是整个集群的可用率。
    1−(1−Asingle)n1 - (1 - A_{\text{single}})^n

运维实战经验

流程标准比个人英雄主义可靠。真正成熟的物流园,每一步操作都写在墙上:火灾先疏散再救火,断电先切备用电源再修线路。ES 集群运维也有一本“标准作业手册”。

节点重启流程:先执行 PUT _cluster/settings 把分片分配关掉,再重启节点,最后重新打开分配。否则节点一掉线,master 会立刻把它的分片转移到其他节点,重启完毕又要搬回来,白白引发一场“数据搬迁风暴”。数据备份:用快照(snapshot)接口把索引定期备份到对象存储或 HDFS,关键索引每天一次。慢日志分析:开启 index.search.slowlog,把超过阈值(如 500ms)的查询记录下来,事后定位是哪条 DSL 拖垮了集群。集群扩容:新节点加入后,分片迁移需要时间,如果迁移太猛会影响线上业务,要限速。

迁移时间可以粗略估算:

标准作业手册像园区的保命符——平时不起眼,出事时每一秒都在救人。

Tmigrate≈SshardBnetworkT_{\text{migrate}} \approx \frac{S_{\text{shard}}}{B_{\text{network}}}
通俗理解:搬一箱货的时间,大约等于箱子大小除以传送带速度。网络带宽越大、分片体积越小,迁移越快。
  1. 分片体积单个分片的实际大小,决定搬运量。
    SshardS_{\text{shard}}
  2. 网络带宽节点间实际可用传输速率,决定搬得多快。
    BnetworkB_{\text{network}}
  3. 相除得时间体积除以带宽得到理想迁移时间,加 30% 冗余更稳妥。
    Tmigrate≈SshardBnetworkT_{\text{migrate}} \approx \frac{S_{\text{shard}}}{B_{\text{network}}}

AI 应用场景

电商大促搜索集群扩容

阶段:运维与部署

解决的问题:大促前临时加机器是传统做法,容易出现分片分布不均、热节点堆积、扩容速度跟不上流量增长。ES 的容量规划与滚动扩容,让扩缩容变成可预测的工程步骤。

具体案例:某电商平台在双 11 前两个月做容量预估,发现搜索 QPS 将增长 10 倍。团队用生成的新索引重建数据,把单索引从 3 个主分片扩到 12 个主分片,并用 ILM 把历史订单索引转入 warm 阶段,保证大促期间新数据写入不受历史查询干扰。

ELK 日志平台索引生命周期管理

阶段:数据治理与存储优化

解决的问题:日志平台每天产生 TB 级数据,传统做法是写定时脚本手动删索引,容易误删或删晚了,存储成本居高不下。ILM 让“热温冷删”全自动流转。

具体案例:某可观测性平台为应用日志配置 ILM:hot 阶段保留 3 天,warm 阶段保留 30 天并收缩分片,cold 阶段保留 90 天,超过 180 天自动 delete。磁盘成本下降约 60%,排查问题时仍能按时间范围找到冷数据。

金融风控系统跨机房高可用

阶段:高可用架构设计

解决的问题:金融风控要求任何时刻都能查询用户风险画像,单机房故障会导致服务不可用甚至违规。跨可用区部署与故障转移机制保障 RPO 接近 0。

具体案例:某支付公司在两个可用区部署 42 个 ES 节点,每个索引设置 2 个副本,master 候选节点双区各 3 个。某次机房冷却故障触发自动切换,副本提升为主分片,业务只出现 1 秒的写入抖动。

Python 演示

python34 行
import jsonimport urllib.request ES_HOST = "http://127.0.0.1:9200" def fetch(path):    req = urllib.request.Request(ES_HOST + path)    with urllib.request.urlopen(req, timeout=5) as resp:        return json.loads(resp.read().decode("utf-8")) def main():    health = fetch("/_cluster/health")    nodes = fetch("/_nodes/_local")["nodes"]    indices = fetch("/_cat/indices?format=json")     print("集群健康状态:", health["status"])    print("节点数:", health["number_of_nodes"])    print("活跃主分片:", health["active_primary_shards"])    print("活跃副本分片:", health["active_shards"] - health["active_primary_shards"])    print("未分配分片:", health["unassigned_shards"])     print("\n各索引分片统计:")    for idx in indices:        print(f"  {idx['index']}: 主分片 {idx['pri']}, 副本 {idx['rep']}")     if health["unassigned_shards"] > 0:        shards = fetch("/_cat/shards?format=json")        unassigned = [s for s in shards if s["state"] == "UNASSIGNED"]        print("\n存在未分配分片, 建议排查:")        for s in unassigned:            print(f"  index={s['index']} shard={s['shard']} type={s['prirep']}") if __name__ == "__main__":    main()
目的:连接 ES 集群 REST API,打印集群健康三色状态、节点数、分片分布,并列出所有未分配分片,是一份标准运维巡检脚本。
  1. ES_HOST = "http://127.0.0.1:9200"设定集群入口地址,相当于给仓储园区写上门牌号。
  2. def fetch(path)封装一次 HTTP GET 请求,统一处理 URL 拼接与 JSON 解析。
  3. health = fetch("/_cluster/health")拉取集群总览,核心字段就是三色状态。
  4. nodes = fetch("/_nodes/_local")["nodes"]获取当前节点列表,用于核对节点数是否和预期一致。
  5. health["active_shards"] - health["active_primary_shards"]用总活跃分片减去主分片,得到副本分片数量。
  6. if health["unassigned_shards"] > 0:只要有分片没被分配到节点,就说明集群处于 yellow 或 red 状态。
  7. [s for s in shards if s["state"] == "UNASSIGNED"]用列表推导式筛出所有未分配分片,方便格式化输出明细。

常见误区

误区我以为把索引设置成几百个分片,就能让查询飞快,反正机器多、磁盘大,分片多一点更“均匀”。

正解分片太多会导致每个分片都很小,查询变成“给每个仓管员都打个电话”,网络开销、元数据开销、段合并开销全部上涨。业界通常建议单个分片控制在 30~50GB,单节点分片数不超过 1000,按目标数据量倒推分片数。

为何常见:直观上“拆得越细越能并行”,但忽略了 ES 是“先广播再汇总”的分布式查询模型,调度成本随分片数线性增长。

误区我看到 `_cluster/health` 返回 yellow,以为只是小问题,过几分钟就会自动变绿。

正解yellow 表示存在未分配副本,背后可能是磁盘不足、节点掉线、或副本数超过了最大可用节点数。若不处理,主分片一旦也丢失,集群会直接变红。正确做法是查看 unassigned shards 的原因(_cat/shards?h=index,shard,prirep,state,node&s=state),尽快腾出空间或补节点。

为何常见:yellow 听起来比 red 温和,加上“自动恢复”的说法容易让人放松警惕,忽略了它可能持续数天甚至数周。

误区我让每个节点都既是 master 候选又是 data 节点,觉得这样万一某个节点挂了,其他节点也能顶上,更“灵活”。

正解在大集群中,data 节点频繁写数据会触发大量 GC 和磁盘 IO,如果它同时参与 master 选举和管理,会拖慢集群元数据决策;反过来,master 节点宕机时可能连带丢失数据节点。生产环境建议角色分离:master 节点用轻量机器,data 节点用内存大、磁盘快的机器,协调节点单独扛查询压力。

为何常见:小集群(5 节点以内)角色混合确实可行,初学者容易把“小集群经验”直接放大到生产环境。

误区我原来以为重启节点跟重启电脑差不多,直接杀掉进程,等几秒再拉起就好。

正解如果分片分配没有关闭,节点掉线的瞬间 master 会把它的分片调到别处,重启后再搬回来,产生大量无意义的迁移流量,甚至拖垮集群。标准流程是:先关闭分片分配 → 逐台优雅重启 → 等待节点加入 → 重新开启分片分配,最后观察健康状态恢复绿色。

为何常见:ES 的自动恢复机制过于“勤快”,反而让不了解调度逻辑的运维人员踩了坑,以为杀进程是万能操作。

面试题

Q1. ES 集群健康状态为 yellow 说明什么?

单选题 · easy

  • A.主分片与副本全部分配完成
  • B.主分片全部就绪但有副本未分配← 正确答案
  • C.存在未分配的主分片数据丢失
  • D.集群拒绝请求且无法正常写入

答案:B。yellow 对应“主分片都健康,但至少一个副本没分配”,比如节点数不足以分散副本或磁盘空间不足。green 才是全部分配完成,red 是主分片缺失。

Q2. 分片路由计算使用哪个公式?

单选题 · mid

  • A.shard = hash(routing) % 主分片数← 正确答案
  • B.shard = hash(routing) % 副本分片数
  • C.shard = hash(routing) + 主分片数
  • D.shard = hash(routing) - 副本分片数

答案:A。ES 用 hash(routing) % primary_count 决定文档落到哪个主分片。取模对象是主分片数,不是副本数,也不是加减法,因为只有主分片才承担写入路由。

Q3. 关于 ES 主节点(master)的说法,正确的有哪些?

多选题 · mid

  • A.负责创建索引、管理节点上下线← 正确答案
  • B.负责主分片与副本的数据同步
  • C.需要奇数个候选节点防止脑裂← 正确答案
  • D.候选节点数量越多性能越高

答案:AC。A 是主节点的核心职责;C 是法定人数防脑裂的设计。B 是主分片所在数据节点的职责而非主节点;D 错误,候选节点多不代表性能高,反而可能拖慢选举。

Q4. 副本分片的主要作用是什么?

单选题 · easy

  • A.增加查询吞吐与故障冗余← 正确答案
  • B.提高单次写入的速度
  • C.降低磁盘空间的消耗
  • D.替换主分片并主动写入

答案:A。副本能分摊读请求,提升查询吞吐;主分片故障时副本还能晋升为新的主分片,提供容灾能力。副本不承担写入,写入只走主分片,磁盘空间反而会翻倍。

Q5. ILM(索引生命周期管理)包含哪些常见阶段?

多选题 · mid

  • A.hot 热阶段(活跃写入)← 正确答案
  • B.warm 温阶段(只读压缩)← 正确答案
  • C.frozen 阶段(冻结数据)
  • D.cache 阶段(缓存层管理)

答案:AB。ILM 标准四阶段是 hot、warm、cold、delete。A 和 B 是其中的热、温阶段。ES 没有标准 ILM frozen 阶段,也没有 cache 阶段,它们容易和搜索快照/缓存特性混淆。

Q6. 为什么建议 JVM 堆大小不要超过 32GB?

单选题 · hard

  • A.超过 32GB 后 GC 暂停时间显著增大
  • B.超过 32GB 后无法使用压缩指针← 正确答案
  • C.超过 32GB 后索引无法正常分片
  • D.超过 32GB 后写入性能彻底归零

答案:B。JVM 在堆小于 32GB 时默认启用压缩 oops 指针(Compressed OOPs),对象引用只需 4 字节;超过 32GB 后指针膨胀为 8 字节,同等数据量占用内存更大,GC 压力激增。A 是表现而非根本原因,C、D 是错误推断。

Q7. ES 滚动升级集群时,第一步通常应做什么?

单选题 · mid

  • A.直接重启所有节点并升级版本
  • B.关闭分片分配以阻止数据迁移← 正确答案
  • C.备份全部索引并删除旧索引
  • D.停止写入并清空全部缓存

答案:B。滚动升级必须“一次只停一个节点”。先关闭分片分配,节点离线时就不会触发大规模分片迁移;升级完成后再重新开启分配,避免数据搬迁风暴。A 会造成集群长时间不可用,C 和 D 会破坏数据。

Q8. 关于集群“脑裂”问题,正确的说法有哪些?

多选题 · extreme

  • A.候选节点失联时可能出现多个 master← 正确答案
  • B.设置法定最小节点数可防止脑裂← 正确答案
  • C.脑裂会导致索引元数据被多次修改← 正确答案
  • D.增加副本数量可以完全避免脑裂

答案:ABC。网络分区时,两侧节点都选自己为 master 就形成脑裂,所以需要法定人数(quorum)保证只有一个投票过半的 master。脑裂时多个 master 同时改元数据,破坏极大。D 错误:副本解决的是数据冗余,不影响 master 选举。

Q9. 请求到达协调节点后,下一步主要做什么?

单选题 · easy

  • A.一直停留在协调节点等待返回结果
  • B.根据路由规则转发到对应数据节点← 正确答案
  • C.把文档直接写入协调节点本地磁盘
  • D.在所有数据节点上广播写入所有分片

答案:B。协调节点负责路由,它根据待搜索的分片列表把请求转发到持有对应分片的数据节点,再汇总返回。协调节点本身不存储数据,也不会向所有分片广播写入。

Q10. 下面哪个方法可以避免深分页查询?

单选题 · mid

  • A.使用 from+size 设置超大数值
  • B.使用 search_after 或游标遍历← 正确答案
  • C.把 from 值设置成负数绕过限制
  • D.把 size 设置成 0 来提高性能

答案:B。深分页要先把前面所有结果排序后丢弃,代价极大。search_after 基于上一页最后一条结果继续翻页,适合滚动遍历大量数据。A 会加重深分页问题,C 是非法操作,D 查不出内容。