Why Multiple Elasticsearch Nodes Fail to Form a Cluster: master_not_discovered_exception or NotMasterException and How to Fix It

If you’ve encountered a situation where multiple Elasticsearch nodes fail to automatically form a cluster and the logs show the error: “master_not_discovered_exception”, you are not alone.

The Problem

The root cause is often related to system administrators cloning virtual machines to create multiple Elasticsearch servers. When this happens, every Elasticsearch node ends up with the same node ID, and as a result, the cluster cannot successfully elect a master node.

Verifying the Issue

You can verify this issue by listing all the node IDs with the following command:

GET /_cat/nodes?v&h=id,ip,name&full_id=true

However, since the Elasticsearch cluster hasn’t formed, you need to query each node individually, like this:

curl 192.168.110.111:9200/_cat/nodes?v&h=id,ip,name&full_id=true
curl 192.168.110.112:9200/_cat/nodes?v&h=id,ip,name&full_id=true

The Solution

Elasticsearch requires each node to have a unique node ID. To fix this issue, you need to delete the index data on each node. If Elasticsearch was installed using the RPM package, the index data is usually stored in /var/lib/elasticsearch by default. After deleting the data, restart Elasticsearch, and it will generate a new, unique node ID for each node.

Reference

For further details, check the full article here: https://www.656463.com/wenda/jdbhjrjqNotMasterExceptionqgddxc_359.

_seq_no和_primary_term这两个字段在Elasticsearch里的作用是什么?

它们用来实现基于乐观锁的版本控制。

Elasticsearch跟踪上次操作的序列号(sequence number)和主项(primary term),以更改它存储的每个文档。通过GET API响应信息中的_seq_no和_primary_term字段返回序列号和主项。

_primary_term

在故障转移期间,每当不同的分片成为主分片时,primary term的值就会增加。这有助于解决重新联机的旧的主分片上发生的更改,与新的主分片上发生的更改的冲突问题,一般新的主分片会获胜。

这里我们要先理解Elasticsearch的高性能和高可用机制:一个索引的数据可以存储到多个分片上,以均衡负载,提升读写性能;而每个分片(主分片)又可以有多个副本(副分片),在主分片脱机后Elasticsearch可以快速把其中一个副分片推举为新的主分片,保障Elasticsearch集群的高可用性。

副分片的primary term只是主分片更改次数的计数器。

这些primary term是递增的,并且在主分片升级时会发生变化。它们作为集群状态的一部分被持久化,因此代表了集群所在的主分片的一种“版本号”或“年代(generation)”。

为了确保文档的旧版本不会覆盖新版本,对文档执行的每个操作都由对应更改的主分片分配一个序列号。

假设你的索引由5个主分片组成(在Elasticsearch 7之前是默认的)。索引(新增文档)和更新的请求是针对主分片执行的。如果你有多个主分片,Elasticsearch能够将传入请求(例如,巨大的批量插入文档的请求)并行化/分发到多个分片,以提高性能。

因此,_primary_term字段提供了关于执行或协调更改主分片(本例中为#1、#2、#3、#4或#5)的信息。

_seq_no

序列号在Elasticsearch 6.0.0中引入。

一旦我们对primary term进行了保护,我们就添加了一个简单的计数器,并开始向每个操作发出该计数器的序列号。因此,这些序列号使我们能够理解发生在主分片上的对索引的操作。

_version字段存储了一个序列号,用于统计文档更新的次数。_seq_no字段也是一个序列号,用于统计索引上发生的操作次数。因此,如果你创建第二个文档,你将看到该文档的_seq_no和第一个文档的_seq_no有所不同。

实例

创建多个文档:

POST test/_doc/_bulk
{"index": {}}
{"test": 1}
{"index": {}}
{"test": 2}
{"index": {}}
{"test": 3}

返回响应信息:

{
  "took" : 166,
  "errors" : false,
  "items" : [
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "d2zbSW4BJvP7VWZfYMwQ",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 0,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "eGzbSW4BJvP7VWZfYMwQ",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 1,
        "_primary_term" : 1,
        "status" : 201
      }
    },
    {
      "index" : {
        "_index" : "test",
        "_type" : "_doc",
        "_id" : "eWzbSW4BJvP7VWZfYMwQ",
        "_version" : 1,
        "result" : "created",
        "_shards" : {
          "total" : 2,
          "successful" : 1,
          "failed" : 0
        },
        "_seq_no" : 2,
        "_primary_term" : 1,
        "status" : 201
      }
    }
  ]
}

正如你所看到的:

  • 对于所有文档,版本号_version为1;
  • 对于文档1,_seq_no为0(第一次索引操作);
  • 对于文档2,_seq_no为1(第二次索引操作);
  • 对于文档3,_seq_no为2(第三次索引操作)。

在Elasticsearch的文档中如何管理嵌套对象(nested objects)?

本文翻译自《How to manage nested objects in Elasticsearch documents》。

如何使用Update API和painless脚本在Elasticsearch文档中添加、更新和删除嵌套对象。

2019/05 /02

在这篇文章中,我们将管理使用Elasticsearch索引的文档的嵌套对象(nested objects)

