ES相关知识

ES常见知识点

ES集群,一个node一般会分配几个分片?

  • ES中的数据组织成索引,每一个索引由一个或多个分片组成.每个分片是Luncene索引中的一个实例,可以把实例理解成自管理的搜索引擎,用于在ES集群中对一部分数据进行索引和处理查询. 分片是ES在集群周围分发数据的单位,ES在重新平衡数据时移动分片的速度取决于分片的大小,数量以及网络和磁盘性能.
  • 避免非常大的分片,因为大的分片可能会对集群从故障中恢复的能力产生负面影响.分片大小为50GB通常被界定为适用于各种用例的限制.
  • 在集群节点上保存的分片数量与可用的堆内存大小成正比,经验来说,每个节点的分片数量保持在低于每1GB堆内存对应集群的分片在20-25之间. 如30GB内存的堆内存节点最多可以有600-750个分片.
  • 参考文章:

Elasticsearch如何实现master选举

  • Elasticsearch的任意一个节点都可以设置node.master和node.data属性
    • master=true,data=true: 即是Master Eligible又是data节点
    • master=true,data=false: 单纯的Master Eligible节点
    • master=false,data=true: 单纯的data节点
    • master=false,data=false: 纯粹的Coordinating Node, 协调节点负责查询时的数据收集,合并,以及聚合操作. ES中所有节点都是协调节点
  • ES针对当前集群的所有Master Eligible节点进行选举,为了避免split-brain现象,ES选取QUORUM思想,只有超过半数选票的节点才能成为master.(eligibleNodesNum/2 + 1)
  • 当满足下列条件时就会触发一次master选举
    • 当前master eligible节点不是master
    • 当前master eligible节点与其他节点通信无法发现master
    • 集群中无法连接到master的master eligible节点达到minimum_master_nodes的值
  • 某个节点决定要选举时,会实现如下操作:
    • 寻找ClusterStateVersion比自己高的master eligible节点,向其发送选举投票
    • 如果CLusterStateVersion相同,则计算自己能找到的master eligible节点(包含自己)中节点id最小的节点,向其发送选举投票
    • 如果一个节点收到足够多的选票,并且向自己也投票了,则该节点成为master开始发布集群信息
  • 与其他选举方法对比
    • Zookeeper: ES可以使用Zookeeper来进行选举,方法如下:
      • 所有master eligible尝试在ZK上创建指定路径
      • 只有第一个节点创建成功,该节点成为master,其余节点watch此路径
      • 一旦ZK失去master链接,该路径被删除,其他master eligible继续尝试创建路径, 重复上述操作
    • Raft: 相比ES自身的选举算法,Raft是经过严格论证的一致性算法,ES早期版本时Raft还未提出,可能后续会参考改进.

如何做到ES写入调优?

  • 客户端: 多线程批量写
    • 通过性能测试,确定最佳文档数量
    • 多线程,观察HTTP429返回,实现retry以及线程数量的自动调节
  • 服务器端:
    • 降低IO操作
      • 使用ES自动生成的文档ID(避免Get操作)
      • 调整Refresh interval等配置(降低搜索实时性)
      • 调整translog选项,降低写磁盘频率(牺牲容灾能力)
    • 降低CPU和存储开销
      • 减少不必要的分词
      • 避免不需要的doc_values
      • 文档字段尽量保持相同顺序,提高文档的压缩率
    • 尽量做到写入和分片的负载均衡,实现水平扩展
      • Shard Filtering
      • Write load balance
    • 调整Bulk线程池和队列
      • 客户端:
        • 单个bulk请求体的数据量不要太大,官方建议5-15mb
        • 写入bulk请求超时时间需要足够长,建议60s以上
        • 写入端尽量将数据轮询到不同节点
      • 客户端:
        • 索引创建属于计算密集型,应该使用固定大小的线程池,来不及处理的放入队列.线程数配置为CPU核数+1,避免过多上下文切换
        • 队列大小可以适当增加,不要太大否则占用内存导致GC
  • Index建模实践
    • 如果只需要聚合不需要搜索,index设置为false
    • 如果不需要算分,Norms设为false
    • 不对字符串使用默认的dynamic mapping,字段数量过多会对性能产生较大影响
    • Index_options控制在创建倒排索引时哪些内容会被添加到倒排索引中,节约CPU
    • 关闭_source减少IO操作,适合指标数据

