너무 많은 필드 수! Elasticsearch에서 매핑 폭발을 방지하는 3가지 방법

blog-thumb-website-search.png

시스템이 로그, 메트릭, 추적이라는 세 가지 기능을 갖추었을 때 우리는 “통합 가시성”이 있다고 합니다. 메트릭과 추적은 예측 가능한 구조를 가지고 있지만, 로그(특히 애플리케이션 로그)는 비정형 데이터로서 수집 및 구문 분석을 거쳐야만 실제로 유용하게 쓸 수 있는 경우가 대부분입니다. 따라서 통합 가시성을 달성하는 데 있어 로그 관리가 가장 어려운 부분일 것입니다. 

이 게시물에서는 개발자가 Elasticsearch를 통해 로그를 관리하는 데 사용할 수 있는 세 가지 효과적인 전략에 대해 살펴보겠습니다. 더 자세한 내용은 아래의 동영상을 확인하세요.

Video thumbnail

[관련 게시물: Elastic을 활용하여 클라우드에서 데이터 관리 및 Observability 개선]

데이터에 Elasticsearch 활용

클러스터에서 수신하는 로그 유형을 제어할 수 없는 경우가 있습니다. 고객의 로그를 저장하는 데 특정 예산이 할당되어 있고 저장 공간을 통제해야 하는 로그 분석 제공업체가 있다고 가정해 보겠습니다. (Elastic은 Consulting에서 많은 유사한 사례를 다룹니다.) 

많은 고객이 검색에 필요한 "만일의 경우"에 대비해 필드를 색인합니다. 이러한 경우에는 다음 기술이 비용을 절감하고 클러스터 성능을 정말로 중요한 사항에 집중하는 데 유용한 것으로 알려져 있습니다.

먼저 문제를 요약해 보겠습니다. 다음 JSON 문서에 message, transaction.user, transaction.amount라는 3개의 필드가 있습니다.

{
 "message": "2023-06-01T01:02:03.000Z|TT|Bob|3.14|hello",
 "transaction": {
   "user": "bob",
   "amount": 3.14
 }
}

이러한 문서를 저장할 인덱스에 대한 매핑은 다음과 같은 형태일 수 있습니다.

PUT dynamic-mapping-test
{
 "mappings": {
   "properties": {
     "message": {
       "type": "text"
     },
     "transaction": {
       "properties": {
         "user": {
           "type": "keyword"
         },
         "amount": {
           "type": "long"
         }
       }
     }
   }
 }
}

그러나 Elasticsearch를 사용하면 매핑을 미리 지정할 필요 없이 새 필드를 색인할 수 있습니다. 이러한 점이 Elasticsearch가 제공하는 사용 편의성 중의 하나입니다. 즉, 새 데이터를 손쉽게 추가할 수 있습니다. 따라서 다음과 같이 원래 매핑에서 벗어난 항목을 색인해도 무방합니다.

POST dynamic-mapping-test/_doc
{
 "message": "hello",
 "transaction": {
   "user": "hey",
   "amount": 3.14,
   "field3": "hey there, new field with arbitrary data"
 }
}

GET dynamic-mapping-test/_mapping은 인덱스에 대한 새로운 매핑 결과를 보여줍니다. 이제 transaction.field3textkeyword로 존재하며 실제로는 두 개의 새 필드입니다.

{
 "dynamic-mapping-test" : {
   "mappings" : {
     "properties" : {
       "transaction" : {
         "properties" : {
           "user" : {
             "type" : "keyword"
           },
           "amount" : {
             "type" : "long"
           },
           "field3" : {
             "type" : "text",
             "fields" : {
               "keyword" : {
                 "type" : "keyword",
                 "ignore_above" : 256
               }
             }
           }
         }
       },
       "message" : {
         "type" : "text"
       }
     }
   }
 }
}

하지만 이제 이것이 문제의 일부입니다. Elasticsearch로 전송되는 내용을 제어할 수 없게 되면 틀림없이 매핑 폭발이라는 문제에 직면하게 됩니다. 다음과 같이 동일한 두 가지 유형인 textkeyword라는 하위 필드 및 하위 하위 필드를 생성할 수 있습니다.

POST dynamic-mapping-test/_doc
{
 "message": "hello",
 "transaction": {
   "user": "hey",
   "amount": 3.14,
   "field3": "hey there, new field",
   "field4": {
     "sub_user": "a sub field",
     "sub_amount": "another sub field",
     "sub_field3": "yet another subfield",
     "sub_field4": "yet another subfield",
     "sub_field5": "yet another subfield",
     "sub_field6": "yet another subfield",
     "sub_field7": "yet another subfield",
     "sub_field8": "yet another subfield",
     "sub_field9": "yet another subfield"
   }
 }
}

검색 및 집계가 가능하도록 데이터 구조가 생성되므로 이러한 필드를 저장하는 데 RAM 및 디스크 공간을 낭비하게 됩니다. 검색에 사용해야 하는 "만일의 경우"에 대비해 마련된 이러한 필드는 한 번도 사용되지 않을 수 있습니다. 

