Loger 2019-11-03
Elasticsearch(简称ES)是一个分布式、可扩展、实时的搜索与数据分析引擎。ES不仅仅只是全文搜索,还支持结构化搜索、数据分析、复杂的语言处理、地理位置和对象间关联关系等。
ES的底层依赖Lucene,Lucene可以说是当下最先进、高性能、全功能的搜索引擎库。但是Lucene仅仅只是一个库。为了充分发挥其功能,你需要使用Java并将Lucene直接集成到应用程序中。更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理,因为Lucene非常复杂——《ElasticSearch官方权威指南》。
鉴于Lucene如此强大却难以上手的特点,诞生了ES。ES也是使用Java编写的,它的内部使用Lucene做索引与搜索,它的目的是隐藏Lucene的复杂性,取而代之的提供一套简单一致的RESTful API。
总体来说,ES具有如下特点:
节点类型
ES的架构很简单,集群的HA不需要依赖任务外部组件(例如Zookeeper、HDFS等),master节点的主备依赖于内部自建的选举算法,通过副本分片的方式实现了数据的备份的同时,也提高了并发查询的能力。
ES集群的服务器分为以下四种角色:
1.列表项目master节点,负责保存和更新集群的一些元数据信息,之后同步到所有节点,所以每个节点都需要保存全量的元数据信息:
2.datanode:负责数据存储和查询
3.coordinator:
4.ingestor:
如何配置节点类型
一个节点的缺省配置是:主节点+数据节点两属性为一身。对于3-5个节点的小集群来讲,通常让所有节点存储数据和具有获得主节点的资格。
专用协调节点(也称为client节点或路由节点)从数据节点中消除了聚合/查询的请求解析和最终阶段,随着集群写入以及查询负载的增大,可以通过协调节点减轻数据节点的压力,可以让数据节点更多专注于数据的写入以及查询。
master选举
选举策略
选举时机
集群启动:后台启动线程去ping集群中的节点,按照上述策略从具有master资格的节点中选举出master
现有的master离开集群:后台一直有一个线程定时ping master节点,超过一定次数没有ping成功之后,重新进行master的选举
选举流程
避免脑裂
脑裂问题是采用master-slave模式的分布式集群普遍需要关注的问题,脑裂一旦出现,会导致集群的状态出现不一致,导致数据错误甚至丢失。
ES避免脑裂的策略:过半原则,可以在ES的集群配置中添加一下配置,避免脑裂的发生
#一个节点多久ping一次,默认1s discovery.zen.fd.ping_interval: 1s ##等待ping返回时间,默认30s discovery.zen.fd.ping_timeout: 10s ##ping超时重试次数,默认3次 discovery.zen.fd.ping_retries: 3 ##选举时需要的节点连接数,N为具有master资格的节点数量 discovery.zen.minimum_master_nodes=N/2+1
注意问题
负载均衡
ES集群是分布式的,数据分布到集群的不同机器上,对于ES中的一个索引来说,ES通过分片的方式实现数据的分布式和负载均衡。创建索引的时候,需要指定分片的数量,分片会均匀的分布到集群的机器中。分片的数量是需要创建索引的时候就需要设置的,而且设置之后不能更改,虽然ES提供了相应的api来缩减和扩增分片,但是代价是很高的,需要重建整个索引。
考虑到并发响应以及后续扩展节点的能力,分片的数量不能太少,假如你只有一个分片,随着索引数据量的增大,后续进行了节点的扩充,但是由于一个分片只能分布在一台机器上,所以集群扩容对于该索引来说没有意义了。
但是分片数量也不能太多,每个分片都相当于一个独立的lucene引擎,太多的分片意味着集群中需要管理的元数据信息增多,master节点有可能成为瓶颈;同时集群中的小文件会增多,内存以及文件句柄的占用量会增大,查询速度也会变慢。
数据副本
ES通过副本分片的方式,保证集群数据的高可用,同时增加集群并发处理查询请求的能力,相应的,在数据写入阶段会增大集群的写入压力。
数据写入的过程中,首先被路由到主分片,写入成功之后,将数据发送到副本分片,为了保证数据不丢失,最好保证至少一个副本分片写入成功以后才返回客户端成功。
相关配置
5.0之前通过consistency来设置
consistency参数的值可以设为 :
5.0之后通过wait_for_active_shards参数设置
写入过程
几个概念:
translog
写入ES的数据首先会被写入translog文件,该文件持久化到磁盘,保证服务器宕机的时候数据不会丢失,由于顺序写磁盘,速度也会很快。
refresh
经过固定的时间,或者手动触发之后,将内存中的数据构建索引生成segment,写入文件系统缓冲区
commit/flush
超过固定的时间,或者translog文件过大之后,触发flush操作:
merge
上面提到,每次refresh的时候,都会在文件系统缓冲区中生成一个segment,后续flush触发的时候持久化到磁盘。所以,随着数据的写入,尤其是refresh的时间设置的很短的时候,磁盘中会生成越来越多的segment:
merge的过程大致描述如下:
删改操作
segment的不可变性的好处
删除
磁盘上的每个segment都有一个.del文件与它相关联。当发送删除请求时,该文档未被真正删除,而是在.del文件中标记为已删除。此文档可能仍然能被搜索到,但会从结果中过滤掉。当segment合并时,在.del文件中标记为已删除的文档不会被包括在新的segment中,也就是说merge的时候会真正删除被删除的文档。
更新
创建新文档时,Elasticsearch将为该文档分配一个版本号。对文档的每次更改都会产生一个新的版本号。当执行更新时,旧版本在.del文件中被标记为已删除,并且新版本在新的segment中写入索引。旧版本可能仍然与搜索查询匹配,但是从结果中将其过滤掉。
版本控制
通过添加版本号的乐观锁机制保证高并发的时候,数据更新不会出现线程安全的问题,避免数据更新被覆盖之类的异常出现。
使用内部版本号:删除或者更新数据的时候,携带_version参数,如果文档的最新版本不是这个版本号,那么操作会失败,这个版本号是ES内部自动生成的,每次操作之后都会递增一。
PUT /website/blog/1?version=1 { "title": "My first blog entry", "text": "Starting to get the hang of this..." }
使用外部版本号:ES默认采用递增的整数作为版本号,也可以通过外部自定义整数(long类型)作为版本号,例如时间戳。通过添加参数version_type=external,可以使用自定义版本号。内部版本号使用的时候,更新或者删除操作需要携带ES索引当前最新的版本号,匹配上了才能成功操作。但是外部版本号使用的时候,可以将版本号更新为指定的值。
PUT /website/blog/2?version=5&version_type=external { "title": "My first external blog entry", "text": "Starting to get the hang of this..." }
fdt文件
文档内容的物理存储文件,由多个chunk组成,Lucene索引文档时,先缓存文档,缓存大于16KB时,就会把文档压缩存储。
fdx文件
文档内容的位置索引,由多个block组成:
fnm文件
文档元数据信息,包括文档字段的名称、类型、数量等。
原始文档的查询
注意问题:lucene对原始文件的存放是行式存储,并且为了提高空间利用率,是多文档一起压缩,因此取文档时需要读入和解压额外文档,因此取文档过程非常依赖CPU以及随机IO。
相关设置
压缩方式的设置
原始文档的存储对应_source字段,是默认开启的,会占用大量的磁盘空间,上面提到的chunk中的文档压缩,ES默认采用的是LZ4,如果想要提高压缩率,可以将设置改成best_compression。
index.codec: best_compression
特定字段的内容存储
查询的时候,如果想要获取原始字段,需要在_source中获取,因为所有的字段存储在一起,所以获取完整的文档内容与获取其中某个字段,在资源消耗上几乎相同,只是返回给客户端的时候,减少了一定量的网络IO。
ES提供了特定字段内容存储的设置,在设置mappings的时候可以开启,默认是false。如果你的文档内容很大,而其中某个字段的内容有需要经常获取,可以设置开启,将该字段的内容单独存储。
PUT my_index { "mappings": { "_doc": { "properties": { "title": { "type": "text", "store": true } } } } }
倒排索引中记录的信息主要有:
倒排索引的查找过程本质上是通过单词找对应的文档列表的过程,因此倒排索引中字典的设计决定了倒排索引的查询速度,字典主要包括前缀索引(.tip文件)和后缀索引(.tim)文件。
字典前缀索引(.tip文件)
一个合格的词典结构一般有以下特点:
-查询速度快 -内存占用小 -内存+磁盘相结合
Lucene采用的前缀索引数据结构为FST,它的优点有:
词查找复杂度为O(len(str))
字典后缀(.tim文件)
后缀词块主要保存了单词后缀,以及对应的文档列表的位置。
文档列表(.doc文件)
lucene对文档列表存储进行了很好的压缩,来保证缓存友好:
上图经过压缩之后将6个数字从原先的24bytes压缩到7bytes。
文档列表的合并
ES的一个重要的查询场景是bool查询,类似于mysql中的and操作,需要将两个条件对应的文档列表进行合并。为了加快文档列表的合并,lucene底层采用了跳表的数据结构,合并过程中,优先遍历较短的链表,去较长的列表中进行查询,如果存在,则该文档符合条件。
倒排索引的查询过程
filter查询的缓存
对于filter过滤查询的结果,ES会进行缓存,缓存采用的数据结构是RoaringBitmap,在match查询中配合filter能有效加快查询速度。
倒排索引保存的是词项到文档的映射,也就是词项存在于哪些文档中,DocValues保存的是文档到词项的映射,也就是文档中有哪些词项。
相关设置
keyword字段默认开启
ES6.0(lucene7.0)之前
DocValues采用的数据结构是bitset,bitset对于稀疏数据的支持不好:
查询逻辑很简单,类似于数组通过下标进行索引,因为每个value都是固定长度,所以读取文档id为N的value直接从N*固定长度位置开始读取固定长度即可。
ES6.0(lucene7.0)
因为value存储的时候,空值不再分配空间,所以查询的时候不能通过上述通过文档id直接映射到在bitset中的偏移量来获取对应的value,需要通过获取docid的位置来找到对应的value的位置。
所以对于DocValues的查找,关键在于DocIDSet中ID的查找,如果按照简单的链表的查找逻辑,那么DocID的查找速度将会很慢。lucene7借用了RoaringBitmap的分片的思想来加快DocIDSet的查找速度:
最终DocIDSet的查找逻辑为:
查询过程(query then fetch)
get查询更快
默认根据id对文档进行路由,所以指定id的查询可以定位到文档所在的分片,只对某个分片进行查询即可。当然非get查询,只要写入和查询的时候指定routing,同样可以达到该效果。
主分片与副本分片
ES的分片有主备之分,但是对于查询来说,主备分片的地位完全相同,平等的接收查询请求。这里涉及到一个请求的负载均衡策略,6.0之前采用的是轮询的策略,但是这种策略存在不足,轮询方案只能保证查询数据量在主备分片是均衡的,但是不能保证查询压力在主备分片上是均衡的,可能出现慢查询都路由到了主分片上,导致主分片所在的机器压力过大,影响了整个集群对外提供服务的能力。
新版本中优化了该策略,采用了基于负载的请求路由,基于队列的耗费时间自动调节队列长度,负载高的节点的队列长度将减少,让其他节点分摊更多的压力,搜索和索引都将基于这种机制。
get查询的实时性
ES数据写入之后,要经过一个refresh操作之后,才能够创建索引,进行查询。但是get查询很特殊,数据实时可查。
ES5.0之前translog可以提供实时的CRUD,get查询会首先检查translog中有没有最新的修改,然后再尝试去segment中对id进行查找。5.0之后,为了减少translog设计的负责性以便于再其他更重要的方面对translog进行优化,所以取消了translog的实时查询功能。
get查询的实时性,通过每次get查询的时候,如果发现该id还在内存中没有创建索引,那么首先会触发refresh操作,来让id可查。
查询方式
两种查询上下文:
过滤(filter)的目标是减少那些需要进行评分查询(scoring queries)的文档数量。
分析器(analyzer)
当索引一个文档时,它的全文域被分析成词条以用来创建倒排索引。当进行分词字段的搜索的时候,同样需要将查询字符串通过相同的分析过程,以保证搜索的词条格式与索引中的词条格式一致。当查询一个不分词字段时,不会分析查询字符串,而是搜索指定的精确值。
可以通过下面的命令查看分词结果:
GET /_analyze { "analyzer": "standard", "text": "Text to analyze" }
相关性
默认情况下,返回结果是按相关性倒序排列的。每个文档都有相关性评分,用一个正浮点数字段score来表示。score的评分越高,相关性越高。
ES的相似度算法被定义为检索词频率/反向文档频率(TF/IDF),包括以下内容:
查询的时候可以通过添加?explain参数,查看上述各个算法的评分结果。
日志查询工具
TalkingData 移动广告监测产品Ad Tracking(简称ADT)的系统会接收媒体发过来的点击数据以及SDK发过来的激活和各种效果点数据,这些数据的处理过程正确与否至关重要。例如,设备的一条激活数据为啥没有归因到点击,这类问题的排查在Ad Tracking中很常见,通过将数据流中的各个处理环节的重要日志统一发送到ES,可以很方便的进行查询,技术支持的同事可以通过拼写简单的查询条件排查客户的问题。
{ "settings": { "index": { "refresh_interval": "120s", "number_of_shards": "12", "translog": { "flush_threshold_size": "2048mb" }, "merge": { "scheduler": { "max_thread_count": "1" } }, "unassigned": { "node_left": { "delayed_timeout": "180m" } } } } }
索引mapping的设置
{ "properties": { "action_content": { "type": "string", "analyzer": "standard" }, "time": { "type": "long" }, "trackid": { "type": "string", "index": "not_analyzed" } } }
点击数据存储(kv存储场景)
Ad Tracking收集的点击数据是与广告投放直接相关的数据,应用安装之后,SDK会上报激活事件,系统会去查找这个激活事件是否来自于之前用户点击的某个广告,如果是,那么该激活就是一个推广量,也就是投放的广告带来的激活。激活后续的效果点数据也都会去查找点击,从点击中获取广告投放的一些信息,所以点击查询在Ad Tracking的业务中至关重要。
业务的前期,点击数据是存储在Mysql中的,随着后续点击量的暴增,由于Mysql不能横向扩展,所以需要更换为新的存储。由于ES拥有横向扩展和强悍的搜索能力,并且之前日志查询工具中也一直使用ES,所以决定使用ES来进行点击的存储。
重要的设置
结合业务进行系统优化
结合业务定期关闭索引释放资源:Ad Tracking的点击数据具有有效期的概念,超过有效期的点击,激活不会去归因。点击有效期最长一个月,所以理论上每天创建的索引在一个月之后才能关闭。但是用户配置的点击有效期大部分都是一天,这大部分点击在集群中保存30天是没有意义的,而且会占用大部分的系统资源。所以根据点击的这个业务特点,将每天创建的索引拆分成两个,一个是有效期是一天的点击,一个是超过一天的点击,有效期一天的点击的索引在一天之后就可以关闭,从而保证集群中打开的索引的数据量维持在一个较少的水平。
结合业务将热点数据单独索引:激活和效果点数据都需要去ES中查询点击,但是两者对于点击的查询场景是有差异的,因为效果点事件(例如登录、注册等)归因的时候不是去直接查找点击,而是查找激活进而找到点击,效果点要找的点击一定是之前激活归因到的,所以激活归因到的这部分点击也就是热点数据。激活归因到点击之后,将这部分点击单独存储到单独的索引中,由于这部分点击量少很多,所以效果点查询的时候很快。
索引拆分:Ad Tracking的点击数据按天进行存储,但是随着点击量的增大,单天的索引大小持续增大,尤其是晚上的时候,merge需要合并的segment数量以及大小都很大,造成了很高的IO压力,导致集群的写入受限。后续采用了拆分索引的方案,每天的索引按照上午9点和下午5点两个时间点将索引拆分成三个,由于索引之间的segment合并是相互独立的,只会在一个索引内部进行segment的合并,所以在每个小索引内部,segment合并的压力就会减少。
其他调优
分片的数量
经验值:
JVM设置
通过_cat api获取任务执行情况
GET http://localhost:9201/_cat/thread_pool?v&h=host,search.active,search.rejected,search.completed
小技巧
参考资料
https://www.elastic.co/guide/...
https://github.com/Neway6655/...
https://www.elastic.co/blog/f...
https://blog.csdn.net/zteny/a...
https://www.elastic.co/blog/m...
作者:TalkingData战鹏弘
封面图来源于网络,如有侵权,请联系删除
另外一部分,则需要先做聚类、分类处理,将聚合出的分类结果存入ES集群的聚类索引中。数据处理层的聚合结果存入ES中的指定索引,同时将每个聚合主题相关的数据存入每个document下面的某个field下。