来自尚硅谷 ES 教程
背景知识
从MySQL 到 ES
这一小节是我的一点点理解,如果有不对的话,欢迎指正
ES 是一个开源的高扩展的分布式全文搜索引擎,这样讲似乎还是有点抽象,那我们用一个更加熟悉的东西 MySQL来辅助理解。
既然是搜索引擎,必须要把数据放进去才能搜索,那我们先把ES当成一种存储 JSON 格式数据的数据库。在实际场景中,我们会出现很多模糊查询的场景,对于MySQL 来说,为了加快查询速度,我们一般用到了索引,但是在模糊查询的时候,%放在开头作模糊匹配会产生索引失效问题,当然极端的方法是进行覆盖索引,所查皆索引,不用回表,但不能解决实际问题。
这时候就有一个初学者不常用的概念,全文索引 FullText index
,它通过建立倒排索引,实现对字段进行高效的模糊查询。
在MySQL中我们平常建立的是 B+树索引,可能没有注意到,MySQL 的 innoDB
引擎在新版本中也是支持全文索引的,但是性能上就差了些。具体为什么ES在全文索引中性能好,在后面会有提到。
那么什么是倒排索引,我用简单的话描述一下,假设现在有个表,字段是 book_id, author, book_name,以及一些书籍信息字段。正常的索引是 一个 book_id 对应一行数据,我们通过给 book_id 加索引,可以快速找到一定条件下的 book_id 对应的 author;如果我们想找某个作者写过的所有书,这也可以解决,给 author 也加索引就行了,否则就得全表扫描了。但如果我们记不得具体名字了,要模糊查找这个 author,或者模糊查找 book_name,或者模糊查找书的类别,甚至模糊查找摘要中的提到的一个词,就很麻烦了 。
**倒排索引 是在写入数据时,找出 author 对应的所有 book_id,保存到倒排列表(PostingList
)中,那么我们通过给 author 加索引,快速找到 author 就能找到对应的 book_id,这样看起来似乎没加快速度,但实际中,我们可能会对 author(或者是book_name,甚至是一句摘要)以某种细粒度进行进一步拆词,将分解后的词(或者字,字母)和出现这些词的 id 保存到倒排列表中,这样我们进行 模糊查找 的时候,就会快很多。每个拆出来的字/词叫做 词条(Term),保存词条和倒排列表的结构叫做 词典(Term Dictionary)。
简单说就是一种用空间换时间,加快模糊查找的方法。查询的时候,先看我们模糊查询的项在不在词典中,不在就退出查询,在的话就在倒排列表中找到对应出现的位置返回。
这篇文章中提到了MySQL对复杂条件的查询支持不好,而这个恰恰是实际场景中的需要。而 ElasticSearch
因其特性,十分适合进行复杂条件查询,是业界主流的复杂条件查询场景解决方案,广泛应用于订单和日志查询等场景。
MySQL 对于复杂条件查询的支持并不好。MySQL 最多使用一个条件涉及的索引来过滤,然后剩余的条件只能在遍历行过程中进行内存过滤。上述这种处理复杂条件查询的方式因为只能通过一个索引进行过滤,所以需要进行大量的 I/O 操作来读取行数据,并消耗 CPU 进行内存过滤,导致查询性能的下降。
Restful
REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就 是 RESTful。
Web 应用程序最重要的 REST 原则是,客户端和服务器之间的交互在请求之间是无状态的。从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外,无状态请求可以由任何可用服务器回答,这十分适合云计算之类的环境。客户端可以缓存数据以改进性能。 在服务器端,应用程序状态和功能可以分为各种资源。资源是一个有趣的概念实体,它向客户端公开。资源的例子有:应用程序对象、数据库记录、算法等等。**每个资源都使用 URI (Universal Resource Identifier) 得到一个唯一的地址。所有资源都共享统一的接口,**以便在客户端和服务器之间传输状态。使用的是标准的 HTTP 方法,比如 GET、PUT、POST 和 DELETE。 在 REST 样式的 Web 服务中,每个资源都有一个地址。资源本身都是方法调用的目标,方法列表对所有资源都是一样的。这些方法都是标准方法,包括 HTTP GET、POST、 PUT、DELETE,还可能包括 HEAD 和 OPTIONS。简单的理解就是,如果想要访问互联网上的资源,就必须向资源所在的服务器发出请求,请求体中必须包含资源的网络路径,以及对资源进行的操作(增删改查)。
我归纳一下,在 REST 样式的 Web 服务中,每个资源都有唯一地址 URI,和统一的接口,我们操作资源的时候,用请求方法来告诉服务器我们要对资源的操作方式(增删改查)。
这里需要复习一下原理:
我们知道,请求分为请求行,请求头,(空行),请求体,请求行中有请求方法和请求的URI,以及 HTTP 协议版本
https://blog.csdn.net/weixin_39803022/article/details/111060742
HTTP1.0定义了三种请求方法:GET, POST 和 HEAD方法。
HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法
JSON
没啥好说的
var obj = {"name":"zhangsan", "age":30, "info":{"email":"xxx"}}
var objs = [obj, obj]
幂等性
幂等性:就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
举个最简单的例子,那就是支付,用户购买商品使用约支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条
ES 高级
结构
-
索引(Index)
类比数据库。
一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的 索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。 能搜索的数据必须索引,这样的好处是可以提高查询速度。
Elasticsearch 索引的精髓:一切设计都是为了提高搜索的性能。
-
类型(Type)
类比表,最新版本已经弃用,默认为 _doc
-
文档(Document)
以 JSON 形式存储在 Index 中的一条数据
-
字段(Field)
就相当于数据库的字段,对文档数据根据不同属性进行的分类标识。
-
映射(Mapping)
mapping 是处理数据的方式和规则方面做一些限制,如:某个字段的数据类型、默认值、 分析器、是否被索引等等。这些都是映射里面可以设置的。数据的一些使用规则设置也叫做映射,按着最优规则处理数据对性能提高很大,因此才需要建立映射, 并且需要思考如何建立映射才能对性能更好。
-
分片(Shards)
Elasticsearch 提供了将索引划分成多份的能力, 每一份就称之为分片。当你创建一个索引的时候,你可以指定你想要的分片的数量,创建完后不能改变。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。
1)允许你水平分割 / 扩展你的内容容量。
2)允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。
-
副本(Replicas)
Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做副本或复制分片或从分片,创建完后副本数可以增减。
1) 在分片/节点失败的情况下,提供了高可用性。主从分片不能置于同一节点。
2) 扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。
-
分配(Allocation)
将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分 片复制数据的过程。这个过程是由 master 节点完成的。
-
节点和主节点
一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据(水平扩容)。
当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、 删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
当主节点挂掉后,ES会自动选举新的主节点保证正常工作。如果挂掉的节点上有主分片,主节点也会自动提升挂掉的分片的副本为主分片。
作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。
分片策略
-
路由计算
我们需要按照一个确定规则保存和读取数据在某个分片上
shard = hash(_id) % num_of_primary_shards //
-
分片控制
我们可以发送请求到集群中的任一节点。 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。将接收请求的节点叫协调节点(coordinating node) 。虽然单一节点就能满足要求,但是为了降低负载,我们会把请求分散。
-
写流程
客户端向集群发送写的请求,可以选择任何一个节点,这个节点就是协调节点
-
协调节点默认使用文档 ID 参与计算(默认值是文档的 id,也可以采用自定义值,比如用户 id),以便为路由提供合适的分片(即找到对应的主分片):
shard = hash(document_id) % (num_of_primary_shards)
-
为保证数据的安全性,主分片会将存入的数据向副本进行同步。(调整一致性 consistency 参数,可以先备份才能查询或者不备份就能查询或者备份一半才能查询)
延时:主分片的延时 + 并行写入各副本的最大延时
-
读流程
客户端向集群发送读的请求,可以选择任何一个节点,这个节点就是协调节点
这个和写的区别在于,**为了负载均衡(因为ES本身就是读的居多),会把所有的分片(主从)**的信息拿到,进行轮询,将请求转发给空闲的节点,或者说分配的节点。
-
更新流程
客户端向集群发送更新的请求,可以选择任何一个节点,这个节点就是协调节点
发送到主分片所在节点,进行不断写(可能会有抢占锁),然后分发到副本
动态更新索引
传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要可以对单个字段索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。
早期的倒排索引被写入磁盘后是不可改变的:它永远不会修改。这样不管是从缓存的角度(始终有效,大部分请求不会命中磁盘),还是从锁的角度(不写不变就不用加锁)都是很高效的,但是问题在于如果加入新数据,就要重建全部索引。
如何在保留不变性的前提下实现倒排索引的更新?
通过增加新的补充索引来反映最近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到。
Elasticsearch 使用 Lucene 作为其全文搜索引擎,用于处理纯文本的数据,但 Lucene 只是一个库,提供建立索引、执行搜索等接口,但不包含分布式服务,这些正是 Elasticsearch 做的。Lucene 这个 java 库引入了按段搜索的概念。 还增加了commit point 提交点的概念,一个列出了所有已知段的文件。每一段本身都是一个倒排索引,最后将结果进行合并。
段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档 的更新。 取而代之的是,每个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。 当一个文档被 “删除” 时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。
文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
近实时搜索(Refresh)
随着按段(per-segment)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这样还是不够快。磁盘在这里成为了瓶颈。提交一个新的段到磁盘需要一个 fsync
来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。 但是 fsync
操作代价很大; 如果每次索引一个文档都去执行一次的话会造成很大的性能问题。 我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着 fsync
要从整个过程中被移除。在 Elasticsearch 和磁盘之间是文件系统缓存。 像之前描述的一样, 在内存索引缓冲区中的文档会被写入到一个新的段中。 但是这里新段会被先写入到 文件系统缓存(OS Cache) ,这一步代价会比较低,稍后再被刷新到磁盘,这一步代价比较高。不过只要文件已经在缓存中, 就可以像其它文件一样被打开和读取了。
在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 refresh(写入系统缓存) 。 默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch 是 近实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。 这些行为可能会对新用户造成困惑: 他们索引了一个文档然后尝试搜索它,但却没有搜到。 这个问题的解决办法是用 refresh API 执行一次手动刷新:
/users/_refresh
尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要 在生产环境下每次索引一个文档都去手动刷新。 相反,你的应用需要意识到 Elasticsearch 的近实时的性质,并接受它的不足。 并不是所有的情况都需要每秒刷新。可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是近实时搜索, 可以通过设置 refresh_interval , 降低每个索引的刷新频率。refresh_interval 可以在既存索引上进行动态更新。 在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来。
持久化变更(Flush)
如果没有用 fsync 把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断 电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch 的可靠性,需要确保数据变化被持久化到磁盘。在动态更新索引,我们说一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。
即使通过每秒刷新(refresh)实现了近实时搜索,我们仍然需要经常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档怎么办?我们也不希望丢失掉这些数据。Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行 操作时均进行了日志记录。
当 Elasticsearch 启动的时 候,translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。
translog 也被用来提供实时 CRUD 。当你试着通过 ID 查询、更新、删除一个文档,它会 在尝试从相应的段中检索之前, 首先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。
执行一个提交并且截断 translog 的行为在 Elasticsearch 被称作一次 flush,分片每 30 分钟被自动刷新(flush),或者在 translog 太大的时候也会刷新。
段合并(Segment merge)
由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和 cpu 运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。 Elasticsearch 通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大 段再被合并到更大的段。
段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的 旧版本)不会被拷贝到新的大段中。 启动段合并不需要你做任何事。进行索引和搜索时会自动进行。
-
当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。
-
合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。
-
一旦合并结束,老的段被删除
- 新的段被刷新(flush)到了磁盘。 写入一个包含新段且排除旧的和较小的段的新提交点。
- 新的段被打开用来搜索。
- 老的段被删除。
合并大的段需要消耗大量的 I/O 和 CPU 资源,如果任其发展会影响搜索性能。Elasticsearch 在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。
ES 优化
硬件选择
磁盘在现代服务器上通常都是瓶颈。Elasticsearch 重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。这里有一些优化磁盘 I/O 的技巧:
- 使用 SSD
-
使用 RAID 0。RAID 0将N块硬盘上选择合理的带区来创建带区集。其原理是将类似于显示器隔行扫描,将数据分割成不同条带(Stripe)分散写入到所有的硬盘中同时进行读写。多块硬盘的并行操作使同一时间内磁盘读写的速度提升N倍。代价显然就是当一块硬盘故障时整个就故障了。不要使用镜像或者奇偶校验 RAID 因为副本已经提供了这个功能。
- 另外,使用多块硬盘,并允许 Elasticsearch 通过多个 path.data 目录配置把数据条带化分配到它们上面,并行操作。
-
不要使用远程挂载的存储,比如 NFS 或者 SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰的。
分片策略
路由选择
当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来:
shard = hash(routing) % number_of_primary_shards routing
默认值是文档的 id,也可以采用自定义值,比如用户 id
写入速度优化
ES 的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。实际使用时, 我们需要根据公司要求,进行偏向性的优化。
针对于搜索性能要求不高,但是对写入要求较高的场景,我们需要尽可能的选择恰当写优化策略。
综合来说,可以考虑以下几个方面来提升写索引的性能:
-
加大 Translog Flush ,目的是降低 Iops(磁盘每秒输入输出)、Writeblock(写锁,少写少用锁)。
Flush 的主要目的是把文件缓存系统中的段持久化到硬盘,当 Translog 的数据量达到 512MB 或者 30 分钟时,会触发一次 Flush。 index.translog.flush_threshold_size 参数的默认值是 512MB,我们进行修改。 增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以我们需要为操作系统 的文件缓存系统留下足够的空间。但这这样做可以减少落盘次数。
-
增加 Index Refresh 间隔,减少 Refresh 次数,目的是减少 Segment Merge 的次数。
Lucene 在新增数据时,采用了延迟写入的策略,默认情况下索引的 refresh_interval 为 1 秒。 Lucene 将待写入的数据先写到内存中,超过 1 秒(默认)时就会触发一次 Refresh, 然后 Refresh 会把内存中的的数据刷新到操作系统的文件缓存系统中。 如果我们对搜索的实效性要求不高,可以将 Refresh 周期延长,例如 30 秒。 这样还可以有效地减少段刷新次数,但这同时意味着需要消耗更多的 Heap 内存。
Refresh 间隔如果太小,就会频繁 refresh 到 系统缓存,这样带来的是 segment 就会很多,占用资源,效率下降。
-
调整 Bulk 线程池和队列(批量处理越多,越快)。
通用的策略如下:Bulk 默认设置批量提交的数据量不能超过 100M。数据条数一般是 根据文档的大小和服务器性能而定的,但是单次批处理的数据大小应从 5MB~15MB 逐渐 增加,当性能没有提升时,把这个数据量作为最大值。
-
优化节点间的任务分布。
-
优化 Lucene 层的索引建立,目的是降低 CPU 及 IO。
-
优化存储设备
-
根据实际情况,合理使用合并
Lucene 以段的形式存储数据。当有新的数据写入索引时,Lucene 就会自动创建一个新的段。 随着数据量的变化,段的数量会越来越多,消耗的多文件句柄数及 CPU 就越多,查询 效率就会下降。 由于 Lucene 段合并的计算量庞大,会消耗大量的 I/O 和 CPU 资源,所以 ES 默认采用较保守的策略,让后台定期进行段合并。
-
减少副本的数量
ES 为了保证集群的可用性,提供了 Replicas(副本)支持,然而每个副本也会执行分 析、索引及可能的合并过程,所以 Replicas 的数量会严重影响写索引的效率。 当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越 慢。 如 果 我 们 需 要 大 批 量 进 行 写 入 操 作 , 可 以 先 禁 止 Replica 复 制 , 设 置 index.number_of_replicas: 0 关闭副本。在写入完成后,Replica 修改回正常的状态。
内存设置
ES 默认安装后设置的内存是 1GB,对于任何一个现实业务来说,这个设置都太小了。 如果是通过解压安装的 ES,则在 ES 安装文件中包含一个 jvm.option 文件,添加如下命 令来设置 ES 的堆大小,Xms 表示堆的初始大小,Xmx 表示可分配的最大内存,都是 1GB。
确保 Xmx 和 Xms 的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完 堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。
假设你有一个 64G 内存的机器,按照正常思维思考,你可能会认为把 64G 内存都给 ES 比较好,但现实是这样吗, 越大越好?虽然内存对 ES 来说是非常重要的,但是答案 是否定的! 因为 ES 堆内存的分配需要满足以下两个原则:
-
不要超过物理内存的 50%:
Lucene 的设计目的是把底层 OS 里的数据缓存到内存中。 Lucene 的段是分别存储到单个文件中的,这些文件都是不会变化的,所以很利于缓存,同时操作系 统也会把这些段文件缓存起来,以便更快的访问。 如果我们设置的堆内存过大,Lucene 可用的内存将会减少,就会严重影响降低 Lucene 的全文本查 询性能。
-
堆内存的大小最好不要超过 32GB:
在 Java 中,所有对象都分配在堆上,然后有一个 Klass Pointer 指 针指向它的类元数据。 这个指针在 64 位的操作系统上为 64 位,64 位的操作系统可以使用更多的内存(2^64)。在 32 位 的系统上为 32 位,32 位的操作系统的最大寻址空间为 4GB(2^32)。 但是 64 位的指针意味着更大的浪费,因为你的指针本身大了。浪费内存不算,更糟糕的是,更大的 指针在主内存和缓存器(例如 LLC, L1 等)之间移动数据的时候,会占用更多的带宽。 最终我们都会采用 31 G 设置。
假设你有个机器有 128 GB 的内存,你可以创建两个节点,每个节点内存分配不超过 32 GB。 也就是说 不超过 64 GB 内存给 ES 的堆内存,剩下的超过 64 GB 的内存给 Lucene