北京海淀公司网站icp备案,网站页面设计制作费,三网合一网站建设是指什么,官方app下载安装前言
QBM、MFS的试题检索、试题查重、公式转换映射等业务场景以及XOP题库广泛使用搜索中间件#xff0c;业务场景有着数据量大、对内容搜索性能要求高等特点#xff0c;其中XOP题库数据量更是接近1亿#xff0c;对检索性能以及召回率要求高。目前QBM、MFS使用的搜索中间件是…前言
QBM、MFS的试题检索、试题查重、公式转换映射等业务场景以及XOP题库广泛使用搜索中间件业务场景有着数据量大、对内容搜索性能要求高等特点其中XOP题库数据量更是接近1亿对检索性能以及召回率要求高。目前QBM、MFS使用的搜索中间件是Solr后续需要升级为ES。
看的书是《ElasticSearch源码解读与优化实战》的前半部分与这篇博客部分内容重合主要是ES的一些工程模块分布式集群的一些理论知识。Lucene的部分知识主要来源一些写的比较全面的博客Lucene涉及的数据结构与算法比较复杂其中涉及的如FST前缀字典、列式存储数据压缩、ES相关的分布式Paxos算法细节等都是很复杂还是值得思考研究下。
//TODO 该博客主要使用ES、Lucene过程一些小计以及一些原理分析初学原理涉及的深度难免不够不过后续随着学习内容持续更新ing…
目录
概述实践原理分析 搜索引擎流程Lucene相关原理ES相关原理
一、概述
ES是什么
非关系型、搜索引擎、近实时搜索与分析、高可用、天然分布式、横向可扩展。
ElasticSearch是一款非常强大的、基于Lucene的开源搜索及分析引擎它是一个实时的分布式搜索分析引擎它能让你以前所未有的速度和规模去探索你的数据。属于NoSQL文档性DB的一种内容检索性能是最大的优势。
实时搜索实时搜索Real-time Search很好理解对于一个数据库系统执行插入以后立刻就能搜索到刚刚插入到数据。而近实时Near Real-time所谓“近”也就是说比实时要慢一点点。像常用的MySQL等关系型数据库不能称之为实时搜索数据库MySQL可以配置为提供较低的延迟和更高的实时性能。但是MySQL的实时性取决于多个因素包括硬件性能、数据库设计、查询优化和负载等因素。
全文搜索属于最常见的需求开源的 Elasticsearch 以下简称 Elastic是目前全文搜索引擎的首选。 它可以快速地储存、搜索和分析海量数据。维基百科、Stack Overflow、Github 都采用它。 图源https://db-engines.com/en/ranking
主要功能 1海量数据的分布式存储以及集群管理达到了服务与数据的高可用以及水平扩展 2近实时搜索性能卓越。对结构化、全文、地理位置等类型数据的处理 3海量数据的近实时分析聚合功能
应用场景 1网站搜索、垂直搜索、代码搜索 2日志管理与分析、安全指标监控、应用性能监控
常用非结构化数据存储中间件区别ES、Solr、MongoDB都属于NoSQL的家族的一员
ES和MongoDB区别它们有不同的设计目标和用例因此在许多方面存在区别MongoDB 在某些查询场景下表现很好但对于全文搜索和实时分析来说性能通常不如ES。如果你需要处理大量文本数据并进行实时搜索和分析ES 可能更适合。如果你的应用需要存储和查询半结构化或非结构化的文档数据MongoDB可能更合适。有时候这两个数据库也可以组合使用以满足不同方面的需求。ES和Solr的区别都是建立在Lucene库上的提供RESTful的API用于CRUD以及拓展其他高级特性语法类似。 ES 通常更适合用于实时搜索、日志和指标分析、全文搜索等需要高度动态性和实时性能的应用。Solr 更适合处理传统的文档检索和结构化数据分析例如图书馆目录、商品搜索等。 ES 在分布式性能方面表现出色天生支持分片和复制易于横向扩展。Solr 也支持分布式部署但需要更多的手动配置和管理。Solr需要配合Zookeeper使用ES自身带有分布式系统管理功能。
参考
ES和MongoDB对比https://leriou.github.io/2019-01-09-mongodb-compareto-elasticsearch/ES和Solr对比https://zhuanlan.zhihu.com/p/85362497ChatGPT
二、实践
1、安装ES和Kibana
ES下载https://www.elastic.co/cn/downloads/elasticsearch
ES在使用容器安装https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html
练习测试直接使用docker安装8.9.1 版本的ES、Kibana。建议不要安装这么新的否则会有很多坑
# 一、安装es和kibana# 拉取es镜像
docker pull docker.elastic.co/elasticsearch/elasticsearch:8.9.1# 创建一个新的Docker网络通过创建自定义的Docker网络你可以轻松地管理容器之间的通信并根据需要隔离它们。这在多容器应用程序和微服务架构中特别有用。
docker network create elastic-demo# 启动es容器
# 使用-m标志设置容器的内存限制。这样就不 需要手动设置JVM大小。
docker run --name es01 --net elastic-demo -p 9200:9200 -it -m 1GB docker.elastic.co/elasticsearch/elasticsearch:8.9.1
# 该命令打印elastic用户密码和Kibana的注册令牌。# 从新生成为elastic用户设置密码elastic
docker exec -it es01 /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic -i elastic
docker exec -it es01 /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana
# kibana的token等会要用eyJ2ZXIiOiI4LjkuMSIsImFkciI6WyIxNzIuMTguMC4yOjkyMDAiXSwiZmdyIjoiNTU0ZDQyY2Y3OGZmZTUwYmEzZWExZjk2ZTljOWM4YmQyOTIwYjc2OTA0ZWY4OWEwZWI5YzkwNDU4YjUzNjNmNyIsImtleSI6InRQRTFXb29CRWF5NzJYQmM1cTFqOkNUU2tOOHF0VDc2b2xLbHRwNWtLTEEifQ# 启动es报错1https://discuss.elastic.co/t/elasticsearch-bootstrap-checks-failing/302442
# 需要设置 vm.max_map_count 至少 262144
# 编辑vm.max_map_count内核设置必须至少设置为262144以供生产使用。# 查看
grep vm.max_map_count /etc/sysctl.conf
# 临时设置
# 永久设置要永久更改vm.max_map_count设置的值请更新 /etc/sysctl.conf的值。
sysctl -w vm.max_map_count262144# 二、本地测试es# 我们建议将elastic密码作为环境变量存储在shell中。范例
export ELASTIC_PASSWORDelastic
# 将http_ca.crt SSL证书从容器复制到本地计算机。
docker cp es01:/usr/share/elasticsearch/config/certs/http_ca.crt .
curl --cacert http_ca.crt -u elastic:$ELASTIC_PASSWORD https://localhost:9200# 输出
[roothecs-148865 ~]# curl --cacert http_ca.crt -u elastic:$ELASTIC_PASSWORD https://localhost:9200
{name : 7e48b6d68e30,cluster_name : docker-cluster,cluster_uuid : zak41dJ-Q6qb4DPckXn7fQ,version : {number : 8.9.1,build_flavor : default,build_type : docker,build_hash : a813d015ef1826148d9d389bd1c0d781c6e349f0,build_date : 2023-08-10T05:02:32.517455352Z,build_snapshot : false,lucene_version : 9.7.0,minimum_wire_compatibility_version : 7.17.0,minimum_index_compatibility_version : 7.0.0},tagline : You Know, for Search
}# 关闭es的ssl证书校验否则整合springboot使用需要证书很麻烦修改elasticsearch.yml文件设置 xpack.security.enabled: false# 将docker文件复制到本地修改完再上传docker cp es01:/usr/share/elasticsearch/config/elasticsearch.yml /home/docker/mydata/elastic-search/elasticsearch.yml
docker cp /home/docker/mydata/elastic-search/elasticsearch.yml es01:/usr/share/elasticsearch/config/elasticsearch.yml# 修改完es发现kibana连接不上了kibana.yml也需要修改
docker cp kibana:/usr/share/kibana/config/kibana.yml /home/docker/mydata/elastic-search/kibana.yml
docker cp /home/docker/mydata/elastic-search/kibana.yml kibana:/usr/share/kibana/config/kibana.yml # 三、安装启动kibana
docker pull docker.elastic.co/kibana/kibana:8.9.1
docker run --name kibana --net elastic-demo -p 5601:5601 docker.elastic.co/kibana/kibana:8.9.1# 四、访问kibana面板
# http://0.0.0.0:5601/?code376811
# http://120.46.82.xxx:5601/?code537195# Kibanahttp://120.46.82.xxx:5601/app/dev_tools#/console
# ES-APIhttps://120.46.82.xxx:9200/8.x版本之后开启了SSL校验需要HTTPS验证https://www.cnblogs.com/chaos-li/p/13667687.html也可修改elasticsearch.yml关闭2、ES的数据模型
方便理解类比关系型数据库的数据模型ES的数据模型分为
index(索引)类比一张表代表文档数据的集合文档指的是ES中存储的一条数据。type(文档类型)在新版的Elasticsearch中已经不使用文档类型了在ES6.x版本中1个索引indices只能创建对应一个types因为不同types下的字段不能冲突删除types也不会释放空间推荐需要多个types时候直接创建多个indices。在ES7.x版本中直接删除掉了type的概念。在Elasticsearch老的版本中文档类型代表一类文档的集合index(索引)类似mysql的数据库、文档类型类似MySQL的表。既然新的版本文档类型没什么作用了那么index索引就类似mysql的表的概念ES没有数据库的概念了。Document(文档)类比一行数据Elasticsearch是面向文档的数据库文档是最基本的存储单元文档类似mysql表中的一行数据。简单的说在ES中文档指的就是一条JSON数据JSON数据的字段可以是任意的这些Documents属于一个index。Field(文档字段)类比一个字段文档由多个字段Field组成。Mapping映射类比元数据映射定义了文档中每个字段的数据类型、分析器和索引选项。映射是用于索引和搜索的关键元素它决定了如何存储和检索文档中的数据。
Elasticsearch 支持如下简单域类型
字符串: textstring在5.4版本被text替代、keyword 当一个字段需要用于全文搜索(会被分词), 比如产品名称、产品描述信息, 就应该使用text类型不能排序很少用于聚合。当一个字段需要按照精确值进行过滤、排序、聚合等操作时, 就应该使用keyword类型.不会被分词。 整数 : byte, short, integer, long浮点数: float, double布尔型: boolean日期: date
其他还有object、array、geo、binary
3、ES的领域特定查询语言(Query DSL)
ES如何查询参考
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.htmlES权威指南DSL语法https://www.elastic.co/guide/cn/elasticsearch/guide/current/empty-search.html博客总结命令https://www.cnblogs.com/machangwei-8/p/14979956.html#_label1
3.1、简单基本查询
以下是 Elasticsearch 中最常用的一些命令汇总简单列举具体看文档
1、Index 命令
创建一个索引PUT /index_name
删除一个索引DELETE /index_name
列出所有索引GET /_cat/indices?v2、Document 命令
添加或更新文档PUT /index_name/_doc/document_id
获取文档GET /index_name/_doc/document_id
删除文档DELETE /index_name/_doc/document_id3、检索命令
使用查询字符串搜索GET /index_name/_search?qquery_string
使用请求体搜索POST /index_name/_search
{query: {...}
}4、聚合命令
执行聚合操作POST /index_name/_search
{aggs: {...},size: 0
}5、映射命令
获取索引映射定义GET /index_name/_mapping
更新索引映射PUT /index_name/_mapping
{properties: {...}
}6、设置命令
获取集群设置GET /_cluster/settings
修改集群设置实时生效PUT /_cluster/settings
{persistent: {...},transient: {...}
}DSL的写法很多这里列举出练习demo的聚合查询
# 1、统计每个州的state聚合查询
GET /accounts/_search
{size: 0,aggs: {group_by_state: {terms: {field: state.keyword}}}
}
GET /accounts/_search# 2、嵌套聚合查询
# 对每个州的state分组的基础上聚合求出平均balance
GET /accounts/_search
{size: 0,aggs: {sichaolong: {terms: {field: state.keyword},aggs: {average_balance: {avg: {field: balance}}}}}
}# 3、聚合结果排序查询
GET /accounts/_search
{size: 0,aggs: {group_by_state: {terms: {field: state.keyword,order: {average_balance: desc}},aggs: {average_balance: {avg: {field: balance}}}}}
}
3.2、复合查询
多种条件组合的查询在ES中叫做复合查询ES提供5种复合查询方式。
bool query(布尔查询)boosting query(提高查询)constant_score固定分数查询dis_max(最佳匹配查询function_score(函数查询
具体的用法直接看官网文档。
4、ES全文查询搜索
4.1、match相关查询和term查询的区别
match以及相关的match_phrase、match_phrase_prefix 查询本质上是term查询的组合。
match查询和term查询是Elasticsearch中两种常用的查询类型它们在处理方式上略有不同。需要注意的是在Elasticsearch中text字段通常适合使用Match查询而keyword字段适合使用Term查询这取决于你想要实现的具体需求和查询场景。
Match查询
Match查询是一种全文搜索查询它会将查询字符串分析成词项并根据相关性来评分。默认情况下它会尝试将查询字符串与目标字段中的所有词项进行匹配。Match查询还支持布尔操作符AND、OR和NOT以及短语搜索等高级功能。
例如对于一个名为title的字段使用Match查询可以执行如下查询
{query: {match: {title: quick brown fox}}
}Term查询
Term查询是一种精确匹配查询它会将查询字符串作为整体进行匹配而不会对其进行分析或拆解为词项。默认情况下Term查询是区分大小写的。
例如对于一个名为user.keyword的关键字字段使用Term查询可以执行如下查询
{query: {term: {user.keyword: {value: john smith}}}
}match查询的步骤
检查文档字段类型检查文档的字段类型是否是全文检索字段分析查询字符串查询字符串本身也需要分词如果是单个词执行一个词的term查询。如果是多个词那么会被分词执行多次term查询然后结果合并。查找匹配文档倒排索引后面以及往期文章会详细介绍找到文档为每个文档评分用 term 查询计算每个文档相关度评分 _score 主要依据词频查询词在某文档出现的频率、反向文档频率查询词在所有文档中出现的频率、字段内容的长度 相结合计算得出。
下面查询1、2两个查询结果是一致的查询3、4两个查询结果是一致的
# 查询1
GET /test-dsl-match/_search
{query: {match: {title: BROWN DOG,operator: or # 默认被省略了缺省就是or}}
}# 查询2
GET /test-dsl-match/_search
{query: {bool: {should: [{term: {title: brown}},{term: {title: dog}}]}}# 查询3
GET /test-dsl-match/_search
{query: {match: {title: BROWN DOG,operator: and}}
}# 查询4
GET /test-dsl-match/_search
{query: {bool: {must: [{term: {title: brown}},{term: {title: dog}}]}}另外match的匹配精度也是可以配置的如果用户给定 3 个查询词想查找至少包含其中 2 个的文档该如何处理
将 operator 操作符参数设置成 and 或者 or 都是不合适的。match 查询支持 minimum_should_match 最小匹配参数这让我们可以指定必须匹配的词项数用来表示一个文档是否相关。
我们可以将其设置为某个具体数字更常用的做法是将其设置为一个百分数因为我们无法控制用户搜索时输入的单词数量查询5、6结果也是等价的。
# 查询5
GET /test-dsl-match/_search
{query: {match: {title: {query:quick brown dog,minimum_should_match: 75%}}}
}# 查询6
GET /test-dsl-match/_search
{query: {bool: {should: [{ match: { title: quick }},{ match: { title: brown }},{ match: { title: dog }}],minimum_should_match: 2 }}
}4.2、match_phrase查询match_phrase_prefix的区别
match_phrase本质上是多个有序term查询。
前面说match如果涉及多个词会被拆分为多个term查询而且多个term是按照or查询的。 如果想查询某个段落可以使用match_pharse、match_phrase_prefix。关于两者是有差别的match_phrase往往会被认为是查询字符串不被分词直接去文档检索这是错误的其实match_phrase也是会对查询字符串进行分词的只不过相比match那种方式分词之后的顺序是保证的。而match_phrase_prefix对应的是上述情况。
如
match_phrase本质是连续的term的查询所以f并不是一个分词不满足term查询所以最终查不出任何内容了。
而match_phrase_prefix可以查到。另外还有一个match_bool_prefix本质上可以转化为
GET /test-dsl-match/_search
{query: {bool : {should: [{ term: { title: quick }},{ term: { title: brown }},{ prefix: { title: f}}]}}
}4.3、其他查询
类似match的查询
multi_match对多个字段同时查询query_string支持多内容通过运算符组合interval查找的内容字符串在原文档中需要保持顺序
对于term查询 // TODO
5、使用
使用方式很多官网推荐使用Java API ClientSpring Data Elasticsearch是高级封装随着依赖的升级以及ES的升级可能后续不是很容易维护。
TransportClientTransportClient 在 Elasticsearch 7.0.0 中已被弃用取而代之的是 Java High Level REST Client并将在 Elasticsearch 8.0中删除。在项目中不再建议使用详见官方链接https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/java-api.html#java-apiJava High Level REST Client 在 Elasticsearch 7.15.0 中已弃用取而代之的是 Java API Client。在项目中不再建议使用详见官方链接https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.htmlJava API Client官方推荐使用的方式。详见官方链接https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.htmlSpring Data Elasticsearch关键功能领域是一个以 POJO 为中心的模型用于与 Elastichsearch 文档进行交互并轻松编写存储库数据访问层。类似MyBatis那种方式有着Repository、Service等抽象。springboot、spring-data-elasticsearch、elasticsearch版本对应https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#preface.requirements
参考
http://masikkk.com/article/Elasticsearch/https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/installation.html
三、原理分析
1、搜索引擎流程
全文检索可以分为索引、搜索返回两个阶段过程主要分为四个部分
查询分析搜索内容自然语言处理如敏感词过滤错别字纠正等。分词技术搜索内容拆成词条以下是一些常用的 Lucene 分词器 StandardAnalyzer标准分词器它基于 Unicode 文本字符边界来划分词条并去除一些停用词如介词、连词等。SimpleAnalyzer简单分词器将文本转换为小写并根据非字母字符进行划分WhitespaceAnalyzer空格分词器通过空格字符进行划分。KeywordAnalyzer关键字分词器将整个输入当作一个词条不进行进一步划分。StopAnalyzer停用词分词器类似于标准分词器但还会移除一些自定义的停用词。此外还有很多其他特殊用途的分词器可供选择IKAnalyzer、SmartChineseAnalyzer 等比如中文领域的。 关键词检索在倒排索引库索引搜索找到具体的文档数据。搜索排序返回对多个文档进行相关度计算排序返回数据。
2、Lucene相关原理
Lucene概述 Apache Lucene™是一个 完全用Java编写的高性能、全功能搜索引擎库。https://lucene.apache.org/core/index.html Lucene的目的是为软件开发人员提供一个简单易用的工具包You need four JARs: the Lucene JAR, the queryparser JAR, the common analysis JAR, and the Lucene demo JAR. 以下是常见的Lucene JAR包
lucene-core.jar包含了Lucene的核心功能如倒排索引、分词器、查询解析器等。lucene-analyzers-common.jar包含了一系列常用的分析器用于将文本进行分词和标准化处理。lucene-queryparser.jar包含了用于解析用户输入的搜索查询字符串并生成相应查询对象的工具。lucene-highlighter.jar包含了实现搜索结果高亮显示的组件。lucene-suggest.jar包含了构建自动补全和建议功能的相关类和接口。lucene-grouping.jar包含了根据指定字段对搜索结果进行分组的功能。 使用Lucene代码demo
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import pojo.Student;import java.io.IOException;
import java.nio.file.Paths;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** Auther: sichaolong* Date: 2023/9/5 14:29* Description: 简单使用Lucen工具包*/
public class LuceneDemo {// 模拟数据public static final ListStudent STUDENT_LIST new ArrayListStudent(Arrays.asList(new Student(1, 张三, 18, 北京市海淀区温泉镇, 法外狂徒),new Student(2, 李四, 19, 北京市海底区东升镇, 唱、跳、rap),new Student(3, 王武, 20, 北京市海淀区上庄镇, 吸烟、喝酒、烫头),new Student(4, 王五, 21, 北京市海淀区苏家坨镇, 点烟、倒酒、给别人烫头),new Student(5, 麻六, 18, 北京市海淀区西北旺镇, 吃饭、喝酒),new Student(6, 酸菜, 17, 统一老坛酸菜牛肉面, 带着酸菜),new Student(7, 麻辣, 10, 统一麻辣牛肉面, 带着没有牛肉的牛肉面),new Student(8, 老母鸡, 14, 康师傅老母鸡汤面, 没有老母鸡的老母鸡面),new Student(9, 酱香, 15, 酱香味小龙虾, 88元一斤),new Student(10, 蒜蓉, 19, 蒜蓉味小龙虾, 100元一斤)));// 数据存储路径private static final String INDEX_PATH ./lucene-data-demo/index;public static void main(String[] args) throws IOException, ParseException {// createIndex();search();}/*** 创建索引功能的测试** throws Exception*/public static void createIndex() throws IOException {// 1. 创建文档对象ListDocument documents new ArrayListDocument();for (Student student : STUDENT_LIST) {Document document new Document();// 2. 给文档对象添加域// add方法: 把域添加到文档对象中, field参数: 要添加的域// TextField: 文本域, 属性name:域的名称, value:域的值, store:指定是否将域值保存到文档中document.add(new TextField(id, student.getId() , Field.Store.YES));document.add(new TextField(name, student.getName(), Field.Store.YES));document.add(new TextField(age, student.getAge() , Field.Store.YES));document.add(new TextField(address, student.getAddress(), Field.Store.YES));document.add(new TextField(desc, student.getDesc(), Field.Store.YES));// 将文档对象添加到文档对象集合中documents.add(document);}// 3. 创建分析器对象(Analyzer), 用于分词Analyzer analyzer new StandardAnalyzer();// 4. 创建索引配置对象(IndexWriterConfig), 用于配置LuceneIndexWriterConfig indexConfig new IndexWriterConfig(analyzer);// 5. 创建索引库目录位置对象(Directory), 指定索引库的存储位置,创建一个indexWriter对象,IndexWriter indexWriter new IndexWriter(FSDirectory.open(Paths.get(INDEX_PATH)), indexConfig);indexWriter.addDocuments(documents);indexWriter.commit();// 6、关闭indexWriter对象。java11报错解决参考http://community.jedit.org/?qnode/view/37964indexWriter.close();}/*** 搜索索引测试*/public static void search() throws IOException, ParseException {// 创建一个Directory对象也就是索引库存的位置。Directory directory FSDirectory.open(Paths.get(INDEX_PATH));// 创建一个IndexReader对象需要指定Directory对象。IndexReader indexReader DirectoryReader.open(directory);// 创建一个indexSearcher对象需要指定IndexReader对象。IndexSearcher indexSearcher new IndexSearcher(indexReader);// 创建一个TermQuery对象指定查询的域和查询的关键词。Query query new TermQuery(new Term(name, 张));// 执行查询TopDocs topDocs indexSearcher.search(query, 10);System.out.println(查询结果的总条数 topDocs.totalHits);// 返回查询结果遍历查询结果并输出for (ScoreDoc scoreDoc : topDocs.scoreDocs) {//scoreDoc.doc属性就是document对象的id//根据document的id找到document对象Document document indexSearcher.doc(scoreDoc.doc);System.out.println(document.get(id));//System.out.println(document.get(content));System.out.println(document.get(name));System.out.println(document.get(address));System.out.println(document.get(desc));System.out.println(-------------------------);}/*** 输出* 查询结果的总条数1* 1* 张三* 北京市海淀区温泉镇* 法外狂徒* -------------------------*/// 关闭indexReader对象indexReader.close();}}
// TODO 如何实现快、准搜索Lucene很复杂 检索引擎最核心的部分就是索引的设计、数据的存储重点关注索引如何设计如何储存用什么数据结构数据如何组织如何压缩
从数据层面分析整个Lucene把需要处理的数据分为这么几类 前四种是所有检索引擎都会保存的数据后三种是Lucene特有的
PostingList 倒排表也就是term-[doc1 doc3, doc5]这种倒排索引数据 。真实的倒排记录也并非一个链表而是采用了SkipList、BitSet等结构。TermDict, 从term和PostingList的映射关系这种映射一般都用FST这种数据结构来表示这种数据结构其实是一种有向图类似于前缀树所以Lucene这里就叫BlockTree 其实我更习惯叫它TermDict。StoredField 存进去的原始信息 行式组织数据存储通常用于存储需要在搜索结果中返回的field以便在检索文档时能够获取原始字段值。DocValue 键值数据通常用于存储需要在搜索结果中需要聚合、排序、筛选的field这种数据主要是用来对于高级查询加速对字段的排序、筛选的。列式组织数据存储TermVector词向量信息主要记一个不同term的全局出现频率等信息。Norms用来存储Normalisation信息分词后的文本进行规范化处理小写转换删除特殊字符然后存储以提高搜索和匹配的准确性。PointValue 用来加速 range Query的信息实现基于BKDTree。
针对不同的数据结构采用不同的字典索引倒排索引基于字典树使用了FST模型压缩索引使用SkipList和BitSet加速多条件查询磁盘存储组织PointValue组织形式基于BKDTree等结构加速范围查询。
参考
Lucene源码剖析理解ElasticSearch工作原理使用Java调用Lucene实现简单demo倒排索引ES倒排索引底层原理及FST算法的实现过程Lucene BKD树-动态磁盘优化BSP树
2.1、倒排索引之FSTFinite State Transducers
概述也常被称为反向索引是一种索引方法被用来存储在全文搜索下某个词条在一个文档或者一组文档中的存储位置的映射它是文档检索系统中最常用的数据结构。 采用映射表记录哪些词条出现在哪些文档中然后实现快速检索。
举例
随着的词条的增多这个倒排记录表也越来越大倒排记录也越来越多每次遍历查找的效率不高肯定不行。而且全部将倒排记录加载进内存也吃不消。
因此Lucene在前面倒排记录表前加了一层增加一个字典结构索引Term Index字典结构搜索场景用的比较多实现方式有很多
以Trie树实现的字典为例他不存储所有的单词只存储单词前缀从Trie树索引树找词条最后找到词条对应的文档列表。 下图是一个简化的Trie树真实的Trie实现kv结构实现方式有很多种
树节点为固定长度的数组数组的元素需要包含两个域一个是k一个是next指向下一节点。一般一个节点需要固定长度如存储英文字母的节点长度为26缺点对于英文做前缀还能接受中文字符集太多太占内存。时间复杂度O1空间复杂度ON。树节点为一个链表链表节点需要包含三个域一个是k一个是next指向下一个链表节点另外一个child指向下一个树木节点。缺点每次都需要从链表头节点开始访问。时间复杂度ON空间复杂度O1树节点为哈希表。时间复杂度为上述两种方式之间。 Finite State Transducer (FST)是一种计算模型它基于有限状态自动机FSM并添加了输出功能。 主要描述有限个状态睡觉、玩耍、吃饭、躲藏、猫砂窝与状态转移动作提供食物、有大声音等之间的关系。 比如算法中的动态规划思想就是一种对状态的抽象核心的就是抽象状态转移方程比如爬楼梯算法的状态转移方程为 f(n) f(n-2) f(n-1)
基于FSM实现的字典FST不但能共享前缀还能共享后缀压缩数据。不但能判断查找的key是否存在还能给出响应的输出output。 它在时间复杂度和空间复杂度上都做了最大程度的优化使得Lucene能够将Term Dictionary完全加载到内存快速的定位Term找到响应的outputposting倒排列表
普通的Trie和FST对比 Trie有重复的3个final状态3811. 而811都是s转移是可以合并的。FST可以看做是一个带有度的有向无环图
Lucene从4开始大量使用的数据结构是FST。FST有两个优点 1空间占用小。通过对词典中单词前缀和后缀的重复利用压缩了存储空间。 2查询速度快。O(len(str))的查询时间复杂度。 缺点 1FST通常不适合频繁的插入和删除操作因为它的构建和修改开销较大需要调整有向图边上的度出现公共前缀、后缀就要调整。
我们可以将FST当做Key-Value数据结构来进行使用特别在对内存开销要求少的应用场景。FST压缩率一般在3倍~20倍之间相对于TreeMap/HashMap的膨胀3倍内存节省就有9倍到60倍摘自把自动机用作 Key-Value 存储
参考
字典数据结构-FST(Finite State Transducers)FSM有限状态自动机-维基百科关于Lucene的词典FST深入剖析
2.2、倒排索引之SkipList、BitSet
倒排索引采用这两种数据结构主要是为了多条件查询SkipList用于构建Term DictBitSet用于对查找到的多个倒排记录指向的docids做交集合。
对于FST字典结构也不是完全映射倒排记录表的也是做的一个前缀因为组合实在太多了实际是类似一个目录通过FST找出Term Dict的起始指针、结束指针位置。
如单查询过滤条件 name Alice 的过程就是先利用FST结构从Term Index找到Alice在Term Dict 的大概位置然后再从Term Dict里利用SkipList精确地找到Alice这个term然后找到指向的docids 如多条件查询 nameAlice AND gender女 就是把两个 posting list 做一个“与”的合并。也就是取两个docids的交集合。
如何高效的合并呢
如果查询的filter条件缓存到了内存中以BitSet的形式那么合并就是两个BitSet的AND举例如[1,3,4]压缩进位图中就是[1,0,1,1]。Redis中的BitMap就是这个原理将大量数据压缩进位图如应用布隆过滤器。如果查询的filter没有缓存那么就用SkipList的方式去遍历两个on disk的posting list。找一个最短的post list先遍历Block Max WAND块最大WAND策略是一种用于提高布尔查询性能的优化技术特别是对于AND操作逻辑与的查询使用最小的IO成本过滤掉那些不匹配的docid比如nameAlice对应的是[1,3,4]gender 女对应的是[5,6,8,9,29,34,54,545,54545]
// 类似木桶原理如果list1[1,3,4]遍历到完了遍历3次此时list2[5,6,8,9,29,34,54,545,54545]才读取第一个元素。
// 双指针写法def block_max_wand_intersection(list1, list2):result []i, j 0, 0while i len(list1) and j len(list2):if list1[i] list2[j]:# 如果文档ID匹配将其添加到结果中result.append(list1[i])i 1j 1elif list1[i] list2[j]:# 如果第一个列表中的文档ID较小增加其索引以找到更大的文档IDi 1else:# 如果第二个列表中的文档ID较小增加其索引以找到更大的文档IDj 1return result# 使用示例
name_list [1, 3, 4]
gender_list [5, 6, 8, 9, 29, 34, 54, 545, 54545]
result_intersection block_max_wand_intersection(name_list, gender_list)
print(result_intersection)
2.3、Stored Field存储方式
比如为什莫要区分Stored Field行式存储和 Doc Value列式存储是否可以手动指定
// Lucene API
// 常规行式存储document.add(new TextField(age, student.getAge() , Field.Store.YES));
document.add(new SortedDocValuesField(age, new BytesRef(student.getAge())));// ES API
PUT /my_index
{mappings: {properties: {field1: {type: text,store: true},field2: {type: text,store: false}}}
}
主要是有两方面的原因性能、存储成本。
性能考虑行式存储document数据可以方便一次获取全部需要查询展示的fields数据按照列式存储field可以方便排序、统计、筛选。存储考虑Doc Values通常用于存储静态字段值例如文档的ID、日期、标签或其他结构化数据。这些字段值是文档的一部分但它们不依赖于查询条件或文档匹配度因此适合使用Doc Values进行列式存储。 代码层面 参考前面简单实用lucnen的代码整个过程逻辑前四层式逻辑调用层中间层是索引链式处理层 图源https://zhuanlan.zhihu.com/p/384486147
DefaultIndexingChain是一个非常核心的类负责对当前文档个建索引的核心操作它定义了什么时候该写倒排拉链什么时候写DocValue,什么时候写入StoredField 等。 processDocument 是整个索引链个入口方法它会负责将整个文档按照Field拆开分别调用下面的processField方法
private int processField(IndexableField field, long fieldGen, int fieldCount) throws IOException {String fieldName field.name();IndexableFieldType fieldType field.fieldType();PerField fp null;if (fieldType.indexOptions() null) {throw new NullPointerException(IndexOptions must not be null (field: \ field.name() \));}// Invert indexed fields:// 在该Field上面建倒排表if (fieldType.indexOptions() ! IndexOptions.NONE) {fp getOrAddField(fieldName, fieldType, true);boolean first fp.fieldGen ! fieldGen;fp.invert(field, first);if (first) {fields[fieldCount] fp;fp.fieldGen fieldGen;}} else {verifyUnIndexedFieldType(fieldName, fieldType);}// Add stored fields: 存储该field的storedFieldif (fieldType.stored()) {if (fp null) {fp getOrAddField(fieldName, fieldType, false);}if (fieldType.stored()) {String value field.stringValue();if (value ! null value.length() IndexWriter.MAX_STORED_STRING_LENGTH) {throw new IllegalArgumentException(stored field \ field.name() \ is too large ( value.length() characters) to store);}try {storedFieldsConsumer.writeField(fp.fieldInfo, field);} catch (Throwable th) {docWriter.onAbortingException(th);throw th;}}}// 建docValueDocValuesType dvType fieldType.docValuesType();if (dvType null) {throw new NullPointerException(docValuesType must not be null (field: \ fieldName \));}if (dvType ! DocValuesType.NONE) {if (fp null) {fp getOrAddField(fieldName, fieldType, false);}indexDocValue(fp, dvType, field);}if (fieldType.pointDataDimensionCount() ! 0) {if (fp null) {fp getOrAddField(fieldName, fieldType, false);}indexPoint(fp, field);}return fieldCount;
} 数据的落盘Lucene也是采用一些方法如vint可变长编码方式压缩数据存储空间。 2.4、倒排索引之不可变性
Lucene倒排索引设计是不可变的如何进行索引与数据的维护呢
由于倒排索引的结构特性在索引建立完成后对其进行修改将会非常复杂。再加上几层索引嵌套更让索引的更新变成了几乎不可能的动作。所以索性设计成不可改变的倒排索引被写入磁盘后是不可改变的它永远不会修改。 不变性有重要的价值
并发安全不需要锁。如果你从来不更新索引你就不需要担心多进程同时修改数据的问题并发锁征用问题也是很耗时间的。缓存不用长期更新一旦索引被读入内核的文件系统缓存便会留在哪里由于其不变性。只要文件系统缓存中还有足够的空间那么大部分读请求会直接请求内存而不会命中磁盘。这提供了很大的性能提升。其它缓存(像filter缓存)在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建因为数据不会变化。允许数据压缩写入单个大的倒排索引允许数据压缩减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
缺点
主要事实是它是不可变的你不能修改它。如果你需要让一个新的文档 可被搜索你需要重建整个索引达到动态更新索引这要么对一个索引所能包含的数据量造成了很大的限制要么对索引可被更新的频率造成了很大的限制。
2.5、倒排索引之动态更新索引
更新数据就要更新索引Lucene采用一次写多次读write-once-read-multiple策略来完成动态更新索引。具体来说就是在数据更新的过程中
写入新增或更新的文档首先被写入内存缓冲区随着时间的推移或达到一定大小阈值缓冲区的内容将被刷新到磁盘上的一个新的Segment中同时记录一个Commit Point文件日志表示一次成功更新数据类似MySQL的redolog。读取在查询时Lucene 首先从内存缓冲区中查找匹配的记录如果未找到则继续在磁盘上的Segemts中进行查找。尽量保证搜索实时性。
通过提交点Commit Point保证崩溃恢复在写入过程每次更新数据后会进行一次commit每当发生一次提交操作就会创建一个新的提交点并将Segment cache刷盘。这样可以应对即使系统崩溃或意外关闭引起的Segment cache刷盘失败最近的索引更改也能够恢复并不会丢失。另外即使在机器故障或其他问题导致索引文件出现损坏时Lucene也可通过检测到损坏的提交点来进行相应的修复和恢复工作。
磁盘倒排索引合并问题为了优化索引性能和空间利用率Lucene 定期或在需要的时候会将磁盘上的多个小的Segment合并成更大的Segment。该过程可能存在的问题
索引维护开销由于频繁的段合并和磁盘写入操作索引维护需要消耗一定的时间和资源。特别是当索引较为庞大或者更新频率很高时索引维护可能成为性能瓶颈。在传统的先写后读策略中索引段会不断地增加导致查询时需要搜索更多的段从而影响查询速度。空间浪费在段合并过程中旧的段并不会立即删除而是会等待所有正在读取它们的查询完成后再进行删除。因此对于那些很少访问但占用大量磁盘空间的段存在着空间浪费的问题。读取过期数据由于段合并操作只在某些条件满足时才会触发并且需要一定的时间来完成因此可能会出现在段合并过程中读取到已经过期的数据的情况。
举个例子来说明Lucene索引合并导致读取过期数据这种情况
假设初始状态下磁盘有三个段 A、B、C其中段 B 是最新生成的。在合并过程中Lucene 将段 A 和段 B 合并成一个更大的段 AB。此时如果还有查询任务正在读取段 A 中的数据但由于段合并的过程中还没有完成导致部分查询仍然读取到了段 A 中的数据。这些数据属于已经过期的信息因为它们已经被包含在了新的段 AB 中。
2.6、BKD磁盘树 // TODO
3、ES相关原理
ES就是封装调用Lucene提供方便的RESTful API以及高级的一些查询然后通过分片、副本支持分布式和高可用也对索引等处理做了一些优化。
3.1、倒排索引之ES索引合并优化
在传统的先写后读策略中索引段会不断地增加导致查询时需要搜索更多的段从而影响查询速度。 而且索引段合并需要磁盘IO也需要考虑。针对上述问题ES主要做了些索引Segment的合并上的优化
慢速合并策略ES采用了一个名为“慢速合并”slow merge的机制来减少磁盘上的段数目和整理碎片从而提高查询性能。它通过将较小的段合并成更大的段来优化索引结构。多线程执行合并操作ES利用多线程执行合并操作可以在后台异步地进行索引段的合并过程不会阻塞正常的读写操作。这样可以保证合并过程对用户的影响最小化并且提高了合并的效率。增量式索引合并主要优化Segment的数量ES引入了增量式索引合并incremental index merging策略。当有新的索引段生成时ES会尝试将其与已经存在的段进行合并以减少索引中的段数目。也就是防止大量索引合并堆积在一个时间。这种增量式的合并方式相比传统的全局合并方式在处理大量数据时具有更好的效率和性能。
3.2、倒排索引之ES分布式优化
通过将索引拆分为多个分片类似分治的思想每个分片可以类比一个Lucene。通过ES集群将分片分布在不同的节点上。每个分片都是一个独立的 Lucene 索引包含若干个Segment。搜索操作在每个分片上并行执行然后合并结果。
分片分片是底层基础的读写单元分片的目的是分割巨大索引分片是数据的容器文档保存在分片内分片又被分配到集群内的各个节点里不会跨分片存储。 当你的集群规模扩大或者缩小时 Elasticsearch 会自动的在各节点中迁移分片使得数据仍然均匀分布在集群里。一个分片是一个Lucene索引一个Lucene又分成很多Segment每段都是一个倒排索引。比如有100个indices数据库可以拆分片到5台机器每台20个indices。
分治减少动态更新索引成本。分片便于水平拓展。
分片副本分片进行副本存储主分片、从分片分散分布在不同节点提供高可用。在索引建立的时候就已经确定了主分片数但是副本分片数可以随时修改。
系统高可用索引以及数据拆分的part1、part2等等。每一份如part1也会存在副本放在其他机器上简单说就是part1在机器1那么part1副本需要在其他机器上也存一份。并发更新分为主、从分片及先写主分片再写从分片。读写请求会落在不同的分片上不同的机器上做到读写分离。 关于分片数量的一些建议分片的数量在5.x之前不能修改在5.x-6.x之后支持一定条件的修改可以对主分片大小拆分和缩小分片越小分的片就越多应该根据硬件和业务数据量来进行拆分。 1、分片数量不够时可以考虑重新建立索引搜索1个50分片的索引和搜索50个1分片的索引效果一样建议是周期性创建新索引如website索引index每天创建一个website_时间戳index然后在website主索引进行软连接这样删除数据时可以直接删除某个索引避免以id删除文档不会立即释放空间删除的document时候只有在Lucene分段倒排索引合并时候才会从磁盘删除手动合并会导致较高的I\O压力的问题。 2、分片数量过多若是每天一个索引但是某天数据量很小可以_shrink API来减少主分片数量减低集群管理很多分片的负载。
3.3、WAL机制优化
WALWrite-Ahead Log是用来保证数据在写入索引之前的持久化机制以防止数据丢失或损坏。 ES在WAL机制上做了一些升级通过Translog、异步刷新和分布式复制等措施来提高写入性能、降低IO延迟并保证数据的持久性。
ES引入了TranslogTransaction Log作为WAL的实现方式。Translog是一个高效的、顺序写的日志文件记录了索引更新操作。当文档被索引时它们首先会被写入Translog然后再批量刷新到内存中的索引结构中。通过这种方式ES能够提供更快的写入性能而不必每次将更新操作立即写入磁盘。ES还支持异步刷新Async Refresh操作。当新的文档被索引后在默认情况下ES会将这些文档添加到内存中的索引结构并应答客户端请求。然后ES会异步地将这些内存中的变更刷新到磁盘上的Segment中从而降低了IO延迟和硬盘写入压力。
文档数据更新的流程
write buffer当更新文档数据时它首先会将数据写入内存缓冲区 buffer 中然后在新的segment buffer更新索引信息。write transog同时数据还会被追加到一个称为translogtransaction log事务日志的文件中。这个文件位于每个分片的本地磁盘上。只要写入translog成功那么就意味着一次commit数据这个时候就能被搜索到。refresh默认情况下ES使用异步刷新机制定期将内存缓冲区 segemnt buffer 中的数据写入磁盘。刷新操作会将数据持久化到segment文件中并清空内存缓冲区以便接收新的写入请求。
refresh异步刷新默认是每秒触发一次但也可以手动调整该时间间隔。如果在刷新之前发生了节点或进程故障所有尚未刷入磁盘的数据都可以通过translog文件进行恢复。
ps操作系统中磁盘文件其实都有一个操作系统缓存OS Cache因此Segment file数据写入磁盘文件之前会先进入操作系统级别的内存缓存OS Cache中成为 segemnt buffer当translog fsync之后等待refresh也就是segemnt buffer fsync此时的倒排索引Segment就能被搜索到了。 这就是为什么es被称为准实时NRTnear real-time因为写入的数据默认每隔1秒refresh一次也就是数据每隔一秒才能被 es 搜索到之后才能被看到所以称为准实时。
translog日志文件的作用是什么 在你执行commit操作之前数据要么是停留在buffer中要么是停留在segment cache中无论是buffer还是os cache都是内存一旦这台机器死了内存中的数据就全丢了。 因此需要将数据对应的操作写入一个专门的日志文件也就是translog日志文件一旦此时机器宕机再次重启的时候es会自动读取translog日志文件中的数据恢复到内存buffer和segment cache中去。 psES数据写入之后要经过一个refresh操作之后才能够创建索引文件到磁盘进行查询。但是get查询很特殊数据实时可查。ES5.0之前translog可以提供实时的CRUDget查询会首先检查translog中有没有最新的修改然后再尝试去segment中对id进行查找。5.0之后为了减少translog设计的复杂性以便于再其他更重要的方面对translog进行优化所以取消了translog的实时查询功能。get查询的实时性也是一次写多次读通过每次get查询的时候如果发现该id还在内存中没有创建索引那么首先会触发refresh操作来让id可查。
文档数据更新之文档版本号
删除文档段是不可改变的所以既不能从把文档从旧的段中移除也不能修改旧的段来进行反映文档的更新。磁盘上的每个segment都有一个.del文件与它相关联。当发送删除请求时该文档未被真正删除而是在.del文件中标记为已删除。此文档可能仍然能被搜索到但会从结果中过滤掉。当segment合并时在.del文件中标记为已删除的文档不会被包括在新的segment中也就是说merge的时候会真正删除被删除的文档。
更新文档创建新文档时ES将为该文档分配一个版本号。对文档的每次更改都会产生一个新的版本号。当执行更新时旧版本doc在.del文件中被标记为已删除并且新版本doc在新的Segment中更新倒排索引。旧版本可能仍然与搜索查询匹配但是从结果中将其过滤掉。
使用版本号机制乐观控制并发每个文档都有一个 _version 版本号当文档被修改时版本号递增。 Elasticsearch 使用这个 _version 号来确保变更以正确顺序得到执行。如果旧版本的文档在新版本之后到达它可以被简单的忽略。
使用内部版本号删除或者更新数据的时候携带_version参数如果文档的最新版本不是这个版本号那么操作会失败这个版本号是ES内部自动生成的每次操作之后都会递增一。如操作 PUT /website/blog/1?version1 表示文档版本不是1就会操作失败。使用外部版本号ES默认采用递增的整数作为版本号也可以通过外部自定义整数long类型作为版本号例如时间戳。通过添加参数version_typeexternal可以使用自定义版本号。内部版本号使用的时候更新或者删除操作需要携带ES索引当前最新的版本号匹配上了才能成功操作。外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同 ES 不是检查当前 _version 和请求中指定的版本号是否相同 而是检查当前 _version 是否 小于 指定的版本号版本号更大的操作才能执行成功。 如果请求成功外部的版本号作为文档的新 _version 进行存储。
// 当前内部版本号version3执行PUT /accounts/_doc/1?version1
{...
}// 执行报错内部版本号不能并发控制
{error: {root_cause: [{type: action_request_validation_exception,reason: Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use if_seq_no and if_primary_term instead;}],type: action_request_validation_exception,reason: Validation Failed: 1: internal versioning can not be used for optimistic concurrency control. Please use if_seq_no and if_primary_term instead;},status: 400
}// 使用自己定义的外部版本号设置为5
// 如果当前的_version 5,那么该操作执行成功否者失败PUT /accounts/_doc/1?version5version_typeexternal
{...
}
3.4、ES各模块与启动流程
参考书籍《ElasticSearch源码解读与深度优化》
ES的架构设计功能实现主要分为8个模块使用Guice框架进行模块化管理Guice是Google开发的轻量级的IoC依赖注入框架
Cluster主节点执行集群管理封装实现在各个节点迁移分片保持数据平衡管理集群状态将新生成的集群状态发布到各个节点维护集群层面的配置信息调用Allocation模块进行分片分配。Allocation封装了分片分配的功能和策略包括主分片的分配和副分片的分配由主节点调用集群完全重启创建新索引都需要分片分配的过程。Discovery发现模块负责发现集群中的节点以及选取主节点可以类比Zookeeper选主节点并管理集群拓扑。gateway负责收到Master广播下来的集群状态数据持久化存储并在集群完全重启的时候恢复它们。Indices管理全局级的索引设置不包括索引级的设置索引设置分为全局级别、索引级别还封装了数据恢复的功能。HTTP该模块允许通过JSON over HTTP的方式访问ES的API该模块完全是异步的没有阻塞线程等待使用异步通信进行HTTP的好处是解决C10k问题10k量级的并发连接。Transport传输模块负责集群各个节点之间的通信从一个节点到另外一个节点的每个请求都是使用传输模块本质上也是使用异步的。使用TCP通信节点之间维持长连接。Engine封装了对Lucene的操作以及translog的调用他是对一个分片读写操作的最终提供者。
ES单节点启动、关闭流程 分析启动流程中进程如何解析配置检查环境、初始化内部模块。 1、当执行bin/elasticsearch启动ES时候脚本通过exec加载Java程序其中JVM的配置在config/jvm.options指定。 启动脚本后面可以加上参数
E设置某项配置项如-E “cluster.name my_cluster”V打印版本号信息d后台运行p启动时候在指定路径创建一个pid文件其中保存了当前进程的pid。之后可以通过查看这个pid文件来关闭进程。q关闭控制台的标准输出和标准错误输出。s终端输出最小信息v终端输出详细信息
2、然后就是Java程序解析配置文件elasticsearch.yml即主要配置文件、log4j2.properties日志配置文件。
3、接着是加载安全配置敏感信息不适合放在配置文件中的配置、检查内部环境Lucene版本防止有人替换不兼容的jar包、检测jar冲突、检测外部环境节点实现时候被封装进Node模块Node.start()就是进行此步骤主要包括
1、堆大小检查2、文件描述符检查3、内存锁定检查4、最大线程数检查5、最大虚拟内存检查6、最大文件大小检查7、虚拟内存区域最大数量检查8、JVM Client模式检查9、串行收集器检查10、系统调用过滤器检查11、OnError和OOM检查12、Early-access检查13、G1GC检查
4、检查完毕之后就是启动ES的内部子模块见上文介绍它们启动方法被封装在Node类如discovery.start()、clusterService.start()等
5、启动keep-alive线程线程本身不做具体的工作主线程执行完启动流程后会退出keepalive线程是唯一的用户线程作用是保证进程运行在Java程序中至少要有一个用户线程否则进程就会退出。
关闭流程中需要按照一定的顺序综合来看大致为
关闭快照和HTTP Server 不再相应用户REST请求关闭集群拓扑管理不在响应ping请求关闭网络模块让节点离线执行各个插件的关闭流程关闭IndicesService最后关闭因为耗时最长
3.5、ES集群与启动流程分析
ES是通过分片支持分布式的分片创建的副本称之为副分片因此主、副分片可以分布在不同的机器节点上共同组成ES集群。
集群主从模式分布式系统的集群方式分为主从模式和无主模式ES、HDFS、HBase使用主从模式主从可以简化系统设计master作为权威节点负责管理元信息缺点是存在单点故障需要解决灾备问题。从机器的角度看分布式系统每个机器可以放多个节点分片数据有规则的和节点对应起来。
集群管理需要考虑数据路由、主副分片数据一致性等问题因此需要为ES所在的机器节点划分角色。ES集群的机器节点角色
Master节点设置可以作为主节点资格后可以被选举需要各节点投票主节点是全局唯一的主节点也可以作为数据节点但是数据量不要太多为了防止数据丢失有主节点资格的节点需要知道有资格成为主节点的节点数量默认为1Data节点CRUD数据对cpu和内存、I\O要求较高。预处理节点5.0后引入的概念允许在索引文档之前写入数据之前通过事先定义好的一系列processors和pipeline对数据进行处理富化。协调节点处理客户端请求每个节点都知道任意文档所处的位置然后转发这些请求到数据节点收集数据合并返回给客户端。部落节点5.0之前有个处理请求的客户端节点可以理解为负载均衡在5.0之后被协调节点取代。
ES集群启动流程集群启动指的是集群首次启动或者是完全重启的启动过程期间要经历选举ES主节点、主分片、数据恢复等重要阶段。其过程可能会出现脑裂、无主、恢复慢、丢数据等问题。
主要分为以下四个阶段
selectmaster集群启动从众多ES节点(ES进程)选取一个主节点选举算法是Bully算法的改进每个节点都有节点ID然后每个节点都会对当前已知活跃排序理论上取ID最大的为主节点但是会存在由于网络分区或者节点启动速度相差太大的时候会导致节点最大ID统计不同一如1节点统计1234但是2节点统计2345此时就会不一致因此此节点会先半数选举一个临时主节点然后半数投票才确认最终的主节点。选举完成后若有节点下线需要判断存活节点数是否大于当前检测到存活的一半节点数达不到就要放弃master重新设置集群假如5台机器网络出现故障分区1、2一组3、4、5一组产生分区前master位于1或2此时三台一组的节点会重新并成功选取master产生双主俗称脑裂。gateway确定最新的集群元信息被选举的master的节ES点存储的集群元信息不一定是最新的需要将其他节点元信息发过来根据版本号来确定最新的元信息。然后把这个元信息广播更新其他节点称为集群元信息的选举。allocation分片分配至集群各节点构建路由表ES节点分配分片全部index需要均分分片到对应ES节点构建路由表1、先要选出主分片所有分配工作由master节点来做。开始所有的分片信息都处于unassigned状态ES中通过分配过程决定哪个分片位于哪个节点因此首先需要选出主分片。首先询问所有节点依次索要part1分片、part2分片…的元信息询问量 节点数 * 分片数part1、2、3、4…)由此可以看出效率受分片数量影响所以最好是控制分片数量。现在拿到了所有的分片信息5.x之前是将所有的从分片元信息汇总比较选出版本号最大的作为主分片但是存在分片所在机器启动慢问题5.x之后给每个分片设置一个uuid然后再集群的元信息记录那个shared是最新的。2、选取从分片从众多收集的分片信息选取一个作为从分片。recovery数据恢复保持主从分片数据一致分片分配到节点后开始统一各主、副分片数据主分片有可能写的数据还没刷盘主分片recovery不会等到副分片分配成功才开始但是副分片recovery需要等到主分片recovery之后因为主写副读主的数据副分片有可能还没统一。一次Lucene倒排索引的提交就会一次写缓冲区fsync刷盘过程。 主的recovery就是将最后一次提交之后的translog进行重放。副的recovery会分为两个阶段为了不影响读1、全量同步获取主的translog锁保证不会受主的fsync改变translog然后备份主分片快照直接更新副分片。此阶段完成前通知完副分片启动engine然后可以接受读写请求了。2、增量同步主分片在上面过程中可能写入新的数据和translog因此副需要增量将translog新增的索引重放恢复增量translog数据指的是对translog从加锁开始到副分片复制完主分片的快照的时刻产生的新增数据可以对主的translog做一个快照发送到副就能找到差异数据。
在recovery的时候主也是可以接受请求更新数据的从的全量、增量同步都需要时间从如何保证这些数据不丢失 关于recovery阶段从如何应对全量同步阶段主的更新导致数据丢失前面说从分片第二阶段增量同步的translog快照包含第一阶段以后所有的新的新增操作如果在第一阶段全量同步还未执行完主发生数据 lucene commit将文件系统写缓冲的数据刷盘并清空translog呢这样是不是在第二阶段就拿不到translog快照了呢在ES2.0之前是阻止刷盘操作这样可能会导致一直往translog写数据而不刷盘2.0之后到6.0之前为了防止期间出现过大的translog使用translog.view来获取后续所有操作。从6.0之后引入TranslogDeletingPolicy的概念他将translog做一个快照保证translog不被清理掉。 关于recovery阶段从如何应对增量量同步阶段主的更新导致数据丢失在ES2.0之前副分片恢复过程其实是有三个阶段的第三阶段会阻塞主的更新数据的操作传输第二阶段执行期间新增的translog这个时间很短在2.0之后第三个阶段就被删除了恢复期间没有任何写阻塞过程副重放translog的时候主在第一阶段和第二阶段的写操作 与 从第二阶段重放translog操作之间的时序错误和冲突通过写流程中进行异常处理对比版本号来过滤掉过期操作。遮这样就把正对于某个doc只有最新的一次操作生效保证了主副分片一致。
ES集群的选主流程 Discovery模块负责发现集群中的节点以及选取主节点因为是分布式存储系统自然要处理一致性问题一般解决方案 1试图避免不一致情况发生CA 2发生不一致如何挽救。第二种一般对数据模型有着较高的要求CP 集群的架构可以为主从模式、哈希表模式
哈希表模式每小时可支持数千个节点的加入和离开其可以在不了解底层网络拓扑的情况下查询相应很快如Cassandra就是这种模式主从模式在网络相对稳定的情况下较为适合当集群没那么多节点的时候通常节点的数量远远小于单个节点能够维持的连接数也就是连接多并且节点不经常变动因此es选择这种模式。
选举算法
Bully算法选举Leader的基本算法之一假设每个节点都有一个唯一的ID然后根据ID排序任何时候选取最大ID对应的节点为Leader这种方式是实现比较简单不足是容易产生脑裂比如A节点之前为Leader但是后来由于负载过重出现假死这个时候排名第二的节点B被选为Leader然后A节点又突然恢复正常了造成脑裂效应。Paxos算法选举更灵活、简单但是实现起来比较复杂。参考https://zhuanlan.zhihu.com/p/31780743、https://www.cnblogs.com/linbingdong/p/6253479.html
详细流程 在ES选Master过程相关的重要配置其中之一discovery.zen.minimum_master_nodes 最小主节点数量值最好设置ES集群总节点数的半数以上比如共三个节点最好设置为 3 / 2 1 2 个这是防止脑裂、数据丢失及其重要的参数作为其他几种集群行为的判断依据。详细流程
1、触发选主当参选的节点数量大于设置的最小节点数才能进行选主 2、确定Master主要分为下面的选出临时Master和确定最终的Master两个步骤原因上文也有说。 2.1、选出临时Master通过配置discovry.zen.ping.unicast.hosts指定集群中的节点列表包含ES进程的ip、port各节点之间投票根据Bully选举算法每个节点计算出一个最小的已知节点ID可以通过启动时间、网络响应时间等等确定详细的流程就是
1每个节点Ping所有的集群节点获取可到达的节点列表加入到fullPingResponses中然后把自己也加入列表2构建两个列表activeMasters列表存储当前活跃的允许被选为Master的节点列表这个列表的数据来自遍历fullPingResponses每个节点根据每个节点选出的ID最小的加入activeMasters列表不包括自身节点其中配置了discovery.zen.master_election.ignore_non_master_pings为true的节点并且配置不具备Master也不会被加入。另外一个是masterCandidates列表是master的候选者列表如果activeMasters为空那么从这个列表选取。3从activeMasters列表中选取一个做为自己认为的临时Master比较方法是选出一个列表中ID最小值的节点。
2.2、投票确定最终Master各节点选出自己确定的临时Master需要半数该值节点数认同才能成为真正的Master否则该临时Master就会加入集群发送投票就是本节点向自己选的临时Master发送加入集群的请求获得的票数就是该临时Master接收到其他节点的加入集群的请求数量。投票过程中对于莫i个临时Master会存在两种情况
1被选上等待其他具备Master资格的节点加入集群即投票达到法定人数默认30s超时未达到法定人数则选举失败选举成功的话发布集群状态clusterState。2其他临时Master被选上当前临时Master不在接受投票信息向被确定为Master的节点发送加入请求并默认等待1min超时会重试三次。最终确定的Master会先发布集群状态然后在确认加入请求。
2.3、Master选取元信息像有Master资格节点配置了node.master true的节点)发请求获取元数据获取响应数量必须达到最小节点数才会选为元信息。 3、Master发布集群状态
4、集群节点失效检测 选举完成之后集群状态发布后面集群需要探测到某些节点失效的异常情况不执行的话可能会造成脑裂双主、多主因此需要启动两种失效探测器
在Master节点启动NodesFaultDetection简称NodesFD定期探测加入集群的节点是否活跃。检查下当前集群存活节点是否达到法定节点数半数以上如果不足则会放弃Master重新加入集群。在其他节点启动MasterFaultDetection简称MasterFD定期探测Master节点是否活跃。尝试重新加入集群发送加入申请。