如何避免split-brain?

当节点崩溃或者节点通讯故障时,如果一个子节点无法连接到主节点,那么它会发起选举从与之连接的master eligible节点中选举一个新的主节点.新主节点将接管开始工作,如果旧的主节点重新加入集群或恢复通信,那么新主节点将降级到子节点.这过程在大多数情况下是不存在冲突,高效无缝衔接的.

但是考虑集群中只有两个节点的极端情况,如果因为网络原因导致节点无响应,节点都相信对方已经挂掉.重新选举后会存在两个主节点,集群处于不一致状态,分片的两份数据分开了,如果不做全量的重索引很难进行重排序.

极端情况下,客户端无法感知到这种不一致状态,因为index请求无论发往哪个节点都会成功,只有在查询时才可能发现问题.

要避免脑裂,就是要关注discovery.zen.minimum_master_nodes的值,要设置为n/2+1,还有建议配置3节点以上的集群. 对于已经存在的两节点集群,可以添加一个新节点并将node.data设为false,放在一台便宜的服务器上.

ES对于大数据量(上亿量级)的聚合如何实现?

有些算法可以分布式执行,类似一些单次请求获得精确结果的聚合.它们无需额外代价,就能在多台机器上运行,如max度量:

  • 请求广播到所有分片
  • 查看每个文档的price字段,如果price>current_max就替换
  • 返回所有分片的最大值给协调节点
  • 找出所有分片返回的最大值即可
    这种算法会随着机器数的线性增长而横向扩展,无需任何协调操作且内存消耗小. 但是不是所有算法都可以如此,一些复杂的算法需要在算法性能和内存使用上做出权衡.

三角因子模型: 大数据,精确性,实时性

  • 精确+实时: 数据可以存入单台机器的内存中,结果会100%精确,响应会相对快速.
  • 大数据+精确: 传统的hadoop,可以处理PB级数据并且未我们提供精确的答案,但它可能需要几周的时间才能为我们提供答案.
  • 大数据+实时: 近似算法为我们提供准确但不精确的结果.

ES目前支持两种近似算法(cardinality和percentiles),以牺牲一点小小的估算错误为代价换回高速的执行效率和极小的内存消耗.

Cardinality

Elasticsearch提供的首个近似聚合是Cardinality度量.它提供一个字段的基数,即该字段的distinct或者unique值的数目.基于HyperLogLog++(HLL)算法,HLL会先对用户输入做哈希运算,然后根据哈希运算结果中的bits做概率估算从而得到基数. 算法特性如下:

  • 可配置的精度用来控制内存的使用(更精确=更多内存)
  • 小的数据集精度非常高
  • 可以通过配置参数,来设置去重需要的固定内存使用量.无论数千还是数十亿的唯一值,内存使用量只与配置的精度相关.

要配置精度,必须指定percision_threshold参数值,这个阈值定义了在何种基数水平下我们希望得到一个近乎精确的结果. 接受0-40,000之间的数字,更大的值还是会被当做40,000来处理.

对于指定阈值,HLL的数据结构大概使用percision_threshold*8字节的内存,所以必须在牺牲内存和获得额外的准确度之间做平衡. 实际使用中,阈值=100时可以在唯一值为百万的情况下仍将误差维持在5%以内.

如果想要获得唯一值的数目,通常需要查询整个数据集合,所有基于所有数据的操作都必须迅速.HyperLogLog已经很快了,它只是简单的对数据做哈希以及一些位操作,但是仍然可以进一步优化.

HLL只需要字段内容的哈希值,因此我们可以在索引时就预先计算好,在查询时跳过哈希计算将哈希值直接从fielddata中加载出来. 在执行聚合时使用X.hash字段而非X字段,cardinality会读取预先计算的哈希值取代动态计算原始值的哈希. 单个文档节省时间非常少,但是如果聚合一亿数据,每个字段会多花费10ns时间,这样每次查询时都会额外增加1s. 如果我们在非常大量的数据里面使用cardinality需要权衡使用预计算的意义,是否需要提前计算hash,从而在查询中获得更好的性能.

Percentiles

