엘라스틱서치:: update_by_query
🙉

엘라스틱서치:: update_by_query

Created
Jun 5, 2024 01:44 AM
Last edited time
Last updated June 7, 2024
Tags
ElasticSearch
Language
URL

Intro::

엘라스틱 서치에서의 update_by_query에 대해 알아봅시다.
 

update_by_query

먼저 검색 쿼리를 통해 주어진 조건을 만족하는 문서를 찾은 뒤 그 문서를 대상으로 업데이트나 삭제 작업을 실시하는 API이다. 동작 특성상 서비스에 일상적으로 사용하기보다는 관리적인 목적으로 호출할 때가 많다. script를 통한 업데이트만을 지원합니다.
POST bulk_test/_update_by_query { "script": { "source": "ctx._source.f1 = ctx._source.f1 + '-' + ctx._id", "lang": "painless" }, "query": { "exists": { "field": "f1" } } }
update_by_query API는 검색 조건에 맞는 문서를 찾아 일종의 스냅샷을 찍고, 문서마다 지정된 스크립트에 맞게 업데이트를 실시합니다. 여러 문서의 업데이트가 순차적으로 진행되는 도중 다른 작업으로 인해 문서에 변경이 생길 수 있습니다. update by query API는 스냅샷을 찍어 뒀던 문서에서 변화가 생긴 문서를 발견하면 이를 업데이트하지 않습니다. 버전 충돌 문제가 발생하면 conflicts 매개변수에 따라 동작방식을 지정할 수 있습니다.
  • abort: 충돌 발견 시 작업 중단한다.(기본값)
  • proceed: 다음 작업으로 넘어간다.
update by query를 이용한 업데이트 작업은 도중에 충돌이 났거나 다른 문제로 인해 중간에 작업이 중단되더라도 그때까지 업데이트된 내용이 롤백되거나 하지 않습니다. 즉, 중단 전까지 작업된 문서는 업데이트된 상태로 남습니다.

스로틀링

대량 작업을 수행하면 운영 중인 기존 서비스에도 영향을 줄 수 있습니다. 그러한 상황을 피하기 위해 update_by_query에는 스로틀링 기능이 있습니다. 적절한 스로틀링 적용을 통해 작업의 속도를 조정하고 클러스터 부하와 서비스 영향을 최소화할 수 있습니다.
POST bulk_test/_update_by_query?scroll_size=1000&scroll=1m&requests_per_second=500 { "script": { "source": "ctx._source.f1 = ctx._source.f1 + '-' + ctx._id", "lang": "painless" }, "query": { "exists": { "field": "f1" } } }
위의 쿼리에 대한 설명을 하자면
  • scroll_size=1000
    • 한번의 검색 수행에 1000개의 문서를 가져온다.
  • scroll=1m
    • 검색 조건을 만족한 모든 문서를 대상으로 검색이 처음 수행됐을 당시 상태를 검색 문맥에 보존하는데, 이때 1분동안 검색 문맥을 유지시킨다는 의미이다.
  • requests_per_second=500
    • 평균적으로 초당 몇개까지의 작업을 수행할 것인지를 지정한다.
예시로 1000개의 문서를 처리하는데 0.5초가 걸렸다면 scroll_size/requests_per_second = 2초 에서 0.5를 뺀 1.5 초 동안 대기하여 평균 초당 문서 처리량이 500이 되게된다.

비동기적 요청과 tasks API

엘라스틱서치에서 update by query 요청 시에는 wait_for_completion 매개변수를 false로 지정하면 비동기적 처리를 할 수 있습니다. 해당 요청을 받으면 엘라스틱서치는 작업을 task로 등록한 뒤 즉시 task의 id가 포함된 응답을 반환합니다. 이 값은 노드의 id와 해당 노드 내 task의 id를 콜론(:)으로 연결한 형태입니다. 클라이언트는 이 값을 가지고 tasks API를 호출함으로써 작업의 진행 경과를 확인하거나 작업 진행을 취소할 수 있습니다.

task 작업 등록과 상태 조회

