Shard allocation

What is Shard

엘라스틱서치에서 실제 실행되는 하나의 엘라스틱서치 인스턴스를 노드라고 하며, 이 노드들이 모인 집합체를 클러스터라고 합니다. 각 노드들은 샤드(Shard)라는 저장 공간으로 분리되는데, 샤드는 루씬 기반의 검색 인스턴스로 클러스터 내의 데이터 집합에 대해 쿼리를 색인하고 처리하는 역할을 합니다.

엘라스틱서치는 도큐먼트라는 데이터 단위를 사용합니다. 이 도큐먼트들이 모인 집합체를 인덱스라고 부르며, 인덱스는 최소 하나 이상의 샤드들로 구성됩니다. 엘라스틱서치 7버전 이후부터 인덱스는 기본적으로 샤드1개와 복제본1개로 구성되며, 이 샤드들이 중복 배치되지 않게 최소한 노드 3개로 클러스터 구성을 하는 것을 권장하고 있습니다.

엘라스틱서치가 샤드를 배치하는 방식은 RAID 0과 RAID 1의 방식을 혼합한 방식이라고 할 수 있습니다. 먼저 한 인덱스의 샤드들은 RAID 0처럼 Round Robin 방식으로 저장됩니다. 이 과정에서 샤드가 저장될 디스크의 사용량이 최소, 최대 Thresholds를 벗어나지 않는지 확인합니다. 그리고 데이터 유실을 막기 위해 기본 옵션으로 샤드의 복제본 1개를 생성하여, RAID 1처럼 서로 중복되지 않게 서로 다른 노드에 저장합니다. 즉 엘라스틱서치의 샤드 구조는 데이터의 분산 저장과(RAID 0), 데이터 복제본 생성(RAID 1)을 합친 '데이터와 복제본의 분산 저장' 구조(RAID 0 + RAID 1)인 것입니다.

위 그림은 노드 3개로 구성된 클러스터에 인덱스 두개를 추가한 구성도입니다. 각 인덱스는 Primary Shard 1개와 Replica Shard 1개의 쌍으로 구성되어 있으며, Primary를 P, Replica를 R로 표기하였고 같은 데이터를 담고 있는 한 쌍의 샤드를 1~3의 숫자로 표현하였습니다. 구성도를 보면 인덱스1은 3개의 Primary와 3개의 Replica로 분산 저장되어 있고, 인덱스2는 한 쌍의 샤드로만 구성되어 있습니다.

앞서 말했듯, 같은 데이터를 담은 샤드들은 반드시 서로 다른 노드로 분리되어 배치됩니다. 즉 노드 하나가 어떠한 이유로 다운되더라도, 다른 노드의 샤드에 같은 내용이 저장되어 있으므로 원상복구가 가능하며 데이터 유실을 막을 수 있습니다.

Shard allocation - Time out

위 개념을 기반으로 노드 하나가 다운되었다가 Timeout 시간 이내에 다시 올라왔을 경우와 시간 내에 올라오지 못했을 경우의 샤드 할당 과정을 그림과 함께 살펴보려 합니다. 그리고 더 나아가 엘라스틱서치 Rolling restart 과정과 함께 엘라스틱서치의 Shard allocation 옵션에 대해 알아보겠습니다.

위 사진처럼 1~3번의 노드 3개로 구성된 엘라스틱 클러스터가 있습니다. 이 클러스터에는 하나의 인덱스가 저장되어 있고, 인덱스는 각각 3개의 Primary와 Replica 샤드로 구성되어 있습니다. Shard allocation에 특별한 설정을 하지 않은 Default인 경우를 가정하겠습니다.

먼저 모종의 이유로 1번 노드가 다운되었다고 생각해봅시다. 이 경우 엘라스틱서치 클러스터는 잠시동안 노드1이 복구되기를 기다립니다. 복구 대기 시간은 기본적으로 1분으로 설정되어 있으며, ( index.unassigned.node_left.delayed_timeout=1m ) 우선 1분 이내로 복구되는 경우를 가정하겠습니다. 현재 노드 하나가 다운되어 Primary 1번과 Replica 3번이 unassigned된 상태입니다.

클러스터는 끊겨진 노드1에 있던 Primary 1번을 할당하기 위해 노드2의 Replica1번을 Primary로 승격시킵니다. 그리고 unassigned된 Replica 3번을 새로 할당하기 전에, Timeout 1분 동안 노드1의 복구를 기다립니다.

