前言
本次主要目的是科普下Elasticsearch(以下简称:ES),同时分享下目前自己使用到的ES的情况。
章一:什么是ES
简介:
Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值。Elasticsearch 的实现原理主要分为以下几个步骤,首先用户将数据提交到Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。
Elasticsearch是与名为Logstash的数据收集和日志解析引擎以及名为Kibana的分析和可视化平台一起开发的。这三个产品被设计成一个集成解决方案,称为"Elastic Stack"(以前称为"ELK stack")。
Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。Elasticsearch是分布式的,这意味着索引可以被分成分片,每个分片可以有0个或多个副本。每个节点托管一个或多个分片,并充当协调器将操作委托给正确的分片。再平衡和路由是自动完成的。相关数据通常存储在同一个索引中,该索引由一个或多个主分片和零个或多个复制分片组成。一旦创建了索引,就不能更改主分片的数量。
Elasticsearch使用Lucene,并试图通过JSON和Java API提供其所有特性。它支持facetting和percolating,如果新文档与注册查询匹配,这对于通知非常有用。另一个特性称为"网关",处理索引的长期持久性;例如,在服务器崩溃的情况下,可以从网关恢复索引。Elasticsearch支持实时GET请求,适合作为NoSQL数据存储,但缺少分布式事务。
ES整体架构:
结构:
- 为方便理解,可以对照下ES和MySQL的关系:
ES数据架构的主要概念与关系数据库Mysql对比表
Type的概念,是适用于早期的ES版本,后续的ES版本,未使用到此概念。
章二:ES中的核心概念
集群
一个集群,集群中有多个节点,其中有一个为主节点,这个主节点是可以通过选举产生的,主从节点是对于集群内部来说的。es的一个概念就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看es集群,在逻辑上是个整体,你与任何一个节点的通信和与整个es集群通信是等价的。
节点
Elasticsearch集群中服务分多个角色
Master节点:
负责集群内调度决策,集群状态、节点信息、索引映射、分片信息、路由信息,Master真正主节点是通过选举诞生的,一般一个集群内至少要有三个Master可竞选成员,防止主节点损坏。
Data存储节点:
用于存储数据及计算,分片的主从副本,热数据节点,冷数据节点;
Client协调节点:
协调多个副本数据查询服务,聚合各个副本的返回结果,返回给客户端;
Elasticsearch的两次查询:
- 多节点及多分片能够提高系统的写性能,但是这会让数据分散在多个Data节点当中,Elasticsearch并不知道我们要找的文档,到底保存在哪个分片的哪个segment文件中。
- 所以,为了均衡各个数据节点的性能压力,Elasticsearch每次查询都是请求 所有 索引所在的Data节点,查询请求时协调节点会在相同数据分片多个副本中,随机选出一个节点发送查询请求,从而实现负载均衡。
- 而收到请求的副本会根据关键词权重对结果先进行一次排序,当协调节点拿到所有副本返回的文档ID列表后,会再次对结果汇总排序,最后才会用 DocId去各个副本Fetch具体的文档数据将结果返回。
- 可以说,Elasticsearch通过这个方式实现了所有分片的大数据集的全文检索,但这种方式也同时加大了Elasticsearch对数据查询请求的耗时。
Kibana计算节点:
作用是实时统计分析、聚合分析统计数据、图形聚合展示。
倒排索引
倒排索引是区别于正排索引的概念:
正排索引:是以文档对象的唯一 ID 作为索引,以文档内容作为记录。
倒排索引:Inverted index,指的是将文档内容中的单词作为索引,将包含该词的文档 ID 作为记录。
举例:
倒排索引-组成
单词词典(Term Dictionary)
单词词典的实现一般用B+树,B+树构造的可视化过程网址:B+ Tree Visualization
倒排列表(Posting List)
倒排列表记录了单词对应的文档集合,有倒排索引项(Posting)组成
倒排索引项主要包含如下信息:
- 文档id用于获取原始信息
- 单词频率(TF,Term Frequency),记录该单词在该文档中出现的次数,用于后续相关性算分
- 位置(Posting),记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query)
- 偏移(Offset),记录单词在文档的开始和结束位置,用于高亮显示
倒排索引不可变的好处
- 不需要锁,提升并发能力,避免锁的问题
- 数据不变,一直保存在os cache中,只要cache内存足够
- filter cache一直驻留在内存,因为数据不变
- 可以压缩,节省cpu和io开销
分片
代表索引分片,es可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上。构成分布式搜索。分片的数量只能在索引创建前指定,并且索引创建后不能更改。
副本
代表索引副本,es可以设置多个索引的副本,副本的作用一是提高系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复。二是提高es的查询效率,es会自动对搜索请求进行负载均衡。
分词
分词是将文本转换成一系列单词(Term or Token)的过程,也可以叫文本分析,在ES里面称为Analysis
分词器
分词器是ES中专门处理分词的组件,英文为Analyzer,它的组成如下:
- Character Filters:针对原始文本进行处理,比如去除html标签
- Tokenizer:将原始文本按照一定规则切分为单词
- Token Filters:针对Tokenizer处理的单词进行再加工,比如转小写、删除或增新等处理
分词器调用顺序
预定义的分词器
Standard Analyzer
默认分词器
按词切分,支持多语言
小写处理
Simple Analyzer
按照非字母切分
小写处理
Whitespace Analyzer
空白字符作为分隔符
Stop Analyzer
相比Simple Analyzer多了去除请用词处理
停用词指语气助词等修饰性词语,如the, an, 的, 这等
Keyword Analyzer
不分词,直接将输入作为一个单词输出
Pattern Analyzer
通过正则表达式自定义分隔符
默认是\W+,即非字词的符号作为分隔符
Language Analyzer 提供了30+种常见语言的分词器
自定义分词
Character Filters
在Tokenizer之前对原始文本进行处理,比如增加、删除或替换字符等
自带的如下:
1.HTML Strip Character Filter:去除HTML标签和转换HTML实体
2.Mapping Character Filter:进行字符替换操作
3.Pattern Replace Character Filter:进行正则匹配替换
会影响后续tokenizer解析的position和offset信息
Tokenizers
将原始文本按照一定规则切分为单词(term or token)
自带的如下:
1.standard 按照单词进行分割
2.letter 按照非字符类进行分割
3.whitespace 按照空格进行分割
4.UAX URL Email 按照standard进行分割,但不会分割邮箱和URL
5.Ngram 和 Edge NGram 连词分割
6.Path Hierarchy 按照文件路径进行分割
Token Filters
- 对于tokenizer输出的单词(term)进行增加、删除、修改等操作
- 自带的如下:
1.lowercase 将所有term转为小写
2.stop 删除停用词
3.Ngram 和 Edge NGram 连词分割
4.Synonym 添加近义词的term
自定义分词
自定义分词需要在索引配置中设定 char_filter、tokenizer、filter、analyzer等
分词使用说明
分词会在如下两个时机使用:
创建或更新文档时(Index Time),会对相应的文档进行分词处理
查询时(Search Time),会对查询语句进行分词
-
查询时通过analyzer指定分词器
-
通过index mapping设置search_analyzer实现
-
一般不需要特别指定查询时分词器,直接使用索引分词器即可,否则会出现无法匹配的情况
-
分词使用建议
- 明确字段是否需要分词,不需要分词的字段就将type设置为keyword,可以节省空间和提高写性能
- 善用_analyze API,查看文档的分词结果
recovery
代表数据恢复或叫数据重新分布,es在有节点加入或退出时会根据机器的负载对索引分片进行重新分配,挂掉的节点重新启动时也会进行数据恢复。
章三:ES的常规操作(具体的查询问题以及优化下一期展开)
官方文档:
www.elastic.co/guide/cn/el...(基于 Elasticsearch 2.x 版本,有些内容可能已经过时。)
索引写入流程图:
-
客户端选择一个node发送请求过去,这个node就是coordinating node (协调节点)
-
coordinating node,对document进行路由,将请求转发给对应的node
-
实际上的node上的primary shard处理请求,然后将数据同步到replica node
-
coordinating node,如果发现primary node和所有的replica node都搞定之后,就会返回请求到客户端
-
ES 使用了一个内存缓冲区 Buffer,先把要写入的数据放进 buffer;同时将数据写入 translog 日志文件(其实是些os cache)。
-
refresh:
- buffer数据满/1s定时器到期会将buffer写入操作系统segment file中,进入cache立马就能搜索到,所以说es是近实时(NRT,near real-time)的
-
flush:
-
tanslog超过指定大小/30min定时器到期会触发commit操作
- 将对应的cache刷到磁盘file,
- commit point写入磁盘,commit point里面包含对应的所有的segment file
-
-
translog 默认5s把cache fsync到磁盘,所以es宕机会有最大5s窗口的丢失数据
索引查询流程图:
读取数据
- 客户端发送任何一个请求到任意一个node,成为coordinate node
- coordinate node 对document进行路由,将请求rr轮训转发到对应的node,在primary shard 以及所有的replica中随机选择一个,让读请求负载均衡,
- 接受请求的node,返回document给coordinate note
- coordinate node返回给客户端
搜索过程
- 客户端发送一个请求给coordinate node
- 协调节点将搜索的请求转发给所有的shard对应的primary shard 或replica shard
- query phase:每一个shard 将自己搜索的结果(其实也就是一些唯一标识),返回给协调节点,有协调节点进行数据的合并,排序,分页等操作,产出最后的结果
- fetch phase ,接着由协调节点,根据唯一标识去各个节点进行拉去数据,最总返回给客户端
创建索引
json
PUT indexName1
{
"mappings" : {
"dynamic" : "strict",
"properties" : {
"advertiser_name" : {
"type" : "object",
"enabled" : false
},
"body" : {
"type" : "text"
},
"cdn_url" : {
"type" : "object",
"enabled" : false
},
"created_at" : {
"type" : "long"
},
"days" : {
"type" : "short"
},
"domain" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword"
}
},
},
"heat_degree" : {
"type" : "short"
},
"html_url" : {
"type" : "object",
"enabled" : false
},
"interaction" : {
"type" : "long"
},
"message" : {
"type" : "text",
},
"page_logo" : {
"type" : "object",
"enabled" : false
},
"sub_domain" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword"
}
},
}
}
},
"settings" : {
"index" : {
"refresh_interval" : "300s",
"indexing" : {
"slowlog" : {
"level" : "info",
"threshold" : {
"index" : {
"warn" : "200ms",
"trace" : "20ms",
"debug" : "50ms",
"info" : "100ms"
}
},
"source" : "1000"
}
},
"unassigned" : {
"node_left" : {
"delayed_timeout" : "5m"
}
},
"analysis" : {
"analyzer" : {
"zf_analyzer" : {
"filter" : [
"lowercase",
"stop",
"snowball"
],
"char_filter" : [
"my_mapping"
],
"type" : "custom",
"tokenizer" : "standard"
}
},
"char_filter" : {
"my_mapping" : {
"type" : "mapping",
"mappings" : [
".=> @",
":=> @"
]
}
}
},
"priority" : "100",
"number_of_replicas" : "1",
"routing" : {
"allocation" : {
"require" : {
"box_type" : "hot"
}
}
},
"search" : {
"slowlog" : {
"level" : "info",
"threshold" : {
"fetch" : {
"warn" : "200ms",
"trace" : "50ms",
"debug" : "80ms",
"info" : "100ms"
},
"query" : {
"warn" : "500ms",
"trace" : "50ms",
"debug" : "100ms",
"info" : "200ms"
}
}
}
},
"number_of_shards" : "12"
}
}
}
打开/关闭索引
bash
POST indexName1/_close
POST indexName1,indexName2/_close
POST indexName1/_open
POST indexName1,indexName2/_open
设置索引刷新时间
bash
PUT indexName1/_settings
{
"refresh_interval": "90s"
}
查询个数
bash
GET indexName1/_count
{
"query": {
"bool": {
"filter": {
"range": {
"sales": {
"gte": 1000
}
}
}
}
}
}
查询所有
bash
//查询所有
GET /indexName1/_doc/_search
{
"query":{
"match_all":{}
}
}
查询不区分大小写
bash
//新建索引,字段不区分大小写
PUT indexName1
{
"mappings": {
"dynamic": "strict",
"properties": {
"store_name": {
"type": "keyword",
"normalizer": "my_normalizer"
},
"store_id": {
"type": "keyword"
}
}
},
"settings": {
"index": {
"search": {
"slowlog": {
"level": "info",
"threshold": {
"fetch": {
"warn": "200ms",
"trace": "50ms",
"debug": "80ms",
"info": "100ms"
},
"query": {
"warn": "500ms",
"trace": "50ms",
"debug": "100ms",
"info": "200ms"
}
}
}
},
"refresh_interval": "10s",
"indexing": {
"slowlog": {
"level": "info",
"threshold": {
"index": {
"warn": "200ms",
"trace": "20ms",
"debug": "50ms",
"info": "100ms"
}
},
"source": "1000"
}
},
"analysis": {
"normalizer": {
"my_normalizer": {
"filter": "lowercase",
"type": "custom"
}
}
},
"number_of_shards": "1",
"unassigned": {
"node_left": {
"delayed_timeout": "5m"
}
},
"number_of_replicas": "0"
}
}
}
//插入数据
PUT /indexName1/_doc/1
{
"store_name":"HighSteel",
"store_id":"com.google.highsteel"
}
PUT /indexName1/_doc/2
{
"store_name":"KKKKKKOOOOO",
"store_id":"com.google.highsteel"
}
//查询所有
GET /indexName1/_doc/_search
{
"query":{
"match_all":{}
}
}
//模糊查询,不区分大小写
GET indexName1/_search
{
"query":{
"wildcard" : {
"store_name": "*KKkkkKOooOO*"
}
}
}
章四:ES的优化项
设置分片数
bash
#创建索引时指定,指定后,不能进行修改
"number_of_shards" : "12"
副本数
bash
#设置副本数
PUT /indexName1/_settings
{
"number_of_replicas": 1
}
内存
内存不用超过节点内存的一半,另外一半给Lucene使用。
冷热节点
bash
PUT indexName1/_settings
{
"index.routing.allocation.require.box_type": "warm"
}
索引模版,索引生命周期
-
索引生命周期管理策略:help.aliyun.com/document_de...
-
可以配置按大小或者按时间(天)来生成新的索引,索引名称,自动+1。旧索引自动切换为冷索引。新生成的索引为热索引。
- eg:index-01,index-02
查询索引信息
返回的统计信息和 节点统计 的输出很相似:search 、 fetch 、 get 、 index 、 bulk 、 segment counts 等等。
bash
GET indexName1/_stats
segment合并
ini
#查看集群上索引的删除条目的情况
GET _cat/indices/?v&s=docs.deleted:desc
#对某个索引,进行段合并
POST indexName1/_forcemerge?max_num_segments=1&only_expunge_deletes=true
#查看正在进行的forcemerge任务
GET _tasks?detailed=true&actions=*forcemerge&human
#取消任务
POST _tasks/J2FOnHkDSIGJ43AaTDniJQ:5929987436/_cancel
慢写入,慢查询
慢查询
ini
#查询条件
search_time_ms:[5000 TO *]
慢写入
ini
#查询条件
index_time_ms:[200 TO *]
章五:ES的注意事项
索引字段不能修改,只能新增
segment段合并,forcemerge,磁盘空间问题考虑。
fielddata占用内存空间问题
ini
indices.fielddata.memory_size_in_bytes
_cat/segments/indexname?v&h=shard,segment,size,size.memory
#fielddata具体字段占用
GET _cat/fielddata?v&s=size:desc
#fielddata具体索引占用
GET _cat/indices?v&h=index,fielddata.memory_size&s=fielddata.memory_size:desc
flush(sync_interval)
-
tanslog超过指定大小/30min定时器到期会触发commit操作
- 将对应的cache刷到磁盘file,
- commit point写入磁盘,commit point里面包含对应的所有的segment file
refresh(refresh_interval)
- buffer数据满/1s定时器到期会将buffer写入操作系统segment file中,进入cache立马就能搜索到,所以说es是近实时(NRT,near real-time)的
- 使用建议:对于实时性要求不高且想优化写入的业务场景,建议根据业务实际调大刷新频率。
修改索引非readonly
bash
PUT /indexName1/_settings
{
"index.blocks.read_only_allow_delete": null
}
热索引、冷索引(自己定义,未用索引模版+索引生命周期管理)
冷热切换索引
bash
PUT indexName1*/_settings
{
"index.routing.allocation.require.box_type": "warm"
}