Blog

2014.09.30 - 번역 - Six Ways to Crash Elasticsearch ...

drscg 2019. 1. 6. 17:03

UPDATE: This article refers to our hosted Elasticsearch offering by an older name, Found. Please note that Found is now known as Elastic Cloud.

이 게시물은 기존에 Found라는 이름으로 제공된 Elasticsearch 서비스에 관한 것이다. Found은 이제 Elasticsearch Cloud로 알려져 있다.

As much as we love Elasticsearch, at Found we've seen customers crash their clusters in numerous ways. Mostly due to simple misunderstandings and usually the fixes are fairly straightforward. In our quest to enlighten new adopters and entertain the experienced users of Elasticsearch, we'd like to share this list of pitfalls.

우리가 Elasticsearch를 좋아하는만큼, Found에서 고객이 여러 가지 방법으로 cluster를 crash시키는 것을 보았다. 대부분 단순한 오해로 인한 것이며, 대개 해결 방법은 매우 간단하다. 새로운 사용자와 경험이 풍부한 사용자를 이해시키기 위해, 이 함정 목록을 공유하려 한다.

Introduction

Any datastore can be brought to its knees, especially if it contains a substantial amount of data compared to the available hardware or if the traffic is simply too high. This article will however focus on ways of taking down an Elasticsearch cluster that are not related to insufficient hardware. Some are quite common and real, while others less so. At any rate, it’s good to be aware of them. These also emphasize why we only provide dedicated clusters at Found: you can shoot yourself in the foot, but not your neighbors’.

사용 가능한 HW에 비해 많은 양의 data가 있거나, traffice이 너무 높을 경우, 모든 datastore가 down될 수 있다. 그러나, 이 게시물에서는, 부족한 HW와 관련이 없는 Elasticsearch cluster를 중지하는 방법에 중점을 둘 것이다. 어떤 것은 꽤 흔하고 진짜이지만, 다른 것은 그렇지 않다. 어쨌든 그것들을 알고 있는 것이 좋다. 또한 Found에서 전용 cluster만을 제공하는 이유에 대해서도 강조한다. 즉, 자기 무덤을 팔수도 있다.

Mapping Explosion

One quick way of making Elasticsearch run out of memory and struggle to recover is by ignoring to consider the difference between keys and values when indexing documents. This has been explained in great detail in the article Troubleshooting Elasticsearch searches, for Beginners, but the essence is that the keys in the documents change, with the result that the mappings for the index is evergrowing. On a small scale, this will require a disproportionate large amount of heap space and for larger clusters it will typically cause problems with distributing the cluster state.

Elasticsearchd의 memory를 부족하게 만들고, 복구(recovery)를 힘들게 만드는 빠른 방법 중 하나는 document를 indexing할 경우, key와 value의 차이점을 고려하지 않는 것이다. 이것은 초심자를 위한 Elasticsearch 검색의 해결 방법이라는 게시물에 아주 자세히 설명되어 있지만, 핵심은 document의 key가 변경되어, 그 결과로 index에 대한 mapping이 끊임없이 증가한다는 점이다. 작은 규모에서는, 너무 많은 양의 heap 공간이 필요하며, 큰 cluster의 경우, 일반적으로 cluster state를 배포할 때 문제가 발생한다.

Let’s make this more clear with an example. We create a script that indexes documents with a single field, the key is a growing integer and the value is the square of the key. The first documents look like this:

예제를 가지고,  좀 더 명확하게 설명하겠다. 단일 field를 가진 document를 indexing하는 script를 작성하자. key는 증가하는 integer이고, vaule는 key의 제곱이다. 첫 번째 document는 다음과 같다.

{"1": 1}
{"2": 4}
{"3": 9}
{"4": 16}
{"5": 25}
{"6": 36}

You can find the entire script in this gist. On a single node cluster with 128MB the script managed to index about 30 000 documents before the node went down, but the index on disk is less than 7 MB. Depending on the use case it is not uncommon to have an index size between one and sixteen times the heap size in a normal system.

이 gist에서 전체 script를 볼 수 있다. 128MB의 단일 node cluster에서, script는 node가 down되기 전에 약 30,000 개의 document를 indexing하지만, disk의 index는 7MB 미만이다. 사용 사례를 보면, 일반적인 system에서 heap 크기의 1 배에서 16 배 사이의 index 크기를 갖는 경우는 드문 경우는 아니다.

