Fargate는 물리 CPU 세대를 고를 수 없어 같은 태스크 정의라도 성능이 1.5~2배 갈린다
요약
- Fargate에서 고를 수 있는 건 CPU 아키텍처(X86_64 / ARM64)와 vCPU 개수뿐이다. 어느 세대 칩에 뜰지는 못 고른다.
- 같은 이미지, 같은 vCPU로 띄운 태스크 8대의 물리 CPU가 2019년 세대부터 2025년 세대까지 섞여 있었다. 응답시간이 느린 순서와 CPU 세대가 오래된 순서가 정확히 같았다.
- 태스크를 다시 띄워도 세대는 뽑기다. 없앨 수 없으니 로드밸런싱으로 우회하는 수밖에 없다.
본문
증상
로드밸런서가 round robin이라 태스크마다 요청 수가 거의 똑같은데(편차 0.04%), 평균 응답시간이 1.5~2배씩 차이 났다. 느린 태스크는 하루가 지나도 회복하지 않았다.
워크로드 차이가 아니라는 것부터 확인
"저 태스크가 무거운 요청을 더 받은 것 아니냐"를 먼저 죽여야 한다. 네 가지를 봤다.
- 엔드포인트를 하나로 고정해도 차이가 그대로였다. 주요 엔드포인트 5개 전부에서 1.9~2.3배.
- 요청당 DB 호출 수가 같았다. 전체 12.20회 대 12.26회, 특정 엔드포인트만 보면 41.03회 대 41.01회. 같은 쿼리를 같은 횟수 던지는데 시간만 2배였다.
- 같은 DB, 같은 Redis를 보는데 태스크가 잰 Redis 지연이 3.8배였다(5.65ms 대 1.49ms). 1ms도 안 걸리는 명령이라 서버 쪽 문제일 수가 없다.
- 같은 일에 CPU를 더 썼다. 1,000행 파싱에 쓴 CPU 시간이 46.6ms 대 19.4ms로 2.4배. 느린 쪽이 한 번에 파싱하는 행 수는 오히려 적었다. 대기 시간이 아니라 CPU 시간이라 큐잉으로는 설명이 안 된다.
여기까지 오면 앱·쿼리·인덱스·데이터량이 다 배제되고 하드웨어만 남는다.
확인 방법
enable_execute_command가 켜져 있으면 컨테이너 안에서 바로 읽을 수 있다.
aws ecs execute-command --cluster <cluster> --task <task-arn> \
--container <container> --interactive \
--command "grep -m1 'model name' /proc/cpuinfo"
결과
운영 중인 태스크 8대를 전부 찍었다. DB 호출 한 번에 걸린 시간 순으로 정렬한 것이다.
| 태스크 | DB 호출당 | 물리 CPU |
|---|---|---|
| 1 | 2.71ms | Xeon 6975P-C (Xeon 6 / Granite Rapids 계열) |
| 2 | 2.86ms | Xeon Platinum 8488C (4세대 / Sapphire Rapids) |
| 3 | 3.03ms | 8488C |
| 4 | 3.36ms | 8488C |
| 5 | 3.36ms | 8488C |
| 6 | 약 3.9ms | Xeon Platinum 8259CL (2세대 / Cascade Lake) |
| 7 | 4.08ms | 8259CL |
| 8 | 4.46ms | 8259CL |
세 세대가 겹치는 구간 없이 세 덩어리로 갈린다. 세대별 평균은 2.71ms, 3.15ms, 약 4.15ms다.
8대 중 3대, 37.5%가 가장 오래된 세대였다. 별개로 8일치 로그에서 태스크 생애 평균을 뽑았을 때도 "빠른 무리 5687ms, 느린 무리 100150ms"로 갈리고 느린 쪽이 약 1/3이었다. 서로 다른 방법으로 같은 비율이 나왔다.
왜 이만큼 차이 나나
CPython은 분기가 많고 포인터를 자주 따라가서 단일 스레드 성능과 캐시에 민감하다. 2019년 칩과 2025년 칩 사이라면 1.5~2배는 이상하지 않다.
부하가 오르면 더 벌어진다
배수가 고정이 아니다. 같은 태스크의 시간대별 비율(가장 빠른 태스크 대비)이 한산할 때 1.26배, 낮에 3.04배, 저녁 피크에 6.44배였다. 오래된 칩은 여유가 적어서 부하가 오를 때 먼저 밀린다.
되먹임 고리는 아니다. 요청이 밀려서 더 느려지는 게 아니라, 같은 요청량에서도 구세대 칩이 먼저 힘에 부치는 것이다.
대응
- 태스크를 다시 띄우는 건 뽑기다. 약 1/3 확률로 또 구세대에 뜬다. 근본 해결이 아니다.
- 로드밸런싱으로 우회한다. 처리 능력이 다른 것이므로 요청 수를 똑같이 나누면 안 된다. 처리 속도가 제각각인 서버에는 round robin 대신 least outstanding requests를 쓴다
- ARM64(Graviton) 검토. Fargate ARM64 쪽은 x86보다 세대 폭이 좁아서 편차가 줄어들 여지가 있다. 이미지 빌드부터 바꿔야 하는 별도 과제다.
출처 신뢰도 메모
이 커스텀 SKU들은 Intel ARK에 페이지가 없다. 8259CL과 8488C는 AWS 인스턴스 페이지가 모델번호와 코드네임을 같이 적어줘서 확인된다. 6975P-C는 AWS가 "AWS 전용 커스텀 Xeon 6"라고만 하고 모델번호를 공개하지 않아, Granite Rapids 분류는 2차 자료(Phoronix 벤치마크)에만 근거한다.
참고로 모델명 끝의 C가 AWS 전용 표기는 아니다. 8480C처럼 ARK에 공개 등재된 SKU에도 붙는다.
관련 노트
- Fargate에서 서브넷을 한 AZ로 묶어도 CPU 세대는 못 고른다
- 처리 속도가 제각각인 서버에는 round robin 대신 least outstanding requests를 쓴다
- 동시 처리 요청 수를 응답시간으로 계산했다면 응답시간을 두 번 센 것이다
- ECS Exec SSM Agent는 readonlyRootFilesystem에서 동작하지 않는다
- N+1 쿼리 해결은 쿼리당 고정비용만 회수하고 행당 파싱 비용은 줄이지 않는다
- Django 느린 API의 CPU 병목은 DB가 아니라 직렬화 계층인 경우가 많다
참고
- https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html
- https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html
- https://aws.amazon.com/ec2/instance-types/storage-optimized/
- https://aws.amazon.com/ec2/instance-types/compute-optimized/
- https://www.phoronix.com/review/aws-m8a-m8g-m8i-benchmarks
- https://scalefactory.com/blog/2021/05/12/what-powers-ecs-fargate/