• 磨刀不误砍柴工—ElasticSearch的schema详解


    前言

    schema即元数据,自从数据库诞生的那一天,这个东西就作为数据库最重要的组成部分而出现了。schema就如同现实世界中的配方或者图纸,被数据库用来生产和管理数据。

    由于RDS的快速发展以及普及,schema已经成为数据库的标配以及重要的设计部分。好的schema设计以及成为了一套好的数据库系统不可或缺的部分。围绕schema的各种原则和规范也应运而生,如著名的数据库三大范式,都是为了帮助数据库设计者和使用者能更高效的使用和维护数据库。彼时,将schema比喻成是一个数据库的灵魂都不为过。

    但是,随着NOSQL的兴起,这种态势开始发生改变。首先发起挑战的是NOSQL的鼻祖HBase,HBase在应用过程中开创了“弱schema”的概念,即仍然应用schema,但是只是定义部分高层级的schema,细节的schema在数据写入的时候动态处理。即创建表的时候只定义到column family级别,而column family中具体的qualifier定义在数据写入时确认。详细内容可以参考:一文详解HBase表设计原则和实现icon-default.png?t=M5H6https://blog.csdn.net/microGP/article/details/115529065

    正是由于HBase的这种特性,导致HBase无法满足当时被尊为数据库设计圣典的三大范式而显得有些“大逆不道”,无数的RDS的DBA和从业者对HBase抱有很大的敌意和偏见。一时间,关于HBase的讨论甚嚣尘上。

    但是随着HBase在处理大数据上的优异表现,弥补了RDS在处理半结构化以及非结构化数据上的短板,也避免了大数据量下RDS schema修改带来的巨大开销。此时的NOSQL已经用实际行动成为大数据处理上的优先选择。

    然而故事并没有结束,而只是刚刚开始。今天的主角ElasticSearch登场了,更“大逆不道”的是,ElasticSearch允许使用者在不定义schema的情况下直接操作数据,ElasticSearch替用户来搞定schema相关的一切操作,对用户透明,用户开箱即用即可。

    听起来是不是很抓人眼球?和json一样,用户只管读写数据,其余的一概不用关心。

    就好比我想吃饭,ElasticSearch就是一个客人提供原料的饭店,我只要给他原始材料,告诉他我要吃饭,他就会根据你给的材料把菜做好端上来给你吃。而其他数据库更像是自己在家做饭,需要先自己买菜,然后根据菜谱自己做菜,然后再吃。

    如果排除获取数据以及使用数据这两部分,最麻烦的数据库处理ElasticSearch都帮我们做了。怎么看都是我们血赚,但是现实真的是这样吗?

    免费的往往可能是最贵的

    免费的往往是最贵的这个至理名言,我相信每个人都有自己的理解和经历。不管是商场的免费服务还是推销的免费试用,乍看起来是我们赚了,其实如果你把持不住,后续的各种成本能惊掉你的下巴。

    回到本文的主角ElasticSearch,这种“开箱即用”也是有代价的。如果你只是做个demo,感受下ElasticSearch的特性,那肯定没有任何问题。但是如果想在生产环境中使用ElasticSearch,“开箱即用”在数据量大时候的性能之差以及资源消耗之多会让你怀疑人生。

    为什么会这样呢?其实ElasticSearch的“开箱即用”并不是没有schema,而是ElasticSearch按照你给的数据根据“自己的理解”生成一套schema。那这样会带来什么影响呢?

    • 首先是影响写入速度。对于字段类型,ElasticSearch只能根据字面量来进行推断,这样第一浪费时间,耽误效率。

    • 其次是浪费系统资源,包含但不限于内存、硬盘、CPU以及IO。由于字段类型的判断并不一定是我们需要的。要知道不同的数据类型,需要创建的索引是不一样的,进而消耗的资源也是不一样的。对于我们不需要的功能和使用场景,这种资源的浪费显然是需要避免的。

    • 最后是影响数据使用效率。要知道每个服务器资源是有限的,当你将有限的资源浪费在不需要的场景中,自然对应的需要使用的场景中的资源数量就会受限,从而引发一系列的不良影响,最终影响到正常的使用效率和使用体验。

    综上,就好比我给饭店一堆材料,让饭店给我做饭。虽然我只想吃一个最简单的麻辣烫,但是厨师在拿到我给的材料后思考了下我到底想吃什么菜,然后根据这些菜的菜谱对材料做了各种处理,准备了各种配菜,然后做了一桌酒席送了上来。

    虽然我很感动,但是最后我还是大骂了厨师一顿。毕竟我就是想二十分钟吃一个简单的麻辣烫,结果他花了两小时给我搞了个我不一定爱吃的各种菜,还收了我很多的钱。故事的结局是我很不爽,厨师又很委屈,你说这哪里说理去?

    说到底还是怪自己没和厨师商量好。如果我把我的需求说明白,可能就没这么多麻烦事了。

    在ElasticSearch的世界中,这就是schema的存在意义,千万别迷信什么“开箱即用”,好好设计schema是一个好的ElasticSearch应用的必备条件。正所谓“磨刀不误砍柴工”,花点时间设计schema能避免后期很多很难解决或者无法解决的问题。

    做了很多铺垫,下面就来看看一个好的schema到底长什么样子。

    准备知识

    在讲解如何设计好schema之前,先来科普些基础知识作为铺垫。

    概念对应

    由于大家对SQL标准比较熟悉,所以先来看看ElasticSearch中的概念和SQL的对应关系:

    SQLElasticSearchSQL/ElasticSearch
    columnfield列/属性
    rowdocument行/文档
    tableindex表/索引
    databasecluster数据库/ ES 集群
    clustercluster多个数据库/ES 集群

    针对上面有两点说明:

    • 从上表可以看出,其实ElasticSearch是不严格区分SQL中的database的。即没有严格意义上的database级别的数据和资源隔离(有其他方式的隔离,如ilm)。如果想要实现此功能可以部署多个ElasticSearch的集群;如果只想进行数据区分,而不需要隔离的话,在一个ElasticSearch集群中使用不同前缀的index来区分即可。

    • 在ElasticSearch 7 版本以前在index下有一个type的概念。原本是想用type来代表table,index来对应database。但是后期的效果没有达到预期,反而带来了很多问题,于是在7版本已经开始放弃type这个概念,转而使用上面的这种对应关系。

    名词解释

    首先schema是对针对一个index或者一组index(ElasticSearch中的template)来设置的。而schema主要分为两部分,即setting和mapping。下面用过一个例子来讲解下:

    setting

    setting是index的整体配置,这些配置从整体上框定了整个index的边界,后续的index就会在这个大框架下展开。setting的配置会从根本上影响后续index的性能以及扩展性,而且不同的需求和应用场景也会有不同的配置。下面看这个例子:

    1. {
    2.     "test000":{
    3.         "settings":{
    4.             "index":{
    5.                 "codec":"best_compression",
    6.                 "refresh_interval":"5s",
    7.                 "number_of_shards":"16",
    8.                 "translog":{
    9.                     "flush_threshold_size":"1gb",
    10.                     "sync_interval":"120s",
    11.                     "retention":{
    12.                         "age":"1h"
    13.                     },
    14.                     "durability":"async"
    15.                 },
    16.                 "provided_name":"test000",
    17.                 "merge":{
    18.                     "policy":{
    19.                         "segments_per_tier":"7",
    20.                         "max_merge_at_once":"7",
    21.                         "max_merged_segment":"10gb"
    22.                     }
    23.                 },
    24.                 "creation_date":"1634659200652",
    25.                 "number_of_replicas":"0",
    26.                 "uuid":"SdDe_0bDQEaT4J7RlXiToQ",
    27.                 "version":{
    28.                     "created":"6000099"
    29.                 }
    30.             }
    31.         }
    32.     }
    33. }

    直接捞干货,讲下比较有价值的配置项:

    • 首先setting中最重要的是index以及merge两个配置大项,一个配置index的整体属性,一个配置index中segments merge的相关参数。

    • index中的codec控制着ElasticSearch的压缩算法,默认的是lz4,这是一种比较均衡的压缩算法,适合大多数场景。但是如果比较在意硬盘耗费量,可以考虑使用best_compression这个配置项,即使用DEFLATE压缩算法。这个压缩算法的压缩比更高,但这会占用更多的CPU资源。修改过后空间占用量可以下降15%~25%。

    • index中的refresh_interval控制着数据多久从堆内存刷新到操作系统的Page Cache,只有刷新到Page Cache数据才会被Search到,所以这在很大程度上影响到数据展示的实时性。但是实时性过高也是有代价的。频繁的refresh会导致大量的小segment的生成,search的时候会增加很多IO;更多的segment也会影响到segment merge的触发频率,进而增加系统的IO压力;再加上很多缓存的失效,所以除非必要,这个值还是需要重新配置下的。比如:30s

    • index中的number_of_shards用来控制一个index中shard的数量。由于ElasticSearch特殊的search机制,所以shard数量一旦确认,就没办法修改了。而且一个shard合理的存储量是有限的,所以shard数目确认后,整个index能存储的数据量上限也就确认了,如果想扩展shard的数量,只能通过ElasticSearch的reindex操作来实现。这其实就是数据复制重新分配的过程,耗时耗资源增加系统负载。另外,考虑到后续可能有缩减shard数量的需求,配合shrink操作的特点,number_of_shards的数量最好配置成有很多约数的数字,如16或者24这种,这样ElasticSearch可以shrink到约数个shard;反之如果设置成一个素数,那么只能shrink到1个shard了。

    • index中的translog用来配置ElasticSearch中事务日志,事务日志用来容灾,相关的配置此处不详细说明,网上资料很多,大家可以查看下,基本上根据不同场景有几套固定的配置,大家按需使用即可。

    • merge中的配置是用来配置segment merge相关的操作,这是ElasticSearch中相当复杂的一个部分,这里就先不展开了,后续有机会单拿一篇文章来讲解。直接看配置项说明一下:segments_per_tier是每层所允许的分段数 默认为10。较小的值意味着更多的合并,但是存在较少的分段。需要注意的是,这个值必须 >=max_merge_at_once 不然就会强制执行太多的合并;max_merge_at_once是一次合并最多合并多少个segment;max_merged_segment是参与merge的最大分段大小 默认为5gb,但这个值是近似值,需要考虑删除文档的百分比带来的影响。

    mapping

    mapping其实更贴近于RDS中的schema,用来定义每个字段的类型、属性以及查询索引创建的规则。先来看看mapping相关的参数。

    索引mapping参数

    • index:控制字段值是否被索引。它可以设置为true或false,默认为true。未被索引的字段不会被查询到,但是可以聚合。除非禁用doc_values。

    • doc values:默认情况下,大多数字段都被索引,这使得它们可以搜索。倒排索引根据term找到文档列表,然后获取文档原始内容。但是排序和聚合,以及从脚本中访问某个字段值,需要不同的数据访问模式,它们不仅需要根据term找到文档,还要获取文档中字段的值。这些值需要单独存储。doc_values 就是用来存储这些字段值的。它是一种存储在磁盘上的列式存储,在文档索引时构建,这使得上述数据访问模式成为可能。它们以面向列的方式存储与_source相同的值,这使得排序和聚合效率更高。几乎所有字段类型都支持doc_values,但被分析(analyzed)的字符串字段除外(即text类型字符串,它使用的是field data,默认是关闭的)。doc_values默认启用。

    • store:默认情况下,字段值会被索引使它们能搜索,但它们不会被存储(stored)。意味着可以通过这个字段查询,但不能取回它的原始值。但这没有关系。因为字段值已经是_source字段的一部分,它是被默认存储的。如果只想取回一个字段或少部分字段的值,而不是整个_source,则可以通过source filtering达到目的。

    通过上面的介绍可以发现,ElasticSearch的schema配置问题被转化成下面四个主要问题:

    • 字段是什么类型?

    • 字段需不需要被查询?

    • 字段需要不要排序和聚合?

    • 字段需不需要单独存储?

    这样子就好理解多了,看面看这个例子加深下理解:

    1. {
    2.     "test000":{
    3.         "mappings":{
    4.             "test_type":{
    5.                 "dynamic":"false",
    6.                 "numeric_detection":false,
    7.                 "properties":{
    8.                     "agent_id":{
    9.                         "type":"keyword"
    10.                     },
    11.                     "app_info":{
    12.                         "type":"keyword",
    13.                         "doc_values":false,
    14.                         "index":false
    15.                     },
    16.                     "app_name":{
    17.                         "type":"text",
    18.                         "norms":{"enabled"false},
    19.                         "index_options":"freqs" 
    20.                     },
    21.                     "app_version":{
    22.                         "type":"keyword"
    23.                     },
    24.                     "cert_issure":{
    25.                         "type":"keyword"
    26.                     },
    27.                     "city":{
    28.                         "type":"keyword"
    29.                     },
    30.                     "client_ip":{
    31.                         "type":"keyword"
    32.                     }
    33.                 }
    34.             }
    35.         }
    36.     }
    37. }

    直接捞干货,讲下比较有意义的优化:

    禁用对你来说不需要的特性

    默认情况下,ES为大多数的字段建立索引,并添加到doc_values,以便使之可以被搜索和聚合;对于text类型字段,由于涉及到分词、评分以及高亮等机制,相关的索引也需要被添加。另外,本文的schema是基于ElasticSearch 6.X进行的说明,所以在mapping中有type(test_type)相关的配置,如果是ElasticSearch 7.X及以上版本,将type这一项去掉即可。

    但是大多数情况下这些属性不需要同时存在,所以禁用不需要的特性就显得十分必要了。下面有几个规则可供参考:

    通用规则:

    • 减少字段数量,对于不需要建立索引的字段,不写入ES。尤其对于诸如HBase+ES这种索引和存储分离,ES仅做二级索引的场景。

    • 将不需要分词的字段设置为keyword类型;不需要建立索引的字段index属性设置为false。对字段不分词,或者不索引,可以减少很多运算操作,降低CPU占用。尤其是binary类型,默认情况下占用CPU非常高,而这种类型进行分词通常没有什么意义。

    • 减少字段内容长度,如果原始数据的大段内容无须全部建立索引,则可以尽量减少不必要的内容。

    • 使用不同的分析器(analyzer),不同的分析器在索引过程中运算复杂度也有较大的差异。

    具体规则:

    • 如果某个字段仅需要展示,而不需要用作被搜索的条件,则可以将此字段的index属性设置为false。这样针对该字段所有的查询索引都不会被创建,对于系统资源的节省效果明显。参见上面例子中的app_info字段

    • 所有字段都支持doc_values的字段都默认启用了doc_values。如果确定不需要对字段进行排序或聚合,或者从脚本访问字段值,则可以禁用doc_values。禁用之后硬盘上的.dvd和.dvm后缀的文件将不会生成,相关的缓存的内存占用也会被节省,对于系统资源的节省效果明显。参见上面例子中的app_info字段

    • text(text类型应该也可以配置,keyword类型默认关闭norms)类型的字段会在索引中存储归一因子(normalizationactors),以便对文档进行评分,如果只需要在文本字段上进行分词匹配,而不关心生成的得分,则可以配置ElasticSearch不将norms写入索引。禁用之后硬盘上的.nvd和.nvm后缀的文件将不会生成,相关的缓存的内存占用也会被节省,对于系统资源的节省效果明显。参见上面例子中的app_name字段

    • text类型的字段默认情况下也在索引中存储频率和位置。频率用于计算得分,位置用于执行短语(phrase)查询。在text类型的字段上,index_options的默认值为positions。index_options参数用于控制添加到倒排索引中的信息,可选的配置项为:freqs 文档编号和词频被索引,词频用于为搜索评分,重复出现的词条比只出现一次的词条评分更高;positions 文档编号、词频和位置被索引。positions被用于邻近查询(proximity queries)和短语查询(phrase queries)。如果不需要运行短语查询,则可以告诉ES不索引位置,即index_options仅配置频率。禁用之后硬盘上的.pos后缀的文件将不会生成,相关的缓存的内存占用也会被节省,对于系统资源的节省效果明显。参见上面例子中的app_name字段

    • 在ElasticSearch 6.x 版本中,数值类型使用的是BKD-Tree 索引数据结构,适合对数值类型进行范围查询;如果数值类型只会进行精确查询或者是有限个数的integer,设置成keyword, 使用倒排索引进行查询的效率要高。配置之后硬盘上的.dim和.dii后缀的文件将不会生成,相关的缓存的内存占用也会被节省,对于系统资源的节省效果明显。

    schema管理

    对于ElasticSearch的schema管理,尤其是针对按照固定规则管理索引的场景,建议使用template来进行管理。

    这样在client端想生成新的索引的时候,只需要按照规则匹配到对应的模板即可。而且模板可以进行优先级设置来满足模板的嵌套,用以实现更复杂的场景和需求。

    最后用一个相对来说的通用模板的例子以及说明供大家参考来结束本文:

    1. {
    2.        "template""*",//模板匹配全部的索引
    3.        "order"0,// 具有最低的优先级,让用户定义的模板有更高的优先级,以覆盖这个模板中的配置
    4.        "settings": {
    5.               "index.merge.policy.max_merged_segment""2gb",//大于2g的segment不参与merge
    6.               "index.merge.policy.segments_per_tier""24",//每层分段的数量为24,增加每个segment的大小,减少segment merge的发生的次数
    7.               "index.number_of_replicas""1",//数据1备份,容灾并提升数据查询效率
    8.               "index.number_of_shards""24",//每个index有24个shard,这个数字需要根据数据量进行评估,原则上是尽量的少,毕竟shard过多对Elasticsearch的压力也会增加很多。
    9.               "index.optimize_auto_generated_id""true",//自动生成doc ID
    10.               "index.refresh_interval""30s",//refresh的自动刷新间隔,刷新后数据可以被检索到,根据业务的实时性需求来配置该值
    11.               "index.translog.durability""async",//异步刷新translog
    12.               "index.translog.flush_threshold_size""1024mb",//translog强制flush的大小阈值
    13.               "index.translog.sync_interval""120s",//translog定时刷新的间隔,可以根据需求调节该值
    14.               "index.unassigned.node_left.delayed_timeout""5d"//该配置可以避免某些Rebalancing操作,该操作会带来很大的开销,如果节点离开后马上又回来(如网络不好,重启等),则该开销完全没有必要,所以在集群相对稳定以及运维给力的前提下,尽量增大该值以避免不必要的资源开销
    15.        }
    16.        "mappings":{
    17.             "test_type":{
    18.                 "dynamic":"false",
    19.                 "numeric_detection":false,
    20.                 "properties":{
    21.                     "agent_id":{
    22.                         "type":"keyword"
    23.                     },
    24.                     "app_info":{
    25.                         "type":"keyword",
    26.                         "doc_values":false,
    27.                         "index":false
    28.                     },
    29.                     "app_name":{
    30.                         "type":"text",
    31.                         "norms":{"enabled"false},
    32.                         "index_options":"freqs" 
    33.                     },
    34.                     "app_version":{
    35.                         "type":"keyword"
    36.                     },
    37.                     "cert_issure":{
    38.                         "type":"keyword"
    39.                     },
    40.                     "city":{
    41.                         "type":"keyword"
    42.                     },
    43.                     "client_ip":{
    44.                         "type":"keyword"
    45.                     }
    46.                 }
    47.             }
    48.         }
    49. }

    文章到这里就结束了,最后路漫漫其修远兮,大数据之路还很漫长。如果想一起大数据的小伙伴,欢迎点赞转发加关注,下次学习不迷路,我们在大数据的路上共同前进!

    挂个公众号二维码,公众号的文章是最新的,CSDN的会有些滞后,想追更的朋友欢迎大家关注公众号,谢谢大家支持。 

    公众号地址:

     

  • 相关阅读:
    SQL Server创建数据库
    二叉树:什么样的二叉树适合用数组来存储?
    两道面试小Demo
    Linux安装Oracle19c(极简版)
    43、SpringMvc创建拦截器(拦截器的配置)
    2022-03-18-SpringBoot
    给电脑一键重装系统后找回照片查看器的方法
    不花一分钱,在 Mac 上跑 Windows(M1/M2 版)
    进程相关介绍(二)
    MySQL基础【学习至数据的导入导出】
  • 原文地址:https://blog.csdn.net/microGP/article/details/125501544