Elasticsearch分布式工作原理
前言
Elasticsearch 是分布式的,但是对于我们开发者来说并未过多的参与其中,我们只需启动对应数量的节点,并给它们分配相同的 cluster.name 让它们归属于同一个集群,创建索引的时候只需指定索引主分片数和 副分片数 即可,其他的都交给了ES 内部自己去实现
这和数据库的分布式和 同源的 solr 实现分布式都是有区别的,数据库要做集群分布式,比如分库分表需要我们指定路由规则和数据同步策略等,包括读写分离,主从同步等,solr的分布式也需依赖 zookeeper,但是 Elasticsearch 完全屏蔽了这些
虽然Elasticsearch 天生就是分布式的,并且在设计时屏蔽了了分布式的复杂性,但是我们还得知道它内部的原理
节点交互原理
es和其他中间件一样,比如mysql,redis有master-slave模式。es集群也会选举一个节点做为master节点
master节点它的职责是维护全局集群状态,在节点加入或离开集群的时候重新分配分片
所有文档级别的写操作不会与master节点通信,master节点并不需要涉及到文档级别的变更和搜索等操作,es分布式不太像mysql的master-slave模式,mysql是写在主库,然后再同步数据到从库。而es文档写操作是分片上而不是节点上,先写在主分片,主分片再同步给副分片,因为主分片可以分布在不同的节点上,所以当集群只有一个master节点的情况下,即使流量的增加它也不会成为瓶颈,就算它挂了,任何节点都有机会成为主节点
读写可以请求任意节点,节点再通过转发请求到目的节点,比如一个文档的新增,文档通过路由算法分配到某个主分片,然后找到对应的节点,将数据写入到主分片上,然后再同步到副分片上
写入文档
1、客户端向node-1发送新增文档请求
2、节点通过文档的路由算法确定该文档属于主分片-P0。因为主分片-P0在node-3,所以请求会转发到node-3
3、文档在node-3的主分片-P0上新增,新增成功后,将请求转发到node-1和node-2对应的副分片-R0上。一旦所有的副分片都报告成功,node-3向node-1报告成功,node-1向客户端报告成功
读取文档
- 客户端向node-1发送读取文档请求
- 在处理读取请求时,node-1在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡
当新增一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?
路由算法
shard = hash(routing) % number_of_primary_shards
routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置
这就解释了为什么我们要在创建索引的时候就确定好主分片的数量 并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了
PUT /nba/_doc/1
{
"name": "哈登",
"team_name": "火箭",
"position": "得分后卫",
"play_year": "10",
"jerse_no": "13"
}
查看文档在哪个分片上
GET /nba/_search_shards?routing=1
{
"nodes" : {
"naMWyY9TT6OPG56PmI7s1w" : {
"name" : "ADMINISTRATOR",
"ephemeral_id" : "j9EoNmTTQ4mTx-oqDeDiAw",
"transport_address" : "127.0.0.1:9300",
"attributes" : {
"ml.machine_memory" : "16862834688",
"xpack.installed" : "true",
"ml.max_open_jobs" : "20"
}
}
},
"indices" : {
"nba" : { }
},
"shards" : [
[
{
"state" : "STARTED",
"primary" : true,
"node" : "naMWyY9TT6OPG56PmI7s1w",
"relocating_node" : null,
"shard" : 0,
"index" : "nba",
"allocation_id" : {
"id" : "YFJzkvrJQZWAfqV5jm6M1g"
}
}
]
]
}
锁的简单分类
顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,比如可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,因为我们elasticsearch一般业务场景都是写少读多,所以通过乐观锁可以在控制并发的情况下又能有效的提高系统吞吐量
版本号乐观锁
GET /nba/_doc/1
{
"_index": "nba",
"_type": "_doc",
"_id": "1",
"_version": 1,
"_seq_no": 4,
"_primary_term": 7,
"found": true,
"_source": {
"name": "哈登",
"team_name": "⽕火箭",
"position": "得分后卫",
"play_year": "10",
"jerse_no": "13"
}
}
POST /nba/_doc/1?version=2&version_type=external
{
"name": "哈登",
"team_name": "火箭",
"position": "得分后卫",
"play_year": "10",
"jerse_no": "13"
}
例子一
PUT test/_doc/1
{
"msg":"乔丹是篮球之神"
}
POST /test/_search
{
"query": {
"match": {
"msg": "乔丹"
}
}
}
例子二
PUT test/_mapping
{
"properties": {
"msg_chinese": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}POST test/_doc/1
{
"msg": "乔丹是篮球之神",
"msg_chinese": "乔丹是篮球之神"
}POST /test/_search
{
"query": {
"match": {
"msg_chinese": "乔"
}
}
}POST /test/_search
{
"query": {
"match": {
"msg": "乔"
}
}
}
写时分词
POST test/_analyze
{
"field": "msg",
"text": "乔丹是篮球之神"
}乔,丹,是,篮,球,之,神
POST test/_analyze
{
"field": "msg_chinese",
"text": "乔丹是篮球之神"
}乔丹, 是, 篮球, 之神
读时分词
由于读时分词器默认与写时分词器默认保持一致,拿上面的例子,你搜索 msg 字段,那么读时分词器为 Standard ,搜索 msg_chinese 时分词器则为 ik_max_word。这种默认设定也是非常容易理解的,读写采用一致的分词器,才能尽最大可能保证分词的结果是可以匹配的
POST test/_search
{
"query": {
"match": {
"msg_chinese": {
"query": "乔丹",
"analyzer": "standard"
}
}
}
}
一般来讲不需要特别指定读时分词器,如果读的时候不单独设置分词器,那么读时分词器的验证方法与写时一致
深入分析
分析器(analyzer)有三部分组成
char filter : 字符过滤器
tokenizer : 分词器
token filter :token过滤器
字符过滤器以字符流的形式接收原始文本,并可以通过添加、删除或更改字符来转换流。一个分析器可能有0个或多个字符过滤器
一个分词器接收一个字符流,并将其拆分成单个token (通常是单个单词),并输出一个token流。比如使用whitespace分词器当遇到空格的时候会将文本拆分成token。"eating an apple" >> [eating, and, apple]。一个分析器必须只能有一个分词器
POST _analyze
{
"text": "eating an apple",
"analyzer": "whitespace"
}
token filter (token过滤器)
- tokenizer
Stanard tokenizer
- token filters
Standard Token Filter
Lower Case Token Filter