엔지니어링

Elasticsearch: 외부 데이터 저장소의 데이터 무결성 확인

Elasticsearch를 다른 데이터베이스와 함께 사용하는 경우가 있습니다. 그러한 경우에는 관련된 전체 시스템에서의 트랜잭션 자원들의 부하로 인해 2단계 커밋 에 대한 솔루션을 구현하기가 어려울 때가 많습니다.경우에 따라 두 데이터 저장소에 존재하는 데이터의 확인이 필요하거나 필요하지 않을 수 있으며, 이때 한 데이터 저장소가 다른 데이터 저장소에 대한 "원본 기준"이 됩니다.

Elastic의 서포트 엔지니어들은 확인을 위한 최상의 데이터 구성 방법과 효율적인 확인 방법 등에 관해 자주 문의를 받습니다. 이 블로그 게시물에서는 Elasticsearch가 PostgreSQL 등 데이터베이스의 필요한 데이터를 포함하고 있는지 확인하는 몇 가지 예를 기초 수준에서 고급 수준까지 서포트 엔지니어들이 경험하고 지원한 것들 위주로 소개하고자 합니다.

확인을 위한 데이터 모델링

Elasticsearch 안팎에서 데이터가 저장되는 방식은 확인 난이도에 큰 차이를 만들어낼 수 있습니다.  가장 먼저 결정해야 하는 것은 "얼마나 많은 확인이 필요한가?" 입니다:

  • 도큐먼트의 존재여부 확인으로 충분한가?
  • 또는 내부 프로세스가 전체 도큐먼트에 대한 검증을 요구하는가?

이 결정은 확인 작업에 필요한 노력의 양에 영향을 미칩니다.

존재 확인 검증의 개선

다행히도 이는 심층적인 철학적인 질문이 아닙니다. 대신 "Elasticsearch에 모든 도큐먼트가 존재하는가?" 라는 간단한 질문입니다.

상황을 완전히 이해하기만 한다면 Elasticsearch에서는 다양한 방법으로 존재 확인 검증이 가능합니다. Remember that Elasticsearch는 거의 실시간으로 검색을 하지만, 완전한 실시간으로 도큐먼트를 직접 가져올 수 있습니다. 즉, 도큐먼트를 색인한 직후에는 검색 결과에 나타나지 않을 수 있지만 직접적인 GET 요청은 작동합니다.

각각 건별로 확인

소규모 배포의 경우, 가장 간단한 접근 방법은 개별 도큐먼트에 대해 HEAD 요청을 수행한 다음 HTTP 응답 코드가 404(페이지 또는 도큐먼트가 발견되지 않음)가 아님을 확인하는 것입니다.

HEAD /my_index/my_type/my_id1

성공적인 응답의 예는 성공 헤더 메시지 입니다.

HTTP/1.1 200 OK
Content-Type: text/plain; charset=UTF-8
Content-Length: 0 

그리고 존재하지 않는 경우에도, 응답 코드를 제외하고는 실제로 똑같이 보입니다.:

HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Content-Length: 0 

이를 위해서는 N 개의 도큐먼트가 있는 것으로 예상되는 인덱스에 대해 N 번의 요청을 수행해야 합니다. 예상할 수 있는 것처럼 이는 그리 효율적인 방법이 아닙니다.

배치 처리

다음으로 선택할 수 있는 것은 Multi GET API: _mget 을 사용하여 이를 배치 요청으로 수행하는 것입니다.

GET /my_index/my_type/_mget
{
  "ids": [ "my_id1", "my_id2" ]
}

이와 동등한 또 다른 방법은 ID들을 검색한 후 예상되는 건 수를 반환하는 것입니다.

GET /my_index/_refresh
GET /my_index/my_type/_search
{
  "size": 2,
  "query": {
    "ids": {
      "values": [ "my_id1", "my_id2" ]
    }
  }
} 

먼저, _refresh endpoint 를 실행하여 색인된 모든 항목이 검색 가능하도록 합니다. 이렇게 하면 검색에서 "거의 실시간 검색" 효과가 사라집니다. 일반적인 검색 조건에서는 이렇게 하면 안 되지만, 여기서는 이렇게 하는것이 이치에 맞습니다! 아마도 확인 처리를 한 번에 모두 할 것이므로 프로세스가 시작된 후 새로 데이터가 들어오지 않도록 제어만 한다면, 작업을 시작할 때 _refresh 를 한 번 호출하는 것으로 나머지는 충분합니다.

이로서 훌륭한 요청 처리가 됩니다. 하지만 백그라운드 작업을 피할 수 없는 것과 마찬가지로 각 도큐먼트 결과들도 반환되기 때문에 오버헤드가 늘어납니다.

