Blog

2016.09.13 - 번역 - Instant Aggregations: The tale of caching and why it matters ...

drscg 2019. 1. 7. 10:26

This is the first post in a three-part series about Instant Aggregations. See how the story progresses in The Great Query Refactoring: Thou Shalt Only Parse Once and wraps up in Instant Aggregations: Rewriting Queries for Fun and Profit. Enjoy the trilogy!

이 게시물은 Instant Aggregation에 대한 시리즈 중 첫번째 게시물입니다. The Great Query Refactoring: Thou Shalt Only Parse Once에서 어떻게 진행되는지 확인하고 Instant Aggregations: Rewriting Queries for Fun and Profit에서 마무리하자.

It’s early 2013 and an unusually sunny day in Amsterdam and a group of people are meeting around table soccer and ping pong tables for what we call a company all-hands. Just recently Rashid Khan, one of the big characters behind Kibana, joined Elastic and we are still just a handful of engineers. I’m hacking around trying to get checksums to work for recovery, listening to a conversation between Shay and Rashid. It’s Kibana’s initial dashboard slowness that causes this intense conversation. Even though Kibana fires up almost identical searches each time you open the home page, elasticsearch has to recompute everything from scratch. Someone might ask, no caching eh? True!

2013년 초반, 암스테르담, 유별나게 화창한 날이었다. 회사 전체 사람들이 모인 자리에서 table soccer와 탁구 테이블 근체에서 한 그룹의 사람들이 만나고 있었다. 최근에 Elastic에 합류한, Kibana의 대표적인 인물인 Rashid Khan과 소수의 엔지니어였다. 나는 Shay와 Rashid 사이의 대화를 들으면서, recovery를 위한 checksum을 구하기 위해 빈둥거리고 있었다. 이 진지환 대화는 Kibana dashboard의 초기 느림 현상에 대한 것이었다. Kibana는 homepage를 열 때마다, 거의 동일한 search를 수행하는데, Elasticsearch는 처음부터 모든 것을 다시 수행해야 한다. 누군가가 물었다, no cache? 그렇다.

A closer look under the hood shows that searches are almost the same, but are subject to this annoying property of time: it never stands still. If you have used Kibana yourself you might have realized that a default filter is always based on the current time (NOW) going backwards for a defined time range. In other words you never fire the same query more than once a millisecond.

자세히 살펴보며느 search는 거의 같지지만, 다음과 같은 시간의 성가신 속성의 영향을 받는다는 것을 알 수 있다. 시간은 젇대로 가만히 있지 않는다. Kibana를 직접 사용해 보았다면, 기본 filter가 현재 시간(NOW)를 기준으로 정의된 시간 범위에 대해 거꾸로 거꾸로 진행한다는 것을 알게 될 것이다. 즉, 동일한 query를 절대로 1ms에 한 번 이상 실행하지 않는다.

Then the discussion got serious: Rashid and Shay started talking about caching and adding REST level primitives to control the cache key. Time to stop working on checksums; I gotta get involved! If you try to solve one of the hardest problems in computer science and the discussion is heading towards allowing the user to control it, you are either a really brave engineer or all other options would require you to be a hell of a brave engineer! The discussion continued for a while and ideas basically went through the roof.  You might have experienced this in your day to day job before. Luckily, we had so many other problems to solve at that time that we just dropped the ball on it for a while.

그 다음에 토론이 심각해졌다. Rashid와 Shay는 caching과 cache key를 제어하기 위한 REST level primitives를 추가하는 것에 대해 이야기하기 시작했다. checksum 작업을 중단해야 할 때다. 난 참여해야 했다! 컴퓨터 과학에서 가장 어려운 문제를 해결하려 하고, 토론이 사용자가 그것을 제어하는 방향으로 향하고 있다면, 당신은 진정 멋진 엔지니어이거나 굉장히 멋진 엔지니어가 되기 위해 모든 옵션이 필요할 것이다. 토론은 잠시동안 계속되었고, 아이디어는 기본적으로 넘쳐났다. 여러분들도 이전에 작업 중에 매일 이런 경험이 있을 것이다. 운좋게도, 우리는 그 때 해결해야 할 많은 다른 문제가 있어, 잠시 그 문제를 그냥 두었다.

Fast forward: it’s October 1st and my calendar says “The Dudes are in Berlin” meaning that Shay and a bunch of other team leads were coming into town for some planning sessions. That’s usually an intensive time in a distributed company like Elastic since we don’t meet in person more than twice per year. After 3 days of discussions, brainstorming and arguing Shay and I went out for Schnitzel to this awesome Austrian place near my house. Honestly, neither Shay nor I were really up for any more discussions but suddenly the caching thing came up again. I don’t blame anybody; I’m not sure who opened that particular can of worms.