百分位数通常用来找出异常,比如监控网站的延时来判断响应是否能保证良好的用户体验. 和基数一样,计算百分位需要一个近似算法,percentiles中使用一个TDigest算法

  • 百分位的准确度和百分位的极端程度相关. 1和99的百分位要比50百分位要准确
  • 数据集合小的情况,百分位非常准确
  • 随着桶里数值的增长,算法会开始对百分位进行估算.有效在准确度和内存节省之间做出权衡.

通过修改compression参数来控制内存和准确度之间的比值.TDigest算法用节点近似计算百分比,节点越多准确度越高.compression参数限制节点的最大数目为20*compression.

通过增加compression可以以消耗更多内存未代价提高百分位数准确性,更大的压缩比值会使得算法运行更慢,因为底层的树形结构的存储会增长,导致操作的代价更高.默认compression=100

一个节点大约使用32字节的内存,所以在最坏情况下,默认设置会生成一个64KB的TDigest.

ES主分片数量可以在后期更改吗?为什么?

ES 2.X版本时不可以,因为ES对document的处理是通过路由算法来进行处理,更改主分片数量会导致路由被破坏,间接导致数据丢失.所以主分片数量不可以修改.

如果修改分片数量后重新分配数据,分片的切分成本和reindex成本差不多,所以官方直接使用reindex. 如果数据不重复,其实新的业务数据可以切换到新的索引上继续写,查询时查询新旧两个索引.

从ES6.1以后支持在线扩大shard的数量,但是操作期间需要对index锁写:

  • 创建一个新的目标索引,定义与源索引相同,但具有更多的主分片
  • 将段从源索引硬链接到目标索引,如果文件系统不支持hard link,则将所有段都复制到新索引中(非常耗时)
  • 创建低级文件后,再次对所有文档进行hash处理,删除属于不同分片的文档
  • 恢复目标索引

ES更新文档/删除文档的执行流程

更新

  • 在进行写操作时,ES根据传入的_routing参数按照公式计算出文档要分配的分片,从集群元数据中找出对应主分片的位置,将请求路由到该分片.
  • 文档写入Lucene后不能被立即查询,ES提供refresh操作,定时的调用Lucene的reopen(OpenIfChanged)为内存中新写入的数据生成一个新的segment.此时文档可以被检索.
  • 即使refresh后文档仍然在文件系统缓存中,如果服务器宕机这部分数据依旧会丢失. ES为此增加了translog,文档写入时会先将文档写入Lucene,然后写入一份到translog落盘.(如果可靠性要求不高可以设置异步落盘)translog是追加写入,性能比随机写入要好.先写Lucene后写translog是因为写入Lucene可能会失败,减少写入失败回滚的复杂度.
  • 间隔30分钟或者translog大小到达阈值时触发flush操作,ES会先执行refresh操作将buffer生成segment,然后调用Lucene的commit方法将所有内存中的segment fsync到磁盘中.此时Lucene中数据完成持久化,清空translog中数据(6.X版本为了实现sequenceIDs,不删除translog)
  • 由于refresh默认间隔1s,会产生大量的小segment,ES会运行一个任务检测当前磁盘中的segment,对符合条件的进行合并,减少Lucene中的segment个数,提高查询速度减少负载.

Lucene仅支持文档整体更新,ES为了支持局部更新,在Lucene的Store索引中存储一个_source字段,key时文档ID内容是文档原文.更新时先从_source中获取原文,与更新部分合并,再调用Lucene API进行全量更新. 增加版本机制防止其他线程并发写.

删除

  • 提交删除操作,先查询要删除文档所属的segment
  • commit中包含一个.del文件,记录哪些segment中的哪些文档被标记为deleted.
  • 当.del文件中存储的文档足够多时,ES执行物理删除操作,清楚文档
    • 在删除中进行搜索操作: 依次查询所有segment,根据.del文件过滤掉标记为deleted的文档,然后返回搜索结果
    • 在删除过程中更新: 将旧文档标记为deleted,将新文档写入新的segment中.执行查询时通过.del过滤掉旧版本文档

ES shard内部是由什么组成的?

Shard 实际上就是一个Lucene的实例(Lucene Index),单个Lucene实例中最多包含Integer.MAX_VALUE-128个documents

一个LuceneIndex在文件系统表现上来看就是存储了一系列文件的目录,由许多个独立的segments组成. segments包含了文档中的词汇字典,词汇字典的倒排索引,以及document的字段数据. 所有segments数据存储于_.cfs文件中