컨설팅에서 인덱스를 최적화해달라는 요청을 받았을 때 취하는 첫 번째 단계 중 하나는 인덱스에 있는 모든 필드의 사용량을 검사하여 어떤 필드가 실제로 검색되고 어떤 필드가 리소스를 낭비하고 있는지 확인하는 것입니다.

전략 #1: 엄격한 기준

Elasticsearch에 저장하는 로그의 구조와 저장 방법을 완벽하게 제어하고 싶다면, 명확한 매핑 정의를 설정하여 원하는 조건에서 벗어나는 로그는 어떤 것도 저장되지 않도록 할 수 있습니다. 

최상위 수준 또는 일부 하위 필드에서 dynamic: strict를 사용하면 mappings 정의에 맞지 않는 문서를 거부하여 발신자가 아래와 같이 사전 정의된 매핑을 준수하도록 강제합니다.

PUT dynamic-mapping-test
{
 "mappings": {
   "dynamic": "strict",
   "properties": {
     "message": {
       "type": "text"
     },
     "transaction": {
       "properties": {
         "user": {
           "type": "keyword"
         },
         "amount": {
           "type": "long"
         }
       }
     }
   }
 }
}

그런 다음 추가 필드로 문서를 색인하면...

POST dynamic-mapping-test/_doc
{
 "message": "hello",
 "transaction": {
   "user": "hey",
   "amount": 3.14,
   "field3": "hey there, new field"
   }
 }
}

...다음과 같은 응답을 받게 됩니다.

{
 "error" : {
   "root_cause" : [
     {
       "type" : "strict_dynamic_mapping_exception",
       "reason" : "mapping set to strict, dynamic introduction of [field3] within [transaction] is not allowed"
     }
   ],
   "type" : "strict_dynamic_mapping_exception",
   "reason" : "mapping set to strict, dynamic introduction of [field3] within [transaction] is not allowed"
 },
 "status" : 400
}

매핑에 있는 내용만 저장하려는 것이 확실하다면 이 전략은 발신자가 사전 정의된 매핑을 준수하도록 강제합니다.

전략 #2: 약간 엄격한 기준

"dynamic": "false"를 사용하면 문서가 예상과 정확하게 일치하지 않더라도 조금 더 유연하게 문서를 통과시킬 수 있습니다.

PUT dynamic-mapping-disabled
{
 "mappings": {
   "dynamic": "false",
   "properties": {
     "message": {
       "type": "text"
     },
     "transaction": {
       "properties": {
         "user": {
           "type": "keyword"
         },
         "amount": {
           "type": "long"
         }
       }
     }
   }
 }
}

이 전략을 사용할 때는 수신되는 모든 문서를 수락하지만, 매핑에 지정된 필드만 색인하므로 추가 필드를 검색할 수 없습니다. 즉, 새로운 필드에 RAM을 낭비하지 않고 디스크 공간만 낭비하고 있습니다. 이 필드는 여전히 검색의 hits에 표시될 수 있으며 여기에는 top_hits 집계가 포함됩니다. 그러나 콘텐츠를 저장할 데이터 구조가 생성되지 않기 때문에 검색하거나 집계할 수 없습니다.

전부와 아무 것도 아닌 것을 꼭 양자택일할 필요는 없습니다. 루트를 strict로 지정하고 하위 필드는 새 필드를 색인하지 않고 이를 수락하도록 할 수 있습니다. 내부 객체에 대한 dynamic 설정(Setting dynamic on inner objects) 설명서에서 이 부분을 자세히 다루고 있습니다.

PUT dynamic-mapping-disabled
{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "message": {
        "type": "text"
      },
      "transaction": {
        "dynamic": "false",
        "properties": {
          "user": {
            "type": "keyword"
          },
          "amount": {
            "type": "long"
          }
        }
      }
    }
  }
}

전략 #3: Runtime 필드

Elasticsearch는 읽기 스키마와 쓰기 스키마를 모두 지원하며, 각 스키마에 주의 사항이 있습니다. dynamic:runtime을 사용하는 경우 새 필드가 매핑에 Runtime 필드로 추가됩니다. 매핑에 지정된 필드를 색인하고 쿼리 시간에만 추가 필드를 검색/집계할 수 있도록 합니다. 다시 말해, 우리는 새로운 필드에 미리 RAM을 낭비하지 않지만, 데이터 구조가 런타임에 빌드되기 때문에 더 느린 쿼리 응답이라는 대가를 치르게 됩니다.

PUT dynamic-mapping-runtime
{
 "mappings": {
   "dynamic": "runtime",
   "properties": {
     "message": {
       "type": "text"
     },
     "transaction": {
       "properties": {
         "user": {
           "type": "keyword"
         },
         "amount": {
           "type": "long"
         }
       }
     }
   }
 }
}

대용량 문서를 색인해 보겠습니다

POST dynamic-mapping-runtime/_doc
{
 "message": "hello",
 "transaction": {
   "user": "hey",
   "amount": 3.14,
   "field3": "hey there, new field",
   "field4": {
     "sub_user": "a sub field",
     "sub_amount": "another sub field",
     "sub_field3": "yet another subfield",
     "sub_field4": "yet another subfield",
     "sub_field5": "yet another subfield",
     "sub_field6": "yet another subfield",
     "sub_field7": "yet another subfield",
     "sub_field8": "yet another subfield",
     "sub_field9": "yet another subfield"
   }
 }
}

