실시간 복제 데이터를 이관시키는 방법
안녕하세요. 스테이지랩스(STAYGE Labs) Back-End 팀 강은호입니다. 이번 글에서는 원천 DB인 DynamoDB에서 DynamoDB Streams을 통해 대상 DB인 Aurora Serverless V1 PostgreSQL으로 복제되는 데이터를 Aurora PostgreSQL로 마이그레이션 시킨 경험에 대해서 소개하려고 합니다.
목차
- 기존의 실시간 복제 아키택쳐 소개
- Serverless v1의 한계와 필요 개선점
- 마이그래이션 전략
- 작업 시 팁
- 결론
기존의 실시간 복제 아키택쳐 소개
스테이지랩스에서는 실시간 팬커뮤니티인 Mnet Plus를 운영 중입니다. 서비스의 기능을 한 줄로 함축하자면 아래와 같습니다.
“아티스트와 팬이 소통하는 SNS”
팬덤 SNS는 트래픽이 순간적으로 급증하는 특징이 있습니다. 예를 들어 아티스트가 작성한 포스트의 푸시 알람을 받고 접속하는 유저 트래픽, 생방송과 함께 진행되는 실시간 투표등을 예시로 들 수 있습니다. 최근 진행 되었던 생방송 실시간 투표에서는 평소 대비 트래픽이 100배 이상 증가 했던 경험도 있습니다.
DynamoDB
스테이지랩스에서는 이와 같은 예측 하기 어렵고 급증하는 트래픽을 다루기 위해 서버리스 아키택쳐를 채택하고 있고, 데이터베이스는 DynamoDB를 주로 사용하고 있습니다. DyanmoDB는 AWS에서 서비스하는 완전관리 분산형, NoSQL DB입니다. 10ms 이하의 응답을 보장하는 빠른 성능과 유연한 확장을 통해서 급증하는 트래픽에도 일괄된 성능을 보장해야 하는 서비스에 탁월함을 보여줍니다.
하지만, DynamoDB는 데이터를 join 할 수 없고 Sum, Count 등의 집계 함수를 지원하지 않습니다. 이는 DynamoDB만으로는 데이터에 대한 집계나 분석을 할 수 없다는 것을 의미합니다. 스테이지랩스에서는 DynamoDB Streams을 사용해 변경된 데이터(CDC)를 RDB에 복제하는 방법으로 해당 단점을 극복했습니다.
DynamoDB Streams
DynamoDB Streams는 DynamoDB 테이블에서 시간 순서에 따라 데이터의 변경을 캡처하고 이 정보를 최대 24시간 동안 로그에 저장합니다. 애플리케이션은 시간의 흐름에 따라 변경되는 데이터의 정보를 알 수 있습니다. 좀 더 쉬운 설명을 위해서 유저의 커뮤니티 가입 활동을 예시로 이해를 도와보겠습니다.
1시에 유저a가 A, C 커뮤니티에 가입한 상태인데, 2시에는 B, D 커뮤니티에 가입되어 있습니다. 결과적으로 유저 a 는 B, D 커뮤니티에 가입되어있는 상태이지만, 1시간이라는 기간 동안 ( join C => leave A => join D => leave C )라는 행동을 했습니다. 시간 순서에 따른 데이터의 변경 정보가 있다면, 애플리케이션은 실시간으로 싱크가 맞는 복제 데이터베이스를 만들 수 있습니다.
스테이지랩스에서는 Change Data Capture(CDC) 기술을 사용해서 DynamoDB와 실시간 싱크가 맞는 복제 RDB를 만들어 집계 및 분석의 어려움을 극복했습니다.
Aurora Serverless V1
DynamoDB의 온디멘드 버전은 서버리스 서비스입니다. 평소보다 2배 데이터를 쓰기 해도 저장이 되고, 2배에 2배를 쓰기 해도 저장이 되게 자동 확장 됩니다. 데이터 쓰기가 2배 증가한다는 것은 데이터 변경 정보 생성이 2배가 된다는 것을 의미합니다. 그렇기 때문에 복제 대상이 되는 RDB의 성능도 같이 확장되어야 합니다. 데이터 복제 양에 따라 유연하게 확장될 수 있는 Aurora Serverless v1을 ㅎㅐㅆㅈㅣ………….
Serverless v1 의 한계와 필요 개선점
데이터 복제 양에 따라 유연하게 확장되는 것을 기대하며 복제 대상 RDB로 Aurora Serverless v1을 채택했습니다. 하지만, 데이터 쓰기와 읽기 시에 발생하는 문제점과 추가적인 문제점으로 인해서 운영에 어려움을 겪게 되었습니다.
데이터 쓰기 시 문제
Aurora Serverless v1 은 CPU, Memory, Connection 등의 지표가 증가하면 내부적인 규칙으로 성능을 업그레이드합니다. ACU라는 단위로 성능을 측정하는데 2배씩 증가시키는 방법으로 확장합니다. (ex. 2 ACU => 4 ACU => 8 ACU) 확장된 성능은 지표가 안정적이게 되면 자동으로 줄어들게 되는데, ACU가 늘었다가 줄어드는 과정에서 애플리케이션에서 연결 중인 Connection이 끊어지게 됩니다. CDC와 같은 순서가 보장되어야 하는 데이터는 쓰기 중에 실패가 난다면 재 처리가 까다롭습니다.
그리고, ACU의 용량이 증가 되는데 어느정도 시간이 필요하기 때문에 복제 데이터의 양이 급격히 증가 한다면 유연하게 처리하기 어려운 점도 문제로 꼽을 수 있습니다.
데이터 읽기 시 문제
Aurora는 현대 애플리케이션의 운영 측면에서 탁월함을 보입니다. 그중 Writer 인스턴스와 Reader 인스턴스를 분리해서 사용할 수 있다는 점을 뽑고 싶은데요. 데이터 분석가가 Reader 인스턴스에 연결해 무거운 질의문을 아무리 던져도 Writer 인스턴스가 있기 때문에 애플리케이션의 쓰기 작업에는 어떠한 영향을 주지 않는다는 것을 예시로 들 수 있겠습니다. 하지만, Aurora Serverless v1은 Writer와 Reader 인스턴스를 구분하지 않습니다. 실제로 조회하는 데이터의 양이 조금만 커도 ACU가 높아지고 애플리케이션에서 연결이 끊어지며 쓰기 작업에 실패하는 케이스가 종종 발생했습니다. 그때마다 실패 로그를 분석해 가면서 수 작업으로 데이터의 싱크를 맞춰 주곤 했습니다.
추가적인 문제
Aurora에는 다양한 추가 기능들을 지원하는데요. “데이터를 S3로 내보내거나 가져오는 기능”, “데이터 추가, 변경, 삭제 이벤트를 로그로 남기는 기능”, ” Redshift로 zero-ETL 하는 기능” 등 없으면 매우 불편하거나 아쉬운 기능들이 있습니다.
Aurora Serverless v1에는 해당 기능들을 지원하지 않습니다.
여러 가지 이유로 저희 Back-End팀은 Aurora Serverless v1으로 얻는 이점보다 단점이 많다고 판단하고 AuroraDB로 마이그레이션 하기로 판단했습니다.
(단점들이 보완된 Aurora Serverless v2를 사용하지 않는 것이 궁금해하시는 분이 있을 수 있는데 비용이 2배라서 제외했습니다….)
마이그레이션 전략
이기종 DB 간의 데이터 이관 작업은 상당히 까다로운 작업이고 마법 같은 도구는 없습니다. 원본인 DynamoDB에서 복제본인 AuroraDB로 이관시켜야 하는 데이터는 수십GB 이고, 수천만 개의 레코드를 쓰기 작업해야 하기 때문에 DB에 주는 부하와 작업 시간을 신경 써줘야 합니다. 작업이 완료되면 DynamoDB Streams의 CDC를 통해서 원본과 복제본의 싱크도 맞춰야 하죠. 덤프와 싱크 작업으로 명명해 보겠습니다.
덤프와 싱크 작업
덤프 작업이 끝나고 찰나의 딜레이 없이 싱크 작업을 시작시키지 않는 이상 아래 그림과 같이 데이터의 중복 또는 공백 구간이 생깁니다. 중복 구간이 생긴다면 싱크 작업에서 중복된 데이터를 제외해야 합니다. 공백 구간이 생기면 공백의 데이터를 찾아서 수 작업으로 넣어줘야 합니다. 머리 아파지기 시작하죠? 하지만! 괜찮습니다. 우리의 아키택쳐에는 풀었다 잠갔다 할 수 있는 스트리밍 파이프인 DynamoDB Streams 가 있기 때문이죠!
덤프 작업
원본인 DynamoDB에서 데이터를 추출하는 작업은 쉽습니다. DynamoDB 테이블을 S3로 내보내기 기능을 지원하기 때문이죠. 클릭 몇 번 하면 아래와 같은 DynamoDB JSON 형식으로 S3에 파일을 떨궈 줍니다. 이렇게 추출된 데이터는 Athena를 통해서 통해 쿼리를 할 수 있는 상태가 됩니다.
Amazon Athena는 표준 SQL을 사용하여 S3에 있는 데이터를 간편하게 분석하고, 기본적으로 질의 결과를 CSV 형식으로 S3에 저장해 주는 서비스입니다.
// DynamoDB의 데이터를 S3 로 내보내면 아래와 같은 형태이다.
{
"Item": {
"id": {
"S": "my-unique-id"
},
"name": {
"S": "Alex"
},
"coins": {
"N": "100"
}
}
}
이제 복제 대상인 Aurora PostgreSQL에 잘 집어넣기만 하면 됩니다. 하지만 데이터의 양이 많기 때문에 DB에 부화를 최소화하고, 빠르게 쓰기 작업을 할 수 있는 방법 이어야 합니다. 다양한 방법이 있을 수도 있지만 RDS로 S3 데이터 가져오기 기능을 사용하게 되었습니다. S3에 CSV 파일 형식으로 데이터가 있을 때 RDS 테이블에 데이터를 가져올 수 있는 기능이고 내부적으로 PostgreSQL COPY 명령어를 사용합니다.
몇 가지 세팅을 하면 아래와 같은 간단한 쿼리로 S3에 있는 데이터를 복제 대상 테이블로 빠르고 적당한 DB 리소스를 사용해서 데이터를 이관할 수 있습니다.
SELECT aws_s3.table_import_from_s3(
'replica_schema.replica_table',
'',
'DELIMITER as '','' NULL AS ''null'' CSV HEADER',
aws_commons.create_s3_uri(
'my-bucket',
'/my-path/exported-table.csv',
'ap-northeast-2'
)
);
하. 지. 만….! 추출한 데이터 형식은 JSON이고 “table_import_from_s3”를 하기 위한 데이터 형식은 CSV입니다. 여기서 Athena가 활약할 차례입니다. 앞서 말했지만 Athena는 SQL 결과를 S3에 CSV 형식으로 저장해 주기 때문에 JSON을 CSV 로 변환하는 도구로 사용할 수 있습니다. 또한, SQL의 조건문을 사용해서 데이터를 필터링할 수도 있습니다.
싱크 작업
DynamoDB Streams를 활성화하고 Lambda에서 CDC 이벤트를 poling 하면 아래와 같은 JSON 형식으로 데이터를 받아 볼 수 있습니다. “eventName” 에는 INSERT, MODIFY, DELETE 값이 올 수 있고, 각각 데이터의 추가, 변경, 삭제를 의미합니다. Lambda에서 CDC 이벤트로 데이터의 싱크를 맞추는 작업은 간단합니다. 복제 대상 DB와 연결하고 JSON 데이터를 SQL로 변환해 실행시키면 됩니다.
-- Poling 한 JSON 데이터를 코드에서 분석하고 SQL로 변환한다.
INSERT INTO schema.table ("timestamp", "user_name", "message")
VALUES (
'2016–11–18:12:09:36',
'John Doe',
'This is a bark from the Woofer social network'
)
// Lambda 에서 DynamoDB Streams 의 CDC 를 poling 하면 받게 되는 이벤트
{
"Records": [
{
"eventID": "7de3041dd709b024af6f29e4fa13d34c",
"eventName": "INSERT",
"eventVersion": "1.1",
"eventSource": "aws:dynamodb",
"awsRegion": "region",
"dynamodb": {
"ApproximateCreationDateTime": 1479499740,
"Keys": {
"Timestamp": {
"S": "2016-11-18:12:09:36"
},
"Username": {
"S": "John Doe"
}
},
"NewImage": {
"Timestamp": {
"S": "2016-11-18:12:09:36"
},
"Message": {
"S": "This is a bark from the Woofer social network"
},
"Username": {
"S": "John Doe"
}
},
"SequenceNumber": "13021600000000001596893679",
"SizeBytes": 112,
"StreamViewType": "NEW_IMAGE"
},
"eventSourceARN": "arn:aws:dynamodb:region:123456789012:table/BarkTable/stream/2016-11-16T20:42:48.104"
}
]
}
마이그레이션 작업 순서
덤프와 싱크 작업을 할 수 있는 도구들은 모두 준비되었습니다. 데이터 이관 작업의 시나리오를 작성해 보겠습니다.
- DynamoDB Streams를 사용해 복제 대상 DB인 Aurora PostgreSQL로 실시간 복제 하는 기능을 배포합니다.
2. DynamoDB Stream의 복제 프로세스를 일시 중지하고, 복제 대상 테이블의 데이터를 Truncate 합니다.
3. DynamoDB Table S3로 내보내기 기능을 사용해서 데이터를 덤프 합니다.
4. DynamoDB JSON 형식으로 저장되어 있는 덤프 데이터를 Athena SQL을 사용해 필터링 및 CSV로 변환 후 S3에 다시 저장합니다.
5. 정제 및 변환되어 S3에 저장된 CSV 데이터를 Aurora의 “table_import_from_s3” 기능을 사용해 손쉽게 덤프 작업을 합니다.
6. 데이터 덤프 작업이 끝나면 일시 정해 두었던 DynamoDB Streams의 복제 프로세스를 재 구동 시킨다. 덤프 작업 시간 동안 쌓여있던 CDC 이벤트가 대상 DB에 반영되어 데이터의 싱크가 맞게 되고 최종적인 데이터 이관 작업이 완료됩니다.
작업 시 팁
스테이지랩스에서는 위에 소개한 데이터 이관 전략을 통해서 이기종 DB 간의 실시간 복제 중인 데이터를 이관하는 데 성공했습니다. DynamoDB에서 PostgreSQL로 데이터를 이관시키는 작업이 일반적인 케이스는 아니지만, AWS의 여러 가지 도구들을 활용한 방법으로 볼 수 있습니다. 저도 덤프 작업을 하면서 많은 어려움이 있었는데, 몇 가지 경험을 공유드릴까 합니다.
Athena SQL을 사용해 CSV 파일로 변환할 때
아래의 SQL 은 Athena를 사용해 DynamoDB JSON 형식을 CSV 형식으로 바꾸어 S3에 저장하고, 필요에 따라 데이터 필터링 및 변환하는 예시 SQL입니다. WHERE 조건문으로 데이터를 필터링하고, AS 문법으로 컬럼의 이름을 바꿀 수 있습니다. split_part 함수를 사용해서 ‘#’, ‘:’ 등의 문자로 묶여 사용되는 sort key를 분리할 수 있고, regexp_replace 함수를 사용해서 PostgreSQL에 들어갈 수 없는 유저가 입력한 null과 같은 아스키코드를 치환할 수 있습니다. 예시와 같이 Athena에서 지원하는 기능을 사용하면 요구사항에 맞게 CSV 형식으로 변환시킬 수 있습니다.
SELECT
item.pkey.s as u_id,
item.status.s as status,
item.profile.s as profile,
split_part(item.skey.s, '#',2) as community_id,
regexp_replace(item.username.s, '[\u0000\u005C\u0027\u200B]', '') as username,
regexp_replace(item.desc.s, '[\u0000\u005C\u0027\u200B]', '') as descripttion
FROM "glue_database"."glue_table"
where item.entity.s = 'user'
and item.pkey.s IS NOT NULL
and item.skey.s IS NOT NULL
;
스테이지랩스에서는 DynamoDB JSON 형식을 CSV로 변환하는데 Athena를 활용했지만, 사실 python과 같은 프로그래밍 언어로 작성된 스크립트로도 작업이 가능한 부분입니다. 하지만 저희는 Athena를 활용하는 방법이 더 빠르고 간단하다 생각했기 때문에 해당 방법을 채택을 했습니다.
수천만 건의 CSV 파일을 Aurora에 import 시킬 때
수천만 건 또는 수억건의 데이터를 쓰기 작업한다는 것은 DB에 상당한 부하를 줄 수 있습니다. 다행히 “table_import_from_s3”는 내부적으로 PostgreSQL의 COPY로 구현이 되어 있기 때문에 insert 문을 사용하는 것보다 빠르고, 비교적 안정적으로 쓰기 작업을 할 수 있습니다.
저희팀은 COPY 작업이 Aurora PosrtgreSQL의 writer 인스턴스에 끼치는 영향을 파악하기 위해서 개발 환경에 2 VCPU 4memory(GiB)성능인 DB에 우선 적으로 테스트해봤습니다. 그 결과 평균적으로 CPU를 65% 까지 사용하는 부하를 확인했습니다. 이후 약 2배의 성능인 운영 환경의 DB에 쓰기 작업을 했을 때 평균 CPU 45% 를 사용해 쓰기 작업을 했습니다. PostgreSQL COPY 기능을 사용한 쓰기 작업은 writer 인스턴스 성능에 따라 리소스 사용량이 달라지고, 한정된 자원을 사용해 쓰기 작업을 할 수 있다(CPU 사용량이 100%를 찍거나 하지 않는다.)는 결과를 도출했습니다.
또한, 2,276(MB)인 11,323,256 row의 쓰기 작업 시간 323초 4,676(MB)인 15,175,025 row의 쓰기 작업시간 668초로 실험을 통해 데이터의 row 수 보다 데이터의 크기가 쓰기 작업 시간에 영향을 끼친 치고 1초에 약 8MB의 데이터를 쓰기 한다는 결과를 도출했습니다.
저희 팀에서는 여러 실험을 통해서 writer 인스턴스의 성능을 올리고 트래픽이 적은 시간대에 덤프 작업을 한다면 서비스에 영향을 주지 않을 것이라고 판단했습니다.
마무리
시행착오가 많이 있었지만 앞서 설명한 데이터 이관 방법으로 작업을 성공적으로 마무리할 수 있었습니다. 데이터 읽기 및 분석 작업이 쓰기 작업에 영향을 주지 않게 되었고 서비스 운영 경험을 향상할 수 있었습니다. 또한 Aurora PostgreSQL의 추가 기능들을 사용하게 되면서 여러 업무를 자동화시키기도 했습니다.
데이터를 이관시키는 작업은 흔하지 않은 경험입니다. 이기종 DB 간의 실시간 복제 데이터를 이관시키는 작업은 더 흔하지 않은 경험이고, 저에게 큰 도전 과제이었습니다. 저는 해당 작업을 통해 많은 것을 배우고, 한 단계 성장할 수 있는 기회가 되었습니다. 이번 아티클이 데이터 이관 작업을 하는 누군가에게 도움이 되었으면 좋겠습니다. 긴 글 읽어주셔서 감사합니다!