AWS OpenSearch로 document를 쌓다보면 mapper_parsing_exception 에러를 마주할 수 있다. mapper_parsing_exception 에러는 지정한 index에 document를 쌓을 때, 지정된 document의 data type에 새로 넣으려는 data의 type이 parsing되지 않아 발생하는 에러이다. 이 글에서는mapper_parsing_exception 에 어떻게 대처했는지 STAR 기법에 적용하여 작성하고자 한다.
Situation (주어진 상황)
현재 개발서버에서 API 통신을 주고 받는 로그를 전부 AWS OpenSearch로 쌓고 있다. 하나의 통신이 끝난 뒤 가장 마지막 과정에서 OpenSearch로 로그를 전송하는 형태이다. 그런데 특정 API를 호출하고 OpenSearch로 로그를 쌓을 때 OpenSearch SDK(@aws-sdk/client-opensearch)에서 mapper_parsing_exception 예외가 발생하며 로그 전송에 실패했다. 더 나아가 개발서버가 다운되는 현상이 발생되었다. 운영 서버가 아니여서 다행이었지만, 원활한 개발 환경 조성을 위해 빠르게 해결해야하는 상황이었다.
Task (해결해야하는 과제)
해결해야하는 과제는 총 두가지였다.
- OpenSearch 예외 발생으로 인해 개발 서버가 다운되지 않도록 만든다.
- mapper_parsing_exception 예외처리를 해결하여 정상적으로 로그를 전송할 수 있도록한다.
Action (행동)
OpenSearch로 인한 예외 발생으로 인한 서버 다운 방지
이 방법은 아래와 같이 await으로 간단하게 해결할 수 있었다. await은 Promise 객체를 반환하는데, 비동기를 동기적으로 처리할 수 있게 한다. 또한 모든 동기/비동기 에러를 try/catch를 통해 처리할 수 있어 예외 처리 파악이 더욱 손쉽다는 장점이 있다.
try {
await client.send(command);
} catch (error) {
console.log(error)
}
또한 더욱 빠른 에러 처리를 위해 sdk에서 발생한 에러를 후속처리할 수 있도록 Slack으로 알람을 보내도록 설정하였다. 이로서 개발 서버가 다운되는 현상은 해결할 수 있게 되었다.
mapper_parsing_exception 예외처리 해결
mapper_parsing_exception 예외처리를 해결하려면, sdk에서 어떤 field에서 parsing에 실패했는지 먼저 파악해야한다. 아래에는 내가 겪었던 에러 발생 구문이다.
{"error":{"root_cause":[{"type":"mapper_parsing_exception","reason":"failed to parse field [data] of type [long] in document with id 'something'. Preview of field's value: 'something'"}],
"type":"mapper_parsing_exception","reason":"failed to parse field [data] of type [long] in document with id 'something'. Preview of field's value: 'something'",
"caused_by":{"type":"illegal_argument_exception","reason":"For input string: \\"something\\""}},"status":400}
위 구문에 따르면 data field에 long type으로 parsing되어야하는데, something이라는 text를 넣으려다보니 parsing 에러가 발생한 것을 알 수 있다. OpenSearch에서는 기존 SQL type과 살짝 다른 data type 명을 사용하고 있는데, 아래 표를 통해 OpenSearch에서 사용하고 있는 data type을 참고할 수 있다.
OpenSearch Data Type
OpenSearch SQL Type | OpenSearch Type | SQL Type |
boolean | boolean | BOOLEAN |
byte | byte | TINYINT |
short | byte | SMALLINT |
integer | integer | INTEGER |
long | long | BIGINT |
float | float | REAL |
half_float | float | FLOAT |
scaled_float | float | DOUBLE |
double | double | DOUBLE |
keyword | string | VARCHAR |
text | text | VARCHAR |
date | timestamp | TIMESTAMP |
date_nanos | timestamp | TIMESTAMP |
ip | ip | VARCHAR |
date | timestamp | TIMESTAMP |
binary | binary | VARBINARY |
object | struct | STRUCT |
nested | array | STRUCT |
그렇다면, data field의 data type을 text로 바꿔서 document를 넣으면 해결될 일이었다. 즉, index mapping을 update하면 되는 것이다. 하지만 결론적으로 OpenSearch에서는 한 번 정해진 index mapping의 data type을 바꿀 수 없다.
Note that you cannot use this operation to update mappings that already map to existing data in the index.
그렇다면 해결방법은? mapping을 update할 수 없지만, 아래 인용에 따르면 우회적으로 update할 수 있는 것 처럼 만들 수 있는 방법이 있었다.
You must first create a new index with your desired mappings, and then use the reindex API operation to map all the documents from your old index to the new index. If you don’t want any downtime while reindexing your indices, you can use aliases.
위 문서에 따르면 새 index를 만드는데, 이때 mapping을 새로 정의해서 만든다. 그리고 reindex API를 통해 기존 인덱스의 document들을 새 index로 옮기는 작업을 하면된다고 한다. 이때 약간의 다운 타입이 발생하는데, 이를 방지하기 위해 aliases를 사용하면 된다고한다. 개발 서버이기 때문에 약간의 downtime은 감수할 수 있기 때문에 여기서는 aliases를 사용하지 않는다. 결국 업데이트된 mapping을 사용하기 위해 index를 이전하는 작업(reindex)이 필요한 것이다.
하지만 나는 기존 index를 사용을 해야하기 때문에 위 문서를 따라 해결하기 위해 총 3개의 index가 필요했다.
- A → 기존 index : 현재 이슈를 겪고 있는 index
- B → 새 index : 임시적으로 mapping이 update된 index
- C → 기존 index (A)와 이름이 같은 새 index : 다시 기존 처럼 document를 쌓기 위해 필요
위 A,B,C index를 활용한 구체적인 해결 방법은 아래와 같다. 나는 간편하게 OpenSearch API를 사용하기 위해 OpenSearch Dash Board의 console을 사용했다.
1. A index의 정의된 mapping을 조회하여 예외처리가 난 부분의 mapping을 수정 후 복사한다.
GET old/_mapping
2. 새 인덱스를 만드는데 (B index), 이 때 1번에서 정의한 mapping 세팅을 붙여넣는다. 여기서 B index의 명을 test로 지었다.
PUT /test
{ "mappings":{ "properties":{ "data":{ "status":{ "type":"text" } } } } }
3. A index에 있는 document들을 B index로 reindex한다.
POST _reindex
{ "source":{ "index":"old" }, "dest":{ "index":"test" } }
4. A index를 삭제한다. 삭제하는 이유는 업데이트 되지 않은 mapping을 삭제하고 새 index를 만들어 다시 이전을 해야하기 때문이다.
DELETE /old
5. 새 인덱스를 만든다. (C index) 이 때 주의할 점은 방금 삭제한 A index와 이름이 같아야하고, update된 mapping(B index)를 사용한다.
PUT /old
{ "mappings":{ "properties":{ "data":{ "status":{ "type":"text" } } } } }
6. 다시 새로만든 index로 reindex한다.
POST _reindex
{ "source":{ "index":"test" }, "dest":{ "index":"old" } }
7. 더는 사용할 필요 없는 B index를 삭제한다.
DELETE /test
위와 같이 해결하면, 더 이상 특정 API에서 발생하던 mapper_parsing_exception 은 해결되었다.
정리
OpenSearch에서 기존 index mapping을 할 수 있는 방법은 없다. update된 mapping을 가진 새 index로 reindex하는 방법 밖에 없다.
Result (결과)
위와 같은 task로 과제 두 가지를 해결했다.
- OpenSearch 예외 발생으로 인해 개발 서버가 다운되지 않도록 만든다. → 해결
- mapper_parsing_exception 예외처리를 해결하여 정상적으로 로그를 전송할 수 있도록한다. → 해결
Quantitative results (정량적 성과)
- API 1건에 대한 OpenSearch 전송 에러를 해결했다.
- 해결과정에서 약 5만 건 가량의 로그를 안전하게 reindex하여 이전하였다.
Lesson Learned (배운 점)
- API 설계 시 로그 전송을 염두해두고 data type이 파편화되지 않도록 주의해야겠다.
- 항상 예외처리를 염두해두고 설계하여 서버가 다운되지 않도록해야겠다.
'개발' 카테고리의 다른 글
Airflow 로컬에서 세팅하기 (0) | 2022.10.15 |
---|---|
EC2 인스턴스에 AWS Cloud watch Agent 설치하기 (0) | 2022.10.14 |
AWS OpenSearch 시작하기 (feat.node.js) (0) | 2022.10.11 |
Airflow에 BigQuery 연동하기 (0) | 2022.10.07 |
Django에서 AWS SES를 활용한 이메일 보내기 (0) | 2022.10.06 |