엔지니어링

Elasticsearch 내 이벤트 기반 데이터에 대한 효율적인 복제 방지

Elastic Stack은 여러 다양한 사용 사례에 활용됩습니다. 보편적인 경우 한 가지는 보안 이벤트, 로그, 메트릭 등 다양한 유형의 이벤트 또는 시계열 기반 데이터를 저장 및 분석하는 것입니다. 이러한 이벤트는 종종 이벤트가 발생하거나 수집된 시기를 나타내는 특정 타임스탬프에 관련된 데이터로 구성되며, 이벤트를 고유하게 식별할 수 있는 자연스러운 키가 없는 경우가 많습니다.

일부 사용 사례와 심지어는 사용 사례 내 데이터 유형의 경우, Elasticsearch 내의 데이터가 복제되지 않아야 합니다. 복제 문서는 부정확한 분석과 검색 오류로 이어질 수 있습니다. 당사는 작년에 Logstash를 활용한 복제 처리 소개(introduction to duplicate handling using Logstash) 블로그 포스트에서 이 사안을 살펴보기 시작했으며, 여기에서는 더 깊이 파고들어가 보편적인 질문 몇 가지를 다루겠습니다.

Elasticsearch로 색인

귀하가 데이터를 Elasticsearch로 색인할 때, 데이터가 성공적으로 색인되었음을 확인하려면 응답을 받아야 합니다. 연결 오류, 노드 충돌 등의 오류로 인해 응답을 받지 못하는 경우, 데이터가 색인되었는지의 여부를 확인할 수 없습니다. 클라이언트가 이러한 유형의 시나리오를 접하게 되는 경우, 전달을 확인하는 표준적인 방법은 다시 시도하는 것이며, 그렇게 하면 동일한 문서가 여러 번 색인될 수 있습니다.

복제 처리에 대한 블로그 포스트에 기술된 대로, Elasticsearch가 각각을 색인 시점에 자동으로 배정하는 대신 클라이언트 내 각 문서에 대한 고유 ID를 정의하여 진행하는 것이 가능합니다. 복제 문서가 동일한 색인에 작성되는 경우 문서가 두 번째로 작성되는 대신 복제를 방지하는 업데이트가 이뤄질 수 있습니다.

UUID 대 해시 기반 문서 ID

어느 유형의 식별자를 사용할지 결정할 때, 주로 선택할 수 있는 두 가지 유형이 있습니다.

UUID(Universally Unique Identifiers)는 분산형 시스템 전반에서 생성될 수 있는 128비트 숫자에 기반한 식별자로, 실용적인 목적을 위해 고유합니다. 이 유형의 식별자는 일반적으로 관련이 있는 이벤트의 콘텐츠에 의존하지 않습니다.

UUID를 사용하여 복제를 방지하기 위해서는 해당 이벤트가 한 번에 정확하게 전달된다는 점을 보증하는 경계 이벤트를 넘어서기 전에 UUID를 생성하고 이벤트에 배정하는 것이 필수적입니다. 이 과정은 많은 경우 실제로는 UUID가 발생 시점에 배정되어야 한다는 것을 의미합니다. 이벤트가 발생된 시스템이 UUID를 생성할 수 없는 경우, 다양한 유형의 식별자가 사용되어야 할 수 있습니다.

다른 유형의 주요 식별자는 이벤트의 콘텐츠에 기반하여 숫자 해시를 생성하기 위해 해시 함수가 사용되는 종류입니다. 해시 함수는 특정 콘텐츠에 대해 항상 동일한 값을 생성하지만 생성된 값이 고유하다고 보증할 수 없습니다. 두 개의 서로 다른 이벤트가 동일한 해시 값을 산출하는 경우인 해시 충돌의 확률은 인덱스 내 이벤트의 수와 사용된 해시 함수의 유형, 생성된 값의 길이에 따라 다릅니다. MD5나 SHA1 같이 그 길이가 128비트 이상인 해시는 보통 많은 시나리오에서 길이와 낮은 충돌 확률 사이에 좋은 절충안을 제공합니다. 고유성이 더 뛰어난 경우 SHA256과 같이 보다 긴 해시를 사용할 수 있습니다.

