Using Traffic Replayer
출처: 문서
본문
Traffic Replayer 사용하기
참고: 이 페이지는 마이그레이션 중 다운타임을 피하기 위해 Capture and Replay를 사용하는 경우에만 해당돼요. 백필(backfill) 마이그레이션만 수행한다면 이 단계는 건너뛰어도 돼요. {: .note}
이 가이드는 마이그레이션 과정에서 캡처한 트래픽을 소스 클러스터에서 타깃 클러스터로 재생(replay)하기 위해 Traffic Replayer를 사용하는 방법을 다뤄요. Traffic Replayer를 사용하면 타깃 클러스터가 소스 클러스터와 같은 방식으로 요청을 처리할 수 있는지 검증하고, 원활한 마이그레이션을 위해 실시간 트래픽을 따라잡을 수 있어요.
Traffic Replayer 실행 시점
Migration Assistant를 배포한 후에는 Traffic Replayer가 기본적으로 실행되지 않아요. 소스 클러스터의 최근 변경 사항이 타깃 클러스터에 제대로 반영되도록 하려면, 모든 메타데이터와 문서가 마이그레이션된 후에 시작해야 해요.
예를 들어, 스냅샷을 찍은 이후에 문서가 삭제되었다면, 문서 마이그레이션이 완료되기 전에 Traffic Replayer를 시작할 경우 삭제 요청이 문서가 타깃에 추가되기 전에 실행될 수 있어요. 다른 모든 마이그레이션 프로세스가 끝난 후에 Traffic Replayer를 실행하면 타깃 클러스터가 소스 클러스터와 일관되게 유지된다는 점을 보장할 수 있어요.
구성 옵션
Traffic Replayer 설정은 Migration Assistant 배포 중에 구성돼요. Traffic Replayer가 타깃 클러스터와 제대로 통신할 수 있도록 인증 모드를 설정하는 것을 잊지 마세요.
Traffic Replayer 사용하기
Traffic Replayer를 관리하려면 console replay 명령어를 사용하세요. 다음 예시에서 사용 가능한 명령어들을 보여줄게요.
Traffic Replayer 시작하기
다음 명령어는 배포 시 지정한 옵션으로 Traffic Replayer를 시작해요:
console replay start
Traffic Replayer를 시작하면 다음과 비슷한 출력이 표시될 거예요:
root@ip-10-0-2-66:~# console replay start
Replayer started successfully.
Service migration-dev-traffic-Replayer-default set to 1 desired count. Currently 0 running and 0 pending.
Traffic Replayer 상태 확인하기
다음 명령어로 Traffic Replayer의 상태를 확인할 수 있어요:
console replay status
Replay는 다음 상태 중 하나를 반환해요:
Running은 현재 활성 상태로 실행 중인 컨테이너 인스턴스 수를 보여줘요.Pending은 프로비저닝 중인 인스턴스 수를 나타내요.Desired는 실행되어야 하는 인스턴스의 총 개수를 보여줘요.
다음과 비슷한 출력이 표시될 거예요:
root@ip-10-0-2-66:~# console replay status
(<ReplayStatus.STOPPED: 4>, 'Running=0\nPending=0\nDesired=0')
Traffic Replayer 중지하기
다음 명령어는 Traffic Replayer를 중지해요:
console replay stop
다음과 비슷한 출력이 표시될 거예요:
root@ip-10-0-2-66:~# console replay stop
Replayer stopped successfully.
Service migration-dev-traffic-Replayer-default set to 0 desired count. Currently 0 running and 0 pending.
전달 보장
Traffic Replayer는 Kafka에서 트래픽을 가져오고, 타깃 클러스터에 요청을 보낸 후 커밋 커서를 갱신해요. 이를 통해 "at least once" 전달 보장이 제공되지만, 성공이 항상 보장되는 건 아니에요. 따라서 타깃 클러스터가 기대대로 동작하는지 확인하려면 메트릭과 튜플 출력을 모니터링하거나 외부 검증을 수행해야 해요.
시간 스케일링
Traffic Replayer는 소스로의 각 커넥션에서 수신된 순서 그대로 요청을 보내요. 하지만 서로 다른 커넥션 간의 상대적인 타이밍은 보장되지 않아요. 예를 들어:
- 시나리오: 두 개의 커넥션이 있고, 하나는 매분 PUT 요청을 보내고 다른 하나는 매초 GET 요청을 보내요.
- 동작: Traffic Replayer는 각 커넥션 내에서의 순서는 유지하지만, 커넥션 간의 상대적인 타이밍(PUT과 GET 사이)은 유지되지 않아요.
소스 클러스터가 요청(GET과 PUT)에 100 ms 이내로 응답한다고 가정해 볼게요:
- speedup factor가 1이면 타깃은 소스와 동일한 요청 비율과 유휴 기간을 경험해요.
- speedup factor가 2이면 요청이 두 배 빠르게 전송되어 GET은 500 ms마다, PUT은 30초마다 전송돼요.
- speedup factor가 10이면 요청이 10배 빠르게 전송되고, 타깃이 빠르게 응답하기만 한다면 Traffic Replayer는 그 페이스를 유지할 수 있어요.
타깃이 충분히 빠르게 응답하지 못하면, Traffic Replayer는 다음 요청을 보내기 전에 이전 요청이 완료되기를 기다려요. 이로 인해 지연이 발생할 수 있고 전역적인 상대적 순서에 영향을 줄 수 있어요.
변환
마이그레이션 중에는 일부 요청을 버전 간에 변환해야 할 수 있어요. 예를 들어, Elasticsearch는 예전에 인덱스에서 여러 타입 매핑을 지원했지만 OpenSearch에서는 더 이상 그렇지 않아요. 이에 맞춰 문서를 여러 인덱스로 분리하거나 요청 데이터를 변환하는 방식으로 클라이언트를 조정해야 할 수 있어요.
Traffic Replayer는 호스트와 인증 헤더를 자동으로 재작성하지만, 더 복잡한 변환이 필요하다면 --transformer-config 옵션으로 사용자 지정 변환 규칙을 지정할 수 있어요. 자세한 내용은 Traffic Replayer README를 참고하세요.
변환 예시
소스 요청에 제거해야 하고 그 자식들은 승격해야 하는 tagToExcise 요소가 있고, URI 경로에 역시 제거해야 하는 extraThingToRemove가 포함되어 있다고 가정해 볼게요. 다음 Jolt 스크립트가 이 변환을 처리해요:
[{ "JsonJoltTransformerProvider":
[
{
"script": {
"operation": "shift",
"spec": {
"payload": {
"inlinedJsonBody": {
"top": {
"tagToExcise": {
"*": "payload.inlinedJsonBody.top.&"
},
"*": "payload.inlinedJsonBody.top.&"
},
"*": "payload.inlinedJsonBody.&"
},
"*": "payload.&"
},
"*": "&"
}
}
},
{
"script": {
"operation": "modify-overwrite-beta",
"spec": {
"URI": "=split('/extraThingToRemove',@(1,&))"
}
}
},
{
"script": {
"operation": "modify-overwrite-beta",
"spec": {
"URI": "=join('',@(1,&))"
}
}
}
]
}]
타깃으로 전송되는 결과 요청은 다음과 비슷하게 나타날 거예요:
PUT /oldStyleIndex/moreStuff HTTP/1.0
host: testhostname
{"top":{"properties":{"field1":{"type":"text"},"field2":{"type":"keyword"}}}}
{% include copy.html %}
Base64로 인코딩된 변환 스크립트는 --transformer-config-base64로 전달할 수 있어요.
결과 로그
소스 캡처에서 발생한 HTTP 트랜잭션과 타깃 클러스터로 다시 전송된 트랜잭션은 /shared-logs-output/traffic-replayer-default/*/tuples/tuples.log 위치의 파일에 기록돼요. /shared-logs-output 디렉터리는 Migration Console을 포함한 컨테이너들 간에 공유돼요. Migration Console에서 같은 경로로 이 파일들에 접근할 수 있어요. 이전 실행 기록도 gzipped 형식으로 확인할 수 있어요.
각 로그 항목은 줄바꿈으로 구분된 JSON 객체로, 소스와 타깃의 요청/응답 정보와 함께 응답 시간 같은 다른 트랜잭션 세부 정보도 담고 있어요.
이 로그에는 인증 헤더와 모든 HTTP 메시지 내용을 포함해 모든 요청의 내용이 담겨 있어요. 이 로그는 소스와 타깃 클러스터에서 무슨 일이 있었는지 판단하는 기준 자료가 되므로, 마이그레이션 환경에 대한 접근이 제한되어 있는지 꼭 확인하세요. 소스의 응답 시간은 프록시가 요청의 끝을 보내고 응답을 받기까지 걸린 시간을 의미해요. 타깃의 응답 시간도 같은 방식으로 기록되지만, 캡처 프록시, Traffic Replayer, 타깃의 위치가 서로 다를 수 있고 이 로그는 클라이언트의 위치를 고려하지 않는다는 점을 유의하세요. {: .note}
로그 항목 예시
다음 예시 로그 항목은 소스와 타깃 클러스터 양쪽으로 전송된 /_cat/indexes?v 요청을 보여줘요:
{
"sourceRequest": {
"Request-URI": "/_cat/indexes?v",
"Method": "GET",
"HTTP-Version": "HTTP/1.1",
"Host": "capture-proxy:9200",
"Authorization": "Basic YWRtaW46YWRtaW4=",
"User-Agent": "curl/8.5.0",
"Accept": "*/*",
"body": ""
},
"sourceResponse": {
"HTTP-Version": {"keepAliveDefault": true},
"Status-Code": 200,
"Reason-Phrase": "OK",
"response_time_ms": 59,
"content-type": "text/plain; charset=UTF-8",
"content-length": "214",
"body": "aGVhbHRoIHN0YXR1cyBpbmRleCAgICAgICB..."
},
"targetRequest": {
"Request-URI": "/_cat/indexes?v",
"Method": "GET",
"HTTP-Version": "HTTP/1.1",
"Host": "opensearchtarget",
"Authorization": "Basic YWRtaW46bXlTdHJvbmdQYXNzd29yZDEyMyE=",
"User-Agent": "curl/8.5.0",
"Accept": "*/*",
"body": ""
},
"targetResponses": [{
"HTTP-Version": {"keepAliveDefault": true},
"Status-Code": 200,
"Reason-Phrase": "OK",
"response_time_ms": 721,
"content-type": "text/plain; charset=UTF-8",
"content-length": "484",
"body": "aGVhbHRoIHN0YXR1cyBpbmRleCAgICAgICB..."
}],
"connectionId": "0242acfffe13000a-0000000a-00000005-1eb087a9beb83f3e-a32794b4.0",
"numRequests": 1,
"numErrors": 0
}
{% include copy.html %}
로그 내용 디코딩하기
압축된 데이터를 포함한 다양한 유형의 트래픽을 처리하기 위해 HTTP 메시지 본문의 내용은 Base64로 인코딩돼요. 로그를 좀 더 사람이 읽기 쉬운 형태로 보려면 콘솔 라이브러리인 tuples show를 사용하세요. 다음과 같이 스크립트를 실행하면 홈 디렉터리에 readable-tuples.log가 생성돼요:
console tuples show --in /shared-logs-output/traffic-Replayer-default/d3a4b31e1af4/tuples/tuples.log > readable-tuples.log
readable-tuples.log는 다음과 비슷하게 나타날 거예요:
{
"sourceRequest": {
"Request-URI": "/_cat/indexes?v",
"Method": "GET",
"HTTP-Version": "HTTP/1.1",
"Host": "capture-proxy:9200",
"Authorization": "Basic YWRtaW46YWRtaW4=",
"User-Agent": "curl/8.5.0",
"Accept": "*/*",
"body": ""
},
"sourceResponse": {
"HTTP-Version": {"keepAliveDefault": true},
"Status-Code": 200,
"Reason-Phrase": "OK",
"response_time_ms": 59,
"content-type": "text/plain; charset=UTF-8",
"content-length": "214",
"body": "health status index uuid ..."
},
"targetRequest": {
"Request-URI": "/_cat/indexes?v",
"Method": "GET",
"HTTP-Version": "HTTP/1.1",
"Host": "opensearchtarget",
"Authorization": "Basic YWRtaW46bXlTdHJvbmdQYXNzd29yZDEyMyE=",
"User-Agent": "curl/8.5.0",
"Accept": "*/*",
"body": ""
},
"targetResponses": [{
"HTTP-Version": {"keepAliveDefault": true},
"Status-Code": 200,
"Reason-Phrase": "OK",
"response_time_ms": 721,
"content-type": "text/plain; charset=UTF-8",
"content-length": "484",
"body": "health status index uuid ..."
}],
"connectionId": "0242acfffe13000a-0000000a-00000005-1eb087a9beb83f3e-a32794b4.0",
"numRequests": 1,
"numErrors": 0
}
Amazon CloudWatch 메트릭과 대시보드
Migration Assistant는 백필 프로세스의 상태와 성능을 시각화하기 위해 MigrationAssistant_ReindexFromSnapshot_Dashboard라는 이름의 Amazon CloudWatch 대시보드를 만들어요. 이 대시보드는 백필 워커와 Amazon OpenSearch Service로의 마이그레이션에 대한 메트릭을 결합하여, Capture Proxy와 Traffic Replayer 컴포넌트의 성능과 상태를 파악할 수 있게 해줘요. 여기에는 다음과 같은 메트릭이 포함돼요:
- 읽고 쓴 바이트 수.
- 활성 커넥션 수.
- 재생 속도 배율.
Capture and Replay 대시보드는 Migration Assistant를 배포한 AWS 리전의 AWS Management Console에 있는 Amazon CloudWatch 대시보드에서 찾을 수 있어요.
Traffic Replayer는 다양한 OpenTelemetry 메트릭을 Amazon CloudWatch로 내보내고, 트레이스는 AWS X-Ray를 통해 전송돼요. 다음은 마이그레이션 성능을 평가하는 데 도움이 되는 유용한 메트릭들이에요.
sourceStatusCode
이 메트릭은 소스와 타깃 클러스터 양쪽의 HTTP 상태 코드를 추적하며, GET이나 POST 같은 HTTP 동사와 상태 코드 패밀리(200--299)에 대한 차원(dimension)을 가져요. 이러한 차원은 DELETE 200s가 4xx로 바뀌거나 GET 4xx 오류가 5xx 오류로 바뀌는 경우처럼 소스와 타깃 간의 불일치를 빠르게 파악하는 데 도움이 돼요.
lagBetweenSourceAndTargetRequests
이 메트릭은 요청이 소스 클러스터와 타깃 클러스터에 도달하는 사이의 지연을 보여줘요. speedup factor가 1보다 크고 타깃 클러스터가 요청을 효율적으로 처리할 수 있다면, 재생이 진행됨에 따라 이 값은 감소해야 하며, 이는 재생 지연(replay lag)이 줄어들고 있다는 것을 나타내요.
추가 메트릭
다음 메트릭들도 보고돼요:
- 처리량:
bytesWrittenToTarget과bytesReadFromTarget은 클러스터로의 그리고 클러스터로부터의 처리량을 나타내요. - 재시도:
numRetriedRequests는 소스와 타깃 간의 상태 코드 불일치로 인해 재시도된 요청 수를 추적해요. - 이벤트 수: 다양한
(*)Count메트릭은 완료된 이벤트 수를 추적해요. - 소요 시간:
(*)Duration메트릭은 프로세스의 각 단계 소요 시간을 측정해요. - 예외:
(*)ExceptionCount는 각 처리 단계에서 발생한 예외 수를 보여줘요.
CloudWatch 유의 사항
CloudWatch로 푸시된 메트릭과 대시보드는 약 5분 정도 가시성 지연이 있을 수 있어요. CloudWatch는 또한 해상도가 낮은 데이터보다 해상도가 높은 데이터를 더 짧은 기간 동안 보관해요. 자세한 내용은 Amazon CloudWatch 개념을 참고하세요.
문제 해결
다음 섹션들은 발생할 수 있는 일반적인 문제를 진단하는 데 도움이 될 거예요.
Elasticsearch 콘텐츠 타입과 accept 헤더 호환성
최신 Elasticsearch 클라이언트(버전 7.11 이상, 모든 8.x 버전 포함)는 Content-Type과 Accept 헤더 양쪽에서 Elasticsearch 전용 미디어 타입을 사용해요. 이 클라이언트들은 다음과 같은 헤더를 보낼 수 있어요:
Content-Type: application/vnd.elasticsearch+json;compatible-with=8Accept: application/vnd.elasticsearch+json;compatible-with=8
이러한 Elasticsearch 전용 미디어 타입을 지원하지 않는 OpenSearch나 다른 서비스로 마이그레이션할 때는, 이 클라이언트들에서 오는 요청이 실패하거나 타깃 클러스터에 의해 거부될 수 있어요.
중요: 버전 7.11 이상의 Elasticsearch 클라이언트를 사용 중이고, application/vnd.elasticsearch+json 미디어 타입을 인식하지 못하는 OpenSearch나 다른 서비스로 마이그레이션하는 경우, Content-Type과 Accept 헤더를 표준 application/json 형식으로 변환하기 위한 변환을 반드시 적용해야 해요. 마이그레이션 후에는 클라이언트가 표준 미디어 타입을 사용하도록 업데이트해야 한다는 점도 유의하세요.
{: .important}
이 문제를 해결하려면, Elasticsearch 전용 미디어 타입을 표준 application/json 형식으로 변환하는 변환을 Traffic Replayer에 구성해야 해요.
먼저, /shared-logs-output/content-type-transformer.js 위치에 JavaScript 변환 파일을 만드세요:
const NEW_CONTENT_TYPE = "application/json";
const ELASTIC_CONTENT_TYPE = "application/vnd.elasticsearch+json";
function transform(request, context) {
let headers = request.get("headers");
if (headers) {
let contentType = headers.get("Content-Type");
if (Array.isArray(contentType)) {
headers.set("Content-Type", contentType.map(v => v.includes(ELASTIC_CONTENT_TYPE) ? NEW_CONTENT_TYPE : v));
} else if (typeof contentType === "string") {
if (contentType.includes(ELASTIC_CONTENT_TYPE)) {
headers.set("Content-Type", NEW_CONTENT_TYPE);
}
}
let accept = headers.get("Accept");
if (Array.isArray(accept)) {
headers.set("Accept", accept.map(v => v.includes(ELASTIC_CONTENT_TYPE) ? NEW_CONTENT_TYPE : v));
} else if (typeof accept === "string") {
if (accept.includes(ELASTIC_CONTENT_TYPE)) {
headers.set("Accept", NEW_CONTENT_TYPE);
}
}
}
return request;
}
function main(context) {
return (request) => {
if (Array.isArray(request)) {
return request.flat().map(item => transform(item, context));
}
return transform(request, context);
};
}
(() => main)();
{% include copy.html %}
다음으로, /shared-logs-output/replayer-transformation.json 위치에 변환 구성 파일을 만드세요:
[
{
"JsonJSTransformerProvider": {
"initializationScriptFile": "/shared-logs-output/content-type-transformer.js",
"bindingsObject": "{}"
}
}
]
{% include copy.html %}
마지막으로, trafficReplayerExtraArgs에 다음 인자를 추가해서 Traffic Replayer가 이 변환을 사용하도록 구성하세요:
--transformer-config-file /shared-logs-output/Replayer-transformation.json
이 변환 스크립트는 Content-Type과 Accept 헤더 양쪽에서 Elasticsearch 전용 미디어 타입(compatible-with=8 같은 버전 파라미터가 포함된 것 포함)을 자동으로 감지하고, 표준 application/json 형식으로 교체하여 Elasticsearch 전용 미디어 타입을 지원하지 않는 OpenSearch와 다른 서비스와의 호환성을 보장해요.
{% include migration-phase-navigation.html %}