执行分布式检索
一个查询操作,在ES分布式环境中分为两步:查询与合并
查询阶段
ES集群向所有分片传递查询语句。
分片接收到请求后,执行搜索并建立一个长度为top-n的优先队列,存储结果。
top-n 的大小取决于分页参数 top-n=from+size
步骤
- 客户端发送查询请求到节点A,节点A创建分片数*(from+size)长度的优先队列(空的)。
- 此处ES对top-n的长度最大是10000(可配置)
- 节点A将查询请求并行发送到所有主分片和副分片。
- 各分片本地执行检索,并将结果存到各自本地的top-n优先队列
- 各分片将优先队列返回给A节点(仅id和排名(_score))
- 节点A合并各节点返回的内容,产生一个全局排序的结果存到自身优先队列中。
取回阶段
步骤
- 协调节点判断哪些需要返回,哪些需要丢弃。form的丢弃,size的返回。
- 协调节点根据个分片返回的id,向个分片发起get请求。
- 各分片接收到get请求,检索完整文档,返回给协调节点。
- 协调节点判断所有文档均返回后,向客户端返回结果。
深分页
协调节点创建的优先队列,长度是有限制的。即便长度无限,处理他们也是非常耗时的。所以不建议提供过深的分页功能。
搜索选项
有几个影响性能的参数
preference
(偏好)
控制分片是否参与检索_primary
_primary_first
_local
_only_node:xyz
_prefer_node:xyz
_shards:2,3
Bouncing Results
当使用类似入库时间这样的字段检索时,由于主分片和副本分片本身存在入库时间差异,所以导致多次查询结果集变化的情况。可以使用preference处理。
超时问题
处理过程可能超时
路由
在取回阶段,协调节点根据id获取文档,此时会走获取流程,会根据id计算所属分片,快速提取。
搜索类型
缺省类型是query_then_fetch(包含预查阶段)
游标查询
ES为解决深分页的性能问题,提供了游标查询机制。
游标分解
- 初始化阶段:按照检索条件进行数据检索,并将结果缓存起来。
- 遍历阶段:从缓存中提取数据
特点:初始化阶段后任何操作不会对结果产生影响,因为读的是缓存数据
索引管理
创建一个索引
语法格式
PUT /my_index
{
"settings": { ... any settings ... },
"mappings": {
"type_one": { ... any mappings ... },
"type_two": { ... any mappings ... },
...
}
}
禁止自动创建索引
config/elasticsearch.yml
action.auto_create_index: false
删除一个索引
语法格式
DELETE /my_index
DELETE /index_one,index_two
DELETE /index_*
DELETE /_all
DELETE /*
避免误删除,必须指定删除,拒绝模糊范围删除,避免误删除
elasticsearch.yml
action.destructive_requires_name: true
索引设置
ES提倡使用默认设置,不要瞎改
number_of_shards
主分片数,默认5。创建后不可变更。number_of_replicas
副本分片数,默认1。创建后可变更。
配置分析器
ES支持配置已有的分析器,或者为你的索引设置创建自定义分析器。
默认全文分析器
standard分析器是用于全文检索的默认分析器,适用于大部分西方语言。特点如下:
- standard分词器: 通过单词边界分割输入文本。
- standard语汇单元过滤器: 语汇单元过滤器,目的是整理分词器触发的语汇单元(目前什么也没做)
- lowercase语汇单元过滤器: 转换所有的语汇单元为小写
- stop 语汇单元过滤器: 删除停用词-对搜索相关性影响不大的常用词,如a,the,and等等
自定义分析器
可以通过在一个适合你的特定数据的设置之中组合字符过滤器、分词器、词汇单元过滤器来创建自定义的分析器。
一个分析器就是组合了三种函数的一个包装器, 三种函数按照顺序被执行:
- 字符过滤器
- 数量 0 ~ N 个
- 用于整理尚未被分词的字符串,调整内容。比如去除HTML语法,转码等
- 分词器
- 用于将字符串分解成单个词条或者词汇单元。
- 数量 1 唯一
- 词单元过滤器
- 经过分词,作为结果的 词单元流 会按照指定的顺序通过指定的词单元过滤器 。
- 词单元过滤器可以修改、添加或者移除词单元。
- 数量 0 ~ N 个
创建自定义分析器语法
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": { ... custom character filters ... },
"tokenizer": { ... custom tokenizers ... },
"filter": { ... custom token filters ... },
"analyzer": { ... custom analyzers ... }
}
}
}
实例
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": [ "&=> and "]
}},
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": [ "the", "a" ]
}},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": [ "html_strip", "&_to_and" ],
"tokenizer": "standard",
"filter": [ "lowercase", "my_stopwords" ]
}}
}}}
类型和映射(大概意思)
在ES中同一索引下的不同类型通过_type进行区分和使用。但是在Lucene中并没有类型的概念。Lucene中将同一索引下全部类型的映射信息进行无差别的同级存储。
如果不同类型下相同映射采用不同的分析器,ES会报错。
所以新版本中的ES不再支持多类型模式,而是单类型模式的原因
根对象
动态映射
当ES遇到文档中以前未遇到过的字段,会动态的进行映射。该功能可通过dynamic进行配置。
- true 动态添加新的字段映射
- false 忽略新的字段
- strict 遇到新的字段报错
PUT /my_index
{
"mappings": {
"my_type": {
"dynamic": "strict",
"properties": {
"title": { "type": "string"},
"stash": {
"type": "object",
"dynamic": true
}
}
}
}
}
自定义动态映射
日期检测
当新字段内容为‘2020-10-10’时,动态映射成为date类型,此时再次传递字段内容为‘1234’时,将报错。可以通过关闭日期检测避免问题。
关闭日期检测
PUT /my_index
{
"mappings": {
"my_type": {
"date_detection": false
}
}
}
动态模板
通过指定动态模板,实现有规则的进行动态映射。比如通过字段名后缀等等。
缺省映射
通过缺省映射为所有映射进行统一配置,如有不同可在具体映射内更改配置。
PUT /my_index
{
"mappings": {
"_default_": {
"_all": { "enabled": false }
},
"blog": {
"_all": { "enabled": true }
}
}
}
重新索引你的数据
重新索引:用新的设置创建新的索引并把文档从旧的索引复制到新的索引。
GET /old_index/_search?scroll=1m
{
"query": {
"range": {
"date": {
"gte": "2014-01-01",
"lt": "2014-02-01"
}
}
},
"sort": ["_doc"],
"size": 1000
}
索引别名和零停机
别名
别名支持一对多关系
创建别名
PUT /my_index_v1/_alias/my_index
删除别名
查看别名指向索引
GET /*/_alias/my_index
查看索引关联别名
GET /my_index_v1/_alias/*
零停机
在不停机的情况下,删除原始别名,建立新别名
POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index_v1", "alias": "my_index" }},
{ "add": { "index": "my_index_v2", "alias": "my_index" }}
]
}
该操作属于原子操作
分片内部原理
段和提交点的概念
一个索引关联多个段索引(每个段都是倒排索引),并由提交点文件进行记录关联关系。
文档的存储流程A
- 新文档被收集到内存索引缓存
- 定时,缓存到被提交
- 一个新的段-》一个追加的倒排索-》被写入磁盘
- 一个新的包含新段名字的 提交点 被写入磁盘
- 磁盘进行同步:所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。
- 文档保存结束,可以被检索(可检索延迟高,后续流程B将解决)
不变性
倒排索引被写入磁盘后是 不可改变 的,不变性的价值
- 不需要锁。因为没有变更,所以不需要锁来预防并发改的问题。
- 方便缓存。因为没有变更,只要缓存有空间,便可一直缓存。不需要考虑缓存一致性问题。
- 其他缓存(Filter)。在检索过程中产生的缓存,可以重复使用。
- 数据压缩。减少磁盘I/O,节省磁盘空间,节省缓存空间。
动态更新索引
既然索引是不可改变的,又如何应对更新、删除操作?答案是用更多的索引。
当删除一个索引时,其实是状态删除,检索过程中进行过滤。
当更新一个索引时,其实是状态删除和新增索引。检索过程中过滤掉删除的,保留新增的,
近实时索引
文档的存储流程B
- 新文档被收集(组成一个倒排索引段)到内存索引缓存
- 倒排索引段保存到系统文件缓存(此时已支持被检索,延迟低)
- 定时,缓存到被提交
- 一个新的段-》一个追加的倒排索-》被写入磁盘
- 一个新的包含新段名字的 提交点 被写入磁盘
- 磁盘进行同步:所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。
- 文档保存结束,可以被检索(可检索延迟高,后续流程B将解决)
refresh API
ES对一个新段的操作是需要关联的,不能上来直接用。系统采用定时执行的方式实现自动关联。
手动触发
POST /_refresh
POST /blogs/_refresh
设置定时频率
PUT /my_logs
{
"settings": {
"refresh_interval": "30s"
}
}
设置定时开与关
在做大索引迁移过程中,建议先关闭,迁移结束后再开启。
PUT /my_logs/_settings
{ "refresh_interval": -1 }
PUT /my_logs/_settings
{ "refresh_interval": "1s" }
如果refresh_interval=1 表示1毫秒,每毫秒做一次关联,会把ES搞瘫痪
持久化变更
translog 事务日志
ES通过translog机制确保数据安全性
translog记录点
- 新文档被收集(组成一个倒排索引段)到内存索引缓存
- 记录本次操作到translog中。
- 倒排索引段保存到系统文件缓存(此时已支持被检索,延迟低)
- 定时,缓存到被提交(全量提交)
- 一个新的段-》一个追加的倒排索-》被写入磁盘
- 一个新的包含新段名字的 提交点 被写入磁盘
- 磁盘进行同步:所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。
- 文档保存结束,可以被检索(可检索延迟高,后续流程B将解决)
translog处理过程-运行中
每隔一段时间或者translog过大。索引将被刷新(flush);一个新的translog被创建,并且一个全量提交被执行。
- 所有在内存缓冲区的文档都被写入一个新的段。
- 缓冲区被清空
- 一个提交点被写入磁盘
- 文件系统缓存通过fsync被刷新(flush)
- 老的translog被删除
translog处理过程-启动
当ES关闭时,translog中还有剩余数据。在ES启动过程中需要被刷新(或者说启动时需要一个新的translog,才把旧translog刷新,而不是继续使用)。
- 使用提交点记录关联索引与段的关联关系
- 此时不包含未fulsh段。
- 此时内存缓存区是空的。
- 此时translog是有旧数据的。
- 重放translog
重放数据到内存缓存区
建立新段 - 全量提交
- 内存缓存区清空
- translog清空
flush API
通常情况下,自动刷新就足够了。
手动提交
POST /blogs/_flush
POST /_flush?wait_for_ongoing
启动与关闭
PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
durability=async表示开启
sync_interval=5s表示间隔5秒同步到磁盘
translog有多安全
translog先记录在内存,每间隔5S同步到磁盘。
段合并
由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。
ES在后台自动进行段合并操作,在后台将多个相似的小段合并到大段。并且不会影响ES操作。
合并流程
- 新的段被刷新(flush)到了磁盘。 ** 写入一个包含新段且排除旧的和较小的段的新提交点。
- 新的段被打开用来搜索。
- 老的段被删除。
optimize API
API大可看做是 强制合并 API。它会将一个分片强制合并到 max_num_segments
参数指定大小的段数目。 这样做的意图是减少段的数量(通常减少到一个),来提升搜索性能。
POST /logstash-2014-10/_optimize?max_num_segments=1
文章中部分内容来源于:
Elasticsearch: The Definitive Guide by Clinton Gormley and Zachary Tong (O’Reilly). Copyright 2015 Elasticsearch BV, 978-1-449-35854-9。