해시 기반 식별자는 이벤트의 콘텐츠에 따라 달라지므로, 이를 추후의 처리 단계에서 동일한 값이 생성될 때 계산되면 배정할 수 있습니다. 이는 이 유형의 ID를 데이터가 Elasticsearch로 색인되기 이전 시점에 배정할 수 있게 해 주어 인제스트 파이프라인을 설계할 때 유연성을 제공합니다.

Logstash는 UUID 계산을 지원해주며, 일정한 범위의 인기있고 보편적인 지문 필터 플러그인을 통한 해시 함수를 갖추고 있습니다.

효율적인 문서 ID 선택

Elasticsearch가 색인 시점에 문서 식별자를 배정하도록 허용될 때, 생성된 식별자가 색인 내에 미리 존재할 수 없다는 점이 파악되기 때문에 최적화를 수행할 수 있습니다. 이렇게 하면 색인 성능이 개선됩니다. 외부적으로 생성되어 문서로 전달되는 식별자의 경우, Elasticsearch는 이를 반드시 잠재적인 업데이트로 다루며 문서 필터가 기존 색인 세그먼트 내에 이미 존재하는지 점검합니다. 여기에는 추가적인 작업이 필요하며 따라서 속도를 늦춥니다.

모든 외부 문서 식별자가 동일하게 생성되는 것은 아닙니다. 시간별로 분류 순서에 따라 점진적으로 증가하는 식별자는 일반적으로 랜덤 식별자에 비해 더 나은 색인 성능을 보입니다. 그 이유는 Elasticsearch가 검색할 필요 없이 이전 세그먼트 내의 전적으로 최소 및 최대 식별자 값에 근거해 식별자가 해당 색인 세그먼트 내에 존재하는지 여부를 빠르게 결정할 수 있기 때문입니다. 이 사항은 본 블로그 포스트에 기술되어 있으며, 약간 구식이 되어가고 있기는 하지만 여전히 해당되는 내용입니다.

해시 기반 식별자와 많은 유형의 UUID는 보통 본질적으로 랜덤입니다. 각각의 이벤트에 정의된 타임스탬프가 있는 경우 이벤트의 흐름을 취급할 때 우리는 이 타입스탬프를 식별자에 대한 접두사로 이용하여 이벤트를 분류 가능하게 하고 색인 성능을 향상시킬 수 있습니다.

또한 타임스탬프가 접두사로 적용된 식별자를 생성하는 것도 해시 값이 타임스탬프별로 고유하기 때문에 해시 충돌 확률을 감소시키는 장점이 있습니다. 이는 인제스트 볼륨이 높은 시나리오에서도 보다 짧은 해시 값을 사용할 수 있게 해 줍니다.

우리는 지문 필터 플러그인을 사용하여 UUID 또는 해시를 생성하고 Ruby 필터를 사용하여 타임스탬프를 대표하는 헥스 인코딩 스트링을 생성하여 Logstash에서 이러한 유형의 식별자를 생성할 수 있습니다. @timestamp 필드로 해시할 수 있고 이벤트 내 타임스탬프의 분석이 이미 이뤄진 메시지 필드가 있다고 가정하는 경우, 식별자의 구성 요소를 생성하고 다음과 같은 메타데이터 내에 보관할 수 있습니다.

fingerprint {
  source => "message"
  target => "[@metadata][fingerprint]"
  method => "MD5"
  key => "test"
}
ruby {
  code => "event.set('@metadata[tsprefix]', event.get('@timestamp').to_i.to_s(16))"
}

그런 경우 이 두 필드는 Elasticsearch 출력 플러그인에 문서 ID를 생성하는 데 사용될 수 있습니다.

elasticsearch {
  document_id => "%{[@metadata][tsprefix]}%{[@metadata][fingerprint]}"
}

이렇게 하면 문서 ID는 4dad050215ca59aa1e3a26a222a9bbcaced23039와 같이 헥스 인코딩되고 40자 길이의 문자로 이뤄지게 됩니다. 전체 구성의 예가 요약에 나와 있습니다.

색인 성능의 영향

서로 다른 유형의 식별자를 사용할 때의 영향은 데이터, 하드웨어, 사용 사례에 따라 크게 다릅니다. 당사는 귀하에게 일반 지침을 제공해 드릴 수 있지만 귀하의 사용 사례에 정확히 어떤 영향을 미치는지 알기 위해서는 벤치마크를 수행하는 것이 좋습니다.