마침 Timeout 1분 내로 노드1이 복구되었습니다. 클러스터는 Replica 3번을 새로 할당하지 않고, 복구된 노드1의 Replica 3번을 그대로 재할당합니다. 그리고 노드1의 기존에 Primary 1번이었던 샤드를 Replica 1번으로 태깅합니다. 이렇게 노드1이 다운되었다가 복구되는 과정에서 Primary는 유지되었고, 정상적으로 서로 다른 노드에 Primary와 Replica가 분리되었습니다. 결과적으로 Primary 1번과 Replica 1번의 위치가 바뀌었네요.

( 노드 다운 후 재시작 시 클러스터 내 샤드 할당 과정 )

하지만 1분 내로 복구가 되지 않은 경우도 있을 것입니다. 이런 경우를 대비하여 index.unassigned.node_left.delayed_timeout 옵션을 미리 조정해도 되지만, 그럼에도 불구하고 Timeout 시간동안 장애 복구가 진행되지 않을 수 있습니다. 이런 때에는 어떻게 샤드 배치가 이루어지는지 알아보겠습니다.

우선 노드1이 다운되면 클러스터는 Replica 1번을 Primary로 승격시킵니다. 그리고 Timeout 시간동안 노드1이 복구되지 않는 경우, 노드2에 있는 Primary 1번의 Replica를 노드3에 생성합니다. 마찬가지로 노드3에 있는 Primary 3번의 Replica를 노드2에 생성합니다. 결과적으로 노드2에는 Primary 1,2번과 Replica 3번, 노드3에는 Primary 3번과 Repica 1,2번이 생성되어 정상적인 클러스터 구조를 형성합니다.

Shard allocation - Rolling restart

지금까지 노드 하나가 다운되었을 경우에 클러스터 내의 샤드 할당이 어떻게 진행되는지 살펴보았습니다. 이번에는 클러스터를 Rolling restart할 경우를 살펴보며 Shard allocation 옵션은 무엇인지, 옵션별 차이점은 무엇인지 알아보겠습니다. (Rolling restart 뿐 아니라 Rolling upgrade 시에도 동일하게 적용하면 됩니다.)

들어가기에 앞서 엘라스틱서치에 Shard allocation 옵션이 왜 필요한지 다시 한번 짚고 넘어가봅시다. Shard allocation은 엘라스틱서치 클러스터가 특정 노드의 다운으로 장애 상황에 빠져있을 때, 장애극복기능(Fail Over) 역할을 하기 위한 옵션입니다. 이를 통해 특정 샤드가 unassigned 되더라도 다른 노드에서 샤드 재배치를 통해 Primary, Replica 샤드 수를 유지 할 수 있는 기능입니다.

그러나 이 샤드 재할당 과정은 I/O에 상당한 오버헤드를 발생시킵니다. 

엘라스틱서치에서 인덱스의 수정, 삭제 등 데이터에 어떠한 변화가 일어날 때에 샤드에 내려지는 명령들은 모두 Translog에 저장됩니다. 그리고 이 Translog들이 일정량 쌓이면 엘라스틱서치는 자동으로 저장된 명령들을 Lucene에 적용시키는 Lucene commit 작업을 진행합니다. 

Lucene commit 작업이 I/O에 오버헤드를 발생시키는 고비용의 작업이기 때문에 엘라스틱서치는 샤드에 생기는 변경사항들을 바로 적용시키지 않고, Translog에 일정량 모아두었다가 처리합니다. 이 때 현재 Translog에 저장된 데이터들이 Lucene 인덱스에 영구적으로 저장되었는지 확인하는 프로세스를 Flush라고 부릅니다.

샤드 할당 과정은 필수적으로 Flush 과정을 필요로 하며, 만약 클러스터의 노드들에 아무런 설정을 하지 않고 Rolling restart 한다면 노드가 다운될 때 마다 이 Flush 과정을 거치게 됩니다. 그저 노드들을 재시작 할 뿐인 작업에 불필요한 오버헤드가 대량 발생하게 되는 것입니다. 이 때 샤드 할당 규칙을 바꾸어 의미없는 절차를 건너뛰게 해주는 옵션이 바로 Shard allocation 옵션입니다.

