This is the final post in a three-part series about Instant Aggregations. Read how it all started in The Tale of Caching and Why It Matters from Simon Willnauer and Shay Banon and the meaty middle detailed in The Great Query Refactoring: Thou Shalt Only Parse Once. Enjoy the trilogy!
이 게시물은 Instant Aggregations에 대한 시리즈 중 마지막 게시물이다. The Tale of Caching and Why It Matters에서 Simon Willnauer와 Shay Banon이 어떻게 이것을 시작했는지, 가운데 부분인 The Great Query Refactoring: Thou Shalt Only Parse Once에서 요점을 자세히 읽어보자.
In 1.4.0 Elasticsearch gained a shard level ‘Request Cache’ which caches the result of the query phase on each shard keyed on the search request itself. Until 5.0 this feature was disabled by default since it wasn’t as useful as it could be for most of the computationally heavy use cases. The cache was intended to make searches faster, especially for aggregations. One problem was that ordering in JSON is not deterministic so although two requests may be logically the same, when rendered to JSON strings they may not be equal. The other main problem is that these computationally heavy use cases tend to use time windows relative to the current time so subsequent requests will tend to have slightly different time ranges. Enabling it would likely be a waste memory for most of the users since they would rarely ever get a cache hit. So why would we add such a feature?
Elasticsearch 1.4.0 에서는 search request 자체에 있는 각 shard에서 query단계의 cache하는 shard level의 ‘Request Cache’를 확보했다. 5.0까지, 이 기능은 대부분의 대규모 사용 사례에 유용하지 않기 때문에 기본적으로 비활성화되었다. cache는 search 특히 aggregation 속도를 더 높이기 위한 것이었다. 한 가지 문제는 JSON 순서가 정해져 있지 않기 때문에, 2가지 request가 논리적으로 동일하더라도, JSON 문자열이 rendering될 때는 동일하지 않을 수 있다는 것이다. 또 다른 주요 문제는 이들 대규모 사용 사례는 현재 시간에 대해 상대적인 시간대를 사용하는 경향이 있어, 후속 request가 약간 다른 시간 범위를 가지는 경향이 있다는 것이다. cache 적중이 거의 발생하지 않기 때문에, 대부분의 사용자에게는 memory가 낭비될 수 있다. 그렇다면 그런 기능을 왜 추가했을까?
Here at Elastics, we try to follow the rule of “progress over perfection”. Even if we can’t utilize it’s full potential we get a step closer to our goals. Now after 2 years of engineering effort and refactoring the entire search request representation, we can finally take full advantage of it.
여기 Elastic에서는, “완벽함을 뛰어넘은 진보(progress over perfection)”라는 규칙을 다르려 했다. 그것을 충분히 활용할 수 없겠지만, 목표에 더 가까이 다가 갔다. 이제, 2년간의 노력과 전체 search request의 refactoring 이후, 마침내 그것을 최대한 충분히 활용할 수 있었다.
From 5.0 the request cache will be enabled by default for all requests with `size:0`. The request cache is most useful for analytics use cases and generally analytics use cases use search requests with `size: 0`.
5.0 부터, request cache는 `size:0`을 가진 모든 request에 대해 활성화된다. request cache는 분석에 가장 유용하며, 일반적으로 분석은 `size:0`을 가진 search request를 사용한다.
So how did the changes made since 1.4.0 enable us to better use this feature?
그렇다면, 1.4.0 이후 변경사항으로, 이 기능을 더 잘 사용하려면 어떻게 해야 하나?
The search request cache gives massive improvements in search performance when running the same search multiple times on a static index. However, search requests are rarely exactly the same, especially in the time series use case.
search request는 static index에서 동일한 search를 여러번 실행할 경우, search 성능을 크게 향상시킨다. 그러나, search request는 특히 시계열 시리즈에서, 거의 틀림없이 동일하지 않다.
Lets look at some of the characteristics of typical search for time-series use cases:
시계열 사례에 대한 일반적인 search의 몇 가지 특징을 살펴보자.
- Queries have a time range component often relative to current time (e.g. from now-7d to now)
query는 현재 시간과 관련된 시간 범위 요소를 가지는 경우가 많다. (예 from now-7d to now) - Search requests span across multiple indices
search request가 여러 index에 걸쳐있다. - Indexing happens on the latest index, all other indices are static
index는 최신 index에서만 발생하고 다른 모든 index는 정적이다. - Same search requests are run multiple times with different time ranges
다른 시간 범위를 가진 동일한 search request가 여러 번 동작한다.
The above are common characteristics of search requests for a lot of time series use cases including common patterns of usage for Kibana. A lot of Kibana dashboards contain quite a few visualisations a lot of which are complex and can take a number of seconds for the dashboard request to be executed by Elasticsearch. Because search requests are rarely the same, the search request cache can’t be used effectively to improve search performance for these use cases.
위의 내용은 Kibana의 일반적인 사용 패턴을 포함한 많은 시계열 사용 사례에 대한 search request의 공통적인 특징이다. 많은 Kibana dashboard는 복잡하고 Elasticsearch에 의해 dashboard request를 실행하는데 몇 초가 걸릴 수 있는 꽤 많은 시각화를 포함하고 있다. search request는 거의 동일하지 않기 때문에, search request cache를 효율적으로 사용하여, 이러한 사용 사례에 대한 search 성능을 효과적으로 향상시킬 수 없다.
Or can it?
How can we make the search request cache work when the time range is always moving? Let’s solve this by considering an example. Imagine we have the details for every residential house sale in the UK since 1995 (luckily this information is conveniently made available). We can index this into 21 yearly indices (e.g. house-prices-1995, house-prices-1996, …, house-prices-2016). For the purposes of this explanation let’s make each index only have 1 shard (though the same idea can easily be extended to multiple shard indices without modification) Now we can run queries against the indices by using requests like this:
시간 범위가 항상 움직이는 경우, search request cache가 효과적으로 동작하게 하려면 어떻게 해야 할까? 다음과 같은 예를 들어보자. 1995년 이후, 영국의 모든 주거용 주택 매매에 대한 세부사항(다행히도, 이 정보는 편리하게 이용할 수 있다)을 가지고 있다고 가정해 보자. 이를 21개의 연도별 index(예: house-prices-1995, house-prices-1996, …, house-prices-2016)로 index할 수 있다. 이 설명의 목적을 위해, 각 index가 1개의 shard만 가지도록 하자(수정없이 다수의 shard로 쉽게 확장할 수 있겠지만). 이제 다음과 같은 request를 사용하여 index에 대해 query를 실행할 수 있다.
``` GET house-prices-*/_search { "query": { "range": { "date": { "gte": "2013-09-01", "lt": "2016-03-01" } } } } ```
The figure below shows three of these such queries overlaid on the yearly indices. These queries are typical for dashboard style use cases in that they differ only by small amounts (imagine a refreshing dashboard)
아래 그림은 이들 query가 중 3개의 년도별 index에 중첩됨을 보여준다. 이들 query는 dashboard 스타일 사용 사례에 대해서는 약간만 다를뿐 일반적이다. (dashboard refresh를 가정해 보자)
A couple of things stand out from this diagram:
이 diagram에서 두드러지는 2가지는 아래와 같다.
- All three queries match no documents in house-prices-2012 (highlighted red on the diagram)
3가지 query 모두 house-prices-2012 에서 일치하는 document가 없다. (diagram에서 붉은색) - All three queries will match all documents in house-prices-2014 and house-prices-2015 (highlighted green on the diagram)
3가지 query 모두 house-prices-2014, house-prices-2015 에서 모든 document와 일치한다. (diagram에서 녹색)
So the only indices which affect the matching documents between the three queries are house-prices-2013 and house-prices-2016.
따라서, 3가지 query에서 일치하는 document에 영향을 미치는 유일한 index는 house-prices-2013 과 house-prices-2016 이다.
If we could rewrite the queries on each shard based on the range of values present in that index we could make the queries able to actually utilize the request cache. Below is the diagram with the same three queries rewritten to make them more cachable in the search request cache:
해당 index에 있는 값의 범위를 기반으로 각 shard에 대한 query를 다시 작성할 수 있다면, query는 request cache를 실제로 활용할 수 있다. 다음은 search request cache에서 query가 caching이 될 수 있도록 3가지 query를 다시 작성한 그림이다.
You can see that in the diagram above, the first query is rewritten and run as a `match_none` query on the `house-prices-2012` indexes shard and a `match_all` query on the `house-prices-2014` and `house-prices-2015` indices shards. This means that when the second and third query is run it can use the cached results of the search on the `house-prices-2012`, `house-prices-2014` and `house-prices-2015` indices shards instead of actually running a search on those shards. So for the second and third queries we only need to actually search the shards on the `house-prices-2013` and `house-prices-2016` indices.
위의 diagram에서 볼 수 있듯이, 첫번째 query는 다지 작성되어, `house-prices-2012` index에서 `match_none` query로, `house-prices-2014` 과 `house-prices-2015` index에서는 `match_all` query로 실행된다. 즉, 두번째와 세번째 query는 해당 shard의 search를 실행하는 대신, `house-prices-2012`, `house-prices-2014`, `house-prices-2015` index shard에 대한 search의 cache된 결과를 사용하여 실행된다. 따라서, 두번째와 세번째 query는 실제로 `house-prices-2013` 과 `house-prices-2016` index의 shard만 search하면 된다.
Also, even for the first query, running a `match_none` query instead of a range query will be faster since it will not need to try to lookup the date range in the index.
또한, 첫번째 query의 경우에도, range query 대신 `match_none` query를 실행하면 더 빠르다. 왜냐하면, index의 date range를 검색할 필요가 없기 때문이다.
So how does this work in practice?
그렇다면, 실제로 이 작업이 어떻게 동작할까?
This feature not only relies on the search request cache mentioned at the beginning of this post, but it is also heavily based on the query refactoring that we described in a recent blog post. The reason for this is that in order to add the rewrite logic described above we need objects that represent the query and the search request that we can actually rewrite.
이 기능은 이 게시물의 처음에 언급했던 search request cache에 의존할 뿐만 아니라, 최근 게시물에서 설명한 query refactoring을 기반으로 한다. 그 이유는 위에서 설명한 재작성 logic을 추가하기 위해, query와 실제로 다시 작성할 수 있는 search request를 나타내는 object가 필요하다.
When the search request is received by the shard, we rewrite the entire request including `query` and `post_filter` QueryBuilder objects. Each type of query has it’s own implementation of QueryBuilder which will rewrite according to it’s own rules.
shard가 search request를 받으면, `query` 와 `post_filter` QueryBuilder object를 포함한 전체 request를 다시 작한다. 각 query 유형은 자체 규칙에 따라 다시 작성하는 QueryBuilder의 자체 규현을 가지고 있다.
Compound queries, queries that themselves contain queries (such as `bool` and `constant_score`) will call rewrite on the queries they wrap. Most leaf queries, queries that do not contain other queries, currently do not contain any rewrite logic so just return themselves (to indicate they did not change).
compound query, 자체에 query를 포함하는 query는 그들을 감싸고 있는 query에서 query의 재작성을 호출한다. 대부분의 leaf query, 다른 query를 포함하지 않는 query는 현재 어떤 재작성 logic도 포함하고 있지 않으므로, 그냥 그대로 return한다. (그것들이 변경되지 않았을을 나타내기 위해)
The range query however, will check the minimum and maximum value of the field (we will call this the field range) and compare this to the range it contains. There are three cases that we care about when evaluating the rewrite for the range:
그러나, range query는 field의 최소값과 최대값(이를 field range라 한다)을 확인하고, 이를 range와 비교한다. range를 재작성을 평가할 때 주의해야할 3가지 경우가 있다.
- The query range and the field range do not overlap at all - In this case we can rewrite this range as a `match_none` query so we return a `MatchNoneQueryBuilder`
query range와 field range가 전혀 겹치지 않는다. 이 경우 이 range를 `match_none` query로 다시 작성할 수 있다. 따라서, `MatchNoneQueryBuilder` 를 return한다. - The query range contains the entire field range - In this case all documents which contain a value for this field will match the range so we can rewrite the range query as an unbounded range query (not a match_all query) and return it
query range가 전체 field range를 포함한다. - 이 경우 이 field의 값을 포함하는 모든 document는 range에 일치할 것이므로, range query를 무제한 range query(match_all query가 아닌)로 다시 작성해 return할 수 있다. - The query range partially overlaps the field range. There’s not much we can do to help in this case so we don’t rewrite and return the original range query.
query range가 field range와 부분적으로 겹친다. 이 경우에는 효과가 없으므로, 다시 작성하지 않고 원래의 range query를 return한다.
Now that the search request has been rewritten we can check the cache with the rewritten version and either retrieve the cached result or execute the search and add the result to the cache, keyed on the rewritten search request. This has a nice side effect since the rewritten queries are “normalized” search requests that are semantically equivalent but differ in the order of keys in the json will now also produce cache hits.
이제, search request가 다시 작성되었으므로, 다시 작성된 버전으로 cache를 확인하고, cache된 결과를 가져오거나 search를 실행하여 다시 작성된 search request에 따라 cache에 추가한다. 다시 작성된 query는 의미상 동일하나 json에서 Key의 순서가 다른 "정규화된" search request이므로, 좋은 결과를 가지고 있고, cache hit도 생성할 수 있다.
Why not use `match_all` ?
왜 `match_all`을 사용하지 않는가?
In the case that the query range contains the entire field range you might think we could rewrite the `range` query as a `match_all` query. We can’t do this because we can’t assume that all documents have a value for the field. If we rewrite to a `match_all` query we would incorrectly match documents that have no value for the field. For this reason we instead rewrite the range to an unbounded range query (effectively [* TO *]) which still means the query is much more useful to the search request cache.
query range가 전체 field range를 포함하는 경우, `range` query 를 `match_all` query로 다시 작성할 수 있다고 생각할 수 있다. 모든 document에 해당 field 값이 있다고 가정할 수 없기 때문에, 그렇게 할 수 없었다. `match_all` query로 다시 작성하면, field에 값이 없는 document를 잘못 일치시킬 수 있다. 이런 이유로, range를 무제한 range query(사실 [* TO *]로 다시 작성했다. 이것은 query가 search request cache에 훨씬 더 유용함을 의미한다.
Is this all worth the trouble?
이럴만한 가치가 있을까?
To answer this question we’ll go back to our yearly house price indices. This dataset contains 21 yearly indices containing 21,304,688 residential UK house sales from 1995 to 2016. I ran this fairly unscientific test on my Macbook Pro. Each index has a single shard. I created a Kibana dashboard showing 14 different visualisations relevant to the data mixing simple and complex visualisations. The top of the dashboard looks like the following:
이 질문에 답하기 위해, 년간 주택 가격 index로 돌아가 보자. 이 data 집합은 1995 ~ 2016, 영국의 주거용 주택 매매 데이터 21,304,688건을 포함하고 있는 21개의 년간 index를 가지고 있다. 이를 가지고 Macbook Pro에서 상당히 비과학적인 테스트를 실행했다. 각 index는 단일 shard를 가지고 있다. 간단하고 복잡한 시각화를 혼합한, data와 관련된 14개의 다른 시각화를 보요주는 Kibana dashboard를 생성했다. dashboard의 상단은 다음과 같다.
With the request cache disabled, requests to elasticsearch for the dashboard to refresh takes 12s-14s. When the request cache is enabled this drops to ~100ms. The same request is now 100x faster!
request cache를 비활성화하면, dashboard refresh를 위해 Elasticsearch에 request를 하면, 12~14s 정도 걸린다. request cache를 활성화하면, 100ms 미만으로 떨어진다. 동일한 request가 100배 더 빨라진다.
But this is on completely static data so what happens when we are indexing data?
그런데, 이것은 완전히 정적인 data이다. data를 index하고 있는 경우에는 어떨까?
To test this I deleted the data from 2010 to 2016 and loaded it in date order whilst refreshing the dashboard:
이를 test하기 위해, 2010 ~ 2016 의 data를 삭제하고, dashboard를 refresh하는 동안 날짜 순서로 그것을 load하였다.
Now the request to Elasticsearch for the dashboard refresh takes 60-200ms. So the query is still at least 50x faster than our original un-cached query! The refresh time varies depending on how much data is present in the changing index since the search on the latest index does not hit the cache. This is because as new data is indexed, the cache on the shards of the latest index is constantly being invalidated.
이제 dashboard refresh를 위해 Elasticsearch에 request를 하면, 60~200ms 정도 걸린다. 따라서, query는 원래의 cache되지 않은 query에 비해 여전히 최소 50배 정도 더 빠르다. refresh time은 최신 index에 대한 search가 cache를 이용할 수 없기 때문에, 변경되고 있는 index에 있는 data의 양에 따라 다르다. 이것은 새로운 data가 index되면, 최신 index의 shard에 있는 cache가 계속 무효화되기 때문이다.
The Instant Aggregations feature enables much better caching of search requests by rewriting the query based on the data present on the shard. As of 5.0.0 the query is only rewritten for date range queries, but with this infrastructure in place it opens the door to the potential of rewriting other types of query to improve the request cache utilization. We even have the potential to rewrite aggregations to make them more cacheable! We have shown in this post that the potential performance improvements when we use this technique can be huge, so watch this space for more improvements in the future.
Instant Aggregation 기능을 사용하면 shard에 있는 data를 기반으로 query를 재작성하여 search request를 훨신 더 효율적으로 cache할 수 있다. 5.0.0 부터 date range query만 재작성되지만, 이 infrastructure를 이용하면, request cache의 활용도를 개선하기 위하여, 다른 유형의 query를 재작성할 수 있는 가능성을 열어준다. 심지어 더 많이 cache할 수 있도록 aggregation을 재작성할 수도 있다. 이 게시물에서 이 기술을 사용하면 잠재적 성능 향상이 엄청날 수 있다는 것을 보여주었다. 따라서, 미래애 더 많은 개선을 위해 이 공간을 주목하자.
원문 : Instant Aggregations: Rewriting Queries for Fun and Profit