目录
1、集群定义
2、节点类型
2.1 候选主节点 & 主节点
2.2 数据节点 & 协调节点
3、索引原理
3.1 写入数据流程/存储
4、Doc Values和倒排索引
4.1 存储
4.2 禁用DocValues
4.3 对比FieldData
5、深度分页
5.1 from+size
5.2 Scroll
5.3 search after
5.4 原理
6、Search Type
6.1 query then fetch
6.2 dfs_query_then_fetch
7. 性能优化
7.1 合并请求
7.2 优化分段segment
7.3 优化合并
7.4 利用缓存
7.5 预加载/预热器
1、集群定义
ES 集群其实是一个分布式系统,要满足高可用性,高可用就是当集群中有节点服务停止响应的时候,整个服务还能正常工作,也就是服务可用性;或者说整个集群中有部分节点丢失的情况下,不会有数据丢失,即数据可用性。
当用户的请求量越来越高,数据的增长越来越多的时候,系统需要把数据分散到其他节点上,最后来实现水平扩展。当集群中有节点出现问题的时候,整个集群的服务也不会受到影响。
ES 的分布架构当中,不同的集群是通过不同的名字来区分的,默认的名字为 elasticsearch,可以在配置文件中进行修改,或者在命令行中使用 -E cluster.name=wupx 进行设定,一个集群中可以有一个或者多个节点。
2、节点类型
2.1 候选主节点 & 主节点
每一个节点启动后,默认就是一个 Master-eligible 节点,可以通过在配置文件中设置 node.master: false 禁止,Master-eligible 节点可以参加选主流程,成为 Master 节点。当第一个节点启动时候,它会将自己选举成 Master 节点。
每个节点上都保存了集群的状态,只有 Master 节点才能修改集群的状态信息,如果是任意节点都能修改信息就会导致数据的不一致性。
集群状态(Cluster State),维护一个集群中必要的信息,主要包括如下信息:
所有的节点信息所有的索引和其相关的 Mapping 与 Setting 信息分片的路由信息
2.2 数据节点 & 协调节点
顾名思义,可以保存数据的节点叫作 Data Node,负责保存分片上存储的所有数据,当集群无法保存现有数据的时候,可以通过增加数据节点来解决存储上的问题,在数据扩展上有至关重要的作用。
Coordinating Node 负责接收 Client 的请求,将请求分发到合适的节点,最终把结果汇集到一起返回给客户端,每个节点默认都起到了 Coordinating Node 的职责。
3、索引原理
3.1 写入数据流程/存储
4、Doc Values和倒排索引
Es之所以搜索很快速就是归功于它的倒排索引设计,然后倒排索引也不是万能的,倒排索引的检索性非常快,但是在字段值排序时却不是理想的结构。
倒排索引结构:
如上,倒排索引只包含 单词对应的是否在哪个文档中,它并不包含文档中所有的值,如果查询后,在去每个获取以下文档内容,然后取值排序,这样很耗时。DocValues便由此产生
Doc Values
我们再存一份文本,文本内容文档的单词内容,DocValues通过转换索引和这份文本的关系来解决排序等问题。倒排索引将词项映射到包含他们的文档,DocValues 将文档映射到他们包含的词项:
当数据被转置之后,想要收集每个文档行,获取所有的词项就非常简单来,所以搜索使用倒排索引查找文档,聚合操作收集和聚合DocValues数据。
4.1 存储
DocValues是在索引时与倒排索引同时生成。DocValues和倒排索引一样,基于segement生成并且是不可变的。 同时它们一样会序列化到磁盘,我们可以充分利用操作系统的内存,而不是JVM的堆内存,当工作内存远小于系统的可用内存,系统会自动将DocValues保存在内存中,使其读写十分高速。 DocValues本质是一个序列化的列式存储,这种方式非常便于压缩,特别是数字类型。
4.2 禁用DocValues
DocValues默认是开启的,除了 analyzed strings。也就是数字,地理坐标,日期,ip和不分析的字段类型都会默认开启。
虽然DocValues非常好用,但是如何你存储的数据确实不需要这个特性,就不如禁用它,这样不仅节省磁盘空间,也会提升索引速度。
要禁用DocValues ,只需要在 字段映射(mapping)设置 doc_values:false,禁用后,这个字段将不能被用于聚合,排序,脚本等操作
4.3 对比FieldData
4.3 对比FieldData
doc_values比fielddata慢一点,大概10-25%(参考其他资料),但是具有更好的稳定性。另一方面,doc_values写入磁盘文件中,OS Cache先进行缓存,以提升访问doc value正排索引的性能,如果OS Cache内存大小不足够放得下整个正排索引,doc value,就会将doc value的数据写入磁盘文件中
5、深度分页
5.1 from+size
例如:
GET /goods/_search
{
"query":{
"match_all": {}
},
"from":5000,
"size":10
}
上看是查询goods索引,从第5000条起,取前十条。默认是按score排序的。这样就需要es在所有分片上匹配排序并得到5010条数据,协调节点拿到这些数据再进行二次排序取前十条。这样看起来就是做了很多无用功,浪费了一些资源。
而Es为了性能,也限制了我们分页的深度,Es目前支持最大的max_result_window=10000,也就是我们不能分页到10000条数据以上
5.2 Scroll
如果我们确实要请求大数据集下的数据,Es为我们提供了scroll Api。
使用scroll滚动搜索,可以先搜索一批数据,然后下次在搜索一批数据,以此类推,直到搜索出全部的数据。
scroll搜索会在第一次搜索的时候保存一个当时的视图快照,之后只会基于该旧的视图快照提供数据搜索,如果这个期间数据变更,是不会让用户看到的。每次发送scroll请求,我们还需要指定一个scroll参数,指定一个时间窗口,每次搜索请求只要在这个时间窗口内能完成就可以了。一个滚动搜索允许我们做一个初始阶段搜索并且持续批量从Es里拉取结果直到没有结果剩下因为是快照行为,所以在这段时间内查询的数据不保证一定是文档最新的操作数据
5.2.1 如何使用
以下是目前我索引内只有7条数据:
使用scroll取十条,可以看到 返回了一个scroll_id字段,这时已经生成了一个快照,我们可以拿这个id做后续请求
上面查询的是每页数据2,后续的scroll中根据此id请求每次返回的都是此次快照的后续2条数据
5.2.2 删除scroll
当scroll超时后,会自动删除搜索上下文,但是保持这个快照滚动操作也会耗费一些资源成本。因此当我们确定不再使用scroll应该主动删除
DELETe /_search/scroll
{
"scroll_id" : [
"DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==",
"DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAAABFmtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAAAxZ
rUllkUVlCa1NqNmRMaUhiQlZkMWFBAAAAAAAAAAIWa1JZZFFZQmtTajZkTGlIYkJWZDFhQQAAAAAAAAAFF
mtSWWRRWUJrU2o2ZExpSGJCVmQxYUEAAAAAAAAABBZrUllkUVlCa1NqNmRMaUhiQlZkMWFB"
]
}
#删除所有
DELETE /_search/scroll/_all
5.3 search after
from+size 存在深度分页问题。scroll存在数据不实时的问题。search after是Es5版本后引入的新 api。
search after分页需要依赖上一次分页的最后一条数据来确定当前分页的第一条数据,因此这个api分页的排序就相当重要了。
排序中最好要包含一个全局唯一的字段,这样在进行after搜索时会精准一些。
5.4 原理
5.4.1 from+size 基本搜索原理
这分为两个阶段:Query阶段 1)client发送一次搜索请求,node1接收到请求,node1创建一个大小 from+size的优先级队列用来存结果。node1是协调节点 2)node1将请求广播到涉及到的分片上,每个分片内部执行搜索请求,然后将结果存在内部的大小同样为from+size的优先级队列里,可以理解为一个topN的列表 3)每个分片把暂存在自身优先级队列里的数据返回给node1协调节点。node1 拿到 每个分片返回的结果后对结果进行一次合并,产生一个更大的优先级队列,存在自己的节点上 以上队列里存储的都是文档的唯一id doc_id,使尽量缩小返回值
Fetch阶段
上述流程已经重新确定了要返回的文档id
1)node1 节点发送GET请求到 每个分片
2)其他分片根据文档id获取到数据详情,然后返回给node1
3)节点1 组织数据返回给客户端
5.4.2 scroll原理
scroll就是把一次的查询结果缓存一定的时间,如scroll=1m则把查询结果缓存1分钟。 在下一次请求上来时,response比传统的返回多了一个scroll_id,下次带上这个scroll_id即可找回这个缓存的结果。 比from+size方式节省了 query阶段。所以它不是实时数据5.4.3 search after
search after其实最重要的就是排序字段,它也需要发送查询语句到各分片,但是它底层查询不用计算分页信息,只需要查询 search_after 字段后的数据取size个就可以。6、Search Type
ES使用的打分算法包含了称之为“TF-IDF”的统计信息来帮助计算处于那个索引中的文档的相关性。
TFIDF基本思想就是“一个项在文档中出现的次数越多,那么这个文档更加相关;但相关性会被这个项在整个文档库中的次数削弱”。
稀有项出现在相对少的文档中,那么任何查询匹配了一个稀有项的相关性就变得很高。相反,平常项到处都有,他们的相关性就低了。
当用户执行一个搜索时,ES面对一个有趣的困境。你的查询需要找到所有相关的文档,但是这些文档分布在你的cluster中的任何数目的shard中。
每个shard是一个Lucene的索引,保存了自身的TF和DF统计信息。一个shard只知道在其自身中出现的次数,而非整个cluster。
Es会用hash尽量确保文档的随机分布,使得本地的term frequency尽量接近真是值
6.1 query then fetch
默认情况Es会使用此搜索类型做查询操作
- 发送查询到每个shard找到所有匹配的文档,并使用本地的Term/document Frequency信息进行打分对结果构建一个优先队列(排序,标页等)返回关于结果的元数据到请求节点。注意,实际文档还没有发送,只是分数来自所有shard的分数合并起来,并在请求节点上进行排序,文档被按照查询要求进行选择最终,实际文档从他们各自所在的独立的shard上检索出来结果被返回给用户
6.2 dfs_query_then_fetch
当我们遇到打分偏离的情况下,可以使用此查询方式
- 预查询每个shard,询问Term和document frequency发送查询到每隔shard找到所有匹配的文档,并使用全局的Term/document Frequency信息进行打分对结果构建一个优先队列(排序,标页等)返回关于结果的元数据到请求节点。注意,实际文档还没有发送,只是分数来自所有shard的分数合并起来,并在请求节点上进行排序,文档被按照查询要求进行选择最终,实际文档从他们各自所在的独立的shard上检索出来结果被返回给用户
7. 性能优化
7.1 合并请求
为了获得更快的索引速度,需要做的一项优化就是通过bulk批量API,一次发送多篇文档进行索引。这个操作将节省网络来回的开销,并提升索引的吞吐量。一个单独的批量可以接受任何索引操作。
7.1.1 批量索引
bulk Api
文档: Bulk API | Elasticsearch Guide [6.8] | Elastic
格式:
POST {index}/{type}/_bulk
{ "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }
{ "field1" : "value1" }
第一行是 操作类型和参数
第二行是 操作的内容数据。
第一行存在的值包括:index,create,delete,update
注意的是两行必须是以换行符(n)标记
7.1.2 多条搜索
使用多条搜索和多条获取所带来的好处和批量相同:当我们需要从不同索引同时搜索时,将他们合并一起将会节省网络开销
格式
GET /_msearch header n body n header n body n
header
header用于指定index、type、search_type、preference、routing等内容,告诉ElasticSearch下一个body的查询位置.
{"index":"index","type":"type"}
和上面的批量操作一样,可以直接在url上指定index/type/_msearch,但是即使header指定了,内容体里的header部分也不能省略,需要用{}n占位。
body
body的格式和普通search查询相同
{"query":{"match":{"name":"123"}}}
7.1.3 多条获取
多条搜索是我们可以按照不同的查询条件去查询不同的文档,有些时候我们可能想批量获取某几个文档的全部内容。这个时候可以应用mget
基本语法:
### 基本操作 操作
GET /_mget
{
"docs" : [
{
"_index" : "search_demo_v1",
"_id" : "1"
},
{
"_index" : "search_demo_v2",
"_id" : "2"
}
]
}
#指定返回内容
GET /_mget
{
"docs" : [
{
"_index" : "search_demo_v1",
"_id" : "1",
"_source" : false
},
{
"_index" : "search_demo_v1",
"_id" : "5",
"_source" : ["field3", "field4"]
},
{
"_index" : "search_demo_v1",
"_id" : "3",
"_source" : {
"include": ["user"],
"exclude": ["user.location"]
}
}
]
}
#同一个文档,可以直接在url中指定index
GET /search_demo_v2/_mget
{
"docs" : [
{
"_id" : "1"
},
{
"_id" : "2"
}
]
}
7.2 优化分段segment
7.2.1 flush(冲刷)和refresh(刷新)
我们之前的图也介绍过了索引在写入的过程是如何存储和最终落盘的。refresh是将buffer里的数据刷入文件缓存,flush是从缓存落盘。
refresh操作会让Es重新打开索引,让新建的文档可用于搜索。从性能角度看,refresh和flush操作都比较耗资源。
7.2.1.1 何时refresh
默认的行为是每秒自动的刷新每份索引,我们可以设置,改变每份索引的刷新间隔。这个可以在运行时设置
PUT goods/_settings
{
"index.refresh_interval":"5s"
}
#其实就是在新建索引时的_setting里进行设置,
#
也可以设置为-1 表示彻底关闭自动刷新,只能客户端调用api进行手动刷新。因为有些索引只会定期变化,这样就非常有效了。
7.2.1.2 何时flush
触发flush三个条件:
内存缓冲区满了
缓冲区大小在 elasticsearch.yml中设置,indices.memory.index_buffer_size,这个值可以设置的是占用的jvm的内存多少
间隔了一定时间
translog达到了设置值
间隔时间和文件大小是具体到索引上的,可以在新建索引时进行设置
PUT goods/_settings
{
"index.translog":{
"flush_threshold_size":"500mb",
"flush_threshold_period":"30m"
}
}
7.3 优化合并
我们知道了分段越少,搜索就越快。而且,更新/删除 操作也实际只是在索引做了个删除标记,只有等分段合并时才会被真正的移除。所以将分段的总量保持在合理的范围是非常有意义的。
那么合并的时候是如何合并的,合并哪些文件是值得研究了。
7.3.1 合并策略
默认的合并策略是分层配置,该策略将分段划分多个层次,如果你的分段多余某一层设置的最大分段数,该层合并就会被触发。
合并发生在索引,更新,删除时,所以合并的越多,操作成本越大
下面有几个配置参数可以进行调节:
index.merge.policy.segments_per_tier
分层中每层可以存在的分段数,越大就意味着更少的合并以及更好的索引性能。如果索引不多,而你希望更好的搜索性能,这个值可以设置低一些
index.merge.policy.max_merge_at_once
每次最多合并的分段数,可以设置等同于segments_per_tier。
index.merge.policy.max_merge_segment
设置分段最大多大的值,比如 1gb,如果想获得较少的合并次数以及更快的索引速度,最好降低这个值,较大的分段更难以合并
index.merge.scheduler.max_thread_count
可以用于合并操作的最大线程数,根据自己的机器配置决定。
7.4 利用缓存
7.4.1 过滤器缓存
很多查询都有对等的过滤器。过滤器的结果会被缓存到在节点之上,默认大小是10%。
可以使用 indices.cache.filter.size调节。
过期时间
如果有些查询场景应用的比较少,就建议不要用缓存了。在容量一定情况下,每次搜索时,都要回收旧的缓存来进行新缓存的存储,这样是会消耗cpu计算资源的。或者也可以设置缓存的过期时间。
同样可以在_setting中进行设置, index.cache.filter.expire
7.5 预加载/预热器
预加载 fielddata | Elasticsearch: 权威指南 | Elastic
7.5.1 预加载
第一个工具称为 预加载 (与默认的 延迟加载相对)。随着新分段的创建(通过刷新、写入或合并等方式), 启动字段预加载可以使那些对搜索不可见的分段里的 fielddata 提前 加载。
这就意味着首次命中分段的查询不需要促发 fielddata 的加载,因为 fielddata 已经被载入到内存。避免了用户遇到搜索卡顿的情形。
预加载是按字段启用的,所以我们可以控制具体哪个字段可以预先加载:
PUT /music/_mapping/_song
{
"tags": {
"type": "string",
"fielddata": {
"loading" : "eager"
}
}
}
设置 fielddata.loading: eager 可以告诉 Elasticsearch 预先将此字段的内容载入内存中。
Fielddata 的载入可以使用 update-mapping API 对已有字段设置 lazy 或 eager 两种模式。
WARNING:
预加载只是简单的将载入 fielddata 的代价转移到索引刷新的时候,而不是查询时,从而大大提高了搜索体验。
体积大的索引段会比体积小的索引段需要更长的刷新时间。通常,体积大的索引段是由那些已经对查询可见的小分段合并而成的,所以较慢的刷新时间也不是很重要。
7.5.2 预热器
最后我们谈谈 索引预热器 。预热器早于 fielddata 预加载和全局序号预加载之前出现,它们仍然有其存在的理由。一个索引预热器允许我们指定一个查询和聚合须要在新分片对于搜索可见之前执行。 这个想法是通过预先填充或 预热缓存 让用户永远无法遇到延迟的波峰。
原来,预热器最重要的用法是确保 fielddata 被预先加载,因为这通常是最耗时的一步。现在可以通过前面讨论的那些技术来更好的控制它,但是预热器还是可以用来预建过滤器缓存,当然我们也还是能选择用它来预加载 fielddata。
让我们注册一个预热器然后解释发生了什么:
PUT /music/_warmer/warmer_1
{
"query" : {
"bool" : {
"filter" : {
"bool": {
"should": [
{ "term": { "tag": "rock" }},
{ "term": { "tag": "hiphop" }},
{ "term": { "tag": "electronics" }}
]
}
}
}
},
"aggs" : {
"price" : {
"histogram" : {
"field" : "price",
"interval" : 10
}
}
}
}
预热器被关联到索引( music )上,使用接入口 _warmer 以及 ID ( warmer_1 )。
为三种最受欢迎的曲风预建过滤器缓存。
字段 price 的 fielddata 和全局序号会被预加载。
预热器是根据具体索引注册的, 每个预热器都有唯一的 ID ,因为每个索引可能有多个预热器。
然后我们可以指定查询,任何查询。它可以包括查询、过滤器、聚合、排序值、脚本,任何有效的查询表达式都毫不夸张。 这里的目的是想注册那些可以代表用户产生流量压力的查询,从而将合适的内容载入缓存。
当新建一个分段时,Elasticsearch 将会执行注册在预热器中的查询。执行这些查询会强制加载缓存,只有在所有预热器执行完,这个分段才会对搜索可见。
与预加载类似,预热器只是将冷缓存的代价转移到刷新的时候。当注册预热器时,做出明智的决定十分重要。 为了确保每个缓存都被读入,我们 可以 加入上千的预热器,但这也会使新分段对于搜索可见的时间急剧上升。
实际中,我们会选择少量代表大多数用户的查询,然后注册它们。