嵌套类型(nested)是对象类型(object)的一个特殊版本,它对对象数组进行索引,以使数组的元素可以被相互独立地查询。

先决条件

要跟随此帖子继续下去,你需要:

  • 一个正在运行的Elasticsearch实例。我在这里用6.7版本
  • 一个正在运行的Kibana实例,用于与Elasticsearch交互

准备

我们创建一个索引iridakos_nested_objects,它有一个名为human的类型(type)(Elasticsearch 8开始没有type这个概念了),其中有一个嵌套对象cats。

创建这个索引

打开Kibana开发控制台并键入以下内容以创建这个索引。

PUT iridakos_nested_objects
{
  "mappings": {
    "human": {
      "properties": {
        "name": {
          "type": "text"
        },
        "cats": {
          "type": "nested",
          "properties": {
            "colors": {
              "type": "integer"
            },
            "name": {
              "type": "text"
            },
            "breed": {
              "type": "text"
            }
          }
        }
      }
    }
  }
}

// ES 8版本的语句
PUT iridakos_nested_objects
{
  "mappings": {
      "properties": {
        "name": {
          "type": "text"
        },
        "cats": {
          "type": "nested",
          "properties": {
            "colors": {
              "type": "integer"
            },
            "name": {
              "type": "text"
            },
            "breed": {
              "type": "text"
            }
          }
        }
      }
    }
}

返回响应信息:

{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "iridakos_nested_objects"
}

human具有:

  • text类型的name属性
  • 嵌套类型(nested)的cats属性

每只cat都有:

  • integer类型的colors属性
  • text类型的name属性
  • text类型的一个品种特性

添加一个human

在Kibana控制台中,执行以下操作以添加一个human和三只cat。

POST iridakos_nested_objects/human/1
{
  "name": "iridakos",
  "cats": [
    {
      "colors": 1,
      "name": "Irida",
      "breed": "European Shorthair"
    },
    {
      "colors": 2,
      "name": "Phoebe",
      "breed": "European"
    },
    {
      "colors": 3,
      "name": "Nino",
      "breed": "Aegean"
    }
  ]
}

// ES 8版本的语句
PUT iridakos_nested_objects/_doc/1
{
  "name": "iridakos",
  "cats": [
    {
      "colors": 1,
      "name": "Irida",
      "breed": "European Shorthair"
    },
    {
      "colors": 2,
      "name": "Phoebe",
      "breed": "European"
    },
    {
      "colors": 3,
      "name": "Nino",
      "breed": "Aegean"
    }
  ]
}

查询看看是否真的插入成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

你应该能看到类似以下的响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 1,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European"
      },
      {
        "colors": 3,
        "name": "Nino",
        "breed": "Aegean"
      }
    ]
  }
}

管理嵌套对象

添加一个新的嵌套对象

假设iridakos得到了一只新的波斯猫,名叫Leon。要将其添加到iridakos索引的cats集合中,我们将使用更新API。 在Kibana:

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "ctx._source.cats.add(params.cat)",
    "params": {
      "cat": {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "ctx._source.cats.add(params.cat)",
    "params": {
      "cat": {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    }
  }
}

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 1,
  "_primary_term": 1
}

注意事项:

  • 我们使用ctx._source.cats访问了human的嵌套对象cats。这给了我们一个集合(collection)
  • 我们对集合执行了add方法以添加一只新的cat
  • 新cat(params.cat)作为参数传递给add方法,其属性在params中给出。

查询看看是否真的插入成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 2,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European"
      },
      {
        "colors": 3,
        "name": "Nino",
        "breed": "Aegean"
      },
      {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    ]
  }
}

可见,已插入Leon到cats集合。

删除一个嵌套对象

假设我们想把Nino从cats集合中移除。在Kibana:

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "ctx._source.cats.removeIf(cat -> cat.name == params.cat_name)",
    "params": {
      "cat_name": "Nino"
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "ctx._source.cats.removeIf(cat -> cat.name == params.cat_name)",
    "params": {
      "cat_name": "Nino"
    }
  }
}

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 3,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 2,
  "_primary_term": 1
}

注意事项:

  • 我们使用ctx._source.cats访问了嵌套对象cat。这给了我们一个集合(collection)
  • 我们对集合执行removeIf方法,以有条件地移除里面的条目项
  • 我们为removeIf方法提供了一个谓词(Predicate),在该方法中指定要删除的条目项。此谓词将在集合的每个条目项上执行,并返回为布尔值。如果返回true,则该条目项将被删除。在我们的例子中,条件是对cat的name属性进行简单的相等性检查。
  • cat_name是作为参数(params.cat_name)传递的,而不是将其固定到source脚本里。

执行以下语句查看一下是否删除Nino成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 3,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European"
      },
      {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    ]
  }
}

可见name为Nino的cat信息已在cats集合中删除。

更新嵌套对象

