우선 결론을 먼저 말씀드리면, Elasticsearch에서의 deep paging은 안하는게 맞습니다. 지금부터 그 이유를 알아보겠습니다.
Deep Paging 란
인터넷을 하다보면 이런 화면을 여럿 보셨을 겁니다.
화살표를 통하여 현재 보고있는 페이지를 이동하는 것, 우측으로 갈 수록 점점 높은 숫자의 페이지로 이동하고 보고자 하는 내용은 더 오래된, 더 내가 원하는 내용과는 거리가 먼 그런 데이터가 노출되는 페이지로 이동하게 됩니다.
페이지를 우측으로 이동할 수록 더 깊숙히 있는 정보를 조회하는 느낌이 듭니다. 단순 10의 자리 많아야 100의 자리 페이지를 조회하는데는 큰 문제가 없을 수 있습니다.
하지만 만약 9999999 번째 페이지 를 조회하면 어떻게 될까요?
만약 이 페이징의 기준이 데이터가 적재된 순서 라고 하면 서버는 1~9999999번 째 페이지에 해당하는 모든 정보를 데이터가 적재된 순서로 정렬해야합니다.
그래야 가장 최근에 적재된 데이터를 1번 페이지로, 가장 오래전에 적재된 데이터를 9999999 페이지로 보여줄 수 있겠죠.
한 페이지에 10개의 데이터를 보여준다고 했을 때 대략 구천만개 이상의 데이터를 정렬해야합니다.
검색을 시도한 사용자에게 페이징 기능을 제공하기 위해서는 최소한 한번의 정렬 은 이뤄저야합니다.
여기서 잠시 페이징 검색의 요구사항을 하나 짚어보겠습니다
- 첫 조회 후 페이징된 데이터는 실시간으로 변하면 안된다.
- 얕은 페이지(최신 데이터) 와 깊은 페이지 (오래된 데이터)를 자유롭게 오갈 수 있어야한다.
만약 사용자가 “치킨” 이라는 키워드를 검색하였는데, 때마침 월드컵 시즌이라 순식간에 “치킨” 이라는 데이터가 폭발적으로 적재되었다고 가정합니다.
사용자가 검색했을 때 페이지가 총 5페이지 였는데 1번 부터 5번 페이지 까지 이동하는 동안 페이지가 계속 늘어나서 순식간에 9999999 번째 페이지가 생성되었습니다
그럼 이 데이터 역시 데이터가 적재된 순서에 따른 페이징이라면, 방금전까지 보고있던 5번 페이지의 데이터가 뒤로 쭉쭉 밀려 9999999번째 페이지까지 이동 될 겁니다.
그러면 사용자는 “어라? 5번 페이지에서 봤던 내용이 또 나오네?” 라는 불편을 겪게되겠죠.
그렇기 때문에 페이징 검색의 경우 재검색을 시도하지 않을 때는 페이징된 내용이 유지되어야 합니다. 검색 키워드와 일치하는 데이터가 실시간으로 추가된다 하더라도 이미 검색되어진 정보는 유지되어야하죠
또한 사용자가 9999999번 페이지 를 조회하여 보다가 100번 페이지를 다시 보고 싶을 경우 언제든지 이동할 수 있어야합니다.
즉 사용자가 검색을 시도한 시점의 검색 결과를 어딘가에 유지해둬야한다는 말 입니다.
그래야 사용자가 페이지를 이동하는 순간에도 실시간으로 페이지 데이터가 변경되지 않고, 지나쳐온 이전 페이지를 다시 보고싶을 때 언제든지 이동할 수 있기 때문입니다.
이때 Elasticsearch에서는 양날의 검이 발동하는데 자칫하면 큰 문제를 일으킬 수 있습니다, RDBMS와 비교를 통해 알아보겠습니다.
RDBMS VS Elasticsearch
RDBMS와 Elasticsearch의 가장 큰 차이는 서비스 자체의 목적이 다르다는 겁니다
RDBMS | Elasticsearch | |
---|---|---|
목적 | 관계형 데이터 베이스 | 검색 엔진 |
RDBMS의 경우 데이터 베이스 로 사용되기 위한 목적을 가진 서비스입니다.
흔히들 말하는 ACID규칙을 지키는 서비스입니다.
나무위키 : ACID 원자성 (原子性, Atomicity): 한 트랜잭션의 모든 작업이 수행되든지, 아니면 하나도 수행되지 않아야 한다. 트랜잭션이 제대로 실행되지 않았으면 롤백(roll back)한다. 일관성 (一貫性, Consistency): 모든 트랜잭션은 데이터베이스에서 정한 무결성 (無缺性, integrity) 조건을 만족해야 한다. 격리성 (隔離性, Isolation): 두 개의 트랜잭션이 서로에게 영향을 미칠 수 없다. 트랜잭션이 실행되는 동안의 값은 다른 트랜잭션이 접근할 수 없어야 한다. 내구성 (耐久性, Durability): 트랜잭션이 성공적으로 끝난 뒤에는, (시스템 실패가 일어나더라도) 그 결과가 데이터베이스에 계속 유지되어야 한다.
반면에 Elasticsearch는 검색 엔진 입니다.
저희가 주로 사용하는 구글, 네이버 등의 검색 사이트를 보면
검색 키워드와 관련성이 높은! 데이터를 빠르게! 조회한다 를 궁극적이고 원초적인 목적으로 갖고있습니다.
이를 가능케 하기 위해서 존재하는게 검색 엔진 이고요
극단적으로 구분하자면
- RDBMS : 안전이 최고다! 다른건 그 이후 얘기
- Elasticsearch : 속도가 최고다! 다른건 그 이후 얘기
그럼 Elasticsearch에서는 검색 속도를 높이기 위해서 무슨 짓을 하나?
elasticsearch가 조회속도를 빠르게 하기 위하여 취하는 방법은 아래와 같이 두개를 대표로 들 수 있습니다.
- 역색인
- 분산 아키텍처
역색인
RDBMS에서의 검색
예를 들어 RDBMS에 아래와 같은 정보가 있을 때
no | sample_text |
---|---|
1 | hello my world |
2 | hello world |
3 | my world |
4 | world |
5 | good world |
특정 키워드를 기준으로 일치하는 row를 조회한다고 생각해보겠습니다.
- 키워드 :
hello
순차적으로 no = 1 row 부터 no = 5 인 row까지 차례대로 hello를 포함하는지 검색할 겁니다.
이 때 hello
를 포함하고있는 row는 총 두개가 되겠죠
no | sample_text |
---|---|
1 | hello my world |
2 | hello world |
3 | my world |
4 | world |
5 | good world |
그러면 검색된 정보는
|no|sample_text|
|—|—|
|1|hello
my world|
|2|hello
world|
이런식이 될 겁니다.
키워드만 바꿔서 다른 예시를 보겠습니다
- 키워드 :
world
모든 row들이 world
포함하고 있으니 아래와 같이 검색 될 겁니다.
no | sample_text |
---|---|
1 | hello my world |
2 | hello world |
3 | my world |
4 | world |
5 | good world |
이와 같이 순차적으로 키워드 와 일치하는 방식을 찾는 것이 기존 일반적인 RDBMS의 검색 방식입니다.
보통 ‘LIKE’ 문법을 사용하며 쿼리(mysql 기준)는
SELECT * FROM 테이블명 WHERE sample_text LIKE '%hello%';
과 같이 나올 겁니다.
만약 데이터가 1000개라면 어떻게 될까요?
순차적으로 모든 1000개의 데이터를 확인하여 키워드가 일치하는지 확인해볼겁니다.
데이터가 급격하게 증가하여 1억 개가 되었다고 합시다. 이 경우에도 1억개의 데이터를 하나씩 확인하여 키워드와 일치하는지 확인해야죠
이 방식을 간단하게 표현하자면 ROW(문서)
에서 일치하는 키워드
를 찾는 것
순서를 보자면
문서
를 열람한다- 일치하는
키워드
가 있는지 찾는다 - 그 다음
문서
에서 1, 2를 반복한다
이런식으로 동작합니다.
위 방식의 문제점은 데이터의 수에 따라 검색 속도가 급격히 느려진다는 것 입니다.
이 문제를 해결하기 위한 방식이 역색인입니다.
역색인을 활용한 검색
역색인을 먼저 간단하게 표현하자면 키워드
에서 일치하는 문서
를 찾는 것 입니다. 기존 방법과는 뭔가 반대로 동작하는 느낌이죠?
예시를 들어보겠습니다.
위에 예시를 들었던 테이블 정보를 기준으로
no | sample_text |
---|---|
1 | hello my world |
2 | hello world |
3 | my world |
4 | world |
5 | good world |
였던 테이블을 주요 키워드 단위로 미리 아래와 같이 정리해놨다고 생각해 보십니다.
텀(Term) | no |
---|---|
hello | 1, 2 |
world | 1, 2, 3, 4, 5 |
my | 1, 3 |
good | 5 |
여기서 잠깐! Term이란 Elasticsearch에서 추출된 주요 키워드들 을 지칭하는 용어입니다.
자 위와 같이 정리되어있는 데이터에서 똑같이
- 키워드 :
hello
로 검색을 시도하면 “no = 1, 2” 로 검색이 되겠죠 그러면 원본 테이블에서 no가 1인 row와 2인 row 2개가 일치하다는걸 바로 알 수 있습니다.
마찬가지로
- 키워드 :
world
로 검색시에는 “no = 1, 2, 3, 4, 5” 로 검색이 되겠죠
이렇게 키워드 와 일치하는 row를 바로 찾을 수 있습니다.
이렇게되면 모든 데이터를 한번씩 순회해야하는 문제가 해결됩니다.
“그런데 어차피 키워드 단위로 정리를 위해서는 모든 데이터를 순회해서 정리를 해놓는 거 아닌가요?”
그래서! elasticsearch 는 데이터가 삽입되는 시점에 미리 키워드를 추출하여 정리하는 색인 작업을 진행합니다.
데이터 삽입 시 매번 색인 작업이 이뤄지기 때문에 데이터 삽입이 느리다 라는 Elasticsearch의 특징이 있습니다.
분산 아키텍처
Elasticsearch 는 혼자가 아닌 여럿이서 동작할 때 더 능률이 높아집니다.
이때 사용되는 기술이 클러스터링 이라는 것 인데, 여기에 다 적기에는 내용이 너무 길어지기 때문에 기존에 작성했던 Elasticsearch의 기본개념 을 참고해주시면 감사하겠습니다.
간단하게만 말하자면 하나의 Elasticsearch 서비스를 한개의 노드 로 판단하고 이 노드들(N개) 을 하나의 그룹으로 묶는 것이 클러스터링 입니다.
이렇게 만들어진 그룹을 클러스터 라고 하며, 클러스터의 특징은 생성되는 데이터(Elasticsearch에서는 Document, RDBMS에서는 row의 개념)를 여러 노드에 분산 저장하게 됩니다.
A클러스터라는 클러스터가 하나 존재하고 이 A클러스터는 총 3개의 데이터 노드를 갖으며 user 인덱스 가 존재한다고 가정해보겠습니다.
이 user 인덱스에 A,B,C 라는 3명의 user를 등록하는 쿼리를 적용했을 때 데이터는 아래와 같이 쌓입니다.
노드1 | 노드2 | 노드3 |
---|---|---|
A유저 | B유저 | C유저 |
등록되는 정보가 각각 다른 노드에 저장되는걸 볼 수 있습니다.
그럼 조회시 어떻게 동작할까요?
예를들어 B 라는 유저를 조회하는 쿼리를 실행했다고 생각해보겠습니다.
그럼 모든 노드에다가 B라는 유저가 존재하는지 우선 확인을 해야합니다.
B라는 유저 데이터가 노드1, 노드2, 노드3 각각 존재할 수도 있기 때문이니, 각각의 노드를 전부 뒤져봐야합니다.
그렇기 때문에 조회 쿼리는 모든 노드에 전달되는데
이때 각 노드들의 조회 쿼리에 대한 검색을 자신이 들고있는 정보에서 진행하게되는데 각 노드들의 작업이 병렬로 진행됩니다.
이것이 분산 아키텍처의 핵심입니다. 병렬 로 작업이 진행된다는 것!
좀 더 상세히 따지면 샤드 단위로 병렬 진행인데, 분산 아키텍처의 조회 속도가 빠른 이유가 병렬로 작업이 진행되 빠르다는 것을 설명하기 위한 자리이니 상세한 내용은 생략하겠습니다.
역색인, 분산 아키텍처 이게 Deep Paging 과 무슨 상관인가?
다시 Deep Paging 로 돌아가 봅시다.
Elasticsearch의 경우 페이징을 할 수 있는 방법이 크게 3가지 존재합니다.
1. from, size 2. scroll 3. search after (7.10 이하 버전에서는 사용 불가합니다.)
자 하지만 저희는 요구사항이 존재합니다
- 요구사항 1 : 첫 조회 후 페이징된 데이터는 실시간으로 변하면 안된다.
- 요구사항 2 : 얕은 페이지(최신 데이터) 와 깊은 페이지 (오래된 데이터)를 자유롭게 오갈 수 있어야한다.
보통의 페이징 처리의 경우 위 요구사항이 기본적으로 들어갑니다.
Elasticsearch 페이징 방식
방식 | 요구사항 1 | 요구사항 2 | 특징 | 단점 | RDBMS에서 비슷한 방식 |
---|---|---|---|---|---|
from,size | X | O | 임의의 페이지로 한방에 이동할 수 있습니다 (ex : 1페이지에서 바로 30번 페이지로 이동 등 | 인덱스의 max_result_window(default:10000) 크기를 초과할 수 없다 | offset, limit |
scroll | O | X | 최초 검색시의 컨텍스트를 유지하여 실시간 데이터가 추가되어도 컨텍스트가 유지되는(생명주기) 동안은 동일한 데이터로의 페이징을 보장 할 수 있습니다. | 컨텍스트는 검색 결과를 메모리에 유지시키기 때문에 컨텍스트가 살아있는 동안 많은 메모리를 소모합니다 | cursor, fetch |
search after | X | O(불완전) | 이전 검색한 데이터의 마지막 값을 기준으로 다음을 조회합니다. | 페이징 중간에 신규 데이터가 추가되면 페이징에 영향을 끼칩니다(일관성이 없어질 수 있음) | keyset pagination |
search after + PIT | O | O(불완전) | 이전 검색한 데이터의 마지막 값을 기준으로 다음을 조회합니다 뿐만 아니라 PIT 기능을 같이 활용하면 인덱스를 임시 캡쳐할 수 있어서 조회 당시의 데이터에서 변화가 없이 안전하게 조회할 수 있습니다 | 임의의 페이지 접근이 완벽한게 아니다. 한번 조회했던 페이지의 마지막 값을 별도록 저장해놓는 방식을 활용해야한다 | keyset pagination + cursor |
그럼 어느 걸 선택 해야 하나
그나마 요구사항 1, 2를 둘다 부합할 수 있는 것은 search after + PIT 말고는 없습니다.
문제는 PIT 가 7.10 미만 버전에서는 사용 불가능한 기능이라는 겁니다.
그럼 deep paging 을 하지 말란 소린가요?
“네..!! 저의 경험으로는 확실하게 말씀드릴 수 있습니다 안하는게 맞습니다.”
정 페이징을 사용해야할 경우 from, size 를 사용하면서 인덱스의 max_result_window 를 넘는 경우 검색을 하지 못하도록 막는 방법으로 가야합니다.
max_result_window 제한을 늘려서 사용하는 방법은?
이때를 위해 분산 아키텍처 를 설명했습니다.
분산 아키텍처 특징으로 클러스터에 포함된 노드(좀더 명확히는 샤드)들에 쿼리가 똑같이 전달되며 병렬로 작업이 이뤄진다고 했습니다.
엘라스틱서치 공식 페이지의 글을 인용하겠습니다
Deep Paging in Distributed Systems
To understand why deep paging is problematic, let’s imagine that we are searching within a single index with five primary shards. When we request the first page of results (results 1 to 10), each shard produces its own top 10 results and returns them to the coordinating node, which then sorts all 50 results in order to select the overall top 10.
Now imagine that we ask for page 1,000—results 10,001 to 10,010. Everything works in the same way except that each shard has to produce its top 10,010 results. The coordinating node then sorts through all 50,050 results and discards 50,040 of them!
You can see that, in a distributed system, the cost of sorting results grows exponentially the deeper we page. There is a good reason that web search engines don’t return more than 1,000 results for any query. https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html
쉽게 요약하자면 이런 내용입니다.
“깊은 페이지를 검색할 수록 리소스 사용량은 급격하게 늘어난다”
쿼리를 받은 샤드들은 자신이 가진 데이터에서 쿼리에 명시된 데이터 수량만큼 (페이지당 데이터 수) 일단 조회합니다. 이런 샤드들이 여러개있으면 그 모든 샤드들의 조회 결과를 합산한 다음 실제로 검색자가 요구한 데이터와 가장 부합하는 10개의 데이터만을 반환하고 나머지는 버립니다.
만약 max_result_window 를 100만개로 늘려서 마지막 100만 번째 데이터를 from, size로 조회하면..?
각 샤드가 100만개씩, 총 500만개의 데이터가 정렬 작업을 위해 순간적으로 메모리에 올라갈 겁니다. 서비스가 꿱 하고 죽어도 이상이 없게되겠죠
“어? 그러면 다른 방식도 결과적으로 깊은 페이징을 하면 위험한거 아닌가요?”
물론 다른 방식들도 위험하긴 마찬가집니다. 하지만 상대적으로 from, size에 비하여 안전하죠
from, size의 경우 99999 페이지를 조회할때도 전체 데이터에 대한 정렬이 이뤄지고 99998페이지를 조회할 때도 정렬이, 99997페이지를 조회할 때도 정렬이, 페이지를 바꿀때마다 정렬이 이뤄집니다.
보통 정렬 알고리즘은 메모리상에서 동작하는데 그만큼 대용량 메모리 공간이 자주 할당된다는 얘기죠,
반면에 scroll 또는 search after의 경우 최초 한번만 검색 데이터에 대하여 정렬한 후 메모리에서는 내리고 파일로 관리하면 됩니다 페이지를 이동한다고 해서 전체 데이터를 다시 정렬하기 위하여 대량의 메모리공간을 할당하는 증상이 발생하지 않죠
서비스 입장에서는 딱 한번만 버티면 이후에 페이지를 여러번 변경하여도 문제가 없습니다 하지만 이 딱 한번의 타이밍에 여러 동작이 동시에 이뤄지면 서비스가 다운될 수 있게됩니다
결론 : Elasticsearch에서 Deep Paging은 포기하세요
- 분산 아키텍처를 채택한 Elasticsearch에는 Deep Paging 이 어울리지 않음
- 구글 처럼 최대 페이지 크기를 제한 시키고 검색 키워드를 바꿔서 검색을 하도록 유도해야함