10월 1일이었고, "베를린에 녀석들이 있다" 라는 일정이 있었는데, 이는 Shay와 다른 팀 리더들이 몇 가지 계획을 위해 오고 있다는 뜻이었다. 일년에 2번 이상 직접 만나지 않기 때문에, Elastic 처럼 분산된 회사에서는 보통 많은 주의를 기울여야 하는 시간이다. 3일간의 토론, brainstorming, 논쟁 후, Shay와 나는 집 근처의 멋진 오스트리아 식당으로 Schnitzel을 먹으러 갔다. 솔직히, Shay나 나는 더 이상 토론하고 싶지 않았지만, 갑자기 caching이 다시 생각났다. 나는 아무도 탓하지 않는다. 누가 그런 까다로운 문제를 열었는지 모르겠지만 ...

Anyway, this time we came up with a plan! Admittedly, not low hanging fruit, but something that could actually work well, is fully transparent, easy to test and can be disabled if it’s not working. You noticed that escape hatch, did you? Caching is hard but let me explain what we had in mind. Bear with me, I’m going to take a big swing:

어쨌든, 이번에는 계획을 세웠다. 틀림없이, 쉽게 할 수는 없겠지만, 실제로 잘 동작하는 완전히 명료한, 테스트하기 쉽고, 동작하지 않으면 비활성화할 수 있는. 탈출구를 눈치챘겠지? caching은 어렵지만 생각하고 있는 것을 설명하겠다. 

Elasticsearch is based on Apache Lucene™ which works based on point-in-time view of an index. Such a snapshot is basically a set of write once index segments, each holding a subset of the indexed documents. The software construct we use to represent such a snapshot is what we call a “top-level” indexreader. We know that, unless the top-level reader changes, queries are idempotent or in other words, cacheable. In Elasticsearch there is exactly one Lucene index per shard, so we can simplify things to use one top-level reader per shard. Now, if we can identify the outer bounds of an index for any date field we could also make much better decisions if for instance all or even no documents at all would match a certain filter and therefore could rewrite the query to match-all  or match-no-docs respectively. If we could manage to do that then we could put queries that appear to be un-cachable into the request cache we added basically just before that Schnitzel brainstorming session. The request cache utilizes Lucene’s top-level reader as well as the search request’s binary representation (the plain unmodified JSON, YAML or CBOR bytes) as a combined cache key. This allowed us to minimize the space requirement at the same time as minimizing the number of objects to represent a cache entry.

Elasticsearch는 index의 특정 시점(point-in-time) 보기를 기반으로 동작하는 Apache Lucene™ 기반이다. 이러한 snapshot은 기본적으로 index된 document의 하위 집합을 가지고 각자 가지고 있는 한번 기록(write once)된 index segment의 집합이다. 이러한 snapshot을 나타내는데 사용되는 SW 구조를 "top-level" indexreader라 한다. top-level reader가 변경되지 않는 한, query는 idempotent(멱등), 즉 cache될 수 있다. Elasticsearch에는 shard 별로 정확히 하나의 Lucene index가 있으므로, shard별로 하나의 top-level reader를 사용하도록 단순화할 수 있다. 이제, 특정 data field에 대해 index의 바깥 경계를 식별할 수 있다면, 훨씬 더 나은 의사결정을 내릴 수 있다. 예를 들어 document가 특정 filter에 모두 또는 전혀 일치하지 않는 경우, match-all 또는 match-no-docs로 query를 다시 작성할 수 있다. 그렇게 할 수 있다면, 기본적으로 Schnitzel brainstorming 직전에 추가한 request cache에 cache할 수 없을 것으로 보이는 query를 넣을 수 있다. request cache는 search request의 binary 표현(수정되지 않은 plain JSON, YAML, CBOL bytes)뿐만 아니라 Lucene의 top-level reader를 결합된 cache key로 활용한다. 이렇게 하면, cache entry를 나타내는 object의 수를 최소화하는 동시에 space 요구사항을 최소화할 수 있다.

Back to the idea, the main driver of it was a new utility in Lucene that allows us to fetch the upper and lower bounds of a field from a top-level reader. With this information we could do anything you could possibly imagine with a query that has time properties. You can imagine when two engineers get excited over Schnitzel and caching is involved it ain’t gonna end well.

idea로 다시 돌아가 보면, Lucene의 new utility는 top=level reader에서 field의 상한과 하한을 가져올 수 있는 주요 utility이다. 이 정보를 통해, 시간 속성을 가진 query로 상상할 수 있는 모든 것을 할 수 있다. 2사람의 engineer가 Schnitzel를 사이에 두고 흥분해서, 끝나지 않을 caching에 파묻혀 있는 것을 상상할 수 있을 것이다.

