目录
简介
架构原理
基本概念与MySQL的对比
分片机制
document路由原理
集群发现机制
shard&replica规则
避免脑裂
负载均衡
相关配置
容错过程与选举机制
扩容机制
容错机制
Lucene
结构原理
Lucene索引实现
DocValues
关于ES索引与检索分片
运行原理解析
倒排索引说明
检索倒排索引
分词器Analyzer
建立索引和类型
分片内文档写入流程场景
多个分片的文档写入场景
确定文档存储位置
同步副本
根据_id查询文档场景
根据字段值检索数据场景
调优
分片的数量
JVM设置
小技巧
开发说明
Elasticsearch是一个分布式、可扩展、实时的搜索与数据分析引擎。它不仅仅只是全文搜索,还支持结构化搜索、数据分析、复杂的语言处理、地理位置和对象间关联关系等。
ES的底层依赖Lucene。Lucene当下最先进、高性能、全功能的搜索引擎库。但是Lucene仅仅只是一个库,为了充分发挥其功能,需要使用Java并将Lucene直接集成到应用程序中。鉴于Lucene如此强大却难以上手的特点,诞生了ES。ES也是使用Java编写的,它的内部使用Lucene做索引与搜索,它的目的是隐藏Lucene的复杂性,取而代之的提供一套简单一致的RESTful API。
总体来说,ES具有如下特点:
ElasticSearch | MySQL |
Index | Database |
Type (6.0以上版本弃用) | Table |
Document | Row |
Field | Column |
Mapping | Schema |
Everything is indexed | Index |
Query DSL | SQL |
GET HTTP | SELECT |
PUT HTTP | UPDATE |
ES中存储数据的基本单位是索引,比如说ES中存储了一些订单系统的销售数据,就应该在ES中创建一个索引order_index,所有的销售数据都会写到这个索引里面去,一个索引就像一个数据库,而type就相当于库里的表,一个index里面可以有多个type,而mapping就相当于表的结构定义,定义了字段的类型等,往index的一个type里添加一行数据,这行数据就叫做一个document,每个document有多个filed,每一个filed就代表这个document的一个字段的值。
运行流程:
ES客户端将一份数据写入primary shard,然后将数据同步到replica shard中去。ES客户端取数据的时候就会在replica或primary的shard中去读。ES集群有多个节点,会自动选举一个节点为master节点,这个master节点干一些管理类的操作,比如维护元数据,负责切换primary shard和replica shard的身份,要是master节点宕机了,那么就会重新选举下一个节点为master为节点。如果primary shard所在的节点宕机了,那么就会由master节点将那个宕机的节点上的primary shard的身份转移到replica shard上,如果修复了宕机的那台机器,重启之后,master节点就会将缺失的replica shard分配过去,同步后续的修改工作,让集群恢复正常。
在建立索引时,会自动将数据拆分到多个分片(shard)中,默认数量是5,这个就是索引数据分片机制。
document要存储到Elasticsearch中,还要满足后续搜索的需求,路由到分片位置的算法肯定不能是随机的,要不然搜索就没法找了,路由的过程有一个公式:
shard = hash(routing) % number_of_primary_shards
routing值默认是document的ID值,也可以自行指定。先对routing信息求hash值,然后将hash结果对primary_shard的数量求模,比如说primary_shard是5,那么结果肯定落在[0,4]区间内,这个结果值就是该document的分片位置,如示意图所示:
这个求模公式间接的解释了为什么了索引创建时指定了primary shard的值,后续就不让改了,模数改了,之前路由的document再执行该公式时,值就可能跟改之前得到的值不一致,这样document就找不到了,如示意图所示:
在同一个网络环境下,只要启动一个Elasticsearch实例,并且cluster.name配置得一样,这个Elasticsearch实例就会自动加入到集群当中,这个是如何实现的?
这个依赖于Elasticsearch的自动发现机制Zen,在elasticsearch.yml配置文件中,有一行
discovery.zen.ping.unicast.hosts: ["192.168.17.137"]
表示单播发现方式,当该Elasticsearch实例启动时,会向192.168.17.137主机发送请求,并得到整个集群里所有节点的状态,然后去联系master节点,并加入集群。
摘抄了获取配置信息,注册discovery请求的部分源码如下:
org.elasticsearch.discovery.zen.ZenDiscovery启动时的构造器,会调用org.elasticsearch.discovery.zen.UnicastZenPing的构造器,其中UnicastZenPing的构造方式内会加载discovery.zen.ping.unicast.hosts配置项,并发送"internal:discovery/zen/unicast"请求(代码有删节):
public UnicastZenPing(Settings settings, ThreadPool threadPool, TransportService transportService,
UnicastHostsProvider unicastHostsProvider, PingContextProvider contextProvider) {
super(settings);
final int concurrentConnects = DISCOVERY_ZEN_PING_UNICAST_CONCURRENT_CONNECTS_SETTING.get(settings);
if (DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING.exists(settings)) {
configuredHosts = DISCOVERY_ZEN_PING_UNICAST_HOSTS_SETTING.get(settings);
// we only limit to 1 addresses, makes no sense to ping 100 ports
limitPortCounts = LIMIT_FOREIGN_PORTS_COUNT;
} else {
// if unicast hosts are not specified, fill with simple defaults on the local machine
configuredHosts = transportService.getLocalAddresses();
limitPortCounts = LIMIT_LOCAL_PORTS_COUNT;
}
resolveTimeout = DISCOVERY_ZEN_PING_UNICAST_HOSTS_RESOLVE_TIMEOUT.get(settings);
transportService.registerRequestHandler(ACTION_NAME, ThreadPool.Names.SAME, UnicastPingRequest::new,
new UnicastPingRequestHandler());
}
一个index的数据,是拆分存储在多个shard当中,我们可以在Elasticsearch的数据目录里查看一下索引的存储结构(Elasticsearch服务器上导出的树状目录结构):
.
└── nodes
└── 0
├── indices
│ ├── 48G_CgE7TiWomlYsyQW1NQ #索引location的UUID
│ │ ├── 0 #primary shard,从0-4共5个
│ │ │ ├── index
│ │ │ │ ├── segments_3
│ │ │ │ └── write.lock
│ │ │ ├── _state
│ │ │ │ └── state-2.st
│ │ │ └── translog
│ │ │ ├── translog-2.ckp
│ │ │ ├── translog-2.tlog
│ │ │ ├── translog-3.ckp
│ │ │ ├── translog-3.tlog
│ │ │ ├── translog-4.tlog
│ │ │ └── translog.ckp
│ │ ├── 1
│ │ │ ├── index
│ │ │ │ ├── segments_3
│ │ │ │ └── write.lock
│ │ │ ├── _state
│ │ │ │ └── state-2.st
│ │ │ └── translog
│ │ │ ├── translog-2.ckp
│ │ │ ├── translog-2.tlog
│ │ │ ├── translog-3.ckp
│ │ │ ├── translog-3.tlog
│ │ │ ├── translog-4.tlog
│ │ │ └── translog.ckp
│ │ ├── 2
│ │ │ ├── index
│ │ │ │ ├── _1.cfe
│ │ │ │ ├── _1.cfs
│ │ │ │ ├── _1.si
│ │ │ │ ├── segments_7
│ │ │ │ └── write.lock
│ │ │ ├── _state
│ │ │ │ └── state-2.st
│ │ │ └── translog
│ │ │ ├── translog-4.ckp
│ │ │ ├── translog-4.tlog
│ │ │ ├── translog-5.ckp
│ │ │ ├── translog-5.tlog
│ │ │ ├── translog-6.tlog
│ │ │ └── translog.ckp
│ │ ├── 3
│ │ │ ├── index
│ │ │ │ ├── _1.cfe
│ │ │ │ ├── _1.cfs
│ │ │ │ ├── _1.si
│ │ │ │ ├── segments_7
│ │ │ │ └── write.lock
│ │ │ ├── _state
│ │ │ │ └── state-2.st
│ │ │ └── translog
│ │ │ ├── translog-4.ckp
│ │ │ ├── translog-4.tlog
│ │ │ ├── translog-5.ckp
│ │ │ ├── translog-5.tlog
│ │ │ ├── translog-6.tlog
│ │ │ └── translog.ckp
│ │ ├── 4
│ │ │ ├── index
│ │ │ │ ├── _0.cfe
│ │ │ │ ├── _0.cfs
│ │ │ │ ├── _0.si
│ │ │ │ ├── segments_5
│ │ │ │ └── write.lock
│ │ │ ├── _state
│ │ │ │ └── state-2.st
│ │ │ └── translog
│ │ │ ├── translog-3.ckp
│ │ │ ├── translog-3.tlog
│ │ │ ├── translog-4.ckp
│ │ │ ├── translog-4.tlog
│ │ │ ├── translog-5.tlog
│ │ │ └── translog.ckp
│ │ └── _state
│ │ └── state-16.st
├── node.lock
└── _state
├── global-88.st
└── node-22.st
如上目录结构所示,展示了location索引(UUID为48G_CgE7TiWomlYsyQW1NQ)的存储信息,共5个primary shard,编号从0-4。
primary shard与replica shard,还有其他几点特性:
脑裂问题是采用master-slave模式的分布式集群普遍需要关注的问题,脑裂一旦出现,会导致集群的状态出现不一致,导致数据错误甚至丢失。
ES避免脑裂的策略:过半原则,可以在ES的集群配置中添加一下配置,避免脑裂的发生
#一个节点多久ping一次,默认1s
discovery.zen.fd.ping_interval: 1s
##等待ping返回时间,默认30s
discovery.zen.fd.ping_timeout: 10s
##ping超时重试次数,默认3次
discovery.zen.fd.ping_retries: 3
##选举时需要的节点连接数,N为具有master资格的节点数量
discovery.zen.minimum_master_nodes=N/2+1
注意问题
ES集群是分布式的,数据分布到集群的不同机器上,对于ES中的一个索引来说,ES通过分片的方式实现数据的分布式和负载均衡。创建索引的时候,需要指定分片的数量,分片会均匀的分布到集群的机器中。分片的数量是需要创建索引的时候就需要设置的,而且设置之后不能更改,虽然ES提供了相应的api来缩减和扩增分片,但是代价是很高的,需要重建整个索引。
考虑到并发响应以及后续扩展节点的能力,分片的数量不能太少,假如你只有一个分片,随着索引数据量的增大,后续进行了节点的扩充,但是由于一个分片只能分布在一台机器上,所以集群扩容对于该索引来说没有意义了。
但是分片数量也不能太多,每个分片都相当于一个独立的lucene引擎,太多的分片意味着集群中需要管理的元数据信息增多,master节点有可能成为瓶颈;同时集群中的小文件会增多,内存以及文件句柄的占用量会增大,查询速度也会变慢。
5.0之前通过consistency来设置
consistency参数的值可以设为 :
5.0之后通过wait_for_active_shards参数设置
Elasticsearch集群中,所有的node都是对等的角色,所有的node都能接收请求,并且能自动转请求到相应的节点上(数据路由),最后能将其他节点处理的数据进行响应收集,返回给客户端。在集群中,也存在一个master节点,它的职责多一些,需要管理与维护集群的元数据,索引的创建与删除和节点的增加和删除,它都会收到相应的请求,然后进行相应的数据维护。master node在承担索引、搜索请求时,与其他node一起分摊,并不承担所有的请求,因而不存在单点故障这个问题。
我们假设一下集群有3台node,其中node-1宕机的过程,如果node-1是master node,关键步骤如下:
扩容分为垂直扩容和水平扩容两种,垂直扩容指增加单台服务器的CPU、内存大小,磁盘容量,简单来讲就是换更强大的服务器;水平扩容就是增加机器数量,通过集群化部署与分布式的技术手段,也能构建出强大的计算和存储能力。
二者简单对比:
Elastisearch非常适合用水平扩容方案,能胜任上百个节点,支撑PB级别的数据规模,并且扩容操作后,每增加新的节点会触发索引分片的重新分配。
举个例子,假定Elasticsearch有2个节点,primary shard设置为3,replica shard设置为1,这样1个索引就有3个primary shard,3个replica shard,P表示primary shard,R表示replica shard,分布示例图如下:
当新加入一个node-3时,触发node-1和node-2的shard进行重新分配,假定P0和R1两个shard移到node-3当中,如图所示:
重分配完成后,此时集群的示例如下:
最后补充两点:
单node环境下的容错
假定Elasticsearch集群只有一个node,primary shard设置为3,replica shard设置为1,这样1个索引就应该有3个primary shard,3个replica shard,但primary shard不能与其replica shard放在一个node里,导致replica shard无法分配,这样集群的status为yellow,示例图如下:
集群可以正常工作,一旦出现node宕机,数据全部丢失,并且集群不可用。
结论:单node环境容错性为0.
2台node环境下的容错
primary shard与replica shard的设置与上文相同,此时Elasticsearch集群只有2个node,shard分布如下图所示:
如果其中一台宕机,如node-2宕机,如图所示:
此时node-1节点的R2(replica shard)会升为P2(primary shard),此时集群还能正常用,数据未丢失。
结论:双node环境容错性为1。
3台node环境下的容错
我们先按primary shard为3,replica shard为1进行容错性计算。
此时每台node存放2个shard,如果一台宕机,此时另外2台肯定还有完整的数据,如果两台宕机,剩下的那台就只有2/3的数据,数据丢失1/3,容错性为1台。
如果是这样设置,那3台的容错性和2台的容错性一样,就存在资源浪费的情况。
那怎么样提升容错性呢?
把replica shard的值改成2,这样每台node存放3个shard,如下图所示:
如果有2台宕机,就剩下node-2,此时集群的数据还是完整的,replica会升成primary shard继续提供服务,如下图所示:
结论:3台node环境容错性最大可以是2。
ES依赖一个重要的组件Lucene,关于数据结构的优化通常来说是对Lucene的优化,它是集群的一个存储于检索工作单元。
在Lucene中,分为索引(录入)与检索(查询)两部分,索引部分包含分词器、过滤器、字符映射器等,检索部分包含查询解析器等。
一个Lucene索引包含多个segments,一个segment包含多个文档,每个文档包含多个字段,每个字段经过分词后形成一个或多个term。
通过Luke工具查看ES的lucene文件如下,主要增加了_id和_source字段:
Lucene 索引文件结构主要的分为:词典、倒排表、正向文件、DocValues等,如下图:
Lucene随机三次磁盘读取比较耗时。其中.fdt文件保存数据值损耗空间大,.tim和.doc则需要SSD存储提高随机读写性能。另外一个比较消耗性能的是打分流程,不需要则可屏蔽。
倒排索引解决从词快速检索到相应文档ID, 但如果需要对结果进行排序、分组、聚合等操作的时候则需要根据文档ID快速找到对应的值。
通过倒排索引代价缺很高:需迭代索引里的每个词项并收集文档的列里面 token。这很慢而且难以扩展:随着词项和文档的数量增加,执行时间也会增加。Solr docs对此的解释如下:
在lucene 4.0版本前通过FieldCache,原理是通过按列逆转倒排表将(field value ->doc)映射变成(doc -> field value)映射,问题为逐步构建时间长并且消耗大量内存,容易造成OOM。
DocValues是一种列存储结构,能快速通过文档ID找到相关需要排序的字段。在ES中,默认开启所有(除了标记需analyzed的字符串字段)字段的doc values,如果不需要对此字段做任何排序等工作,则可关闭以减少资源消耗。
ES中一个索引由一个或多个lucene索引构成,一个lucene索引由一个或多个segment构成,其中segment是最小的检索域。
数据具体被存储到哪个分片上:shard = hash(routing) % number_of_primary_shards
默认情况下 routing参数是文档ID (murmurhash3),可通过 URL中的 _routing 参数指定数据分布在同一个分片中,index和search的时候都需要一致才能找到数据,如果能明确根据_routing进行数据分区,则可减少分片的检索工作,以提高性能。
参考:https://segmentfault.com/a/1190000020022504
先对检索内容进行分词(适用于match查询方法,term/terms查询不分词),然后在倒排索引中寻找匹配的token,最后返回token对应的文档以及根据文档中token匹配情况给出的评分score。
在Elasticsearch中可通过内置分词器实现分词,也可以按需定制分词器。
Analyzer由三部分组成:
利用ElasticSearch提供的webAPI可以查看索引中某一字段field对文本text的分词策略:
POST /[index]/_analyze
{
"field": [field],
"text": [text]
}
计算方式:
shard = hash(routing) % number_of_primary_shards
每个文档都有一个routing参数,默认情况下就使用其 _id 值。将其 _id 值计算哈希后,对索引的主分片数取余,就得到了文档实际应该存储到的分片。因此索引的主分片数不可以随意修改,一旦主分片数改变,所有文档的存储位置计算结果都会发生改变,索引数据就完全不可读了。
注:图中P代表主分片(Primary Shard),R代表副本(Replica Shard)。
GET /[index]/_doc/[_id]
GET /[index]/_search?q=[field]: [value]
上述流程和根据_id查询文档相比,只是多了一个从倒排索引中根据字段值寻找文档_id的过程,其中的4~6步与其完全相同。
可参考:https://www.amd5.cn/atang_4784.html
通过_cat api获取任务执行情况
GET http://localhost:9201/_cat/thread_pool?v&h=host,search.active,search.rejected,search.completed
http://www.ibm.com/developerworks/library/j-use-elasticsearch-java-apps/index.html
http://blog.csdn.net/laoyang360/article/details/51931981
https://discuss.elastic.co/
http://elasticsearch.cn/
参考文献
https://segmentfault.com/a/1190000020022504
https://www.amd5.cn/atang_4784.html
https://segmentfault.com/a/1190000021091902
https://qimok.cn/1360.html