이슈 및 pr 링크
https://github.com/opensearch-project/OpenSearch/issues/16894
https://github.com/opensearch-project/OpenSearch/pull/17009
♦️이슈 설명
GetStats
클래스의 필드 중 하나가time
으로 명명되어야 하는데,getTime
이라는 잘못된 이름을 가지고 있다.- 2년 전에 코드 리팩토링 중 잘못된 find & replace 작업에서 부터 시작되었다.
time_in_millis
와 쌍을 이루는 "human-readable" 필드가getTime
이다.- 경우에서는
time
이라는 일관된 이름을 사용해고 있다.
♦️문제의 근본 원인
GetStats
클래스에서 "time" 필드의 직렬화(serialization) 및 직렬화 이름이getTime
으로 잘못 지정되어 있음- 이 문제는 리팩토링 시 발생한 실수로 인해 코드의 명명 규칙과 API 응답의 일관성이 깨진 것
♦️재현과정
- 폴더에 docker-compose.yml만들기
- docker compose up -d
version: '3'
services:
opensearch:
image: opensearchproject/opensearch:2.9.0
container_name: opensearch
environment:
- discovery.type=single-node
- bootstrap.memory_lock=true
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
- plugins.security.disabled=true # 에러나서 고친 부분 - 보안을 비활성화
ulimits:
memlock:
soft: -1
hard: -1
ports:
- "9200:9200"
- "9600:9600"
에러
[opensearch@b17d68ada4eb ~]$ curl -X GET "http://localhost:9200/" curl: (52) Empty reply from server
Caused by: io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record: 474554202f20485454502f312e310d0a486f73743a206c6f63616c686f73743a393230300d0a557365722d4167656e743a206375726c2f382e372e310d0a4163636570743a202a2f2a0d0a0d0a at io.nett
io.netty.handler.ssl.NotSslRecordException
에러는 OpenSearch 서버가 SSL/TLS 통신을 기대하고 있지만, 클라이언트(curl
)가 HTTP를 사용하여 요청을 보낼 때 발생→ SSL 설정과 HTTP 요청 간의 불일치
원인 분석
OpenSearch의 SSL/TLS 활성화
- OpenSearch는 보안을 위해 기본적으로 SSL/TLS를 활성화
- SSL이 활성화된 상태에서 HTTP 요청을 보내면 OpenSearch는 해당 요청을 이해하지 못하고
NotSslRecordException
을 반환
클라이언트가 HTTP 요청을 사용
curl -X GET "http://localhost:9200/"
요청은 HTTPS가 아니라 HTTP를 사용- OpenSearch가 HTTPS만 허용하도록 설정되어 있다면, 이 요청은 실패
💡 해결 방법
1. OpenSearch에서 SSL 비활성화
SSL/TLS를 비활성화하여 HTTP 요청을 처리할 수 있도록 만듬.
docker-compose.yml
수정plugins.security.disabled=true
를 설정하여 보안을 비활성화합니다:
hye-oni@hye-oniui-MacBookPro opensearch_test % curl -X GET "http://localhost:9200/"
{
"name" : "806a0aaed5e0",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "baCr8KTOShaPZKz8gievsg",
"version" : {
"distribution" : "opensearch",
"number" : "2.9.0",
"build_type" : "tar",
"build_hash" : "1164221ee2b8ba3560f0ff492309867beea28433",
"build_date" : "2023-07-18T21:22:48.164885046Z",
"build_snapshot" : false,
"lucene_version" : "9.7.0",
"minimum_wire_compatibility_version" : "7.10.0",
"minimum_index_compatibility_version" : "7.0.0"
},
"tagline" : "The OpenSearch Project: https://opensearch.org/"
}
hye-oni@hye-oniui-MacBookPro opensearch_test %
❣️ 인덱스 생성
인덱스를 생성합니다.
curl -X GET "http://localhost:9200/"
인덱스 생성 완료 응답
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "movies"
}
- OpenSearch에서 데이터를 저장하거나 관리하려면 인덱스(index)라는 구조를 먼저 생성해야 합니다.
- 이 명령은
movies
라는 이름의 빈 인덱스를 생성합니다. - 인덱스는 데이터베이스의 테이블과 유사한 개념으로, 특정 데이터셋을 저장할 공간을 만듭니다.
왜 필요한가?
_stats
API는 특정 인덱스의 통계를 반환합니다.
따라서_stats
를 호출하려면 해당 인덱스(movies
)가 존재해야 합니다.- 빈 인덱스를 생성한 뒤
_stats
를 호출하면, 해당 인덱스의 통계 데이터가 기본값으로 반환됩니다.
❣️ 2 _statts API 호출
curl -X GET "http://localhost:9200/movies/_stats?human&filter_path=_all.primaries.get"
curl -X GET "http://localhost:9200/movies/_stats?human&filter_path=_all.primaries.get"
hye-oni@hye-oniui-MacBookPro opensearch_test % curl -X PUT "http://localhost:9200/movies"
{"acknowledged":true,"shards_acknowledged":true,"index":"movies"}% hye-oni@hye-oniui-MacBookPro opensearch_test % curl -X GET "http://localhost:9200/movies/_stats?human&filter_path=_all.primaries.get"
{"_all":{"primaries":{"get":{"total":0,"getTime":"0s","time_in_millis":0,"exists_total":0,"exists_time":"0s","exists_time_in_millis":0,"missing_total":0,"missing_time":"0s","missing_time_in_millis":0,"current":0}}}}%
의미
_stats
API는 특정 인덱스(여기서는movies
)의 통계를 반환합니다.?human
옵션은 숫자 데이터를 사람이 읽기 쉬운 형식(예:5s
,10ms
)으로 변환합니다.filter_path=_all.primaries.get
는 응답에서get
통계만 반환하도록 필터링합니다.
왜 필요한가?
- 이슈에서 언급된 대로 응답에
time
대신getTime
이라는 잘못된 필드가 포함되어 있는지 확인하기!
♦️ 이슈 해결 과정
1. getTIme → time 필드로 수정
- 문제: api응답을 바로 바꾸면 호환성의 문제가 있음
⇒ 해결: time 필드를 추가하고 getTime을 deprecated 처리
2. time을 추가 한 후 둘다(GET_TIME, TIME) humanReadable한 필드로 바꿔주기
→ 통합 테스트 작성 후 에러를 발견하였습니다.
수정한 코드
builder.humanReadableField(Fields.TIME_IN_MILLIS, Fields.GET_TIME, getTime());
// 추가한 코드
builder.humanReadableField(Fields.TIME_IN_MILLIS, Fields.TIME, getTime());
문제
humanReadableField
의 역할을 처음에 TIME_IN_MILLIS의 값을 읽기 쉬운 단위
로 바꿔서 api에 추가해주는 역할이라고 생각 했습니다. 즉 2번째 인자만 api응답에 들어가는 것으로 생각했습니다.
하지만 humanReadableField
의 역할은 TIME_IN_MILLIS
도 api에 추가하고 TIME_IN_MILLIS
을 휴먼 리더블한 필드로 바꿔서 GET_TIME
도 추가하는 것이였습니다.
→ 즉 첫번째 인자, 2번째 인자 총 2개가 api 응답에 추가 됨 (아래 코드 참고)
humanReadableField 구현 코드
public XContentBuilder humanReadableField(String rawFieldName, String readableFieldName, Object value) throws IOException {
assert rawFieldName.equals(readableFieldName) == false : "expected raw and readable field names to differ, but they were both: "
+ rawFieldName;
if (humanReadable) {
// ❣️ 필드 추가 1
field(readableFieldName, Objects.toString(value));
}
HumanReadableTransformer transformer = HUMAN_READABLE_TRANSFORMERS.get(value.getClass());
if (transformer != null) {
Object rawValue = transformer.rawValue(value);
// ❣️ 필드 추가 2
field(rawFieldName, rawValue);
} else {
throw new IllegalArgumentException("no raw transformer found for class " + value.getClass());
}
return this;
}
위 코드에서 에러 발생!
⇒ 이유: TIME_IN_MILLIS가 2번 추가 된다.
테스트 작성 후 에러 발견
중복 발생
builder.humanReadableField(Fields.*TIME_IN_MILLIS*, Fields.*GET_TIME*, getTime());builder.humanReadableField(Fields.*TIME_IN_MILLIS*, Fields.*TIME*, getTime());
java.lang.RuntimeException: Failure at [indices.stats/60_time_fields.yml:11]: Duplicate field 'time_in_millis'
at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION
disabled); line: 1, column: 150]java.lang.RuntimeException: Failure at [indices.stats/60_time_fields.yml:11]: Duplicate field 'time_in_millis'
at [Source: REDACTED (StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION
disabled); line: 1, column: 150]
at __randomizedtesting.SeedInfo.seed([6B31651E5379D26D:E3655AC4FD85BF95]:0)
해결법
time
필드는 humanReadableField
를 사용하고, getTime은 field
를 사용해 api응답에 추가!
❓왜 toString
을 썼을까?
간단하게 설명하자면 ⭐ 다형성 ⭐ 덕분에 toString
을 TimeValue에 쓰면 휴먼 리더블한 값으로 바꿔준다.
그래서 time_in_millis의 값에 아래와 같이 ms, s와 같은 단위를 붙여준다.
(이부분을 자세히 설명하면 길어져서 나중에 자세히 블로그를 써보도록 하겠습니다!)
이와 관해 주고 받은 코드 리뷰!
3. 테스트 작성 및 통과
---
setup:
- do:
indices.create:
index: test1
body:
settings:
number_of_shards: 1
number_of_replicas: 0
wait_for_active_shards: all
- do:
index:
index: test1
id: 1
body: { "foo": "bar" }
- do:
indices.refresh:
index: test1
---
"Test _stats API includes both time and getTime metrics with human filter":
- skip:
version: " - 2.19.99"
reason: "this change is added in 3.0.0"
- do:
indices.stats:
metric: [ get ]
human: true
- is_true: _all.primaries.get.time
- is_true: _all.primaries.get.getTime
- match: { _all.primaries.get.time: "0s" }
- match: { _all.primaries.get.getTime: "0s" }