Elasticsearchで400エラーが出た時の対処法【動的テンプレート編】

2020-05-18

DevOpsの技術スタックで必要とされる「Elasticsearch」(ログ解析などに用いられる)ですが、トラブルシューティング系のドキュメントはほとんど英語になります。

今回はElasticsearch(以下ES)上で「400エラーが出た際の対処法」(object型からtext型に変換される場合)についてお話しします。

今回の検証環境

  • Elasticsearch v6.8.9
  • Docker上にES Single Nodeを構築: $ docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:6.8.9 1
ローカルホスト上でのアクセス例

事の始まり

ある日、突然下記のようなエラーがES上で起き、Fluentdが一切ログを送信できなくなりました。

dump an error event: error_class=Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchError 
error="400 - Rejected by Elasticsearch"

これだけでは流石に意味がわからなかったのでエラー内容を検索。するとGithubのissueで同じ現象に直面した人を発見。2

Should ES plugin display error reason when @log_level debug (or `@log_level info) is set?

アドバイス通りログレベルを「debug」に変更。(infoでは上手く行きませんでした。)

すると具体的に何でエラーが起きているのかを示すようになりました。

dump an error event: error_class=Fluent::Plugin::ElasticsearchErrorHandler::ElasticsearchError 
error="400 - Rejected by Elasticsearch [error type]: 
mapper_parsing_exception [reason]: 'object mapping for [hogehoge] tried to 
parse field [hogehoge] as object, but found a concrete value'"

理由には「hogehogeをオブジェクトとして変換しようとしたけど、別タイプの値を検出したよ」と出ています。

なぜエラーが起きたのか

そして実際に今までとエラーが出た値を比較すると、今までObject(e.g., {“hoge”: 1})だったはずの値が文字型に変わっているではありませんか。

更になぜこのような現象が起きるのかを調べていくと新しいGithubのIssue3では、次のように書かれてました。

From what I understand ES will autodetect type based on what it receives first and then if any of the following documents do not match the schema, it rejects them with a 400.

(意訳)「ESは最初に受け取った値を基準にして受け取る値の型を決定し、その後に別の型が来た場合、400エラーを返すよ」

どのように解決するか

根本的な解決策はなく、Github Issue3上では2つの回避策が提示されており、それを有志の方がFAQにアップしてくれています。

Random 400 - Rejected by Elasticsearch is occured, why?

基本的には以下の2つが回避策として紹介されています。

  • 回避策1: ESの自動型検知をオフにする(ES側)

    • メリット: 影響範囲が狭い
    • デメリット: その値は保存されるが検索できない
  • 回避策2: record_transformer4プラグインを利用(Fluentd側)

    • メリット: ESを変にいじらなくで済む
    • デメリット: Fluentdに詳しくなる必要あり

本記事では回避策1を紹介します。

実際にエラーを起こして動作確認をします。

実際にエラーを起こしてみる

テスト用のindexを作成

  1. 新しいindexを作成します。5
# Request
$ curl -X PUT "localhost:9200/test-index"

# Response
{"acknowledged":true,"shards_acknowledged":true,"index":"test-index"}

「-X, --request <command>」 使用するリクエストのHTTPメソッド(e.g., GET, POST, PUT, DELETE, PATCH, etc)を指定

  1. 適当にデータを挿入
# Request
$ curl -X POST "localhost:9200/test-index/_doc" -H 'Content-Type: application/json' -d'
{
  "counter": 1,
  "tags": ["red"],
  "objects": {"hoge": 2},
  "text": "hoge"
}
'

# Response
{"_index":"test-index","_type":"_doc","_id":"mau3KHIBIK1ezxGy690m","_version":1,"result":"created",         
"_shards":{"total":2,"successful":1,"failed":0},"_seq_no":1,"_primary_term":1}

POSTを使うと「_doc」タイプ上にドキュメントIDを指定しなくてもデータを挿入できます。 データ構造について – AWSで始めるElasticSearch(4)の記事を一読して、ESの「index, type, document, field」を理解するとよりこの先の手順が分かりやすくなります。

別のタイプ型を入れてみる

  1. 今度は「objects」を文字型に変更して再度挑戦
# Request
curl -X POST "localhost:9200/test-index/_doc" -H 'Content-Type: application/json' -d'
{
  "counter": 1,
  "tags": ["red"],
  "objects": "hoge",
  "text": "hoge"
}
'

# Response
{"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"object mapping for [objects] tried to 
parse field [objects] as object, but found a concrete value"}],"type":"mapper_parsing_exception",
"reason":"object mapping for [objects] tried to parse field [objects] as object, but found a concrete 
value"},"status":400}

期待した通り400エラー(status: 400)が返ってきていますね。では実際に紹介された回避策を試してみましょう。

回避策1: ESの自動型検知をオフにする

  1. 動的テンプレートを作成します。
# Request
$ curl -X PUT "localhost:9200/_template/workaround_1" -H 'Content-Type: application/json' -d'
{
  "template": "test-index*",
  "mappings": {
    "_doc": {
      "dynamic_templates": [
        {
          "skip_parsing_text_to_object": {
            "match_pattern": "regex",
            "match": "^(objects|text)(\\..+)?$",
            "mapping": {
              "index": false,
              "enabled": false
            }
          }
        }
      ]
    }
  }
}
'

# Response
{"acknowledged":true}

テンプレートの中身は公式ドキュメント6を参考にしました。上記は「objects、text」のFieldに対して自動変換を行わない、と言う意味です。7

この回避策は既存のindexには適用されません。試したい場合は新しいindexを作りましょう。

  1. 新しいindexを作りながらデータを挿入します。
# Request
$ curl -X POST "localhost:9200/test-index-new/_doc" -H 'Content-Type: application/json' -d'
{
  "counter": 1,
  "tags": ["red"],
  "objects-new": {"hoge": 1},
  "objects": {"hoge": 2},
  "text": "hoge"
}
'

# Response
{"_index":"test-index-new","_type":"_doc","_id":"0av_KHIBIK1ezxGy0N0X","_version":1,"result":"created",
"_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1}
  1. objectsをtext型に変えても怒られません。
# Request
$ curl -X POST "localhost:9200/test-index-new/_doc" -H 'Content-Type: application/json' -d'
{
  "counter": 1,
  "tags": ["red"],
  "objects-new": {"hoge": 1},
  "objects": "hoge",
  "text": "hoge"
}
'

# Response
{"_index":"test-index-new","_type":"_doc","_id":"0qsAKXIBIK1ezxGyvN1l","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"_seq_no":0,"_primary_term":1}
  1. 同じobject型でもobjects-newはテンプレートの適用外なのでtext型にできません。
# Request
$ curl -X POST "localhost:9200/test-index-new/_doc" -H 'Content-Type: application/json' -d'
{
  "counter": 1,
  "tags": ["red"],
  "objects-new": "hoge",
  "objects": "hoge",
  "text": "hoge"
}
'

# Response
{"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"object mapping for [objects-new] tried to parse field [objects-new] as object, but found a concrete value"}],"type":"mapper_parsing_exception","reason":"object mapping for [objects-new] tried to parse field [objects-new] as object, but found a concrete value"},"status":400}
  1. 元々text型だったtextは逆にobject型にできません。
# Request
$ curl -X POST "localhost:9200/test-index-new/_doc" -H 'Content-Type: application/json' -d'
{
  "counter": 1,
  "tags": ["red"],
  "objects-new": {"hoge": 1},
  "objects": "hoge",
  "text": {"hoge": 1}
}
'

# Response
{"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"failed to parse field [text] of type [text] in document with id '1KsDKXIBIK1ezxGyod0A'"}],"type":"mapper_parsing_exception","reason":"failed to parse field [text] of type [text] in document with id '1KsDKXIBIK1ezxGyod0A'","caused_by":{"type":"illegal_state_exception","reason":"Can't get text on a START_OBJECT at 7:11"}},"status":400}

余力があれば回避策2も試してみたいですね!


  1. https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html ↩︎

  2. https://github.com/uken/fluent-plugin-elasticsearch/issues/467 ↩︎

  3. https://github.com/uken/fluent-plugin-elasticsearch/issues/452 ↩︎

  4. https://docs.fluentd.org/filter/record_transformer ↩︎

  5. https://www.elastic.co/guide/en/elasticsearch/reference/6.8/indices-create-index.html ↩︎

  6. https://www.elastic.co/guide/en/elasticsearch/reference/6.8/dynamic-templates.html ↩︎

  7. https://www.elastic.co/guide/en/elasticsearch/reference/6.8/mapping-params.html ↩︎

...SNSにもシェアしてみる?
DevOpselasticsearchfluentdkibana
(C) 2020, All Rights Reserved.

最近あちこちに情報発信をしているので省略URLを作ってみた

Github Actions + AWS CLI (s3 sync)で日本語を使う方法