⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 后端研究所 「大白斯基」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

Part1 获取信息的诉求

我们正处于一个信息过载的时代,有用的信息、无用的信息夹杂在一起。

我们有从海量信息中获取数据的诉求,搜索和推荐就是两个有效的工具。

  • 搜索:想好要什么,然后去查询、再从众多结果中获取对你有用的。
  • 推荐:根据你的用户画像等信息,来主动推送你可能感兴趣的信息。

ChatGPT本质上说是一种更加高效准确的获取信息的渠道,至少对于我来说,有效减少了从传统搜索引擎众多结果中过滤有效信息的成本。

Part2 让搜索飞入寻常百姓家

曾几何时,搜索引擎也是技术壁垒较深的一个领域,像谷歌、百度等老牌网页搜索引擎公司,就靠着搜索和广告赚得盆满钵满。

时间拉到现在,各类app层出不穷,搜索不再均限于网页,更多的是app内的垂直领域检索,像小红书、知乎、淘宝、拼多多都是如此。

再具体到一些我们日常的业务场景,也同样有检索诉求:筛选检索和模糊检索。

以常用的找房app为例,可以输入商圈名称、小区名称,当然这些都不需要非常具体,检索框就可以进行提示,同时还可以根据具体的条件做筛选,比如面积、朝向、总价等。

这些都可以用es来实现,再有上层的排序逻辑,基本上就可以实现面向用户的检索功能了。

Part3 Lucene和ElasticSearch

在聊es之前,就必须要提lucene,如果把es看做是豪车,lucene则是这辆豪车的发动机,真可谓是es的核心部件。

1 Lucene的诞生

Luene是一款高性能、可扩展的信息检索库,用于完成文档元信息、文档内容等搜索功能。

用户可以使用Lucene来快速构建搜索服务,如文件搜索、网页搜索等,它是一个索引和搜索库,不包含爬取和HTML解析功能。

1985年 Doug Cutting毕业于美国斯坦福大学,在1999年编写了Lucene,他是一位资深的全文索引及检索专家,曾经是V-Twin搜索引擎的主要开发者,后来在Excite担任高级系统架构设计师,目前从事于一些互联网底层架构的研究。

值得一提的是,Doug Cutting还是大名鼎鼎的Hadoop之父,大牛果然是高产。

2 elasticsearch的诞生

有了Lucene之后,那么es又是怎么诞生的呢?这就要提到一个曾经的待业青年 Shay Banon。

当年他还是一个待业工程师,跟随自己的新婚妻子来到伦敦,妻子想在伦敦学习做一名厨师,而自己则想为妻子开发一个方便搜索菜谱的应用,所以才接触到 Lucene。

直接使用 Lucene 构建搜索有很多问题,包含大量重复性的工作,所以 Shay Banon 便在 Lucene 的基础上不断地进行抽象,让 Java 程序嵌入搜索变得更容易,经过一段时间的打磨便诞生了他的第一个开源作品Compass。

之后,他找到了一份面对高性能分布式开发环境的新工作,在工作中他渐渐发现越来越需要一个易用的、高性能、实时、分布式搜索服务,于是决定重写 Compass,将它从一个库打造成了一个独立的 server,并创建了开源项目。

第一个公开版本出现在 2010 年 2 月,在那之后 Elasticsearch 已经成为 Github 上最受欢迎的项目之一。

后来和几个志同道合的技术狂人一起把es做大做强,最后敲钟上市,从待业青年成为了亿万富翁,还真是励志!

Part4 ElasticSearch架构原理

我们前面提到,es是基于Lucene打造的开源检索组件,Lucene只是一个裸信息检索库,而es要做的就是解决Lucene到业务场景的最后一公里问题。

当我们尝试去学习一个组件时,不妨把我们自己当做组件的研发者,抱着去做一款产品的思维来看,或许可以更清晰。

在聊es的架构和原理之前,我们也反客为主去思考下,es的目标有哪些:

  • 简单的交互模式、支持多种语言
  • 支持海量数据、高效检索效率
  • 支持实时/准实时地低时延检索
  • 支持高并发&高可用场景

