谈谈“灵活的胖子”ES的索引优化,以及在易观锆云的实践

易观 2018-08-29 570

多管齐下,让Elasticsearch的索引优化起来。

导读:ElasticSearch是基于Lucene的搜索服务器。它是提供了分布式多用户能力的全文搜索引擎。Elasticsearch提供了强大的搜索、集合功能。这里主要想跟大家探讨下Elasticsearch索引结构设计的优化以及Elasticsearch在易观锆云产品中的实践经验。


一、概述


Elasticsearch提供了丰富的数据类型,支持各种复杂的数据结构和复杂的业务场景。为了能够实现更快的索引速度和查询速度,在设计索引结构时,会考虑尽量减少字段数,减小文档大小,缩短索引过程,以期提高检索的速度。另外也会结合业务场景在不同数据结构之间要有所取舍。


二、优化策略

 

1._id生成策略

_id的生成策略对索引速度影响非常大。索引数据时,如果指定_id,Elasticsearch的版本检查机制会校验该_id是否存在,并生成新的版本号,默认先从缓存中取版本信息。如缓存中不存在,则要执行一次检索,随着数据量的增加,该过程的耗时将逐步增加,写入速度逐步下降。如果不指定_id,使用Elasticsearch自增ID,可以跳过版本检查机制,大大提高索引速度。但是,这种场景主要用于不可变数据导入的场景,例如日志导入。而对于大多数业务场景,都需要指定_id,在很多情况下,需要使用文档中一个或多个字段拼接生成唯一字符串,作为_id。这种情况下,能很好地保证数据的唯一性,但是,无规则的字符串作为_id会明显拖累索引速度,根据测试结果,纯数字的_id,效率最高。


2. text or keyword

Elasticsearch中,字符串类型可以选择text和keyword,text类型在写入数据时,会被分词。因此,如果没有分词查询的需要,字符串类型一律使用keyword类型更为高效。

 

3. number or keyword

我们并非一定要用数值类型的字段(byte、integer、long...)存储数字,Elasticsearch在索引数据的时候,对不同数据类型做了不同优化以便于应对不同检索场景,对数值类型,提高了range query的查询速度,而对keyword类型,提高了term query 的查询速度. 因而在设计索引的时,针对数字,如各种ID,若不存在range query的情况,优先使用keyword类型。


4. 尽量避免join

作为全文检索的搜索引擎,join查询,一直是Elasticsearch的弱项。但在日常业务中,有些情况下,又不可避免的需要使用join查询。Elasticsearch 提供了nested、parent-child两种关联查询的方式。但不幸的是,这两种查询方式极大地降低了查询效率,nested会是查询效率下降基本,而会产生几十上百倍的下降。在doc数较小,并发较小的情况下,这种损失可以承受,但是对于亿级甚至十亿级的集群,这种性能损失是无法忍受的,另外,使用关联查询,索引结构变得复杂,索引数据的速度同样受到影响,nested结构的文档无法进行索引排序(index sorting),而这种方式可以很好的提高检索性能。


5.慎用doc_values

Elasticsearch使用倒排索引存储数据,能够提供极高的检索性能,但有得必有失,这种结构并不能带来很好的排序性能。Elasticsearch提供了doc_values功能,实现对倒排索引的装置,这是一种列式存储,能够提供高效的排序和聚合,默认情况下,Elasticsearch会个每个字段开启doc_values,如果某个字段并没有排序和聚合的需求,可以禁用该字段的doc_values属性,以减少索引数据和存储的性能消耗。

 

6._all 字段的使用

_all字段是把其它字段值当作一个大字符串来索引的特殊字段,query_string 查询子句(搜索 ?q=john )在没有指定字段时默认使用 _all 字段,这种设置有利于全文检索,在6.0.0以前的版本里,_all字段默认开启。实际使用过程中,我们并不一定需要这么大的字段,甚至不需要这个字段。根据业务需求,我们可以考虑禁用_all字段,或者使用使用copy_to指定字段拼接。


