1. 基础知识
1.1 认识Lucene
维基百科的定义:
Lucene是一套用于全文检索和搜索的开放源码程序库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程序接口,能够做全文索引和搜索,在Java开发环境里Lucene是一个成熟的免费开放源代码工具;就其本身而论,Lucene是现在并且是这几年,最受欢迎的免费Java信息检索程序库。
Lucene官网:http://lucene.apache.org
1.2 倒排索引
在搜索引擎中,每个文档都有一个对应的文档 ID,文档内容被表示为一系列关键词的集合。例如,文档 1 经过分词,提取了 20 个关键词,每个关键词都会记录它在文档中出现的次数和出现位置。
那么,倒排索引就是关键词到文档 ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了关键词。
举个栗子:
有以下文档:
DocId | Doc |
---|---|
1 | 谷歌地图之父跳槽 Facebook |
2 | 谷歌地图之父加盟 Facebook |
3 | 谷歌地图创始人拉斯离开谷歌加盟 Facebook |
4 | 谷歌地图之父跳槽 Facebook 与 Wave 项目取消有关 |
5 | 谷歌地图之父拉斯加盟社交网站 Facebook |
对文档进行分词之后,得到以下倒排索引。
WordId | Word | DocIds |
---|---|---|
1 | 谷歌 | 1,2,3,4,5 |
2 | 地图 | 1,2,3,4,5 |
3 | 之父 | 1,2,4,5 |
4 | 跳槽 | 1,4 |
5 | 1,2,3,4,5 | |
6 | 加盟 | 2,3,5 |
7 | 创始人 | 3 |
8 | 拉斯 | 3,5 |
9 | 离开 | 3 |
10 | 与 | 4 |
.. | .. | .. |
另外,实用的倒排索引还可以记录更多的信息,比如文档频率信息,表示在文档集合中有多少个文档包含某个单词。
那么,有了倒排索引,搜索引擎可以很方便地响应用户的查询。比如用户输入查询 Facebook
,搜索系统查找倒排索引,从中读出包含这个单词的文档,这些文档就是提供给用户的搜索结果。
要注意倒排索引的两个重要细节:
- 倒排索引中的所有词项对应一个或多个文档;
- 倒排索引中的词项根据字典顺序升序排列
上面只是一个简单的栗子,并没有严格按照字典顺序升序排列。
1.3 Lucene与Elasticsearch
Lucene是一个开源的全文检索引擎工具包(类似于Java api),而Elasticsearch底层是基于这些包,对其进行了扩展,提供了比Lucene更为丰富的查询语言,可以非常方便的通过Elasticsearch的HTTP接口与底层Lucene交互。
如果在应用程序中直接使用Lucene,你需要覆盖大量的集成框架工作,而使用ElasticSearch就省下了这些集成工作。
一句话概括:Elasticsearch是Lucene面向企业搜索应用的扩展,极大的缩短研发周期。
刚刚入门Elasticsearch,只需稍微了解下Lucene,无需去真正学习它,就可以很好的完成全文索引的工作,很好的进行开发。等熟练使用es之后,可以反过头来学习Lucene里面底层的原理,也是一种提升。
因为Lucene是一个编程库,您可以按原始接口来调用。但是Elasticsearch是在它基础上扩展的应用程序,就可以直接拿来使用了。
举个例子,你直接拿汽车(Elasticsearch)来开,开好车就行,无需了解里面的发动机、各个组件(Lucene library)。后面你在去了解一些原理,对于修车等等会有帮助。
1.4 ES的核心面试题
(1)es的分布式架构原理是什么(es是如何实现分布式的)?
(2)es写入数据的工作原理是什么?es查询数据的工作原理是什么?
(3)es在数据量很大的情况下(数十亿级别)如何提高查询性能?
(4)es生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?
2. 认识ES
Lucene 是最先进、功能最强大的搜索库。如果直接基于 lucene 开发,非常复杂,即便写一些简单的功能,也要写大量的 Java 代码,需要深入理解原理。
elasticsearch 基于 lucene,简称es,隐藏了 lucene 的复杂性,提供了简单易用的 restful api / Java api 接口(另外还有其他语言的 api 接口)。现在分布式搜索基本已经成为大部分互联网行业的Java系统的标配,其中尤为流行的就是es,前几年es没火的时候,大家一般用solr。但是这两年基本大部分企业和项目都开始转向es了。
- 分布式的文档存储引擎
- 分布式的搜索引擎和分析引擎
- 分布式,支持 PB 级数据
2.1 ES 的核心概念
Near Realtime
近实时,有两层含义:
- 从写入数据到数据可以被搜索到有一个小延迟(大概是 1s)
- 基于 es 执行搜索和分析可以达到秒级
Cluster 集群
集群包含多个节点,每个节点属于哪个集群都是通过一个配置来决定的,对于中小型应用来说,刚开始一个集群就一个节点很正常。
Node 节点
Node 是集群中的一个节点,每个节点有一个唯一的名称,这个名称默认是随机分配的。默认节点会去加入一个名称为 elasticsearch
的集群。如果直接启动一堆节点,那么它们会自动组成一个 elasticsearch 集群,当然一个节点也可以组成 elasticsearch集群。
Document & field
文档是 es 中最小的数据单元,一个 document 可以是一条客户数据、一条商品分类数据、一条订单数据,通常用 json 数据结构来表示。每个 index 下的 type,都可以存储多条 document。一个 document 里面有多个 field,每个 field 就是一个数据字段。
1 | { |
Index
索引包含了一堆有相似结构的文档数据,比如商品索引。一个索引包含很多 document,一个索引就代表了一类相似或者相同的 ducument。
Type
类型,每个索引里可以有一个或者多个 type,type 是 index 的一个逻辑分类,比如商品 index 下有多个 type:日化商品 type、电器商品 type、生鲜商品 type。每个 type 下的 document 的 field 可能不太一样。
shard
单台机器无法存储大量数据,es 可以将一个索引中的数据切分为多个 shard,分布在多台服务器上存储。有了 shard 就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。每个 shard 都是一个 Lucene index。
replica
任何一个服务器随时可能故障或宕机,此时 shard 可能就会丢失,因此可以为每个 shard 创建多个 replica 副本。replica 可以在 shard 故障时提供备用服务,保证数据不丢失,多个 replica 还可以提升搜索操作的吞吐量和性能。primary shard(建立索引时一次设置,不能修改,默认 5 个),replica shard(随时修改数量,默认 1 个),默认每个索引 10 个 shard,5 个 primary shard,5个 replica shard,最小的高可用配置,是 2 台服务器。
这么说吧,shard 分为 primary shard 和 replica shard。而 primary shard 一般简称为 shard,而 replica shard 一般简称为 replica。
ES 核心概念 vs DB 核心概念
es | db |
---|---|
index | 数据库 |
type | 数据表 |
docuemnt | 一行数据 |
以上是一个简单的类比。
3. ES架构原理
elasticsearch设计的理念就是分布式搜索引擎,底层其实还是基于lucene的。核心思想就是在多台机器上启动多个es进程实例,组成了一个es集群。
es中存储数据的基本单位是索引,比如现在要在es中存储一些订单数据,此时就应该在es中创建一个索引,order_idx,所有的订单数据就都写到这个索引里面去,一个索引相当于mysql里的一张表。
1 | index -> type -> mapping -> document -> field |
以下是对上述名词概念的一种类比,仅仅是一种类比描述:
index: mysql 里的一张表。
type:没法跟mysql里去对比,一个index里可以有多个type,每个type的字段都是差不多的,但是有一些略微的差别。
假设有一个订单的index,里面专门是放订单数据。
在 mysql 中建表,有些订单是实物商品的订单,比如一件衣服、一双鞋子;有些订单是虚拟商品的订单,比如游戏点卡,话费充值。这两种订单大部分字段是一样的,但是少部分字段可能存在略微的一些差别。
所以就需要在订单 index 里创建两个 type,一个是实物商品订单 type,一个是虚拟商品订单 type,这两个 type 大部分字段是一样的,少部分字段是不一样的。
很多情况下,一个 index 里可能就一个 type,但是确实如果说是一个 index 里有多个 type 的情况(注意,mapping types
这个概念在 ElasticSearch 7.X 已被完全移除,详细说明可以参考官方文档)。
可以认为 index 是一个类别的表,具体的每个 type 代表了 mysql 中的一个表。每个 type 有一个 mapping,如果你认为一个 type 是具体的一个表,index 就代表多个 type 同属于的一个类型,而 mapping 就是这个 type 的表结构定义。
场景类比:在 mysql 中创建一个表的时候,需要定义表结构、表结构中的字段、每个字段的类型。因此 index 里的一个 type 里面写的一条数据,这条数据叫做一条 document,一条 document 就代表了 mysql 中某个表里的一行,每个 document 有多个 field,每个 field 就代表了这个 document 中的一个字段的值。
创建一个es索引的时候,这个索引可以拆分成多个 shard
,每个 shard 存储部分数据。拆分多个 shard 是有好处:
支持横向扩展
比如整个数据量是 3T,3 个 shard,每个 shard 可以分别拆分成 1T 的数据,若现在数据量增加到 4T 该怎么扩展?很简单,重新建一个有 4 个 shard 的索引,将数据导进去;
提高性能
数据分布在多个 shard,即多台服务器上,所有的操作都会在多台机器上并行分布式执行,提高了吞吐量和性能。
另外 shard 的数据实际是有多个备份,也就是说每个 shard 都有一个 primary shard
,这个 primary shard 专门负责写入数据,同时这个还有几个副本 replica shard
。primary shard
写入数据之后,会将数据同步到自己的所有副本 replica shard
上去(primary shard和其副本的replica shard不会在同一台机器上)。
因此通过上述的主副shard方案,每个shard的数据在多台机器上都有备份,如果某个机器宕机了,没关系,还有别的数据副本在别的机器上,这就可以保证高可用了。
es集群会自动从众多节点中选举某一个节点为master节点,这个master节点其实就是干一些管理的工作,比如维护索引元数据拉,负责切换primary shard和replica shard身份等。
当master节点宕机了,那么es集群会重新选举一个节点成为新的master节点。
当非master节点宕机了,那么会由master节点,让那个宕机节点上的primary shard的身份转移到其他机器上的replica shard。随后当修复了那个宕机机器并重启之后,master 节点会控制将缺失的 replica shard 分配过去,同步后续修改的数据之类的,让集群恢复正常。
简单说,当某个非 master 节点宕机了,那么此节点上的 primary shard 就消失了,那么 master 会检测到该节点挂了,随即让那个 primary shard 对应的 replica shard(在其他机器上)切换为 primary shard。如果宕机的机器修复了,修复后的节点也不再是 primary shard,而是 replica shard。
上述就是elasticsearch作为一个分布式搜索引擎最基本的一个架构设计。
4. 写入&查询数据的工作原理
如果只把es当作一个黑盒,只会使用api读写数据是不能满足实际开发需求的,因为一旦出了问题,根本无从下手解决。
4.1 写数据过程
客户端选择一个node发送请求过去,这个node就是
coordinating node
(协调节点)。coordinating node
对document进行路由,将请求转发给对应的node(有primary shard)。实际的node上的
primary shard
处理请求,然后将数据同步到有对应replica shard
的node上。coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端。
4.2 读数据过程
数据写入某个document的时候,es会自动会给这个document自动分配一个全局唯一的id,称为doc id
,协同节点也是根据doc id进行hash路由到对应的primary shard上面去。也可以手动指定doc id,比如用订单id,用户id。
读数据的过程大致是通过doc id查询,es会根据doc id进行hash,判断出来当时把doc id分配到了哪个shard上面去,再从那个shard去查询:
客户端发送请求到任意一个node,当前node成为
coordinate node
(协调节点)。coordinate node
对doc id
进行哈希路由,将请求转发到对应的 node,此时会使用round-robin
随机轮询算法,在primary shard
以及其所有 replica 中随机选择一个,让读请求负载均衡。接收请求的 node 返回 document 给
coordinate node
。coordinate node
返回 document 给客户端。
4.3 搜索数据过程
es 最强大的是做全文检索,就是比如你有三条数据:
1 | java真好玩儿啊 |
es可以根据 “java” 关键词来搜索,将包含 “java”关键词的 document
给搜索出来。es 就会给你返回:”java真好玩儿啊”,”java好难学啊”。
- 客户端发送请求到一个
coordinate node
。 - 协调节点将搜索请求转发到所有的 shard 对应的
primary shard
或replica shard
。 - query phase:每个 shard 将自己的搜索结果(其实就是一些
doc id
)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。 - fetch phase:接着由协调节点根据
doc id
去各个节点上拉取实际的document
数据,最终返回给客户端。
写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。
4.4 写数据底层原理
es里的写流程,有4个底层的核心概念:refresh
、flush
、translog
、merge
1)数据先写入内存buffer,在写入buffer的同时将数据写入translog
日志文件,注意:此时数据还没有被成功es索引记录,因此无法搜索到对应数据;
2)如果buffer快满了或者到一定时间,es就会将buffer数据refresh
到一个新的segment file
中,但是此时数据不是直接进入segment file
的磁盘文件,而是先进入os cache
的。这个过程就是refresh
。
每隔1秒钟,es将buffer中的数据写入一个新的
segment file
,因此每秒钟会产生一个新的磁盘文件segment file
,这个segment file中就存储最近1秒内buffer中写入的数据。如果buffer里面此时没有数据,那当然不会执行refresh操作咯,每秒创建换一个空的segment file,如果buffer里面有数据,默认1秒钟执行一次refresh操作,刷入一个新的segment file中。
操作系统中,磁盘文件其实都有一个操作系统缓存
os cache
,因此数据写入磁盘文件之前,会先进入操作系统级别的内存缓存os cache
中。
一旦buffer中的数据被refresh操作,刷入os cache
中,就代表这个数据就可以被搜索到了。
这就是为什么es被称为准实时(NRT,near real-time):因为写入的数据默认每隔1秒refresh
一次,也就是数据每隔一秒才能被 es 搜索到,之后才能被看到,所以称为准实时。
es可以通过restful api或者java api,手动执行一次
refresh
操作,也就是手动将buffer中的数据刷入os cache中,让数据立马就可以被搜索到。
只要数据被输入os cache
中,buffer就会被清空,并且数据在translog
日志文件里面持久化到磁盘了一份,此时就可以让这个segment file
的数据对外提供搜索了。
3)重复1~2步骤,新的数据不断进入buffer
和translog
,不断将buffer数据写入一个又一个新的segment file中去,每次refresh
完,buffer就会被清空,同时translog保留一份日志数据。随着这个过程推进,translog
文件会不断变大。当translog
文件达到一定程度时,就会执行commit
操作。
4)commit操作发生第一步,就是将buffer中现有数据refresh
到os cache
中去,清空buffer
。
5)将一个 commit point
写入磁盘文件,里面标识着这个 commit point
对应的所有 segment file
,同时强行将 os cache
中目前所有的数据都 fsync
到磁盘文件中去。
8)将现有的translog
清空,然后再次重启启用一个translog
,此时commit操作完成。
默认每隔30分钟会自动执行一次
commit
,但是如果translog
文件过大,也会触发commit。整个commit的过程,叫做flush
操作。我们可以手动执行flush
操作,就是将所有os cache
数据刷到磁盘文件中去。我们也可以通过es的api,手动执行
flush
操作,手动将os cache
中的数据fsync强刷到磁盘上去,记录一个commit point,清空translog日志文件。
translog
日志文件的作用是什么?
在你执行commit操作之前,数据要么是停留在buffer中,要么是停留在os cache
中,无论是buffer还是os cache都是内存,一旦这台机器死了,内存中的数据就全丢了。
因此需要将数据对应的操作写入一个专门的日志文件,也就是translog
日志文件,一旦此时机器宕机,再次重启的时候,es会自动读取translog
日志文件中的数据,恢复到内存buffer和os cache中去。
translog
其实也是先写入os cache的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有5秒的数据会仅仅停留在buffer或者translog文件的os cache
中,如果此时机器挂了,会丢失5秒钟的数据。但是这样性能比较好,最多丢5秒的数据。也可以将translog
设置成每次写操作必须是直接fsync到磁盘,但是性能会差很多。
综上可以看出:
- es是准实时的,因此数据写入1秒后才可以搜索到。
- es可能会丢失数据:有5秒的数据停留在buffer、translog的os cache、segment file的os cache中,也就是这5秒的数据不在磁盘上,此时如果宕机,会导致5秒的数据丢失。
如果你希望一定不能丢失数据,可以查官方文档设置个参数。使得每次写入一条数据,都是写入buffer,同时写入磁盘上的translog
,但是这会导致写性能、写入吞吐量会下降一个数量级。本来一秒钟可以写2000条,现在一秒钟可能只能写200条。
总结一下:
数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 是准实时的,因为从写入到能被搜索到中间有 1s 的延迟)。
每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失)。
translog
大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。
数据写入 segment file 之后,同时就建立好了倒排索引。
4.5 删除/更新数据底层原理
如果是删除操作,commit 的时候会生成一个 .del
文件,里面将某个 doc 标识为 deleted
状态,那么搜索的时候根据 .del
文件就知道这个 doc 是否被删除了。
如果是更新操作,就是将原来的 doc 标识为 deleted
状态,然后新写入一条数据。
segment file 的 merge 操作:
buffer 每 refresh 一次,就会产生一个 segment file
,所以默认情况下是 1 秒钟一个 segment file
,这样下来 segment file
文件数量会越来越多,此时es会定期执行 merge。
每次 merge 的时候,会将多个 segment file
合并成一个,同时这里会将标识为 deleted
的 doc 给物理删除掉,然后将新的 segment file
写入磁盘,这里会写一个 commit point
,标识所有新的 segment file
,然后打开 segment file
供搜索使用,同时删除旧的 segment file
。
总的来说,就是当segment file多到一定程度的时候,es就会自动触发merge操作,将多个segment file给merge成一个segment file。
5. 提高查询性能
es在数据量很大的情况下(数十亿级别)如何提高查询效率/性能?
es性能其实并没有你想象中那么好的。很多时候数据量大了,特别是有几亿条数据的时候,可能跑个搜索怎么需要5秒10秒。第一次搜索的时候,是510秒,后面反而就快了,可能就几百毫秒。
es性能优化是没有什么银弹的,不要期待着随手调一个参数,就可以万能的应对所有的性能慢的场景。也许有的场景是换个参数,或者调整一下语法,就可以搞定,但是绝对不是用一个配置参数通用所有场景的。
在这个海量数据的场景下,如何提升es搜索的性能,需要生产环境实践经验积累。
5.1 filesystem cache(性能优化的杀手锏)
利用操作系统的缓存
os cache
进行性能优化点。
es里写的数据,实际上最终都会写到磁盘文件里去,磁盘文件里的数据操作系统会自动将里面的数据缓存到os cache
里面去。
因为es的搜索引擎严重依赖于底层的filesystem cache
,所以如果给filesystem cache
更多的内存,尽量让内存可以容纳所有的indx
的segment file
索引数据文件,那么es搜索的时候就基本都是走内存的,性能会非常高。
很多测试和压测可以看出性能差距巨大:如果走磁盘一般肯定超秒,搜索性能绝对是秒级别的,1秒,5秒,10秒。但如果是走filesystem cache
,也就是是走纯内存,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。
案例:某个公司 es 节点有 3 台机器,每台机器看起来内存很多,64G,总内存就是 64 * 3 = 192G
。每台机器给 es jvm heap 是 32G
,那么剩下来留给 filesystem cache
的就是每台机器才 32G
,总共集群里给 filesystem cache
的就是 32 * 3 = 96G
内存。而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 1T
的磁盘容量,es 数据量是 1T
,那么每台机器的数据量是 300G
。这样性能好吗? filesystem cache
的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。
归根结底,让 es 查询性能很高,最佳实践是让es进程占用系统的内存尽量大,至少可以容纳你的总数据量的一半。
根据我们自己的生产环境实践经验,最佳的情况下,是仅仅在 es 中就存少量的数据,就是只存用来搜索的那些索引,如果内存留给 filesystem cache
的是 100G,那么就将索引数据量控制在 100G 以内,这样的话,数据几乎全部走内存来搜索,性能非常之高,一般可以在 1 秒以内。
比如说你现在有一行数据。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
对应的完整的数据,给查出来,再返回给前端。
写入 es 的数据最好小于等于(略微大于一点也可) es 的 filesystem cache 的内存容量。粗略估算从 es 检索可能需要耗时 20ms,然后再根据 es 返回的 id 去 hbase 里查询,查 20 条数据,可能需要耗时 30ms,这样每次查询就是 50ms。而原来的一次查询就是耗时 5~10s,这种性能提升是非常显著的。
5.1 数据预热
实际生产中还有一种情况:即使按照上述的方案去做了,因为物理设备的限制,导致 es 集群中每个机器写入的数据量还是超过了 filesystem cache
一倍,比如有 60G 数据需要写入一台机器,结果 filesystem cache
撑死只有 30G,那么剩下的 30G 数据只存留在了磁盘上。
对于这种情况,最佳解决方案就是做数据预热。
举例微博热搜数据,自己搭建一个后台系统,这个系统就是定时去搜索当前用户的热数据或者可能上热搜的数据,提前刷到 filesystem cache
里去,之后用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了。
电商业务也是如此,可以将用户平时热搜的商品,比如说新品上市手机或数码设备等热搜商品数据提前在后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 filesystem cache
里去。
对于那些比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache
里面去。这样下次别人访问的时候,性能一定会好很多。
5.3 冷热分离
es 可以做类似于 mysql 的水平拆分,也就是将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让它们留在 filesystem os cache
里,别让冷数据给冲刷掉。
假设有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 shard。3 台机器放热数据 index,另外 3 台机器放冷数据 index。90%的用户群体大量时间是在访问热数据 index,热数据可能就占总数据量的 10%,由于这部分频繁搜索的数据量很少,于是几乎全都保留在 filesystem cache
里面了,就可以确保热数据的访问性能是很高的。
对于冷数据而言,是在另一个 index 里的,跟热数据 index 不在相同的机器上,两种数据存放的机器之间不存在联系。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,因为冷数据本身就是很少人访问(10%左右的用户群体),这些用户随便怎么搜索冷数据也不会把热数据从内存中”挤出来”。
5.4 Document 模型设计
对于 MySQL,我们经常有一些复杂的关联查询。但在 es 里尽量不要用复杂的关联查询,一旦用了性能一般都不会太好。
document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。
因此,在搜索/查询的时候,要执行一些业务强相关的特别复杂的操作:
在写入数据的之前就设计好模型,加几个字段,把处理好的数据写入加的字段里面。
用 Java 程序封装的复杂关联操作,es仅用来做搜索数据的操作。
5.5 分页性能优化
5.5.1 分页的坑
举个例子,假如每页是 10 条数据,用户现在要查询第 100 页,es实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果这个 index 有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。
分布式情况下,要查第 100 页的 10 条数据,不可能是从 5 个 shard,每个 shard 就查 2 条数据,最后到协调节点合并成 10 条数据。
es必须得从每个 shard 都查 1000 条数据过来,然后根据用户搜索的需求进行排序、筛选等等操作,最后再次分页,拿到里面第 100 页的数据。当搜索翻页的时候,翻的越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用 es 做分页的时候,明显会发现越翻到后面越翻越慢。
实际开发测试中遇到过这个问题,用 es 作分页,前几页就几十毫秒,翻到 10 页或者几十页的时候,基本上就要 5~10 秒才能查出来一页数据了。
5.5.2 解决方案
1)不允许深度分页(默认深度分页性能很差)
一般公司要求,系统不允许翻很深的页,因为默认翻的越深,性能就越差。
2)类似于 APP 里的推荐商品不断下拉出来一页一页的
类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 scroll api
,关于如何使用,可自行搜索。
scroll 会一次性给你生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id
移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多,基本上都是毫秒级的。
但唯一需要注意的是:这个适合于那种类似微博下拉翻页的,不能随意跳到任何一页的场景。
也就是说,用户不能先进入第 10 页,再跳到第 120 页,然后又跳到第 58 页,不能随意乱跳页。因此现在很多移动产品都是不允许用户随意翻页,也有一些网站做的就是你只能往下拉,一页一页的翻。
初始化时必须指定 scroll
参数,告诉 es 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。
除了用 scroll api
,也可以用 search_after
来做,search_after
的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许用户随意翻页,用户只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。
6. 生产部署
ES 生产集群的部署架构是什么?每个索引的数据量大概有多少?每个索引大概有多少个分片?
这样的问题不是技术能力问题,就是单纯考察面试者是否在真正的生产环境里使用过es,属于面试必问问题。
这里有个基本版本部署配置:
- es 生产集群,部署了 5 台机器,每台机器是 6 核 64G 的,集群总内存是 320G。
- es 集群的日增量数据大概是 2000 万条,每天日增量数据大概是 500MB,每月增量数据大概是 6 亿,也就是15G。目前系统已经运行了几个月,现在 es 集群里数据总量大概是 100G 左右。
- 目前线上有 5 个索引(这个结合业务来看,看有哪些数据可以放 es 里),每个索引的数据量大概是 20G,所以这个数据量之内,每个索引分配的是 8 个 shard,比默认的 5 个 shard 多了 3 个 shard。
7. 更深的问题
分布式搜索技术如果挖深了可以问得极其深,比较严格的面试官可能会挖到es底层:相关度评分算法(TF/IDF算法)、deep paging、上千万数据批处理、跨机房多集群同步、搜索效果优化等很多的实际生产问题。
分布式消息队列也是如此:Kafka的主从复制的底层原理、leader选举的算法、增加partition以后的rebalance算法,如何优化 Kafka 写入的吞吐量。
TF/IDF算法
比如要搜索”dota ti5”,我希望第一个搜索的结果文档叫做”dota2”而不是”英雄联盟(LOL)”,那么lucene是如何做的呢?Lucene 默认的搜索算法叫做TF/IDF 算法。
TF-IDF算法全称为term frequency–inverse document frequency。TF就是term frequency的缩写,意为词频。IDF则是inverse document frequency的缩写,意为逆文档频率。该算法在信息处理中通常用来抽取关键词。
比如,对一个文章提取关键词作为搜索词,就可以采用TF-IDF算法。
要找出一篇文章中的关键词,通常的思路就是,就是找到出现次数最多的词。如果某个词很重要,它应该在这篇文章中多次出现。于是,我们进行”词频”(Term Frequency,缩写为TF)统计。
但是通常,一篇中文的文章中,都会有很多没有实际意义的词,比如”的”、”是”、”了”等助词类词,称为停用词,称它们为停用词是因为在文本处理过程中如果遇到它们,则立即停止处理,将其扔掉。将这些词扔掉减少了索引量,增加了检索效率,并且通常都会提高检索的效果。停用词主要包括英文字符、数字、数学字符、标点符号及使用频率特高的单汉字等。
当过滤掉所有的停用词后,剩下的都是实际意义的词,但也不能简单的认为那个词出现的次数多就是关键词。比如在一篇如何组装电脑的文章中,出现”CPU”、”主板”等关键词和出现”说明书”的次数一样多,但很显然,”CPU”、”主板”等关键词,更能确定这个文章的特性。也就是说,”CPU”、”主板”等关键词比”说明书”这个关键词更重要,需要排在前面。
所以我们就需要一个权重系数,用来调整各个关键词的重要性。如果一个词很少见,但是它在某个文章中反复出现多次,那么可以认为这个词反应了这个文章的特性,可以把它作为关键词。在信息检索中,这个权重非常重要,它决定了关键词的重要度,这个权重叫做”逆文档频率”(Inverse Document Frequency,缩写为IDF),它的大小与一个词的常见程度成反比。
在知道了词频和权重之后,两者相乘,就得到一个词的TF-IDF值,某个词对文章的重要性越高,它的TF-IDF值就越大。所以,排在最前面的几个词,就是这篇文章的关键词。
因此TF-IDF算法的主要工作就是计算出TF*IDF
值最大的那几个词,作为文章的关键词。
TF-IDF算法的优点是简单快速,结果比较符合实际情况。缺点是,单纯以”词频”衡量一个词的重要性,不够全面,有时重要的词可能出现次数并不多。而且,这种算法无法体现词的位置信息,出现位置靠前的词与出现位置靠后的词,都被视为重要性相同,这是不正确的。(一种解决方法是,对全文的第一段和每一段的第一句话,给予较大的权重。)
当通过TF-IDF算法找出文章的关键字后,可以运用到一些具体的场景。比如:根据关键字找出相似的文章。
如果你认为lucene本身的算法不够好,那么你可以考虑去实现其他的算法。比如BM25和BM25F算法。在lucene当中,如果你想更改原本的算法,那么你需要extends原来的Similarity类,然后将它配置到你的索引writer的配置中。如果你需要更好的索引。那么你可能需要为lucene重写很多代码,包括权重包(Weight),Query包(查询),Score包(打分)。
8. 扩展博文
Elasticsearch 入门教程-bitiger知乎专栏
视频教程:
【博学谷】搜索集大成者(lucene&solr&es)
https://www.bilibili.com/video/av45594844?from=search&seid=10490762564639410509
千锋Java:ElasticSearch6入门教程
https://www.bilibili.com/video/av45558199?from=search&seid=10490762564639410509
龙果学院-es核心知识篇
https://www.bilibili.com/video/av28091206?from=search&seid=7590158700323939959