StudyDad Loop 제품을 만들며 배운 운영과 설계를 기록합니다.

FamBlend를 중심으로 실제 구현, 운영 메모, GitHub 포트폴리오를 연결해 쌓아가는 StudyDad의 작업 기록입니다.

성장과 기술/개발과 자동화

AWS 지표로 Datadog 가설을 검증하기

박세식 2026. 7. 24. 15:00

AI 장애 분석 포스트모템 시리즈 5/9

Datadog에서 나온 후보를 그대로 결론으로 옮기지 않았습니다. 그래서 AWS로 반박할 수 있는지 먼저 봤습니다. 이번 사례에서 Datadog은 세 가지 큰 단서를 보여줬습니다.

  • auth endpoint long span
  • /heartbeat timeout
  • MySQL pressure

이 세 가지를 AWS CLI로 ECS, ALB, CodeDeploy, RDS를 조회해서 다시 확인했습니다. 만약 AWS MCP를 붙였다면 같은 질문을 다른 도구로 던지는 문제였을 것입니다. 검증 방식은 도구와 별개였습니다.

검증할 질문을 고정했습니다

새 데이터를 많이 모으는 게 목적이 아니었습니다. 이번 단계에서는 Datadog 가설을 검증하는 데 집중했습니다. 그래서 질문은 다섯 가지로 좁혔습니다.

1. ECS가 왜 task를 교체했는가?
2. /heartbeat health check 설정은 무엇인가?
3. MySQL은 root cause인가, co-symptom인가?
4. 장애 직전 fresh deploy가 있었는가?
5. 왜 CPU maximum은 높았는데 autoscaling은 scale out하지 않았는가?

최근 배포는 원인 후보에서 낮아졌습니다

먼저 배포 이력을 봤습니다. 그 결과 incident 당시의 task definition/image는 약 17일 전에 배포된 버전이었습니다. 장애 직전 새 API 배포가 바로 원인이었다는 가설은 그 자리에서 약해졌습니다.

fresh deploy regression 가능성 낮음

장애가 나면 최근 배포를 먼저 의심하게 되지만, 배포와 장애의 시간대를 맞추지 않고 결론으로 옮기면 다른 후보를 놓칩니다.

/heartbeat가 증폭 요인이 된 자리

ALB target group health check는 이렇게 설정되어 있었습니다.

설정
path /heartbeat
interval 60초
timeout 30초
unhealthy threshold 5
healthy threshold 2

ALB target group은 /heartbeat를 60초마다 확인했고 timeout은 30초였습니다. Slow start는 0초라 새 target이 즉시 트래픽을 받았습니다.

한 task는 60초마다 health check를 받고 30초 안에 200을 주지 못하면 실패했습니다. 그리고 5번 연속 실패하면 unhealthy가 되고, 대략 5분의 지속 실패가 task replacement로 이어졌습니다. /heartbeat가 단순히 메모리 안에서 OK를 반환하는 endpoint라면 여기서 끝났을 문제였습니다.

그런데 이 서비스의 /heartbeat는 실제 DB ping을 했고, 그 ping은 주요 API와 같은 R2DBC pool 압력을 받았습니다. health check는 서비스가 살아 있는지 보는 별도 경로가 아니라, 장애가 난 경로에 같이 줄을 서는 요청이 되었습니다.

GET /v3/auth traffic surge
-> DB connection pool pressure
-> /heartbeat도 connection 대기
-> ALB 30초 timeout
-> ECS task unhealthy

slow start 0초가 replacement를 반복 루프로 만들었습니다

Target group의 slow start는 0초였습니다. 즉 새 task가 등록되자마자 full traffic을 받는다는 뜻이었습니다.

기존 task unhealthy
-> 새 task 시작
-> 새 task가 바로 full traffic 수신
-> 같은 auth surge와 pool pressure에 노출
-> 다시 health check 실패

그래서 task replacement가 회복이 아니라 반복 루프가 되었습니다.

CPU average만 본 autoscaling이 hotspot을 놓쳤습니다