Segments

segments直接提供了搜索功能,ES中的一个shard由大量的segments文件组成,且每一次fresh都会产生一个新的segment文件,segment文件有大有小,相当碎片化. ES内部则会开启一个线程将小的segment合并减少碎片化,降低文件打开数提升IO性能.

segment文件是不可变更的,当一个document更新时,实际上是将旧的文档标记删除,索引一个新文档(在_.del标记某个文档删除,查询时会跳过).在Merge时会将旧文档删除掉(物理删除).

ES中分析器是什么?

分析 包含以下过程:

  • 将文本分成适合于倒排索引的独立词条
  • 将词条统一化为标准格式以提高可搜索性
    分析器执行上述工作,实际上将三个功能封装到一个包中:
  • 字符过滤器: 分词前整理字符串
  • 分词器: 拆分字符串到单个词条
  • Token过滤器: 词条按顺序通过每个token过滤器,该过程可能会改变词条(大小写),删除此条(无用词删除),或者增加词条(同义词).

ES附带了可以直接使用的预包装的分析器:

  • 标准分析器: ES默认使用的分析器, 分析各语言文本最常用的选择,根据Unicode定义的单词边界划分文本. 删除绝大部分标点并将词条小写.
  • 简单分析器: 在任何不是字母的地方分割文本,将词条小写
  • 空格分析器: 在空格的地方划文本
  • 语言分析器: 考虑指定语言的特点,如英语分析器删除无用的单词(the and…),并且提取英语单词的词干

当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引.当全文域检索的时候,需要将查询字符串通过相同的分析过程,以保证搜索的词条格式和索引中词条格式一致.

客户端和集群连接时,如果选择特定的节点执行请求?

TransportClient利用transport模块远程连接一个elasticsearch集群。它并不加入到集群中,只是简单的获得一个或者多个初始化的transport地址,并以 轮询 的方式与这些地址进行通信。

ES中倒排索引是什么?

Inverted Index也叫反向索引,通过value找key. 对比词典的话,Term就相当于词语,Term Dictionary相当于词典,Term Index相当于词典的目录索引, Posting List相当于词语在字典的页数集合.

  • Term: 一段文本经过分析器分析后会输出一串单词,单词即是Term
  • Term Dictionary: 里面维护的Term,可以理解为Term集合.
  • Term Index: 为了更快的找到某个单词,我们为单词建立索引.B-Tree通过减少磁盘寻道次数来提高查询性能. ES也是采用同样思路,直接通过内存查找Term,不读磁盘. 如果term过多,Term dictionary会很大无法都放入内存,因此通过TermIndex(字典树). 这棵树不会包含所有的Term,包含的是一些Term的前缀,通过term index快速定位到Term dictionary的offset,然后从这个位置向后顺序查找. 再加上一些压缩基数,term index的尺寸可以只有所有Term尺寸的几十分之一,使得内存可以缓存整个term index.
  • PostingList(倒排列表):记录了出现过某个单词的所有文档的文档列表,以及该单词出现的位置信息,每条记录成为一个倒排项Posting.

为什么Elasticsearch/Lucene检索可以比mysql快了? Mysql只有term dictionary这一层,是以b-tree排序的方式存储在磁盘上的。检索一个term需要若干次的random access的磁盘操作。而Lucene在term dictionary的基础上添加了term index来加速检索,term index以树的形式缓存在内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘的random access次数。

ES在建立倒排索引时,会对拆分的各个单词进行相应处理,以提升后面搜索的时候能够搜索到相关联的文档的概率,这就是标准化规则转换,主要包括:时态的转换(例如liked转换为like)、单复数的转换(hospitals转换为hospitals)、同义词的转换(small转换为little)、大小写的转换(默认转换为小写)

当利用ES进行查询时,查询结果都会返回一个对应词条的相关度分数(score)。相关度分数的计算基于TF/IDF算法(Term Frequence&Inverse Doucument Frequency)

  • Term Frequence ,TF(t in f):我们查询的词条在文本中出现多少次,出现次数越多,相关度越高.
  • Inverse Doucument Frequency,IDF(t in all-f):查询词条在所有文本中出现的次数,出现次数越高,相关度越低。
  • Field-length(字段长度规约):字段的长度越长,相关度越低。