With all that excitement I went and asked Mike Mccandless to give the idea a shot. Mike pulled off a prototype quite quickly. As usual after all that excitement we had to face reality, but the basic idea worked, YAY! Well, reality was that the prototype worked but it was far from anything that could go into production any time soon. We had to realize that to effectively modify queries in a generic and safe way (both are good properties to have if you want to use something as a cache key) we need an intermediate representation for our entire request constructs.

그렇게 흥분하여, Mike Mccandless에게 아이디어를 구현해달라고 요청했다. Mike는 아주 빨리 prototype을 구현했다. 늘 그렇듯이 그런 흥븐 후에, 우리는 현실에 직면했다. 그러나, 기본적인 idea는 효과가 있었다. YAY! 음, 현실은 prototype은 동작했지만, 곧 제품에 적용할 수는 없었다. 일반적이고 안전한 방법(2개 모두는 cache key로 사용하려면 좋은 속성이다)으로 query를 효과적으로 수정하려면 전체 request 구조에 대한 중간 표현이 필요하다는 것을 깨달아야 했다.

At this point we had no way to parse the request, modify it and write it back out into a byte representation that can be used as a cache key. This was kind of a downer for all of us since we had to face the fact that each of these ~70 queries, aggregations, suggest, highlight, sort, etc. classes had to be refactored in order read, normalize, modify and write back its values. What could possibly go wrong.

이 시점에서, request를 parse하고 변경하여, cache key로 사용될 수 있는, byte 표현으로 다시 작성할 수 있는 방법이 없었다. 읽기, 정규화, 수정, 값을 다시 쓰려면, 약 70개의 query, aggregation, suggest, highlight, 정렬 class 등을 refactoring해야 한다는 사실에 직면했기 때문에, 이는 우리 모두에게 실망스러운 결과였다. 무엇이 문제일까?

Is this worth the trouble?

After that prototype and the reality check that our code wasn’t ready we needed to talk about the exciting possibilities again. When you look at your code and you realize you have to basically invest 6 month worth of engineering time to make the first step towards a new feature you think twice about whether it’s worth it.

해당 prototype과 우리 code가 준비되지 않았다는 것을 실제로 확인한 후에, 흥미로운 가능성에 대해 다시 이야기해야 했다. code를 검토해 보고, 새로운 기능을 도입하는데 기본적으로 6개월의 시간이 필요하다는 것을 알게 되었다면, 그럴만한 가치가 있는지 2번 생각해 봐야 한다.

That said, with the immense growth of time-based data and how the data is structured on a macro level it became obvious why it would make sense to invest in solutions that are done the right way. A typical installation of logging data might have daily indices spanning period of a week or a month. Searches typically span all indices in that time range but, except for the current daily index, all other indices are static: they don’t receive any changes anymore and therefore they maintain the same point-in-time snapshot. This means we’d get 100% cache hits if queries included the entire day for each shard. In other words we could reduce the search workload for these machines dramatically and kibana dashboards would appear instantly.

즉, 시간 기반 data의 엄청난 증가와 macro level에서 data가 구성되는 방식을 통해, 올바른 방식으로 진행되는 솔루션에 투자하는 것이 왜 합리적인지 알게 되었다. 일반적인 log data는 일주일 또는 한달에 걸친 일별 index를 가질 것이다. 일반적으로 search는 해당 기간에 해당하는 모든 index를 포함하지만, 현재 날짜의 index를 제외한 나머지 모든 index는 정적이다. 그것들은 더 이상 변경이 없으므로, 동일한 시점의 snapshot을 유지한다. 즉, query가 각 shard에 대해 전체 날짜를 포함하면, cache 적중률이 100% 이다. 즉, 이들 machine의 search 부하를 크게 줄일 수 있고, Kibana dashboard는 즉시 나타난다.

Well, these were convincing arguments, so now it was time to get things started. In March 2015 we started prototyping how we could represent queries and write them back out. Nobody knew that it would take another 12 months for all the needed parts to come together and yet another 6 months for this feature to be released in an alpha release. I won’t tell the rest of the story because I want to leave that to the hard working folks who implemented all these changes. So, stay tuned for the upcoming articles in this series.

이것들은 설득력있는 논쟁이었다. 그러니 이제 시작할 때가 되었다. 2015년 3월, query를 표현하고 다시 작성하는 방법에 대한 prototype을 시작했다. 필요한 부분들을 모두 합치는데 12개월이 걸리고, 이 기능의 alpha 버전을 출시하는데 다시 6개월이 소요되리라는 것을 아무도 알지 못했다. 나머지 이야기는 하지 않겠다. 이 모든 변화를 구현한 열심히 작업한 사람들에게 이를 맡기고 싶기 때문이다. 그러니, 이 시리즈의 다음 이야기를 기대해 달라.

원문: The tale of caching and why it matters