CloudWatch에서 CPUUtilization Maximum은 특정 시점에 90% 이상까지 올랐습니다. 반면 CPUUtilization Average는 낮았습니다. 이 차이가 결정적이었습니다.

ECS autoscaling 정책은 task 하나의 maximum이 아니라 service 단위 average를 보고 있었습니다.

task A: 98%
task B: 2%
task C: 3%
task D: 4%

service average: 약 27%

이 상태에서는 task 하나가 거의 포화되어도 average 기반 scale-out alarm은 울리지 않았습니다. 실제 autoscaling 내역에도 자동 scale-out은 없었습니다. 장애 중 capacity가 늘어난 구간은 autoscaling이 아니라 운영자가 minimum capacity를 올린 결과였습니다.

오히려 average CPU가 낮게 보이면서 scale-in alarm이 실행된 흔적도 있었습니다. 이 지점은 비용 리포트에도 그대로 옮겨집니다. 평균만 보면 서비스는 여유 있어 보이지만 maximum을 보면 특정 task는 거의 포화 상태였습니다.

비용을 정리할 때도 service average와 task maximum, request count per target, replacement timing을 같이 봐야 했습니다.

ALB round-robin이 고장난 게 아니었습니다

교체 전 sampled span은 당시 healthy target 두 개에 거의 반반으로 분산되어 있었습니다. 따라서 "ALB round robin이 고장났다"기보다는 ALB가 선택할 수 있는 healthy target pool이 줄어든 상태에 가까웠습니다.

기존 task health check 실패
-> healthy target 수 감소
-> replacement task 등록
-> slow start 0초
-> 새 task가 즉시 live traffic과 backlog 수신
-> 새 task CPU spike

로드밸런서 알고리즘보다 target pool 상태와 slow start 설정을 보는 쪽이 맞았습니다. 실제로 stickiness는 꺼져 있었고, backend HTTP/2 multiplexing이나 cross-zone routing 문제를 뒷받침하는 근거도 약했습니다.

replacement task가 CPU를 올렸습니다

hot task에서 가장 많이 보인 resource는 여전히 GET /v3/auth였습니다. 따라서 별도의 batch나 health check 폭증이 CPU를 만든 게 아니라, auth hot path가 traffic surge와 replacement timing을 만나 CPU를 올린 쪽에 가까웠습니다. CPU를 쓴 후보들은 아래 항목이었습니다.

JWT parsing
cookie parsing / response cookie generation
session lookup 주변 처리
stuck request cancellation 처리
JSON serialization
logging / error handling

함수 단위 CPU 원인을 확정하려면 hot task에서 JFR 또는 async-profiler를 떠야 했습니다. 이번 분석에서 확인된 건 함수명이 아니라 패턴이었습니다.

원래 task는 오래 기다렸지만 CPU-bound는 아니었다.
새 replacement task는 등록 직후 auth traffic과 backlog를 받아 CPU-bound가 됐다.

incident 중 blue/green이 실패했습니다

장애 중간에 blue/green deployment가 시작됐습니다. 그러나 새 task set도 unhealthy로 판단되어 실패했고, CodeDeploy는 rollback했습니다. 두 가지가 드러났습니다.

특정 오래된 task 하나가 망가진 게 아니라, 새 task도 같은 traffic과 같은 code path를 만나면 실패했습니다. 또한 CodeDeploy deployment group에는 CloudWatch alarm gate가 연결되어 있지 않았습니다. 장애 중 수동 배포는 도움이 되기도 하지만, guard 없이 실행하면 혼선을 키웁니다.

ALB idle timeout과 deregistration delay의 불일치

ALB idle timeout은 1800초였습니다. 반면 target group deregistration delay는 30초였습니다. ALB는 긴 요청을 오래 둘 수 있지만 unhealthy target은 빠르게 drain되는 구조였습니다.

긴 hung request가 있는 상황에서 이 조합은 깔끔하지 않았습니다. Datadog에서 보인 AbortedException 계열 span은 이 drain/abort 흐름과 맞아떨어졌습니다.