3 分布式系统的引入

要解决海量数据检索、高并发、高可用等问题,就必须要引入分布式系统,集群模式下吞吐量和稳定性都能有保证。

在es集群中每台机器从不同角度看有不同的角色,其中重要的几个包括:

  • Master Node 负责监控和协调工作,保证整个集群的稳定性,管事的节点
  • Work Node 负责数据的写入和读取,也就是干活的节点
  • Coordinating Node 协调节点,负责请求的路由分发等,每个节点都可以是协调节点,同时也可以只负责协调,不做具体的数据处理工作
  • Master-eligible node 候选主节点,作为Master节点的接班人之一,可以参与投票和竞选新的Master节点

再细分的角色还有很多,在此不展开了,实际上分布式系统中各个节点的角色和要做的事情,基本都差不多,和人类社会运行中的各个角色都非常相似。

引入分布式系统之后,就会面临很多新的问题:网络延迟、消息丢失、集群脑裂、故障容错和恢复、一致性、共识问题、选举问题、等。

分布式系统也是一把双刃剑,但是其带来的好处遇大于问题,在分布式基础理论和基础算法的加持下,让分布式系统应用于生产实践成为了现实。

4 高可用和高并发

基于分布式系统,es存储的数据会进行分割和备份,也就是我们常说的分片和副本。

  • 分片是为了提高并发能力,化整为零,并行工作
  • 副本是为了提高可用能力,防止某台机器挂掉,数据丢失

如图所示,Data-A和Data-B分割为两个分片shard,每个shard有1个主分片2个副本分片,这12块数据被交错无重复地分配到4台机器上:

这种分配模式可以有效降低机器故障带来的数据丢失风险,副本数增加也提升了读的并发量。

5 数据写入

路由过程

ES的任意节点都可以作为协调节点(coordinating node)接受请求,当协调节点接受到请求后进行一系列处理,然后通过_routing字段找到对应的主分片primary shard,并将请求转发给primary shard。

一种常用的路由算法是:

shard_id = hash(doc_id)%num_of_primary_shard

primary shard完成写入后,将写入并发发送给各replica, raplica执行写入操作后返回给primary shard, primary shard再将请求返回给协调节点。

主分片primary shard与副本分片replica之间的同步,有两种模式:

  • 同步复制,需要所有副本分片全部写入才可以
  • 异步复制,只有一半以上的副本完成写入即可

倒排索引

倒排索引(Inverted Index)是通过value找key,这是全文检索的关键,但是大文本数据使用B+树作为底层存储容易造成树深度增加,IO次数增加等问题,因此es的倒排索引采用了另外一种结构:

  • Term(单词):⼀段⽂本经过分析器分析以后就会输出⼀串单词,这⼀个⼀个的就叫做Term
  • Term Dictionary(单词字典):⾥⾯维护的是Term,可以理解为Term的集合
  • Term Index(单词索引):为了更快的找到某个单词,为单词建⽴索引,如果term太多,term dictionary也会很⼤,放内存不现实。
  • Posting List(倒排列表):倒排列表记录了出现过某个单词的所有⽂档的⽂档列表及单词在该⽂档中出现的位置信息,每条记录称为⼀个倒排项(Posting)。根据倒排列表,即可获知哪些⽂档包含某个单词。

写入细节

说明写入细节之前,有几个概念需要对齐:

  • 两种介质:内存和磁盘

es写入的数据最先放到内存中,再做一系列的操作写到磁盘中

  • 两个内存区域:buffer和cache

内存缓冲区(memory buffer)和文件系统缓存区(file system cache),这是两种的内存区域,目的是为了提高写入的速度,作为写入磁盘前的缓冲地带,但是buffer和cache并不是同一个东西。

  • 两个动作:refresh和flush

refresh就是将buffer中的数据写入cache的过程,flush就是将内存中的数据刷到磁盘的过程。

