top bar

글 목록

2015년 4월 21일 화요일

[Elasticsearch] VersionConflictEngineException 에 대하여

1. 발단



현재 필자가 담당하고 있는 시스템의 주요 이슈중 하나가, 데이터의 누락이었다.
예를들면 아래와 같다.


물론 위의 화면은, Elasticsearch에 색인된 데이터를 가져와 뿌려준다.
다만, 저런 현상이 보인다는건, 해당 document의 필드값이 이빨이 빠져있다는 거다.

이리저리 뒤져가며 삽질하다가, 아래와 같은 로그를 발견했다.


VersionConflictEngineException... 이게 문제의 원인인 듯 하다.

2. 분석 & 해결과정



elasticsearch 문서에는 이러한 Version Conflict를 아래의 url에서 다루고 있다.


elasticsearch에 색인되어있는 각각의 document는 '_version'이라는 내장 필드를 가지고있다. document가 처음 색인되면 version은 1이다. 이후 update, delete와 같은 작업이 해당 document에 이루어지면, 버전이 1씩 올라간다.

하지만, 이것이 무엇이 문제일까? 이러한 version conflict를 재현해보았다.

1) 먼저 아래와 같은 document를 색인한다.
$ curl -XGET 'localhost:9200/test_index/test_type/1?pretty=true'
{
  "_index" : "test_index",
  "_type" : "test_type",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source":{"name":"john","address":"boston"}
}
테스트용 인덱스와 타입을 만들었고, id를 1로 하여 위와같은 doc하나를 색인하였다.
필드는 'name'과 'address'이며 최초 값은 'john'과 'boston'이다.

2) 두개의 shell script를 준비한다.
## name_update.sh 파일 내용 ##
#!bash 
## name을 업데이트하는 shell script 
curl -XPUT 'localhost:9200/_bulk?pretty=true' --data-binary @requests1.json;

## request1.json 파일 내용
{"update":{"_id":"1","_type":"test_type","_index":"test_index"}}\n
{"doc":{"name":"tom"}}

## address_update.sh 파일 내용 ##
#!bash 
## address를 업데이트하는 shell script 
curl -XPUT 'localhost:9200/_bulk?pretty=true' --data-binary @requests2.json;

## request2.json 파일 내용
{"update":{"_id":"1","_type":"test_type","_index":"test_index"}}\n
{"doc":{"address":"newyork"}}
 _bulk를 이용해 업데이트 요청을 처리하는 shell script이다. 두개의 script파일을 만들었는데, 하나는 name필드를 'tom'으로 업데이트하는 스크립트이고, 또 하나는 address필드를 'newyork'으로 업데이트하는 스크립트이다.

3) Crontab을 사용하여 두개의 스크립트를 '동시에' 실행시킨다.

결과는 어떻게 될까?

name필드는 'john'에서 'tom'으로, address필드는 'boston'에서 'newyork'으로 업데이트
되어야 하는게 우리가 기대하는 결과일 것이다.

하지만, 결과는 아래와 같았다. 해당 doc을 조회해 보자.
$ curl -XGET 'localhost:9200/test_index/test_type/1?pretty=true'
{
  "_index" : "test_index",
  "_type" : "test_type",
  "_id" : "1",
  "_version" : 2,
  "found" : true,
  "_source":{"name":"john","address":"newyork"} <-- name이 최초 버전 그대로 'john'이다!
}
 address필드는 예상되로 'newyork' 으로 업데이트되었지만, name필드는 업데이트되지
않았다. 어째서 이런결과가 발생한 것일까.

name을 업데이트하는 스크립트 수행결과에 아래와 같은실행 로그가 찍혀있었다.

{
  "took" : 2,
  "errors" : true,
  "items" : [ {
    "update" : {
      "_index" : "index1",
      "_type" : "type1",
      "_id" : "5",
      "status" : 409,
      "error" : "VersionConflictEngineException[[index1][1] [type1][5]: 
version conflict, current [2], provided [1]]"
    }
  } ]
}

오오잇! VersionConflictEngineException이 보인다. 실무에서 똑같이 보았던 Exception이다.

원인은 이렇다.

먼저 name필드와 address필드를 업데이트하는 스크립트가 '동시에' 실행되었다는것, 그리고 앞서 말한것 같이 document에는 'version'이라는 개념이 있다는것을 주목해 보자.

그리고 두개의 스크립트를 일련의 '프로세스'로 간주하자. 두개의 프로세스는 각각 version 1의 대상 document에 대해 update를 수행한다. 이때, 밀리세컨드이든, 나노세컨드이든, 간만의 '시간차'로 어느 한 프로세스가 '먼저' update를 수행하게 될 것이다. 그렇게되면 해당 document의 version은 '2' 가 된다.

곧바로 수행되는 다른 프로세스도 version '1'의 document를 업데이트하려고 할 것이다. 하지만? 이미 해당 document의 버전은 version '2'가 되어있다. 두번째 프로세스가 업데이트를 수행하려고 했던 version 1의 document는 'Out of date' 가 되어버린 것이다.

그렇게되면 두번째 프로세스 예를들어 위의 경우처럼 address를 먼저 업데이트하고 두번째로 name을 업데이트 한다고 하면 name은 업데이트를 수행하지 못한다.

이것이 문제이다. 최초에 실무에서 겪었던 데이터 누락의 경우를 보자


위의 경우처럼 데이터가 누락되는 원인은 위의 재현된 사항과 같았다. A행(document)의 경우를 본다면, 최초 D열과 E열은 필드 값이 비어있고, 각각 D열을 업데이트하는 스레드, E열을 업데이트하는 스레드가 돌고있다

각각의 스레드가 A행의 D와 E열을 업데이트하려고(값을채우려고) 시도한다. 위에 재현했던대로 Version Conflict가 발생한다. 그렇게되면 둘중 하나의 값은 유실 되어 버린다

이게 문제다.

3. 해결




이를 해결하는것은 매우매우매우 간단하다. 바로 bulk를 이용하여 update쿼리를 할때, 'retry_on_conflict'라는 옵션을 사용하는것이다. 아래는 자바코드에서 Elasticsearch에 bulk 요청을 하는 예제이다.

// 3. 검색엔진에 인덱싱 요청
indexMessage = new StringBuffer();
indexMessage.append("{\"update\":{\"_index\":\"test_index\"," + 
"\"_type\":\"test_type\",\"_id\":\"")
// version conflict발생시 최대 5번까지 retry 한다.
.append(doNotification.getMsgId()).append("\", \"_retry_on_conflict\":5}}\n");

'retry_on_conflict' 옵션은, 말그대로 Version Conflict가 발생할 때마다 '재시도' 하는 횟수를 말한다. 이렇게되면 충돌이 발생하더라도 다시 업데이트를 시도하여 최종적으로는 업데이트가 정상적으로 이루어 지게 된다
하나의 이슈도 참 심오하다. 그래서 결코 쉽지않은 Elasticsearch이다.

댓글 1개: