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