모든 수행 중인 update by querywait_for_completion 값에 상관없이 모두 task의 형태로 동작하며 tasks 조회 API를 통해 작업 진행을 확인할 수 있습니다. wait_for_completionfalse로 지정하면 이 진행 상황이 .tasks라는 내부 인덱스에 문서로 저장됩니다. 또한 작업이 종료된 이후에도 작업 결과를 조회할 수 있습니다.
POST bulk_test/_update_by_query?wait_for_completion=false { "script": { "source": "ctx._source.f1 = ctx._source.f1 + '-' + ctx._id", "lang": "painless" }, "query": { "exists": { "field": "f1" } } } // 응답이 약간 다르지만 어떤 API를 호출해도 무관하다. GET .tasks/_doc/[2vN7VCdRQAuH911MPkzaPg:2495969] GET _tasks/2vN7VCdRQAuH911MPkzaPg:2495969// 요즘 사용하는 추세 // 작업 진행 중 문제가 발생했다면 취소 POST _tasks/2vN7VCdRQAuH911MPkzaPg:2495969/_cancel
 

스로틀링 동적 변경

서비스를 운영하는 중에 대량 작업을 수행할 때는 예상치 못한 문제를 만날 수 있습니다. 예시로 일괄 업데이트에 적절한 스로틀링을 적용해 작업을 진행 중이었지만 외부적인 이슈로 인해 나중에 서비스 트래픽이 급증하는 문제가 발생할 수 있습니다. 이러한 경우 클러스터 전체에 심각한 문제가 생기는 일을 막기 위해 일단 일괄 업데이트 작업을 취소할 수 있습니다.
서비스 중요도에 따라 다르지만 이전에 긴 시간을 수행하던 작업이 있더라도 일단 취소하는 것이 안정적입니다. 이유인즉슨 앞으로의 작업이 안전하게 끝날것이라는 보장이 없고, 기존까지 진행한 작업의 경우 롤백이 되지 않고 저장되기 때문입니다. 때문에 트래픽 급증 상황을 해결하고 난 뒤 검색 조건을 수정해 아직 변경되지 않은 문서만을 대상으로 업데이트를 수행하는 편이 좋습니다.
한편 작업 특성에 따라 아직 변경되지 않은 문서를 걸러내기가 매우 어려울 수도 있습니다. 이러한 경우 스로틀링을 동적으로 변경하는 방법이 있습니다.
POST _update_by_query/2vN7VCdRQAuH911MPkzaPg:2495969/_rethrottle?requests_per_second=100
 

task 결과 삭제

wait_for_completion=false를 통해 .task 인덱스에 등록된 작업 결과는 엘라스틱서치에 계속 남습니다. 작업의 상황을 충분히 확인했다면 다음과 같이 .task 인덱스에서 문서를 삭제하면 좋습니다.
// 단일 삭제 DELETE .tasks/_doc/[task id] // delete by query // 완료된 task 전체 삭제 예제 POST .tasks/_delete_by_query { "query": { "term": { "completed": true } } }
 

슬라이싱

관리적 목적의 대량 업데이트를 수행하는 경우, 스토틀링을 적용해 부하를 줄이는 선택도 있지만 반대로 성능을 최대로 끌어내 빠른 시간 안에 끝내고자 하는 선택도 있습니다. 정기점검을 걸고 서비스 요청을 차단한 다음 작업을 끝내야 하는 상황이 대표적입니다. 이러한 경우 슬라이싱을 사용할 수 있습니다.
 
slices 매개변수를 지정하면 검색과 업데이트를 지정한 개수로 쪼개 병렬적으로 수행합니다.
POST bulk_test/_update_by_query?slices=auto { "script": { "source": "ctx._source.f1 = ctx._source.f1 + '-' + ctx._id", "lang": "painless" }, "query": { "exists": { "field": "f1" } } }
slices의 기본값은 1로, 작업을 병렬로 쪼개지 않습니다. slices=auto의 경우 엘라스틱서치가 적절한 개수를 지정해서 작업을 병렬 수행하게 됩니다. 보통은 지정한 인덱스의 주 샤드 수가 슬라이스의 수가 됩니다.
슬라이싱은 기본적으로 샤드를 기준으로 작업을 쪼개는 것이기 때문에 각 요청 슬라이스가 동일한 작업량을 배분받는 것은 아니라는 사실을 알고 있어야합니다. 또한 request_per_second 옵션은 각 슬라이스에 쪼개져서 적용됩니다.
예시로 request_per_second이 1000일때 슬라이스가 5개라면 각 슬라이스는request_per_second 200씩을 분배받습니다.
 

References::

엘라스틱서치 바이블 - 여동현 지음

Loading Comments...