I terminated the script after the first out-of-memory exception and our system restarted the instance. It managed to recover, but even at idle status it is doing heavy garbage collection. If you imagine what the mappings will look like for this index, with more than 30000 different fields, it’s not difficult to understand that the cluster is struggling. Especially considering that this mapping is included in the cluster state that is broadcast to all nodes on several occasions. Put differently, we have not reached a limit on documents, but a limit on the metadata for an index.

첫 번째 out-of-memory exception이 발생한 후 script를 종료하고, system이 instance를 다시 시작했다. 가까스로 복구되었지만, idle 상태에서도 garbage collection이 많이 발생한다. 30,000 개 이상의 서로 다른 filed를 가진, 이 index가 mapping이 어떻게 보일지 상상해 보면, cluster가 어려움을 겪고 있음을 이해하는 것이 어렵지 않다. 특히, 이 mapping이 여러 번에 걸쳐 모든 node에 broadcast되는 cluster state에 포함된다는 것을 고려하면 더욱 그렇다. 다시 말하자면, document에 대한 한계에 도달하지 않았지만, index에 대한 metadata의 한계에 도달했다.

The proper way of indexing this data would have been something like this:

이 data를 indexing하는 적절한 방법은 다음과 같다.

{"key": 1, "value": 1}
{"key": 2, "value": 4}
{"key": 3, "value": 9}
{"key": 4, "value": 16}
{"key": 5, "value": 25}
{"key": 6, "value": 36}

Too Many Shards or the Gazillion Shards Problem

Too many shards or the gazillion shards problem, as some of the Elasticsearch developers like to refer to it, comes from the fact that there is a base cost to every shard and index even if it does not contain any documents.

Elasticsearch 개발자 일부가 언급하기를 좋아하는, 너무 많은 shard나 엄청난 수의 shard 문제는 document가 없는 경우에도 모든 shard 및 index에 대한 기본 비용이 있다는 사실에서 비롯딘다.

In this rather extreme example we create an index with 1073741824 shards.

이 극단적인 예에서는, 1,073,741,824개의 shard를 가진 index를 생성한다.

curl -XPOST https://<cluster_id>-region.foundcluster.com:9243/toomanyshards -d '{"settings":{"index":{"number_of_shards":"1073741824","number_of_replicas":"1"}}}'

A few seconds after issuing this request the connection was closed and a few seconds after that I got an email saying the instance ran out of memory. This without indexing a single document. Now, the next time you set up Logstash with its default index template of one index per day, you might want to have a look at the size of those indexes after a few days. Why? Well, each index implies at least one shard and if you don’t have that many log entries, you will probably do better with one index per week or per month. For Elasticsearch it is the same thing (in terms of performance) if you are having one index with two shards or two indexes with one shard. Both cases have two Lucene indexes.

이 request를 실행한 후 몇 초만에, 연결이 닫히고, 몇 초 후에 instance에 memory가 부족하다는 email을 받았다. 이것은 단 하나의 document도 indexing하지 않았다. 하루에 하나의 index를 가지는 기본 index template을 가진 Logstash를 설정하면, 며칠 후 해당 index의 크기를 살펴볼 것이다. 왜? 음, 각 index는 적어도 하나의 shard를 의미하며, 많은 log 항목이 없으면 주별 또는 월별로 하나의 index로 하는 것이 아마도 더 나을 것이다. Elasticsearch의 경우, 두 개의 shard가 있는 index 하나 또는 하나의 shard가 있는 두 개의 index가 있다면, 성능면에서 동일하다. 두 경우 모두 두 개의 Lucene index를 가진다.

Arbitrary Large Size Parameter

The classic example: a developer knows that a search will not return that many hits, and he wants all of them without paging, so he simply sets the size parameter to an arbitrarily large size like Integer.MAX_VALUE (2147483647).

개발자는 검색이 많은 결과를 return하지 않는다는 것을 알고 있는데, paging 없이 모두를 원한다면, size parameter를 Integer.MAX_VALUE (2,147,483,647)와 같이, 임의의 큰 크기로 설정하기만 하면 된다.

When optimizing code like Elasticsearch developers do, there is no getting around making some assumptions. One of the assumptions is that a query usually has more hits than the number of elements to be returned. This implies that they will choose to prepare internal data structures for the number of documents specified in the size. In other words, if the size parameter is ridiculously large, then the Elasticsearch will create a ridiculously large internal data structure. And the one who pays the price is you, waiting forever - or so it seems! - for an insignificant result.