MySQL은 co-symptom이었습니다

Datadog Watchdog은 MySQL도 같이 열었습니다. 실제로 AWS RDS metric에서 MySQL pressure는 실제로 관측됐습니다.

  • Select throughput 증가
  • RowLockTime 증가
  • active transaction 증가
  • reader connection 증가
  • deadlock 소수 발생

다만 root cause는 아니었습니다. 반대 근거가 더 강했습니다.

  • slow MySQL span은 초 단위였지만 application span은 수백 초~20분이었습니다.
  • 가장 긴 application trace에는 MySQL child span이 없었습니다.
  • writer/reader CPU는 상승했지만 saturation은 아니었습니다.
  • buffer cache hit ratio와 memory는 안정적이었습니다.
  • control DB는 영향을 받지 않았습니다.
  • DB metric은 application task churn보다 먼저 안정화됐습니다.

분류는 이렇게 정리했습니다.

MySQL = co-symptom + minor contributor, not root cause

AWS 조사가 좁힌 결론

Datadog만 봤을 때:

auth path hang
heartbeat timeout
MySQL pressure
Old Gen increase

AWS까지 본 뒤:

auth traffic surge가 먼저 있었다.
ECS/ALB health check 설정이 replacement loop를 만들었다.
CPU max spike는 replacement task가 갑자기 full traffic을 받은 결과로 보는 편이 더 자연스러웠다.
average-only autoscaling 정책은 이 hotspot을 보지 못했다.
MySQL은 같이 힘들었지만 root cause는 아니었다.
fresh deploy regression 가능성은 낮았다.

AI가 잘한 일은 한 화면을 보고 원인을 맞힌 것이 아니었습니다. Datadog에서 나온 후보를 AWS 원천 지표로 반박하거나 낮추고, 남은 후보를 포스트모템 문장으로 정리한 쪽에 가까웠습니다.

비용 리포트도 같은 검증이 필요했습니다

Cursor에서 Datadog MCP로 RDS와 ECS Fargate 사용량을 비용 관점으로 정리시켜봤을 때도 구조는 같았습니다. 먼저 질문을 정하고, AI가 운영 데이터를 조회하고, 사람이 수치와 해석을 검토했습니다. 이 흐름에서 유효했던 질문은 아래와 같았습니다.

1. RDS 사용량은 어느 시간대와 워크로드에서 증가했는가?
2. ECS Fargate vCPU/memory 사용량은 서비스별로 어떻게 분포했는가?
3. peak 사용량과 평균 사용량의 차이가 비용 낭비인지, 장애 대응 여유분인지 구분할 수 있는가?
4. scale-out이 비용 증가로 이어진 구간과 장애 완화에 기여한 구간을 분리할 수 있는가?

AI가 잘한 일은 "비용 줄일 방법 알려줘"에 답한 것이 아니었습니다. 오히려 사용량과 정책과 이벤트를 보고서 구조로 묶어 사람이 검토할 수 있게 만든 쪽에 가까웠습니다. 운영자가 해야 할 일은 그 보고서의 수치 출처와 해석을 확인하는 일입니다.

다음 조사부터 미리 정리하는 것

이번 조사 이후 아래는 재발 시간을 줄이려고 문서에 남겨뒀습니다.

  • health check가 어떤 code path와 resource를 공유하는지 확인합니다.
  • replacement task의 slow start 설정을 확인합니다.
  • autoscaling 정책이 service average만 보는지 task maximum도 보는지 확인합니다.
  • Datadog Watchdog이 DB를 같이 열었을 때 원인 확정 전에 AWS 지표로 반박합니다.

다음 편에서는 이 시점에 GitLab 배포 이력을 붙여 어떤 배포가 강한 후보였고 어떤 배포가 약한 후보였는지 정리합니다.

Next Step 이 글은 StudyDad 작업 루프의 한 조각입니다.

글에서 정리한 생각은 GitHub의 코드와 포트폴리오로 이어지고, 일부는 FamBlend 같은 제품 실험으로 확장됩니다.