ES源码解析与优化实践
2021-12-05 21:39:34 0 举报
AI智能生成
ES源码解析与优化实战,版本基于6.X。
作者其他创作
大纲/内容
写入速度优化
ES的默认设置是综合考虑数据可靠性、搜索实时性、写入速度等因素的。
有时候,业务上对数据可靠性和搜索实时性要求并不高,反而对写入速度要求很高,此时可以调整一些策略,最大化写入速度。
提升写入速度可以从以下几方面入手:
1. 加大 translog flush 间隔,目的是降低iops、writelock。
2. 加大 index refresh 间隔,除了降低I/O,更重要的是降低了 segment merge 频率。
3. 调整 bulk 请求。
4. 优化磁盘间的任务均匀情况,将 shard 尽量均匀分布到物理主机的各个磁盘。
5. 优化节点间的任务分布,将任务尽量均匀地发送到各节点。
6. 优化 Lucene 层建立索引的过程,目的是降低CPU占用率及I/O,例如:禁用_all字段。
1. translog flush 间隔调整
ES默认设置,translog的持久化策略为:每个请求都“flush”。对应配置项如下:index.translog.durability:request。这是影响ES写入写入的最大因素。但是只有这样,写操作才有可能是可靠的。
如果系统可以接受一定概率的数据丢失,则调整 translog 持久化策略为周期性和一定大小的时候“flush”。例如: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。
2. 索引刷新间隔 refresh_interval
默认情况下索引的 refresh_interval 为1s,这意味着数据写1s后就可以被搜索到,每次索引的refresh会产生一个新的Lucene段,这会导致频繁的 segment merge 行为,如果不需要这么高的搜索实时性,应该降低索引 refresh 周期,例如:index.refresh_interval:120s
3. 段合并优化
segment merge 操作对系统 I/O 和内存占用都比较高,从ES2.0开始,merge行为不再由ES控制,而是由Lucene控制。配置:index.merge.scheduler.max_thread_count index.merge.policy.* 最大线程数max_thread_count 的默认值如下:Math.max(1, Math.min(4, Runtime.getRuntime().avaliableProcessors() / 2)) (范围:[1,4])
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,可以考虑适当降低此值。
4. Indexing buffer
indexing buffer在为doc建立索引时使用,当缓冲满时会刷入磁盘,生成一个新的segment,这是除 refresh_interval 刷新索引外,另一个生成新segment的机会。每个shard有自己的indexing buffer。
indices.memory.index_buffer_size:这个buffer大小的配置需要除以这个节点上所有shard的数量。默认为堆空间的10%
indices.memory.min_index_buffer_size:默认为48MB
indices.memory.max_index_buffer_size:默认无限制。
在执行大量的索引操作时,indices.memory.index_buffer_size的默认值可能不够,这和可用堆内存、单节点上的shard数量相关,可以考虑适当增大该值。
5. 使用bulk请求
批量写比一个索引请求只写单个文档的效率要高得多,但是要注意bulk请求的整体字节数不要太大,太大的请求可能会给集群带来内存压力,因此每个请求最好避免超过几十兆字节,即使较大的请求看上去执行得更好。
1. bulk线程池和队列
建立索引的过程属于计算密集型任务,应该使用固定大小的线程池配置,来不及处理的任务放入队列。线程池最大线程数量应配置为CPU核心数+1,可以避免过多的上下文切换。队列大小可以适当增加,但一定要严格控制大小,过大的队列导致较高的GC压力,并可能导致FGC频繁发生。
2. 并发执行bulk请求
bulk是个长任务,为了给系统增加足够的写入压力,写入过程应该多个客户端、多线程并行执行,如果要验证系统的极限写入能力,那么目标就是把CPU压满。如果CPU没有压满,则应该提高写入端的并发数量。但是要注意bulk线程池队列的reject情况,出现reject代表ES的bulk队列已满,客户端请求被拒绝,此时客户端会收到429错误,客户端对此的处理策略应该是延迟重试,应该避免产生reject。
6. 磁盘间的任务均衡
如果部署方案是为 path.data 配置多个路径来使用多块磁盘,则ES在分配shard时,落到各磁盘上的shard可能并不均匀,这种不均匀可能会导致某些磁盘繁忙,利用零在较长时间内持续达到100%。这种不均匀达到一定程度会对写入性能产生负面影响。
ES在处理多路径时,优先将shard分配到可用空间百分比最多的磁盘上,因此短时间内创建的shard可能被集中分配到这个磁盘上。
从可用空间减去预估大小:这种机制只存在于一次索引创建的过程中,下一次的索引创建,磁盘可用空间并不是上次做完减法以后的结果。
ES增加了两种策略:1.简单轮询:在系统初始阶段,简单轮询的效果是最均匀的。
2.基于可用空间的动态加权轮询:以可用空间作为权重,在磁盘之间加权轮询。
2.基于可用空间的动态加权轮询:以可用空间作为权重,在磁盘之间加权轮询。
7. 节点间的任务均衡
1. 为了节点间的任务尽量均衡,数据写入客户端应该把bulk请求轮询发送到各个节点。
2. 当使用Java API或REST API的bulk接口发送数据时,客户端将会轮询发送到集群节点。
3. 观察bulk请求在不同节点间的均衡性,可以通过cat接口观察bulk线程池和队列情况,_cat/thread_pool
8. 索引过程调整和优化
1. 自动生成doc ID:写入doc时如果外部指定了id,则ES会先尝试读取原来doc的版本号,来判断是否需要更新。
这会涉及一次读取磁盘操作,通过自动生成doc ID可以避免这个环节。
这会涉及一次读取磁盘操作,通过自动生成doc ID可以避免这个环节。
2. 调整字段Mappings:(1)减少字段的数量,对于不需要建立索引的字段,不写入ES。(2)将不需要建立索引的字段index属性设置为not_analyzed或no。对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。尤其是binary类型,默认情况下占用CPU非常高,而这种类型进行分词通常没有什么意义。(3)减少字段内容长度。如果原始数据的大段内容无需全部建立索引时,则可以尽量减少不必要的内容。(4)使用不同的分析器。不同的分析器在索引过程中运算复杂度,也有较大的差异。
3. 调整_source字段:_source字段用于存储doc原始数据,对于部分不需要存储的字段,可以通过includes/excludes过滤,或者将_source禁用,一般用于索引和数据分离。这样可以降低I/O压力,不过实际场景中大多不会禁用_source,而即使过滤掉某些字段,对于写入速度的提升作用也不大。
4. 禁用_all字段:默认不启用。_all字段中包含所有字段分词后的关键词,作用是可以在搜索的时候不指定特定字段,从所有字段中检索。_all字段占用非常大的空间,_all字段有自己的分析器,在进行某些查询时,结果不符合预期,因为没有匹配同一个分析器;由于数据重复引起的额外建立索引的开销;有些用户甚至不知道存在这个字段,导致查询混乱。禁用_all字段可以明显降低对CPU和I/O的压力。
5. 对Analyzed的字段禁用Norms。Norms用于在搜索时计算doc的评分,如果不需要评分,则可以将其禁用。
6. index_options设置。用于控制在建立倒排索引过程中,哪些内容会被添加到倒排索引,例如:doc数量、词频、positioins、offsets等信息,优化这些设置可以一定程度降低索引过程中的运算任务,节省CPU占用率。
搜索速度优化
1. 为文件系统cache预留足够的内存。
一般情况下,应用程序的读写都会被操作系统“cache”,cache保存在系统物理内存中,命中cache可以降低对磁盘的直接访问频率。搜索很依赖对系统cache的命中,如果某个请求需要从磁盘读取数据,则一定会产生相对较高的延迟。应该至少为系统cache预留一半的可用物理内存,更大的内存有更高的cache命中率。
2.使用更快的硬件。
写入性能对CPU的性能更敏感,而搜索性能在一半情况下更多的是在于I/O能力,使用SSD会比旋转类存储介质好得多。尽量避免使用NFS等远程文件系统,如果NFS比本地存储慢3倍,则搜索响应速度可能慢10倍。可能是因为搜索请求有更多的随机访问。
3.文档模型
为了让搜索时的成本更低,文档应该合理建模。特别是应该避免join操作,嵌套会使查询慢几倍,父子关系可能使查询慢数百倍,因此,如果可以通过非规范化文档来回答相同的问题,则可以显著地提高搜索速度。
4.预索引数据
针对某些查询的模式来优化数据的索引方式。例如:如果所有文档都有一个price字段,并且大多数查询在一个固定的范围上运行RANGE聚合,那么可以通过将范围“pre-indexing”到索引中并使用terms聚合来加快聚合速度。
5.字段映射
有些字段的内容是数值,但并不意味着其总是应该被映射为数值类型,例如,一些标识符,将它们映射为keyword可能会比integer或long更好,
6.避免使用脚本
一般来说,应该避免使用脚本。如果一定要用,则应该考虑painless和expressions。
7.优化日期搜索
在使用日期范围检索时,使用now的查询通常不能缓存,因为匹配到的范围一直在变化。但是切换到一个完整的日期通常是可以接受的,这样可以更好地利用查询缓存。
8.为只读索引执行force-merge
为不再更新的只读索引执行force merge,将Lucene索引合并为单个分段,可以提升查询速度。当一个Lucene索引存在多个分段时,每个分段会单独执行搜索再将结果合并,将只读索引强制合并为一个Lucene分段不仅可以优化搜索过程,对索引恢复速度也有好处。
基于日期进行轮询的索引的旧数据一般都不会再更新。应该按照一定的策略,例如:每天生成一个新的索引,然后用别名关联,或者用索引通配符。这样,可以每天选一个时间点对昨天的索引执行force-merge、shrink等操作。
9.预热全局序号(global ordinals)
全局序号是一种数据结构,用于在keyword字段上运行terms聚合。它用一个数值来代表字段中的字符串值,然后为每一数值分配一个bucket。这需要一个对global ordinals和bucket的构建过程。默认情况下,它们被延迟构建,因为ES不知道哪些字段将用于terms聚合,哪些字段不会。可以通过配置映射在刷新(refresh)时告诉ES预先加载全局序数。
10.execution hint
terms聚合有两种不同的机制:1.通过直接使用字段值来聚合每个桶的数据(map)。2.通过使用字段的全局序号并为每个全局序号分配一个bucket(global_ordinals)。
ES使用global_ordinals作为keywords字段的默认选项,它使用全局序号动态地分配bucket,因此内存使用与聚合结果中的字段数量是线性关系。在大部分的情况下,这种方式的速度很快。
当查询只会匹配少量文档时,可以考虑使用map。默认情况下,map只在脚本上运行聚合时使用,因为它们没有序数。
11.预热文件系统cache
如果ES主机重启,则文件系统缓存将为空,此时搜索会比较满。可以使用 index.store.preload 设置,通过指定文件扩展名,显示地告诉操作系统应该将哪些文件加载到内存中。
如果文件系统缓存不够大,则无法保存所有数据,那么为太多文件预加载数据到文件系统缓存中会使搜索速度变慢,应谨慎使用。
12.转换查询表达式
在组合查询中可以通过 bool 过滤器进行 and、or 和 not 的多个逻辑组合检索,这种组合查询中的表达式在下面的情况下可以做等价转换:(A|B)&(C|D)==》(A&C)|(A&D)|(B&C)|(B&D)
13.调节搜索请求中的batched_reduce_size
该字段是搜索请求中的一个参数。默认情况下,聚合操作在协调节点需要等所有的分片都取回后才执行,使用batched_reduce_size参数可以不等待全部分片返回结果,而是在指定数量的分片返回结果之后就可以先处理一部分(reduce)。这样可以避免协调节点在等待全部结果的过程中占用大量内存,避免极端情况下可能导致的OOM。
14.使用近似聚合
近似聚合以牺牲少量的精确度为代价,大幅度提高了执行效率,降低了内存使用。
15.深度优先还是广度优先
ES有两种不同的聚合方式:深度优先和广度优先。深度优先是默认设置,先构建完整的树,然后修剪无用节点。大多数情况下深度聚合都能正常工作,但是有些特殊的场景更适合广度优先,先执行第一层聚合,再继续下一层聚合之前会先做修剪。
16.限制搜索请求的分片数
一个搜索请求设计的分片数量越多,协调节点的CPU和内存压力就越大。默认情况下,ES会拒绝超过1000个分片的搜索请求。如果想调节这个值,通过action.search.shard_count配置项进行修改。
虽然限制搜索的分片数不能直接提升单个搜索请求的速度,但协调节点的压力会间接影响搜索速度。例如:占用更多内存会产生更多的 GC 压力,可能导致更多的 stop-the-world 时间等,因此间接影响了协调节点的性能。
17.利用自适应副本选择(ARS)提升ES响应速度
为了充分利用计算资源和负载均衡,协调节点将搜索请求轮询转发到分片的每个副本,轮询策略是负载均衡过程中最简单的策略,任何一个负载均衡器都具备这种基础的策略,缺点是不考虑后端实际系统压力和健康水平。
ES的ARS实现基于这样一个公式:对每个搜索请求,将分片的每个副本进行排序,以确定哪个最可能是转发请求的“最佳”副本。与轮询方式向分片的每个副本发送请求不同,ES选择“最佳”副本并将请求路由到那里。
ES6.1 默认关闭。ES7.0,默认开启。cluster.routing.use_adaptive_replica_selection:true
磁盘使用量优化
优化磁盘使用量与建立索引时的映射参数和索引元数据字段密切相关。
1.元数据字段
每个文档都有与其相关的元数据,比如_id。当创建映射类型时,可以定制其中一些元数据字段。1. _source:原始JSON文档数据。2. _all:索引所有其他字段值的一种通用字段,这个字段中包含了所有其他字段的值。允许在搜索的时候不指定特定的字段名,意味着“从全部字段中搜索”。
_all字段是一个全文字段,有自己的分析器。ES6.0之后该字段被禁用。
2.索引映射参数
索引创建时可以设置很多映射参数。1.index:控制字段值是否被索引。未被索引的字段不会被查询到,但是可以聚合。
2.doc values:默认情况下,大多数字段都被索引,这使得它们可以搜索。倒排索引根据term找到文档列表,然后获取文档原始内容。但是排序和聚合,以及从脚本中访问某个字段值,需要不同的数据访问模式,它们不仅需要根据term找到文档,还要获取文档中字段的值。这些值需要单独存储。
doc_values就是用来存储这些字段值的。它是一种存储在磁盘上的列式存储,在文档索引时创建,这使得上述数据访问模式成为可能。它们以面向列的方式存储与_source相同的值,使得排序和聚合效率更高。几乎所有字段类型都支持doc_values,但被分析的字符串除外。doc_values默认启用。
3. store:默认情况下,字段值会被索引使他们能搜索,但它们不会被存储(stored)。意味着可以通过这个字段查询,但不能取回它的原始值。
在某些情况下,存储字段是有意义的。例如:如果有一个包含标题、日期和非常多的内容字段的文档,则可能希望只检索标题和日期,而不需要从大型_source字段中提取这些字段。
doc_values和存储字段都属于正排内容。
stored fields被设计为优化存储,doc_values被设计为快速访问字段值。搜索可能会访问很多doc values中的字段,所以比必须能够快速访问,我们将doc_values用于聚合、排序,以及脚本中。
另一方面,存储字段仅用于返回前几个最匹配文档的字段值,默认情况下ES只将其用于这种情况,解压存储字段,将其发送给客户端。为少量文档获取存储字段还好。它不能再查询的时候使用,否则会让查询变得非常慢。
3.优化措施
禁用对你来说不需要的特性
默认情况下,ES为大多数的字段建立索引,并添加到doc_values,以便可以被搜索和聚合。但有时候不需要通过某些字段过滤,可以添加"index": false参数
text类型的字段会在索引中存储归一因子,以便对文档进行评分。如果只需在文本字段上进行匹配,而不关心生成的得分,则可以配置ES不讲norms写入索引。("norms":false)
text类型的字段默认情况下也在索引中存储频率和位置。频率用于计算得分,位置用于执行短语(phrase)查询。如果不需要运行短语查询:("index_options": "freqs")
在text类型的字段上,index_options的默认值为positions。index_options参数用于控制添加到倒排索引中的信息。
freqs文档编号和词频被索引,词频用于为搜索评分,重复出现的词条比只出现一次的词条评分更高。position文档编号、词频和位置被索引。位置被用于邻近查询和短语查询。
如果也不关心评分,则可以将ES配置为只为每个term索引匹配的文档。仍然可以在这个字段上搜索,但是短语查询会出现错误,评分将假定在每个文档中只出现一次词汇。("norms": false, "index_options": "freqs")
禁用doc values
所有支持dov values的字段都默认启用了doc value。如果确定不需要对字段进行排序或聚合,或者从脚本访问字段值,则可以禁用doc value以节省磁盘空间。("doc_values":false)
不要使用默认的动态字符串映射
默认的动态字符串映射会把字符串类型的字段同时索引为text和keyword。通常,id字段只需作为keyword类型进行索引。要禁用默认的动态字符串映射,则可以显示地指定字段类型,在动态模板中指定将字符串映射为text或keyword。
观察分片大小
较大的分片可以更有效地存储数据。缺点:较长的索引恢复时间。
禁用_source
_source字段存储文档的原始内容,如果不需要访问它,则可以将其禁用。
使用best_compression
_source和设置为"store":true的字段占用磁盘空间都比较多。默认情况下,它们都是被压缩存储的,默认的压缩算法为LZ4,可以通过使用best_compression来执行压缩比更高的算法:DEFLATE。但这会占用更多的CPU资源。
Fource Merge
较大的Lucene分段可以更有效地存储数据。使用_forcemerge API来对分段执行合并操作,通常,我们将分段合并为一个单个的分段:max_num_segments = 1
Shrink Index
Shrink API允许减少索引的分片数量,结合上面的Force Merge API,可以显著减少索引的分片和Lucene分段数量。
数值类型长度够用就好
为数值类型选择的字段类型也可能会对磁盘使用空间产生较大影响。
使用索引排序来排列类似的文档
当ES存储_source时,它同时压缩多个文档以提高整体压缩比。例如:文档共享相同的字段名,或者它们共享一些字段值,特别是在具有低基数或zipfian分布的字段上。
默认情况下,文档按照添加到索引中的顺序压缩在一起。如果启用了索引排序,那么它们将按排序顺序压缩。对具有相似结构、字段和值的文档进行排序可以提高压缩比。
在文档中以相同的顺序放置字段
由于多个文档被压缩成块,如果字段总是以相同的顺序出现,那么在这些_source文档中可以找到更长的重复字符串的可能性更大。
测试数据
1.禁用_source,空间占用量下降30%左右。2.禁用doc values,空间占用量下降10%左右;3.压缩算法将LZ4改为Deflate,空间占用量可以下降15%~25%。
综合应用实战
集群层
1.规划集群规模
在部署一个新集群时,应该根据多方面的情况评估需要多大的集群规模来支撑业务。
根据具体业务情况来评估初始集群大小。包括:数据总量,每天的增量;查询类型和搜索开发,QPS;SLA级别。
控制最大集群规模和数据总量。1. 节点总数不应该太多,最大集群规模最好控制在100个节点左右。2. 单个分片不要超过50GB,最大集群分片总数控制在几十万的级别。太多分片同样增加了主节点的管理负担,而且集群重启恢复时间会很长。
建议单个点是数据总量不超过5TB。这是为了让JVM使用率及GC时间保持在一个合理水平。一般数据样本中1TB索引的FST要占用2GB的JVM内存。5TB索引就占用10GB内存。如果节点长时间GC,会导致一个简单的请求都很慢,且主节点发布集群状态也会因为这个节点而长时间等待,因此控制GC时长很重要。
可以使用冷冻索引来节约内存占用。未来的Lucene版本中考虑将倒排索引放到堆外加载,值得期待。
2.单节点还是多节点部署
ES不建议为JVM配置超过32GB的内存。超过32GB时,Java内存指针压缩失效,比较安全的是设置为30GB。可以在JVM参数中添加”-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompressedOoopsMode“来确认,如果输出信息包含”zero based Compressed Oops“字样,则代表指针压缩生效。
当物理主机内存在64GB以上,并且拥有多个数据盘,不做raid的情况下,部署ES节点有两点建议:1. 部署单个节点,JVM内存配置不超过32GB,配置全部数据盘。这种部署模式的缺点是多余的物理内存只能被cache使用,而且只要存在一个坏盘,节点重启就会无法启动。2.使用内存大小除以64GB来确定要部署的节点数,每个节点配置一部分数据盘,优点是利用率高,缺点是部署复杂。
目前ES对于磁盘管理方面有待改进,坏盘不会从系统中排除出去,导致节点无法启动,并且部分场景下可能会产生热点数据集中在少数磁盘的情况,如果这些问题很严重,也可以考虑一些raid方案。
3.移除节点
当由于坏盘、维护等故障需要下线一个节点时,我们需要先将节点的数据迁移,这可以通过分配过滤器实现。
当执行完下线命令后,分片开始迁移,可以通过_cat/shard API来查看该节点的分片是否迁移完毕。当节点维护完毕,重新上线之后,需要取消排除设置,以便后续的分片可以分配到node-1节点上。
4.独立部署节点
将主节点和数据节点分离部署最大的好处是Master切换过程可以迅速完成,有机会跳过gateway和分片重新分配的过程。
例如:有3台具备Master资格的节点独立部署,然后关闭当前活跃的主节点,新主当选后由于内存中持有最新的集群状态,因此可以跳过gateway的恢复过程,并且由于主节点没有存储数据,所以旧的Master离线不会产生未分配状态的分片。新主当选后集群状态可以迅速变为Green。
节点层
1.控制线程池的队列大小
不要为bulk和search分配过大的队列,队列并非越大越好,队列缓存的数据越多,GC压力越大,默认的队列大小基本够用了。
如果每个请求的数据量都非常小,可能需要增加队列大小。但是推荐写数据时组合较大的bulk请求。
2. 为系统cache保留一半物理内存
搜索操作很依赖于系统cache的命中,标准的建议是把50%的可用内存作为ES的堆内存,为Lucene保留剩下的50%,用作系统cache。
系统层
1.关闭swap
个人PC上,交换分区或许有用,如果物理内存不够,则交换分区可以让系统缓存运行。但是在服务器系统上,无论物理内存多么小,哪怕只有1GB,都应该关闭交换分区。
当服务程序在交换分区上缓慢运行时,往往会产生更多不可预期的错误,因此当一个申请内存的操作如果真的遇到物理内存不足时,宁可让它直接失败。
一般在安装操作系统的时候直接关闭交换分区,或者通过swapoff命令来关闭。
2.配置Linux OOM Killer
OOM并非JVM的OOM,而是Linux操作系统的OOM。在Linux下,进程申请的内存并不会立刻为进程分配真实大小的内存,因为进程申请的内存不一定全部使用,内核在利用这些空闲内存时采取过度分配的策略,假如物理内存为 1GB,则两个进程都可以申请1GB的内存,这超过了系统的实际内存大小。当应用程序实际消耗完内存的时候,系统需要”杀掉“一些进程来保障系统正常运行。这就出发了 Linux OOM Killer。
如果ES与其他服务混合部署,当系统产生OOM的时候,ES有可能会无辜被”杀“。为了避免这种情况,可以在用户态调节一些进程参数来让某些进程不容易被OOM Killer”杀掉“。如果不希望被杀,设置进程的oom_score_adj。
3.优化内核参数
在生产环境上,我们可以根据自己的场景调节内核参数,让搜索服务更有效率地运行。Linux默认的TCP选项不一定完全合适,因为它需要考虑在互联网上传输时可能出现的更大的延迟和丢包率。因此可以调节一些TCP选项,让TCP协议在局域网上更高效。调节内核参数可以通过两种方式:1.临时设置,系统重启后失效。通过sysctl -w来设置。2.永久设置,将参数写入配置文件/etc/sysctl.conf,然后执行 sysctl -p使其生效。
1.TCP相关参数
net.ipv.tcp_syn_retries:默认为6,参考值为2。主机作为客户端,对外发起TCP连接时,即三次握手的第一步,内核发送SYN报文的重试次数,超过这个次数后放弃连接。
net.ipv.tcp_synack_retries:默认5,参考值2。主机作为服务端,接受TCP连接时,在三次握手的第二步,向客户端发送SYN+ACK报文的重试次数,超过这个次数后放弃连接。
net.ipv.tcp_timestamps:默认1,参考值1。是否开启时间戳,开启后可以更精准地计算RTT,一些特性也依赖时间戳字段。
net.ipv.tcp_tw_reuse:默认0,建议1。是否允许将处于TIME_WAIT状态的socket用于新的TCP连接。这对于降低TIME_WAIT数据很有效。该参数只有在开启tcp_timestamps的情况下才会生效。
net.ipv.tcp_syn_recycle:默认0,参考值0。是否开启TIME_WAIT套接字的快速回收,这势必tcp_tw_reuse更激进的一种方式,它同样依赖tcp_timestamps选项。强烈建议不要开启tcp_tw_recycle。原因:1.TIME_WAIT是十分必要的状态,避免关闭中的连接与新建连接之间的数据混淆;2.tcp_tw_recycle选项在NAT环境下会导致一些二新建连接被拒绝,因为NAT下每个主机存在时差,这体现在套接字中的时间戳字段,服务端会发现某个IP上的本应递增的时间戳出现降低的情况,时间戳相对降低的报文将被丢弃。
net.core.somaxconn:默认128,参考2048。定义了系统中每一个端口上最大的监听队列的长度。当服务器监听了某个端口时,操作系统内部完成对客户端连接请求的三次握手。这些已建立的连接存储在一个队列中,等待accept调用取走。本选项就是定义这个队列的长度。该队列实际大小取决于listen调用传入的第二个参数:backlog和本选项的最小值:min(backlog,somaxconn)。ES需要建立许多连接,当集群节点数比较大,集群完全重启时可能会在瞬间建立大量连接,默认的连接队列长度可能不够用,因此适当提高此值。
net.ipv.tcp_max_syn_backlog:默认128,参考值8192。内核会为服务端的连接建立两个队列:1. 已完成三次握手,连接建立,等待accept的队列,全局长度由somaxconn定义。2. 三次握手执行到第二步,等待客户端返回ACK,这些未完成的连接单独放到一个队列中,由tcp_max_syn_backlog定义队列大小。由于可能会有较多的连接数,我们适度增加”未完成连接“的队列大小。
net.ipv.tcp_max_tw_buckets:默认4096,参考值180000。定义系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数,则TIME_WAIT套接字将立刻被清除并打印警告信息。如果系统被TIME_WAIT过多问题困扰,则可以调节tcp_max_tw_buckets、tcp_t_reuse、tcp_timestamps三个选项来缓解。TIME_WAIT状态产生在TCP会话关闭时主动关闭的一端,如果向从根本上解决问题,则让客户端主动关闭连接,而非服务端。
net.ipv.tcp_max_orphans:默认4096,参考值262144。定义最大孤儿套节字(未附加到任何用户文件句柄的套接字)数量。如果孤儿套接字超过此值,则这些连接立即”reset“,并显示警告信息。该值可以简单地抵御DOS攻击,但不能通过降低此值来抵御DOS。为了应对高负载,应该提高此值。
2. TCP的接受窗口(RWND)
TCP采用两个基本原则决定何时发送及发送多少数据:流量控制:为了确保接收者可以接收数据。拥塞控制:为了管理网络带宽。
流量控制通过在接收方指定接收窗口大小来实现。接收窗口用于接收端告诉发送端,自己还有多大的缓冲区可以接收数据。发送端参考这个值来发送数据,就不会导致客户端处理不过来。
接收窗口的大小可以通过内核参数来调整,其理想值时BDP:服务端可以发出的未被客户端确认的数据量,也就是在网络上缓存的数据量。
网络连接通常以管道为模型,BDP是带宽与RTT的乘积,表示需要多少数据填充管道。BDP = TX ✖ RTT。例如:在千兆的网络上,RTT为10ms,那么BDP = (1000/8)✖ 0.01s = 1.25MB。
net.ipv4.tcp_rmem = <MIN><DEFAULT><MAX>:调整RWND设置。
默认情况下,系统会在最大值和最小值之间自动调整缓冲区大小,是否自动调整通过tcp_moderate_rcvbuf选项来决定。在开启缓冲自动调整的情况下,可以把最大值设置为BDP。
TCP使用2个字节记录窗口大小,因此最大值为64KB,如果超过这个值,则需要使用tcp_window_scaling机制,通过设置开启。net.ipv4.tcp_window_scaling = 1
RWND和CWND可能是让系统达到最大吞吐量的两个限制因素。
3. TCP的拥塞窗口(CWND)
TCP的滑动窗口机制依据接收端的能力来进行流控,并不能感知网络延迟等网络因素。拥塞控制机制会评估网络能承受的符合,避免过量数据发送到网络中,拥塞程度会涉及主机、路由器等网络中的所有因素。
拥塞控制由发送方实现,发送方会将它传输的数据流量限制为CWND和RWND的最小值。CWND会随着时间和对端的ACK增长,如果检测到网络拥塞,则缩小CWND。拥塞控制主要有4种算法:慢启动、拥塞避免、快速重传和快速恢复。
慢启动:对于刚建立的连接,开始发送数据时,一点点提速,而不是一下子使用很大的带宽。慢启动时指数上升的过程,直到CWND>=ssthresh,进入拥塞避免算法。
调节初始拥塞窗口(INTCWND)的大小。适度增加INTCWND可以降低HTTP响应延迟。
例如:HTTP要返回的内容为20K,MSS(最大报文段长度)的大小为1460,整个内容需要传送15个MSS。当INTCWND为3时,服务端先发送3个MSS,1460×3=4380字节,待客户端ACK后,根据指数增加算法。第二次发送9个MSS,1460×9=13140字节,第三次发送3个MSS的剩余字节。整个传输过程经过了3次RTT。如果INTCWND设置为15,则只需要一次RTT就可以完成传输。
如果系统上的INTCWND低于10,可以使用ip命令调整。
4.vm相关参数
文件的读和写操作都会经过操作系统的cache,读缓存时比较简单的,写缓存相对复杂。一般情况下,写文件的数据先到系统缓存(page cache),再由系统定期异步地刷入磁盘,这些存储与page cache中尚未刷盘的数据成为脏数据(脏页,dirty page)。写缓存可以提升I/O速度,但存在数据丢失的风险。例如:尚未刷盘的时候主机断电。
从page cache刷到磁盘有以下三种时机:1,可用物理内存低于特定阈值时,为了给系统腾出空闲内存;2.脏页驻留书简超过特定阈值时,为了避免脏页无限期驻留内存;3.被用户的sync()或fsync()触发。
由系统执行的刷盘有两种写入策略:1.异步执行刷盘,不阻塞用户I/O;2. 同步执行刷盘,用户I/O被阻塞,直到脏页低于某个阈值。一般情况下,系统先执行第一种策略,当脏页数据量过大,异步执行来不及完成刷盘时,切换到同步方式。我们可以通过内核参数调整脏数据的刷盘阈值:1. vm.dirty_background_rtio,默认10。定义了一个百分比。当内存中的参数超过这个百分比后,系统使用异步方式刷盘。2. vm.dirty_ratio,默认30。定义了一个百分比,当内存中的脏数据超过这个百分比后,系统使用同步方式刷盘,写请求被阻塞,直到脏数据低于dirty_ration。如果还高于dirty_background_ratio,则切换到异步方式刷盘。因此dirty_ratio应高于dirty_background_ratio。
vm.dirty_expire_centisecs:默认值3000(30s),单位为百分之1s,定义脏数据的过期时间,超过这个时间后,脏数据被异步刷盘。
vm.dirty_writeback_centisecs:默认500(5s),单位百分之1s,新系统周期性地启动线程来检查是否需要刷盘,该选项定义这个间隔时间。
如果数据安全性要求没有那么高,想要多”cache“一些数据,让读取更容易命中,则可以增加脏数据占比和过期时间:vm.dirty_background_ratio = 30;vm.dirty_ratio = 60;vm.dirty_expire_centisecs = 6000,反之则可以降低它们。如果只希望写入过程不要被系统的同步刷盘策略影响,则可以让系统多容纳脏数据,当早一些触发异步刷盘。这样也可以让I/O更平滑:vm.dirty_background_ratio = 5;vm.dirty_ratio = 60
5. 禁用透明大页(Transparent Hugepages)
透明大页是Linux的一个内核特性,它通过更有效地使用过处理器的内存映射硬件来提高性能,默认情况下是启用的。透明大页能略微提升程序性能,但是也可能对程序产生负面影响,甚至是严重的内存泄露。为了避免这些问题,我们应该禁用它。
索引层
1. 使用全局模板
从ES5.X开始,索引级别的配置需要写到模板中,而不是.yml文件,但是我们需要一些索引级别的全局设置信息,例如,translog的刷盘方式等,因此我们可以将这些设置编写到一个模板中,并让这些模板匹配全部索引”*“,这个模板我们称为全局模板。
2. 索引轮转
如果有一个索引每天都有新增内容,那么不要让这个索引持续增大,建议使用日期等规则按一定频率生成索引。同时将索引设置写入模板,让模板匹配这一系列的索引,还可以为索引生成一个别名关联部分索引。
3. 避免热索引分片不均
默认情况下,ES的分片均衡策略是尽量保证各个节点分片数量大致相同。但是当集群扩容时,新加入集群的节点没有分片,此时新创建的索引分片会集中在新节点上,这导致新节点有太多热点数据,该节点可能会面临巨大的写入压力。因此,对于一个索引的全部分片,我们需要控制单个节点上存储的该索引的分片总数,使索引分片在节点上分布得更均匀一些。
例如:10个节点的集群,索引主分片数为5,副本数量为1,那么平均下来应该有(5×2)/10=1个分片,考虑到节点故障、分片迁移的情况,可以设置节点分片总数为2:("routing.allocation.total_shards_per_node":2)
4. 副本数选择
大部分场景下,将副本数number_of_replicas设置为1即可。如果对搜索请求的吞吐量要求较高,则可以适当增加副本数量。如果在项目初始阶段不知道多少副本数够用,则可以先设置为1,后期再动态调整。对副本数的调整只会设计数据复制和网络传输,不会重索引,因此代价较小。
5. Force Merge
对冷索引执行Force Merge有许多好处:1. 单一的分段比众多分段占用的磁盘空间更小一些;2. 可以大幅度减少进程需要打开的文件fd;3. 可以加快搜索的过程,因为搜索需要检索全部分片;4. 单个分段加载到内存时也比多个分段更节省内存占用。可以选择在系统的空闲时间段对不再更新的只读索引执行Force Merge。
段合并命令将分段合并为单个分段,执行成功后会自行”flush“。理想情况下,Force Merge为单一的分段比较好,因为ES要为非单一分段创建全局序数,占用较多fielddata空间。如果业务低谷开始执行的Force Merge到了业务高峰期还没Merge完毕,则需要调整目标分段数量。
6. Shrink Index
分片数越多集群压力越大。创建索引时,为索引分配了较多的分片,但可能实际数据量并没有多大,对这种索引可以执行Shrink操作来降低索引分片数量。
可以为Shrink Index和上一节的Force Merge编写自动运行脚本,通过crontab选择在凌晨的某个时间对索引进行优化。然后将定时任务添加到普通用户的定时任务中:crontab escron。通常会为部署集群编写部署脚本,这些工作都可以放到部署脚本中。
7. close 索引
如果有些索引暂时不使用,则不会再有新增数据,也不会对它的查询操作,但是可能以后会用而不能删除,那么可以把这些索引关闭,在需要时再打开。关闭的索引除存储空间外不占用其他资源。
8. 延迟分配分片
当一个节点由于某些原因离开集群时,默认情况下ES会重新确定主分片,并立即重新分配缺失的副分片。但是,一般来说,节点离线是常态,可能由于网络原因、主机断电、进程退出等因素,重新分配副分片的操作代价是很大的,该节点上存储的数据需要再集群上重新分配,赋值这些数据需要大量带宽和时间,因此我们调整节点离线后分片重新分配的延迟时间:index.unassigned.node_left.delayed_timeout:5d。
节点离线一般是暂时的,如果因为硬件故障,则修复时间是可以预期的。
9. 小心地使用fielddata
聚合时,ES通过doc_values获取字段值,但是text类型不支持doc_values。当在text字段上聚合时,就会依赖fielddata数据结构,但fielddata默认关闭。因为它会消耗很多堆空间,并且再text类型字段上聚合通常没有什么意义。
doc_values在索引文档时就会创建,而fielddata是在聚合、排序,或者脚本中根据需要动态创建的。其读取每个分段中的整个倒排索引,反转term和doc的关系,将结果存储到JVM堆空间,这是非常昂贵的过程,会让用户感到明显的延迟。
fielddata所占用的大小默认没有上限,可以通过indices.fielddata.cache.size来控制,该选项设置一个堆内存的百分比,超过这个百分比后,使用LRU算法将老数据淘汰。
客户端
1. 使用REST API而非 Java API
Java API将在未来的版本中废弃,客户端最好选择REST API作为客户端。
2. 注意429状态码
bulk请求被放入ES队列,当队列满时,新请求被拒绝,并给客户端返回429的状态码。客户端需要处理这个状态码,并在稍后重发请求。此刻客户端需要处理bulk请求中部分成功、部分失败的情况。
这种情况产生在协调节点转发基于分片的请求到数据节点时,有可能因为对方的bulk队列满而拒绝写操作,而其他数据节点正常处理,于是客户端的bulk请求部分写入成功、部分写入失败。客户端需要将返回429的对应数据重试写入,而不是全部数据,否则写入的内容就会存在重启。
产生429错误是因为ES来不及处理,一般是由于写入端的并发过大导致的,建议适当降低写入并发。
3. curl的HEAD请求
curl -X HEAD只是将HTTP头部的方法设置为HEAD,还会等待服务器返回body,所以现象就是curl命令阻塞在那里,正确的方式应该是使用-I参数curl -H " "。使用-I参数会将HTTP方法设置为HEAD,并在收到服务器返回的HTTP头部信息后关闭TCP连接。
4. 了解你的搜索计划
只有了解搜索指令的执行代价,才能更好地使用ES进行搜索。例如:尽量让少的分片参与工作,如果要检索当天的内容,则只搜索当天的索引即可。除了认为评估查询语句,还可以使用Profile API分析会命中哪些分片,每个分片执行的查询时间等细节。
5. 为读写请求设置比较长的超时时间
读写操作都有可能是比较长的操作,例如:写一个比较大的bulk数据,或者执行较大范围的聚合。此时客户端应该为请求设置的超时时间应该尽量长,因为即使客户端断开连接,ES仍然会在后台将请求处理完,如果超时设置比较短,则在密集的请求时会对ES造成非常大的压力。
读写
1. 避免搜索操作返回巨大的结果集
由于协调节点的合并压力,所有的搜索系统都会限制返回的结果集大小,如果确实需要很大的结果集,则应该使用Scroll API。
2. 避免索引巨大的文档
http.max_context_length的默认值为100MB,ES会拒绝索引超过此大小的文档,可以增加这个值,但Lucene仍然大约有2GB的限制。大型文档给网络、内存和磁盘造成了更大的压力。
例如:想要为一本书建立索引使之可以被搜索,这并不意味着把整本书的内容作为单个文档进行索引。最好使用章节或段落作为文档,然后在文档中加一个属性标识它们属于哪本书。这样避免了大文档的问题。
3. 避免使用多个_type
_type本来是用于区分存储到同一个索引中的不同格式的数据,但是实际上面这种情况应该用不同的索引解决,而不是在一同一个索引中使用不同的_type。
4. 避免使用_all字段
默认禁用。此类需求可以通过mapping中的copy_to参数创建自定义的_all字段。
5. 避免将请求发送到同一个协调节点
无论索引文档还是执行搜索请求,客户端都应该避免将请求发送到固定的某个或少数几个节点,因为少数几个协调节点作为整个集群对外的读写节点的情况下,它们很可能承受不了那么多的客户端请求。尤其是搜索请求,协调节点的合并及排序会占用比较高的内存和CPU,聚合会占用更多内存。因此会导致给客户端的返回慢,甚至导致节点OOM。
正确的做法是将请求轮询发送到集群所有节点,如果使用REST API,则可以在构建客户端的客户端对象时传入全部节点列表。
如果在前端或脚本中访问ES集群,则可以部署LVS,客户端使用虚IP,或者部署Nginx使用反向代理。
控制相关度
通过Painless脚本控制搜索评分。
ES有多种方式控制对搜索结果的评分,如果常规方式无法得到想要的评分结果,则可以通过脚本的方式完全自己实现评分算法,以得到预期的评分结果。ES自己实现的专用语言:Painless。Painless是内置支持的,脚本内容通过REST接口传递给ES,ES将其保存在集群状态中。6.X版本只能通过REST接口写入。
故障诊断
使用Profile API定位慢查询
Profile API返回所有分片的详细信息。
1.Query:Query段由构成Query的元素以及它们的时间信息组成。
2. Rewrite Time:由于多个关键字会分解以创建个别查询,所以在这个过程中肯定会花费一些时间。将查询重写一个或多个组合查询的时间被称为”重写时间“。
3. Collectors:在Lucene中,收集器负责收集原始结果,并对它们进行组合、过滤、排序等处理。
使用Explain API分析未分配的分片
一个ES索引由多个分片组成,由于某些原因,某些分片可能会处于未分配状态,导致集群健康处Yellow或Red状态。导致分片处于未分配的原因可能是节点离线、分片数量设置错误等原因。
Explain API可以分析出当前的分片分配情况。这个API主要解决了下面两个问题:1. 对于未分配的分片,给出为什么没有分配的具体原因;2. 对于已分配的分片,给出为什么将分片分配给特定节点的理由。
诊断未分配的主分片
诊断未分配的副分片
诊断已分配的分片
如果想将分片手工迁移到某个节点,但是出于某些原因分片没有迁移。
节点CPU使用率高
节点占用CPU很高,想知道CPU正在运行什么任务,一般通过线程堆栈来看。
两种方式查看哪些线程CPU占用率比较高。1. 使用hot_threads API;2. 使用top+jstack,top获取线程级CPU占用率,再根据线程ID,配合jstack定位CPU占用率在特定比例上的线程堆栈。
节点内存使用率高
节点内存使用率高,我们想知道内存是被哪些数据结构占据的,通用方式是用jmap导一个堆出来,加载到MAT中进行分析,可以精准定位数据结构占用内存大小,以及被哪些对象引用。
jmap导出的堆可能非常大,操作比较花时间,ES中有几个占用内存比较大的数据结构,有些数据结构无法看到其当前的实际大小,只能通过设置的上限粗略评估。
bulk队列。可以通过_cat API查看bulk队列中当前的使用量,任务总数乘以bulk请求的大小就是占用内存的大小。如果队列设置很大,则在写入压力大的时候就会导致比较高的内存占用。默认值为200,一般情况下都够用了。
Netty缓冲。在一些特别的情况下,Netty的内存池也可能会占用比较高的内存。Netty收到一个客户端请求时,为连接分配内存池,客户端发送的数据存储到Netty的内存池中,直到ES层处理完上层逻辑,回复客户端时,才释放内存。
当ES收到客户端请求时,如果在处理完毕之前客户端关闭连接,则ES依然会把这个请求处理完,只是最后才出现回复客户端失败。这个过程可能会导致内存累积。例如:执行bulk请求时,客户端发送完毕,不等ES返回响应就关闭连接,然后立即发起下一个请求,结果这些请求实际上都在等待处理,就可能占用非常多的内存。所以客户端的请求超时时间应该尽量设置得长一些,建议设置为分钟级。
Indexing buffer。索引写入缓冲用于存储索引好的文档数据,当缓冲满时,生成一个新的Lucene段。在一个节点上,该节点的全部分片共享indexing buffer。该缓冲默认大小为堆内存10%,加大该缓冲需要考虑到对GC的压力。
超大数据集的聚合。协调节点对检索结果进行汇总和聚合,当聚合设计的数据量很大时,协调节点需要拉取非常多的内容,大范围的聚合是导致节点OOM的常见原因之一。
分段内存。一个Lucene分段就是一个完整的倒排索引,倒排索引由单词词典和倒排列表组成。在Lucene中,单词词典中的FST结构会被加载到内存。因此每个分段都会占用一定的内存空间。(API:查看每个节点上的所有分段占用的内存总量;单独查看每个分段的内存占用量。)
Fielddata cache。在text类型字段上进行过聚合和排序时会用到Fielddata,默认是关闭的。
Shard request cache。分片级别的请求缓存。每个分片独立地缓存查询结果。该缓冲默认是开启的,默认为堆大小的1%。(可以通过 indices.request.cache.size 调整。)其使用LRU淘汰策略。默认情况下,只会缓存 size = 0 的请求,它并不缓存命中结果(hits),但是会缓存 hits.total、aggregations和suggestions。
Node Query Cache。节点查询缓存由节点上的所有分片共享,也是一个LRU缓存,用于缓存查询结果,只缓存在过滤器上下文中使用的查询。该缓存默认开启,大小为堆大小的10%。(indices.queries.cache.size可以配置大小;index.queries.cache.enabled在索引级启用或禁用该缓存;)
ES进程的内存使用量与Lucene以mmap方式加载段文件相关。mmap加载的文件会被分配进程地址空间,因此它们同样算作ES占用的内存,可以通过mmap命令查看进程都有哪些文件被映射进来。通过mmap系统调用映射进来的段文件数据量通常都比较大,如果mmap带的来难以控制的内存占用对系统来说是个麻烦。
Slow Logs
当遇到查询慢的时候,想知道对方的查询语句是什么,在日志记录所有查询语句可能会导致日志量太大,因此ES允许只将执行慢的请求记录日志,”慢“的程度可以自己定义。
ES记录了两种类型的慢日志。
慢搜索日志。用来记录哪些查询比较慢,”慢“的程度由程序自己定义,每个节点可以设置不同的阈值。ES的搜索由两个阶段组成:查询和取回。慢搜索日志给出了每个阶段所花费的时间,以及整个查询内容本身。慢搜索日志可以为查询和取回阶段单独设置以时间为单位的阈值,如果设置为0,则输出全部搜索日志。在定义好每个级别的时间后,通过level决定输出哪个级别的日志。
慢索引日志。用来记录哪些索引操作比较慢,其记录了哪些索引操作耗时比较长,阈值可以设置。与慢搜索不同,索引内容可能非常大,因此默认记录源文档内容的前1000行。可以设置为捕获整个文档,或者不记录原始文档本身。
分析工具
1. I/O信息
iostat。iostat是用来分析I/O状态的常用工具,其输出结果是以/proc/diskstats为基础计算的。iops:r/s,w/s。await:平均I/O等待时间。%util:设备的繁忙比,是设备执行的I/O时间与所经过的时间百分比。
进程级I/O状态。iostat提供磁盘级的I/O状态,可以使用blktrace来分析I/O请求的各个环节。如果想看哪些进程的I/O最高,则可以使用pidstat或iotop两个工具。它们可以动态给出每个进程的读写速度。
2. 内存
top、free、vmstat等工具可以帮助我们看到基础的内存信息。当系统物理内存不足时,系统回收内存的效率如何。sar -B观察内存分页的统计信息。当一个进程需要更多内存而实际空间不足时,就会发生页面扫描。
sar -W。在开启了交换分区的系统上,查看页面交换情况。发生页面交换会导致服务器性能严重下降,我们应该在生产环境关闭交换分区。
3. CPU信息
基本信息。vmstat输出用户级(us)和内核级(sy)的CPU占用百分比,以及采样周期内的上下文切换次数。mpstat除了获取用户级(usr)和内核级(sys)的CPU占用百分比,还可以输出采样周期内的软中断(soft)、硬中断(hard)占用时间的百分比。
诊断导致CPU高的系统调用。正常情况下应用程序占用用户态CPU时间,如果进程暂用sys比较高,则表示程序执行在内核态的操作非常耗费CPU。
4. 网络连接和流量
sar。sar是用来查看网卡流量的常用方式,它以字节为单位输出网络传入和传出流量。
netstat。netstat -anp可以列出连接的详细信息,并且可以将连接、监听的端口对应到进程。其中Recv-Q和Send-Q代表该连接在内核中等待发送和接收的数据长度,单位为字节。例如:发送数据时,send调用将数据从用户态复制到内核态后返回,TCP协议栈负责将数据发送出去,Send-Q代表了尚未发送出去的数据量。在未发送完之前,这些数据停留在内核缓冲,原因可能是网络延时,或者对端的滑动窗口限制。Recv-Q则代表协议栈已完成接收,但尚未被应用层的read调用从内核态赋值到用户态的数据长度。
ss与netstat类似,适合处理海量连接。
ifconfig。留意其中的RX/TX errors、dropped、overruns信息,大部分情况下没什么问题,但是当网卡流量跑满的时候可能会出现意外。
sysdig。可以分析系统级和进程级许多方面的情况。例如:系统调用、网络统计、文件I/O等,用它捕获某个进程到某个IP的网络流量。
重大版本变化
5.X 重大变化。string类型被text/keyword两个类型取代。索引级的设置不再写入.yml文件,需要为每个索引单独设置。添加了Profile API、Shrink API、Rollover API、Reindex API。添加了Plainless脚本。增加了Task Manager,重建索引等任务被Task Manager管理。评分算法使用BM25代替TF/IDF。
6.X 变化。每个索引支持一个_type。默认禁用_all。优化了doc values,占用磁盘空间少,读写速度更快。增加了序列ID,加快索引恢复速度。查询语法变多。
ES基本概念
基本概念和原理
ES是实时的分布式搜索引擎,新增到ES的数据1s后就可以被检索到。什么是全文?对全部的文本内容进行分析,建立索引,使之可以被搜索。
索引结构:JSON作为文档的序列化格式。
在创建索引时,定义字段的数据类型,指定不同的分析器。
搜索是基于索引为单位的,而不是基于文档的。
分片
增加系统可用性,读操作可以并发执行,分担集群压力。
带来一致性问题:部分副本写成功,部分副本写失败。
应对并发更新问题,ES将数据副本分为主分片、副本分片。写过程先写主分片,成功后再写副分片,恢复阶段以主分片为主。
分片的目的是分割巨大索引,让读写可以并行操作。
一个索引包含多个分片,一个分片是一个Lucene索引,Lucene索引又有很多分段组成,每一个分段都是一个倒排索引。ES每次“refresh”时都会生成一个新的分段。
ES index —> Lucene索引 —> segment段 —> Field字段
实际应用中,不应该向单个索引持续写数据。
1.巨大的索引在数据老化后难以删除,以_id为单位删除文档不会立刻释放空间,删除的doc只在Lucene分段合并时才会真正从磁盘中删除。
2. 手工触发分段合并,仍然可能会引起较高的I/O压力,并且可能因为分段巨大导致在合并过程中磁盘空间不足(分段大小大于磁盘可用空间的一半)。
动态更新索引
倒排索引一旦被写入就具有不变性。
好处:对文件的访问不需要加锁,读取索引时可以被文件系统缓存等。
新增内容并写入到一个新的倒排索引中,查询时,每个倒排索引都被轮流查询,查询完再对结果进行合并。
由于分段的不变性,更新、删除等操作实际上是将数据标记为删除。因此删除部分数据不会释放磁盘空间。
近实时搜索
ES每秒产生一个新分段,新段先写入文件系统缓存(该数据已经对读取可见),但稍后再执行flush刷盘操作,写操作很快会执行完,一旦写入成功,就可以像其他文件一样被打开和读取了。
由于系统先缓存一段数据才写,且新段不会立即刷入磁盘,这两个过程中如果出现意外情况,则存在数据丢失的风险。通用做法是记录事务日志。
段合并
refresh:在ES中,每秒清空一次写缓冲,将这些数据写入文件,这个过程成为refresh,每次refresh都会创建一个新的Lucene段。
每个搜索请求都需要轮流检查每个段,查询完再对结果进行合并。所以段越多,搜索也就越慢。
小段合并为大段策略:选择大小相似的段分段进行合并。合并过程中,标记为删除的数据不会写入新分段,当合并过程结果,旧的分段数据被删除,标记删除的数据才从磁盘删除。
当持续地向一个表中写数据,如果段文件大小没有上限,当巨大的段达到磁盘空间一半时,剩余空间不足以进行新的段合并过程。
如果段文件设置一定上限不再合并,则对表中部分数据无法实现真正的物理删除。
如果段文件设置一定上限不再合并,则对表中部分数据无法实现真正的物理删除。
集群内部原理
分布式系统的集群方式大致可以分为主从模式和无主模式。
集群的节点管理
主节点:负责集群层面的相关操作,管理集群变更。全局唯一。
主节点也可以作为数据节点,生产环境给应尽量分离节点和数据节点,创建独立节点配置:node.master=true; node.data=false
为了防止数据丢失,每个主节点都应该知道有资格成为主节点的数量,默认为1。为了避免网络分区时出现多主的情况,配置discovery.zen.minimum_master_nodes原则上最小值应该是:(master_eligible_nodes / 2)+ 1
数据节点:负责保存数据、执行数据相关操作:CRUD、搜索、聚合等。数据节点只与数据节点打交道。
数据节点配置:node.master:false; node.data:true; node.ignest: false
预处理节点
预处理操作允许在索引文档之前,即写入数据之前,通过事先定义好的一系列的processors(处理器)和pipeline(管道),对数据进行某种转换、富化。
processors呵呵pipeline拦截bulk和index请求,在应用相关操作后将文档传回给index或bulk API。
默认情况下,在所有节点上启用ignest。node.ignest: true
协调节点:处理客户端请求的节点。
客户端请求可以发送到集群的任何节点,每个节点都知道任意文档所处的位置,然后转发这些请求,收集数据并返回给客户端。
node.master: false node.data: false node.ignest:false
部落节点:部落功能允许部落节点在多个集群之前充当联合客户端。
集群健康状态
集群状态
集群扩容
当集群扩容、添加节点时,分片会均衡地分配到集群的各个节点,从而对索引和搜索过程进行负载均衡,这些都是系统自动完成的。
分片副本实现了数据冗余,防止硬件故障导致数据丢失。
分片过程中除了让节点间均匀存储,还要保证不把主分片和副分片分配到同一节点,避免单个节点故障引起数据丢失。
分布式系统中,当节点异常时,ES会自动处理节点异常。当主节点异常时,集群会重新选举主节点。当某个主分片异常时,会将副分片提升为主分片。
客户端API
主要模块简介
Cluster
Cluster模块是主节点执行集群管理的封装实现,管理集群状态,维护集群层面的配置信息。
管理集群状态,将新生成的集群状态发布到集群所有节点。
在集群各节点中直接迁移分片,保持数据平衡。
allocation
封装分片相关的功能和策略,包括主分片的分配和副分片的分配,由主节点调用。
Discovery
发现模块负责发现集群中的节点,以及选举节点。
发现模块类似Zookeeper的作用,选主并管理集群拓扑。
gateway
负责对收到Master广播下来的集群状态数据的持久化存储,并在集群完全重启时恢复它们。
Indices
索引模块管理全局级的索引设置,不包括索引级的。
集群启动阶段需要的主分片恢复和副分片恢复就是在这个模块中实现的。
HTTP
HTTP模块本质上是完全异步的,没有阻塞线程等待响应。使用异步线程进行HTTP好处是解决了C10k问题(10k量级的并发连接)
部分场景下,使用HTTP keepalive提升性能。不要在客户端使用HTTP chunking
Transport
传输模块用于集群内节点之间的内部通信。从一个节点到另一个节点的每个请求都是使用传输模块。
传输模块本质上是完全异步的。
传输模块使用TCP通信,每个节点都与其他节点维持若干TCP长连接。
Engine
封装了对Lucene操作及translog的调用,它是对一个分片读写操作的最终提供者。
ES使用Guice框架进行模块化管理。
选主流程
Discovery模块负责发现集群中的节点,以及选择主节点。ES内置的实现称为Zen Discovery。
设计思想:所有分布式系统都需要以某种方式处理不一致问题。一般情况下,可以将策略分为两组:试图避免不一致以及发生不一致之后如何协调他们。
为什么使用主从模式?
ES的典型场景中的另一个简化是集群中没有那么多节点。通常,节点的数量远远小于单个节点能够维护的连接数,并且网络环境不必经常处理节点的加入和离开。
这就是为什么主从模式能适合ES
选举算法
Bully算法
假定所有节点都有一个唯一的ID,使用该ID对节点进行排序。任何时候的当前Leader都是参与集群的最高ID节点。但是当拥有最大ID的节点处于不稳定状态的场景下会有问题。例如:Master负载过重而假死,集群拥有第二大ID节点被选为新主,这时原来的Master恢复,再次被选为新主,然后又假死....
ES通过推迟选举,直到当前的Master失效来解决上述问题,只要当前节点不挂掉,就不重新选主。但是容易产生脑裂(双主),为此,再通过“法定得票人数过半”解决脑裂问题。
Paxos算法
Paxos在进行选举方面的灵活性比简单的Bully算法有很大的优势,因为在现实生活中,存在比网络连接异常更多的故障模式。
Paxos实现起来非常复杂。
相关配置
discovery.zen.minimum_master_nodes:最小主节点数,这是防止脑裂、防止数据丢失的及其重要的参数。
为了避免脑裂,这个值因该是半数以上。(master_eligible_nodes / 2) + 1
discovery.zen.ping.unicast.hosts: 集群的种子节点列表,构建集群时本节点会尝试连接这个节点列表,那么列表中的主机会看到整个集群中都有哪些主机。
discovery.zen.ping.unicast.hosts.resolve_timeout:DNS解析超时时间,默认为5秒。
discovery.zen.join_timeout:节点加入现有集群时的超时时间,默认为ping_timeout的20倍。
discovery.zen.join_retry_attempts join_timeout:超时之后的重试次数,默认为3次。
discovery.zen.join_retry_delay join_timeout:超时之后,重试前的延迟时间,默认100ms。
discovery.zen.master_election.ignore_non_master_pings:设置为true时,选主阶段将忽略来自不具备Master资格节点(node.master: false)的ping请求,默认为false。
discovery.zen.fd.ping_interval:故障检测间隔周期,默认为1s。
discovery.zen.fd.ping_timeout:故障检测请求超时时间,默认为30s。
discovery.zen.fd.ping_retries:故障 检测超时后的重试次数,默认为3次。
流程概述
1.每个节点计算最小的已知节点ID,该节点为临时Master。向该节点发送领导投票。
2.如果一个节点收到足够多的票数,并且该节点也为自己投票,那么它将扮演领导者的角色,开始发布集群状态。
所有节点都会参与选举,并参与投票,但是,只有有资格成为Master节点(node.master为true)的投票才有效。
配置项:discovery.zen.minimum_master_nodes。为了避免脑裂,最小值应该是有Master资格的节点数 n/2 + 1
流程分析
整体流程:选举临时Master,如果本节点当选,则等待确立Master,如果其他节点当选,则尝试加入集群,然后启动节点失效探测器。
线程池:generic
选举流程:
选举临时Master
(1)“ping”所有节点,获取节点列表fullPingResponses,ping结果不包含本节点,把本节点单独添加到fullPingResponse中。
(2)构建两个列表。activeMasters列表:存储集群当前活跃Master列表。masterCandidates列表:存储master候选者列表。
(3)如果activeMasters为空,则从masterCandidates中选举,判断候选者是否达到法定人数,否则选举失败。
从masterCandidates中选主:先比较集群状态版本,从大到小排序,如果版本号相同 ,比较节点ID。
从activeMasters中选主:取列表的最小值。
投票与的票的实现
在ES中,发送投票就是发送加入集群请求。得票就是申请加入该节点的请求的数量。
当节点检查收到的投票是否足够时,就是检查加入它的连接数是否足够,其中会去掉没有Master资格节点的投票。
确立Master或加入集群
节点失效检测
监控节点是否离线,然后处理其中的异常。不执行失效检测可能会产生脑裂(双主或多主)。
两种失效探测器:在Master节点,启动NodesFD。定期探测加入集群的节点是否活跃。在非Master节点启动MasterFD。定期探测Master节点是否活跃。
两种都是通过定期(每隔1s)发送的ping请求探测节点是否正常的,当失败达到一定次数(默认3次),或者接收到来自底层连接模块的节点离线通知时,开始处理节点离开事件。
NodesFD:主节点在探测到节点离线的事件处理中,如果发现当前集群的节点数量不足法定人数,则放弃Master身份,从而避免产生双主。
MasterFD:探测Master离线的处理很简单,重新加入集群。本质上个就是该节点重新执行一遍选主的流程。
写流程
Index/Bulk基本流程
新建、索引(写入)和删除操作都是写操作。写操作必须在主分片执行成功后才能复制到相关的副分片。
(1)客户端向Node1发送写请求;
(2)Node1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0的主分片位于Node3,因此请求被转发到Node3上。
(3)Node3上的主分片执行写操作。如果写入成功,则它将请求并行转发到Node1和Node2的副分片上,等待返回结果。当所有的副分片都报告成功,Node3将向协调节点报告成功,协调节点再向客户端报告成功。
2.x版本之前,写一致性的默认策略是quorum,即多数的分片在写入时处于可用状态。
5.x和6.x版本中,写一致性由wait_for_active_shards参数控制,默认情况下,在执行写入操作前,只要主分片处于活跃状态就可以执行写入操作。但这和写入操作开始执行之后有多少个分片写入成功没有关系。
Index/Bulk详细流程
(1)协调节点流程
协调节点负责创建索引、转发请求到主分片节点、等待响应、恢复客户端。
实现位于TransportBulkAction。执行本流程的线程池:http_server_worker。
1.参数检查
2.处理pipeline请求。数据预处理(ingest)工作通过定义pipeline和processors实现。
3.自动创建索引。创建索引的请求被发送到Master节点,待收到全部创建请求的Response之后,才进入下一个流程。在Master节点执行完创建索引流程,将新的clusterState发布完毕后才返回。Master发布clusterState的Request收到半数以上节点的Response,认为发布成功。
4.对请求的预先处理。对请求的预先处理只是检查参数、自动生成id、处理routing等。实现位于TransportBulkAction.BulkOperation#doRun
5.检测集群状态。协调节点在开始处理时会先检测集群状态,若集群异常则取消写入。例如,Master节点不存在,会阻塞等待Master节点直至超时。
6.内容路由,构建基于shard的请求。将用户的bulkRequest重新组织为基于shard的请求列表。本质上是合并请求的过程。根绝路由算法计算某文档属于哪个分片。遍历所有的用户请求,重新封装后添加到map结构中。
7.路由算法。根据routing和文档id计算目标shardid的过程。shard_num = hash(_routing) % num_primary_shards。当使用自定义id或routing时,id或routing值可能不够随机,造成数据倾斜,部分分片过大。可以使用index.routing_partition_size 配置来减少倾斜的风险。公式:shard_num = (hash(_routing) + hash(id) % routing_partition_size) % num_primary_shards
8.转发请求并等待响应。根据集群状态中的内容路由表确定主分片所在节点,转发请求并等待响应。
(2)主分片节点流程
执行本流程的线程池:bulk
1.检查请求。检测要写的是否是主分片,AllocationId是否符合预期,索引是否处于关闭状态等。
2.是否延迟执行。判断请求是否需要延迟执行,如果需要延迟则放入队列,否则继续下面的流程。
3.判断主分片是否已经发生迁移。如果已经发生迁移,则转发请求到迁移的节点。
4.检测写一致性。检测本次写操作涉及的shard,活跃shard数量是否足够,不足则不执行写入。
5.写Lucene和事务日志。索引过程先写Lucene,后写translog。因为Lucene写入时对数据有检查,写操作有可能失败。如果先写translog,写入Lucene时失败,则还需要对translog进行回滚处理。
6.flush translog。根据配置的translog flush策略进行刷盘控制,定时或立即刷盘。
7.写副分片。向目标节点发送请求,等待响应。这个过程是异步并行的。
8.处理副本分片写失败情况。主分片所在节点将发送一个shardFailed请求给Master:然后Master会更新集群状态,在新的集群状态中,这个shard将:从in_sync_allocations列表中删除;在routing_table的shard列表中将state由STARTED更改为UNASSIGNED;添加到routingNodes的unassignedShards列表。
(3)副分片节点流程
线程池:bulk
1.执行与主分片基本相同的写doc操作,写完毕后回复主分片节点。
2.在副分片的写入过程中,参数检查的实现与主分片略有不同,最终都调用IndexShard-OperationPermits#acquire判断是否需要delay,继续后面的写流程。
I/O异常处理
(1)Engine关闭过程。将Lucene标记为异常,关闭shard,然后汇报给Master。
(2)Master的对应处理。收到节点的SHARD_FAILED_ACTION_NAME消息后,Master通过reroute将失败的shard通过reroute迁移到新的节点,并更新集群状态。
(3)如果请求在协调节点的路由阶段失败,则会等待集群状态更新,拿到更新后,进行重试,直到超时1分钟。
在主分片写入的过程中,写入是阻塞的。只有写入成功,才会发起写副本请求。如果主分片写入失败,则认为整个请求被认为处理失败。
无论主分片还是副分片,当写入一个doc失败时,集群不会重试,而是关闭本地shard,然后向Master汇报,删除是以shard为单位的。
在主分片写入的过程中,写入是阻塞的。只有写入成功,才会发起写副本请求。如果主分片写入失败,则认为整个请求被认为处理失败。
无论主分片还是副分片,当写入一个doc失败时,集群不会重试,而是关闭本地shard,然后向Master汇报,删除是以shard为单位的。
系统特性
数据可靠性:通过分片副本和事务日志机制保障数据安全。
服务可用性:可用性和一致性的取舍方面,默认情况下ES更倾向于可用性,只要主分片可用即可执行成功。
一致性:弱一致性。只要主分片写成功,数据就可能被读读取。因此读取操作在主分片和副分片上可能得到不同结果。
原子性:索引的读写、别名更新是原子操作,不会出现中间态。但bulk不是原子操作,不能用来实现事务。
扩展性:主副分片都可以承担读请求,分担系统负载。
GET流程
ES读取分为GET和Search两种。GET/MGET必须指定三元组:_index、_type、_id,根据文档id从正排索引中获取内容。而Search不指定id,根据关键词从倒排索引中获取内容。
1.可选参数
2.GET基本流程
(1)客户端向Node1发送读请求;
(2)Node1使用文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获知分片0有三个副本数据,位于所有的三个节点中,此时它可以将请求发送到任意节点,这里它将请求发送到NODE2;
(3)NODE2将文档返回给NODE1,NODE1将文档返回给客户端。
Node1作为协调节点,会将客户端请求轮询发送到集群的所有副本来实现负载均衡。在读取时,文档可能已经存在于主分片上,但还没有复制到副分片。这种情况下,读请求命中副分片时可能会报告文档不存在,但是命中主分片可能成功 返回文档。
3.GET详细分析
1.协调节点
执行本流程的线程池:http_server_worker
(1)内容路由。根据内容路由算法计算目标shardid,也就是文档应该落在哪个分片上。
(2)转发请求。作为协调节点,向目标节点转发请求,或者目标是本地节点,直接读取数据。如果是发送到网络,则请求被异步发送,等待Response,直到超时。
2.数据节点
执行本流程的线程池:get
(1)读取及过滤
(2)InternalEngine的读取过程
4.MGET流程分析
封装单个GET请求
(1)遍历请求,计算出每隔doc的路由信息,得到由shardid为key组成的request map。
(2)循环处理组织好的每个shard级请求,调用处理GET请求时使用TransportSingle-ShardAction#AsyncSingleAction处理单个doc的流程。
(3)收集Response,全部Response返回后执行finishHim(),给客户端返回结果。
think
(1)读失败是怎么处理的?尝试从别的分片副本读取。
(2)优先级。优先级策略只是将匹配到优先级的节点放到了目标节点列表的前面。
Search流程
query then fetch:因为查询的时候不知道文档位于哪个分片,因此索引的所有分片都要参与搜索,然后协调节点将结果返回。
索引和搜索
ES中的数据可以分为两类:精确值和全文。
精确值:比如日期和用户id、IP地址等。
精确值比较的是二进制,要么匹配,要么不匹配。
全文:文本内容,比如一条日志,或邮件的内容。
找到结果是“看起来像”要查询的东西,把查询结果按相似度排序,评分越高,相似度越大。
1.建立索引
字符过滤器
对字符串进行预处理。
分词器
英文:空格
中文:IK分词器
Token过滤器
根据停止词删除词元。
语言处理
例如:大小写转换。
将Term传给索引组件,生成倒排/正排索引,再存储到文件系统中。
2.执行搜索
搜索调用Luene完成,如果是全文检索:
对检索字段使用建立索引时相同的分析器进行分析,产生Token列表;
根据查询语句的语法规则转换成一颗语法树;
查找符合语法树的文档;
对匹配到的文档列表进行相关性评分,评分策略一般使用TF/IDF;
根据评分结果进行排序。
search type
ES有两种搜索类型
DFS_QUERY_THEN_FETCH
QUERY_THEN_FETCH(默认)
DFS查询阶段的流程要多一些,它使用全局信息来获取更准确的评分。
分布式搜索过程
一次搜索请求只会命所有分片副本中的一个。
协调节点流程
两阶段相应的实现位置
查询阶段:——search.InitialSearchPhase
取回阶段:——search.FetchSearchPhase
(1)Query阶段
查询会广播到索引中每一个分片副本。每个分片在本地执行搜索并构建一个匹配文档的优先队列。
优先队列时一个存有TOPN匹配文档的有序列表。优先队列大小为分页参数 from+size
QUERY_THEN_FETCH搜索步骤如下:
node1: R0/P1 node2:R0/R1 node3:P0/R1
1.客户端发送search请求到node3
2.Node3将查询请求转发到索引的每个主分片或副分片中。
3.每个分片在本地执行查询,并使用本地的Term/Document Frequency 信息进行打分,添加结果到大小为 from + size 的本地有序优先队列中。
4.每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表。
协调节点广播查询请求到 所有相关分片时,可以是主分片或副分片,协调节点将在之后请求中轮询所有的分片副本来分摊负载。
查询阶段不会对搜索请求的内容进行解析,只看本次搜索需要命中哪些shard,然后针对每个特定shard选择一个副本,转发搜索请求。
Query源码解析
执行本流程的线程池:http_server_work
1.解析请求
RestSearchAction#prepareRequest
performPhaseOnShard
2.构造目的shard列表
将请求涉及的本集群shard列表和远程集群的shard列表合并
3.遍历所有shard发送请求
请求是基于shard遍历,如果列表中有N个shard位于同一个节点,则向其发送N次请求,并不会把请求合并为一个
4.收集返回结果
本过程在search线程池中执行
(2)Fetch阶段
Fetch过程
Query阶段知道了要取哪些数据,但是并没有取具体的数据,这就是Fetch阶段要做的。
协调节点向相关NODE发送GET请求
分片所在节点向协调节点返回数据
协调节点等待所有文档被取得,然后返回给客户端
为了避免在协调节点中创建的number_of_shards*(from + size)优先队列过大,应尽量控制分页深度。
Fetch源码阶段
执行本流程的线程池:search
1.发送Fetch请求
2.收集结果
3.ExpandSearchPhase
4.回复客户端
执行搜索的数据节点流程
执行本流程的线程池:search
1.响应Query请求
execute(),调用Lucene、searcher.search()实现搜索
rescorePhase,全文检索且需要打分
suggestPhase,自动补全及纠错
aggregationPhase,实现聚合
总结:
慢查询Query日志的统计时间在于本阶段的处理时间
聚合操作在本阶段实现,在Lucene检索后完成
2.响应Fetch请求
慢查询Fetch日志的统计时间在于本阶段的处理时间
小结
聚合是在ES中实现的,而非Lucene
Query和Fetch请求之间是无状态的,除非是scroll方式
分页搜索不会单独“cache”,cache和分页没有关系
每次分页请求都是一次重新搜索的过程,而不是从第一次搜索的结果中获取。
搜索需要遍历分片所有的Lucene分段,因此合并Lucene分段对搜索性能有好处。
0 条评论
下一页