假设我们想把所有的猫品种从欧洲(European)改为欧洲短毛猫(European Shorthair)(在我们的案例中目前只有Phoebe的品种是European)。

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> cat.breed == params.current_breed); for(cat in targets) { cat.breed = params.breed }",
    "params": {
      "current_breed": "European",
      "breed": "European Shorthair"
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> cat.breed == params.current_breed); for(cat in targets) { cat.breed = params.breed }",
    "params": {
      "current_breed": "European",
      "breed": "European Shorthair"
    }
  }
}

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 4,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 3,
  "_primary_term": 1
}

注意事项:

  • 我们使用ctx._source.cats访问了嵌套对象cat。这给了我们一个集合(collection)
  • 我们对cats集合执行了findAll方法来选择特定的条目项
  • 我们为findAll方法提供了一个谓词(Predicate),在其中我们指定要选择的条目项。此谓词将在集合的每个条目项上执行,并返回布尔值。如果返回true,就会选择该条目项。在我们的例子中,条件是对猫的品种(breed)属性进行简单的相等性检查。
  • current_breed作为参数(params.current_breed)传递,而不是将其固定到source脚本里。
  • 然后,我们循环遍历选中的猫(其品种属性值为European),并将它们的品种更改为我们通过另一个参数params.breed传递的新值。

查看是否更新成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 5,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European Shorthair"
      },
      {
        "colors": 4,
        "name": "Leon",
        "breed": "Persian"
      }
    ]
  }
}

可见更新成功。

更新满足多个条件的嵌套对象的多个属性

现在,在一个更高级的示例中,我们将使用一个更灵活的脚本来:

  • 基于多种条件来匹配目标对象(此处为color和breed)
  • 更新多个属性(此处为color和breed)

假设我们想改变有4种颜色的猫的品种,它们的品种从Persian到Aegean,它们的颜色改为3种。使用下面的脚本:

POST iridakos_nested_objects/human/1/_update
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> { for (condition in params.conditions.entrySet()) { if (cat[condition.getKey()] != condition.getValue()) { return false; } } return true; }); for (cat in targets) { for (change in params.changes.entrySet()) { cat[change.getKey()] = change.getValue() } }",
    "params": {
      "conditions": {
        "breed": "Persian",
        "colors": 4
      },
      "changes": {
        "breed": "Aegean",
        "colors": 3
      }
    }
  }
}

// ES 8版本的语句
POST iridakos_nested_objects/_update/1
{
  "script": {
    "source": "def targets = ctx._source.cats.findAll(cat -> { for (condition in params.conditions.entrySet()) { if (cat[condition.getKey()] != condition.getValue()) { return false; } } return true; }); for (cat in targets) { for (change in params.changes.entrySet()) { cat[change.getKey()] = change.getValue() } }",
    "params": {
      "conditions": {
        "breed": "Persian",
        "colors": 4
      },
      "changes": {
        "breed": "Aegean",
        "colors": 3
      }
    }
  }
}

为了方便起见,下面是带有适当缩进的脚本的源代码。

注意事项:

  • 我们通过检查cat的属性是否具有params.conditions中指定的值来选择要更新的cat。
  • 对于每个选定的cat,我们按照params.changes中给出的值更改它对应的属性值。

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_id": "1",
  "_version": 5,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 4,
  "_primary_term": 1
}

确认一下是否更新成功:

GET iridakos_nested_objects/human/1

// ES 8版本的语句
GET iridakos_nested_objects/_doc/1

返回响应信息:

{
  "_index": "iridakos_nested_objects",
  "_type": "human",
  "_id": "1",
  "_version": 5,
  "found": true,
  "_source": {
    "name": "iridakos",
    "cats": [
      {
        "colors": 1,
        "name": "Irida",
        "breed": "European Shorthair"
      },
      {
        "colors": 2,
        "name": "Phoebe",
        "breed": "European Shorthair"
      },
      {
        "name": "Leon",
        "colors": 3,
        "breed": "Aegean"
      }
    ]
  }
}

可见,更新成功。

参考

https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html

https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html

https://www.elastic.co/guide/en/elasticsearch/painless/6.7/painless-api-reference.html

多个Elasticsearch节点无法组成集群,报错master_not_discovered_exception或NotMasterException的原因和解决方法

多个Elasticsearch节点无法自动组成集群,查看日志发现错误信息:“master_not_discovered_exception”。

原因是运维人员通过克隆虚拟机来获得多台Elasticsearch服务器,这样每个Elasticsearch节点都具有相同的节点ID,因此在组成集群时,无法选举出master节点。

这可以通过以下命令进行验证,列出所有节点ID:

GET /_cat/nodes?v&h=id,ip,name&full_id=true

请注意,由于Elasticsearch集群尚未形成,因此需要单独查询每个节点,即: 

curl 192.168.110.111:9200/_cat/nodes?v&h=id,ip,name&full_id=true
curl 192.168.110.112:9200/_cat/nodes?v&h=id,ip,name&full_id=true
......

Elasticsearch节点ID必须是唯一的。要解决这个问题,我们需要删除每个节点上的索引(RPM方式安装的Elasticsearch的索引数据默认位于/var/lib/elasticsearch)。重启Elasticsearch就会重置节点ID。 

参考

https://www.656463.com/wenda/jdbhjrjqNotMasterExceptionqgddxc_359