ElasticSearch深入:内部机制浅析(三)@
一、Shard
Shard 实际上是一个 Lucene 的一个实例(Lucene Index),但往往一个 Elastic Index 都是由多个 Shards (primary & replica)构成的。
特别注意,在单个 Lucene 实例里最多包含2,147,483,519 (= Integer.MAX_VALUE - 128)
个 Documents。
可以利用 _cat/shards API 来监控 shards 大小。
二、Lucene Index 结构
一个 Lucene Index 在文件系统的表现上来看就是存储了一系列文件的一个目录。一个 Lucene Index 由许多独立的 segments 组成,而 segments 包含了文档中的词汇字典、词汇字典的倒排索引以及 Document 的字段数据(设置为Stored.YES
的字段),所有的 segments 数据存储于 _<segment_name>.cfs
的文件中。
1.Segments
Segment 直接提供了搜索功能的,ES 的一个 Shard (Lucene Index)中是由大量的 Segment 文件组成的,且每一次 fresh 都会产生一个新的 Segment 文件,这样一来 Segment 文件有大有小,相当碎片化。ES 内部则会开启一个线程将小的 Segment 合并(Merge)成大的 Segment,减少碎片化,降低文件打开数,提升 I/O 性能。
Segment 文件是不可变更的。当一个 Document 更新的时候,实际上是将旧的文档标记为删除,然后索引一个新的文档。在 Merge 的过程中会将旧的 Document 删除掉。具体到文件系统来说,文档 A 是写入到 _<segment_name>.cfs
文件里的,删除文档 A 实际上是在_<segment_name>.del
文件里标记某个 document 已被删除,那么下次查询的时候则会跳过这个文档,是为逻辑删除。当归并(Merge)的时候,老的 segment 文件将会被删除,合并成新的 segment 文件,这个时候也就是物理删除了。
2.Type or Index
先来看看一个比较完备的 Schema
{ "zipkin-2017-03-01": {
"aliases": {},
"mappings": {
"_default_": {
"_all": {
"enabled": false
},
"dynamic_templates": [
{
"strings": {
"match": "*",
"match_mapping_type": "string",
"mapping": {
"ignore_above": 256,
"type": "keyword"
}
}
},
{
"value": {
"match": "value",
"mapping": {
"ignore_above": 256,
"ignore_malformed": true,
"match_mapping_type": "string",
"type": "keyword"
}
}
},
{
"annotations": {
"match": "annotations",
"mapping": {
"type": "nested"
}
}
},
{
"binaryAnnotations": {
"match": "binaryAnnotations",
"mapping": {
"type": "nested"
}
}
}
]
},
"span": {
"_all": {
"enabled": false
},
"dynamic_templates": [
{
"strings": {
"match": "*",
"match_mapping_type": "string",
"mapping": {
"ignore_above": 256,
"type": "keyword"
}
}
},
{
"value": {
"match": "value",
"mapping": {
"ignore_above": 256,
"ignore_malformed": true,
"match_mapping_type": "string",
"type": "keyword"
}
}
},
{
"annotations": {
"match": "annotations",
"mapping": {
"type": "nested"
}
}
},
{
"binaryAnnotations": {
"match": "binaryAnnotations",
"mapping": {
"type": "nested"
}
}
}
],
"properties": {
"annotations": {
"type": "nested",
"properties": {
"endpoint": {
"properties": {
"ipv4": {
"type": "keyword",
"ignore_above": 256
},
"serviceName": {
"type": "keyword",
"ignore_above": 256
}
}
},
"timestamp": {
"type": "long"
},
"value": {
"type": "keyword",
"ignore_above": 256
}
}
},
"binaryAnnotations": {
"type": "nested",
"properties": {
"endpoint": {
"properties": {
"ipv4": {
"type": "keyword",
"ignore_above": 256
},
"port": {
"type": "long"
},
"serviceName": {
"type": "keyword",
"ignore_above": 256
}
}
},
"key": {
"type": "keyword",
"ignore_above": 256
},
"value": {
"type": "keyword",
"ignore_above": 256
}
}
},
"duration": {
"type": "long"
},
"id": {
"type": "keyword",
"ignore_above": 256
},
"name": {
"type": "keyword",
"ignore_above": 256
},
"parentId": {
"type": "keyword",
"ignore_above": 256
},
"timestamp": {
"type": "long"
},
"timestamp_millis": {
"type": "date",
"format": "epoch_millis"
},
"traceId": {
"type": "keyword"
}
}
},
"dependencylink": {
"_all": {
"enabled": false
},
"dynamic_templates": [
{
"strings": {
"match": "*",
"match_mapping_type": "string",
"mapping": {
"ignore_above": 256,
"type": "keyword"
}
}
},
{
"value": {
"match": "value",
"mapping": {
"ignore_above": 256,
"ignore_malformed": true,
"match_mapping_type": "string",
"type": "keyword"
}
}
},
{
"annotations": {
"match": "annotations",
"mapping": {
"type": "nested"
}
}
},
{
"binaryAnnotations": {
"match": "binaryAnnotations",
"mapping": {
"type": "nested"
}
}
}
],
"properties": {
"callCount": {
"type": "long"
},
"child": {
"type": "keyword",
"ignore_above": 256
},
"id": {
"type": "keyword",
"ignore_above": 256
},
"parent": {
"type": "keyword",
"ignore_above": 256
}
}
}
},
"settings": {
"index": {
"number_of_shards": "6",
"provided_name": "zipkin-2017-03-01",
"creation_date": "1488328116434",
"requests": {
"cache": {
"enable": "true"
}
},
"analysis": {
"filter": {
"traceId_filter": {
"type": "pattern_capture",
"preserve_original": "true",
"patterns": [
"([0-9a-f]{1,16})$"
]
}
},
"analyzer": {
"traceId_analyzer": {
"filter": "traceId_filter",
"type": "custom",
"tokenizer": "keyword"
}
}
},
"number_of_replicas": "2",
"uuid": "_9j3jYVDRbOp8rOec8fEqw",
"version": {
"created": "5020199"
}
}
}
}
}
这里的 zipkin-2017-03-01
是 Index name,span
和 dependencylink
是 Type name。很多人在前期规范 Schema 的时候犯难,不知道应该直接用 Type 表示数据 Schema 好还是新建一个 Index ?这里接下来会进一步探讨。
- Index 局限性
当需要对大量的 index 做聚合查询的时候,是将 index中的每一个分片单独查询,再将结果聚合出来,如果量级特别大,那么占用的 CPU 和内存资源将会十分庞大;
- Type 局限性
在同一个 Index 下,可以通过 _Type
字段指定不同的 Type,其中无论多少个 Type 的聚合查询,shard 数目维持不变,因此聚合所消耗的资源大量减少,然而也会有一定的限制:
① 同一 Index 下,同名 Field 类型必须相同,即使不同的 Type;
② 同一 Index 下,TypeA 的 Field 会占用 TypeB 的资源(互相消耗资源),会形成一种稀疏存储的情况。尤其是 doc value ,为什么这么说呢?doc value为了性能考虑会保留一部分的磁盘空间,这意味着 TypeB 可能不需要这个字段的 doc_value 而 TypeA 需要,那么 TypeB 就被白白占用了一部分没有半点用处的资源;
③ 同理,Score 评分机制是 index-wide 的,这会导致评分受到影响。
ES 本身是 Schemaless 的,意味着你前期设计需要更多的花费心思,没有类似于 RDB 的范式。原则上来说,只要基本尊重范式要求,关系型数据库的设计问题都不大,而对于 Schemaless 的 ES 来说,前期设计数据结构的时候需要多花费点心思。
基本来说,数据关联度不大的不建议放到同一个 Index 且以 Type 来区分,如果考虑到 Index 分片过大导致后续检索压力增加的情况,应该对 Index进行拆分,ES 的通常做法是,以时间为维度做数据拆分,比如一天一个 Index 或者一个月一个 Index,依据数据量大小来衡量。
tips: 对于字段信息变更频繁的数据, 也不适合与其他 Index 存在一起且以 Type 来区分。因为在 ES 中,索引元数据本身是放在主节点中维护的,CP 设计。意味着涉及到大量字段变更及元数据变更的操作,都会导致该 Index 被堵塞或假死。我们应该对这样的 Index 做隔离,避免影响到其他 Index 正常的增删改查。甚至当涉及到字段变更十分频繁且无法预定义 schema 的场景时,是否要使用 ES 都应该慎思熟虑了!
3._source
& _all
& _field_names
字段
在定义某个 Index 的 schema 的时候,涉及到这几个参数,任何事情都不是一概而论的,因此需要结合具体场景考量一下这些参数设置的意图。
_source
:Lucene 对于源文档的存储提供了一个选项(Stored.YES/NO),如果选了YES,其数据就会检索的过程中返回;否则 Lucene 即使成功做了检索,也只能返回数据的 ID,然后借助第三方存储通过 ID 将数据查询出来。该参数默认enabled
,表示需要将文档原始数据一并存入索引之中,disabled
则表示我的 Lucene 实例中只包含倒排索引以及词典,不对原始文档做持久化;_all
:默认enabled
,意思是需要用额外的存储空间存储该 Index 的各字段信息以及数据,目的是检索的过程中可以不需要指定任何字段,也即是全字段匹配;对于字段结构简单的,可以disabled
取消掉这块功能;_field_names
字段 :这个字段默认开启,存储的是每个 document 中的字段名,如果没有检查 Document 的字段是否存在这样类似的需求的话,建议关闭,写入性能大约提升20%。
4.doc_value
之前介绍过,倒排索引的数据组织方式大概是这样的:
如果我要查询包含 brown
的文档有哪些?这个就是全文检索了,也相当好办,先从词典里遍历到 brown
这个单词,然后根据倒排索引查得 Doc_1 和 Doc_2 包含这个单词。那如果我要查 Doc_1 和 Doc_2 包含的单词分别有什么?这个用倒排索引的话开销会非常大,至少是要将整张表关于 Doc_1 和 Doc_2 的列数据遍历一遍才行。这时候我们将数据换一种组织形式,将会起到非常好的效果。
Doc_1 和 Doc_2 存了什么单词,一目了然。
当然对于数字类型的字段也是一样的。
我们把这种数据的组织方式叫做doc_value
。
倒排索引的特点很明显,就是为了全文检索而生的,但是对于一些聚合查询(排序、求平均值等等)的场景来说,显然不适用。那么这样一来我们为了应对一些聚合场景就需要结构化数据来应付,这里说的结构化数据就是『列存储』,也就是上面说的
doc_value
。When searching, we need to be able to map a term to a list of documents.When sorting, we need to map a document to its terms. In other words, we need to “uninvert” the inverted index.This “uninverted” structure is often called a “column-store” in other systems. Essentially, it stores all the values for a single field together in a single column of data, which makes it very efficient for operations like sorting
doc_value
在 ES 中有几个应用场景:
- 对某个字段排序;
- 某个字段聚合查询( max/min/count );
- 部分过滤器 ( 地理位置过滤器 );
- 某个字段的脚本执行。等等。
doc_value
是顺序存储到磁盘的,因此访问是很快的。当我们所处理的集合小于所给的 JVM 堆内存,那么整个数据集合是会被加载到内存里的;如果数据集合大于所给的堆内存,那么就会分页加载到内存之中,而不会报出『OutOfMemory Error』。
值得一提的是,doc_value
的字段使用极其频繁,因此在5.x 版本后强化成为两个字段,分别是 text 和 keyword。
text
:string 类型,支持倒排索引,不支持doc_value
;keyword
:string 类型,不支持倒排索引,支持doc_value
。
三、父子关系树构建
上面介绍了doc_value
,有个很重要的应用便是利用doc_value
构建父子关系的树。这里说的父和子实际上是两个独立的 document,所以和嵌套对象(nested objects)是不一样的,关于嵌套对象会在未来的文章里叙述。这样父子节点独立存储的好处是,父与子之间的更新或者增加节点都不会相互影响,也不需要重建索引。
那么 ES 是如何构建父子树呢?
ES 通过doc_value
维护了一套父子关系,类似如下数据组织方式:
这一套数据之前提及过,是在内存里维护的(当然数据超出内存大小后是可以做分页的),而这就意味着:
父节点和其所有子节点必须在同一个 shard 之中!
四、时间序列数据库(TSDB)
从上面的参数我们引出这么一个问题,如何利用 ES 做 TSDB(时序数据库)?
首先我们需要知道 TSDB 需要什么不需要什么,原则来说,TSDB 是以时间作为索引的,其数据结构越简单越好,层次最好就一层,字段固定且单一,大致包含了<id,timestamp,metric>
这几个参数,对硬盘的占用也需要做优化,系统高可用性比强一致性要重要一些,对 PB 级别的数据的处理有极低的延迟等等。
系统中每一条记录代表某个时间节点下某样事物的指标,可以是业务指标,也可以是机器/服务性能指标。指标本身数据简单,但是很可能每隔一秒或一分钟就要收集一次指标,从长远来看将会产生十分庞大的数据,并且还要对这样庞大的数据做聚合查询,统计分析等等。
根据这个思路,我们可以对 ES 的 schema 做出如下优化,使之成为一个合格的 TSDB:
- 禁用
_source
&_all
,TSDB 并不需要存储指标原文,更不需要对指标本身做全文检索; - 启用
doc_value
字段,提供聚合查询的支持,后续会做详细介绍; - 字段类型能用 float 就不用 double;
- 长期来看,为了合理的优化存储,我们可以周期性执行
/_forcemerge
方法合并碎片,优化存储空间。
学习文章
https://leonlibraries.github.io/2017/04/27/ElasticSearch%E5%86%85%E9%83%A8%E6%9C%BA%E5%88%B6%E6%B5%85%E6%9E%90%E4%B8%89/
以上是 ElasticSearch深入:内部机制浅析(三)@ 的全部内容, 来源链接: utcz.com/z/512982.html