7._source 字段的优化

_source字段存储了原始json文档,默认包含所有字段。虽然在获取文档字段或整个文档内容时,非常高效,但同样也占用了大量存储空间。如果某些字段只需要进行检索,无需返回值,那么可以通过在_soucrce 中配置includes和excludes来自定义字段存储,节省存储空间。


8.dynamic mapping的使用

Elasticsearch默认允许在索引数据的时候可以动态新增字段,但是这种配置有相当风险。因为Elasticsearch的字段不允许删除和修改数据类型,一旦在索引数据时出现意外,这可能导致索引结构急速膨胀。而且,为了提高读写性能我们需要对很多字段进行特殊设置,建议禁用dynamic mapping功能,或者通过dynamic_templates预设数据类型。


9.更少的索引字段

由于Elasticsearch 的join查询功能的弱项,通常情况下,每个文档都包含大量字段,有些字段我们需要用来进行检索过滤,有些字段仅仅用来存储数据。因而在设计索引的时候,对于无需检索的字段可以设置enable=false,这样只存储不索引,可以提高索引速度,同时,这种字段可以合并成单个字段。

 

10.预索引数据

在某些业务场景下,为了提高检索和聚合的速度,我们可以对某些字段的进行预分组。如年龄字段经常有需要按照年龄段进行检索或聚合,这种情况下,我们可以在每个文档增加年龄段这个字段,使用keyword类型,这样可以大大提高检索和聚合的速度。


三、易观锆云产品中ES索引的设计


易观锆云面向企业级用户,提供一站式的第三方数据交易及算力加持服务。基于Elasticsearch提供实时/离线数据服务,数据总量大,文档字段多,业务复杂,并发高,伴随着高速写入,读写矛盾严重。对此我们做了多方位的优化,尽最大努力提高集群性能,以下是索引结构方面优化总结:

 

1. 文档含数十个字段,且多个字段中存储数十条数据。因业务需要,唯一标识的字段是无规则的字符串。索引数据时,我们对该字段进行了散列转换、取hash等操作,保证了极低的重复率,也提高了数据索引速度。

 

2. 大量字段经过标准化处理过后,都使用id存储,在设计索引结构时,统一使用keyword存储,提高了检索和聚合性能;禁用了dynamic mapping,自定义copy_to 。

 

3. 增加分组字段,如,年龄age按照age_range进行预处理,提高检索和聚合性能。


4. 在锆云产品中,有多个字段,是 id -> [value1,value2...]结构,并且需要进行关联查询,就需要使用nested Object结构,这大大降低了数据写入速度和查询速度,同时,nested结构使得我们无法在写入数据时,使用顺序存储(index sorting)。对此,我们将字段合并,避开了object结构,同时,更好的支持了关联查询。


如, app_tgi:{id:integer, tgi:float},每个app_tgi字段都要存储几十条数据,业务中,需要对id及tgi进行关联查询过滤,在实际索引中,我们通过将id和tgi分别定长,然后拼装成一个整数,如id=1000023,tgi=1.6558 最终生成的app_tgi = 10000230000016558,同时,对app_tgi字段进行索引排序,这样,在实际使用过程中,如果需要读检索 id=id=1000023,tgi> 1.6558,就可以通过  app_tgi > 10000230000016558 && app_tgi < 10000239999999999,nested 查询转变成数值的range查询,大大提高了检索速度。

 

综上所述,针对Elasticsearch索引结构的优化就是让字段更少,文档更小,读写更快。业务场景不同,取舍不同,锆云产品查询的压力高于写入压力,某些情况下我们牺牲了写入的性能来获得更好的查询性能。Elasticsearch的调优是多方位的,在后续的文章中,我们将逐步介绍易观在使用Elasticsearch过程中,在服务端、客户端针对读、写、稳定性等各方面的优化经验。


2018易观A10峰会尊享票