Shard allocation 설정에는 아주 직관적인 이름의 4가지 옵션이 있습니다.



알아두어야 할 점은 "unassigned 된 primary의 replica가 자동으로 primary로 승격되는 절차"와 이 옵션은 상관이 없다는 점입니다. replica의 primary 승격 절차는 클러스터 단에서 자동으로 진행되는 일종의 Tagging 작업입니다. 다시 말해 'primary를 잃은 replica의 승격'과 '사라진 replica의 재할당'은 서로 다른 프로세스라는 것입니다.

만일 all 옵션을 주어 모든 종류의 샤드 할당을 허용한다면, Rolling restart 절차에서 노드를 재부팅 할 때 마다 샤드 할당을 할 것입니다. 따라서 이 과정을 건너뛰기 위하여 다른 옵션을 주어야 하는데, 공식문서에 따르면 엘라스틱서치 6.6버전 까지는 none 옵션을 권장하고 있으며 그 이후 버전부터는 primaries 옵션을 권장하고 있습니다. 

옵션이 바뀐 이유는 공식문서에 기술되어 있지 않지만, 아마도 혹시나 Tagging 절차의 오류로 primary가 unassigned 된 replica가 primary로 승격되지 못하였을 때 Shard allocation 옵션마저 none일 경우에는 클러스터가 Red 상태에 빠질 수 있기 때문이 아닐까 생각합니다.


primaries 옵션을 주면 primary가 unassigned 된 replica는 자동으로 primary로 승격이 되고, 사라진 replica에 대한 재배치 과정은 이루어지지 않습니다. 즉 Tagging은 이루어지지만 Lucene commit은 이루어지지 않게 되고, Rolling restart 및 Rolling upgrade 속도를 비약적으로 향상시킬 수 있습니다.

여기에 더해 Synced Flush를 사용하여 Lucene commit 검증 절차인 Flush 과정 역시 간소화할 수 있습니다. Synced Flush란 동일한 Lucene Index를 가진 replica들에게 sync_id라는 일종의 마커를 추가하여, 이 마커를 통해 서로 동일한 샤드인지 비교할 수 있게 해주는 기능입니다. 특히 인덱스는 많지만 업데이트는 드문 클러스터에 효율적이며, 재시작 과정에서 가장 cost 소모가 많은 Flush 과정을 간소화해 재시작에 드는 소요시간을 단축시킬 수 있습니다. 

단일 인덱스에 대한 flush 옵션 변경은 위 사진과 같으며, 전체 인덱스에 대한 flush 옵션 변경은 "POST /_flush"를 사용하면 됩니다. 엘라스틱서치 7.6버전 이후부터 Synced flush API가 deprecated 되며, 8버전 이후로는 removed 되었습니다. 기존의 Synced flush API는 Flush API로 대체되었습니다.

최종적으로 위에서 설명한 샤드 할당 옵션과 Synced Flush 옵션들을 고려하여, Rolling restart에 필요한 소요시간과 시스템 자원을 최적화한 결과는 다음과 같습니다. (엘라스틱서치 8버전 이상일 경우)



먼저 불필요한 샤드 할당 작업을 제한하기 위해 allocation 옵션을 primaries로 변경합니다. 그 다음 샤드 비교 절차를 간소화 하기 위해 synced flush 작업을 진행합니다. 이후 노드를 재시작 합니다. 노드가 올라온 것을 확인한 이후에 다시 샤드 할당 옵션을 초기값인 null 또는 "all"로 변경합니다. 이후 클러스터의 상태가 green인지 확인하고, 다시 앞의 과정을 반복합니다.

Conclusion

만일 샤드 할당 옵션이나 Synced flush 옵션을 모른채로 Rolling restart나 Rolling upgrade를 진행한다면, 운이 좋을 경우 약간의 시간낭비만 생기겠지만 최악의 경우에는 클러스터가 Red 상태로 빠지거나 시스템이 중단되고 데이터가 유실될 수 있습니다. 특히 IOPS가 높은 서비스이거나, 내부 네트워크 트래픽량이 높거나, 시스템 자원이 부족한 경우에는 발생하는 오버헤드를 감당하지 못할 가능성이 더욱 높습니다.

본 포스팅을 통해 이러한 불상사를 겪는 엔지니어가 줄어들기를 바라며 글을 마칩니다.

ⓒ 김선민

Link