一、倒排索引
ElasticSearch引擎把文档数据写入到倒排索引(Inverted Index)的数据结构中,倒排索引建立的是分词(Term)和文档(Document)之间的映射关系,在倒排索引中,数据是面向词(Term)而不是面向文档的。
举个例子,文档和词条之间的关系如下图:
字段值被分析之后,存储在倒排索引中,倒排索引存储的是分词(Term)和文档(Doc)之间的关系,简化版的倒排索引如下图:
从图中可以看出,倒排索引有一个词条的列表,每个分词在列表中是唯一的,记录着词条出现的次数,以及包含词条的文档。实际上,ElasticSearch引擎创建的倒排索引比这个复杂得多。
倒排索引:倒排索引是实现“单词-文档矩阵”的一种具体存储形式,通过倒排索引,可以根据单词快速获取包含这个单词的文档列表。倒排索引主要由两个部分组成:“单词词典”和“倒排文件”。
单词词典:搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向“倒排列表”的指针。
倒排列表:倒排列表记载了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。
倒排文件:所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件即被称之为倒排文件,倒排文件是存储倒排索引的物理文件。
关于这些概念之间的关系,通过下图可以比较清晰的看出来。
单词词典是倒排索引中非常重要的组成部分,它用来维护文档集合中出现过的所有单词的相关信息,同时用来记载某个单词对应的倒排列表在倒排文件中的位置信息。在支持搜索时,根据用户的查询词,去单词词典里查询,就能够获得相应的倒排列表,并以此作为后续排序的基础。
对于一个规模很大的文档集合来说,可能包含几十万甚至上百万的不同单词,能否快速定位某个单词,这直接影响搜索时的响应速度,所以需要高效的数据结构来对单词词典进行构建和查找,常用的数据结构包括哈希加链表结构和B+树形词典结构。
二、写入性能分析
当数据被写入分片时,它会定期发布到磁盘上的不可变的 Lucene 分段中用于查询。随着分段数量的增长,这些分段会定期合并为更大的分段。 此过程称为合并。 由于所有分段都是不可变的,这意味着所使用的磁盘空间通常会在索引期间波动,因为需要在删除替换分段之前创建新的合并分段。 合并可能非常耗费资源,特别是在磁盘I / O方面。
分片的大小如何影响性能?
在Elasticsearch中,每个查询在每个分片的单个线程中执行。然而,可以并行处理多个分片,并可以在相同分片上执行多个查询和聚合。
【小分片的利弊】这意味着,在不涉及高速缓存时,最小查询延迟将取决于数据、查询的类型、分片的大小。查询大量小分片将使得每个分片的处理速度更快,但是随着更多的任务需要按顺序排队和处理,它不一定要比查询较小数量的更大的分片更快。如果有多个并发查询,则有很多小碎片也会降低查询吞吐量。
提示:从查询性能角度确定最大分片大小的最佳方法是使用逼真的数据和查询进行基准测试(真实数据而非模拟数据)。 始终使用查询和索引负载进行基准测试,代表节点在生产中需要处理的内容,因为单个查询的优化可能会产生误导性的结果。
注1:避免使用非常大的分片,因为这会对群集从故障中恢复的能力产生负面影响。 对分片的大小没有固定的限制,但是通常情况下很多场景限制在 50GB 的分片大小以内。
注2:当在ElasticSearch集群中配置好你的索引后, 你要明白在集群运行中你无法调整分片设置. 即便以后你发现需要调整分片数量, 你也只能新建创建并对数据进行重新索引(reindex)(虽然reindex会比较耗时, 但至少能保证你不会停机).
主分片的配置与硬盘分区很类似, 在对一块空的硬盘空间进行分区时, 会要求用户先进行数据备份, 然后配置新的分区, 最后把数据写到新的分区上。
注3:尽可能使用基于时间的索引来管理数据保留期。根据保留期(retention period,可以理解成有效期)将数据分组。基于时间的索引还可以轻松地随时间改变主分片和副本分片的数量(以为要生成的下一个索引进行更改)。这简化了适应不断变化的数据量和需求。
三、性能调优
1.关闭data结点的http功能
针对ElasticSearch集群中的所有数据节点,不用开启http服务。将其中的配置 参数这样设置:http.enabled: false,同时也不要安装head, bigdesk, marvel等监控 插件,这样保证data节点服务器只需处理创建/更新/删除/查询索引数据等操作。http功能可以在非数据节点服务器上开启,上述相关的监控插件也安装到这些服 务器上,用于监控ElasticSearch集群状态等数据信息。可以提高服务性能及数据安全
2.最小主节点数量,防止脑裂
正常情况下,集群中的所有节点,应该对主节点的选择是一致的,即一个集群中只有一个选举出来的主节点。然而,在某些情况下,比如网络通信出现问题、主节点因为负载过大停止响应等等,就会导致重新选举主节点,此时可能会出现集群中有多个主节点的现象,即节点对集群状态的认知不一致,称之为脑裂现象。为了尽量避免此种情况的出现,可以通过discovery.zen.minimum_master_nodes来设置最少可工作的候选主节点个数,建议设置为(候选主节点数 / 2) + 1, 比如,当有三个候选主节点时,该配置项的值为(3/2)+1=2,也就是保证集群中有半数以上的候选主节点。候选主节点的设置方法是设置node.mater为true
3.指定集群及节点名字
Elasticsearch 默认启动的集群名字叫 elasticsearch,部署时修改默认名字,防止其它实例不小心加入集群。
Elasticsearch 会在节点启动的时候随机给它指定一个名字,这些名字是在启动的时候产生的,每次启动节点, 它都会得到一个新的名字。这会使日志变得很混乱,因为所有节点的名称都是不断变化的。因此需要手动指定每个节点 名字,方便跟踪,排查问题
4.一台服务器上最好只部署一个Node
一台物理服务器上可以启动多个Node服务器节点(通过设置不同的启动port),但一台服务器上的CPU,内存,硬盘等资源毕竟有限,从服务器性能考虑,不建议一台服务器上启动多个node节点。
5.多个path.data
如果磁盘空间和IO性能是Elasticsearch的瓶颈的话,使用多个IO设备(通过设置多个path.data路径)存储shards,能够增加总的存储空间和提升IO性能。
如果您添加多个驱动器来提高一个单独索引的性能,可能帮助不大,因为 大多数节点只有一个分片和这样一个积极的驱动器。多个数据路径只是帮助如果你有许多索引/分片在单个节点上。如果需要更高级的、稳健的、灵活的配置, 可使用软磁盘阵列( software RAID )的软件,而不是多个数据路径的功能。
6.集群恢复配置
当你集群重启时,几个配置项影响你的分片恢复的表现。 首先,我们需要明白如果什么也没配置将会发生什么。
想象一下假设你有 10 个节点,每个节点只保存一个分片,这个分片是一个主分片或者是一个副本分片,或者说有一个有 5 个主分片/1 个副本分片的索引。有时你需要为整个集群做离线维护(比如,为了安装一个新的驱动程序), 当你重启你的集群,恰巧出现了 5 个节点已经启动,还有 5 个还没启动的场景。
假设其它 5 个节点出问题,或者他们根本没有收到立即重启的命令。不管什么原因,你有 5 个节点在线上,这五个节点会相互通信,选出一个 master,从而形成一个集群。 他们注意到数据不再均匀分布,因为有 5 个节点在集群中丢失了,所以他们之间会立即启动分片复制。
最后,你的其它 5 个节点打开加入了集群。这些节点会发现 它们 的数据正在被复制到其他节点,所以他们删除本地数据(因为这份数据要么是多余的,要么是过时的)。 然后整个集群重新进行平衡,因为集群的大小已经从 5 变成了 10。在整个过程中,你的节点会消耗磁盘和网络带宽,来回移动数据,因为没有更好的办法。对于有 TB 数据的大集群, 这种无用的数据传输需要 很长时间 。如果等待所有的节点重启好了,整个集群再上线,所有的本地的数据都不需要移动。
所以现在问题我们已经知道了,那么我们就可以配置一些设置来解决这个问题。首先我们需要设置一个参数,gateway.recover_after_nodes: 8。这个参数可以让es直到有足够的node都上线之后,再开始shard recovery的过程。所以这个参数是跟具体的集群相关的,要根据我们的集群中节点的数量来决定。此外,还应该设置一个集群中至少要有多少个node,等待那些node的时间:gateway.expected_nodes: 10,gateway.recover_after_time: 5m。经过上面的配置之后,es集群的行为会变成下面这样,等待至少8个节点在线,然后等待最多5分钟,或者10个节点都在线,开始shard recovery的过程。这样就可以避免少数node启动时,就立即开始shard recovery,消耗大量的网络和磁盘资源,甚至可以将shard recovery过程从数小时缩短为数分钟。具体参照 官网本地网关配置
7.预留一半内存给Lucene使用
一个常见的问题是配置堆太大。你有一个64 GB的机器,觉得JVM内存越大越好,想给Elasticsearch所有64 GB的内存。
内存对于Elasticsearch来说绝对是重要的,用于更多的内存数据提供更快的操作。而且还有一个内存消耗大户-Lucene。
Lucene的设计目的是把底层OS里的数据缓存到内存中。Lucene的段是分别存储到单个文件中的,这些文件都是不会变化的,所以很利于缓存,同时操作系统也会把这些段文件缓存起来,以便更快的访问。
Lucene的性能取决于和OS的交互,如果你把所有的内存都分配给Elasticsearch,不留一点给Lucene,那你的全文检索性能会很差的。
最后标准的建议是把50%的内存给elasticsearch,剩下的50%也不会没有用处的,Lucene会很快吞噬剩下的这部分内存。
文件系统缓存是为了缓冲磁盘的IO操作。至少确保有一半机器的内存保留给操作系统,而不是JAVA JVM占用了全部内存
8.内存优化
Elasticsearch默认堆内存为1.9 GB,对于实际应用,显然不够,不建议使用默认配置。
设置Elasticsearch的内存又两种方案,最简单的方法是设置一个环境变量为ES_HEAP_SIZE。当服务器启动时,它将读取此环境变量并相应设置内存。命令如下:
export ES_HEAP_SIZE=8g
另外一种方法则是在启动ES的时候,设置。
./bin/elasticsearch -Xmx8g -Xms8g
推荐采用第一种设置方法。
ES内存建议采用分配机器物理内存的一半,但最大不要超过32GB(原因请看 堆内存配置)。如何判断内存设置是否恰当,看ES启动日志中的:
[2017-09-28T17:07:34,334][INFO ][o.e.e.NodeEnvironment ] [node182e] heap size [1.9gb], compressed ordinary object pointers [true]
如果[true],则表示ok。一半超过32GB会为[false],请依次降低内存设置试试。
另外注意关闭Linux的swap!
如果机器物理内存很大的话,可以多启动几个节点(每个32GB)
9.索引别名和零停机
Elasticsearch的API支持给索引起别名,有了别名之后可以像使用索引一样使用它。但不只是这些,一个别名可以映射多个索引,所以在需要经常指定多个索引查询的情况下,大可将所查询的索引起一个别名来查。别名也可以将索引查询的过滤条件包含在内,使用别名查询时可以查询索引的一个子集。别名应用实例: elasticsearch-不停服务修改mapping , Elasticsearch 之索引别名 alias
10.设置最优的分片数和备份数
根据机器数,磁盘数,索引大小等硬件环境,根据测试结果,设置最优的分片数和备份数,单个分片最好不超过10GB,定期删除不用的索引,做好冷数据的迁移。
分片数是与检索速度非常相关的的指标,如果分片数过少或过多都会导致检索比较慢。分片数过多会导致检索时打开比较多的文件别外也会导致多台服务器之间通讯。而分片数过少会导致单个分片索引过大,所以检索速度慢。一个索引会分成多个分片存储,分片数量在索引建立后不可更改。
ElasticSearch在创建索引数据时,最好指定相关的shards数量和replicas, 否则会使用服务器中的默认配置参数shards=5,replicas=1。
因为这两个属性的设置直接影响集群中索引和搜索操作的执行。假设你有足够的机器来持有碎片和副本,那么可以按如下规则设置这两个值:
1) 拥有更多的碎片可以提升索引执行能力,并允许通过机器分发一个大型的索引;
2) 拥有更多的副本能够提升搜索执行能力以及集群稳定性。
对于一个索引来说,number_of_shards只能设置一次,而number_of_replicas可以使用索引更新设置API在任何时候被增加或者减少。
这两个配置参数在配置文件的配置如下:
index.number_of_shards: 5 number_of_replicas: 1
Elastic官方文档建议:一个Node中一个索引最好不要多于三个shards.配置total_shards_per_node参数,限制每个index每个节点最多分配多少个分片.
副本数与索引的稳定性有比较大的关系,如果Node在非正常挂了,经常会导致分片丢失,为了保证这些数据的完整性,可以通过副本来解决这个问题。建议在建完索引后在执行Optimize后,马上将副本数调整过来。
11.设置合理的刷新时间
建立的索引,不会立马查到,这是为什么elasticsearch为near-real-time的原因需要配置index.refresh_interval参数,默认是1s。
这迫使Elasticsearch集群每秒创建一个新的 segment (可以理解为Lucene 的索引文件)。增加这个值,例如30s,60s,可以允许更大的segment写入,减后以后的segment合并压力。
12.根据业务设置合理的字段类型
1、文档值(doc_values)是存储在硬盘上的数据结构,在索引时(index time)根据文档的原始值创建,文档值是一个列式存储风格的数据结构,非常适合执行存储和聚合操作,除了字符类型的分析字段之外,其他字段类型都支持文档值存储。默认情况下,字段的文档值存储是启用的,除了字符类型的分析字段之外。如果不需要对字段执行排序或聚合操作,可以禁用字段的文档值,以节省硬盘空间。
"mappings": {
"my_type": {
"properties": {
"status_code": {
"type": "text",
"index": "not_analyzed"
},
"session_id": {
"type": "text",
"index": "not_analyzed",
"doc_values": false
}
}
}
2、fielddata会消耗大量的JVM内存,因此,尽量为JVM设置大的内存,不要为不必要的字段启用fielddata存储。通过format参数控制是否启用字段的fielddata特性,字符类型的分析字段,fielddata的默认值是paged_bytes,这就意味着,默认情况下,字符类型的分析字段启用fielddata存储。一旦禁用fielddata存储,那么字符类型的分析字段将不再支持排序和聚合查询。
"mappings": {
"my_type": {
"properties": {
"text": {
"type": "text",
"fielddata": {
"format": "disabled"
}
}
}
}
13.不允许深度分页(默认深度分页性能很差)
假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。不允许翻那么深的页,默认翻的越深,性能就越差。
类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 scroll api.
scroll 会一次性给你生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id
移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。
14.document 模型设计
document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
15.数据预热
做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache
里面去。这样下次别人访问的时候,性能一定会好很多。
四、部署拓扑示意图
节点角色
Master
node.master: true 节点可以作为主节点,建议至少部署3个
DataNode
node.data: true 默认是数据节点,据业务存储数据量估算
Coordinate node
协调节点据客户端查询量计算,不指定此节点会随机集群内所有节点
说明:
一个节点可以充当一个或多个角色,默认三个角色都有
协调节点:一个节点只作为接收请求、转发请求到其他节点、汇总各个节点返回数据等功能的节点。就叫协调节点
五、es增删改查的原理
ES写入数据的过程
- 客户端选择一个node发送请求过去,这个node就是coordinating node (协调节点)
- coordinating node,对document进行路由,将请求转发给对应的node
- 实际上的node上的primary shard处理请求,然后将数据同步到replica node
- coordinating node,如果发现primary node和所有的replica node都搞定之后,就会返回请求到客户端
ES读数据过程
查询,GET某一条的数据,写入某个document,这个document会自动给你分配一个全局的唯一ID,同时跟住这个ID进行hash路由到对应的primary shard上面去,当然也可以手动的设置ID
- 客户端发送任何一个请求到任意一个node,成为coordinate node
- coordinate node 对document进行使用随机轮寻算法,路由到shard(在primary shard 以及所有的replica中随机选择一个实现负载均衡)
- 接受请求的node,返回document给coordinate note
- coordinate node返回给客户端
ES搜索数据过程
- 客户端发送一个请求给coordinate node
- 协调节点将搜索的请求转发给所有的shard对应的primary shard 或replica shard
- query phase:每一个shard 将自己搜索的结果(其实也就是一些唯一标识),返回给协调节点,有协调节点进行数据的合并,排序,分页等操作,产出最后的结果
- fetch phase ,接着由协调节点,根据唯一标识去各个节点进行拉去数据,最总返回给客户端
备注
document路由到shard上是什么意思?
一个index的数据会被分为多片,每片都在一个shard中。所以说,一个document,只能存在于一个shard中。
当客户端创建document的时候,es此时就需要决定说,这个document是放在这个index的哪个shard上。
这个过程,就称之为document routing,数据路由。
(2)路由算法:shard = hash(routing) % number_of_primary_shards
ES删除/更新数据底层原理
如果是删除操作,commit 的时候会生成一个 .del
文件,里面将某个 doc 标识为 deleted
状态,那么搜索的时候根据 .del
文件就知道这个 doc 是否被删除了。
如果是更新操作,就是将原来的 doc 标识为 deleted
状态,然后新写入一条数据。
buffer 每 refresh 一次,就会产生一个 segment file
,所以默认情况下是 1 秒钟一个 segment file
,这样下来 segment file
会越来越多
此时会定期执行 merge。每次 merge 的时候,会将多个 segment file
合并成一个
同时这里会将标识为 deleted
的 doc 给物理删除掉,然后将新的 segment file
写入磁盘
这里会写一个 commit point
,标识所有新的 segment file
,然后打开 segment file
供搜索使用,同时删除旧的 segment file
。
六、实际案例分析
往 es 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 filesystem cache
里面去。es 的搜索引擎严重依赖于底层的 filesystem cache
,你如果给 filesystem cache
更多的内存,尽量让内存可以容纳所有的 idx segment file
索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。归根结底,你要让 es 性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。
比如说你现在有一行数据。id,name,age ....
30 个字段。但是你现在搜索,只需要根据 id,name,age
三个字段来搜索。如果你傻乎乎往 es 里写入一行数据所有的字段,就会导致说 90%
的数据是不用来搜索的,结果硬是占据了 es 机器上的 filesystem cache
的空间,单条数据的数据量越大,就会导致 filesystem cahce
能缓存的数据就越少。其实,仅仅写入 es 中要用来检索的少数几个字段就可以了,比如说就写入 es id,name,age
三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 es + hbase
这么一个架构。
hbase 的特点是适用于海量数据的在线存储,就是对 hbase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id
,然后根据 doc id
到 hbase 里去查询每个 doc id
对应的完整的数据,给查出来,再返回给前端。