검색 후 배치 처리

배치 처리에서 사람들은 대개 문제를 분할해서 해결하려고 합니다. 먼저, 검색을 수행하여 누락된 것이 무엇인지 확인한 후에야 누락된 도큐먼트에 대한 심층 조사를 시작하는 경우가 많습니다.:

GET /my_index/_refresh
GET /my_index/my_type/_search
{
  "size": 0,
  "query": {
    "ids": {
      "values": [ "my_id1", "my_id2" ]
    }
  }
}

응답의 hits.total 값을 확인하는 경우, 예상한 값과 일치해야 합니다. 이 간단한 예제 에서는 결과값이 2 여야 합니다. 2가 아니면 방식을 바꿔 다음 중 하나를 수행해야 합니다:

  1. 모든 ID를 검색하고 적절한 크기를 지정하여 이들을 실제로 반환한 후 건초 더미에서 바늘을 찾습니다.
  2. 위의 _mget 예를 이용하여 같은 작업을 반복합니다. 즉, 누락된 ID가 무엇인지 확인합니다.

더 좋은 방법이 있을까요?

장시간 처리나 2단계 처리는 규모 면에서 약간의 문제가 될 수 있으며, 이는 고비용의 질문을 던지거나 잘못된 질문을 던지고 있음을 나타냅니다.

쿼리나 질문을 거꾸로 뒤집어 "존재하는 모든 것을 찾는" 대신 "누락된 것을 찾는다면" 어떻게 될까요? "없는 것"을 쿼리하기는 어려우며, 실제로 데이터가 존재하지 않으므로 불가능합니다. 하지만 데이터를 효율적으로 구성한다면 어그리게이션을 통해 질문에 대한 답을 얻을 수 있습니다.

대부분의 검증 사례는 SQL 환경에서 오며, 이 환경에서는 일반적으로 primary key와 foreign key에 정수 기반의 키가 사용됩니다. 그러한 숫자 ID를 Elasticsearch _id 로 사용하지 않는 경우에도, doc value를 enabled 하여(ES 2.x 이상에서는 기본적으로 켜짐) 정수 기반 값으로 인덱스를 작성해야 합니다. 예:

POST /my_index/my_type/my_id1
{
  "id": 1,
  …
} 

필드 이름은 유연하게 할 수 있지만, Elasticsearch의 예약된 메타데이터 필드인 _id는 사용하지 말아야 합니다. 이 값에 대해 색인을 하고 나면 누락된 데이터를 히스토그램으로 손쉽게 찾을 수 있게 됩니다:

GET /my_index/my_type/_search
{
  "size": 0,
  "aggs": {
    "find_missing_ids": {
      "histogram": {
        "field": "id",
        "interval": 1,
        "min_doc_count": 0
      },
      "aggs": {
        "remove_existing_bucket_selector": {
          "bucket_selector": {
            "buckets_path": {
              "count": "_count"
            },
            "script": {
              "inline": "count == 0",
              "lang": "expression"
            }
          }
        }
      }
    }
  }
} 

이때 다음 두 가지 작업이 수행됩니다:

  1. 숫자값으로 된 id 필드에서 각 값 사이에 1 단위로 히스토그램을 수행합니다. 즉, 모든 id에 대해 서로 간의 모든 정수 기반 값을 찾습니다(예: 1 ~ 5는 1, 2, 3, 4, 5의 히스토그램이 됨).
  2. Elasticsearch 2.0 이상에만 존재하는 bucket selector를 사용하여 데이터가 실제로 포함된 모든 히스토그램 버킷을 제거합니다.

존재하는 항목을 제거함으로써 존재하지 않는 항목만 남게 되며, 그 결과는 다음과 같습니다:

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 5,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "find_missing_ids": {
      "buckets": [
        {
          "key": 4,
          "doc_count": 0
        }
      ]
    }
  }
} 

5개 도큐먼트가 있는 작은 인덱스로부터 hits.total 덕분에 id 가 4인 도큐먼트가 누락된 것을 알 수 있습니다.

이것이 다입니다. 도큐먼트의 고유한 키로부터 히스토그램을 생성할 수 있다면 누락된 데이터만 보여주는 히스토그램을 생성할 수 있습니다. 하지만 문자열로는 이렇게 할 수 없습니다. 즉, "my_id" 와 UUID(예: GUID) 등에는 어그리게이션을 통해 이 접근 방법을 이용할 수 없습니다! 이러한 유형의 점검을 위해 데이터 구성의 중요성이 여기서 다시 부각됩니다.

차이에 대한 고려