GET dynamic-mapping-runtime/_mapping은 대용량 문서를 색인할 때 매핑이 변경되었음을 보여줍니다.

{
 "dynamic-mapping-runtime" : {
   "mappings" : {
     "dynamic" : "runtime",
     "runtime" : {
       "transaction.field3" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_amount" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field3" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field4" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field5" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field6" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field7" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field8" : {
         "type" : "keyword"
       },
       "transaction.field4.sub_field9" : {
         "type" : "keyword"
       }
     },
     "properties" : {
       "transaction" : {
         "properties" : {
           "user" : {
             "type" : "keyword"
           },
           "amount" : {
             "type" : "long"
           }
         }
       },
       "message" : {
         "type" : "text"
       }
     }
   }
 }
}

이제 일반 키워드 필드처럼 새 필드를 검색할 수 있습니다. 데이터 유형은 첫 번째 문서를 색인할 때 추측하게 되지만 동적 템플릿을 사용하여 제어할 수도 있습니다.

GET dynamic-mapping-runtime/_search
{
 "query": {
   "wildcard": {
     "transaction.field4.sub_field6": "yet*"
   }
 }
}

결과:

{
…
 "hits" : {
   "total" : {
     "value" : 1,
     "relation" : "eq"
   },
   "hits" : [
     {
       "_source" : {
         "message" : "hello",
         "transaction" : {
           "user" : "hey",
           "amount" : 3.14,
           "field3" : "hey there, new field",
           "field4" : {
             "sub_user" : "a sub field",
             "sub_amount" : "another sub field",
             "sub_field3" : "yet another subfield",
             "sub_field4" : "yet another subfield",
             "sub_field5" : "yet another subfield",
             "sub_field6" : "yet another subfield",
             "sub_field7" : "yet another subfield",
             "sub_field8" : "yet another subfield",
             "sub_field9" : "yet another subfield"
           }
         }
       }
     }
   ]
 }
}

좋습니다! 수집하려는 문서 유형을 모를 때 이 전략이 얼마나 유용한지 쉽게 알 수 있습니다. 따라서 Runtime 필드를 사용하는 것은 성능과 매핑 복잡성 간에 적절한 균형을 유지하는 보수적인 접근 방식처럼 들립니다.

Kibana 및 Runtime 필드 사용에 대한 참고 사항

Kibana에서 검색할 때 검색창을 사용하여 필드를 지정하지 않는 경우(예: "message:hello" 대신 "hello"만 입력) 해당 검색은 모든 필드를 일치시키며 여기에는 선언된 모든 runtime 필드가 포함됩니다. 사용자가 이 동작을 원하지는 않을 것이므로 인덱스는 동적 설정인 index.query.default_field를 사용해야 합니다. 이를 매핑된 필드의 전부 또는 일부로 설정하고 runtime 필드를 명시적으로 쿼리할 수 있도록 합니다(예: "transaction.field3: hey").

최종적으로 업데이트된 매핑은 다음과 같습니다.

PUT dynamic-mapping-runtime
{
  "mappings": {
    "dynamic": "runtime",
    "properties": {
      "message": {
        "type": "text"
      },
      "transaction": {
        "properties": {
          "user": {
            "type": "keyword"
          },
          "amount": {
            "type": "long"
          }
        }
      }
    }
  },
  "settings": {
    "index": {
      "query": {
        "default_field": [
          "message",
          "transaction.user"
        ]
      }
    }
  }
}

최상의 전략 선택

각 전략에는 장단점이 있으므로 최상의 전략은 궁극적으로 특정 사용 사례에 따라 달라집니다. 아래 요약된 표는 요건에 따라 적절한 선택을 하는 데 도움이 될 것입니다.

전략

장점

단점

#1 - strict

저장된 문서는 매핑을 준수하도록 보장됨

매핑에 선언되지 않은 필드가 있는 문서는 거부됨

#2 - dynamic: false

저장된 문서는 필드 수에 제한이 없지만 매핑된 필드만 리소스를 사용

매핑되지 않은 필드는 검색 또는 집계에 사용할 수 없음

#3 - Runtime 필드

#2의 모든 장점

Runtime 필드는 다른 필드와 마찬가지로 Kibana에서 사용할 수 있음

Runtime 필드를 쿼리할 때 상대적으로 느린 검색 응답 시간

통합 가시성은 Elastic Stack이 진정으로 빛을 발하는 부분입니다. 영향을 받는 시스템을 추적하면서 수년간의 금융 트랜잭션을 안전하게 저장하든, 매일 수 테라바이트의 네트워크 메트릭을 수집하든 상관없이 Elastic 고객은 훨씬 적은 비용으로 10배 더 빠르게 Observability를 수행하고 있습니다. 

Elastic Observability를 시작하고 싶으신가요? 가장 좋은 방법은 클라우드에 있습니다. 지금 Elastic Cloud 무료 체험판을 시작해 보세요!