Legacy Oracle을 AWS로 옮기며 RDS와 EC2를 저울질했고, 우리는 EC2를 택했다.
비용 때문이었다. 대신 “RDS만큼은 나와야 한다” 는 숙제가 따라왔다. RDS였다면 매니지드가 알아서 켜줬을 튜닝을 EC2에선 직접 해야 했고, 그 과정에서 io2 IOPS를 높였더니 오히려 커밋이 느려지는 역설까지 만났다. 이 글은 그 결정(EC2 vs RDS)과, RDS만큼의 쓰기 성능을 손수 만들어간 기록이다.

발단: 비용 때문에 RDS 대신 EC2
AWS로 Oracle을 옮기면서 가장 먼저 갈린 갈림길은 RDS for Oracle이냐, EC2 자체 구축이냐였다. 관리 부담만 보면 RDS가 압도적으로 편하다. 하지만 우리 규모에서는 비용 차이가 컸다. 인스턴스·스토리지를 따져보니 EC2에 직접 올리는 쪽이 눈에 띄게 저렴했고, 결국 비용을 이유로 EC2 자체 구축으로 결정했다. (라이선스는 RDS도 BYOL이 되니 결정적인 변수는 아니었다.)
솔직히 나는 Oracle을 이렇게 깊이 다뤄본 게 처음이었다. 기존엔 매뉴얼대로 설치하고 띄우는 정도가 전부였다. 그래서 이 결정은 비용을 아끼는 일이자, 동시에 매니지드 뒤에 가려져 있던 것들을 처음으로 직접 파보는 일이기도 했다. 다행히 AI의 도움을 받아 여러 튜닝 가설을 빠르게 세우고 시도할 수 있었고, 그때마다 지표로 효과를 확인하며 하나씩 익혔다.
그렇게 인스턴스에 Oracle 19c를 올렸다. RDS였다면 자동으로 켜졌을 설정을, 이제는 전부 직접 손봐야 했다.
게다가 우리 워크로드는 트랜잭션이 쏟아지는 OLTP다. 처리량(throughput)보다 커밋 한 건의 지연이 성능을 좌우한다. 뒤에서 redo와 io2에 매달리는 이유도, 결국 “OLTP에서 commit이 빨라야 한다”는 이 한 줄로 모인다.
AWS의 Oracle on AWS 베스트 프랙티스 백서와 권장 가이드도 EC2 자체 구동 시 Oracle Linux(또는 RHEL)를 권한다. Graviton(ARM)·Amazon Linux는 Oracle DB를 지원하지 않으니 OS 선택부터 주의해야 한다. 스토리지·ASM·redo 구성의 기준도 이 백서를 출발점으로 삼았다.
ASM은 처음 계획이 아니었다 — 단일 디스크의 벽
사실 처음 그림은 ASM이 아니었다. 레거시가 non-ASM 구조였기 때문이다. 온프레미스 시절 우리 Oracle은 ASM 없이 로컬 디스크의 파일시스템에 데이터파일을 직접 얹는 단순한 구조였고, 클라우드로 옮길 때도 일단 똑같이 가려 했다 — EBS 볼륨 하나에 파일시스템을 만들고 그 위에 데이터를 적재하는 식으로.
그런데 클라우드에서 그대로 재현하자 단일 디스크의 한계가 명확히 드러났다. 부하를 주면 iowait가 좀처럼 떨어지지 않았다. EBS 볼륨 하나는 프로비저닝한 IOPS·throughput에 상한이 있어, OLTP처럼 IO가 몰리면 그 한 볼륨이 곧바로 병목이 됐다. IOPS를 더 줘봐도 단일 볼륨·단일 IO 경로로는 벽이 있었다.
그래서 구조를 바꿨다 — ASM 디스크그룹으로 여러 EBS 볼륨을 묶어 병렬로 쓰기로. ASM이 데이터를 여러 디스크에 스트라이핑해 흩으면, IO가 한 볼륨에 쏠리지 않고 여러 볼륨·여러 큐로 분산된다. 디스크 그룹에 볼륨을 더해 IO 경로를 넓히자 iowait가 눈에 띄게 내려갔다. 레거시의 non-ASM을 그대로 들고 왔다면 못 넘었을 벽을, ASM으로 IO 대역을 확보해 넘었다. 그렇게 최종 구성은 ASM standalone + Grid Infrastructure가 됐다.
단, 여기엔 함정이 하나 있다. “여러 디스크로 병렬화 = 무조건 빠름”은 데이터 IO에는 맞지만 redo에는 아니다. redo의 작고 잦은 동기 쓰기는 오히려 과한 병렬화가 독이 됐는데, 그 얘기는 뒤에서 따로 다룬다.
스토리지부터 용도별로 갈랐다
EC2로 직접 올리니 스토리지부터 손수 나눠야 했다. RDS라면 신경 쓸 일이 없었을 부분인데, 막상 해보니 워크로드 성격에 맞춰 볼륨 타입을 가르는 게 redo 성능의 출발점이었다.
| 용도 | 볼륨 | 이유 |
|---|---|---|
| Data + Redo | io2 (ASM DATA 디스크그룹) | redo는 커밋 지연에, data는 랜덤 I/O에 민감 → 일관된 sub-ms 지연 |
| Archive 로그 | gp3 | 한 번 쓰고 거의 안 읽음, 순차 쓰기 → IOPS보다 처리량, 비용 효율 |
| Backup | gp3 → S3 | 백업 임시 저장 후 S3로 이관 |
핵심은 redo가 들어가는 디스크는 io2라는 것. 커밋할 때마다 redo 쓰기가 일어나고, 그 지연이 곧 DB 전체 성능을 좌우하기 때문이다. AWS도 sub-ms 지연이 필요한 DB I/O에는 io2를, 처리량 중심엔 gp3를 권한다. ASM 디스크그룹은 외부 이중화(external)로, redo는 미세 분산(FINE) 스트라이프로 뒀다.
처음엔 단순하게 생각했다. io2니까 IOPS를 넉넉히 주면 빨라지겠지. redo 디스크 IOPS를 더 높여봤다.
직관에 반하는 현상
IOPS를 과하게 올리자 이상한 일이 벌어졌다.
- throughput(처리량) 그래프는 최대치를 찍었다.
- 그런데 트랜잭션 처리(커밋)는 오히려 느려졌다.
- 디스크 큐잉(queue depth)이 계속 올라갔다.
빠르라고 준 IOPS가 발목을 잡고 있었다.
왜? redo는 sync write다
핵심은 redo 로그가 동기 쓰기(sync write) 라는 데 있다.
- 커밋이 일어나면 LGWR가 redo를 디스크에 쓰고, 그 flush가 끝날 때까지 트랜잭션은 대기한다. 하나라도 늦게 끝나면 전체 커밋이 기다린다.
- io2 IOPS를 높이면 Oracle/OS가 I/O를 더 많이 병렬로 쏟아붓는다. 그러다 디스크 내부 큐가 포화되면, 개별 쓰기의 지연시간(latency)이 길어진다.
- 즉 처리량은 늘지만 지연은 나빠진다. 그런데 redo는 처리량이 아니라 지연에 민감한 워크로드다.
| redo 디스크 IOPS | 결과 |
|---|---|
| 높음 | 디스크 큐 포화 → LGWR wait 증가 → log file sync 증가 → 느려짐 |
| 적정 | 큐 안정 → 낮은 latency → 빠른 flush → 커밋 빨라짐 |
진단: v$system_event로 본다
추측 대신 대기 이벤트를 봤다. 두 지표가 핵심이다.
log file sync— 커밋한 세션이 redo flush를 기다린 시간 (사용자가 체감하는 커밋 지연)log file parallel write— LGWR가 redo를 디스크에 쓴 시간 (실제 I/O 지연)
SELECT event, total_waits, time_waited_micro / 1000 AS time_waited_ms, time_waited_micro / total_waits AS avg_wait_microFROM v$system_eventWHERE event IN ('log file sync', 'log file parallel write');IOPS를 과하게 준 구간에서 log file parallel write의 평균 대기가 치솟았고, 그게 그대로 log file sync로 전이됐다. 디스크가 큐에 눌려 늦게 응답하니, 커밋이 줄줄이 밀린 것이다.
또 하나의 함정: 과한 redo 병렬화
“그럼 redo를 여러 디스크 그룹으로 더 쪼개 병렬로 쓰면 낫지 않을까?” 싶어 redo를 과하게 분산·다중화해봤다. 결과는 반대였다 — latency가 더 올라갔다. 디스크 그룹을 잘게 쪼개 병렬로 돌릴수록, redo처럼 작고 잦은 sync write는 경로만 복잡해져 오히려 지연이 커졌다.
단, 헷갈리지 말아야 할 게 있다. ASM의 FINE 스트라이프(128KB 단위 분산)는 다르다. 이건 redo 같은 작은 I/O를 여러 디스크에 잘게 흩어 LGWR 지연을 줄여준다(Oracle이 redo·archive에 권장하는 방식). 실제로 redo 디스크 그룹을 FINE 스트라이프로 바꾸자 redo 쓰기 지연이 떨어지고 commit 속도가 눈에 띄게 빨라졌다. 그래서 FINE은 확실히 적용하되, 그 위에 디스크 그룹을 더 쪼개는 식의 과한 병렬화만 걷어냈다.
-- redo(ONLINELOG)는 FINE 스트라이프 — 작은 I/O를 128KB 단위로 흩어 지연을 낮춘다ALTER DISKGROUP REDO MODIFY TEMPLATE ONLINELOG ATTRIBUTES (FINE UNPROTECTED);여기에 redo 로그 그룹도 넉넉히(1GB짜리 여러 벌로) 증설했다. 로그 스위치가 잦으면 그 자체가 대기를 만들기 때문이다.
”RDS만큼”은 RDS를 역설계해서
방향을 바꿨다. RDS가 빠른 건 AWS가 기본으로 박아둔 튜닝 덕이다. 그러니 RDS의 실제 설정을 떠서 EC2에 그대로 옮기면 출발점이 된다. v$sga, v$parameter로 RDS 인스턴스를 덤프해 EC2와 한 줄씩 비교했다.
-- RDS에서 현재 메모리/파라미터를 확인해 EC2와 대조SELECT * FROM v$sga;SELECT name, value FROM v$parameterWHERE name IN ('pga_aggregate_target', 'use_large_pages', 'filesystemio_options', 'db_writer_processes', 'disk_asynch_io', 'log_buffer');확인해보니 RDS는 HugePages 전용(use_large_pages=ONLY)에, Direct/Async I/O(filesystemio_options=setall, disk_asynch_io=TRUE)가 켜져 있었다. 눈에 안 보이던 이 기본값들이 성능의 절반이었다. EC2에 동일하게 맞췄다.
-- 8코어 64GB 기준 (RDS 설정을 역설계해 적용)ALTER SYSTEM SET sga_target = 47G SCOPE=SPFILE;ALTER SYSTEM SET sga_max_size = 47G SCOPE=SPFILE;ALTER SYSTEM SET pga_aggregate_target = 8G SCOPE=SPFILE;ALTER SYSTEM SET use_large_pages = ONLY SCOPE=SPFILE; -- HugePages 전용ALTER SYSTEM SET filesystemio_options = setall SCOPE=SPFILE; -- Direct + Async I/OALTER SYSTEM SET disk_asynch_io = TRUE SCOPE=SPFILE;ALTER SYSTEM SET db_writer_processes = 2 SCOPE=SPFILE;ALTER SYSTEM SET log_buffer = 116776960 SCOPE=SPFILE;이건 Oracle 튜닝의 정석이기도 하다 — DBWR의
db file parallel write대기가 길면 비동기 I/O(disk_asynch_io)가 켜져 있는지 확인하고, IOPS와 버퍼 캐시를 충분히 둔다. 우리가 부딪힌 게 정확히 그 대기였다.
OS도 DB에 맞춰 손봤다
매니지드가 대신 해주던 마지막 조각은 커널 튜닝이었다. HugePages를 고정하고, 스왑을 억제하고, dirty 페이지 플러시를 당겼다.
vm.nr_hugepages = 23200 # SGA를 덮을 만큼vm.hugetlb_shm_group = 54321 # oracle 그룹에만 HugePages 허용vm.swappiness = 10 # 스왑 최소화 (물리 메모리 우선)vm.dirty_ratio = 15 # dirty 데이터 강제 플러시 임계vm.dirty_background_ratio = 5 # 백그라운드 플러시 시작 임계vm.dirty_expire_centisecs = 3000 # dirty 페이지 최대 30초 내 기록vm.vfs_cache_pressure = 50 # inode/dentry 캐시 적절히 유지# /etc/security/limits.conf — HugePages 메모리 고정용oracle soft memlock unlimitedoracle hard memlock unlimiteduse_large_pages=ONLY는 HugePages가 모자라면 기동을 막아 조용히 일반 페이지로 떨어지는 사고를 차단한다. grep Huge /proc/meminfo로 HugePages_Total이 SGA를 덮는지 꼭 확인한다.
마지막 한 구간: App ↔ DB 네트워크
DB 안을 아무리 조여도, 애플리케이션 서버와 Oracle 사이의 네트워크가 느리면 OLTP 응답은 거기서 깎인다. OLTP는 쿼리 한 번에 짧은 왕복이 수없이 오가는 구조라, 그 사이 네트워크 지연(RTT) 이 조금만 쌓여도 사용자 체감으로 그대로 드러난다.
그래서 App 서버와 Oracle 인스턴스를 클러스터 placement group으로 묶었다. 같은 AZ 안에서 물리적으로 가까운 하드웨어에 인스턴스를 모아 배치해, 인스턴스 간 네트워크 지연을 최소화하고 대역폭을 끌어올리는 방식이다. App↔DB를 한 placement group에 두자 둘 사이 RTT가 떨어졌고, 잦은 쿼리 왕복이 쌓아 올리던 지연이 줄었다.
결국 commit을 빠르게 만드는 일은 DB 한 곳이 아니었다. 스토리지(ASM·io2) → DB 파라미터 → OS 커널 → 네트워크(placement group) 로 이어지는, 경로 전체를 손보는 일이었다.
마치며
여섯 가지가 남았다.
- 단일 디스크엔 천장이 있다. 레거시의 non-ASM을 그대로 옮기니
iowait가 안 빠졌다. ASM 디스크그룹으로 여러 EBS를 병렬화해 IO 대역부터 확보해야 했다. - 높은 IOPS가 곧 빠름은 아니다. redo 같은 sync write는 처리량(throughput)이 아니라 지연(latency) 으로 평가된다. 과한 IOPS는 큐를 포화시켜 오히려 커밋을 늦춘다.
- 과한 분산·병렬이 늘 답은 아니다. redo를 디스크 그룹으로 과하게 쪼개니 latency가 올랐다. (단 ASM FINE 스트라이프는 예외 — 작은 I/O 지연을 오히려 줄여준다.)
- “RDS만큼”은 RDS를 역설계해서 만든다.
v$sga·v$parameter로 떠본 HugePages·filesystemio_options·disk_asynch_io가 숨은 절반이었다. - DB만 튜닝하는 게 아니다. HugePages·swappiness·dirty 비율 같은 커널 파라미터까지 맞춰야 매니지드가 대신 켜주던 성능에 닿는다.
- commit 경로는 DB 밖까지다. App↔DB 네트워크를 placement group으로 좁혀야 OLTP의 잦은 왕복 지연이 빠진다.
돌이켜보면 이 작업은 Oracle을 처음으로 제대로 들여다본 시간이었다. 매뉴얼대로 설치만 하던 내가, redo가 왜 느린지 대기 이벤트를 뜯어보고 RDS가 조용히 켜두던 값을 하나씩 옮겨 심으며 — 매니지드가 가려두었던 레이어를 처음으로 직접 만졌다.
io2를 높이면 빨라질 거라는 단순한 직관이 디스크 큐 앞에서 깨지고, commit이 DB 밖 네트워크까지 걸쳐 있다는 걸 RTT로 확인하던 — 그 하나하나가 셀프 호스팅의 진짜 절반이었다. 비용을 아낀 만큼 그 자유를 감당하는 일이었지만, 막상 감당해보니 남는 게 더 많은 시간이었다.
이 글은 commit 성능(쓰기 지연)에 집중했다. 나머지 절반인 백업 — 어떤 백업 정책으로 가져갔고(RMAN 풀백업·Data Pump),
/backup → S3업로드 속도를 어떻게 확보했는지는 다음 글에서 따로 다룰 예정이다.
참고
- AWS 백서 — Best Practices for Running Oracle Database on AWS
- AWS — RDS for Oracle의 IOPS·지연 곡선
- Oracle — FILESYSTEMIO_OPTIONS
- HugePages와 use_large_pages (MS Learn · Oracle on Azure)
- AWS — EBS Provisioned IOPS (io2) 볼륨
- AWS — EBS 볼륨 타입 (gp3 vs io2)
- Oracle — ASM 파일·디렉터리·템플릿 (ONLINELOG FINE 스트라이프)
- AWS — EC2 placement group (cluster, 저지연 네트워크)