Elasticsearch注意事项
客观地说,Elasticsearch确实是个好工具,毕竟它在分布式开源搜索和分析引擎中处于领先地位。不过它也存在不少陷阱,以至于身边几个朋友经常抱怨Elasticsearch有多么不好用。
对于Elasticsearch而言,想掌握好这门技术,除需要对它的用法了如指掌 外 , 还 需 要 对 技 术 中 的 各 种 陷 阱 了 然 于 心 。这 里 总 结 一 些 关 于Elasticsearch的使用要点。
1)如何使用Elasticsearch设计表结构?
2)Elasticsearch的存储结构。
3)Elasticsearch如何修改表结构?
4)Elasticsearch的准实时性。
5)Elasticsearch可能丢数据。
6)Elasticsearch分页。
如果你用过Elasticsearch,这一节学习起来会更容易一些。没使用过也没关系,通过这些要点的展开,也能了解Elasticsearch的基本原理和Elasticsearch的一些陷阱。
很多人在用Elasticsearch时的第一个疑问就是:它跟常用的关系型数据库有什么不同?
如何使用Elasticsearch设计表结构
Elasticsearch是基于索引的设计,它无法像MySQL那样使用join查询,所以查询数据时需要把每条主数据及关联子表的数据全部整合在一条记录中。
比如,MySQL中有一个订单数据,使用Elasticsearch查询时,会把每条主数据及关联子表数据全部整合,见表2-3。
表2-3 订单数据结构
但是,使用Elasticsearch存储数据时并不会设计多个表,而是将所有表的相关字段数据汇集在一个Document中,即一个完整的文档结构,类似下面的示例代码(此处使用JSON):
看到这里,是不是很疑惑:为什么把所有表汇聚在一个Document中,而不是 设 计 成 多 个 表 ?为 什 么 Elasticsearch 不 需 要 关 联 查 询 ?这 就 涉 及Elasticsearch的存储结构原理相关知识了。
Elasticsearch的存储结构
Elasticsearch是一个分布式的查询系统,它的每一个节点都是一个基于Lucene的查询引擎。下面通过与MySQL的概念对比来更快地理解Lucene。
1.Lucene和MySQL的概念对比
Lucene是一个索引系统,此处把Lucene与MySQL的一些概念做简单对照,见表2-4。
表2-4 Lucene与MySQL概念对照
通过表2-4中相关概念的对比,就能比较容易地理解Lucene中每个概念的作用了。
到这里可能还有一个疑问:Lucene的索引(Index)到底是什么?下面继续介绍。
2.无结构文档的倒排索引
实际上,Lucene使用的是倒排索引的结构,具体是什么意思呢?
先举个例子,假如有一些无结构的文档,见表2-5。
表2-5 无结构文档
简单倒排索引后显示的结果见表2-6。
表2-6 倒排索引
可以发现,无结构的文档经过简单的倒排索引后,字典表主要存放关键字,而倒排表存放该关键字所在的文档ID。
这个例子已经简单展示了文档数据的倒排索引结构,但是表数据往往是有结构的,而不是一篇篇文章。如果一个文档有结构,那该怎么办?
3.有结构文档的倒排索引
再来举一个更复杂的例子。比如每个Doc都有多个Field,Field有不同的值(包含不同的Term),见表2-7。
表2-7 有结构文档的倒排索引
倒排表见表2-8~表2-10。
表2-8 性别倒排索引
表2-9 年龄倒排索引
表2-10 武功倒排索引
也就是说,有结构的文档经过倒排索引后,字段中的每个值都是一个关键字,存放在Term Dictionary(词汇表)中,且每个关键字都有对应地址指向所在文档。
以上例子只是一个参考,实际上不管是字典表还是倒排表都是非常复杂的数据结构(这里先讨论到这个深度)。了解了Elasticsearch的存储数据结构,就能更好地理解Elasticsearch的表结构设计思路了。
下面再讨论一下:Elasticsearch的Document如何定义结构和字段格式(类似MySQL的表结构)?
4.Elasticsearch的Document如何定义结构和字段格式?
前面讲解了Elasticsearch的存储结构,从其基于索引的设计来看,设计Elasticsearch Document结构时,并不需要像MySQL那样关联表,而是把所有相关数据汇集在一个Document中。接下来看个例子。
直接将2.4.1节中订单的JSON文档转成一个Elasticsearch文档(这里需要注意,SQL中的子表数据在Elasticsearch中需要以嵌入式对象的格式存储),代码示例如下:
至此,大家已经了解了Elasticsearch表结构的设计。在实际业务中,往往会遇到这种情况:主数据修改了表结构,Elasticsearch也要求修改文档结构,这时该怎么办?这就涉及下面要讨论的另一个问题——如何修改表结构。
Elasticsearch如何修改表结构
在实际业务中,如果想增加新的字段,Elasticsearch可以支持直接添加,但如果想修改字段类型或者改名,Elasticsearch官方文档中有相关的介绍可以参考:
Except for supported mapping parameters, you can't change the mapping or field type of an existing field.Changing an existing field could invalidate data that's already Indexed.
If you need to change the mapping of a field in other indices,create a new index with the correct mapping and reindex your data into that index.
Renaming a field would invalidate data already indexed under the old field name.Instead, add an alias field to create an alternate field name.
其中要点解释如下。因为修改字段的类型会导致索引失效,所以Elasticsearch不支持修改原来字段的类型。
如 果 想 修 改 字 段 的 映 射 , 首 先 需 要 新 建 一 个 索 引 , 然 后 使 用Elasticsearch的reindex功能将旧索引复制到新索引中。
那么什么是reindex呢?reindex是Elasticsearch自带的API,在实际代码中查看一下调用示例就能明白它的功用了。
不过,直接重命名字段时,使用reindex功能会导致原来保存的旧字段名的索引数据失效,这种情况该如何解决?可以使用alias索引功能,代码示例如下:
说到修改表结构,使用普通MySQL时,并不建议直接修改字段的类型、改名或删除字段。因为每次更新版本时,都要做好版本回滚的准备,所以设计每个版本对应的数据库时,要尽量兼容前面版本的代码。
因Elasticsearch的结构基于MySQL而设计,两者之间存在对应关系,所以也不建议直接修改Elasticsearch的表结构。
那如果确实有修改的需求呢?一般而言,会先保留旧的字段,然后直接添加并使用新的字段,直到新版本的代码全部稳定运行后,再找机会清理旧的不用的字段,即分成两个版本完成修改需求。
介绍完如何修改表结构,再继续讲解最后几个要点:使用Elasticsearch时的一些陷阱。
陷阱一:Elasticsearch是准实时的吗
当 更 新 数 据 至 Elasticsearch 且 返 回 成 功 提 示 时 , 会 发 现 通 过Elasticsearch查询返回的数据仍然不是最新的,背后的原因究竟是什么?
因数据索引的整个过程涉及Elasticsearch的Shard(分片),以及LuceneIndex、Segment、Document三者之间的关系等知识点,所以有必要先对这些内容进行说明。
Elasticsearch的一个Shard(Elasticsearch分片的具体介绍可参考官方文 档 ) 就 是 一 个 Lucene Index ( 索 引 ) , 每 一 个 Lucene Index 由 多 个Segment(段)构成,即Lucene Index的子集就是Segment,如图2-9所示。
图2-9 分片(Shard)结构图
Lucene Index、Segment、Document(Doc)三者之间的关系如图2-10所示。
• 图2-10 Index、Segment、Document三者之间的关系
通过图2-10可以知道,一个Lucene Index可以存放多个Segment,而每个Segment又可以存放多个Document。
掌握了以上基础知识点,接下来就进入正题——数据索引的过程详解。
1)当新的Document被创建时,数据首先会存放到新的Segment中,同时旧的Document会被删除,并在原来的Segment上标记一个删除标识。当Document被更新时,旧版Document会被标识为删除,并将新版Document存放在新的Segment中。
2)Shard收到写请求时,请求会被写入Translog中,然后Document被存放在Memory Buffer(内存缓冲区)中,最终Translog保存所有修改记录,如图2-11所示。
• 图2-11 写请求处理示意图
3)每隔1秒(默认设置),Refresh(刷新)操作被执行一次,且MemoryBuffer中的数据会被写入一个Segment,并存放在File System Cache(文件系统缓存)中,这时新的数据就可以被搜索到了,如图2-12所示。
• 图2-12 Refresh操作示意图
通过以上数据索引过程的说明,可以发现Elasticsearch并不是实时的,而是有1秒延时。延时问题的解决方案在前面介绍过,提示用户查询的数据会有一定延时即可。
接下来介绍第二个陷阱。
陷阱二:Elasticsearch宕机恢复后,数据丢失
上一小节中提及每隔1秒(根据配置)Memory Buffer中的数据会被写入Segment中,此时这部分数据可被用户搜索到,但没有持久化,一旦系统宕机,数据就会丢失,如图2-12最右边的桶所示。
如何防止数据丢失呢?使用Lucene中的Commit操作就能轻松解决这个问题。
Commit操作方法:先将多个Segment合并保存到磁盘中,再将灰色的桶变成图2-12中蓝色的桶。
不过,使用Commit操作存在一点不足:耗费I/O,从而引发Elasticsearch在Commit之前宕机的问题。一旦系统在Translog执行fsync函数之前宕机,数据也会直接丢失,如何保证Elasticsearch数据的完整性便成了亟待解决的问题。
遇到这种情况,通过Translog解决即可,因为Translog中的数据不会直接保存在磁盘中,只有使用fsync函数后才会保存。具体实现方式有两种。
1)将index.translog.durability设置成request,其缺点就是耗费资源,性能差一些,如果发现启用这个配置后系统运行得不错,采用这种方式即可。
2)将index.translog.durability设置为fsync,每次Elasticsearch宕机启动后,先将主数据和Elasticsearch数据进行对比,再将Elasticsearch缺失的数据找出来。
Tips
Translog 何 时 会 执 行 fsync? 当 index.translog.durability 设 置 为request后,每个请求都会执行fsync,不过这样会影响Elasticsearch的性能。这时可以把index.translog.durability设置成fsync,那么每隔时间 index.translog.sync_interval,每个请求才会执行一次fsync。
陷阱三:分页越深,查询效率越低
Elasticsearch分页这个陷阱的出现,与Elasticsearch读操作请求的处理流程密切关联,如图2-13所示。Elasticsearch的读操作流程主要分为两个阶段:Query Phase、FetchPhase。
1)Query Phase:协调的节点先把请求分发到所有分片,然后每个分片在本地查询后建一个结果集队列,并将命令中的Document ID以及搜索分数存放在队列中,再返回给协调节点,最后协调节点会建一个全局队列,归并收到的所有结果集并进行全局排序。
Tips
在Elasticsearch查询过程中,如果search方法带有from和size参数,Elasticsearch集群需要给协调节点返回分片数*(from size)条数据,然后在单机上进行排序,最后给客户端返回size大小的数据。比如客户端请求10条数据,有3个分片,那么每个分片会返回10条数据,协调节点最后会归并30条数据,但最终只返回10条数据给客户端。
图2-13 Elasticsearch读操作示意图
2)Fetch Phase:协调节点先根据结果集里的Document ID向所有分片获取完整的Document,然后所有分片返回完整的Document给协调节点,最后协调节点将结果返回给客户端。
比如有5个分片,需要查询排序序号从10000到10010(from=10000,size=10)的结果,每个分片到底返回多少数据给协调节点计算呢?不是10条,是10010条。也就是说,协调节点需要在内存中计算10010*5=50050条记录,所以在系统使用中,用户分页越深查询速度会越慢,也就是说分页并不是越多越好。
那如何更好地解决Elasticsearch分页问题呢?为了控制性能,可以使用Elas-ticsearch中的max_result_window进行配置,这个数据默认为10000,当from size>max result window时,Elasticsearch将返回错误。
这个配置就是要控制用户翻页不能太深,而这在现实场景中用户也能接受,本项目的方案就采用了这种设计方式。如果用户确实有深度翻页的需求,使用Elasticsearch中search_after的功能也能解决,只是无法实现跳页了。
举一个例子,查询结果按照订单总金额分页,上一页最后一个订单的总金额total_amount是10,那么下一页的查询示例代码如下:
这个search_after里的值,就是上次查询结果排序字段的结果值。
至此,Elasticsearch的一些要点就介绍完了。MQ也有一些要点,比如确保时序、确保重试、确保消息重复消费不会影响业务,以及确保消息不丢失等,后续各章节会有相应的场景描述,这里就不再展开了。
小结
查询分离这个解决方案虽然能解决一些问题,但也要认识到它的不足。
1)使用Elasticsearch存储查询数据时,就要接受上面列出的一些局限性:有一定延时,深度分页不能自由跳页,会有丢数据的可能性。
2)主数据量越来越大后,写操作还是慢,到时还是会出问题。比如这里的工单数据,虽然已经去掉了所有外键,但是当数据量上亿的时候,插入还是会有问题。
3)主数据和查询数据不一致时,如果业务逻辑需要查询数据保持一致性呢?这里的查询数据同步到最新数据会有一定的延时,大约为2秒。某些业务场景下用户可能无法接受这个延时,特别是跟钱有关的场景。
架构“没有银弹”,不能期望一个解决方案既能覆盖所有的问题,还能实现最小的成本损耗。
如果碰到一个场景不能接受上面某个或某些不足时,该怎么解决?接着看后面的章节。
本文给大家讲解的内容是Elasticsearch注意事项
- 下篇文章给大家讲解的内容是分库分表:拆分存储的技术选型
- 觉得文章不错的朋友可以转发此文关注小编;
- 感谢大家的支持
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。