Elasticsearch 개발자가 하는 것처럼, code를 최적화할 경우, 몇 가지 가정은 피할 수 없다. 가정 중 하나는, 일반적으로 query에 return되는 요소 수보다 더 많은 결과를 가지고 있다는 것이다. 이는 size에 지정된 document 수에 대해 내부 data 구조를 준비하하는 것을 선택할 것임을 의미한다. 즉, size parameter가 터무니없이 큰 경우, Elasticsearch는 엄청나게 큰 내부 데이터 구조를 생성한다. 그리고 비용을 지불하는 사람은 당신이다. 중요하지 않은 결과를 영원히 기다리는 것이다. 또는 그렇게 보인다!

In earlier versions of Elasticsearch this was perhaps the quickest trick to wreak havoc on somebody’s cluster. Now, in recent versions of Elasticsearch this is not as big a problem as it used to be, but it’s still a good reason to use the scan and scroll API instead. Even if you have tested it before and you know that there is lots of free heap on the instance, it can blow up simply because one of those structures needs contiguous memory on the heap, and your instance is not able to provide that without doing a major garbage collection.

초기 버전의 Elasticsearch에서, 이것은 아마도 누군가의 cluster에 혼란을 가져 오는 가장 빠른 트릭이었다.  Elasticsearch의 최신 버전에서는 예전처럼 큰 문제는 아니지만, 대신 scan 및 scroll API를 사용하는 것이 여전히 좋은 이유이다. 이전에 테스트를 해 봤고, instance에 사용가능한 heap이 많다는 것을 알고 있는 경우에도, 그들 구조 중 하나는 heap에 연속 메모리가 필요하고, instance가 garbage collection을 하지 않고는 memoey를 제공할 수 없기 때문에, 이 메모리가 폭발할 수 있다.

Scripting vs Halting Problem

In academia it is a known problem that one cannot determine if a running process will terminate or not, and since Elasticsearch does not have a timeout for long running scripts the following script will never halt. This results in the script keeping one search thread that will never be released back to the pool. Subsequent executions of such scripts will eventually consume all search threads and put all future searches in the queue until the queue reaches its maximum and all future searches are halted. This is also the case for the new sandboxed scripts.

실행 중인 process가 종료 여부를 판단할 수 없다는 것이 학계에서는 알려진 문제이다. 그리고, Elasticsearch에는 장기 실행 script에 대한 timeout이 없으므로, 다음 script는 중단되지 않는다. 그 결과 script가 절대로 pool로 release되지 않는 검색 thread를 유지하게 된다. 계속해서 그러한 script를 실행하면, 결과적으로 모든 검색 thread가 소모되어, queue가 최대값에 도달하고 이후의 모든 검색이 중지될 때까지, queue에 모든 검색이 저장된다. 새로운 sandbox script의 경우에도 마찬가지이다.

{
    "script_fields": {
        "test1": {
            "lang": "groovy",
            "script": "while (true) {print 'Hello world'}"
        }
    }
}

This script does however have one benevolent purpose: testing how your client behaves when the search queue fills up. Neeless to say, don’t do this in production, use a staging or test cluster instead. By starting this script repeatedly until all the search threads are taken, the normal searches issued by your client will be put in the queue. This will simulate the effect of an overloaded cluster where searches take longer time to process than the rate at which they are received. Another entry into this pit is if your client mistakes an expensive and thus slow query for a failed query and executes it again. This is particularly bad if the query for some reason is not capable of utilizing caches, like queries calculating date ranges based on the current time.

그러나, 이 script는 검색 queue가 가득 차면, client가 작동하는 방식을 테스트하는, 하나의 좋은 점을 가지고 있다. 운영 단계에서는 이를 하지 않는 것은 말할 필요도 없고, 진행 단계나 테스트 cluster에서 사용하자. 모든 검색 thread가 수행될 때까지, 이 script를 반복적으로 시작하면, client가 실행한 일반적인 검색이 queue에 저장됩니다. 이렇게 하면, 검색을 처리하는데 걸리는 시간이 수신되는 속도보다 오래 걸리는, 과부하가 걸린 cluster를 시뮬레이션할 수 있다. 또 다른 경우는, client가 실패한 query에 대해 값 비싸고 느린 query를 실수로 다시 실행하는 경우이다. 현재 시간을 기준으로 날짜 범위를 계산하는 query처럼, 어떤 이유로 query가 cache를 사용할 수없는 경우에 특히 심각하다.

Too Deep Aggregations

Deep aggregations can still be an issue, but the following extreme example has been fixed in the 1.2 release of Elasticsearch. In Elasticsearch 1.1 we could create and index like this:

deep aggregation은 여전히 문제일 수 있지만, 다음과 같은 극단적인 예가 Elasticsearch의 1.2 에서 수정되었다. Elasticsearch 1.1에서는 다음과 같이 index를 만들고 indexing할 수 있다.

curl -XPOST https://<cluster>-region.foundcluster.com:9243/toodeepaggs -d '{
   "settings":{
      "number_of_shards":1
   },
   "mappings":{
      "logs":{
         "_all":{
            "enabled":false
         },
         "properties":{
            "cust_id":{
               "type":"long"
            },
            "devi_id":{
               "type":"long"
            },
            "evt_date":{
               "type":"date",
               "format":"yyyy-MM-dd"
            },
            "hr":{
               "type":"string"
            },
            "type":{
               "type":"string"
            },
            "action":{
               "type":"string"
            }
         }
      }
   }
}'

And then, without indexing a single document we could make the following aggregation:

그런 다음, document를 indexing하지 않고, 다음 aggregation을 만들 수 있다.

{
    "aggregations": {
        "date": {
            "terms": {
                "field": "evt_date"
            },
            "aggregations": {
                "customer": {
                    "terms": {
                        "field": "cust_id"
                    },
                    "aggregations": {
                        "device": {
                            "terms": {
                                "field": "devi_id"
                            },
                            "aggregations": {
                                "type": {
                                    "terms": {
                                        "field": "type"
                                    },
                                    "aggregations": {
                                        "action": {
                                            "terms": {
                                                "field": "action"
                                            },
                                            "aggregations": {
                                                "hr": {
                                                    "terms": {
                                                        "field": "hr"
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

Even without a single document this way of nesting aggregations resulted in the instance running out of memory. I have tested this for instances with heaps as large as 8GB.

document없이 aggregation을 중첩(nesting)하는 방식으로 인해, instance의 memory가 부족해졌다. 8GB의 heap을 가진 instance를 테스트했다.

This example is actually based on a real world problem where a developer most likely had misunderstood the aggregations API and created nested aggregations instead of sibling aggregates, but it still shows that the temporary structures created inside Elasticsearch when processing nested aggregations can be quite large.

이 예제는 개발자가 aggregation API를 오해하고, slibling aggregation 대신 nested aggregation 생성할 가능성이 있는 실제 문제를 기반으로 하지만, nested aggregation을 처리할 경우, Elasticsearch 내부에서 생성 된 임시 구조가 상당히 클 수 있음을 보여준다.

Even if this contrived example without a single document in the index has been fixed in newer versions of Elasticsearch it can be good idea to just snapshot the indexes you need over to a separate cluster when doing ad hoc analytics. Personally, I often do that when analyzing logs, our Logstash cluster is simply not tuned for heavy analytics. The safety of steering clear of ad hoc queries on the production cluster is not the only benefit. It also allows me to use a cluster with more memory, since it will only be running for the few hours that I need it.

Elasticsearch의 최신 버전에서, index에 document가 없는 이 부자연스러운 예제가 수정되었지만, 임시 분석을 할 경우, 필요한 index를 별도의 cluster에 snapshot하는 것이 좋다. 개인적으로, log를 분석할 경우 그렇게 하는데, 우리 Logstash cluster는 단순히 지나친 분석을 위해 조정되지 않는다. 운영 cluster에서 임시 query를 피하는 것의 안정성이 유일한 이점은 아니다. 또한 필요한 시간 동안만 실행되므로, cluster가 더 많은 memory를 활용할 수 있다.

Long Garbage Collection Pauses

At Found we have experienced both customers and ourselves pushing clusters to their limits. Failing to take action when a cluster reaches its limits can be dangerous to the integrity of your data.

Found에서, 고객과 우리들은 cluster의 한계에 도전하는 경험을 했다. cluster가 한계에 도달했을 때 조치를 취하지 않으면, 데이터 무결성이 위험할 수 있다.

To demonstrate this I added a second node to the previous cluster, making it redundant (2 x 128MB heap). I recreated the index with one shard and one replica and started the same script used for demonstrating the bad mappings, but instead of stopping on the first out-of-memory error I simply restarted the script and kept pushing more data.

이를 증명하기 위해, 이전 cluster에 두 번째 node를 추가하여 이중화(2 x 128MB heap)를 구성했다. 하나의 shard와 하나의 replica를 가진 index를 다시 생성하고, 잘못된 mapping을 테스트하기 위해 사용된 동일한 script를 시작했다. 그러나 첫 번째 out-of-memory 오류에서 멈추는 대신, script를 다시 시작하고 더 많은 data를 계속 보냈다.

The first thing I noticed was a dramatic slowdown in indexing speed in the script. I then looked up the cluster in our metrics console and as expected it was collecting garbage continously. Elasticsearch configures the JVM to use the ConcurrentMarkSweep collector and since it is concurrent it avoids stop-the-world pauses as much as possible, but it cannot avoid them entirely, and with small instances like these the effect of the collector consuming CPU is also very much noticeable.

내가 알게된 첫 번째는 scripi의 indexing 속도가 크게 느려졌다는 점이다. 그래서 metric console에서 cluster를 검색하니, 예상대로 cluster는 계속해서 garbage 수집 중이었다. Elasticsearch는 ConcurrentMarkSweep collector를 사용하도록 JVM을 구성하고, 동시성 때문에 가능한 한 중지(stop-the-world)시키지 않지만, 완전히 피할 수는 없다.이러한 작은 instance의 경우 CPU를 사용하는 collector의 영향은 매우 크다.

Looking at the cluster in Kopf I also noticed that the replicas where out of sync, at least that’s what the document counts reported. Elasticsearch is usually pretty quick to discard a replica if it does not match the master, but in this case the nodes were slowed down to a point where they had not been able to do so yet, or they were too slow to respond with an updated statistic to Kopf.

Kopf에서 cluster를 살펴보니, 동기화(sync)되지 않은 replica의 경우 적어도 document의 수가 나타나는 것도 알 수 있었다. 일반적으로 Elasticsearch는 master와 일치하지 않는 경우, replica를 삭제하는 것이 매우 빠르지만, 이 경우 아직 replica를 수행할 수 없었던 곳까지 node는 속도가 느려지거나, Kopf의 update  통계에 대한 update 통계가 너무 느렸다.

In the end, both nodes went out of memory at the same time and I stopped the script. This allowed the nodes to come back online. The replica was then discarded and the master shard recovered from the local gateway of that node. This time the recovery mechanism worked and having a high availaility setup did make the cluster handle the load better, but the high load made the entire cluster unavailable, and for a while the redundancy was lost. In other words, if one does not respond to a situation like this, then it’s just a matter of time before another error comes along and data loss is a fact.

결국, 양쪽 node는 동시에 memory가 부족하게 되었고, 나는 script를 중단했다. 이로 인해 node가 다시 online 상태로 전환되었다. 그런 다음 replica가 삭제되고, master shard가 해당 node의 local gateway에서 recovery되었다. 이번에는 recovery mechanism이 동작했고, 높은 가용성 설정이 있어, cluster가 load를 더 잘 처리했지만, 높은 load로 인해, 전체 cluster가 사용할 수 없게 되었고, 잠시 동안, 중복성이 손실되었다. 즉, 이런 상황에 대응하지 않으면, 다른 오류가 발생하고, data 손실이 나는 것은 시간 문제일 뿐이다

Conclusion

Despite the oddities and pitfalls described in this article, our opinion is that Elasticsearch is a really great product. Most of these issues can easily be avoided or will be addressed in future versions as similar issues have been adressed previously, but at the moment their consequences can be destructive.

이 게시물에서 설명된 이상한 점과 함정에도 불구하고, Elasticsearch는 정말 훌륭한 제품이라는 생각이 든다. 이러한 문제의 대부분은 이전에 비슷한 문제가 제기되었으므로, 쉽게 피할 수 있거나 향후 버전에서 다루게 될 것이다. 그러나 그 순간 그 결과는 파괴적일 수 있다.

Personally, I am careful with who I allow access to my cluster and I would never use a shared cluster environment, at least not until there have been major changes to how Elasticsearch balances resources between different tasks. Even if the circuit breakers are constantly improving and considering more and more resources, can you be sure they will always be able to keep up with the new features?

개인적으로, 나는 cluster에 access하는 사용자에 대해 주의하고 있으며, 최소한 공유된 cluster 환경을 사용하지 않는다. 적어도 Elasticsearch가 서로 다른 작업간에 resource를 조정하는 방법에 대한 주요 변경 사항이 있을 때까지는 아니다. circuit breaker가 끊임없이 개선되고 점점 더 많은 resource를 고려하더라도, 항상 새로운 기능을 따라 잡을 수 있을까?

원문 : Six Ways to Crash Elasticsearch