ElasticSearch知识图谱
2021-07-11 22:14:19 2 举报
AI智能生成
ElasticSearch知识图谱
作者其他创作
大纲/内容
基础知识
基本概念和原理<br>
Elasticsearch是<font color="#ff0000">实时</font>的<font color="#ff0000">分布式</font>搜索分析引擎,内部使用Lucene做索引与搜索
Lucene
索引结构
ES是<font color="#ff0000">面向文档</font>的。各种文本内容以文档的形式存储到ES中,一般使用 JSON
在存储结构上,由_index、_type和_id唯一标识一个文档
分片
存储需求
数据安全,可用性
主分片(primary shard)和副分片(replica shard)<br>
数据分片和数据副本之间的关系
索引与分片的关系
经验分享
动态更新索引
近实时搜索
在写操作中,一般会先在内存中缓冲一段数据,再将这些数据写入硬盘,每次写入硬盘的这批数据称为一个分段,如同任何写操作一样。
ES利用操作系统的特性实现近实时搜索(见备注),每秒产生一个新分段,<font color="#ff0000">新段先写入文件系统缓存</font>,但稍后再执行flush刷盘操作,写操作很快会执行完,一旦写成功,就可以像其他文件一样被打开和读取了。
由于系统先缓冲一段数据才写,且新段不会立即刷入磁盘,这两个过程中如果出现某些意外情况(如主机断电),则会存在丢失数据的风险。通用的做法是<font color="#ff0000">记录事务日志</font>,每次对ES进行操作时均记录事务日志,当ES启动的时候,重放translog中所有在最后一次提交后发生的变更操作
段合并<br>
在ES中,每秒清空一次写缓冲,将这些数据写入文件,这个过程称为refresh,每次refresh会创建一个新的Lucene 段
分段数量太多会带来较大的麻烦,每个段都会消耗文件句柄、内存
每个搜索请求都需要轮流检查每个段,查询完再对结果进行合并;所以段越多,搜索也就越慢。<br>
因此需要通过一定的策略将这些较小的段合并为大的段,常用的方案是选择大小相似的分段进行合并。在合并过程中,标记为删除的数据不会写入新分段,当合并过程结束,旧的分段数据被删除,标记删除的数据才从磁盘删除。<br>
集群内部原理
集群节点角色
主节点(Master node)
数据节点(Data node)
预处理节点(Ingest node)
协调节点(Coordinating node)
部落节点(Tribe node)
集群健康状况
Green
所有的主分片和副分片都正常运行。
Yellow
所有的主分片都正常运行,但不是所有的副分片都正常<br>运行。这意味着存在单点故障风险
Red
有主分片没能正常运行。
集群状态
集群状态元数据是全局信息,元数据包括内容路由信息、配置信息等,其中最重要的是内容路由信息,它描述了“哪个分片位于哪个节<br>点”这种信息
集群状态由主节点负责维护,如果主节点从数据节点接收更新,则将这些更新广播到集群的其他节点,让每个节点上的集群状态保持最<br>新。ES 2.0版本之后,更新的集群状态信息只发增量内容,并且是被压缩的。
集群扩容
系统自动完成
分片副本实现了数据冗余,从而防止硬件故障导致的数据丢失
演示
起初,在NODE1上有三个主分片,没有副分片
初始状态<br>
添加第二个节点后,副分片被分配到NODE2
副本分片分配
添加第三个节点后,索引的六个分片被平均分配到集群的三个节点
分片平均分配
客户端API
主要的内部模块
Cluster
Cluster模块是主节点执行集群管理的封装实现,管理集群状态,维护集群层面的配置信息。
allocation<br>
封装了分片分配相关的功能和策略,包括主分片的分配和副分片的<br>分配,本模块由主节点调用。创建新索引、集群完全重启都需要分片分<br>配的过程。
Discovery<br>
发现模块负责发现集群中的节点,以及选举主节点。当节点加入或<br>退出集群时,主节点会采取相应的行动。从某种角度来说,发现模块起<br>到类似ZooKeeper的作用,选主并管理集群拓扑。
gateway<br>
负责对收到Master广播下来的集群状态(cluster state)数据的持久<br>化存储,并在集群完全重启时恢复它们。<br>
Indices<br>
索引模块管理全局级的索引设置,不包括索引级的(索引设置分为全局级和每个索引级)。 它还封装了索引数据恢复功能。集群启动阶段需要的主分片恢复和副分片恢复就是在这个模块实现的。
HTTP<br>
HTTP模块允许通过JSON over HTTP的方式访问ES的API,HTTP模块<br>本质上是完全异步的,这意味着没有阻塞线程等待响应。使用异步通信<br>进行 HTTP 的好处是解决了 C10k 问题(10k量级的并发连接)。
Transport
传输模块用于集群内节点之间的内部通信。从一个节点到另一个节点的每个请求都使用传输模块。<br>如同HTTP模块,传输模块本质上也是完全异步的。<br>传输模块使用 TCP 通信,每个节点都与其他节点维持若干 TCP 长连接。 内部节点间的所有通信都是本模块承载的。
Engine<br>
Engine模块封装了对Lucene的操作及translog的调用,它是对一个分<br>片读写操作的最终提供者。<br>ES使用Guice框架进行模块化管理。Guice是Google开发的轻量级依<br>赖注入框架(IoC)
模块结构
在Guice框架下,一个典型的模块由Service和Module类(类名可以<br>自由定义)组成,Service用于实现业务功能,Module类中配置绑定信<br>息。
以ClusterModule为例,类结构如下图所示
AbstractModule是Guice提供的基类,模块需要从这个类继承。
Module类的主要作用是定义绑定关系,例如 protected void configure() {<br> //绑定实现类<br> bind(ClusterService.class).toInstance(clusterService);<br>}
定义好的模块由ModulesBuilder类统一管理,ModulesBuilder是ES对<br>Guice的封装,内部调用Guice接口,主要对外提供两个方法<br>
add方法:添加创建好的模块
createInjector方法:调用Guice.createInjector创建并返回Injector,<br>后续通过Injector获取相应Service类的实例
使用ModulesBuilder进行模块管理的伪代码示例:<br>ModulesBuilder modules = new ModulesBuilder();<br>//以Cluster模块为例<br>ClusterModule clusterModule = new ClusterModule();<br>modules.add(clusterModule);<br>//省略其他模块的创建和添加<br>...<br>//创建Injector,并获取相应类的实例<br>injector = modules.createInjector();<br>setGatewayAllocator(injector.getInstance(GatewayAllocator.class)
模块化的封装让 ES 易于扩展,插件本身也是一个模块,节点启动<br>时被模块管理器添加进来
编译和调试环境 TODO
编译源码
JDK+Gradle
下载源代码
编译 打包
导入IDEA
调试ES
本地运行调试项目
远程调试
ES 的主要流程
集群节点启动流程
选举主节点
(1)参选人数需要过半,达到quorum(多数)后就选出了临时的主
(2)得票数需过半。
(3)当探测到节点离开事件时,必须判断当前节点数是否过半。<br>如果达不到quorum,则放弃Master身份,重新加入集群。
选举集群源信息<br>
被选出的Master和集群元信息的新旧程度没有关系。因此它的第<br>一个任务是选举元信息,让各节点把各自存储的元信息发过来,<font color="#ff0000">根据版<br>本号确定最新的元信息</font>,然后把这个信息广播下去,这样集群的所有节<br>点都有了最新的元信息。
集群元信息的选举包括两个级别:<font color="#ff0000">集群级和索引级</font>。不包含哪个<br>shard存于哪个节点这种信息。这种信息以节点磁盘存储的为准,需要上<br>报。为什么呢?因为读写流程是不经过Master的,Master不知道各<br>shard副本直接的数据差异。
为了集群一致性,参与选举的元信息<font color="#ff0000">数量需要过半</font>,Master发布集<br>群状态成功的规则也是等待发布成功的节点数过半。
在选举过程中,不接受新节点的加入请求。
集群元信息选举完毕后,Master发布首次集群状态,然后开始选举<br>shard级元信息。
allocation过程
1.选主分片
现在看某个主分片[website][0]是怎么分配的。所有的分配工作都是Master来做的,此时,Master不知道主分片在哪,它向集群的所有节点询问
大家把[website][0]分片的元信息发给我。然后,Master等待所有<br>的请求返回,正常情况下它就有了这个shard的信息,然后根据某种策<br>略选一个分片作为主分片。<br>
<font color="#ff0000">是不是效率有些低?这种询问量=shard数×<br>节点数。所以说我们最好控制shard的总规模别太大。</font>
现在有了shard[website][0]的分片的多份信息,具体数量取决于副本<br>数设置了多少。现在考虑把哪个分片作为主分片。<br>
ES5.x以下的版本,通过对比shard级元信息的版本号来决定。在多副本的情况下,考虑到如<br>果只有一个shard信息汇报上来,则它一定会被选为主分片,但也许数<br>据不是最新的,版本号比它大的那个shard所在节点还没启动。<br>
在解决这个问题的时候,ES5.x开始实施一种新的策略:给每个shard都设置一<br>个UUID,然后在集群级的元信息中记录哪个shard是最新的,因为ES是<br>先写主分片,再由主分片节点转发请求去写副分片,所以主分片所在节<br>点肯定是最新的,如果它转发失败了,则要求Master删除那个节点。
所以,从ES5.x开始,主分片选举过程是通过集群级元信息中记录的“最新<br>主分片的列表”来确定主分片的:汇报信息中存在,并且这个列表中也<br>存在。<br>
如果集群设置了:"cluster.routing.allocation.enable":"none"禁止分配分片,集群仍会强制分配主分片。因此,在设置了上述选项的情况下,集群重启后的状态为Yellow,而非Red。
2.选副分片<br>
主分片选举完成后,从上一个过程汇总的shard信息中选择一个副本作为副分片。如果汇总信息中不存在,则分配一个全新副本的操作依赖于延迟配置项:<br>index.unassigned.node_left.delayed_timeout<br>我们的线上环境中最大的集群有100+节点,掉节点的情况并不罕见,很多时候不能第一时间处理,这个延迟我们一般配置为以天为单位。
3. 最后,allocation过程中允许新启动的节点加入集群。
Index Recovery<br>
<b>1.主分片recovery</b><br>
由于每次写操作都会记录事务日志(<font color="#ff0000">translog</font>),事务日志中记录<br>了哪种操作,以及相关的数据。因此<font color="#ff0000">将最后一次提交(Lucene的一次提<br>交就是一次fsync刷盘的过程)之后的translog中进行重放,建立Lucene<br>索引</font>,如此完成主分片的recovery。
<b>2.副分片recovery</b>
副分片需要恢复成与主分片一致,同时,恢复期间允许新的索引操<br>作。在目前的6.0版本中,恢复分成两阶段执行
phase1:在主分片所在节点,获取translog保留锁,从获取保留锁<br>开始,会保留translog不受其刷盘清空的影响。 然后调用Lucene接口把<br>shard做快照,这是已经刷磁盘中的分片数据。把这些shard数据复制到<br>副本节点。在phase1完毕前,会向副分片节点发送告知对方启动<br>engine,在phase2开始之前,副分片就可以正常处理写请求了。
phase2:对translog做快照,这个快照里包含从phase1开始,到执<br>行translog快照期间的新增索引。将这些translog发送到副分片所在节点<br>进行重放。
集群启动日志
日志
总结
当一个索引的主分片分配成功后,到此分片的写操作就是允许的。<br>当一个索引所有的主分片都分配成功后,该索引变为Yellow。当全部索<br>引的主分片都分配成功后,整个集群变为Yellow。当一个索引全部分片<br>分配成功后,该索引变为Green。当全部索引的索引分片分配成功后,<br>整个集群变为Green<br>
索引数据恢复是最漫长的过程。当shard总量达到十万级的时候,<br>6.x之前的版本集群从Red变为Green的时间可能需要小时级。ES6.x中的<br>副本允许从本地translog恢复是一次重大的改进,避免了从主分片所在<br>节点拉取全量数据,为恢复过程节约了大量时间。
节点的启动和关闭
启动流程做了什么
解析配置,包括配置文件和命令行参数。
检查外部环境和内部环境,例如,JVM版本、操作系统内核参数等。
初始化内部资源,创建内部模块,初始化探测器。
启动各个子模块和keepalive线程。
启动流程分析
启动脚本
当我们通过启动脚本bin/elasticsearch启动ES时,脚本通过exec加载Java程序
shell
ES_JAVA_OPTS变量保存了JVM参数,其内容来自对<br>config/jvm.options配置文件的解析。<br>如果执行启动脚本时添加了-d参数:<br>bin/elasticsearch–d<br>则启动脚本会在exec中添加<&-&。<&-的作用是关闭标准输入,即进程中的0号fd。&的作用是让进程在后台运行。
解析命令行参数和配置文件
目前支持的命令行参数有下面几种,默认启动时都不使用,如下表所示。
实际工程应用中建议在启动参数中添加-d和-p,例如:<br>bin/elasticsearch-d-pes.pid<br>此处解析的配置文件有下面两个,jvm.options是在启动脚本中解析<br>的。<br>elasticsearch.yml#主要配置文件<br>log4j2.properties#日志配置文件
加载安全配置
检查内部环境
内部环境指ES软件包本身的完整性和正确性。包括:<br>
·检查Lucene版本,ES各版本对使用的Lucene版本是有要求的,在这里检查Lucene版本以防止有人替换不兼容的jar包。
·检测jar冲突(JarHell),发现冲突则退出进程。
检测外部环境<br>
外部环境指运行时的JVM、操作系统相关参数,这些在ES中称<br>为“<b><font color="#ff0000">BootstrapCheck</font></b>”。
在早期的ES版本中,ES检测到一些不合理的配置会记录到日志中继续运行。但是有时候用户会错过这些日志。为了避免后期才发现问题,ES在启动阶段对那些很重要的参数做检查,<font color="#ff0000"><b>一些影响性能的配置会被标记为错误,让用户足够重视这些参数</b></font>。
所有这些检查被单独封装在BootstrapChecks类中。目前有下面这些<br>检测项
1.堆大小检查
如果JVM初始堆大小(Xms)与最大堆大小(Xmx)的值不同,则<br>使用期间JVM堆大小调整时可能会出现停顿。因此应该设置为相同值。<br>如果开启了bootstrap.memory_lock,则JVM将在启动时锁定堆的初<br>始大小。如果初始堆大小与最大堆大小不同,那么在堆大小发生变化<br>后,可能无法保证所有JVM堆都锁定在内存中。<br>要通过本项检查,就必须配置堆大小。
2.文件描述符检查
UNIX架构的系统中,“文件”可以是普通的物理文件,也可以是虚<br>拟文件,网络套接字也是文件描述符。ES进程需要非常多的文件描述<br>符。例如,每个分片有很多段,每个段都有很多文件。同时包括许多与<br>其他节点的网络连接等。
要通过此项检查,就需要调整系统的默认配置,在Linux下,执行<br>ulimit-n65536(只对当前终端生效),或者在/etc/security/limits.conf文<br>件中配置“*-nofile65536”(所有用户永久生效)。Ubuntu下limits.conf<br>默认被忽略,需要开启pam_limits.so模块。<br>
由于Ubuntu版本更新比较快,而生产环境不适合频繁更新,因此我们推荐使用CentOS作为服务器操作系统。
3.内存锁定检查
ES允许进程只使用物理内存,避免使用交换分区。实际上,我们建<br>议生产环境中直接禁用操作系统的交换分区。现在已经不是因为内存不<br>足而需要交换到硬盘上的时代,对于服务器来说,当内存真的用完时,<br>交换到硬盘上会引起更多问题。<br>
开启bootstrap.memory_lock选项来让ES锁定内存,在开启本项检查,而锁定失败的情况下,本项检查执行失败。
4.最大线程数检查
ES将请求分解为多个阶段执行,每个阶段使用不同的线程池来执<br>行。因此ES进程需要创建很多线程,本项检查就是确保ES进程有创建<br>足够多线程的权限。本项检查只对Linux系统进行。你需要调节进程可<br>以创建的最大线程数,这个值至少是2048。<br>
要通过这项检查,可以修改/etc/security/limits.conf文件的nproc来完成配置。
5. 最大虚拟内存检查
Lucene使用mmap来映射部分索引到进程地址空间,最大虚拟内存<br>检查确保ES进程拥有足够多的地址空间,这项检查只对Linux执行。
要通过这项检查,可以修改/etc/security/limits.conf文件,设置as为<br>unlimited
6.最大文件大小检查
段文件和事务日志文件存储在本地磁盘中,它们可能会非常大,在<br>有最大文件大小限制的操作系统中,可能会导致写入失败。建议将最大<br>文件的大小设置为无限。<br>
要通过这项检查,可以修改/etc/security/limits.conf文件,修改fsize<br>为unlimited。
7.虚拟内存区域最大数量检查
ES进程需要创建很多内存映射区,本项检查是要确保内核允许创建<br>至少262144个内存映射区。该检查只对Linux执行。
要通过这项检查,可以执行下面的命令(临时生效,重启后失<br>效):<br>sysctl-wvm.max_map_count=262144<br>或者在/etc/sysctl.conf文件中添加一行vm.max_map_count=262144,<br>然后执行下面的命令(立即,且永久生效)<br>sysctl–p
8.JVMClient模式检查
OpenJDK提供了两种JVM的运行模式:clientJVM模式与serverJVM<br>模式。clientJVM调优了启动时间和内存消耗,serverJVM提供了更高的<br>性能。要想通过此检查,需要以server的方式来启动ES,这也是默认<br>的。
9. 串行收集检查
串行收集器(serialcollector)适合单逻辑CPU的机器或非常小的<br>堆,不适合ES。使用串行收集器对ES有非常大的负面影响。本项检查<br>就是确保没有使用串行收集器。ES默认使用CMS收集器
10.系统调用过滤器检查
根据不同的操作系统,ES安装各种不同的系统调用过滤器(在<br>Linux下使用seccomp)。这些过滤器可以阻止一些攻击行为。
作为一个服务端进程,当由于某些系统漏洞被攻击者取得进程的权<br>限时,攻击者可以使用启动当前进程的用户权限执行一些操作。首先,<br>以普通用户权限启动进程可以降低安全风险。其次,把服务本身不需要<br>的系统调用通过过滤器关闭,当进程被攻击者取得权限时,进一步的权<br>限提升等行为会增加攻击难度(例如,创建子进程执行其他程序,获得<br>一个shell等)。这样被攻击的损失仅限于当前进程,而不是整个操作<br>系统及其他数据。
要通过此项检查,可能需要解决过滤器安装期间遇到的错误,或者<br>通过下面的设置来关闭系统调用过滤器:<br>bootstrap.system_call_filter:false
11.OnError与OnOutOfMemoryError检查
如果JVM遇到致命错误(OnError)或<br>OutOfMemoryError(OnOutOfMemoryError),那么JVM选项OnError和<br>OnOutOfMemoryError可以执行任意命令。
但是,默认情况下,ES的系统调用过滤器是启用的<br>(seccomp),fork会被阻止。因此,使用OnError或OnOutOfMemoryError<br>和系统调用过滤器不兼容。
若要通过此项检查,则不要启用OnError或OnOutOfMemoryError,<br>而是升级到Java8u92并使用ExitOnOutOfMemoryError。
12.Early-access检查
OpenJDK为即将发布的版本提供了early-access快照,这些发行版<br>不适合生产环境。若要通过此项检查,则需要让ES运行在JVM的稳定<br>版。
13.G1GC检查
JDK8的早期版本有些问题,会导致索引损坏,JDK8u40之前的版<br>本都会受影响。本项检查验证是否是早期的HotSpotJVM版本
启动内部模块
环境检查完毕,开始启动各子模块。子模块在Node类中创建,启动<br>它们时调用各自的start()方法<br>
例如:<br>discovery.start();<br>clusterService.start();<br>nodeConnectionsService.start();<br>子模块的start方法基本就是初始化内部数据、创建线程池、启动线<br>程池等操作。
启动keepalive线程
调用keepAliveThread.start()方法启动keepalive线程,线程本身不<br>做具体的工作。主线程执行完启动流程后会退出,keepalive线程是唯一<br>的用户线程,作用是保持进程运行。在Java程序中,至少要有一个用户<br>线程。当用户线程数为零时退出进程。
节点关闭流程
进程重启期间,如果主节点被关闭,则集群会重新选主,在这期<br>间,集群有一个短暂的无主状态。
如果集群中的主节点是单独部署的,<br>则新主当选后,可以跳过gateway和recovery流程,否则新主需要重新分<br>配旧主所持有的分片:提升其他副本为主分片,以及分配新的副分片
如果数据节点被关闭,则读写请求的TCP连接也会因此关闭,对客<br>户端来说写操作执行失败。但写流程已经到达Engine环节的会正常写<br>完,只是客户端无法感知结果。此时客户端重试,如果使用自动生成<br>ID,则数据内容会重复
综合来说,滚动升级产生的影响是中断当前写请求,以及主节点重<br>启可能引起的分片分配过程。提升新的主分片一般都比较快,因此对集<br>群的写入可用性影响不大。
当索引部分主分片未分配时,使用自动生成ID的情况下,如果持续<br>写入,则客户端对失败重试可能会成功(请求到达已分配成功的主分<br>片),但是会在不同的分片之间产生数据倾斜,倾斜程度视期间数量而<br>定。
关闭流程分析
在节点启动过程中,Bootstrap#setup方法中添加了shutdownhook,<br>当进程收到系统SIGTERM(kill命令默认信号)或SIGINT信号时,调用<br>Node#close方法,执行节点关闭流程。
每个模块的Service中都实现了doStop和doClose,用于处理这个模块<br>的正常关闭流程。节点总的关闭流程位于Node#close,在close方法的实<br>现中,先调用一遍各个模块的doStop,然后再次遍历各个模块执行<br>doClose。主要实现代码如下:
子主题
各模块的关闭有一定的顺序关系,以doStop为例,按下表所示的<br>顺序调用各模块doStop方法。
子主题
综合来看,关闭顺序大致如下:<br>·关闭快照和HTTPServer,不再响应用户REST请求。<br>·关闭集群拓扑管理,不再响应ping请求。<br>·关闭网络模块,让节点离线。<br>·执行各个插件的关闭流程。<br>·关闭IndicesService。<br>最后才关闭IndicesService,是因为这期间需要等待释放的资源最多,时间最长。
分片读写过程中执行关闭
写入过程中关闭<br>
线程在写入数据时,会对Engine加写锁。<br>IndicesService的doStop方法对本节点上全部索引并行执行removeIndex,<br>当执行到Engine的flushAndClose(先flush然后关闭Engine),也会对<br>Engine加写锁。由于写入操作已经加了写锁,此时写锁会等待,直到写<br>入执行完毕。因此数据写入过程不会被中断。但是由于网络模块被关<br>闭,客户端的连接会被断开。客户端应当作为失败处理,虽然ES服务端<br>的写流程还在继续。
读取过程中关闭
线程在读取数据时,会对Engine加读锁。<br>flushAndClose时的写锁会等待读取过程执行完毕。但是由于连接被关<br>闭,无法发送给客户端,导致客户端读失败。
下图展示了Engine的flushAndClose过程<br>
节点关闭过程中,IndicesService的doStop对Engine设置了超时,如<br>果flushAndClose一直等待,则CountDownLatch.await默认1天才会继续后<br>面的流程。<br>
主节点被关闭
主节点被关闭时,没有想象中的特殊处理,节点正常执行关闭流<br>程,当TransportService模块被关闭后,集群重新选举新Master。因此,<br>滚动重启期间会有一段时间处于无主状态。
小结
(1)总体来说,节点启动流程做的就是初始化和检查工作,各个<br>子模块启动后异步地工作,加载本地数据,或者选主、加入集群等<br>
(2)节点在关闭时有机会处理未写完的数据,但是写完后可能来<br>不及通知客户端。包括线程池中尚未执行的任务,在一定的超时时间内<br>都有机会执行完。<br>
集群健康从Red变为Green的时间主要消耗在维护主副分片的一致性<br>上。我们也可以选择在集群健康为Yellow时就允许客户端写入,但是会<br>牺牲一些数据安全性。
选主流程<br>
设计思想
所有分布式系统都需要以某种方式处理一致性问题。一般情况下,<br>可以将策略分为两组:<font color="#ff0000"><b>试图避免不一致</b></font>及定义发生不一致之后如何协调<br>它们。后者在适用场景下非常强大,但对数据模型有比较严格的限制。<br>因此这里研究前者,以及如何应对网络故障。
为什么使用主从模式
除主从(Leader/Follower)模式外,另一种选择是分布式哈希表<br>(DHT),可以支持每小时数千个节点的离开和加入,其可以在不了解<br>底层网络拓扑的异构网络中工作,查询响应时间大约为4到10跳(中转<br>次数),例如,Cassandra就使用这种方案。但是在相对稳定的对等网络<br>中,主从模式会更好。
<div><span style="font-size: 14px;">ES的典型场景中的另一个简化是集群中没有那么多节点。通常,节</span></div><div><span style="font-size: 14px;">点的数量远远小于单个节点能够维护的连接数,并且网络环境不必经常</span></div><div><span style="font-size: 14px;">处理节点的加入和离开。这就是为什么主从模式更适合ES。</span></div>
选举算法<br>
在主节点选举算法的选择上,基本原则是不重复造轮子。最好实现<br>一个众所周知的算法,这样的好处是其中的优点和缺陷是已知的。ES的<br>选举算法的选择上主要考虑下面两种
<font color="#ff0000"><b>1.Bully算法</b></font><br>
Leader选举的基本算法之一。
它假定所有节点都有一个唯一的ID,<br>使用该ID对节点进行排序。任何时候的当前Leader都是参与集群的最高<br>ID节点。该算法的优点是易于实现。
但是,当拥有最大ID的节点处于不<br>稳定状态的场景下会有问题。例如,Master负载过重而假死,集群拥有<br>第二大ID的节点被选为新主,这时原来的Master恢复,再次被选为新<br>主,然后又假死……
ES通过推迟选举,直到当前的Master失效来解决上述问题,只要<br>当前主节点不挂掉,就不重新选主。但是容易产生脑裂(双主),为<br>此,再通过“法定得票人数过半”解决脑裂问题。
2.Paxos算法
Paxos非常强大,尤其在什么时机,以及如何进行选举方面的灵活<br>性比简单的Bully算法有很大的优势,因为在现实生活中,存在比网络<br>连接异常更多的故障模式。<br>
但 Paxos 实现起来非常复杂。<br>
相关配置<br>
discovery.zen.minimum_master_nodes
discovery.zen.ping.unicast.hosts<br>
discovery.zen.ping.unicast.hosts.resolve_timeout<br>
discovery.zen.join_timeout
discovery.zen.join_retry_attempts<br>
discovery.zen.join_retry_delay<br>
discovery.zen.master_election.ignore_non_master_pings
discovery.zen.fd.ping_interval<br>
discovery.zen.fd.ping_timeout
discovery.zen.fd.ping_retries
流程概述<br>
流程分析
节点失效检测
数据模型
写流程
Get流程
Search流程
索引恢复流程分析<br>
重要内部模块
调优和诊断
写入速度优化<br>
加大translogflush间隔,目的是降低iops、writeblock。
从ES2.x开始,在默认设置下,translog的持久化策略为:每个请求<br>都“flush”。对应配置项如下:index.translog.durability:request<br><font color="#ff0000">这是影响ES写入速度的最大因素</font>。
<font color="#ff0000"><b>index.translog.durability:async<br></b></font>设置为async表示translog的刷盘策略按sync_interval配置指定的时间<br>周期进行。<br><font color="#ff0000"><b>index.translog.sync_interval:120s<br></b></font>加大translog刷盘间隔时间。默认为5s,不可低于100ms。<br><font color="#ff0000"><b>index.translog.flush_threshold_size:1024mb<br></b></font>超过这个大小会导致refresh操作,产生新的Lucene分段。默认值为<br>512MB。
加大indexrefresh间隔,除了降低I/O,更重要的是降低了segment<br>merge频率
默认情况下索引的refresh_interval为1秒,这意味着数据写1秒后就<br>可以被搜索到,每次索引的refresh会产生一个新的Lucene段,这会导致<br>频繁的segmentmerge行为,如果不需要这么高的搜索实时性,应该降低<br>索引refresh周期,例如:<br><font color="#ff0000"><b>index.refresh_interval:120s</b></font>
段合并优化
<b><font color="#ff0000">segmentmerge操作对系统I/O和内存占用都比较高</font></b>,从ES2.0开始,<br>merge行为不再由ES控制,而是由Lucene控制,因此以下配置已被删<br>除:<br>indices.store.throttle.type<br>indices.store.throttle.max_bytes_per_sec<br>index.store.throttle.type<br>index.store.throttle.max_bytes_per_sec<br>改为以下调整开关:<br><b><font color="#ff0000">index.merge.scheduler.max_thread_count<br>index.merge.policy.*</font></b>
最大线程数max_thread_count的默认值如下:<br>Math.max(1,Math.min(4,Runtime.getRuntime().availableProcessors()/2))<br>以上是一个比较理想的值,<b style=""><font color="#00ff00">如果只有一块硬盘并且非SSD,则应该<br>把它设置为1,因为在旋转存储介质上并发写,由于寻址的原因,只会<br>降低写入速度。</font></b>
merge策略index.merge.policy有三种:<br>·tiered(默认策略);<br>·log_byete_size;<br>·log_doc。
索引创建时合并策略就已确定,不能更改,但是可以动态更新策略<br>参数,可以不做此项调整。如果堆栈经常有很多merge,则可以尝试调<br>整以下策略配置:<br>index.merge.policy.segments_per_tier<br>该属性指定了每层分段的数量,取值越小则最终segment越少,因<br>此需要merge的操作更多,可以考虑适当增加此值。默认为10,其应该<br>大于等于index.merge.policy.max_merge_at_once。<br>index.merge.policy.max_merged_segment<br>指定了单个segment的最大容量,默认为5GB,可以考虑适当降低<br>此值。
indexing buffer
indexingbuffer在为doc建立索引时使用,当缓冲满时会刷入磁盘,<br>生成一个新的segment,这是除refresh_interval刷新索引外,另一个生成<br>新segment的机会。每个shard有自己的indexingbuffer,下面的这个buffer<br>大小的配置需要除以这个节点上所有shard的数量:<br><b><font color="#ff0000">indices.memory.index_buffer_size<br></font></b>默认为整个堆空间的10%。<br><font color="#ff0000"><b>indices.memory.min_index_buffer_size<br></b></font>默认为48MB。<br><font color="#ff0000"><b>indices.memory.max_index_buffer_size<br></b></font>默认为无限制。<br>在执行大量的索引操作时,indices.memory.index_buffer_size的默认<br>设置可能不够,这和可用堆内存、单节点上的shard数量相关,可以考虑<br>适当增大该值。
调整bulk请求。
批量写比一个索引请求只写单个文档的效率高得多,但是要注意<br>bulk请求的整体字节数不要太大,太大的请求可能会给集群带来内存压<br>力,因此每个请求最好避免超过几十兆字节,即使较大的请求看上去执<br>行得更好。
bulk线程池和队列
建立索引的过程属于计算密集型任务,应该使用固定大小的线程池<br>配置,来不及处理的任务放入队列。线程池最大线程数量应配置为CPU<br>核心数+1,这也是bulk线程池的默认设置,可以避免过多的上下文切<br>换。队列大小可以适当增加,但一定要严格控制大小,过大的队列导致<br>较高的GC压力,并可能导致FGC频繁发生。
并发执行bulk请求<br>
bulk写请求是个长任务,为了给系统增加足够的写入压力,写入过<br>程应该多个客户端、多线程地并行执行,如果要验证系统的极限写入能<br>力,那么目标就是把CPU压满。
磁盘util、内存等一般都不是瓶颈。如<br>果CPU没有压满,则应该提高写入端的并发数量。<br>
但是要注意bulk线程池队列的reject情况,出现reject代表ES的bulk队列已满,客户端请求<br>被拒绝,此时客户端会收到429错误(TOO_MANY_REQUESTS),客<br>户端对此的处理策略应该是延迟重试。不可忽略这个异常,否则写入系<br>统的数据会少于预期。
即使客户端正确处理了429错误,我们<b><font color="#ff0000">仍然应该尽量避免产生reject。因此,在评估极限的写入能力时,客户端的极限写入并发量应该控制在不产生reject前提下的最大值为宜。</font></b><br>
优化磁盘间的任务均匀情况,将shard尽量均匀分布到物理主机的<br>各个磁盘。
如果部署方案是为path.data配置多个路径来使用多块磁盘,则ES在<br>分配shard时,落到各磁盘上的shard可能并不均匀,这种不均匀可能会<br>导致某些磁盘繁忙,利用率在较长时间内持续达到100%。这种不均匀<br>达到一定程度会对写入性能产生负面影响。<br>
<b><font color="#ff0000">ES在处理多路径时,优先将shard分配到可用空间百分比最多的磁<br>盘上</font></b>,因此短时间内创建的shard可能被集中分配到这个磁盘上,即使可<br>用空间是99%和98%的差别。<br>
后来ES在2.x版本中开始解决这个问题:预估一下shard会使用的空间,从磁盘可用空间中减去这部分,直到现在<br>6.x版也是这种处理方式。但是实现也存在一些问题
从可用空间减去预估大小
为此,我们为ES增加了两种策略。<br>·简单轮询:在系统初始阶段,简单轮询的效果是最均匀的。<br>·基于可用空间的动态加权轮询:以可用空间作为权重,在磁盘之<br>间加权轮询。<br>
优化节点间的任务分布,将任务尽量均匀地发到各节点。
为了节点间的任务尽量均衡,数据写入客户端应该把bulk请求轮询<br>发送到各个节点。
使用RESTAPI时,列表为构建对象时添加进去的节点。<br>
使用JavaAPI时,当设置client.transport.sniff为true(默认为false)<br>时,列表为所有数据节点,否则节点列表为构建客户端对象时传入的节<br>点列表。
要观察bulk请求在不同节点间的均衡性,可以通过cat接口观察bulk<br>线程池和队列情况:<br>_cat/thread_pool
优化Lucene层建立索引的过程,目的是降低CPU占用率及I/O,例<br>如,禁用_all字段。(<font color="#ff0000">6.x 默认禁用_all字段</font>)
自动生成docID
通过ES写入流程可以看出,写入doc时如果外部指定了id,则ES会<br>先尝试读取原来doc的版本号,以判断是否需要更新。这会涉及一次读<br>取磁盘的操作,通过自动生成docID可以避免这个环节。
调整字段Mappings<br>
(1)减少字段数量,对于不需要建立索引的字段,不写入ES。
(2)将不需要建立索引的字段index属性设置为not_analyzed或no。<br>对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。<br>尤其是binary类型,默认情况下占用CPU非常高,而这种类型进行分词<br>通常没有什么意义。
(3)减少字段内容长度,如果原始数据的大段内容无须全部建立<br>索引,则可以尽量减少不必要的内容。
(4)使用不同的分析器(analyzer),不同的分析器在索引过程中<br>运算复杂度也有较大的差异。
调整_source字段
_source字段用于存储doc原始数据,对于部分不需要存储的字<br>段,可以通过includesexcludes过滤,或者将_source禁用,一般用于索<br>引和数据分离。<br>
这样可以降低I/O的压力,不过实际场景中大多不会禁用_source,
而即使过滤掉某些字段,对于写入速度的提升作用也不大,满负荷写入<br>情况下,基本是CPU先跑满了,瓶颈在于CPU。
禁用_all字段
<font color="#ff0000"><b>从ES6.0开始,_all字段默认为不启用,而在此前的版本中,_all字<br>段默认是开启的。</b></font>
_all字段中包含所有字段分词后的关键词,作用是可<br>以在搜索的时候不指定特定字段,从所有字段中检索。
在ES6.0之前的版本中,可以在mapping中将enabled设置为false来禁<br>用_all字段:<br>curl-XPUT"localhost:9200/my_index"-H'Content-Type:<br>application/json'-d'<br>{<br>"mappings":{<br>"type_1":{<br>"_all":{<br>"enabled":false<br>},<br>"properties":{...}<br>}<br>}<br>}<br>'<br><b>禁用_all字段可以明显降低对CPU和I/O的压力。</b>
对Analyzed的字段禁用Norms
Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其<br>禁用:<br>"title":{"type":"string","norms":{"enabled":false}}
index_options设置
index_options用于控制在建立倒排索引过程中,哪些内容会被添加<br>到倒排索引,例如,doc数量、词频、positions、offsets等信息,优化这<br>些设置可以一定程度降低索引过程中的运算任务,节省CPU占用率。<br>不过在实际场景中,通常很难确定业务将来会不会用到这些信息,<br>除非一开始方案就明确是这样设计的。
参考配置
从ES5.x开始,索引级设置需要写在模板中,或者在创建索引时指<br>定,我们把各个索引通用的配置写到了模板中,这个模板匹配全部的索<br>引,并且具有最低的优先级,让用户定义的模板有更高的优先级,以覆<br>盖这个模板中的配置
{<br>"template":"*",<br>"order":0,<br>"settings":{<br>"index.merge.policy.max_merged_segment":"2gb",<br>"index.merge.policy.segments_per_tier":"24",<br>"index.number_of_replicas":"1",<br>"index.number_of_shards":"24",<br>"index.optimize_auto_generated_id":"true",<br>"index.refresh_interval":"120s",<br>"index.translog.durability":"async",<br>"index.translog.flush_threshold_size":"1000mb",<br>"index.translog.sync_interval":"120s",<br>"index.unassigned.node_left.delayed_timeout":"5d"<br>}<br>}
elasticsearch.yml中的配置:<br><font color="#ff0000"><b>indices.memory.index_buffer_size:30%</b></font>
思考与总结
(1)方法比结论重要。一个系统性问题往往是多种因素造成的,<br>在处理集群的写入性能问题上,先将问题分解,在单台上进行压测,观<br>察哪种系统资源达到极限,例如,CPU或磁盘利用率、I/Oblock、线程<br>切换、堆栈状态等。然后分析并调整参数,优化单台上的能力,先解决<br>局部问题,在此基础上解决整体问题会容易得多。
(2)可以使用更好的CPU,或者使用SSD,对写入性能提升明显。在我们的测试中,在相同条件下,E52650V4比E52430v2的写入速<br>度高60%左右。
(3)在我们的压测环境中,写入速度稳定在平均单机每秒3万条以<br>上,使用的测试数据:每个文档的字段数量为10个左右,文档大小约<br>100字节,CPU使用E52430v2。<br>
搜索速度的优化
为文件系统cache预留足够的内存
在一般情况下,应用程序的读写都会被操作系统“cache”(除了<br>direct方式),cache保存在系统物理内存中(线上应该禁用swap),命中<br>cache可以降低对磁盘的直接访问频率。
搜索很依赖对系统cache的命中,如果某个请求需要从磁盘读取数据,则一定会产生相对较高的延<br>迟。
<font color="#ff0000"><b>应该至少为系统cache预留一半的可用物理内存,更大的内存有更高的cache命中率。</b></font>
使用更快的硬件<br>
写入性能对CPU的性能更敏感,而搜索性能在一般情况下更多的是<br>在于I/O能力,使用SSD会比旋转类存储介质好得多。
尽量避免使用NFS等远程文件系统,如果NFS比本地存储慢3倍,则在搜索场景下响<br>应速度可能会慢10倍左右。这可能是因为搜索请求有更多的随机访问。
如果搜索类型属于计算比较多,则可以考虑使用更快的CPU。
文档模型
为了让搜索时的成本更低,文档应该合理建模。特别是<b><font color="#ff0000">应该避免<br>join操作,嵌套(nested)会使查询慢几倍,父子(parent-child)关系可<br>能使查询慢数百倍</font></b>
因此,如果可以通过非规范化(denormalizing)文档来回答相同的问题,则可以显著地提高搜索速度。
预索引数据
<font color="#ff0000"><b>还可以针对某些查询的模式来优化数据的索引方式</b></font>。例如,如果所<br>有文档都有一个price字段,并且大多数查询在一个固定的范围上运行<br>range聚合,那么<font color="#ff0000"><b>可以通过将范围“pre-indexing”到索引中并使用terms聚<br>合来加快聚合速度</b></font>
例如,文档起初是这样的:<br>PUTindex/type/1<br>{<br>"designation":"spoon",<br>"price":13<br>}<br><br><br>采用如上的搜索方式:
那么我们的优化是,在建立索引时对文档进行富化,增加<br>price_range字段,mapping为keyword类型:
接下来,搜索请求可以聚合这个新字段,而不是在price字段上运行<br>range聚合。
字段映射
有些字段的内容是数值,但并不意味着其总是应该被映射为数值类<br>型,例如,一些标识符,将它们映射为keyword可能会比integer或long更<br>好。
避免使用脚本
一般来说,应该避免使用脚本。如果一定要用,则应该优先考虑<br>painless和expressions。
优化日期搜索
在使用日期范围检索时,使用now的查询通常不能缓存,因为匹配<br>到的范围一直在变化。但是,从用户体验的角度来看,切换到一个完整<br>的日期通常是可以接受的,这样可以更好地利用查询缓存。
例如,有下列查询:
可以替换成下面的查询方式:<br>
在这个例子中,我们将日期四舍五入到分钟,因此如果当前时间是<br>16:31:29,那么range查询将匹配my_date字段的值在15:31~16:31之间的<br>所有内容。 如果几个用户同时运行一个包含此范围的查询,则查询缓存<br>可以加快查询速度。用于舍入的时间间隔越长,查询缓存就越有帮助,<br>但要注意,太高的舍入也可能损害用户体验。
为了能够利用查询缓存,可以很容易将范围分割成一个大的可缓存<br>部分和一个小的不可缓存部分,如下所示。
接上面的DSL
然而,这种做法可能会使查询在某些情况下运行得更慢,因为bool<br>查询引入的开销可能会抵销利用查询缓存所节省的开销。
为只读索引执行force-merge
<font color="#ff0000"><b>为不再更新的只读索引执行forcemerge,将Lucene索引合并为单个<br>分段,可以提升查询速度。</b></font>
当一个Lucene索引存在多个分段时,每个分段会单独执行搜索再将结果合并,将只读索引强制合并为一个Lucene分<br>段不仅可以优化搜索过程,对索引恢复速度也有好处。
基于日期进行轮询的索引的旧数据一般都不会再更新。应该避免持续地写一个固定的索引,直到它巨大无比,而应该按一定的策略,例如,每天生成一个新的索引,然后用别名关联,或者使用索引通配符。这样,可以每天选一个时间点对昨天的索引执行force-merge、Shrink等操作。
预热全局序号(global ordinals)
全局序号是一种数据结构,用于在keyword字段上运行terms聚合。<br>它用一个数值来代表字段中的字符串值,然后为每一数值分配一个bucket。这需要一个对globalordinals和bucket的构建过程。默认情况下,它们被延迟构建,因为ES不知道哪些字段将用于terms聚合,哪些字段不会。
可以通过配置映射在刷新(refresh)时告诉ES预先加载全局序数:
execution hint
terms聚合有两种不同的机制:<br>· 通过直接使用字段值来聚合每个桶的数据(map)。<br>· 通过使用字段的全局序号并为每个全局序号分配一个bucket(global_ordinals)
ES使用global_ordinals作为keyword字段的默认选项,它使用全局<br>序号动态地分配bucket,因此内存使用与聚合结果中的字段数量是线性<br>关系。在大部分情况下,这种方式的速度很快。
当查询只会匹配少量文档时,可以考虑使用map。默认情况下,<br>map只在脚本上运行聚合时使用,因为它们没有序数。
demo
预热文件系统cache
如果ES主机重启,则文件系统缓存将为空,此时搜索会比较慢。可以使用index.store.preload设置,通过指定文件扩展名,显式地告诉操作系统应该将哪些文件加载到内存中
例如,配置到elasticsearch.yml文件中:<br><font color="#ff0000"><b>index.store.preload:["nvd","dvd"]<br></b></font>或者在索引创建时设置:<br>PUT/my_index<br>{<br>"settings":{<br>"index.store.preload":["nvd","dvd"]<br>}<br>}
<font color="#ff0000"><b>如果文件系统缓存不够大,则无法保存所有数据,那么为太多文件<br>预加载数据到文件系统缓存中会使搜索速度变慢,应谨慎使用。</b></font>
转换查询表达式
在组合查询中可以通过bool过滤器进行and、or和not的多个逻辑组<br>合检索,这种组合查询中的表达式在下面的情况下可以做等价转换:<br>(A|B)&(C|D)==>(A&C)|(A&D)|(B&C)|(B&D)
调节搜索请求中的batched_reduce_size
该字段是搜索请求中的一个参数。默认情况下,聚合操作在协调节<br>点需要等所有的分片都取回结果后才执行,使用batched_reduce_size参<br>数可以不等待全部分片返回结果,而是在指定数量的分片返回结果之后<br>就可以先处理一部分(reduce)。这样可以避免协调节点在等待全部结<br>果的过程中占用大量内存,避免极端情况下可能导致的OOM。该字段<br>的默认值为512,从ES5.4开始支持
使用近似聚合<br>
近似聚合以牺牲少量的精确度为代价,大幅提高了执行效率,降低<br>了内存使用。近似聚合的使用方式可以参考官方手册:<br>·<b><font color="#ff0000">Percentiles</font></b><br>Aggregation(https://www.elastic.co/guide/en/elasticsearch/<br>reference/current/search-aggregations-metrics-percentile-<br>aggregation.html)。<br>·<font color="#ff0000"><b>Cardinality</b></font><br>Aggregation(https://www.elastic.co/guide/en/elasticsearch/reference/current/search-<br>aggregations-metrics-cardinality-aggregation.html)
深度优先还是广度优先<br>
ES有两种不同的聚合方式:深度优先和广度优先。<font color="#ff0000"><b>深度优先是默认<br>设置,先构建完整的树,然后修剪无用节点。</b></font>大多数情况下深度聚合都<br>能正常工作,但是有些特殊的场景更适合广度优先,先执行第一层聚<br>合,再继续下一层聚合之前会先做修剪,官方有一个例子可以参考:<br>https://www.elastic.co/guide/cn/elasticsearch/guide/current/_preventing_combinatorial_explosions.<br>html
限制搜索请求的分片数<br>
一个搜索请求涉及的分片数量越多,协调节点的CPU和内存压力就<br>越大。默认情况下,ES会拒绝超过1000个分片的搜索请求。我们应该更<br>好地组织数据,让搜索请求的分片数更少。如果想调节这个值,则可以<br>通过action.search.shard_count配置项进行修改。
虽然限制搜索的分片数并不能直接提升单个搜索请求的速度,但协<br>调节点的压力会间接影响搜索速度,例如,占用更多内存会产生更多的<br>GC压力,可能导致更多的stop-the-world时间等,因此间接影响了协调<br>节点的性能
利用自适应副本选择(ARS)提升ES响应速度
为了充分利用计算资源和负载均衡,协调节点将搜索请求轮询转发<br>到分片的每个副本,轮询策略是负载均衡过程中最简单的策略,任何一<br>个负载均衡器都具备这种基础的策略,缺点是不考虑后端实际系统压力<br>和健康水平。
例如,一个分片的三个副本分布在三个节点上,其中Node2可能因<br>为长时间GC、磁盘I/O过高、网络带宽跑满等原因处于忙碌状态,如图所示
如果搜索请求被转发到副本2,则会看到相对于其他分片来说,副<br>本2有更高的延迟。<br>·分片副本1:100ms。<br>·分片副本2(degraded):1350ms。<br>·分片副本3:150ms。<br>由于副本2的高延迟,使得整个搜索请求产生长尾效应。
ES希望这个过程足够智能,能够将请求路由到其他数据副本,直到<br>该节点恢复到足以处理更多搜索请求的程度。在ES中,此过程称为“自<br>适应副本选择”
ES的ARS实现基于这样一个公式:对每个搜索请求,将分片的每个<br>副本进行排序,以确定哪个最可能是转发请求的“最佳”副本。与轮询方<br>式向分片的每个副本发送请求不同,ES选择“最佳”副本并将请求路由到<br>那里。<br>
ARS从6.1版本开始支持,但是默认关闭,可以通过下面的命令动态<br>开启:<br>PUT/_cluster/settings<br>{<br>"transient":{<br>"cluster.routing.use_adaptive_replica_selection":true<br>}<br>}
从ES7.0开始,ARS将默认开启。
使用ARS,在某个数据节点处于高负载的情况下,吞吐量有了很大<br>的提高。延迟中位数有所增加是预料之中的,为了绕开高负载的节点,<br>稍微增加了无压力节点的负载,从而增加了延迟。
磁盘使用量优化
基础知识
元数据字段<br>
每个文档都有与其相关的元数据,比如_index、_type和_id。当创<br>建映射类型时,可以定制其中一些元数据字段。下面列出了与本文相关<br>的元数据字段,完整的介绍请参考官方手册:<br>https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-<br>fields.html
_source:原始的JSON文档数据。<br>·_all:索引所有其他字段值的一种通用字段,这个字段中包含了所<br>有其他字段的值。<br>允许在搜索的时候不指定特定的字段名,意味着“从全部字段中搜<br>索”,例如:<br>http://localhost:9200/website/_search?q=keyword<br>_all字段是一个全文字段,有自己的分析器。从ES6.0开始该字段被<br>禁用。之前的版本默认启用,但字段的store属性为false,因此它不能被<br>查询后取回显示
索引映射参数
索引创建时可以设置很多映射参数,各种映射参数的详细说明可参<br>考官方手册:<br>https://www.elastic.co/guide/en/elasticsearch/reference/master/mapping-<br>params.html
index:控制字段值是否被索引。它可以设置为true或false,默认<br>为true。未被索引的字段不会被查询到,但是可以聚合。除非禁用<br>doc_values
docvalues:默认情况下,大多数字段都被索引,这使得它们可以<br>搜索。倒排索引根据term找到文档列表,然后获取文档原始内容。但是<br>排序和聚合,以及从脚本中访问某个字段值,需要不同的数据访问模<br>式,它们不仅需要根据term找到文档,还要获取文档中字段的值。这些<br>值需要单独存储。doc_values就是用来存储这些字段值的。
它是一种存储在磁盘上的列式存储,在文档索引时构建,这使得上述数据访问模式<br>成为可能。它们以面向列的方式存储与_source相同的值,这使得排序和<br>聚合效率更高。几乎所有字段类型都支持doc_values,但被分析<br>(analyzed)的字符串字段除外(即text类型字符串)。<b><font color="#ff0000">doc_values默认<br>启用。</font></b>
store:默认情况下,字段值会被索引使它们能搜索,但它们不会<br>被存储(stored)。意味着可以通过这个字段查询,但不能取回它的原<br>始值
但这没有关系。因为字段值已经是_source字段的一部分,它是被默<br>认存储的。如果只想取回一个字段或少部分字段的值,而不是整个<br>_source,则可以通过sourcefiltering达到目的
优化措施
禁用不需要的特性
<b><font color="#ff0000">默认情况下,ES为大多数的字段建立索引,并添加到doc_values,<br>以便使之可以被搜索和聚合</font></b>。但是有时候不需要通过某些字段过滤,例<br>如,有一个名为foo的数值类型字段,需要运行直方图,但不需要在这<br>个字段上过滤,那么可以<b><font color="#ff0000">不索引这个字段</font></b>
text类型的字段会在索引中存储归一因子(normalization<br>factors),以便对文档进行评分,如果只需要在文本字段上进行匹配,<br>而不关心生成的得分,则可以配置ES不将norms写入索引
text类型的字段默认情况下也在索引中存储频率和位置。频率用于<br>计算得分,位置用于执行短语(phrase)查询。如果不需要运行短语查<br>询,则可以告诉ES不索引位置:
在text类型的字段上,index_options的默认值为positions。<br>index_options参数用于控制添加到倒排索引中的信息。
freqs文档编号和词频被索引,词频用于为搜索评分,重复出现的<br>词条比只出现一次的词条评分更高。positions文档编号、词频和位置被<br>索引。位置被用于邻近查询(proximityqueries)和短语查询(phrase<br>queries)。<br>完整的index_options选项请参考官方手册:<br>https://www.elastic.co/guide/en/elasticsearch/reference/master/index-<br>options.html
此外,如果也不关心评分,则可以将ES配置为只为每个term索引匹<br>配的文档。仍然可以在这个字段上搜索,但是短语查询会出现错误,评<br>分将假定在每个文档中只出现一次词汇。
子主题
禁用doc values<br>
所有支持docvalue的字段都默认启用了docvalue。<b><font color="#ff0000">如果确定不需要<br>对字段进行排序或聚合,或者从脚本访问字段值,则可以禁用docvalue<br>以节省磁盘空间</font></b>
设置
不要使用默认的动态字符串映射
默认的动态字符串映射会把字符串类型的字段同时索引为text和<br>keyword。如果只需要其中之一,则显然是一种浪费。通常,id字段只<br>需作为keyword类型进行索引,而body字段只需作为text类型进行索<br>引。<br>要禁用默认的动态字符串映射,则可以显式地指定字段类型,或者<br>在动态模板中指定将字符串映射为text或keyword。
下例将字符串字段映射为keyword:
观察分片大小
较大的分片可以更有效地存储数据。为了增加分片大小,可以在创<br>建索引的时候设置较少的主分片数量,或者使用shrinkAPI来修改现有<br>索引的主分片数量。但是较大的分片也有缺点,例如,较长的索引恢复<br>时间
_source字段存储文档的原始内容。如果不需要访问它,则可以将<br>其禁用。但是,需要访问_source的API将无法使用,至少包括下列情况<br>
·update、update_by_query、reindex;<br>·高亮搜索;<br>·重建索引(包括更新mapping、分词器,或者集群跨大版本升级可<br>能会用到);<br>·调试聚合查询功能,需要对比原始数据。
使用best_compression
_source和设置为"store":true的字段占用磁盘空间都比较多。默认<br>情况下,它们都是被压缩存储的。默认的压缩算法为LZ4,可以<font color="#ff0000"><b>通过使<br>用best_compression来执行压缩比更高的算法:DEFLATE。但这会占用<br>更多的CPU资源。</b></font>
PUTindex<br>{<br>"settings":{<br>"index":{<br>"codec":"best_compression"<br>}<br>}<br>}<br>
Force Merge<br>
一个ES索引由若干分片组成,一个分片有若干Lucene分段,较大的<br>Lucene分段可以更有效地存储数据。<br>使用_forcemergeAPI来对分段执行合并操作,通常,我们将分段合<br>并为一个单个的分段:max_num_segments=1
Shrink Index
ShrinkAPI允许减少索引的分片数量,结合上面的ForceMerge<br>API,可以显著减少索引的分片和Lucene分段数量
数值类型长度够用就好
为数值类型选择的字段类型也可能会对磁盘使用空间产生较大影<br>响,整型可以选择byte、short、integer或long,浮点型可以选择<br>scaled_float、float、double、half_float,每个数据类型的字节长度是不<br>同的,为业务选择够用的最小数据类型,可以节省磁盘空间
使用索引排序来排列类似的文档
当ES存储_source时,它同时压缩多个文档以提高整体压缩比。例<br>如,文档共享相同的字段名,或者它们共享一些字段值,特别是在具有<br>低基数或zipfian分布(参考https://en.wikipedia.org/wiki/Zipf%27s_law)<br>的字段上
默认情况下,文档按照添加到索引中的顺序压缩在一起。如果启用<br>了索引排序,那么它们将按排序顺序压缩。对具有相似结构、字段和值<br>的文档进行排序可以提高压缩比。<br>关于索引排序的详细内容请参考官方手册:<br>https://www.elastic.co/guide/en/elasticsearch/reference/master/index-<br>modules-index-sorting.html
在文档中以相同的顺序放置字段
由于多个文档被压缩成块,如果字段总是以相同的顺序出现,那么<br>在这些_source文档中可以找到更长的重复字符串的可能性更大。
测试数据
使用测试数据调整不同索引方式的测试结论。测试数据为单个文档十几个字段,大小为800字节左右。数据样本如上
在其他条件不变的情况下,调整单个参数,测试结果如下
禁用_source,空间占用量下降30%左右;<br>
禁用doc values,空间占用量下降10%左右;
压缩算法将LZ4改为Deflate,空间占用量可以下降15%~25%。<br>
实际业务最好使用自己的样本数据进行压力测试以获取准确的结果
综合应用实践
集群层
规划集群规模
在部署一个新集群时,应该根据多方面的情况评估需要多大的集群<br>规模来支撑业务。
需要一些基础的测试数据,包括在特定的硬件下,<br>特定业务数据样本的写入性能和搜索性能。然后根据具体业务情况来评<br>估初始集群大小,这些信息包括:<br>·数据总量,每天的增量;<br>·查询类型和搜索并发,QPS;<br>·SLA级别。
另一方面,需要控制最大集群规模和数据总量,参考下列两个限制<br>条件:<br>·节点总数不应该太多,一般来说,最大集群规模最好控制在100个<br>节点左右。我们曾经测试过上千个节点集群,在这种规模下,节点间的<br>连接数和通信量倍增,主节点管理压力比较大。<br>·单个分片不要超过50GB,最大集群分片总数控制在几十万的级<br>别。太多分片同样增加了主节点的管理负担,而且集群重启恢复时间会<br>很长
建议为集群配置较好的硬件,而不是普通的PC,搜索对CPU、内<br>存、磁盘的性能要求都很高,要达到比较低的延迟就需要较好的硬件资<br>源。另外,如果使用不同配置的服务器混合部署,则搜索速度可能会取<br>决于最慢的那个节点,产生长尾效应。
单节点还是多节点部署<br>
ES不建议为JVM配置超过32GB的内存,<font color="#ff0000"><b>超过32GB时,Java内存指<br>针压缩失效,浪费一些内存,降低了CPU性能,GC压力也较大</b></font>。因此<br>推荐设置为31GB:<br>-Xmx31g-Xms31g<br>确保堆内存最小值(Xms)与最大值(Xmx)大小相同,防止程序<br>在运行时动态改变堆内存大小,这是很耗系统资源的过程。
<b><font color="#ff0000">当物理主机内存在64GB以上,并且拥有多个数据盘,不做raid的情<br>况下</font></b>,部署ES节点时有多种选择
(1)部署单个节点,JVM内存配置不超过32GB,配置全部数据<br>盘。这种部署模式的缺点是多余的物理内存只能被cache使用,而且只<br>要存在一个坏盘,节点重启会无法启动。
(2)部署单个节点,JVM内存配置超过32GB,配置全部数据盘。<br>接受指针压缩失效和更长时间的GC等负面影响。
(3)有多少个数据盘就部署多少个节点,每个节点配置单个数据<br>路径。优点是可以统一配置,缺点是节点数较多,集群管理负担大,只<br>适用于集群规模较小的场景。
(4)使用内存大小除以64GB来确定要部署的节点数,每个节点配<br>置一部分数据盘,优点是利用率最高,缺点是部署复杂<br>
官方的建议是方案4,但是为了管理和维护的简便,也可以使用方<br>案1和3。这两种部署模式在我们的集群中都在使用,集群规模较小时可<br>以考虑使用多节点方式部署,例如,只有3台物理机。当集群规模较大<br>时,建议单节点方式部署,例如,物理机达到100台以上
移除节点<br>
当由于坏盘、维护等故障需要下线一个节点时,我们需要先将该节<br>点的数据迁移,这可以通过<font color="#ff0000"><b>分配过滤器</b></font>实现
例如,我们将node-1下线
PUT_cluster/settings<br>{<br>"transient":{<br>"cluster.routing.allocation.exclude._name":"node-1"<br>}<br>}<br>执行命令后,分片开始迁移,我们可以通过_cat/shardAPI来查看该<br>节点的分片是否迁移完毕。
当节点维护完毕,重新上线之后,需要取消<br>排除设置,以便后续的分片可以分配到node-1节点上。<br>PUT_cluster/settings<br>{<br>"transient":{<br>"cluster.routing.allocation.exclude._name":""<br>}<br>}<br>完整的分配过滤器使用方式请参考官方手册:<br>https://www.elastic.co/guide/en/elasticsearch/reference/current/allocation-<br>filtering.html
独立部署主节点
将主节点和数据节点分离部署最大的好处是Master切换过程可以<br>迅速完成,有机会跳过gateway和分片重新分配的过程
例如,有3台具备Master资格的节点独立部署,然后关闭当前活跃的主节点,新主当选<br>后由于内存中持有最新的集群状态,因此可以跳过gateway的恢复过<br>程,并且由于主节点没有存储数据,所以旧的Master离线不会产生未分<br>配状态的分片。新主当选后集群状态可以迅速变为Green。
节点层
控制线程池的队列大小
不要为bulk和search分配过大的队列,队列并非越大越好,队列缓<br>存的数据越多,GC压力越大,默认的队列大小基本够用了,即使在压<br>力测试的场景中,默认队列大小也足以支持。除非在一些特别的情况<br>下,例如,每个请求的数据量都非常小,可能需要增加队列大小。但是<br>我们推荐写数据时组合较大的bulk请求。
为系统cache保留一半物理内存<br>
搜索操作很依赖对系统cache的命中,标准的建议是把50%的可用内<br>存作为ES的堆内存,为Lucene保留剩下的50%,用作系统cache
系统层
关闭swap<br>
在个人PC上,交换分区或许有用,如果物理内存不够,则交换分<br>区可以让系统缓慢运行。但是<font color="#ff0000"><b>在服务器系统上,无论物理内存多么小,<br>哪怕只有1GB,都应该关闭交换分区。当服务程序在交换分区上缓慢运<br>行时,往往会产生更多不可预期的错误</b></font>,因此当一个申请内存的操作如<br>果真的遇到物理内存不足时,宁可让它直接失败。
<b><font color="#ff0000">一般在安装操作系统的时候直接关闭交换分区,或者通过swapoff<br>命令来关闭</font></b>
配置LinuxOOMKiller
现在讨论的OOM并非JVM的OOM,而是<font color="#ff0000"><b>Linux操作系统的OOM</b></font>。<br>在Linux下,进程申请的内存并不会立刻为进程分配真实大小的内存,<br>因为进程申请的内存不一定全部使用,内核在利用这些空闲内存时采取<br>过度分配的策略,假如物理内存为1GB,则两个进程都可以申请1GB的<br>内存,这超过了系统的实际内存大小。当应用程序实际消耗完内存的时<br>候,怎么办?系统需要“杀掉”一些进程来保障系统正常运行。这就触发<br>了OOMKiller,通过一些策略给每个进程打分,根据分值高低决定“杀<br>掉”哪些进程。<b><font color="#ff0000">默认情况下,占用内存最多的进程被“杀掉”。</font></b>
如果ES与其他服务混合部署,当系统产生OOM的时候,ES有可能<br>会无辜被“杀”。为了避免这种情况,我们可以在用户态调节一些进程参<br>数来让某些进程不容易被OOMKiller“杀掉
例如,我们不希望ES进程被“杀”,<font color="#ff0000"><b>可以设置进程的oom_score_adj参数为-17(越小越不容易被<br>杀)</b></font>:<br>$jps<br>1849Elasticsearch<br>$cat/proc/1849/oom_score_adj<br>0<br>$sudoecho-17>/proc/1849/oom_score_adj<br>可以将这个信息写到ES的启动脚本中自动执行。
优化内核参数
在生产环境上,我们可以根据自己的场景调节内核参数,让搜索服<br>务更有效率地运行。
例如,ES集群中的节点一般处于同一个子网,也就<br>是在同一个局域网,Linux默认的TCP选项不一定完全合适,因为它需<br>要考虑在互联网上传输时可能出现的更大的延迟和丢包率。因此<b><font color="#ff0000">我们可<br>以调节一些TCP选项,让TCP协议在局域网上更高效</font></b>
调节内核参数可以通过两种方式:<br>(1)临时设置,系统重启后失效。通过sysctl-w来设置,例如:<br>sysctl-wnet.ipv4.tcp_timestamps=1,命令执行后该参数立即生效。<br>(2)永久设置,将参数写入配置文件/etc/sysctl.conf,然后执行<br>sysctl-p使其生效。<br>可以通过sysctl-A配合grep查看某个参数的当前值。
下面给出一些比较通用的内核参数设置建议,这些参数的默认值以<br>CentOS7.2为参考,在其他系统上可能会有些差异。
<font color="#ff0000"><b>1.TCP相关参数</b></font>
net.ipv4.tcp_syn_retries
默认值为6,参考值为2。主机作为客户端,对外发起TCP连接时,<br>即三次握手的第一步,内核发送SYN报文的重试次数,超过这个次数后<br>放弃连接。内网环境通信良好,因此可以适度降低此值。
net.ipv4.tcp_synack_retries
默认值为5,参考值为2。主机作为服务端,接受TCP连接时,在三<br>次握手的第二步,向客户端发送SYN+ACK报文的重试次数,超过这个<br>次数后放弃连接。内网环境中可适度降低此值。
net.ipv4.tcp_timestamps
默认值为1,参考值为1。是否开启时间戳,开启后可以更精确地计<br>算RTT,一些其他特性也依赖时间戳字段。
net.ipv4.tcp_tw_reuse<br>
默认值为0,建议值为1。是否允许将处于TIME_WAIT状态的socket<br>用于新的TCP连接。这对于降低TIME_WAIT数据很有效。该参数只有<br>在开启tcp_timestamps的情况下才会生效。
net.ipv4.tcp_tw_recycle
默认值为0,参考值为0。是否开启TIME_WAIT套接字的快速回<br>收,这是比tcp_tw_reuse更激进的一种方式,它同样依赖tcp_timestamps<br>选项。强烈建议不要开启tcp_tw_recycle,原因有两点,一是<br>TIME_WAIT是十分必要的状态,避免关闭中的连接与新建连接之间的<br>数据混淆,二是tcp_tw_recycle选项在NAT环境下会导致一些新建连接被<br>拒绝,因为NAT下每个主机存在时差,这体现在套接字中的时间戳字<br>段,服务端会发现某个IP上的本应递增的时间戳出现降低的情况,时间<br>戳相对降低的报文将被丢弃
net.core.somaxconn
默认值为128,参考值为2048。定义了系统中每一个端口上最大的<br>监听队列的长度。当服务端监听了某个端口时,操作系统内部完成对客<br>户端连接请求的三次握手。这些已建立的连接存储在一个队列中,等待<br>accept调用取走。本选项就是定义这个队列的长度。该队列实际大小取<br>决于listen调用传入的第二个参数:backlog和本选项的最小值:<br>min(backlog,somaxconn)。ES需要建立许多连接,当集群节点数比较<br>大,集群完全重启时可能会在瞬间建立大量连接,默认的连接队列长度<br>可能不够用,因此适当提高此值。
net.ipv4.tcp_max_syn_backlog<br>
默认值为128,参考值为8192。内核会服务端的连接建立两个队<br>列:<br>·已完成三次握手,连接已建立,等待accept的队列,全局长度由<br>somaxconn定义。<br>·三次握手执行到第二步,等待客户端返回ACK,这些未完成的连<br>接单独放到一个队列中,由tcp_max_syn_backlog定义队列大小。<br>由于可能会有较多的连接数,我们适度增加“未完成连接”的队列大<br>小。
net.ipv4.tcp_max_tw_buckets
默认值为4096,参考值为180000。定义系统同时保持TIME_WAIT<br>套接字的最大数量,如果超过这个数,则TIME_WAIT套接字将立刻被<br>清除并打印警告信息。如果系统被TIME_WAIT过多问题困扰,则可以<br>调节tcp_max_tw_buckets、tcp_tw_reuse、tcp_timestamps三个选项来缓<br>解。TIME_WAIT状态产生在TCP会话关闭时主动关闭的一端,如果想<br>从根本上解决问题,则让客户端主动关闭连接,而非服务端。
net.ipv4.tcp_max_orphans
默认值为4096,参考值为262144。定义最大孤儿套接字(未附加到<br>任何用户文件句柄的套接字)数量。如果孤儿套接字数量超过此值,则<br>这些连接立即“reset”,并显示警告信息。该值可以简单地抵御DOS攻<br>击,但不能通过降低此值来抵御DOS。为了应对高负载,应该提高此<br>值。
<font color="#ff0000"><b>2.TCP的接收窗口(RWND)</b></font>
TCP采用两个基本原则决定何时发送及发送多少数据:<br>·流量控制,为了确保接收者可以接收数据。<br>·拥塞控制,为了管理网络带宽。<br>流量控制通过在接收方指定接收窗口大小来实现。接收窗口用于接<br>收端告诉发送端,自己还有多大的缓冲区可以接收数据。发送端参考这<br>个值来发送数据,就不会导致客户端处理不过来。<br>接收窗口的大小可以通过内核参数来调整,其理想值是<br>BDP(bandwidth-delayproduct):服务端可以发出的未被客户端确认的<br>数据量,也就是在网络上缓存的数据量。<br>
网络连接通常以管道为模型,BDP是带宽与RTT的乘积,表示需要<br>多少数据填充管道。下图展示了BDP的基本概念<br>
例如,在千兆的网络上,RTT为10毫秒,那么BDP=(1000/8)<br>×0.01s=1.25MB。在这种情况下,如果想最大限度地提升TCP吞吐量,<br>则RWND大小不应小于1.25MB。<br>可以通过下面的选项调整RWND:<br>net.ipv4.tcp_rmem=<MIN><DEFAULT><MAX><br>默认情况下,系统会在最大值和最小值之间自动调整缓冲区大小,<br>是否自动调整通过tcp_moderate_rcvbuf选项来决定。在开启缓冲自动调<br>整的情况下,可以把最大值设置为BDP。<br>TCP使用2个字节记录窗口大小,因此最大值为64KB,如果超过这<br>个值,则需要使用tcp_window_scaling机制,通过下面的设置开启(默<br>认启用):<br>net.ipv4.tcp_window_scaling=1<br>RWND和CWND可能是让系统达到最大吞吐量的两个限制因素,接<br>下来我们讨论CWND。
<font color="#ff0000"><b>3.TCP的拥塞窗口(CWND)</b></font>
TCP的滑动窗口机制依据接收端的能力来进行流控,并不能感知网<br>络延迟等网络因素。拥塞控制机制会评估网络能承受的负荷,避免过量<br>数据发送到网络中,拥塞程度会涉及主机、路由器等网络上的所有因<br>素。<br>拥塞控制由发送方实现,发送方会将它传输的数据量限制为CWND<br>和RWND的最小值。CWND会随着时间和对端的ACK增长,如果检测<br>到网络拥塞,则缩小CWND。拥塞控制主要有4种算法:慢启动、拥塞<br>避免、快速重传和快速恢复。<br>慢启动的意思是对于刚建立的连接,开始发送数据时,一点点提<br>速,而不是一下子使用很大的带宽。慢启动是指数上升的过程,直到<br>CWND≥ssthresh,进入拥塞避免算法
调节初始拥塞窗口(INITCWND)的大<br>小。适度增加INITCWND可以降低HTTP响应延迟,可以参考Google的<br>论文:AnArgumentforIncreasingTCP'sInitialCongestionWindow。<br>例如,HTTP要返回的内容为20K,MSS的大小为1460,整个内容需<br>要传送15个MSS。当INITCWND为3时,服务端先发送3个<br>MSS,1460×3=4380字节,待客户端ACK后,根据指数增加算法,第二次<br>发送9个MSS,1460×9=13140字节,第三次发送3个MSS的剩余字节。整<br>个传输过程经过了3次RTT。如果INITCWND设置为15,则只需要一次<br>RTT就可以完成传输。<br>在Linux2.6.39版本之前,INITCWND值根据MSS值来计算,参考<br>RFC3390:http://www.rfc-editor.org/rfc/rfc3390.txt。以太网的MSS大小一<br>般是1460,因此INITCWND为3,这个初始值比较小。从2.6.39版本及之<br>后,采取了Google的建议,把INITCWND调到了10,参考:<br>https://kernelnewbies.org/Linux_2_6_39#head-<br>1d11935223b203d28a660417627514973de4e218。<br>如果系统上的INITCWND低于10,可以使用ip命令调整。调节和测<br>试INITCWND的具体方法可以参考:<br>https://www.cdnplanet.com/blog/tune-tcp-initcwnd-for-optimum-<br>performance/
<b><font color="#ff0000">4.vm相关参数</font></b>
文件的读和写操作都会经过操作系统的cache,读缓存是比较简单<br>的,而写缓存相对复杂。在一般情况下,写文件的数据先到系统缓存<br>(pagecache),再由系统定期异步地刷入磁盘,这些存储于pagecache<br>中尚未刷盘的数据称为脏数据(或者脏页,dirtypage)。写缓存可以提<br>升I/O速度,但存在数据丢失的风险。例如,在尚未刷盘的时候主机断<br>电。
系统当前pagecache信息可以通过<font color="#ff0000"><b>/proc/meminfo</b></font>文件查看
讨论一下写缓存的细节和控制策略。<br>从pagecache刷到磁盘有以下三种时机<br>
1.可用物理内存低于特定阈值时,为了给系统腾出空闲内存;
2.脏页驻留时间超过特定阈值时,为了避免脏页无限期驻留内存;
3.被用户的sync()或fsync()触发。
由系统执行的刷盘有两种写入策略:
异步执行刷盘,不阻塞用户I/O
同步执行刷盘,用户I/O被阻塞,直到脏页低于某个阈值。
在一般情况下,系统先执行第一种策略,当脏页数据量过大,异步<br>执行来不及完成刷盘时,切换到同步方式。我们可以通过内核参数调整<br>脏数据的刷盘阈值
vm.dirty_background_ratio,默认值为10。该参数定义了一个百分<br>比。当内存中的脏数据超过这个百分比后,系统使用异步方式刷盘
vm.dirty_ratio,默认值为30。同样定义了一个百分比,当内存中<br>的脏数据超过这个百分比后,系统使用同步方式刷盘,写请求被阻塞,<br>直到脏数据低于dirty_ratio。如果还高于dirty_background_ratio,则切换<br>到异步方式刷盘。因此dirty_ratio应高于dirty_background_ratio。
除了通过百分比控制,还可以指定字节大小,类似的参数有:<br>dirty_background_bytes<br>dirty_bytes<br>
vm.dirty_expire_centisecs,默认值为3000(30秒),单位为百分之<br>1秒,定义脏数据的过期时间,超过这个时间后,脏数据被异步刷盘
vm.dirty_writeback_centisecs,默认值为500(5秒),单位为百分<br>之1秒,系统周期性地启动线程来检查是否需要刷盘,该选项定义这个<br>间隔时间
可以通过下面的命令查看系统当前的脏页数量:<br>cat/proc/vmstat|egrep"dirty|writeback"<br>nr_dirty951<br>nr_writeback0<br>nr_writeback_temp0<br>输出显示有951个脏页等待写到磁盘。默认情况下每页大小为<br>4KB。
另外,也可以在/proc/meminfo文件中看到这些信息。
如果数据安全性要求没有那么高,想要多“cache”一些数据,让读取<br>更容易命中,则可以增加脏数据占比和过期时间:<br>vm.dirty_background_ratio=30<br>vm.dirty_ratio=60<br>vm.dirty_expire_centisecs=6000<br>反之则可以降低它们。如果只希望写入过程不要被系统的同步刷盘<br>策略影响,则可以让系统多容纳脏数据,但早一些触发异步刷盘。这样<br>也可以让I/O更平滑:<br>vm.dirty_background_ratio=5<br>vm.dirty_ratio=60
<b><font color="#ff0000">5.禁用透明大页(TransparentHugepages)</font></b>
透明大页是Linux的一个内核特性,它通过更有效地使用处理器的<br>内存映射硬件来提高性能,默认情况下是启用的。禁用透明大页能略微<br>提升程序性能,但是也可能对程序产生负面影响,甚至是严重的内存泄<br>漏。为了避免这些问题,我们应该禁用它(许多项目都建议禁用透明大<br>页,例如,MongoDB、Oracle)。
可以通过下面的命令检查其是否开<br>启:<br>cat/sys/kernel/mm/transparent_hugepage/enabled<br>[always]madvisenever<br>always代表开启,通过下面的命令将其禁用(系统重启后失效):<br>echonever|sudotee/sys/kernel/mm/transparent_hugepage/enabled<br>关于透明大页对应用程序的具体影响可以参考一篇分析文章:<br>https://blog.nelhage.com/post/transparent-hugepages/。<br>更多的内核参数信息可以参考<br>https://www.kernel.org/doc/Documentation/networking/ipsysctl.txt和<br>https://www.kernel.org/doc/Documentation/sysctl/
索引层
使用全局模板
<b style=""><font color="#ff0000">从ES5.x开始,索引级别的配置需要写到模板中,而不是<br>elasticsearch.yml配置文件</font></b>,但是我们需要一些索引级别的全局设置信<br>息,例如,translog的刷盘方式等,因此我们可以将这些设置编写到一<br>个模板中,并让这个模板匹配全部索引“*”,这个模板我们称为全局模<br>板
例如:<br>{<br>"template":"*",<br>"order":0,<br>"settings":{<br>"index.number_of_replicas":"1",<br>"index.number_of_shards":"24"<br>}<br>}
order为0代表该模板有最小的优先级。当索引创建时,ES判断都匹<br>配到哪些模板,如果匹配到多个模板,则将模板中的参数进行合并。当<br>遇到冲突的设置项时,根据模板优先级order来决定谁的配置会生效。我<br>们为全局模板设置最低的优先级,任何其他索引自定义的模板都可以覆<br>盖它的设
索引轮转
如果有一个索引每天都有新增内容,那么不要让这个索引持续增<br>大,建议使用日期等规则按一定频率生成索引。
同时将索引设置写入模板,让模板匹配这一系列的索引,还可以为索引生成一个别名关联部分<br>索引。
我们一般按天生成索引,例如,要新增一个名为dns_log的索引,<br>
我们先创建模板,在模板中描述该索引的设置和mapping信息:<br>
该模块会匹配dns_log-*规则的索引,对匹配规则的索引应用模板设<br>置。写入程序每天生成一个索引,例如,dns_log-20180614。在该索引<br>中只写入当天的数据。在搜索时,可以使用索引前缀dns_log-*进行搜<br>索。当需要删除旧数据时,可以按日期删除索引的旧数据,删除索引会<br>立即删除磁盘文件,释放存储空间。而如果不这么做,只删除部分<br>doc,则依赖Lucene分段的合并过程才能释放空间。
避免热索引分片不均
默认情况下,ES的分片均衡策略是尽量保持各个节点分片数量大致<br>相同。但是当集群扩容时,新加入集群的节点没有分片,此时新创建的<br>索引分片会集中在新节点上,这导致新节点拥有太多热点数据,该节点<br>可能会面临巨大的写入压力
因此,对于一个索引的全部分片,我们需<br>要控制单个节点上存储的该索引的分片总数,使索引分片在节点上分布<br>得更均匀一些。
例如,10个节点的集群,索引主分片数为5,副本数量为1,那么平<br>均下来每个节点应该有(5×2)/10=1个分片,考虑到节点故障、分片迁<br>移的情况,可以设置节点分片总数为2:<br>curl-XPUThttp://127.0.0.1:9200/myindex/_settings-d'{<br>"index":{"routing.allocation.total_shards_per_node":"2<br>"}<br>}'<br>通常,我们会把index.routing.allocation.total_shards_per_node与索引<br>主分片数、副本数等信息统一写到索引模板中。
副本数选择
由于搜索使用较好的硬件配置,硬件故障的概率相对较低。在大部<br>分场景下,将副本数number_of_replicas设置为1即可。这样每个分片存<br>在两个副本。如果对搜索请求的吞吐量要求较高,则可以适当增加副本<br>数量,让搜索操作可以利用更多的节点。如果在项目初始阶段不知道多<br>少副本数够用,则可以先设置为1,后期再动态调整。对副本数的调整<br>只会涉及数据复制和网络传输,不会重建索引,因此代价较小
ForceMerge
对冷索引执行ForceMerge会有许多好处
- 单一的分段比众多分段占用的磁盘空间更小一些;<br>·可以大幅减少进程需要打开的文件fd;<br>·可以加快搜索过程,因为搜索需要检索全部分段;<br>·单个分段加载到内存时也比多个分段更节省内存占用;<br>·可以加快索引恢复速度。
可以选择在系统的空闲时间段对不再更新的只读索引执行Force<br>Merge:<br>curl-XPOST"localhost:9200/twitter/_forcemerge"<br>该命令将分段合并为单个分段,执行成功后会自行“flush”。
ShrinkIndex
需要密切注意集群分片总数,分片数越多集群压力越大。在创建索<br>引时,为索引分配了较多的分片,但可能实际数据量并没有多大
例如,按日期轮询生成的索引,可能有些日子里数据量并不大,对这种索<br>引可以执行Shrink操作来降低索引分片数量。
我们可以为ShrinkIndex和ForceMerge编写自动运行脚<br>本,通过crontab选择在凌晨的某个时间对索引进行优化,编写crontab文<br>件内容如下:<br>$cat escron<br>02***es/home/es/software/elasticsearch/bin/index.sh<br>然后将定时任务添加到普通用户的定时任务中:<br>crontab escron<br>我们通常会为部署集群编写部署脚本,这些工作都可以放到部署脚<br>本中
close索引
如果有些索引暂时不使用,则不会再有新增数据,也不会有对它的<br>查询操作,但是可能以后会用而不能删除,那么可以把这些索引关闭,<br>在需要时再打开。关闭的索引除存储空间外不占用其他资源。
通过下面的命令关闭或打开一个索引:<br>curl-XPOST"localhost:9200/my_index/_close"<br>curl-XPOST"localhost:9200/my_index/_open"
延迟分配分片
当一个节点由于某些原因离开集群时,默认情况下ES会重新确定<br>主分片,并立即重新分配缺失的副分片。但是,一般来说节点离线是常<br>态,可能因为网络问题、主机断电、进程退出等因素是我们经常面对节<br>点离线的情况,而重新分配副分片的操作代价是很大的,该节点上存储<br>的数据需要在集群上重新分配,复制这些数据需要大量带宽和时间,
因此我们调整节点离线后分片重新分配的延迟时间:<br><font color="#ff0000"><b>"index.unassigned.node_left.delayed_timeout":"5d"</b></font><br>这个索引级的设置写到模板的全局设置信息中,节点离线一般是暂<br>时的,如果因为硬件故障,则修复时间一般是可以预期的,根据实际情<br>况来调节这个延迟时间。
小心地使用fielddata
<font color="#ff0000"><b>聚合时,ES通过doc_values获取字段值,但是text类型不支持<br>doc_values。</b></font>当在text类型字段上聚合时,就会依赖fielddata数据结构,<br>但fielddata默认关闭。因为它会消耗很多堆空间,并且在text类型字段上<br>聚合通常没有什么意义。
<font color="#00ff00"><b>doc_values在索引文档时就会创建,而fielddata是在聚合、排序,<br>或者脚本中根据需要动态创建的。其读取每个分段中的整个倒排索引,<br>反转term和doc的关系,将结果存储到JVM堆空间,这是非常昂贵的<br>过程,会让用户感到明显的延迟</b></font>
fielddata所占用的大小默认没有上限,可以通过<br>indices.fielddata.cache.size来控制,该选项设置一个堆内存的百分比,超<br>过这个百分后,使用LRU算法将老数据淘汰。
客户端
使用RESTAPI而非JavaAPI
由于JavaAPI引起版本兼容性问题,以及微弱<br>到可以忽略的性能提升,JavaAPI将在未来的版本中废弃,客户端最好<br>选择RESTAPI作为客户端,而不是JavaAPI
注意429状态码
bulk请求被放入ES的队列,当队列满时,新请求被拒绝,并给客户<br>端返回429的状态码。
客户端需要处理这个状态码,并在稍后重发请<br>求。此刻客户端需要处理bulk请求中部分成功、部分失败的情况。这<br>种情况产生在协调节点转发基于分片的请求到数据节点时,有可能因为<br>对方的bulk队列满而拒绝写操作,而其他数据节点正常处理,于是客户<br>端的bulk请求部分写入成功、部分写入失败。
客户端需要将返回429的<br>对应数据重试写入,而不是全部数据,否则写入的内容就会存在重复。
产生429错误是因为ES来不及处理,一般是由于写入端的并发过大<br>导致的,建议适当降低写入并发。
curl的HEAD请求
我们经常使用curl作为客户端进行一些日常操作。但是需要注意curl<br>发送HEAD请求的方式并非我们预想的那样,例如,通过HEAD请求检<br>查doc是否存在,官网的这个例子就是错误的:<br>curl-XHEAD"localhost:9200/twitter/_doc/0"
curl-XHEAD只是将HTTP头部的方法设置为HEAD,还会等待服务<br>器返回body,所以现象就是curl命令阻塞在那里。正确的方式应该是使<br>用-I参数:<br>curl-I"localhost:9200/twitter/_doc/0"<br>使用-I参数curl会将HTTP方法设置为HEAD,并在收到服务器返回<br>的HTTP头部信息后关闭TCP连接。
了解你的搜索计划
就像在执行一条SQL语句时,需要了解其执行计划一样,我们需要<br>知道一个搜索操作可能会命中多少分片,它执行的任务复杂性有多大,<br>聚合范围有多大等情况。只有了解了搜索指令的执行代价,才能更好地<br>使用ES进行搜索。
例如,搜索应该只让尽量少的分片参与工作,如果<br>只需要检索当天的内容,则在按天生成的索引中,只搜索当天的单个索<br>引即可。通过日期范围查询会让其他天的索引不必要地执行一次搜索。<br>除了人为评估查询语句,还可以使用ProfileAPI分析会命中哪些分<br>片,每个分片执行的查询时间等细节。
为读写请求设置比较长的超时时间<br>
读写操作都有可能是比较长的操作,例如,写一个比较大的bulk数<br>据,或者执行较大范围的聚合。此时客户端为请求设置的超时时间应该<br>尽量长,因为即使客户端断开连接,ES仍然会在后台将请求处理完,如<br>果超时设置比较短,则在密集的请求时会对ES造成非常大的压力。
读写
避免搜索操作返回巨大的结果集
在搜索流程中讨论过,由于协调节点的合并压力,所有的搜索<br>系统都会限制返回的结果集大小,如果确实需要很大的结果集,则应该<br>使用ScrollAPI。
避免索引巨大的文档
<b><font color="#ff0000">http.max_context_length的默认值为100MB,ES会拒绝索引超过此大<br>小的文档,可以增加这个值,但Lucene仍然有大约2GB的限制。</font></b>
即使不考虑这些限制,大型文档通常也不实用。大型文档给网络、<br>内存和磁盘造成了更大的压力。即使搜索操作设置为不返回_source,ES<br>总要获取_id,对于大型文档来说,获取这个字段的代价是很大的,这<br>是由于操作系统的cache机制决定的。索引一个文档需要一些内存,所<br>需内存大小是原始文档大小的几倍。邻近(Proximity)搜索(例如,短<br>语查询)和高亮也会变得更加昂贵,因为它们的成本直接取决于原始文<br>档大小。
因此可能要重新考虑信息的单位,例如,想要为一本书建立索引使<br>之可以被搜索,这并不意味着把整本书的内容作为单个文档进行索引。<br>最好使用章节或段落作为文档,然后在文档中加一个属性标识它们属于<br>哪本书。这样不仅避免了大文档的问题,还使搜索的体验更好
避免使用多个_type
_type本来是用于区分存储到同一个索引中的不同格式的数据,但<br>是实际上面对这种情况应该用不同的索引解决,而不是在同一个索引中<br>使用不同的_type。因为不能通过_type来删除数据<br>
从ES6.0开始,索引只允许存在一个_type,7.0版本之后将完全废弃<br>了_type的概念。
避免使用_all字段<br>
<b><font color="#ff0000">在写入速度优化中讨论过all使用字段带来的负面影响,从ES6.0开<br>始,_all字段默认被禁用,并且不建议使用。此类需求可以通过<br>mapping中的copy_to参数创建自定义的_all字段</font></b>。参考官方手册:<br>https://www.elastic.co/guide/en/elasticsearch/reference/master/copy-<br>to.html
避免将请求发送到同一个协调节点
无论索引文档还是执行搜索请求,客户端都应该避免将请求发送到<br>固定的某个或少数几个节点,因为少数几个协调节点作为整个集群对外<br>的读写节点的情况下,它们很有可能承受不了那么多的客户端请求。尤<br>其是搜索请求,协调节点的合并及排序会占用比较高的内存和CPU,聚<br>合会占用更多内存。因此会导致给客户端的返回慢,甚至导致节点<br>OOM。
子主题
控制相关度<br>
通过Painless脚本控制搜索评分
ES有多种方式控制对搜索结果的评分,如果常规方式无法得到想要<br>的评分结果,则可以通过脚本的方式完全自己实现评分算法,以得到预<br>期的评分结果。
ES支持多种脚本语言,经历各版本演变后,从5.0版本<br>开始实现了自己专用的语言:Painless。Groovy脚本已弃用。
Painless是<br>内置支持的,脚本内容通过REST接口传递给ES,ES将其保存在集群状态<br>中。在5.x版本中可以放到config/scripts下,6.x版本中只能通REST接口<br>写入。
通过脚本控制评分的原理是编写一个自定义脚本,该脚本返回评分<br>值,该分值与原分值进行加法等运算,从而完全控制了评分算法。
例如,我们有一个通讯录的名单索引user_info,为了简便说明问<br>题,索引只有一个name字段:<br>PUTuser_info<br>{<br>"mappings":{<br>"user":{<br>"properties":{<br>"name":{<br>"type":"keyword"<br>}<br>}<br>} }<br>}
写入下列测试数据:<br>POST/user_info/_bulk<br>{"index":{"_type":"user","_id":"1"}}<br>{"name":"高X"}<br>{"index":{"_type":"user","_id":"2"}}<br>{"name":"高XX"}<br>{"index":{"_type":"user","_id":"3"}}<br>{"name":"X高X"}<br>{"index":{"_type":"user","_id":"4"}}<br>{"name":"X高X"}<br>{"index":{"_type":"user","_id":"5"}}<br>{"name":"XX高"}<br>{"index":{"_type":"user","_id":"6"}}<br>{"name":"高XXX"}
我们期望的返回顺序与两个原则有关:关键词出现的位置越靠前,<br>排序应该越靠前;字段值约短,说明匹配度越高,排序应该越靠前。<br>因此,理想的顺序应该是:<br>高X<br>高XX<br>高XXX<br>X高X<br>X高X<br>XX高
下面执行搜索:<br>GETuser_info/_search?size=20<br>{<br>"query":{<br>"query_string":{<br>"query":"(name:(*高*))"<br>}<br>}<br>}<br>实际返回结果顺序如下,每一项的得分(_score)都是1.0。<br>X高X<br>高XX<br>X高X<br>高XXX<br>高X<br>XX高
我们编写一个简单的脚本,通过doc['name'].value获取文档值,<br>然后根据位置和相似度分别计算评分,将结果乘以不同权重再相加。
子主题
去掉脚本中的注释,并格式化为单行(在sublime中可以通过<br>Ctrl+A、Ctrl+J组合键实现)内容后,写入ES:<br>POST_scripts/user_info_score<br>{<br>"script":{<br>"lang":"painless",<br>"source":"格式化为单行的脚本内容"<br>}<br>}<br>我们的脚本id为user_info_score,在script_score函数中指定脚本id,<br>再次执行查询:
子主题
子主题
function_score查询是用来控制评分的终极武器,它允许为每个与主<br>查询匹配的文档应用一个内置或自定义函数,以达到改变原始查询评分<br>_score的目的。其中的script_score用于指定自定义脚本。params指定作<br>为变量传递到脚本中的参数。<br>boost_mode字段用来指定新计算的分数与_score的结合方式,取值<br>可以是:multiply相乘(默认)、replace替换、_scoresum相加、avg取<br>平均值、max取最大值、min取最小值。<br>
这次查询返回了我们期望的结果
子主题
故障诊断
概述
当集群出现故障时,不必担心,我们有许多方法和工具来分析问<br>题。在大部分情况下,我们遇到的问题都是由一些简单的原因导致的。<br>但由于分布式系统的复杂性,有时候故障现象只出现了一次,并且难以<br>复现,这就需要采取一些措施来缩小可疑的问题范围,虽然不能立刻解<br>决问题,但是可以向前迈进一步。综合来说,当遇到故障时,分析问题有两种思路
(1)从故障的具体现象和具体信息出发,逻辑性地向上推理可能<br>的因素,并逐步排除,渐渐缩小问题范围,直到定位问题。
(2)根据故障信息和经验直接猜测故障原因,这种凭空设想在面<br>对简单问题时能比较快速定位,但在面对错综复杂、多种因素混合的问<br>题时更多地需要理性推导。尽管如此,“假设故障原因”仍然很重要,在<br>推导问题过程中,以及不可重现的问题时,需要联想到与其相关的都有<br>哪些因素。
在逐步缩小问题范围、验证想法的过程中,验证方法可能有多种选<br>择。最好选能证明问题,同时是最简单的方式。有些方法不一定能很严<br>谨地验证问题,那这种验证方式的参考性和可信度就不大。同时有些方<br>法可能操作复杂,有些可能耗时比较长,这就需要我们做出选
使用ProfileAPI定位慢查询
有时在发起一个查询时,查询会被延迟执行,或者响应时间很慢,<br>查询缓慢可能会有多种原因:分片问题,或者计算查询中的某些元素。<br>ES从2.2版本开始提供ProfileAPI,供用户检查查询执行时间和其他详细<br>信息。
ProfileAPI返回所有分片的详细信息。我们使用一个例子来演示其<br>使用方式。为了简单起见,我们创建一个只有1个主分片、没有副分片<br>的索引,然后写入若干文档:<br>curl-XPOSThttp://localhost:9200/myindex/mytype/1-d'{<br>"brand":"CottonPlus"<br>}'<br>curl-XPOSThttp://localhost:9200/myindex/mytype/2-d'{<br>"brand":"VanHuesen"<br>}'<br>curl-XPOSThttp://localhost:9200/myindex/mytype/3-d'{<br>"brand":"Arrow"<br>}'
使用ProfileAPI来看一下检索的返回信息,可以通过<b><font color="#ff0000">在query部分上<br>方提供"profile":true来启用ProfileAPI</font></b>。
<b><font color="#ff0000">ProfileAPI的结果是基于每个分片计算的。</font></b>由于在我们的例子中只<br>有一个分片,在ProfileAPI响应的分片数组中只有一个数组元素,如下<br>所示。
子主题
子主题<br>
子主题
上面的响应显示的是单个分片。每个分片都被分配一个唯一的<br>ID,ID的格式是nodeID[shardID]。现在在“shards”数组里还有另外三个元<br>素,它们是:<br>(1)query。<br>(2)rewrite_time。<br>(3)collector。
1.Query
Query段由构成Query的元素及它们的时间信息组成。ProfileAPI结<br>果中Query部分的基本组成如下
query_type,它向我们显示了哪种类型的查询被触发。此处是布尔<br>值。因为多个关键字匹配查询,因此被分成两个布尔查询。
lucene,该字段显示启动查询的Lucene方法。这里是"brand:levi<br>brand:goals"
time,Lucene执行此查询所用的时间。单位是毫秒。<br>
breakdown,有关查询的更详细的细节,主要与Lucene参数有关。
children,具有多个关键字的查询被拆分成相应术语的布尔查询,<br>每个查询都作为单独的查询来执行。每个子查询的详细信息将填充到<br>ProfileAPI输出的子段中。可以看到第一个子元素查询是“levi”,下面给<br>出查询时间和其他breakdown参数等详细信息。同样,对于第二个关键<br>字,有一个名为“goals”的子元素具有与其兄弟相同的信息。从查询中的<br>子段中,我们可以得到关于哪个搜索项在总体搜索中造成最大延迟的信<br>息
2.RewriteTime
由于多个关键字会分解以创建个别查询,所以在这个过程中肯定会<br>花费一些时间。将查询重写一个或多个组合查询的时间被称为“重写时<br>间”(单位为纳秒)。
3.Collectors
在Lucene中,收集器负责收集原始结果,并对它们进行组合、过<br>滤、排序等处理。
例如,在上面的执行的查询中,当查询语句中给出<br>size:0时,使用的收集器是“totalHitCountCollector”。这只返回搜索结果<br>的数量(search_count),不返回文档。此外,收集者所用的时间也一<br>起给出了
ProfileAPI非常有用,它让我们清楚地看到查询时间。通过向我们<br>提供有关子查询的详细信息,我们可以清楚地知道在哪个环节查询慢,<br>这是非常有用的。另外,在API返回的结果中,关于Lucene的详细信息<br>也让我们深入了解到ES是如何执行查询的。
使用ExplainAPI分析未分配的分片<br>(UnassignedShards)
一个ES索引由多个分片组成,由于某些原因,某些分片可能会处于<br>未分配状态(Unassigned),导致集群健康处于Yellow或Red状态,这<br>是一种比较常见的错误信息,导致分片处于未分配的原因可能是节点离<br>线、分片数量设置错误等原因,使用ExplainAPI可以很容易分析当前的<br>分片分配情况。
这个API主要为了解决下面两个问题
(1)对于未分配的分片,给出为什么没有分配的具体原因。
(2)对于已分配的分片,给出为什么将分片分配给特定节点的理<br>由。
通过几个例子来演示如何通过ExplainAPI来定位分片分<br>配问题。
诊断未分配的主分片<br>
我们创建一个名为test_idx的索引,该索引只有一个主分片,没有<br>副分片。集群有两个节点,名为A和B。但是在创建索引时,我们设置<br>分配过滤器,使其不能被分配到节点A和B上 。 PUT/test_idx?wait_for_active_shards=0<br>{<br>"settings":<br>{<br>"number_of_shards":1,<br>"number_of_replicas":0,<br>"index.routing.allocation.exclude._name":"A,B"<br>}<br>}
虽然索引能创建成功,但是因为过滤规则的限制,索引分片无法分<br>配到集群仅有的A和B两个节点。此时集群处于Red状态,我们通过<br>ExplainAPI来获取第一个未分配分片的原因解释(在本例中,集群中只<br>有一个未分配的分片)。
GET/_cluster/allocation/explain<br>返回信息摘要如下:
子主题
子主题
ExplainAPI给出了该分片未分配的原因,由于索引刚创建<br>(unassigned_info),它处于未分配状态(current_state)。由于没有节<br>点允许分配给该分片(allocate_explain),所以无法分配分片<br>(can_allocation)。深入每个节点的决策信息<br>(node_allocation_decisions),可以看到由于索引的过滤器设置,分配<br>操作被decider拦截。decider给出了具体的decider名称,接下来是决策结<br>果及具体的原因(explanation)。
下面更新分配过滤器设置:<br>PUT/test_idx/_settings<br>{<br>"index.routing.allocation.exclude._name":null<br>}<br>然后重新运行ExplainAPI,将收到以下结果:<br>unabletofindanyunassignedshardstoexplain<br>表示当前没有未分配的分片。我们还可以在主分片上运行Explain<br>API:<br>GET/_cluster/allocation/explain<br>{<br>"index":"test_idx",<br>"shard":0,<br>"primary":true<br>}<br>我们可以看到分片被分配到了哪个节点,返回信息摘要如下:<br>{<br>"index":"test_idx",<br>"shard":0,<br>"primary":true,<br>"current_state":"started",<br>"current_node":{<br>"id":"tn3qdPdnQWuumLxVVjJJYQ",<br>"name":"A",<br>"weight_ranking":1<br>},<br>...<br>}<br>可以看出分片被成功(started)分配到节点A。<br>现在,我们向索引test_idx中写入一些数据,然后停掉节点A,由<br>于该索引没有副分片,所以集群变为Red状态。在主分片上重新运行<br>ExplainAPI,返回信息摘要如下:<br>{<br>"index":"test_idx",<br>"shard":0,<br>"primary":true,<br>"current_state":"unassigned",<br>"unassigned_info":{<br>"reason":"NODE_LEFT",<br>"details":"node_left[qU98BvbtQu2crqXF2ATFdA]",<br>"last_allocation_status":"no_valid_shard_copy"<br>},<br>"can_allocate":"no_valid_shard_copy",<br>"allocate_explanation":"cannotallocatebecauseaprevious<br>copyoftheprimary<br>shardexistedbutcannolongerbefoundonthenodesin<br>thecluster"<br>}<br>输出信息告诉我们主分片处于未分配状态(current_state),原因是<br>持有此分片的节点离线(unassigned_info)。ExplainAPI给出了分片不<br>能分配的原因,是因为集群中没有任何分片0的有效副本<br>(can_allocate)。allocate_explanation字段给出了详细的解释。<br>ExplainAPI告诉我们主分片不再有有效的分片副本,我们知道持<br>有该分片的节点离线了,此时唯一的办法就是等待节点重新加入集群。<br>在极端情况下,节点永久移除,只能接受丢失数据的现实,并通过<br>rerouteAPI重新分配空的主分片。
诊断未分配的副分片<br>
我们把刚才的索引test_idx副本数调整为1:<br>PUT/test_idx/_settings<br>{<br>"number_of_replicas":1<br>}<br>索引test_idx现在有两个分片,分片0的主分片及分片0的副分片。因<br>为A节点已经存储了主分片,所以副分片将被分配到节点B,以达到集<br>群均衡的目的。现在在副分片上运行ExplainAPI:<br>GET/_cluster/allocation/explain<br>{<br>"index":"test_idx",<br>"shard":0,<br>"primary":false<br>}<br>返回信息摘要如下:<br>{<br>"index":"test_idx",<br>"shard":0,<br>"primary":false,<br>"current_state":"started",<br>"current_node":{<br>"id":"qNgMCvaCSPi3th0mTcyvKQ",<br>"name":"B",<br>"weight_ranking":1<br>},<br>…<br>}
出信息显示分片已经被分配到节点B,并且为started状态。<br>接下来,我们再次设置索引的分配过滤器,但是这次阻止分配分片<br>的节点B:<br>PUT/test_idx/_settings<br>{<br>"index.routing.allocation.exclude._name":"B"<br>}
现在重启节点B,在副分片上重新运行ExplainAPI,返回信息摘要<br>如下:
子主题
结果显示副分片当前处于未分配状态(can_allocate),因为分配过<br>滤设置了禁止把分片分配到节点B上(explanation)。因为节点A上已经<br>指派了主分片,所以不允许再把该分片的其他副本分配到A节点<br>(explanation)。ES会避免将主副分片分配到同一个节点,主要是为了<br>防止当节点失效时所有副本都不可用,以及可能的数据丢失。
诊断已分配的分片
如果分片可以正常分配,为什么还要关注explain信息呢?一个常见<br>的原因是想将分片手工迁移到某个节点,但是出于某些原因分片没有迁<br>移,这时ExplainAPI帮助我们展示其中原因
我们清除test_idx索引的过滤器设置,使主副分片都可以正常分配。<br>PUT/test_idx/_settings<br>{<br>"index.routing.allocation.exclude._name":null<br>}<br>然后重新设置分配过滤器,使主分片从当前节点迁移走:<br>PUT/test_idx/_settings<br>{<br>"index.routing.allocation.exclude._name":"A"<br>}
我们期望的结果是主分片从节点A移动到另一个节点,然而事与<br>愿违,在主分片上运行ExplainAPI,看看具体原因:
子主题
从返回结果可以看出,主分片仍然被分配给节点<br>A(current_node),集群知道这个分片不应该保留在当前节点<br>(can_remain_on_current_node),原因是主分片被当前节点的decider<br>拦截(can_remain_decisions)。尽管分片不允许保留在当前节点,但它<br>也不能移动到其他节点(can_move_to_other_node),因为被节点B的<br>decider拦截,主副分片不能分配到同一节点。<br>
ExplainAPI是诊断生产环境分片分配过程的利器,它为定位问题节<br>省了很多时间。关于该命令的完整参数请参考官方手册:<br>https://www.elastic.co/guide/en/elasticsearch/reference/current/search-<br>explain.html
节点CPU使用率高
点占用CPU很高,我们想知道CPU在运行什么任务,一般通过线<br>程堆栈来查看。有两种方式可以查看哪些线程CPU占用率比较高。<br>·使用hot_threadsAPI,参考ThreadPool 。<br>·使用top+jstack,top获取线程级CPU占用率,再根据线程ID,配合<br>jstack定位CPU占用率在特定比例之上的线程堆栈。
推荐使用hot_threadsAPI获取繁忙线程的堆栈。top+jstack的方式由<br>于两个命令的执行在时间上(对进程采样的时间点)存在误差,所以定<br>位出的堆栈存在一定概率的偏差。为了降低这种误差,可以在脚本中让<br>两个命令同时执行,参考下面的实现
shell
子主题
节点内存使用率高
节点内存使用率高,我们想要知道内存是被哪些数据结构占据的,<br>通用方式是用jmap导一个堆出来,加载到MAT中进行分析,可以精确<br>定位数据结构占用内存的大小,以及被哪些对象引用。这是定位此类问<br>题最直接的方式。<br>
jmap导出的堆可能非常大,操作比较花时间,我们也可以简单看一<br>下ES中几个占用内存比较大的数据结构,有些数据结构无法看到其当前<br>的实际大小,只能通过设置的上限粗略评估。
bulk队列可以通过_catAPI查看bulk队列中当前的使用量,任务总<br>数乘以bluk请求的大小就是占用内存的大小。如果队列设置很大,则在<br>写入压力大的时候就会导致比较高的内存占用。默认值为200,一般情<br>况下都够用了。
Netty缓冲在一些特别的情况下,Netty的内存池也可能会占用比较<br>高的内存。Netty收到一个客户端请求时,为连接分配内存池,客户端<br>发送的数据存储到Netty的内存池中,直到ES层处理完上层逻辑,回复<br>客户端时,才释放该内存。当ES收到客户端请求时,如果在处理完毕<br>之前客户端关闭连接,则ES依然会把这个请求处理完,只是最后才出<br>现回复客户端失败。这个过程可能会导致内存累积,例如,执行bulk请<br>求时,客户端发送完毕,不等ES返回响应就关闭连接,然后立即发起下<br>一个请求,结果这些请求实际上都在等待处理,就可能占用非常多的内<br>存。所以<font color="#ff0000"><b>客户端的请求超时时间应该尽量设置得长一些,建议设置为分<br>钟级。</b></font>
indexingbuffer索引写入缓冲用于存储索引好的文档数据,当缓冲<br>满时,生成一个新的Lucene分段。在一个节点上,该节点的全部分片共<br>享indexingbuffer。该缓冲默认大小为堆内存的10%,加大该缓冲需要<br>考虑到对GC的压力。
超大数据集的聚合协调节点对检索结果进行汇总和聚合,当聚合涉<br>及的数据量很大时,协调节点需要拉取非常多的内容,大范围的聚合是<br>导致节点OOM的常见原因之一<br>
分段内存一个Lucene分段就是一个完整的倒排索引,倒排索引由单<br>词词典和倒排列表组成。在Lucene中,单词词典中的FST结构会被加<br>载到内存。因此每个分段都会占用一定的内存空间。可以通过下面的<br>API来查看某个节点上的所有分段占用的内存总量:<br>curl-XGET"localhost:9200/_cat/nodes?v&h=segments.memory"<br>6.7kb<br>也可以单独查看每个分段的内存占用量:<br>curl-XGET"localhost:9200/_cat/segments?<br>v&h=index,shard,segment,size.memory"<br>indexshardsegmentsize.memory<br>website1_01195<br>website1_11187<br>website1_2994<br>size.memory字段代表分段的内存占用量。该API可以根据索引名称<br>等进行过滤,完整的使用手册可以参考:<br>https://www.elastic.co/guide/en/elasticsearch/reference/master/cat-<br>segments.html<br>
Fielddatacache在text类型字段上进行聚合和排序时会用到<br>Fielddata,默认是关闭的,如果开启了Fielddata,则其大小默认没有上<br>限,可以通过indices.fielddata.cache.size设置一个百分比来控制其使用的<br>堆内存上限。可以通过下面的命令查看节点上的Fielddata使用情况:curl-XGET"localhost:9200/_cat/nodes?v&h=fielddata.memory_size<br>"<br>curl-XGET'http://localhost:9200/_nodes/stats/indices/fielddata?<br>fields=field1,field2&pretty'<br>也可以查看索引级的Fielddata使用情况:<br>curl-XGET'http://localhost:9200/_stats/fielddata/?<br>fields=field1,field2&pretty'<br>完整的NodesStatsAPI使用方式可以参考官方手册:<br>https://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-nodes-<br>stats.html。<br>catnodesAPI可以参考:<br>https://www.elastic.co/guide/en/elasticsearch/reference/master/catnodes.html
Shardrequestcache分片级别的请求缓存。每个分片独立地缓存查<br>询结果。该缓冲默认是开启的,默认为堆大小的1%,可以通过<br>indices.requests.cache.size选项来调整。其使用LRU淘汰策略。默认情况<br>下,只会缓存size=0(结果为空)的请求,它并不缓存命中结果<br>(hits),但是会缓存hits.total、aggregations和suggestions。<br>是否开启该缓冲可以动态调整:<br>PUT/my_index/_settings<br>{"index.requests.cache.enable":true}<br>也可以在某个请求中指定不使用缓存:<br>GET/my_index/_search?request_cache=false<br>可以使用下面的API来获取缓存使用量:
子主题
NodeQueryCache节点查询缓存由节点上的所有分片共享,也是一<br>个LRU缓存,用于缓存查询结果,只缓存在过滤器上下文中使用的查<br>询。该缓存默认开启,大小为堆大小的10%。可以通过<br>indices.queries.cache.size选项来配置大小,同时可以通过<br>index.queries.cache.enabled选项在索引级启用或禁用该缓存。<br>该缓存的使用量可以通过下面的命令来获取:<br>curl-XGET"localhost:9200/_cat/nodes?<br>v&h=query_cache.memory_size"
此外,ES进程的内存使用量还与Lucene以mmap方式加载段文件相<br>关。mmap加载的文件会被分配进程地址空间,因此它们同样算作ES占<br>用的内存,我们可以通过pmap命令查看进程都有哪些文件被映射进<br>来。通过mmap系统调用映射进来的段文件数据量通常都比较大,如果<br>mmap带的来难以控制的内存占用对系统来说是个麻烦,则可以考虑调<br>整存储类型:<br>index.store.type:niofs<br>将默认的mmapfs修改为niofs等类型,虽然使用mmap可以少一次内<br>存拷贝,但是由于目前Lucene使用mmap时不控制它的预读方式,mmap<br>会预读取2MB的数据,在随机I/O的场景中,其效率未必会高于NIO。具<br>体可以参考https://github.com/elastic/elasticsearch/issues/27748
SlowLogs
当遇到查询慢的时候,想知道对方的查询语句是什么,在日志中记<br>录所有查询语句可能会导致日志量太大,因此ES允许只将执行慢的请求<br>记录日志,“慢”的程度可以自己定义。写入慢或查询慢的原因可能会有<br>多种因素,慢日志对于我们诊断这些问题非常有用。<br>目前,ES记录了两种类型的慢日志。<br>慢搜索日志用来记录哪些查询比较慢,“慢”的程度由程序自己定<br>义,每个节点可以设置不同的阈值。ES的搜索由两个阶段组成:查询和<br>取回。慢搜索日志给出了每个阶段所花费的时间,以及整个查询内容本<br>身。
<b><font color="#ff0000">慢搜索</font></b>日志有以下索引级配置项:<br>index.search.slowlog.threshold.query.warn<br>index.search.slowlog.threshold.query.info<br>index.search.slowlog.threshold.query.debug<br>index.search.slowlog.threshold.query.trace<br>index.search.slowlog.threshold.fetch.warn<br>index.search.slowlog.threshold.fetch.info<br>index.search.slowlog.threshold.fetch.debug<br>index.search.slowlog.threshold.fetch.trace<br>index.search.slowlog.level<br>慢搜索日志可以为查询和取回阶段单独设置以时间为单位的阈值,<br>如果设置为0,则输出全部搜索日志。在定义好每个级别的时间后,通<br>过level决定输出哪个级别的日志。
日志输出内容样例如下:<br>
<b><font color="#ff0000">慢索引</font></b>日志用来记录哪些索引操作比较慢,其记录了哪些索引操作<br>耗时比较长,阈值同样可以设置。与慢搜索不同,索引内容可能非常<br>大,因此默认记录源文档内容的前1000行。可以设置为捕获整个文档,<br>或者不记录原始文档本身。<br>慢索引日志有以下索引级配置项:<br>index.indexing.slowlog.threshold.index.warn<br>index.indexing.slowlog.threshold.index.info<br>index.indexing.slowlog.threshold.index.debug<br>index.indexing.slowlog.threshold.index.trace<br>index.indexing.slowlog.level<br>index.indexing.slowlog.source<br>与慢搜索日志的配置类似,同样可以定义4种不同的慢日志级别,<br>为每个级别设置不同的时间,然后通过level来决定输出哪个级别的日<br>志。如果阈值设置为0,则将输出全部索引日志。source参数设置了日志<br>中捕获源文档的行数。
日志输出内容样例如下:<br>[2017-09-10T12:07:26,683][WARN][index.indexing.slowlog.index]<br>[GOgO9TD][testindex-slowlogs/yNbyYk1ARSW_hd0YRh6J0A]<br>took[142.3micros],took_millis[0],type[product],id[105],routing[],<br>source[{"price":9925,"name":"Nariko"}]
分析工具
I/O信息
iostat
iostat是用来分析I/O状态的常用工具,其输出结果是<br>以/proc/diskstats为基础计算的
例如,我们使用1秒的间隔来采样:<br><b><font color="#ff0000">iostat -xd 1</font></b><br>输出结果包括系统中全部磁盘的信息 . 我们经常关注的几个指标<br>
iops,由r/s(每秒读次数)和w/s(每秒写次数)组成。<br>
await,平均I/O等待时间,包括硬件处理I/O的时间和在队列中的<br>等待时间。
%util,设备的繁忙比,是设备执行的I/O时间与所经过的时间百分<br>比。当值接近100%时设备产生饱和。在设备具有并行处理能力的情况<br>下,util达到100%不代表设备没有余力处理更多I/O请求。<br>
blktrace<br>
当I/O产生性能问题时,iostat可能不足以定位故障,可以使用<br>blktrace来分析I/O请求的各个环节。该工具的原理和使用方式可以参考<br>http://bean-li.github.io/blktrace-to-report/
pidstat或iotop
进程级I/O状态iostat提供磁盘级的I/O状态,无法关联到进程,如果<br>想查看哪些进程的I/O最高,则可以使用pidstat或iotop两个工具,它们可<br>以动态给出每个进程的读写速度。<br>
例如,下面为iotop的输出结果,在统<br>计信息中给出了全部磁盘的读写速度,以及每个进程的读写速度<br>
systemtap
如果想看到特定磁盘上特定进程的I/O情况,则这两个工具无法做<br>到,这种情况可以通过systemtap来监控。
内存<br>
top、free、vmstat等工具可以帮助我们看到基础的内存信息,包括<br>物理内存总量、剩余空间、cache量等。
sar -B
我们感兴趣的是,当系统物理内存不足时,系统回收内存的效率如<br>何。在这种场景下,我们可以通过sar-B来观察内存分页的统计信息
输出结果中几个字段的含义如下
pgfree/s:每秒被放入空闲列表中的页数,如果其他进程需要内<br>存,则这些页可以被分页(pagedout)。
pgscank/s:每秒被kswapd守护进程扫描的页数。<br>
pgscand/s:每秒被直接扫描的页数。
pgsteal/s:为了满足内存需求,系统每秒从缓存(pagecache和<br>swapcache)回收的页面数。
%vmeff:代表页面回收效率:计算方式为pgsteal/(pgscand+<br>pgscank)。过低表明虚拟内存存在问题,如果在采样周期内没有发生<br>页面扫描,则该值为0或接近100。
当一个进程需要更多内存而实际空间不足时,就会发生页面扫描。<br>内核检查页面,找出哪些页面需要分页(pagedout)。独立地观察<br>pgscand、pgsteal等数值一般没太多参考意义,除非异常高。通常我们可<br>以重点关注vmeff的百分比,当值为0或接近100时代表内存够用,当值<br>比较低时,(例如,低于30%,甚至低于1%)就需要小心,这时页面回<br>收效率就存在问题。页面回收效率问题可以检查一下内核参数:<br>vm.zone_reclaim_mode。CentOS7中其默认值为0,当为1的时候,可能<br>会导致回收效率低下。我们建议确保此值为0,详细内容可参阅NUMA<br>资料。<br>关于内存分页的更多信息可以参考维基<br>Paging:https://en.wikipedia.org/wiki/Paging。
sar-W
另外一种情况,在开启了交换分区的系统上,可以通过sar-W查看<br>页面交换情况:<br>sar-W<br>09:30:01AMpswpin/spswpout/s<br>09:40:01AM3.320.00<br>09:50:01AM13.180.00<br>10:00:01AM0.540.00<br>10:10:02AM0.961.3710:20:01AM0.240.00<br>10:30:01AM0.150.00<br>10:40:01AM0.850.00<br>10:50:01AM0.160.00<br>Average:2.430.17
pswpin/s:每秒系统换入交换页面(swappage)数量。<br>
pswpout/s:每秒系统换出交换页面(swappage)数量。
发生页面交换会导致服务器性能严重下降,我们<b><font color="#ff0000">应该在生产环境关<br>闭交换分区。</font></b>
CPU信息<br>
vmstat
基本信息vmstat输出用户级(us)和内核级(sy)的CPU占用百分<br>比,以及采样周期内的上下文切换次数,blockin、blockout次数等信息<br>如下:<br>vmstat<br>
mpstat<br>
mpstat除了获取用户级(usr)和内核级(sys)的CPU占用百分比,<br>还可以输出采样周期内的软中断(soft)、硬中断(irq)占用时间的百<br>分比:<br>mpstat
strace
诊断导致CPU高的系统调用正常情况下应用程序占用用户态CPU时<br>间,如果进程占用sys比较高,则表示程序执行在内核态的操作非常耗<br>费CPU,我们可以使用一些工具来检查应用程序产生系统调用的统计信<br>息。
strace的-c参数可以统计系统调用次数和执行时间。例如:<br>strace-ooutput.txt-f-c-etrace=all-p5233<br>strace命令可以指定跟踪哪些类型的系统调用,例如,文件级、进<br>程相关、网络相关等,也可以跟踪特定的系统调用,例如,open、<br>close。我们的示例中选择跟踪所有调用。命令执行一段时间后需要手工<br>按“Ctrl+C”组合键退出,获得以下输出结果:
这个例子中显示主要的系统调用是futex,占了系统调用的83.32%,<br>花费时间为6.6秒。
perf
perf是Linux用户主要的性能分析工具,它可以做的事情很多,现在<br>我们用它分析CPU。<br>perf top用来实时显示系统最耗时的内核函数及进程,动态输出<br>TopN个结果,如下图所示。
我们还可以使用perf record来记录函数级别的统计信息,下列命令<br>对系统采样10秒,并将采样数据输出到cycle.perf文件中。<br>sudo perf record -a -ecycles -ocycle.perf -g sleep10<br>接下来,根据采样数据生成报告:<br>perf report -i cycle.perf|more<br>输出信息如下:
输出显示87%的时间花在rest_init函数上。<br>关于perf工具的更多使用方式可以参考IBM的文章:<br>https://www.ibm.com/developerworks/cn/linux/l-cn-perf1/index.html。Java<br>也有自己专用的perf工具
网络连接和流量
sar
sar是用来查看网卡流量的最常用方式,它以字节为单位输出网<br>络传入和传出流量
子主题
netstat
netstat-anp可以列出连接的详细信息,并且可以将连接、监听的端<br>口对应到进程。其中<b><font color="#ff0000">Recv-Q和Send-Q代表该连接在内核中等待发送和接<br>收的数据长度,单位为字节</font></b>。
例如,发送数据时,send调用将数据从用<br>户态复制到内核态后返回,TCP协议栈负责将数据发送出去,Send-Q<br>代表了尚未发送出去(未被对端ACK)的数据量。在未发送完之前,<br>这些数据停留在内核缓冲,原因可能是网络延时,或者对端的滑动窗口<br>限制(例如,对端没有read)。Recv-Q则代表协议栈已完成接收,但尚<br>未被应用层的read调用从内核态复制到用户态的数据长度
子主题
netstat-s 提供了各个协议下的统计信息,例如,活跃连接数、重传<br>次数、reset信息等都非常有用。部分输出信息如下所示
子主题
netstat是观察网络连接的常用工具,但是在无法处理海量网络连接<br>的情况下可以用ss代替
ss
ss与netstat功能类似,但适合处理海量连接。
sudo ss – anp
其返回信息与netstat类似:
ifconfig<br>
除了用来查看IP地址,还需要留意其中的RX/TXerrors、dropped、<br>overruns信息,大部分情况下它们没什么问题,但是当网卡流量跑满的<br>时候可能会出现意外。
子主题<br>
sysdig
sysdig可以分析系统级和进程级许多方面的状况,例如,系统调<br>用、网络统计、文件I/O等,现在我们用它捕获某个进程到某个IP的网<br>络流量。<br>通过proc.pid指定要捕获的进程,fd.cip过滤特定IP地址,evt.buffer<br>contains指定要过滤的文本内容,此处我们以捕获ES的ping请求为例
子主题
t3.xx.xx.net$T<br>sysdig还可以将捕获的数据录制为文本或二进制格式,关于该工具<br>的更多详细信息可以参考官网:http://www.sysdig.org/。
小结
大部分问题是由于比较简单的因素导致的。系统化地分析问题需要<br>故障能够重现,至少系统正处于异常状态。有些故障只经历了短暂的时<br>间,或者在故障之后无法确认之前都做了哪些操作,或者故障永远都无<br>法在测试环境中重现。这类问题我们可以仔细分析相关流程,在关键的<br>地方添加日志,或者开发特定的接口来了解到底发生了什么。
0 条评论
下一页