本文原文链接
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
- 项目中是如何使用ES(elasticsearch)的?如何优化的?
- 对于 GC 方面,在使用 Elasticsearch 时要注意什么?
- ES 调优的手段有哪些?
最近有小伙伴在面试 jd、字节,又遇到了相关的面试题。小伙伴懵了,因为没有遇到过,所以支支吾吾的说了几句,面试官不满意,面试挂了。
所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书
来一个大的介绍:ElasticSearch的性能优化的几大方面
总的来说,ElasticSearch的性能优化,可以从以下方面的考虑:
硬件层面的优化:
机器分配,机器配置,机器内存,机器CPU,机器网络,机器磁盘性能
系统层面的优化:
文件句柄优化、swap关闭
ElasticSearch集群层面的优化
合理分配节点,
合理分配参加竞选Master的节点
ElasticSearch 索引层面的优化,
副本数量、索引数量、分片数量
ElasticSearch查询层面的优化
职责分离,全面监控
1 硬件层面的优化
1.1 为ES进行 硬件层面的 内存优化:
Elasticsearch是一个内存密集型应用,合理分配内存对于Elasticsearch 提高性能至关重要:
确保系统内存有足够的空间用于文件缓存,而不单单是JVM堆内存。
建议将一半的内存分配给Elasticsearch的堆内存,另一半留给操作系统和其他进程。
为了避免垃圾回收(GC)成为瓶颈,建议设置堆内存的最大值为30GB或更小。可以通过修改
jvm.options
文件来调整堆内存的大小。
总之,ES是比较依赖内存的,并且对内存的消耗也很大,内存对ES的重要性甚至是高于CPU的,所以即使是数据量不大的业务,为了保证服务的稳定性,在满足业务需求的前提下,我们仍需考虑留有不少于20%的冗余性能。
根据业务量不同,内存的需求也不同,一般生产建议总体 32内存,其中JVM 不要少于16G, 还有 16G给底层的Luence。
为ES进行 系统层面的 磁盘优化
对于ES来说,磁盘可能是最重要的了,因为数据都是存储在磁盘上的,当然这里说的磁盘指的是磁盘的性能。
磁盘性能往往是硬件性能的瓶颈,木桶效应中的最短板。
方式一:纵向扩展: 使用最高性能的 磁盘
ES 磁盘优化的最为简单,最为粗暴的方式是 纵向扩展,就是使用最高性能的 磁盘,比如SSD 固态硬盘。
ES应用可能要面临不间断的大量的数据读取和写入,数据规模非常庞大, 尼恩曾经架构过的一个30个节点ES集群,数据量非常庞大,达到1TB 。
一般来说,速度最快的,也是最昂贵的。
不可能都用SSD硬盘, 一般是 SSD和机械硬盘组合使用。
所以,可以考虑把节点冷热分离:
“热节点”使用SSD做存储,可以大幅提高系统性能;
“冷数据“存储在机械硬盘中,降低成本。
如果使用磁盘阵列,可以使用RAID 0。
这里,尼恩给大家来点基础知识, 什么是RAID 0?
RAID(Redundant Array of Independent Disks,独立磁盘冗余阵列)是一种将多个物理磁盘组合成一个逻辑单元的数据存储虚拟化技术,其目的是提高数据存储的性能、可靠性或两者兼顾。
RAID 0 是RAID中一种配置方式,具体含义如下:
RAID 0 定义:
RAID 0 又称为条带化(Striping),它将数据分散存储在两个或多个硬盘上,每个硬盘只存储数据的一部分,从而提高数据的读写速度。
工作原理:
在RAID 0配置中,数据被分成多个块(称为“条带”),并按顺序存储在多个硬盘上。
例如,如果有一个4KB的数据块,它会被分成4个1KB的块,分别存储在4个硬盘上。
读取数据时,因为可以同时从多个硬盘读取,所以读取速度会加快。
写入数据时也是同理,数据会被分成多个块并行写入多个硬盘。
性能提升:
RAID 0 的主要优势在于提高数据的读写速度。
由于数据被分散在多个硬盘上,所以理论上,RAID 0 的读写速度可以达到单个硬盘速度的总和。
没有冗余:
RAID 0 的缺点是没有数据冗余。如果其中一个硬盘发生故障,整个阵列上的数据都会丢失,因为每个硬盘上只存储数据的一部分,没有副本。
适用场景:
RAID 0 适用于对性能要求高而对数据安全性要求不高的场景,如视频编辑、大型数据库的临时存储等。
RAID 0 风险:
由于没有数据冗余,RAID 0 的风险较高。
一旦硬盘发生故障,整个RAID 0 阵列上的数据都会丢失,因此在使用RAID 0 时需要非常小心,并且定期备份数据。
总结来说,RAID 0 是一种通过将数据分散存储在多个硬盘上来提高性能的配置方式,但它不提供任何数据冗余,因此使用时需要考虑到数据丢失的风险。
这里,尼恩给大家再来点基础知识, 看看 RAID 0 和 RAID 1 的区别?
RAID 0 和 RAID 1 是两种不同的RAID配置,RAID 0 和 RAID 1 在数据存储、性能和可靠性方面有显著的区别:
RAID 0(条带化): 多块磁盘并行读写
数据存储:数据被分割成多个块(条带),然后分散存储在两个或多个硬盘上。这意味着每个硬盘上存储的是数据的不同部分,而不是数据的副本。
性能:由于数据的读写操作可以并行进行,RAID 0 可以提供很高的数据传输速率,特别是在写入大量数据时。
可靠性:RAID 0 没有数据冗余,任何一个硬盘的故障都会导致整个阵列上的数据丢失。因此,RAID 0 的可靠性最低。
适用场景:适合对读写性能要求高、对数据安全性要求不高的应用,如视频处理、大型数据库的临时存储等。
RAID 1(镜像): 多块磁盘冗写,并行读
数据存储:数据在两个硬盘上存储完全相同的副本,即镜像。这意味着数据在两个硬盘上都有备份。
性能:RAID 1 的读取性能通常比写入性能要好,因为多个硬盘可以同时读取数据。但是,写入性能可能受到限制,因为数据需要同时写入两个硬盘。
可靠性:RAID 1 提供了很好的数据冗余,即使一个硬盘发生故障,另一个硬盘上的数据副本仍然可以保证数据的完整性和可用性。
适用场景:适合对数据安全性要求高的应用,如关键业务数据存储、服务器操作系统等。
总结来说,RAID 0 提供了高性能但牺牲了数据安全性,而RAID 1 则提供了数据冗余和较高的可靠性,但性能相对较低。在选择RAID配置时,需要根据具体的应用需求和预算来决定使用哪种RAID级别。
方式二:横向扩展:使用多块硬盘提高 硬盘的性能
横向 扩展,就是 搞多块磁盘。
Elasticsearch 通过多个path.data
目录配置,把Elasticsearch数据条带化分配到使用多块硬盘上面,以提高I/O性能。
要在Elasticsearch中使用多块硬盘,并通过配置多个path.data
目录来实现数据的条带化分配,可以按照以下步骤操作:
- 配置多个数据路径:
在Elasticsearch的配置文件elasticsearch.yml
中,可以指定多个数据存储路径(对应到多个磁盘),用逗号分隔。
例如,如果你有三块硬盘,并且已经分别挂载在/mnt/disk1
、/mnt/disk2
和/mnt/disk3
,你可以这样配置:
path.data: /mnt/disk1,/mnt/disk2,/mnt/disk3
这样配置后,Elasticsearch会尝试在这些路径上存储数据,类似于软件层面的RAID 0配置。
- ES的分片分配机制:
Elasticsearch在选择存储分片的路径时,会根据一定的逻辑来选择最佳路径。
Elasticsearch会优先选择磁盘使用率较低(/最为空闲)的磁盘来存储新的分片数据文件。
如果多个路径可用,Elasticsearch会根据剩余空间和已存储的分片数量来决定将分片分配到哪个路径上。
- 数据安全性问题:
虽然使用多个path.data
路径可以提高I/O性能,但这种做法类似于软件层面的RAID 0,没有在磁盘层面 做 数据冗余。
如果其中一个硬盘发生故障,存储在该硬盘上的所有分片都会丢失。
因此,官方建议,确保每个分片至少有一个副本分片,以防止数据丢失。
系统层面的ES 磁盘优化 总结:
- ES应用可能要面临不间断的大量的数据读取和写入,磁盘关系到 ES的性能,建议使用最高速的磁盘设备(如SSD)可以显著提高Elasticsearch的性能。
- 当然最高速也是最昂贵的,不可能都用SSD硬盘, 一般是 SSD和机械硬盘组合使用。
- 合理规划磁盘布局,可以考虑把节点冷热分离, 将数据按照冷热的程度,存储在不同的磁盘上,可以避免磁盘I/O竞争。
- 使用RAID 0可以提高磁盘I/O,但要注意数据的安全性。
- 通过多个
path.data
目录配置,把Elasticsearch数据条带化分配到使用多块硬盘上面,以提高I/O性能。 - 避免使用远程挂载的存储,比如NFS或者SMB/CIFS,因为这会引入延迟,影响性能。
1.2 为ES进行 硬件层面的 CPU 调优
CPU对其他的 应用程序而言,可谓是最重要的硬件,
但对于ES来说,CPU不是他最依赖的硬件, 提升CPU配置可能不会像提升磁盘或者内存配置带来的性能收益更直接、显著。在成本预算一定的前提下,应该把更多的预算花在磁盘以及内存上面。
当然也不是说CPU的性能就不重要,Elasticsearch在处理查询和索引操作时会消耗大量的CPU资源。
服务器的CPU不需要太高的单核性能,更多的核心数和线程数意味着更高的并发处理能力。现在PC的配置8核都已经普及了,更不用说服务器了。
对于ES来说,通常来说单节点cpu 4核起步,不同角色的节点对CPU的要求也不同。
CPU优化的第一步:角色分离,合理分配节点角色与资源
在 ElasticSearch 集群中,明确划分主节点(Master)、数据节点(Data)和协调节点(Coordinating)的角色。
主节点(Master)主要负责集群的管理和元数据存储,其 CPU 负载主要来自于集群状态的维护和更新。
数据节点(Data)负责存储和索引数据,在数据写入和查询操作时会占用较多 CPU 资源,这是CPU的大户。
协调节点(Coordinating)负责接收用户查询请求并将其分发到合适的数据节点进行处理,也会消耗一定的 CPU 资源用于请求的转发和结果的合并。
例如,在一个大型的ES集群中,大致的CPU配置如下:
主节点(Master)配置在性能稳定、CPU 核心数适中(如 4 - 8 核)的服务器上,专门用于处理集群管理任务;
数据节点(Data)则根据数据量和查询负载,分配具有较高 CPU 核心数(如 16 - 32 核)的服务器,以满足大量数据的索引和查询操作;
协调节点(Coordinating)可以选择 CPU 核心数相对较少(如 8 - 16 核)但网络性能良好的服务器,用于高效地分发和接收查询请求。
CPU优化的第二步:对线程池进行监控和动态调优
ElasticSearch 线程池类型
ElasticSearch 中有多种线程池,如搜索线程池(search
)、索引线程池(index
)、批量线程池(bulk
)等。
每个线程池负责不同类型的操作,并且其大小会影响 CPU 的利用率。例如:
- 搜索线程池(
search
)大小决定了可以同时处理的搜索查询数量 - 索引线程池(
index
)大小则影响数据索引的并发处理能力。
优化线程池大小
根据 CPU 核心数和集群的实际负载情况,合理调整线程池大小。
一般来说,线程池大小不应超过 CPU 核心数的两倍,以避免过多的线程竞争 CPU 资源导致性能下降。
例如,在一个具有 16 核 CPU 的数据节点上,可以将搜索线程池大小设置为 30 左右,索引线程池大小设置为 20 左右。
同时,可以通过监控工具(如 ElasticSearch 自带的监控 API 或者第三方监控工具)观察线程池的活跃线程数、队列长度等指标,根据这些指标动态调整线程池大小。
使用 ElasticSearch 自带监控 API获取监控数据
ElasticSearch 提供了丰富的 REST API 用于监控集群状态。可以通过访问
_cat/thread_pool
端点来获取线程池相关的信息。例如,发送一个 HTTP GET 请求到
http://<es-node-ip>:9200/_cat/thread_pool?v
v参数用于以详细格式返回结果),会返回类似如下内容:
pool_name active queue rejected
bulk 0 0 0
fetch 0 0 0
flush 0 0 0
generic 0 0 0
get 0 0 0
index 0 0 0
listener 0 0 0
management 0 0 0
percolate 0 0 0
refresh 0 0 0
search 0 0 0
snapshot 0 0 0
suggest 0 0 0
war m 0 0 0
write 0 0 0
其中,
active
列表示当前活跃的线程数,queue
列表示线程池队列中的任务数,rejected
列表示被拒绝的任务数。
通过这些核心指标,可以解每个线程池的负载情况。
动态调整线程池大小(通过 API 或配置文件)
如果发现 线程池负载高,或者 `queue 线程池队列中的任务数 积压多,可以进行动态调整线程池大小调整
在 ElasticSearch 中,可以通过更新集群设置来动态调整线程池大小,可以使用下面的API,
_cluster/settings
发送一个 PUT类型的settings 请求,例如:
{
"persistent": {
"thread_pool.search.size": 30
}
}
这个请求将搜索线程池(search)的大小调整为 30。
需要注意的是,这种调整方式需要谨慎操作,并且最好在低峰期进行,因为不当的调整可能会对集群性能产生暂时的负面影响。
另外,也可以通过修改elasticsearch.yml配置文件来调整线程池大小,不过这种方式需要重启集群才能生效。
CPU 的优化建议:
确保服务器有足够的CPU核心,并合理调整Elasticsearch的线程池配置,可以提高系统的并发处理能力。
推荐使用多核处理器(至少8核及以上)来提升处理能力。
避免虚拟化过度使用,特别是在生产环境中,因为这可能会引入不必要的性能开销。
对线程池进行监控和动态调优, 防止线程任务积压
1.3 为ES进行 硬件层面的 网络优化:
建议为ES 使用 低延迟的网络 :
ES是天生自带分布式属性的,并且ES的分布式系统是基于对等网络的,节点与节点之间的通信十分的频繁,延迟对于ES的用户体验是致命的,所以对于ES来说,低延迟的网络是非常有必要的。
高延迟的,跨多个数据中心的ES集群方案,是不太可取的,
虽然,ES可以容忍集群跨多个机房,可以有多个内网环境,支持跨AZ部署,但是不建议多个机房跨地域构建集群,一旦发生了网络故障,集群可能直接雪崩,即使能够保证服务正常运行,维护这样(跨地域单个集群)的集群带来的额外成本可能远小于它带来的额外收益。
2 系统层面的优化
2.1 为ES进行 系统层面的 文件句柄数 调优
修改ES启动用户可使用的系统文件句柄数,以适应Elasticsearch的需求。
Elasticsearch 需要大量 的文件句柄数, 主要 两大 原因:
第一:ES集群有大量的TCP /http 连接需求
大量的节点间通信连接:
Elasticsearch 集群中节点之间, 需要进行频繁的通信。
如数据同步、状态协调等,这会建立大量的 TCP 连接,每个 TCP 连接都需要占用一个文件句柄1。
ES集群有大量的客户端连接:
当有大量的 HTTP 客户端连接到 Elasticsearch 集群进行数据读写操作时,也会消耗大量的文件句柄。
如果文件句柄数不足,可能导致新的连接无法建立,客户端出现连接超时等问题 。
第二:ES集群有大量的索引与数据操作
ES集群有大量的索引文件操作:
Elasticsearch 在处理索引时,会涉及到大量的文件操作。
例如,在索引创建、更新、删除过程中,需要打开和操作多个索引文件,包括索引元数据文件、数据文件、段文件等。
每个文件都需要一个文件句柄,如果文件句柄数受限,可能会导致索引操作失败或异常。
ES集群有大量的数据段合并:
为了提高查询性能,Elasticsearch 会在后台进行数据段合并操作。
在合并过程中,需要同时打开多个段文件进行读写,如果文件句柄数不足,会影响段合并的效率,甚至导致合并失败 。
问题:文件句柄数的 系统限制,满足不了es的句柄需求
系统默认文件句柄数限制过低:
许多 Linux 发行版默认每个进程允许的文件句柄数通常只有 1024 或更低,这对于处理大量数据和高并发请求的 Elasticsearch 来说远远不够。
避免资源耗尽风险:
如果不调整文件句柄数,当 Elasticsearch 的文件句柄使用达到系统默认限制时,继续请求打开文件就会失败,可能导致数据丢失、节点故障等严重问题 。
调优:如何解除 文件句柄数的 系统限制?
step1:查看当前系统文件句柄数限制:
可以使用ulimit -n
命令查看当前文件描述符限制,默认通常是 1024,对于 ES 来说远远不够。
step2:修改文件句柄数
临时修改文件句柄数:
可以 通过ulimit -n 65536
命令将当前会话的文件描述符限制临时设置为 65536 。
永久修改文件句柄数:
使用vim /etc/security/limits.conf
命令编辑该limits.conf 文件,添加或修改以下配置项:
* soft nofile 65536
* hard nofile 65536
上述配置中,*
表示匹配所有用户,soft
为警告值,hard
为最大值。
检查配置是否生效:重新登录系统后,使用ulimit -n
命令查看文件句柄数限制是否已生效
2.2 为ES进行 系统层面的 swap 调优
这里就是简单粗暴,关掉swap。
内存交换 到磁盘对服务器性能来说是 致命 的。
如果内存交换到磁盘上,性能就会急剧下降, 一个 100 微秒的操作可能变成 10 毫秒,下降100倍以上。想想那么多 10 微秒的操作时延累加起来。
不难看出 swapping 对于性能是多么可怕。
所以,Elasticsearch建议禁用Swap分区,因为当物理内存不足时,操作系统会将一些内存页交换到磁盘上,这会导致性能急剧下降。
在Linux系统中, 用以下命令关掉swap:
sudo swapoff -a
在Linux系统中使用ES,最好可以通过修改/etc/sysctl.conf
文件来禁用Swap分区:
# 在文件末尾添加以下行
vm.swappiness=1
然后运行sudo sysctl -p
使配置生效。
除了 关掉swap ,还可以 使用 memlock ,为ES 锁定的物理内存。
memlock 是一个与内存锁定相关的系统资源限制。它用于指定一个进程能够锁定的物理内存(RAM)的大小。内存锁定意味着将进程使用的内存固定在物理内存中,防止操作系统将其交换(swap)到磁盘上的虚拟内存(磁盘交换空间)。
对于像 Elasticsearch 这样的应用程序,将内存锁定是很重要的。
因为 Elasticsearch 大量使用内存来缓存索引数据,如果这些数据被交换到磁盘,会导致查询性能大幅下降。内存锁定可以确保关键的内存区域(如索引缓存)始终在物理内存中,提高数据访问速度。
可以通过设置Elasticsearch的bootstrap.memory_lock
选项来尝试锁定JVM内存,防止其被交换到磁盘上, 在 配置文件elasticsearch.yml中添加以下行
bootstrap.memory_lock: true
注意:这需要用户 有memlock
权限。
可以通过ulimit -l
命令查看当前用户的memlock
限制,并通过ulimit -l unlimited
命令设置无限制(但这通常需要root权限)。
在生产环境中,更推荐的方式是通过修改/etc/security/limits.conf
文件来永久设置 memlock 限制。
通过修改/etc/security/limits.conf
文件来永久设置memlock
限制,可按以下步骤进行操作:
用文本编辑器打开/etc/security/limits.conf
文件, 在文件末尾添加或修改以下内容 :
* soft memlock [期望的软限制大小]
* hard memlock [期望的硬限制大小]
例如,如果想将memlock
限制设置为 8GB(以字节为单位),则可写成:
* soft memlock 8388608
* hard memlock 8388608
这里的*
表示匹配所有用户,你也可以根据实际情况指定具体的用户,如elastic soft memlock 8388608
和elastic hard memlock 8388608
,只对elastic
用户生效 。
3 JVM层面的优化
3.1 为ES进行 JVM层面的 JVM堆大小调优
Elasticsearch是运行在JVM上的,对其做JVM参数调优至关重要。
最常见的调优是Java内存的分配。
下面是JVM的内存模型,具体每块的作用,不在这里阐述。
新生代和老年代分配的内存比例给多大?
Jvm内存分为新生代和老年代。
新生代(或者伊甸园)
新实例化的对象分配的空间。
新生代空间通常都非常小,一般在 100 MB–500 MB。
新生代也包含两个 幸存 空间。
老年代
较老的对象存储的空间。
这些对象预计将长期留存并持续上很长一段时间。
老生代通常比新生代大很多。
新生代、老生代的垃圾回收都有一个阶段会“stop the world”。给新生代和老年代分配多大的内存呢?他们的比例是多少呢?
一般来说,老年代和新生代的内存比例为2:1是比较合适的。
比如给堆内存分配3G,则新生代分配1G,其余都给老年代。
在ElasticSearce的配置文件jvm.options文件配置:
-Xms3g //配置堆初始化大小
-Xmx3g //配置堆的最大内存
-Xmn1g //配置新生代内存。
一个Elasticesearch节点的两个内存大户
内存对于 Elasticsearch 来说绝对是重要的,它可以被许多内存数据结构使用来提供更快的操作。
一台机器装一个Elasticesearch节点,我们应该怎么分配机器的内存呢?
官方给出了解决方案,把一半(少于)的内存分配给Luence,另外的内存分配给ElasticSearch.
是一个Elasticesearch内存大户,底层的 Luence 另一个内存大户:
第一个 内存大户Elasticsearch 缓存
(1) 文档 缓存数据
最重要的是缓存索引数据。
首先 是 文档数据缓存。 当执行查询操作时,经常访问的文档数据会被缓存在堆内存中。
例如:一个电商商品搜索系统使用 Elasticsearch,用户频繁搜索的热门商品信息(如商品名称、价格、品牌等)会被缓存,这样在后续查询时可以快速从内存中获取,减少磁盘 I/O 操作,提高查询响应速度。
其次 是过滤器缓存, 也存储在堆内存中。
当使用过滤器进行查询(如筛选出特定品牌的商品)时,过滤器的结果会被缓存,下次遇到相同的过滤条件时可以直接利用缓存结果,提高过滤效率。
(2) 索引缓冲区
在数据写入索引的过程中,会有一个索引缓冲区(Index Buffer)在堆内存中。
新的数据首先进入这个缓冲区,当缓冲区满或者达到一定的阈值时,数据会被刷新到磁盘上的索引文件中。
(3) 查询相关数据结构
存储查询执行过程中的临时数据结构,如用于排序的字段值集合。
例如,对商品按照价格进行排序的查询,排序过程中的价格数据会暂存在堆内存中。
还有聚合操作的中间结果,比如在统计不同品牌商品数量的聚合查询中,每个品牌的计数中间结果会存储在堆内存。
(4) 集群状态 缓存
Elasticsearch 主节点需要在堆内存中存储集群状态信息,包括索引元数据(如索引名称、分片数量、副本数量等)、节点信息(如节点角色、节点健康状态等)。
这些信息对于集群的管理和协调至关重要,如在进行分片分配、故障转移等操作时会使用到这些信息。
第二个 内存大户 Lucene 非堆内存(off - heap)
(1) 索引文件数据
Lucene 是 Elasticsearch 的底层搜索引擎库,大部分索引数据实际上存储在非堆内存(磁盘文件)中。
索引文件包含了倒排索引,倒排索引是 Lucene 的核心数据结构。
例如,对于文档中的每个单词,倒排索引记录了包含该单词的文档列表。
在文本搜索场景下,如新闻网站的文章搜索,文章中的词汇和对应的文档编号存储在非堆内存的索引文件中,用于快速定位包含特定词汇的文章。
(2) 段文件(Segment Files)存储在非堆内存中。
Lucene 会将索引划分为多个段,每个段是一个独立的索引部分。
这些段文件包含了文档数据、词向量等信息,并且会根据一定的策略进行合并和优化。
例如,在数据更新过程中,新的数据可能会形成新的段,然后通过段合并操作来优化索引结构,这些段文件存储在非堆内存中。
(3)文件系统缓存
操作系统会对磁盘上的索引文件进行缓存,这部分缓存数据虽然不在 Java 堆内存中, 也是(off - heap)非堆内存的一部分。
当 Lucene 读取索引文件时,操作系统会将最近访问的文件块缓存在内存中,提高文件的访问速度。
例如,频繁被查询的索引段文件部分会被操作系统缓存,下次访问时可以直接从缓存中读取,减少磁盘读取时间。
Lucene 使用 内存映射文件(Memory - Mapped Files)数据,将磁盘上的索引文件映射到内存中,使得文件访问就像访问内存一样高效。
这部分内存映射的数据也属于非堆内存。通过这种方式,Lucene 可以直接在内存中操作索引文件的部分内容,而不需要进行大量的文件 I/O 操作,提高了索引数据的读写效率。
该分配多大的内存给Elasticesearch?
如果 把所有的内存都分配给 Elasticsearch 的堆内存,那将不会有剩余的内存交给 Lucene。这将严重地影响全文检索的性能。
标准的建议是把 50% 的可用内存作为 Elasticsearch 的堆内存,保留剩下的 50%,Lucene 会很乐意利用起余下的内存,也就是把 50% 的可用内存作为 Lucene 的非堆内存(off - heap)。
Lucene 被设计为可以利用操作系统底层机制来缓存内存数据结构,所以这部分Lucene 的非堆内存(off - heap),预留就OK了,不需要专门设置。
所以,在使用Elasticesearch的时候,按照实际的系统内存大小,一般进行Elasticesearch堆大小进行对应的配置,从最小规格的8G(如果 os内存 16g) 到了中等规模16G内存(如果 os内存 32g),然后最大规格32G内存(如果 os内存>64g)。
分配的建议如下:
- 如果 os内存 16g ,jvm 8G
- 如果 os内存 32g ,jvm 16g
- 如果 os内存>64g ,jvm 32g
Xms和Xmx设置:
合理设置堆内存大小, 一般建议将堆内存设置为物理内存的 50 - 70%。
通过-Xms(初始堆大小)和-Xmx(最大堆大小)参数来设置初始堆大小、最大堆大小 相同。例如:
-Xms30g
-Xmx30g
确保-Xms
和-Xmx
的值相同,这样可以避免在堆内存扩展或收缩过程中产生性能开销。
建议将JVM堆内存的初始大小(-Xms)和最大大小(-Xmx)设置为相同的值,以避免运行过程中动态调整堆内存大小,这是一个损耗性能的操作。
可以通过修改
jvm.options
文件来调整堆内存的大小。例如,将堆内存设置为16GB的配置如下:
-Xms16g
-Xmx16g
这里将堆内存的初始大小和最大大小都设置为16GB,你可以根据实际情况调整
分配给Elasticesearch 最大内存应该小于 32766 mb(~31.99 gb)
为啥呢? JVM 在内存小于 32 GB 的时候会采用一个内存对象指针压缩技术。
对于 32 位的系统,意味着堆内存大小最大为 4 GB。
对于 64 位的系统, 可以使用更大的内存,但是 64 位的指针意味着更大的浪费,因为你的指针本身大了。
更糟糕的是, 更大的指针在主内存和各级缓存(例如 LLC,L1 等)之间移动数据的时候,会占用更多的带宽。
所以, 为了节省 64 位的指针 占用内存的大小, Java 使用一个叫作 内存指针压缩(compressed oops)的技术来解决这个问题。
使用 内存指针压缩(compressed oops)的技术, 对象的指针不再表示对象在内存中的精确位置,而是表示 偏移量 。
这意味着 32 位的指针可以引用 40 亿个 对象 , 而不是 40 亿个字节。
从极端角度来说,假设一个对象一个字节,使用 内存指针压缩(compressed oops)的技术后, , 32 位的指针表示对象的指针理解为,最大增长到 32 GB 的物理内存。
一旦你越过那个神奇的 ~32 GB 的边界,指针就会切回普通对象的指针。
切回普通对象的64位指针,每个对象的指针都变长了,就会使用更多的 CPU 内存带宽,也就是说你实际上失去了更多的内存。
事实上,哪怕是 40–50 GB 内存,和使用了内存对象指针压缩技术时候的 32 GB 内存的 效果是一样的, 10多个G白白的给普通对象的64位指针了 。
总之:即便你有足够的内存,JVM 也尽量不要 超过 32 GB。因为它浪费了内存,降低了 CPU 的性能,还要让 GC 应对大内存。
3.2 垃圾回收器的优化
3.2.1:JDK8 + G1 垃圾回收器
很多公司生产环境使用的是老的es 6 系列版本(比较稳定),如
- JDK:JDK1.8_171-b11 (64位)
- ES集群:一般由3台16核32G的虚拟机部署 ES 集群,每个节点分配16G堆内存
- ELK版本:6.3.0
- 垃圾回收器:ES 默认指定的老年代(CMS)+ 新生代(ParNew)
- 操作系统:CentOS Linux release 7.4.1708(Core)
Elasticsearch 默认的垃圾回收器( GC )是 CMS。官方建议使用 CMS。
这个垃圾回收器可以和应用并行处理,以便它可以最小化停顿。
然而,它有两个 stop-the-world 阶段,但是 CMS处理大内存也有点吃力。
启用 G1 垃圾回收器
在 Elasticsearch 的 jvm.options文件中,将默认的垃圾回收器设置为 G1。找到以下行(如果不存在可以添加):
-XX:+UseG1GC
这会告诉 JVM 使用 G1 垃圾回收器来管理内存。
设置 G1堆内存 参数
根据堆内存大小来调整 G1 的相关参数。
第一:确定 G1 垃圾回收器的堆内存区域(Region)大小:
例如,-XX:G1HeapRegionSize
参数用于确定 G1 垃圾回收器的堆内存区域(Region)大小。
其默认值会根据堆内存大小自动调整,但在某些情况下可能需要手动优化。
计算公式为:1M <= G1HeapRegionSize <= 32M
,并且堆内存大小 / G1HeapRegionSize = Region数量
应该是2^n
。
例如,对于 30GB 的堆内存,可以尝试将-XX:G1HeapRegionSize
设置为16M
,这样Region
数量为30 * 1024 / 16 = 1920
(近似值),是一个比较合适的数值。
第二:优化停顿时间相关参数
设置最大停顿时间目标,使用-XX:MaxGCPauseMillis
参数来控制 G1 垃圾回收器的最大停顿时间目标。
例如,设置为200
表示希望垃圾回收的停顿时间尽量不超过 200 毫秒。
但要注意,设置的值过小可能会导致 G1 频繁地进行垃圾回收以满足停顿时间要求,反而增加了总的垃圾回收时间。
需要根据实际的业务场景和性能测试来调整这个参数。
第三:调整混合垃圾回收(Mixed GC)的触发条件
G1 会在一定条件下触发混合垃圾回收,即同时回收年轻代和部分老年代。
可以通过-XX:InitiatingHeapOccupancyPercent
参数来调整触发混合垃圾回收的堆内存占用比例。
例如,将其设置为45
,表示当堆内存占用达到 45% 时,G1 开始考虑触发混合垃圾回收。
这个参数的调整需要综合考虑堆内存大小、对象分配速率和停顿时间要求等因素。
3.2.2:jdk17 + ZGC垃圾回收器
45岁老架构师尼恩建议,如果条件允许,可以升级jdk版本,提升性能。
那为什么要升级到jdk17呢,主要是由于:
- 1、jdk17的g1垃圾回收期相较于jdk11,提升到15%左右。
- 2、在jdk17中,有向量化支持,同时在字符串处理等方面做了一些优化,相对于集群的写入压力会小很多。
- 3、在jdk17中,对于ZGC的适配做了一些加强,这个ZGC的垃圾回收器比G1稳定性和性能有很大的提升。
在考虑把jdk的版本升级到jdk17去,同时尽量把elasticsearch的版本更新到最新的版本去。
在完成了jdk17的升级之后,下一步考虑的就是配置ZGC垃圾回收器。
ZGC垃圾回收器是一个并发的、单代的、基于区域的、NUMA 感知的垃圾回收器,Stop-the-world 阶段仅限于根扫描,因此 GC 暂停时间不会随堆或 live set 的变大而增加。
而且ZGC垃圾回收期在回收的过程中几乎是全程并发处理,如下图:
所以对于短暂停顿的时间来说非常小,几乎可以忽略不计。下面我们来演示下配置ZGC的垃圾回收器方法。
关于zgc的文章,尼恩后续写一个系统化的介绍, 帮助大家穿透zgc。
在elasticsearch的config目录下有一个jvm.options文件
打开后,找到如下部分:
因为我们的jdk版本是jdk17,所以找到14-的地方把这里所有的东西注释掉,然后添加如下的代码:
14-:-XX:+UseZGC
14-:-XX:+UnlockExperimentalVMOptions
最后的样子如下图:
此时我们就配制好了当前的elasticsearch的环境使用ZGC垃圾收集器。
然后我们启动下elasticsearch,启动完成后在日志中查找下ZGC关键词,如下图:
可以看到启动的时候使用的是ZGC垃圾收集器。
以上就是ZGC垃圾收集器的配置全过程。
1、zgc垃圾收集器从jdk11版本开始支持,所以只要是jdk11及以上的版本都支持zgc垃圾收集器。
2、zgc垃圾收集器的由于是并行进行处理,所以生产环境中ZGC的垃圾收集停顿时间不超过10ms,可以放心大胆的使用起来。
3、使用ZGC是并行的进行垃圾回收,所以有时候会发生查询耗时增加的情况,可以在jvm.options文件中添加如下的参数:-XX:+UseDynamicNumberOfGCThreads 代表动态配置GC线程,降低CPU的使用量,这个是JDK17的新特性,在jdk17以下是没有这个参数的。
具体如下图:
Elasticsearch的内存优化总结
Elasticsearch的内存配置是其性能优化中非常重要的一部分。
以下是一些关键点:
- Elasticsearch默认将JVM堆内存设置为1GB或系统物理内存的一半,取较小值。但是这个默认值可能不适合所有场景,因此需要根据实际情况进行调整。
- 官方建议堆内存大小不要超过32GB,因为当堆大小超过32GB时,JVM会使用64位指针,这会导致更多的内存消耗和额外的开销。如果需要更大的堆内存,可以考虑增加节点数量而不是单个节点的堆内存大小。
- 堆内存应该合理分配给Elasticsearch进程,同时要确保操作系统和其他进程有足够的内存可用。通常,将机器总内存的50%到60%分配给Elasticsearch是合理的,剩下的内存留给操作系统缓存和其他服务。
- 对垃圾回收器进行优化,可以使用zgc
4 ElasticSearch集群层面的优化
Elasticsearch
的核心架构,是围绕着节点(Node
)和集群(Cluster
)展开。
节点是Elasticsearch
的运行实例,每个节点都承担着特定的任务和职责。
多个节点组成一个集群,通过分布式的方式存储和处理数据,实现了高可用性、可扩展性和高性能。
4.1. Elasticsearch节点概述
4.1.1 节点的基本概念
在Elasticsearch
中,节点(Node) 是一个运行中的Elasticsearch
实例。
它是集群的基本组成单元,负责存储数据、处理查询请求以及与其他节点进行通信。
每个节点都有一个唯一的标识符,用于在集群中进行识别和通信。
4.1.2 节点的类型划分
Elasticsearch
中的节点可以根据其功能和角色进行不同的划分,常见的节点类型包括:
4.1.2.1 主节点(Master Node)
主节点 负责管理整个集群的状态,包括节点的加入和离开、索引的创建和删除、分片的分配和迁移等。
主节点并不负责处理数据的存储和查询,它的主要职责是维护集群的元数据和协调各个节点的工作。
以下是一个简单的示例,展示如何在Elasticsearch的配置文件中设置节点为主节点:
node.name: master-node
node.master: true
node.data: false
在上述配置中,node.master
设置为 true
表示该节点可以作为候选主节点,node.data
设置为 false
表示该节点不存储数据。
这里设置的是候选主节点,真正的主节点通过选举产生,45岁老架构师尼恩提示, es 集群只能有一个 真正主节点。
4.1.2.2 数据节点(Data Node)
数据节点 负责存储和检索数据。它是集群中最核心的节点类型,负责处理文档的索引和查询操作。数据节点会将数据按照一定的规则进行分片存储,以实现数据的分布式存储和并行处理。
以下是一个设置数据节点的示例配置:
node.name: data-node
node.master: false
node.data: true
在这个配置中,node.master
设置为 false
表示该节点不作为主节点,node.data
设置为 true
表示该节点是数据节点。
4.1.2.3 协调节点(Coordinating Node)
协调节点 负责接收客户端的查询请求,并将请求转发到合适的数据节点进行处理。协调节点会收集各个数据节点的查询结果,并进行合并和排序,最终将结果返回给客户端。协调节点本身不存储数据,它的主要作用是协调和管理查询请求的处理过程。
以下是一个设置协调节点的示例配置:
node.name: coordinating-node
node.master: false
node.data: false
node.ingest: false
search.remote.connect: false
在这个配置中,除了将 node.master
和 node.data
设置为 false
外,还将 node.ingest
和 search.remote.connect
设置为 false
,以确保该节点只作为协调节点。
4.1.3 节点的通信机制
Elasticsearch
中的节点通过 TCP 协议进行通信。
节点之间会建立连接,通过发送和接收消息来进行数据的传输和交互。
节点之间的通信是基于请求-响应模型的,一个节点发送请求,另一个节点接收请求并返回响应。
为了提高通信效率和可靠性,Elasticsearch采用了一些优化措施,例如:
- 连接池:节点会维护一个连接池,用于管理与其他节点的连接。连接池可以复用已建立的连接,减少连接的建立和销毁开销。
- 消息压缩:在节点之间传输消息时,Elasticsearch会对消息进行压缩,以减少网络传输的数据量。
- 异步通信:节点之间的通信采用异步方式,发送请求后可以继续执行其他操作,不需要等待响应返回。
4.2 合理划分节点角色
4.2.1 主节点(Master):
主节点负责集群的管理和元数据存储,如索引的创建、删除、分片分配等操作。
为了确保集群的稳定性,应该选择性能稳定、网络连接良好的节点作为主节点。
一般建议将主节点与数据节点分离,避免主节点因处理大量的数据存储和查询任务而负载过重。
例如,在一个大规模的日志分析集群中,可以选择 3 - 5 个配置较高的节点作为master candidate 主节点候选节点,并且这些节点最好分布在不同的机架或数据中心,以防止单点故障。
同时,合理设置最小主节点,通过minimum_master_nodes 完成。
minimum_master_nodes 设置及其重要,为了防止集群脑裂,这个参数应该设置为法定个数就是 ( master 候选节点个数 / 2) + 1。
minimum_master_nodes的定义与作用
minimum_master_nodes
是 Elasticsearch 集群中的一个重要设置,用于定义在集群中可以正常选举主节点(Master)的最少候选主节点数量。
minimum_master_nodes 的主要目的是防止集群出现 “脑裂(split - brain)” 现象。
脑裂是指在集群中,由于网络分区或其他原因,导致多个节点分别认为自己是主节点,从而使集群分裂成多个独立的部分,这会导致数据不一致和集群的混乱。
通过设置minimum_master_nodes
,可以确保在选举主节点时,只有当足够数量的候选主节点能够相互通信时,才能成功选举出主节点,从而避免脑裂情况的发生。
计算minimum_master_nodes的合适值
计算minimum_master_nodes
的公式为:(master_eligible_nodes / 2) + 1
,
其中master_eligible_nodes
是集群中具有主节点选举资格的节点数量。
例如,在一个有 5 个具有主节点选举资格的节点的集群中,minimum_master_nodes
的值应该是(5 / 2) + 1 = 3
(向上取整)。
这意味着在进行主节点选举时,至少需要 3 个候选主节点能够相互通信,才能成功选举出主节点。
4.2.2 数据节点(Data)的配置:
数据节点主要负责存储和索引数据,它们会占用大量的磁盘空间和 I/O 资源。根据数据量和查询负载来合理分配数据节点的数量。
如果数据量增长迅速或者查询并发量较高,可以适当增加数据节点。
同时,要注意数据节点的硬件配置,如配备足够的内存来缓存索引数据,使用高性能的磁盘(如 SSD)来提高数据读写速度。
除此之前,数据节点(Data)的配置的配置,和数据规模有关。
预估存储一个亿文档的 Elasticsearch 数据节点数量需要综合多方面因素考虑,以下是一些分析:
第一,评估数据量与存储
单个文档大小估算:假设每个文档平均大小为 1KB,那么一亿个文档大约需要 100GB 的存储空间。如果考虑副本以及索引的额外开销,实际所需空间会更大。
节点存储容量规划:通常情况下,一个数据节点的磁盘容量建议不超过其总容量的 70% 至 80%,以预留一定空间用于数据的临时写入和系统文件存储等。如果使用普通机械硬盘,每个节点的存储容量可以控制在 2TB 至 3TB 左右;如果是 SSD 硬盘,可根据其性能和成本适当增加存储容量,但也不建议超过单节点 5TB 左右的存储量。
第二,规划分片与副本
分片数量:根据经验,分片大小保持在 10GB 至 50GB 之间较为合适。
对于一亿个文档( 100GB),若按每个分片 30GB 左右估算,大约需要 4 至 5 个主分片。
同时,还需要考虑副本数量,通常副本数量可以设置为 1 至 2 个,以提高数据的可用性和查询性能。
节点承载分片数:每个数据节点能够承载的分片数量也有限制,官方一般建议每 GB 堆内存对应少于 20 个分片。
其实,官方的这个建议,这个其实性能很低的。为什么呢 ?
按照官方建议,一个具有 32GB 堆内存的数据节点,最多可承载 640 个分片左右, 对应到的磁盘空间 640 *30G =19200G 。
假设缓存16G,对19200G 的数据进行缓存,当然 这么少的内存,这么大规模的数据,这个内存的缓存的作用,可以说聊胜于无,没啥命中率的。
如果估算数据节点数
怎么估算较好呢?
尼恩的经验, 可以按照 内存需求,反向计算节点数。
一般来说, 最好有磁盘空间的30%以上的内存空间,作为 堆内存。
当然,这个比例越高,性能越好。
比如有磁盘空间的50%以上的内存空间, es的性能越高。
那么, 一亿个文档( 100GB),加速副本最少 200G, 最好有60个G的内存。
假设一个节点 16G内存, 那么 需要 5个节点。
当然,如果有10个节点, 性能会更好。
4.2.3 协调节点(Coordinating)的配置:
协调节点负责接收用户查询请求,并将请求分发到合适的数据节点进行处理,然后汇总结果返回给用户。
协调节点需要有良好的网络和 CPU 性能,以快速地转发和处理查询。
在集群中,可以根据查询流量来设置协调节点的数量,一般可以按照一定的比例(如每 10 个数据节点配置 2 - 3 个协调节点)来配置。
4.3 ES 数据节点的写入调优
ES 数据写入具有一定的延时性,这是为了减少频繁的索引文件产生。另外,在搜索引擎的业务场景下,用户一般并不需要那么高的写入实时性。比如 在网站发布一条征婚信息,或者二手交易平台发布一个商品信息。其他人并不是马上能搜索到的,这其实也是正常的处理逻辑。
默认情况下 ES 每秒生成一个 segment 文件,当达到一定阈值的时候 会执行merge,merge 过程发生在 JVM中,频繁的生成 Segmen 文件可能会导致频繁的触发 FGC,导致 OOM。
为了避免避免这种情况,通常采取的手段是降低 segment 文件的生成频率,手段有两个
- 一个是 增加时间阈值,
- 另一个是增大 Buffer的空间阈值,因为缓冲区写满也会生成 Segment 文件。
生产经常面临的写入可以分为两种情况:
- 高频低量:高频的创建或更新索引或文档一般发生在 处理 C 端业务的场景下。
- 低频高量:一般情况为定期重建索引或批量更新文档数据。
4.3.1 写入优化的目标
写入调优均,常常以提升写入吞吐量和并发能力为目标,而非提升写入实时性。
4.3.2.增加 flush 时间间隔,
目的是减小数据写入磁盘的频率,减小磁盘IO频率。
在 Elasticsearch 中,flush
操作主要是将索引缓冲区(Index Buffer)中的数据刷新到磁盘上的索引文件中。
当新的数据写入 Elasticsearch 时,会先存储在内存的索引缓冲区中。flush
操作会将这些缓冲区中的数据持久化,以确保数据的安全性和可恢复性。
同时,flush
操作还会更新索引的相关元数据,如提交点(Commit Point)等。
通过配置文件修改flush时间间隔
在 Elasticsearch 的elasticsearch.yml
配置文件中,可以找到与flush
相关的参数。
主要参数是index.translog.flush_threshold_period
,它用于控制flush
的时间间隔。
这个参数的默认值是30s
,表示每隔 30 秒会自动进行一次flush
操作。
修改时间间隔要增加flush时间间隔,例如将其设置为60s,可以在elasticsearch.yml配置文件中添加或修改以下内容:
index.translog.flush_threshold_period: 60s
修改完成后,保存配置文件并重启 Elasticsearch 服务,新的flush
时间间隔就会生效。
尼恩提示,增加flush
时间间隔意味着数据在内存缓冲区中停留的时间更长。
如果在这段时间内发生服务器故障或 Elasticsearch 进程意外终止,可能会导致缓冲区中的数据丢失。
因此,在增加flush
时间间隔的同时,需要权衡数据安全性和性能之间的关系。
如果数据的可靠性要求极高,不建议过长地延长flush
时间间隔。
4.3.3 增加refresh_interval
的参数值
在 Elasticsearch 中,refresh_interval
是一个用于控制索引数据可见性的重要参数。它定义了索引数据被刷新(Refresh)的时间间隔。当新的数据写入索引后,并不是立即可以被搜索到,而是要等待索引被刷新。刷新操作会使新写入的数据对搜索可见。
具体来说,Elasticsearch 会在内存中维护一个数据结构来存储索引数据,这个数据结构在刷新之前是不断更新的。
refresh 刷新操作, 会将这个内存中的数据结构更新到磁盘上的索引文件(倒排索引等)中,并且更新相关的搜索视图,使得新数据能够被查询到。
增加refresh_interval
的参数值 目的,是减少segment文件的创建,减少segment的merge次数,merge是发生在jvm中的,有可能导致full GC,增加refresh会降低搜索的实时性。
ES的 refresh 行为非常昂贵,并且在正在进行的索引活动时经常调用,会降低索引速度。
refresh_interval的默认值和取值范围
默认值:默认的refresh_interval
是1s
。这意味着每隔 1 秒,索引会被刷新一次,新写入的数据会在 1 秒后对搜索可见。
取值范围:可以设置为一个时间值,如-1
(表示禁用自动刷新,需要手动刷新)、30s
、1m
等。时间单位可以是秒(s
)、分钟(m
)、小时(h
)等。
默认情况下,Elasticsearch 每秒(1s ) 定期刷新索引,但仅在最近 30 秒内收到一个或多个搜索请求的索引上。
如果没有搜索流量或搜索流量很少(例如每 5 分钟不到一个搜索请求), 并且想要优化索引速度,这是最佳配置。
此行为旨在在不执行搜索的默认情况下自动优化批量索引。建议显式配置此配置项,如 30秒。
如何修改refresh_interval? 通过配置文件修改,在elasticsearch.yml配置文件中,可以通过添加或修改以下内容来设置refresh_interval
index.refresh_interval: 30s
这会将所有新创建的索引的刷新间隔设置为 30 秒。
如果只想对特定的现有索引进行设置,可以使用索引设置 API。发送一个 PUT 请求到
http://<es - node - ip>:9200/<index - name>/_settings
,其中是要修改的索引名称。请求体如下:
{
"index": {
"refresh_interval": "30s"
}
}
这种方式可以在不重启 Elasticsearch 的情况下,动态地修改特定索引的刷新间隔。
延长refresh_interval
可以提高索引的写入性能。因为刷新操作是一个相对较重的操作,它涉及到内存数据结构到磁盘索引的更新以及搜索视图的更新。减少刷新频率可以减少磁盘 I/O 和 CPU 的消耗,使得更多的资源可以用于数据写入。例如,在大数据量的批量写入场景下,将refresh_interval
延长到 30 秒或更长,可以显著提高写入速度。
但是,延长refresh_interval
会导致新数据对搜索可见的延迟增加。如果应用场景对数据的实时性要求很高,如实时搜索系统,可能需要谨慎考虑延长刷新间隔的影响。
4.3.4 写入的时候, 增加Buffer大小
本质也是减小refresh的时间间隔,因为导致segment文件创建的原因不仅有时间阈值,还有buffer空间大小,写满了也会创建。
默认最小值 48MB< 默认值 JVM 空间的10% < 默认最大无限制
4.3.5 写入的时候, 关闭副本
当需要单次写入大量数据的时候,建议关闭副本,暂停搜索服务,或选择在检索请求量谷值区间时间段来完成。
- 第一是减小读写之间的资源抢占,读写分离
- 第二,当检索请求数量很少的时候,可以减少甚至完全删除副本分片,关闭segment的自动创建以达到高效利用内存的目的,因为副本的存在会导致主从之间频繁的进行数据同步,大大增加服务器的资源占用。
具体可通过则设置index.number_of_replicas
为0
以加快索引速度。
没有副本意味着丢失单个节点可能会导致数据丢失,因此数据保存在其他地方很重要,以便在出现问题时可以重试初始加载。
初始加载完成后,可以设置index.number_of_replicas
改回其原始值。
4.4 数据分片设置
4.4.1 数据分片的预估
ES一旦创建好索引后,就无法调整分片的设置,而在ES中,一个分片实际上对应一个lucene 索引,而lucene索引的读写会占用很多的系统资源,因此,分片数不能设置过大;
所以,在创建索引时,合理配置分片数是非常重要的。
一般来说,我们遵循一些原则:
控制每个分片占用的硬盘容量,不超过ES的最大JVM的堆空间设置(一般设置不超过32G,参加上文的JVM设置原则),
因此,如果索引的总容量在500G左右,那分片大小在16个左右即可。
一般都设置分片数不超过节点数的3倍。
考虑一下node数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,
一旦该节点故障,即使保持了1个以上的副本,同样有可能会导致数据丢失,集群无法恢复。
所以, 一般都设置分片数不超过节点数的3倍。
4.4.2 分片分配策略
ES使用数据分片(shard)来提高服务的可用性,将数据分散保存在不同的节点上以降低当单个节点发生故障时对数据完整性的影响,同时使用副本(repiica)来保证数据的完整性。
关于分片的默认分配策略,在7.x之前,默认5个primary shard,每个primary shard默认分配一个replica,即5主1副,而7.x之后,默认1主1副
ES的 分片分配策略,大致如下:
- ES在分配单个索引的分片时会将每个分片尽可能分配到更多的节点上。但是,实际情况取决于集群拥有的分片和索引的数量以及它们的大小,不一定总是能均匀地分布。
- Paimary只能在索引创建时配置数量,而replica可以在任何时间分配,并且primary支持读和写操作,而replica只支持客户端的读取操作,数据由es自动管理,从primary同步。
- ES不允许Primary和它的Replica放在同一个节点中,并且同一个节点不接受完全相同的两个Replica
- 同一个节点允许多个索引的分片同时存在。
4.4.3 分片的数量
避免分片过多:
大多数搜索会命中多个分片。
每个分片在单个 CPU 线程上运行搜索。
虽然分片可以运行多个并发搜索,但跨大量分片的搜索会耗尽节点的搜索线程池。这会导致低吞吐量和缓慢的搜索速度。
分片越少越好:
每个分片都使用内存和 CPU 资源。在大多数情况下,一小组大分片比许多小分片使用更少的资源。
4.4.4 分片的大小决策
分片的合理容量:10GB-50GB。
虽然不是硬性限制,但 10GB 到 50GB 之间的分片往往效果很好。
根据网络和用例,也许可以使用更大的分片。在索引的生命周期管理中,一般设置50GB为单个索引的最大阈值。
堆内存容量和分片数量的关联:
小于20分片/每GB堆内存,一个节点可以容纳的分片数量与节点的堆内存成正比。
例如,一个拥有 30GB 堆内存的节点最多应该有 600 个分片。
如果节点超过每 GB 20 个分片,考虑添加另一个节点。
5 ElasticSearch 索引层面的优化
5.1 避免使用 dynamic mapping
尽量避免使用 dynamic mapping,即不要让 Elasticsearch 自动根据新插入的数据来推测字段类型并创建映射。
而是在创建索引时,提前明确地定义好每个字段的类型、属性(如是否存储、是否索引、分词器等)。
这样可以避免因数据类型不一致导致的索引混乱和性能问题,同时也能更好地控制索引的结构和资源占用。
例如,在一个用户信息索引中,明确指定用户的年龄字段为integer
类型,姓名字段为text
类型并设置合适的分词器,而不是依赖 Elasticsearch 自动判断。
5.2 合理设置 doc_values 和 fielddata
doc_values定义与概念
doc_values 是 Elasticsearch 中一种用于存储正排索引的数据结构。
它是在索引构建阶段就被创建并存储在磁盘上的,主要用于快速地基于字段进行排序、聚合和脚本计算等操作。
当执行这些操作时,Elasticsearch 会从磁盘读取 doc_values 数据,而不需要重新解析原始的文档源(_source)数据,这大大提高了这些操作的效率。
doc_values工作原理
对于每个文档中的字段,doc_values 会以一种列式存储的方式记录其数据。
例如,对于一个包含 “姓名” 和 “年龄” 字段的文档集合,doc_values 会分别存储所有文档的 “姓名” 列和 “年龄” 列的数据。这种列式存储结构在进行排序和聚合操作时非常高效,因为它可以快速地遍历和处理同一列的数据。
当进行排序操作时,比如按照年龄从小到大排序,Elasticsearch 可以直接从 doc_values 中读取年龄列的数据,然后进行排序,而不需要逐个文档去解析获取年龄信息。
同样,在进行聚合操作(如计算年龄的平均值、统计不同姓名的数量等)时,doc_values 提供了一种高效的数据访问方式,使得这些操作能够快速完成。
doc_values适用场景和优势
- 排序操作:当需要对字段进行排序时,doc_values 的优势明显。例如,在电商产品搜索中,按照价格、销量等字段排序,或者在日志分析中按照时间戳排序,doc_values 能够快速提供排序所需的数据,提高排序效率。
- 聚合操作:对于统计计算(如求和、平均值、最大值、最小值等)和分组聚合(如统计不同类别产品的数量、不同时间段内日志的数量等),doc_values 是必不可少的。它允许 Elasticsearch 快速访问字段数据,而不需要在内存中重新构建数据结构。
- 脚本计算:在使用脚本进行数据处理时,如果脚本涉及到对字段的访问和操作,doc_values 可以提供高效的数据获取方式,提高脚本执行的速度。
关闭不需要的doc_values
对于不需要进行聚合操作的字段,在索引映射(Mapping)中设置"doc_values": false
,以节省磁盘空间和资源消耗,提高查询速度。
例如,对于一些仅用于存储但不参与聚合计算的日志时间戳字段,如果不需要对时间戳进行统计分析等聚合操作,就可以关闭其 doc_values。
当创建一个新的索引时,可以在索引映射中明确指定哪些字段不需要 doc_values。
例如,假设要创建一个名my_index的索引,其中包含title(文章标题)和content(文章内容)两个字段,且content字段不需要 doc_values,因为这个字段主要用于全文搜索,很少用于排序或聚合等操作。
可以使用以下的索引映射定义来关闭content字段的 doc_values:
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"doc_values": true
},
"content": {
"type": "text",
"doc_values": false
}
}
}
}
fielddata定义与概念
fielddata 是一种在内存中缓存的数据结构,主要用于支持对文本类型字段(text 类型)的聚合操作。
fielddata 与 doc_values 不同,fielddata 是在运行时(runtime)构建的,并且存储在 JVM 堆内存中。
fielddata 的目的是为了弥补 text 类型字段在默认情况下无法进行聚合操作的不足。
fielddata工作原理
当对 text 类型字段进行聚合操作时,Elasticsearch 会将该字段的所有词项(terms)加载到 fielddata 缓存中。
例如,对于一个包含产品描述的 text 字段,当需要统计不同关键词在产品描述中出现的频率时,Elasticsearch 会先将产品描述中的所有词汇加载到 fielddata 缓存中,然后进行统计计算。
fielddata 的构建是一个相对昂贵的过程,它需要对文本进行分词处理,并将分词后的结果存储在内存中。
而且,由于它存储在 JVM 堆内存中,对于大量的文本数据进行聚合操作时,可能会导致内存占用过大,甚至出现内存溢出(OOM)的情况。
谨慎使用 fielddata
谨慎使用 fielddata,因为它会占用大量的 JVM 堆内存空间。
如果确实需要对某个字段进行聚合操作,且该字段数据量较大,考虑提前规划好 JVM 内存设置,防止出现 OutOfMemoryError(OOM)。
同时,尽量避免在高并发或大数据量聚合场景下频繁使用 fielddata。
例如,对于一个电商商品搜索索引中的商品描述字段,如果只是偶尔进行简单的关键词搜索,而几乎不会对该字段进行聚合操作,那么就不要开启 fielddata。
创建新索引时关闭 fielddata
当创建新索引时,可以在映射中指定字段的属性来控制 fielddata 的开启或关闭。假设创建一个名为new_index
的索引,包含product_name(产品名称)和product_description(产品描述)两个字段,且不想为这些字段开启 fielddata(因为这些字段主要用于搜索,而非复杂的文本聚合),可以按照以下方式定义索引映射:
PUT /new_index
{
"mappings": {
"properties": {
"product_name": {
"type": "text",
"fielddata": false
},
"product_description": {
"type": "text",
"fielddata": false
}
}
}
}
启用特定聚合功能但不使用 fielddata
对于一些文本字段,如果需要进行简单的聚合操作,如统计不同词条的文档计数(这是一种比较常见的聚合场景),可以考虑使用keyword类型来代替text类型。
例如,对于一个category(产品类别)字段,如果只想统计不同类别产品的数量,将其定义为keyword类型而不是text类型,就不需要开启 fielddata 来进行聚合操作。
PUT /new_index
{
"mappings": {
"properties": {
"category": {
"type": "keyword",
"fielddata": false
}
}
}
}
5.3. 优化 ignore_above 设置
对于字符串类型的字段,根据业务需求合理设置ignore_above
参数。
该参数指定了字符串字段的最大长度,超过此长度的文本将被忽略。
将其设置得越小越好,但要确保不会丢失重要的业务数据。
例如,在一个商品评论索引中,如果业务只关心评论的前 100 个字符用于搜索和分析,那么可以将评论内容字段的ignore_above
设置为 100,这样可以减少索引的大小和资源消耗,同时提高查询性能。
5.4. 调整 _source 字段
通过_source
字段的include
和exclude
参数来精细控制哪些字段需要存储在_source
中。
只保留对业务查询和显示有必要的字段,避免存储过多不必要的字段数据,以节省磁盘空间和网络带宽。
例如,在一个新闻文章索引中,如果业务只需要在搜索结果中展示文章标题、发布时间和正文的前 200 个字符,那么可以使用"_source": {"includes": ["title", "publish_time", "body.substring(0,200)"]}
来配置_source
字段,排除其他不需要的字段。
5.5. 谨慎使用 store 属性
对于一些需要频繁访问但又不想每次都从_source
中提取的字段,可以考虑使用store
属性为其开辟单独的存储空间。
这样在查询时可以直接从存储的字段中获取数据,而无需解析_source
,从而节省网络带宽和查询时间。
但要注意,过多地使用store
属性会增加磁盘空间的占用,所以需要根据字段的访问频率和重要性进行权衡。
例如,对于一个经常需要在搜索结果中单独展示的商品图片 URL 字段,可以设置"store": true
,以便快速获取该字段的值。
5.6. 关于禁用 _source 字段的考虑
虽然禁用_source字段可以节省大量磁盘空间,但在决定禁用之前,必须充分考虑其带来的后果:
- 由于
update
、update_by_query
和reindex
操作都依赖于_source
字段,禁用后这些操作将不可用。如果业务可能需要对索引数据进行更新或重新索引,那么禁用_source
字段可能会带来不便。 - 禁用
_source
字段会导致高亮显示功能失效,因为高亮显示通常是基于_source
中的原始文本进行的。如果搜索结果需要高亮显示匹配的关键词,那么禁用_source
字段就不适合。 - 禁用
_source
字段会影响索引的容灾能力,因为在数据丢失或损坏的情况下,没有_source
字段将难以恢复原始数据。因此,除非对磁盘空间有极其严格的限制,并且确定上述功能在业务中几乎不会用到,否则不建议轻易禁用_source
字段。
5.7. 禁用 all 字段
在 Elasticsearch 6.0 及以上版本中,all
字段默认是关闭的。
如果使用的是早期版本,且确定不需要在搜索时从所有字段中进行模糊检索(即不使用未指定字段名的搜索),那么可以手动关闭all
字段,以节省磁盘空间和提高索引创建速度。
例如,在一个结构化数据索引中,每个字段的用途都很明确,很少会进行跨所有字段的模糊搜索,此时关闭all
字段是一个优化选择。
5.8. 关闭 Norms 字段
对于那些确定不需要用于计算文档相关性评分(如在过滤查询和聚合操作中使用的字段),在索引映射中设置"norms": false
。
这样可以节省大量的磁盘空间,尤其是对于那些包含大量文本的字段。
例如,在一个日志索引中,对于日志级别、IP 地址等字段,这些字段通常只用于过滤查询而不需要计算评分,将其norms
字段关闭可以优化索引性能。
5.9. 谨慎关闭 index_options(高端操作)
index_options
参数控制着在索引创建过程中哪些信息会被添加到倒排索引文件中,如词频(TF)、文档频率(docCount)、位置(postion)、偏移量(offsets)等。
减少这些选项可以降低索引创建时的 CPU 占用率,但需要谨慎操作。
因为在实际业务中,很难预先确定将来是否会用到这些信息。
除非在创建索引之前,对业务需求有非常清晰的了解,并且确定某些信息绝对不会被使用,否则不建议随意关闭index_options
中的选项,以免影响后续的查询功能和准确性。
5.10. 合理设置 enabled 属性
对于那些不需要进行查询操作的字段,在索引映射中设置"enabled": false
,以避免创建不必要的倒排索引,从而节省磁盘空间和资源消耗。
例如,在一个只用于数据存储和备份的索引中,某些仅用于记录内部标识或辅助信息的字段,如果不需要对其进行搜索查询,就可以将其设置为enabled
为false
。
在实施上述优化方案时,需要根据具体的业务需求、数据特点和硬件资源进行综合考虑和调整,并且在优化前后进行充分的性能测试,以确保优化措施能够有效地提高 Elasticsearch 索引的性能和资源利用率。同时,随着业务的发展和数据的变化,可能需要定期对索引进行重新评估和优化,以维持良好的性能表现。
5.11. max_result_window
参数
max_result_window是分页返回的最大数值,默认值为10000。
max_result_window本身是对JVM的一种保护机制,通过设定一个合理的阈值,避免初学者分页查询时由于单页数据过大而导致OOM。
在很多业务场景中经常需要查询10000条以后的数据,当遇到不能查询10000条以后的数据的问题之后,网上的很多答案会告诉你可以通过放开这个参数的限制,将其配置为100万,甚至1000万就行。
但是如果仅仅放开这个参数就行,那么这个参数限制的意义有何在呢?
如果你不知道这个参数的意义,很可能导致的后果就是频繁的发生OOM而且很难找到原因,设置一个合理的大小是需要通过你的各项指标参数来衡量确定的,比如你用户量、数据量、物理内存的大小、分片的数量等等。
通过监控数据和分析各项指标从而确定一个最佳值,并非越大越好
6 ElasticSearch查询层面的优化
避免使用稀疏数据
- 查询调优
- 索引时间精度优化
研究Filter的工作原理可以看出,它每次工作都是遍历整个索引的,所以时间粒度越大,对比越快,搜索时间越短,在不影响功能的情况下,时间精度越低越好,有时甚至牺牲一点精度也值得,当然最好的情况是根本不作时间限制。
es重新刷索引,增加冗余的时间字段,精确到天。带有时间范围的查询使用该字段进行查询
- 查询Fetch Source优化
业务查询语句获取的数据集比较大,并且从source中获取了非必须的字段,导致查询较慢。
举例:只需要从es中查询id这一个字段,却把所有字段查询了出来
- ‘数据预索引’查询优化
数据预索引查询优化是一种在Elasticsearch中通过预先处理和索引数据来提高查询性能的技术。这种方法的核心思想是识别查询中的常见模式,并根据这些模式来优化数据的索引方式,从而加快查询速度。
6.1:调整filter过滤顺序, 过滤优先原则
问题:
如果把全文查询(如
match
)的条件放在了前面,导致命中不了缓存,导致查询变慢。如果把过滤效果不明显的条件放在了前面,导致查询出大量不需要的数据,导致查询变慢。
在Elasticsearch中,查询优化的一个关键方面是正确使用过滤器(filters)和查询(queries)。
过滤器(如term
、range
等)通常比全文查询(如match
) 更快,为啥呢?
因为 filters 可以被缓存,而全文查询(如match
)则不可以被缓存。
因此,调整过滤器和查询的顺序,优先使用过滤器,可以显著提高查询性能。
以下是一个使用Elasticsearch查询DSL的例子,展示了如何通过调整filter
和query
的顺序来优化查询:
未优化的查询
假设我们有一个商品索引,包含商品的名称(name
)、价格(price
)和分类(category
)字段。
一个未优化的查询可能如下:
GET /products/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "apple"
}
}
],
"filter": [
{
"term": {
"category": "fruit"
}
},
{
"range": {
"price": {
"lte": 100
}
}
}
]
}
}
}
在这个查询中,我们首先使用match
查询来搜索名称中包含“apple”的商品,然后使用filter
来过滤属于“fruit”分类且价格不超过100的商品。
优化后的查询
如何优化这个查询?
可以将filter
条件移到must
子句中,因为filter
可以被缓存,这样可以减少需要检查的文档数量,从而提高查询效率:
GET /products/_search
{
"query": {
"bool": {
"must": [
{
"bool": {
"filter": [
{
"term": {
"category": "fruit"
}
},
{
"range": {
"price": {
"lte": 100
}
}
}
]
}
},
{
"match": {
"name": "apple"
}
}
]
}
}
}
在这个优化后的查询中,我们首先使用bool
查询的filter
子句来过滤出属于“fruit”分类且价格不超过100的商品,然后再使用match
查询来搜索名称中包含“apple”的商品。
这样,match
查询只需要在已经过滤过的文档集上运行,从而提高了查询效率。
通过这种方式,我们可以确保使用过滤器来减少搜索空间,然后再应用全文查询,这样可以显著提高查询性能,尤其是在处理大量数据时。
6.2 使用 Keyword 类型
ES中,并非所有数值数据都应映射为数值字段数据类型,
Elasticsearch为 数值数据提供了 查询优化 ,例如integer
、 long。 如果不需要范围查找,对于 term查询而言,keyword 比 integer 性能更好。
假设一个电商产品索引,其中包含产品 ID(product_id
)和产品价格(price
)两个字段。
产品 ID 是一个唯一标识每个产品的数字,产品价格是实际的价格数值。
我们将分别比较使用keyword
和integer
类型来存储产品 ID 在term
查询中的性能差异。
索引创建与数据插入,使用integer
类型存储产品 ID 并插入数据
PUT /products_index_integer_id
{
"mappings": {
"properties": {
"product_id": {
"type": "integer"
},
"price": {
"type": "float"
}
}
}
}
插入一些示例数据,假设有 10000 个产品,产品 ID 从 1 到 10000,价格随机生成在 10.0 到 1000.0 之间。
尼恩这里省略了过程, 大家可以自己设计一个脚本试一下。
使用keyword
类型存储产品 ID 并插入数据,创建另一个索引 products_index_keyword_id,将产品 ID 字段定义为keyword类型,产品价格字段同样定义为float类型。
PUT /products_index_keyword_id
{
"mappings": {
"properties": {
"product_id": {
"type": "keyword"
},
"price": {
"type": "float"
}
}
}
}
使用相同的方法插入 10000 个产品数据到这个索引中。
然后去做 精确匹配(term
查询) 的对比测试:
"query": {
"term": {
"product_id": 5000
}
}
在多次测试后,你可能会发现,对于term
查询产品 ID,使用keyword
类型的查询,比int 类型的查询,性能更好。
这是因为keyword
类型在存储和查询数据时,内部的数据结构更适合精确匹配(term
查询)。
integer
类型在 Elasticsearch 中是作为数值类型存储的,虽然它也支持term
查询,但在底层存储和检索机制上,对于这种精确匹配的term
查询,会有一些额外的处理开销,比如数值范围的校验等(尽管在这个例子中没有涉及范围查询)。
而keyword
类型就像一个简单的字符串存储结构,当进行term
查询时,可以直接通过字符串匹配的方式快速定位到对应的文档,减少了不必要的处理步骤,从而提高了查询性能。
keyword 和 integer 类型的两大不同:
第一个不同:keyword 和 integer 类型的 数据存储结构差异:
- keyword 类型:
当一个字段被定义为keyword
类型时,Elasticsearch 会将其视为一个简单的字符串进行存储。在底层,keyword
类型的数据存储类似于一个字典结构,其中每个唯一的字符串值都有一个对应的标识符。在进行term
查询时,Elasticsearch 直接在这个字典结构中查找给定的字符串,就像在一个键值对集合中查找键一样,这种查找方式非常直接和高效。
- integer 类型:
对于integer
类型,Elasticsearch 会将其作为数值进行存储。虽然它也支持term
查询,但由于其数值属性,在存储和检索过程中会涉及一些额外的处理。例如,它可能会考虑数值的范围、排序规则等因素,即使在进行简单的term
查询时,这些额外的机制也可能会增加查询的处理步骤。
第二个不同:索引构建和查询优化机制不同:
- keyword 类型:
keyword
类型在索引构建过程中主要关注的是字符串的精确匹配。它会为每个不同的字符串值构建一个简单而高效的索引结构,用于快速定位包含该字符串的文档。
在term
查询时,Elasticsearch 可以直接利用这个索引结构,通过字符串的哈希值或者其他快速查找方法来定位文档,减少了不必要的计算和比较。
- integer 类型:
对于integer
类型,索引构建过程可能会考虑到数值的各种特性,如大小排序、范围划分等。
在进行term
查询时,虽然最终也是要找到匹配的数值,但它可能需要经过一些额外的验证步骤,以确保查询的数值符合数值类型的规则。
例如,它可能会检查数值是否在合理的范围之内,这种额外的检查在term
查询这种只需要精确匹配的场景下会增加一定的开销。
6.3 避免使用脚本
Scripting是Elasticsearch支持的一种专门用于复杂场景下支持自定义编程的强大的脚本功能。
相对于 DSL 而言,脚本的性能更差,DSL能解决 80% 以上的查询需求,如非必须,尽量避免使用 Script
6.4 避免单次召回大量数据
搜索引擎最擅长的事情是从海量数据中查询少量相关文档,而非单次检索大量文档。
非常不建议动辄查询上万数据。如果有这样的需求,建议使用滚动查询
6.5 避免单个文档过大
鉴于默认http.max_content_length
设置为 100MB,Elasticsearch 将拒绝索引任何大于该值的文档。
您可能决定增加该特定设置,但 Lucene 仍然有大约 2GB 的限制。
即使不考虑硬性限制,大型文档通常也不实用。
大型文档对网络、内存使用和磁盘造成了更大的压力,即使对于不请求的搜索请求也是如此,_source
因为 Elasticsearch_id
在所有情况下都需要获取文档的文件系统缓存有效。
对该文档进行索引可能会占用文档原始大小的倍数的内存量。
Proximity Search(例如短语查询)和高亮查询也变得更加昂贵,因为它们的成本直接取决于原始文档的大小。
有时重新考虑信息单元应该是什么是有用的。
例如,您想让书籍可搜索的事实并不一定意味着文档应该包含整本书。
使用章节甚至段落作为文档可能是一个更好的主意,然后在这些文档中拥有一个属性来标识它们属于哪本书。这不仅避免了大文档的问题,还使搜索体验更好。
例如,如果用户搜索两个单词foo
and bar
,则不同章节之间的匹配可能很差,而同一段落中的匹配可能很好。
6.6 单次查询10条文档 好于 10次查询每次一条
批量请求将产生比单文档索引请求更好的性能。
但是每次查询多少文档最佳,不同的集群最佳值可能不同,为了获得批量请求的最佳阈值,建议在具有单个分片的单个节点上运行基准测试。
首先尝试一次索引 100 个文档,然后是 200 个,然后是 400 个等。
在每次基准测试运行中,批量请求中的文档数量翻倍。当索引速度开始趋于平稳时,就可以获得已达到数据批量请求的最佳大小。
在相同性能的情况下,当大量请求同时发送时,太大的批量请求可能会使集群承受内存压力,因此建议避免每个请求超过几十兆字节。
6.7 避免 Nested 内嵌 和 Join 连接
很多人会忽略对 Elasticsearch 数据建模的重要性。
一个特别要注意的是,应避免Nested 内嵌。nested属于object类型的一种,是Elasticsearch中用于复杂类型对象数组的索引操作。
Elasticsearch没有内部对象的概念,因此,ES在存储复杂类型的时候会把对象的复杂层次结果扁平化为一个键值对列表。
另外一个特别要注意的是,应避免连接。Nested 可以使查询慢几倍,Join 会使查询慢数百倍。
两种类型的使用场景应该是:Nested针对字段值为非基本数据类型的时候,而Join则用于 当子文档数量级非常大的时候。
6.8 使用filter代替query
query和filter的主要区别在: filter是结果导向的而query是过程导向。
query倾向于“当前文档和查询的语句的相关度”而filter倾向于“当前文档和查询的条件是不是相符”。
即在查询过程中,query是要对查询的每个结果计算相关性得分的,而filter不会。
另外filter有相应的缓存机制,可以提高查询效率。
6.9 避免深度分页
避免单页数据过大,可以参考百度或者淘宝的做法。
es提供两种解决方案 scroll search 和 search after。
关于深度分页的详细原理,推荐阅读:尼恩的技术自由圈的博客
ElasticSearch 深度分页 (史上最全、面试必备)_elasticsearch深度分页-CSDN博客
6.10 预索引: ‘数据预索引’查询优化
利用查询中的模式来优化数据的索引方式。
例如,如果所有文档都有一个price
字段,并且大多数查询 range 在固定的范围列表上运行聚合,可以通过将范围预先索引到索引中并使用聚合来加快聚合速度。
以下是一个基于电商系统中商品销售数据统计的案例,来展示 ElasticSearch 的 数据预索引 的优化方案。
案例背景
一个电商平台,平台上有大量的商品在售卖,每天都会产生众多的销售订单记录。
我们希望通过 ElasticSearch 对这些销售数据进行分析,以便了解不同年龄段用户的购买行为、商品销量情况等信息。
在业务需求中,经常需要按照用户的年龄范围来进行聚合查询,例如统计不同年龄区间(如 18 - 25 岁、26 - 35 岁、36 - 45 岁等)内购买各类商品的数量、金额等指标。
优化前的查询方式
优化前的数据结构:
销售订单数据以文档的形式存储在 ElasticSearch 索引中,每个文档代表一笔销售订单,包含了多个字段,比如
order_id
(订单编号)product_name
(商品名称)price
(商品价格)user_age
(用户年龄)purchase_date
(购买日期)等。
优化前的查询操作:
当业务人员想要查看不同年龄区间的商品销售总额时,通常会使用如下的聚合查询语句:
{
"size": 0,
"aggs": {
"age_ranges": {
"range": {
"field": "user_age",
"ranges": [
{
"from": 18,
"to": 25
},
{
"from": 26,
"to": 35
},
{
"from": 36,
"to": 45
}
]
},
"aggs": {
"total_sales": {
"sum": {
"field": "price"
}
}
}
}
}
}
上述查询语句的含义是,按照 user_age
字段进行范围划分,分成几个指定的年龄区间,
然后在每个年龄区间内对商品的价格进行求和聚合,以得到不同年龄区间的商品销售总额。
可能每天有成千上万笔订单, 随着订单数据量的不断增大,这样频繁地执行范围聚合查询会对 ElasticSearch 集群的性能造成巨大压力
结果是:查询响应时间可能会逐渐变长,影响业务人员获取数据的效率。
预索引数据查询优化方案
预聚合数据设计
我们决定在数据写入 ElasticSearch 索引时,就提前对用户年龄进行预聚合处理。
新增一个字段,比如叫 age_group
age_group 的值根据 user_age 字段预先划分好的区间来确定。
例如:
- 当
user_age
在 18 - 25 岁之间时,age_group
的值设为18-25
。 - 当
user_age
在 26 - 35 岁之间时,age_group
的值设为26-35
, - 以此类推。
这样,在数据索引阶段,在将订单数据导入 ElasticSearch 时,添加一段逻辑来判断 user_age
的值,并设置对应的 age_group
值。
比如,我们可以通过 Logstash(或者其他 ETL 工具)在将订单数据导入 ,示例 Logstash 配置片段如下:
filter {
if [user_age] >= 18 && [user_age] <= 25 {
mutate {
add_field => {
"age_group" => "18-25" }
}
} else if [user_age] > 25 && [user_age] <= 35 {
mutate {
add_field => {
"age_group" => "26-35" }
}
}
# 其他年龄区间的判断逻辑依次添加
}
优化后的查询操作:
现在,当业务人员想要查询不同年龄区间的商品销售总额时,就可以使用基于 age_group
字段的更简单高效的聚合查询语句:
{
"size": 0,
"aggs": {
"age_groups": {
"terms": {
"field": "age_group",
"size": 10
},
"aggs": {
"total_sales": {
"sum": {
"field": "price"
}
}
}
}
}
}
这个查询语句通过对 age_group
字段进行 terms
聚合(也就是按照不同的年龄分组值进行分组统计),然后在每个分组内对商品价格进行求和聚合,来获取不同年龄区间的销售总额。
优化效果对比
- 性能提升:
在未进行预索引数据查询优化之前,对于千万级别的订单数据进行年龄范围聚合查询,平均响应时间可能在 5 - 10 秒左右,随着数据量继续增加,响应时间还会进一步拉长。而经过优化后,同样的数据量下,查询响应时间可以缩短到 1 - 2 秒以内,大大提高了业务人员获取数据进行分析的效率。 - 资源利用优化:
从 ElasticSearch 集群的资源角度来看,未优化时,复杂的范围聚合查询需要占用较多的 CPU 资源来进行实时的年龄区间划分和数据计算。优化后,由于是基于预聚合的字段进行简单的分组统计,对 CPU 的消耗大幅降低,同时也减少了内存中临时数据的占用,使得集群整体可以更高效地处理更多的查询请求,提升了集群的资源利用率。
通过这个电商销售数据统计的案例可以清晰地看到,对于业务中频繁出现的特定范围聚合查询需求,采用预索引数据查询优化的方式,提前对相关数据进行处理和聚合,可以显著提升 ElasticSearch 的查询性能,更好地满足业务分析需求。
7 职责分离,全面监控
当某个单一角色所负责的业务,或某个单一节点无法满足其业务需要的时候,最好的策略就是 解耦和分离, 常见的操作如:
- 功能分离:本质上也利于服务的轻量化, 百度 搜索引擎的内部人员 聊过 ,百度内部就是基于 Elasticsearch 的源码做修改,删除对其业务不需要的代码,对 ES做轻量化处理
- 业务分离:最简单常见的场景,当我在全文检索服务的时候,应避免在服务期间去执行大量的聚合分析。
- 读写分离:常见操作,不做过多解释
- 冷热分离:基于索引生命周期管理策略下的性能动态分配策略。
另外, 为了更高的性能, 需要进行密切的、全面的指标监控:
- 定期监控集群的健康状况和资源使用情况,以便及时做出调整。
- 监控线程池的情况,进行在线的扩容
- 根据实际需求调整索引设置,比如分片数、副本数等。
- 考虑使用冷热架构,将活跃数据放在高性能节点上,历史数据则可以迁移到成本更低、性能稍弱的节点上。
通过上述措施,可以有效地优化Elasticsearch集群在硬件层面的性能。
说在最后:有问题找老架构取经
回到开始的时候的面试题:
- 如何 使用ZSET 排序统计 ?
- 亿级用户的排行榜,如何设计?
按照此文的套路去回答,一定会 吊打面试官,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。
很多小伙伴刷完后, 吊打面试官, 大厂横着走。
在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。前段时间,刚指导一个小伙 暴涨200%(2倍),29岁/7年/双非一本 , 从13K 涨到 37K ,逆天改命。
狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。
尼恩技术圣经系列PDF
- 《NIO圣经:一次穿透NIO、Selector、Epoll底层原理》
- 《Docker圣经:大白话说Docker底层原理,6W字实现Docker自由》
- 《K8S学习圣经:大白话说K8S底层原理,14W字实现K8S自由》
- 《SpringCloud Alibaba 学习圣经,10万字实现SpringCloud 自由》
- 《大数据HBase学习圣经:一本书实现HBase学习自由》
- 《大数据Flink学习圣经:一本书实现大数据Flink自由》
- 《响应式圣经:10W字,实现Spring响应式编程自由》
- 《Go学习圣经:Go语言实现高并发CRUD业务开发》
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