文章目录
4.1 核心概念
- 索引(Index):类似于mysql的表
- 类型(Type):7.0以后只有 doc类型,后面会被淡化
- 文档(Document):类似于mysql的一行数据,在es里是一个文档
- 字段(Field):和mysql字段一样,可以定义类型
- 映射(Mapping):给字段规定类型。默认值。分析器。分词器,是否索引(约定字段属性)
- 分片(Shards):防止单点压力过大,分布式分散在多个机器上。比如100条数据,分3个片。【0-33 主分片1 服务器1】【33-66 主分片2 服务器2】【67-100 主分片3 服务器3】
- 副本(Replicas):防止服务故障,用于高可用。比如【0-33 主分片1 服务器1】会在【0-33 副本分片1 服务器2】做个备份
- 分配(Allocation):es对分片的分配,和副本的分配(都是由es内置完成)
4.2 系统架构
【图片】
4.3 分布式集群
4.3.1 单节点集群
我们在包含一个空节点的集群内创建名为 users 的索引,为了演示目的,我们将分配3个主分片和一份副本(每个主分片拥有一个副本分片)
PUT http://127.0.0.1:1001/users
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
【图片】
4.3.2 故障转移
当你在启动一个节点,集群名称一样的时候,es将会自动把副本分配出去。
然后把原来备份数据复制过去node2。
【图片】
4.3.3 水平扩容
怎样为我们的正在增长中的应用程序按需扩容呢?当启动了第三个节点,我们的集群将会拥有三个节点的集群:为了分散负载而对分片进行重新分配
会把主节点和副本节点错开不同服务器:
【图片】
但是如果我们想要扩容超过6个节点怎么办呢?
主分片创建表(索引)的时候就需要指定,指定后不可以改。
如果想提高查询并发,可以改副本数量:改为2看看效果。高可用性就更高了。
PUT http://127.0.0.1:3001/users_settings
{ "number_of_replicas" : 2}
users索引现在拥有9个分片:3个主分片和6个副本分片。这意味着我们可以将集群扩容到9个节点,每个节点上一个分片。相比原来3个节点时,集群搜索性能可以提升 3 倍。
【图片】
相同节点数的集群增加副本分片不提升性能(会减少各分片资源),需增加硬件提升吞吐量,但更多副本能提高数据冗余(按上述配置,可承受 2 个节点丢失而不丢数据)。
4.3.4 应对故障
我们关闭第一个节点,这时集群的状态为:关闭了一个节点后的集群。
【图片】
关闭的是主节点,集群需选举新主节点(Node 2)。同时丢失主分片 1 和 2,索引无法工作,集群状态为 red(非所有主分片正常)。
【图片】
好在其他节点有这两个主分片的完整副本,新主节点立即将 Node 2 和 3 上的对应副本升为主分片,集群状态变为 yellow,此过程瞬间完成。
为什么我们集群状态是 yellow 而不是 green 呢?
虽三个主分片都在,但每个需 2 份副本而目前仅 1 份,集群为 yellow。不过无需担心,若关闭 Node 2,程序仍可运行且不丢数据,因 Node 3 有各分片副本。
重启 Node 1,集群会重新分配缺失副本,恢复原状态(仅主节点切换),Node 1 会重用原有分片,仅复制修改的数据。
【图片】
4.4 路由计算
公式: 分片 = hash(routing)% 分片数
- 索引文档存于哪个主分片,由 routing(默认文档_id,可自定义)经哈希生成数字后,除以主分片数所得余数决定。
- 因此主分片数需在创建索引时确定且不可更改,否则路由失效致文档无法查找。
- 所有文档 API 接受 routing 参数,可自定义文档到分片的映射(如同一用户文档存同一片)。
4.5 分片控制
我们假设有一个集群由三个节点组成。它包含一个叫 emps 的索引,有两个主分片,每个主分片有两个副本分片。相同分片的副本不会放在同一节点。
PUT
http://127.0.0.1:1001/emps
{
"settings": {
"number_of_shards": 2,
"number_of_replicas": 2
}
}
【图片】
我们可以发送请求到集群中的任一节点。每个节点都有能力处理任意请求。每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。在下面的例子中,将所有的请求发送到 Node 1,我们将其称为 协调节点(coordinating node) 。
:当发送请求的时候,为了扩展负载,更好的做法是轮询集群中所有的节点。
4.5.1 写流程
是线程同步的
- 客户端向 Node 1 发增删改请求。
- 节点根据文档_id 确定属分片 0,转发请求到主分片所在的 Node 3。
- Node 3 执行请求后,并行转发到 Node 1、2 的副本分片,所有副本成功后,经协调节点向客户端返回成功。
【图片】
4.5.2 读流程
我们可以从主分片或者从其它任意副本分片检索文档
【图片】
从主分片或者副本分片检索文档的步骤顺序:
客户端向 Node 1 发送获取请求。
节点通过文档_id 确定其属分片 0,该分片副本在所有节点,故转发请求至 Node 2。
Node 2 将文档返回 Node 1,再由其返回客户端。
【说明】处理读取请求时,协调节点轮询所有副本,
如果打到副分片上有可能返回数据部不存(还没同步过来)
如果返回成功,说明主+副都可用。
4.5.3 更新流程
(更新操作是同步的,要主1和副2副3成功后才会返回成功)
部分更新一个文档结合了先前说明的读取和写入流程:
【图片】
客户端向 Node 1 发更新请求,转发至主分片所在 Node 3。
- Node 3 取文档修改后重索引,遇并发修改重试至 retry_on_conflict 次。
- 成功后,将文档新版本发往 Node 1、2 的副本分片重索引,所有副本成功后返回客户端。
主分片向副本转发文档新版本(非更新请求),避免异步顺序错乱致文档损坏。
补充:内存缓冲区文档写入新段(不 fsync),段打开可搜索,缓冲区清空。
4.5.4 多文档操作流程
mget 和 bulk API 的模式类似于单文档模式。区别在于协调节点知道每个文档存在于哪个分片中。它将整个多文档请求分解成 每个分片 的多文档请求,并且将这些请求并行转发到每个参与节点。
协调节点一旦收到来自每个节点的应答,就将每个节点的响应收集整理成单个响应,返回给客户端
【图片】
用单个 mget 请求取回多个文档所需的步骤顺序:
- 客户端向 Node 1 发送 mget 请求。
- Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。
可以对 docs 数组中每个文档设置 routing 参数。
bulk API,允许在单个批量请求中执行多个创建、索引、删除和更新请求。
【图片】
bulk API 按如下步骤顺序执行:
- 客户端向 Node 1 发送 bulk 请求。
- Node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。
- 主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。
4.6 分片原理
分片是 Elasticsearch 最小工作单元。传统数据库字段存单个值,不满足全文检索需求(需搜索文本字段每个单词,即单字段索引多值),而倒排索引是支持这一需求的最佳数据结构。
4.6.1 倒排索引
什么是倒排索引:
传统索引(文档里有什么关键字)如 doc :[ 苹果 香蕉 葡萄]
倒排索引(关键字有哪些文档) 如 苹果 :[doc1、doc 2 、doc3 ]
4.6.2 文档搜索
早期全文检索会为整个文档集合建一个大的倒排索引存到磁盘,新索引就绪就替换旧的,这样能检索到最新内容。
倒排索引写入磁盘后就不能改了。
不变性有这些好处:
- 不用锁,因为从不更新,就不用担心多进程同时改数据。
- 索引一旦进入内核的文件系统缓存,就会一直留在那(因为不变)。只要缓存空间够,大部分读请求直接用内存,不用读磁盘,性能提升明显。
- 像 filter 缓存这类缓存,在索引存在期间一直能用,不用因为数据变了而重建。
- 写入一个大的倒排索引能压缩数据,减少磁盘读写和内存缓存的用量。
4.6.3 动态更新索引
不直接修改原有不可变的倒排索引,而是新增一个包含更新内容的新段
删除操作标记文档为删除状态,查询时过滤掉标记文档
更新操作实际是先标记旧文档为删除,再索引新文档
后台定期合并分段,彻底移除已删除文档,优化存储和查询效率
4.6.4 近实时搜索
- 新文档先写入内存缓冲区,同时记录事务日志
- 缓冲区满(或定时)时,数据被写入新的段文件并刷新到磁盘(refresh),此时文档可被搜索(未提交)
- 定期(或手动)执行 flush,将事务日志清空,段文件正式提交
- 刷新(refresh)默认 1 秒一次,这就是 “近实时”(非实时但接近实时)的原因
- 多个小分段会定期合并为大分段,优化查询性能
4.6.5 持久化变更
ES 通过事务日志(Translog)和刷新(Flush)机制实现持久化变更:
- 文档写入时,先写入内存缓冲区,同时同步记录到事务日志(确保数据不丢失)。
- 内存数据刷新(Refresh)到磁盘生成段文件时,事务日志暂不清除。
- 定期或手动执行 Flush 操作:将所有内存数据写入磁盘,清空事务日志,完成索引的正式提交。
4.6.6 段合并
ES 的段合并原理:
- 索引更新会不断生成新的小分段(段是倒排索引的最小存储单元),过多小分段会影响查询效率。
- 后台定期启动合并进程,将多个小分段合并为一个大分段。
- 合并时保留有效文档,彻底移除标记为删除的文档,同时压缩数据。
- 新大分段生成后,替换原小分段并删除,提升查询性能和存储空间利用率。
【图片】
4.7 文档分析
分析器执行上面的工作。分析器实际上是将三个功能封装到了一个包里:
- 字符过滤器:分词前处理字符串,如去除 HTML、将 & 转为 and。
- 分词器:将字符串拆分为单个词条,如按空格和标点拆分。
- Token 过滤器:处理词条,删除无用词(a、and、了、吗、 等)、添加同义词(jump 和 leap)。
4.7.1 内置分析器
Elasticsearch 有多种可直接使用的预包装分析器,以下为主要分析器对字符串 “Set the shape to semi-transparent by calling set_trans (5)” 的处理结果:
- 标准分析器:默认分析器,按 Unicode 单词边界划分,去除大部分标点并小写,产生:set, the, shape, to, semi, transparent, by, calling, set_trans, 5
- 简单分析器:非字母处分隔文本,词条小写,产生:set, the, shape, to, semi, transparent, by, calling, set, trans
- 空格分析器:按空格划分文本,产生:Set, the, shape, to, semi-transparent, by, calling, set_trans (5)
- 语言分析器:针对特定语言设计(如英语),会删除无用词、提取词干,英语分析器产生:set, shape, semi, transpar, call, set_tran, 5(transparent、calling、set_trans 变为词根格式)
4.7.2 分析器使用场景
索引时,全文域被分析为词条建倒排索引;搜索全文域时,查询字符串需经相同分析,确保词条格式一致。
全文查询按域定义处理:
- 查询全文域:用相同分析器处理查询字符串,生成正确搜索词条。
- 查询精确值域:不分析查询字符串,直接搜索指定值。
4.7.3 测试一下内置分析器
GET http://localhost:9200/_analyze
{
"analyzer": "standard",
"text": "Text to analyze"
}
结果中每个元素代表一个单独的词条:
{
"tokens": [
{
"token": "text",
"start_offset": 0,
"end_offset": 4,
"type": "<ALPHANUM>",
"position": 1 # 指明词条在原始文本中出现的位置
},
{
"token": "to",
"start_offset": 5,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "analyze",
"start_offset": 8,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 3
}
]
}
token 是实际存储到索引中的词条。 position 指明词条在原始文本中出现的位置。 start_offset 和 end_offset 指明字符在原始字符串中的位置。
4.7.4 指定分析器
当Elasticsearch在你的文档中检测到一个新的字符串域,它会自动设置其为一个全文 字符串 域,使用 标准 分析器对它进行分析。你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域—不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。要做到这一点,我们必须手动指定这些域的映射。
4.7.5 IK分词器
首先我们通过Postman发送GET请求查询分词效果
默认分析器:
GET http://localhost:9200/_analyze
{
"text":"测试单词"
}
ES的默认分词器无法识别中文中测试、单词这样的词汇,而是简单的将每个字拆完分为一个词
结果是: 测 试 单 词
这样的结果显然不符合我们的使用要求,所以我们需要下载ES对应版本的中文分词器。
我们这里采用IK中文分词器,下载地址为: https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.8.0
将解压后的后的文件夹放入ES根目录下的plugins目录下,重启ES即可使用。
我们这次加入新的查询参数"analyzer":“ik_max_word”
ik_max_word:会将文本做最细粒度的拆分
ik_smart:会将文本做最粗粒度的拆分
GET http://localhost:9200/_analyze
{
"text":"测试单词",
"analyzer":"ik_max_word"
}
结果: 测试 单词
ES中也可以进行扩展词汇,首先查询
GET http://localhost:9200/_analyze
{
"text":"弗雷尔卓德",
"analyzer":"ik_max_word"
}
结果:弗 雷 尔 卓 德
如下添加词汇后:
结果:弗雷尔卓德
首先进入ES根目录中的plugins文件夹下的ik文件夹,进入config目录,创建custom.dic文件,写入弗雷尔卓德。同时打开IKAnalyzer.cfg.xml文件,将新建的custom.dic配置其中,重启ES服务器。
【图片】
4.7.6 自定义分析器
可以自己写分词器。这里略过。
4.8 文档修改时冲突
并发修改的时候会采用乐观锁版本号来控制,版本号不对返回失败:
解决方式:
- 悲观控制:锁资源防冲突(ES 不用)。
- 乐观控制:ES 默认用,通过版本号(旧版用_version,新版用 if_seq_no 和 if_primary_term)确保顺序,版本不符则更新失败,由应用处理(重试等)。