接下来,我们来看下写入的详细过程:

  1. 写入数据时,会先写进内存缓冲区memeory buffer中,此时数据还不能被检索。
  2. 为了防止宕机造成数据丢失保证可靠存储,在每次写入数据成功后,将此操作写到translog事务日志中,translog也位于内存中。
  3. 写⼊translog的数据是要持续去落盘的,如果对可靠性要求不是很⾼,也可以设置异步落盘提⾼性能,可由配置 index.translog.durability 和 index.translog.sync_interval 控制。

  1. 在buffer中的数据不断增长,es提供了⼀个refresh操作,会定时地调⽤lucene的api,将位于buffer中的数据生成segment文件,segment文件仍然位于内存中,只不过从内存buffer换到了文件系统缓存cache中。
  2. refresh操作的时间间隔由 refresh_interval 参数控制,默认为1s, 还可以在写⼊请求中带上refresh表示写⼊后⽴即refresh,refresh完成之后就会清空buffer中的数据,但是translog在segment没有刷入磁盘前是不会被清空的。
  3. refresh期间可能会产⽣⼤量的⼩segment,es会运⾏⼀个任务检测当前磁盘中的segment,对符合条件的segment进⾏合并操作,减少lucene中的segment个数,提⾼查询速度,降低负载。

  1. es持续运行过程中会有更多的doc被添加到内存缓冲区和追加到事务日志,期间buffer到cache的fresh动作持续进行,同时translog也逐渐变大直至到了触发translog提交的点,也就是commit point。
  2. 执行一个提交的行为在 es 被称作一次 flush,每隔设置的时间自动刷新flush或者在 translog 太大的时候也会刷入磁盘。
// 设置控制内存缓冲区刷新到磁盘的频率
index.translog.sync_interval = 5s
index.translog.flush_threshold_size = 512MB
index.translog.flush_threshold_period = 30min

  1. 在 flush 之后,文件缓冲区cache中的segment文件被全量提交,并且translog事务日志被清空,本轮的工作基本结束,创建新的translog迎接下一轮的新数据。

再对整个过程做下总结:

  • 数据被写入buffer是不可被搜索的,期间不断的执行refresh操作将buffer中的数据生成segment文件写入文件系统缓存cache中,此时就可以被检索了。
  • 但是此时的数据仍然驻留在内存中,有丢失风险,为此es设置了translog来记录数据执行操作日志,发生故障时做数据恢复用
  • translog的数据也是在内存中,但是默认每5秒会刷入磁盘,也就是最多丢5秒的数据,在translog到达设定时间或者大小,就会执行commit操作,此时将驻留在内存buffer和cache的数据全部flush到磁盘,从而完成数据的持久化。

在网上看到了另外一张图,更清晰一些:

6 数据检索

es的Search操作分为两个阶段:query then fetch。

需要两阶段完成搜索的原因是:在查询时不知道文档位于哪个分片,因此索引的所有分片都要参与搜索,然后协调节点将结果合并,在根据文档ID获取文档内容。

Query阶段

  • 客户端向集群中的某个节点发送Search请求,该节点就作为本次请求的协调节点;
  • 协调节点将查询请求转发到索引的每个主分片或者副分片中
  • 每个分片在本地执行查询,并使用本地的Term/Document Frequency信息进行打分,添加结果到大小为from+size的本地有序优先队列中
  • 每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表

Fetch阶段

  • 协调节点向相关的节点发送GET请求
  • 分片所在节点向协调节点返回数据
  • 协调阶段等待所有的文档被取得,然后返回给客户端
文章目录
  1. 1. Part1 获取信息的诉求
  2. 2. Part2 让搜索飞入寻常百姓家
  3. 3. Part3 Lucene和ElasticSearch
    1. 3.1. 1 Lucene的诞生
    2. 3.2. 2 elasticsearch的诞生
  4. 4. Part4 ElasticSearch架构原理
    1. 4.1. 3 分布式系统的引入
    2. 4.2. 4 高可用和高并发
    3. 4.3. 5 数据写入
      1. 4.3.1. 路由过程
      2. 4.3.2. 倒排索引
      3. 4.3.3. 写入细节
    4. 4.4. 6 数据检索
      1. 4.4.1. Query阶段
      2. 4.4.2. Fetch阶段