색인 작업 처리량을 최적화하기 위해 Elasticsearch에 의해 자동으로 생성된 식별자를 사용하는 것이 언제나 가장 효율적인 옵션입니다. 업데이트 확인이 필요하지 않기 때문에 인덱스와 샤드가 규모 면에서 성장해도 색인 성능은 크게 변화하지 않습니다. 그러므로 가능한 경우 언제든 이 기능을 사용하는 것이 권장됩니다.

외부 ID를 사용하여 발생하는 업데이트 확인은 추가 디스크 액세스를 필요로 합니다. 이 점이 미치는 영향은 운영체제가 필요한 데이터를 얼마나 효율적으로 캐시할 수 있는지, 저장 공간이 얼마나 빠른지, 랜덤 리드를 얼마나 제대로 처리할 수 있는지에 따라 다릅니다. 또한 인덱스와 샤드가 성장하고 더 많은 세그먼트를 점검해야 하게 되면서 색인 속도는 종종 떨어집니다.

롤오버 API의 사용

전통적인 시간 기반 인덱스는 특정 설정 기간을 대상으로 하는 각 색인에 의존합니다. 이는 데이터 볼륨이 시간에 따라 변하는 경우 색인과 샤드 규모가 크게 차이나게 될 수 있음을 의미합니다. 불균등한 샤드 규모는 좋지 않으며 성능 문제로 이어질 수 있습니다.

롤오버 색인 API가 도입되어 시간 뿐만 아니라 여러 기준에 근거해 시간 기반 인덱스를 관리하는 유연한 방식을 제공했습니다. 이를 통해 기존의 색인이 특정한 규모, 문서 수 및/또는 수명에 도달하면 새로운 색인으로 롤오버할 수 있게 되었으며, 샤드와 색인 규모를 보다 예측하기 쉽게 되었습니다.

하지만 이렇게 하여 이벤트 타임스탬프와 이에 속한 색인 사이의 연결이 단절되었습니다. 인덱스가 엄격하게 시간에만 기반한 경우, 이벤트가 아무리 늦게 도달한 경우라도 동일한 색인으로 이동하게 됩니다. 이 원칙으로 외부 식별자를 사용하여 복제를 방지할 수 있게 되었습니다. 그러므로 롤오버 API를 사용할 때 복제를 방지할 확률이 감소하기는 했지만 완전히 방지할 수는 없게 되었습니다. 두 개의 복제 이벤트에 동일한 타임스탬프가 있는 경우에도 두 복제 이벤트는 롤오버의 양쪽에 도달할 수 있으며, 따라서 다른 인덱스로 종결될 수 있어 업데이트로 이어지지 않습니다.

그러므로 복제 방지가 엄격하게 요구되는 경우 롤오버 API를 사용하는 것은 권장되지 않습니다.

예측 불가능한 트래픽 볼륨 채택

롤오버 API를 사용할 수 없는 경우에도, 볼륨이 변하고 지나치게 작거나 큰 시간 기반 인덱스가 나타나는 경우 샤드 규모를 채택하고 조정할 방법은 여전히 있습니다.

트래픽 급증 등의 이유로 샤드가 지나치게 큰 상태로 종결되는 경우 분할 색인 API를 사용하여 인덱스를 더 많은 수의 샤드로 분할할 수 있습니다. 이 API는 인덱스 생성 시점의 설정 적용을 필요로 하므로 색인 템플릿을 통해 추가되어야 합니다.

반면, 트래픽 볼륨이 지나치게 낮아 비정상적으로 작은 샤드가 나타나는 경우, 수축 색인 API가 사용되어 색인 내 샤드의 수를 줄일 수 있습니다.

결론

이 블로그 포스에서 보신 것처럼 Elasticsearch에서 데이터를 Elasticsearch로 색인하기 전에 문서 식별자를 외부적으로 지정하여 복제를 방지할 수 있습니다. 식별자의 유형과 구조는 색인 성능에 상당한 영향을 미칠 수 있습니다. 하지만 이는 사용 사례에 따라 다르기 때문에 귀하와 귀하의 특정 시나리오에 가장 알맞은 방식을 확인하려면 벤치마크하는 것이 권장됩니다.