ElasticSearch07-分片读写原理
1、数据读取流程
(1)读取流程
- 客户端请求:客户端发起 HTTP请求到Elasticsearch集群。
- 路由计算:请求首先到达一个协调节点(Coordinator Node),该节点根据路由算法计算出分片位置,将请求分发到相关的分片,它会在所有相关的主分片和副本分片上并行执行。
- 分片查询与分词:每个分片在本地执行查询。在这个阶段,文本字段会被分词器(Analyzer)处理,将文本分解为单个的词元(Tokens),以便进行搜索。这个过程涉及到同义词扩展,如果字段配置了同义词,相关的词元会被扩展为同义词。
- 计算权重(Score):查询过程中,Elasticsearch会为每个匹配的文档计算一个权重(Score),这个权重代表了文档与查询条件的相关性。权重计算考虑了多种因素,包括词频(TF)、逆文档频率(IDF)等。
- 结果合并与排序:协调节点收集所有分片的结果,并根据权重进行排序。此外,还可以根据其他字段进行排序,如日期或数字字段。
- 返回响应:协调节点将排序后的全局结果集打包成HTTP响应返回给客户端。响应体包含查询结果的JSON数据,如匹配的文档列表、总命中数等。
- 客户端处理结果:客户端接收到响应后,解析HTTP响应体中的JSON数据,提取查询结果,并可以根据应用程序的需求对结果进行进一步处理。
(2)协调节点功能
- 请求路由:协调节点负责将客户端的请求路由到正确的分片上。当客户端发送一个查询请求时,协调节点会根据文档的
_id
或_routing
字段计算出应该查询哪个分片。 - 查询分发:对于需要在多个分片上执行的操作(如搜索、聚合等),协调节点会将请求分发到所有相关的分片上。
- 结果聚合:协调节点会收集所有分片返回的结果,进行必要的聚合和排序,然后将最终结果返回给客户端。
- 负载均衡:协调节点可以在多个节点之间分发请求,以平衡负载并提高性能。
- 故障转移:如果某个分片所在的节点不可用,协调节点会检测到这种情况,并可能将请求重定向到该分片的其他副本上。
- 简化客户端逻辑:客户端不需要知道集群的内部结构,也不需要知道数据存储在哪个节点上。客户端只需将请求发送到任何一个节点(通常是任意一个协调节点),由协调节点负责后续的处理。
- 透明性:对于客户端来说,协调节点的工作是透明的。客户端不需要关心集群的分片和副本细节,只需与协调节点交互。
(3)路由计算
shard_num = hash(_routing) % num_primary_shards
- **_routing:**是一个可变值,默认是文档的
_id
,也可以设置成一个自定义的值。num_primary_shards
是索引的主分片数。 - 哈希函数:
_routing
通过哈希函数生成一个数字,然后这个数字再除以主分片的数量后得到余数。这个余数就是文档所在分片的位置。 - 分片数量的影响:这就解释了为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。
- 自定义路由:所有的文档API(如get、index、delete、bulk、update以及mget)都接受一个叫做routing的路由参数,通过这个参数我们可以自定义文档到分片的映射。
- 分区路由:Elasticsearch允许使用同一路由值的数据分发到多个分片。在索引设置中启用此功能:
"index.routing_partition_size": N
- **分区路由公式:**通过引入
_id
哈希值和 routing_partition_size
参数,使得具有相同路由值的文档更均匀地分布到多个分片。
shard_num = (hash(_routing) + hash(_id) % routing_partition_size) % num_primary_shards
- 路由值的影响:对于同一个routing值,
hash(_routing)
的结果固定的,hash(_id) % routing_partition_size
的结果有 routing_partition_size
个可能的值,两个组合在一起,对于同一个routing值的不同doc,也就能计算出 routing_partition_size
可能的shard num了,即一个shard集合。
2、数据写入流程
(1)写入流程
- 客户端请求:客户端发起 HTTP请求到Elasticsearch集群。
- 路由计算:请求首先到达一个协调节点(Coordinator Node),该节点根据路由算法计算出分片位置,将请求分发到相关的分片,它会在所有相关的主分片和副本分片上并行执行。
- 数据写入:数据首先被写入内存中的Index Buffer缓冲区和磁盘上的Translog日志文件。Index Buffer作为快速访问的缓存,而Translog确保数据的持久性和恢复能力。
- 副本分片同步:主分片将文档的变更同步到所有的副本分片。副本分片分布在集群的其他节点上,以提供数据冗余。
- 确认和响应:一旦主分片和所有副本分片都确认了文档的写入,协调节点就会向客户端发送一个成功响应。如果任何副本分片因为任何原因未能确认,写入操作可能会失败,并且协调节点会向客户端发送一个错误响应。
- 集群状态更新:写入操作完成后,集群的元数据会更新,以反映新的文档和分片状态。
- 写入刷新:默认情况下,Elasticsearch每秒将Index Buffer中的数据刷新到一个新的内存中的段,此时段被打开并可供搜索使用。没刷到段中的时候数据是不可访问的,所以 es 是近实时。
- 写入合并:随着新数据的不断写入,会产生许多小段。Lucene会定期将这些小段合并成大的段,以优化搜索性能和减少资源消耗。
- Flush操作:当Translog文件达到一定大小(默认512MB),会触发Flush操作。Index Buffer中的数据被写入磁盘。
(2)主分片同步到副本分片
- 写入主分片:
- 当客户端发送写入请求到Elasticsearch集群时,协调节点(Coordinating Node)会根据文档的
_id
和索引的设置(如分片数量)来确定文档应该写入到哪个主分片。 - 协调节点将请求转发给该主分片所在的数据节点,数据节点上的主分片接收到请求后,会先将文档写入到内存中的Lucene索引结构里。
- 异步同步到副本分片:
- 一旦文档被写入到主分片,主分片会开始将数据异步同步到其对应的副本分片上。这是为了保证数据的冗余和可用性。
- 副本分片是主分片的完整拷贝,它们可以处理搜索请求并提供数据恢复的能力。当主分片不可用时,副本分片可以被提升为新的主分片。
- 并行复制:如果主分片写入成功,它会将请求并行转发到所有的副本分片(Replica Shard)所在的节点,等待副本分片写入成功。这是为了保证数据的冗余和一致性。
- 确认写入成功:
- 一旦所有副本分片都成功写入了数据,主分片节点会向协调节点报告成功,随后协调节点向客户端返回写入成功的响应。
- 在某些配置下,比如
index.write.wait_for_active_shards
设置为1
,只要主分片写入成功,协调节点就会向客户端发送成功的响应,而不需要等待所有副本分片都完成同步。
- 使用事务日志(Translog):Elasticsearch使用事务日志(Translog)来确保在发生故障时不会丢失数据。在主分片上执行写入请求的过程中,事务日志会记录所有的写入操作,以便在需要时可以从日志中恢复数据。
- 副本分片的响应:Node 2、Node 3写入成功数据成功后,发送ack信息给主分片所在的Node 1节点。Node 1节点再将ack信息发送给coordinate节点,coordinate节点发送ack节点给客户端。
(3)副本不足的处理方式
- 副本分片未分配:如果集群中的节点数量不足以容纳所有的副本分片,那么这些副本分片会被标记为未分配(Unassigned)。这意味着数据的冗余性和高可用性会受到影响,直到有足够的节点来承载这些副本分片。
- 集群状态变为黄色:当所有的主分片都正常运行,但是副本分片没有全部处在正常状态时,集群的健康状态会变为黄色(Yellow)。这表示集群可以正常服务所有请求,但是副本分片没有全部被分配。
- 写入操作可能被阻止:在某些情况下,如果副本分片长时间未分配,Elasticsearch可能会阻止新的写入操作以保护数据的一致性。这种情况下,需要采取措施来恢复副本分片的正常分配。
- 手动调整副本数量:可以通过API调用来手动减少副本数量,以适应当前集群的节点数量。这样做可以临时解决副本分片未分配的问题,但会降低数据的冗余性。
- 增加节点或减少副本数:为了解决分片数量过多、节点数量不足的问题,可以通过增加节点或减少副本数来解决。可以通过以下命令调整副本数:
PUT /<index_name>/_settings
{
"number_of_replicas": 1
}
- 重新启用分片分配:如果分片分配被禁用,可以使用以下命令重新启用:
PUT /_cluster/settings
{
"persistent": {
"cluster.routing.allocation.enable": "all"
}
}
- 使用Cluster Reroute API:如果分片数据丢失导致未分配,可以使用Cluster Reroute API强制分配分片并重新索引丢失的数据。如下,这可以强制分配未分配的分片,但可能会涉及到数据丢失的风险。
POST /_cluster/reroute
{
"commands": [
{
"allocate_stale_primary": {
"index": "<index_name>",
"shard": 0,
"node": "<node_name>",
"accept_data_loss": true
}
}
]
}
3、批量操作流程
(1)mget 批量读取
特性/操作 | mget (批量读取) | get (单个文档读取) |
---|
请求方式 | 一次性获取多个文档 | 每次请求获取单个文档 |
性能 | 减少网络往返次数,提高效率 | 每次请求都需要网络往返 |
处理过程 | 协调节点异步发送请求到多个分片 | 请求直接发送到负责特定文档ID的分片 |
返回结果 | 文档顺序与请求顺序一致,可能包含失败结果 | 成功返回单个文档,失败返回错误响应 |
部分结果 | 如果分片失败,会响应部分结果 | 请求失败则整个请求失败,不返回部分结果 |
过滤和路由 | 允许对返回的数据进行过滤,可以指定返回整个文档、部分字段或不返回_source字段 | 可以使用路由字段,但针对单个文档 |
并行处理 | 各个get 项可以并行处理 | 顺序处理,每次处理一个文档 |
(2)bulk API 批量写入
特性/操作 | bulk API (批量操作) | POST 单个文件 (单个操作) |
---|
请求方式 | 一次性执行多个索引、删除、更新操作 | 每次请求执行单个操作 |
性能 | 减少网络开销,提高索引速度 | 每次请求都需要网络往返,效率较低 |
处理过程 | 协调节点将批量请求分发到不同分片 | 协调节点将请求直接发送到目标分片 |
返回结果 | 对每一条操作都返回结果,即使某些操作失败 | 成功返回单个操作结果,失败返回错误响应 |
部分结果 | 可以返回部分结果,即使某些操作失败 | 操作失败则整个请求失败,不返回部分结果 |
并行处理 | 多个操作可以并行处理 | 顺序处理,每次处理一个操作 |
格式要求 | 严格遵守NDJSON格式,操作和元数据之间用换行符分隔 | 单个JSON对象,格式要求较为简单 |
错误处理 | 对于失败的操作,返回错误信息,但不影响其他操作 | 单个操作失败则整个请求失败 |
适用场景 | 适合大量数据的批量写入 | 适合单个文档的写入或查询 |
4、倒排索引原理
(1)基本概念
- 倒排索引(Inverted Index)是信息检索系统中常用的数据结构,它使得在大规模文本集合中快速检索单词变得可能。
- 倒排索引是一个从单词到文档列表的映射,记录了每个单词出现在哪些文档中,以及在每个文档中出现的位置(位置可以是单词的索引位置、句子位置等)。
(2)构建过程
- 分词(Tokenization):首先对文档进行分词,将文本内容分解成一系列的单词(Tokens)。
- 标准化(Normalization):对单词进行标准化处理,包括转小写、去除停用词、词干提取(Stemming)等。
- 构建索引项(Term):将处理后的单词称为索引项,每个索引项对应一个或多个文档。
- 记录文档列表:对于每个索引项,记录包含该索引项的所有文档的列表,以及每个文档中索引项出现的次数和位置信息。
(3)数据结构
- 倒排表(Inverted Table):一个哈希表,键是索引项,值是包含该索引项的文档列表。
- 倒排列表(Inverted List):对于每个索引项,倒排列表记录了所有包含该索引项的文档的详细信息,如文档ID、出现次数(Term Frequency, TF)、位置信息等。
(4)优化和压缩
- 为了提高存储效率和查询性能,倒排索引会进行优化和压缩,例如使用变长编码(Variable-Length Encoding)存储位置信息,或者对倒排列表进行分块存储以减少内存占用。
(5)查询处理
- 当用户提交查询时,系统会对查询中的每个单词构建查询条件,并在倒排索引中查找匹配的文档列表。
- 系统会合并这些查询条件,使用布尔逻辑(AND、OR、NOT)等操作符来确定最终的文档集合。
- 根据评分机制(如TF-IDF、BM25等)对结果进行排序,返回最相关的文档。
(6)更新和维护
- 随着新文档的加入和旧文档的删除,倒排索引需要不断更新和维护。这涉及到添加新的索引项、更新倒排列表和删除过时的索引项。
5、倒排索引案例
(1)倒排索引的构建
- 假设我们有以下三个文档:
- Document 1: “The quick brown fox jumps over the lazy dog”
- Document 2: “The fox is very quick and jumps high”
- Document 3: “The lazy dog sleeps all day”
- Document 4: “Quick brown dogs are not lazy”
- **步骤 1: 分词:**首先,我们需要对每个文档进行分词,将文档中的文本分解成一系列的词汇(Tokens)。
- “The”, “quick”, “brown”, “fox”, “jumps”, “over”, “the”, “lazy”, “dog”
- “The”, “fox”, “is”, “very”, “quick”, “and”, “jumps”, “high”
- “The”, “lazy”, “dog”, “sleeps”, “all”, “day”
- “Quick”,“brown”,“dogs”,“are”,“not”,“lazy”
- **步骤 2: 构建索引:**然后,我们构建倒排索引,将每个词映射到包含该词的文档列表。
- “The”: [1, 2, 3, 4]
- “quick”: [1, 2, 4]
- “brown”: [1, 4]
- “fox”: [1, 2]
- “jumps”: [1, 2]
- “over”: [1]
- “lazy”: [1, 3]
- “dog”: [1, 3, 4]
- “is”: [2]
- “very”: [2]
- “and”: [2]
- “high”: [2]
- “sleeps”: [3]
- “all”: [3]
- “day”: [3]
- “not”: [4]
(2)倒排索引的查询
- 现在,我们有一个查询:“quick brown” OR “lazy dog”。这意味着我们想要找到同时包含 “quick” 和 “brown” 的文档,或者包含 “lazy” 和 “dog” 的文档。
- 步骤 1: 分别查询每个部分
- 查询 “quick brown”:
- “quick”: [1, 2, 4]
- “brown”: [1, 4]
- 交集(AND): [1, 4]
- 查询 “lazy dog”:
- “lazy”: [1, 3]
- “dog”: [1, 3, 4]
- 交集(AND): [1, 3]
- 步骤 2: 合并结果
- 现在,我们有两个结果集,一个是 “quick brown” 的结果 [1, 4],另一个是 “lazy dog” 的结果 [1, 3]。我们需要使用 OR 操作来合并这两个结果集。
- 合并(OR): [1, 3, 4]
- 步骤 3: 返回结果
- 最终,查询 “quick brown” OR “lazy dog” 的结果是文档 1、文档 3 和文档 4。
- 结果解释
- Document 1: 同时包含 “quick” 和 “brown”,以及 “lazy” 和 “dog”。
- Document 3: 包含 “lazy” 和 “dog”。
- Document 4: 同时包含 “quick” 和 “brown”。