순차적 ID에 익숙하지 않은 경우에도, 시스템(예: SQL 데이터베이스)에서 ID 배치를 사전에 가져오는 것이 유리합니다. 이러한 배치를 사용하면 트랜잭션 실패라든지 또는 레코드가 이후에 명시적으로 삭제되는 등의 간단한 이유로 인해 일부 값을 건너뛴 것을 확인할 수 있습니다.

그러한 경우에는 해당 값이 히스토그램에 따라 누락된 도큐먼트로 나타난다는 것을 알고 있어야 합니다. 이를 방지하려면 클라이언트가 Elasticsearch에 해당 id를 명시적으로 제거하는 2차 bucket selector를 추가하여 해당 값을 명시적으로 무시하도록 Elasticsearch에 지시하거나 클라이언트 측에서 이를 무시할 수 있습니다.

전체 도큐먼트 확인

전체 도큐먼트 확인은 간단한 존재 여부 점검과는 다른 난제입니다. 이 경우에는 포함된 데이터가 예상된 데이터인지 확인하기 위해 전체 도큐먼트를 확인해야 합니다. Elasticsearch 관점에서 이는 실제로 더 쉬운 일이지만 사용자 입장에서는 작업이 더 까다로워집니다.

데이터 스크롤하기

이 점검을 수행하는 가장 간단한 방법은 데이터를 스크롤하는 것입니다. 다행히도 이를 위해 모든 데이터를 HTML에 집어넣고 마우스 휠을 굴리며 모든 데이터를 확인한다는 뜻이 아닙니다. 이것은 _scroll API를 사용해서 전체 데이터를 반복 확인 하는 것입니다(아래 사용된 _doc 이 여기에 설명됨). 다른 모든 검색 API와 마찬가지로 이 방법은 위에서 설명한 근 실시간(near real time) 환경에서도 작동합니다.

GET /my_index/my_type/_search?scroll=1m
{
  "sort": [
    "_doc"
  ]
}

스크롤 시간은 클라이언트 소프트웨어가 다음 배치를 요청하기 전에 해당 배치를 처리하는 데 필요한 시간입니다. 이후 응답까지 루프를 하고 위의 연결된 도큐먼트를 읽기에 충분한 시간이어야 합니다!

데이터를 스크롤함으로써 각 도큐먼트를 확인하고 모든 데이터가 요건에 부합되는지 확인할 수 있습니다.

버전 관리 수행

Elasticsearch는 또 다른 형태의 버저닝인 낙관적 병행 수행 제어(optimistic concurrency control)를 지원합니다.

고유한 버전 번호를 제공하여 버저닝을 완벽하게 관리할 수 있습니다. 이렇게 하면 버전 번호를 확인하여 전체 도큐먼트 확인을 피할 수 있습니다. 검색 응답에서 버전 번호를 활성화하기 위해 버전 플래그를 지정해야 합니다:

GET /my_index/my_type/_search
{
  "version": true
}

검증 회피

데이터가 신뢰할 수 있는 사용자에 의해 제공되었고 통합 실패가 제대로 처리되었음(예: 문서 15123에 대한 인덱스를 작성했어야 할 때 Elasticsearch가 다운되었으면 무엇인가를 여전히 추가해야 함)을 신뢰할 수 있는 경우 검증을 피할 수 있습니다.  이러한 경우 확인이 중복 작업이 될 수 있습니다.

5.0의 X-Pack Security 그리고 Shield 에서는 보안 측면에서 신뢰할 수 있는 사용자에게 액세스 권한을 부여하고 신뢰할 수 없는 사용자를 차단할 수 있지만, 통합을 수행하는 주체에 완전히 달려 있으므로 통합 실패를 제대로 처리하는 것은 사용자의 몫입니다.

맺음말

저의 긴 블로그 게시물을 읽느라 고생 많이 하셨습니다!

데이터 확인 그리고 데이터 구성 방법에 따라 데이터 확인 방법이 얼마나 간소화되는지에 대해 이해하는 데 도움이 되었기를 바랍니다. 이러한 유형의 접근 방법을 고려하면서 집계나 X-Pack Security와 같은 풍부한 기능들과 함께 쿼리 또는 데이터를 다르게 생각하여 다른 문제를 해결할 수 있게 됩니다.

당사는 흥미로운 문제를 해결할 수 있는 창의적인 방법을 지속적으로 찾고 있으며, 이번 건도 예외가 아닙니다. 언제나 마찬가지로 사용자들이 저희 포럼에서 이러한 문제를 논의하고, 발생한 어떤 문제에 대해서 GitHub에 issue를 제기할 것을 권장합니다. 또한 Twitter (제 계정: @pickypg) 와 IRC를 통해서도 도움을 받으실 수 있습니다!