RUNNING 이 되었다고 해서 바로 정상 서빙 상태가 되는 것은 아니다.healthCheckGracePeriodSeconds 를 잡으면, ALB 의 연속 성공 판정이 끝나기 전에 태스크가 종료될 수 있다.최근에 스테이징 서버가 특별한 에러 없이 한 번 내려갔다가, 다시 재배포되는 일이 있었다.
처음에는 “서버가 실제로 죽었나?” 싶었는데, 로그를 따라가보니 앱 자체가 비정상 종료된 것은 아니었다.
문제는 ECS 와 ALB 의 헬스 체크 타이밍, 그리고 healthCheckGracePeriodSeconds 설정에 있었다.
이번 글에서는 그때 확인했던 설정들과, 왜 서버가 정상 기동 후에도 종료 대상으로 판단되었는지를 정리해보려고 한다.
문제가 발생한 환경은 대략 이런 구조였다.
ECS 는 Task 단위로 동작한다.
그리고 이 Task 가 살아나는 과정과, "정상적으로 트래픽을 받을 수 있는 상태"가 되는 과정은 생각보다 다르다.
ECS agent 는 태스크가 생성될 때 내부적으로 여러 상태를 거친다.
Provisioning -> Pending -> Activating -> Running
각 단계는 대략 아래 의미를 가진다.
반대로 작업이 정상 완료되지 못했다면 아래처럼 역순으로 내려간다.
Running -> Deactivating -> Stopping -> Deprovisioning -> Stopped
여기서 중요했던 포인트는 RUNNING 이 곧바로 정상 서빙 상태는 아니라는 점 이다.
애플리케이션 프로세스가 떴더라도, 실제로는 뒤이어 설명할 헬스 체크들을 통과해야 한다.
ECS Task Definition 에는 아래와 같은 컨테이너 헬스 체크가 있었다.
"healthCheck": {
"command": [
"CMD-SHELL",
"wget -q -O /dev/null http://localhost:8080/api/health || exit 1"
],
"interval": 30,
"timeout": 5,
"retries": 3
}각 옵션은 다음 의미다.
interval: 얼마나 자주 검사하는지timeout: 몇 초 안에 성공해야 하는지retries: 몇 번 연속 실패하면 실패로 볼지이 검사는 컨테이너 내부에서 실행된다.
즉, localhost:8080 기준으로 애플리케이션이 떠 있는지를 확인하는 셈이다.
이 부분을 정리하면서 다시 봤던 포인트는 다음과 같다.
startPeriod 가 없으면, 초기 부팅 중의 실패도 그대로 누적 관찰 대상이 된다.다음은 ALB Target Group 의 헬스 체크 설정이다.
{
"TargetGroupName": "staging",
"TargetType": "instance",
"Protocol": "HTTP",
"Port": 8080,
"ProtocolVersion": "HTTP1",
"HealthCheckProtocol": "HTTP",
"HealthCheckPath": "/api/health",
"HealthCheckPort": "traffic-port",
"HealthCheckIntervalSeconds": 30,
"HealthCheckTimeoutSeconds": 5,
"HealthyThresholdCount": 3,
"UnhealthyThresholdCount": 4,
"Matcher": {
"HttpCode": "200"
}
}이건 ECS 태스크 단위가 아니라, ALB 의 Target Group 단위 설정이다.
즉 컨테이너 내부가 아니라 컨테이너 밖에서 들어오는 요청이라고 봐야 한다.
여기서 기억해야 할 점은 다음이다.
TargetType 이 instance 면, 타깃 그룹은 인스턴스의 트래픽 포트로 검사한다.HealthCheckPort 가 traffic-port 면 실제 서비스가 바인딩된 포트로 요청이 간다.HealthyThresholdCount = 3 이므로 연속 3회 성공해야 healthy 가 된다.UnhealthyThresholdCount = 4 이므로 연속 4회 실패하면 unhealthy 로 본다.예를 들어 트래픽 포트가 28080 이라면, ALB 는 해당 포트의 /api/health 로 30초 간격 요청을 보내며 상태를 확인한다.
그리고 이 ALB 검사는 Container Health Check 와 병렬로 동작한다.
이게 이번 이슈의 핵심이었다.
ECS 서비스에는 healthCheckGracePeriodSeconds 라는 설정이 있다.
새로운 태스크를 띄웠을 때, 헬스 체크가 바로 실패하더라도 일정 시간 동안은 ECS 가 기다려주는 유예 시간이다.
로드밸런서를 사용하는 서비스라면, ECS 는 단순히 프로세스가 떠 있는지만 보는 게 아니라 다음을 함께 본다.
우리 스테이징 ECS 의 healthCheckGracePeriodSeconds 는 150s 로 설정되어 있었다.
문제는 ALB Target Health Check 가 앱 준비 완료 시점 기준으로 시작되는 게 아니라, Task 가 ACTIVATING 상태가 되는 시점부터 시작된다는 점 이다.
즉, 체크 타이밍은 대략 이렇게 될 수 있다.
10 -> 40 -> 70 -> 10025 -> 55 -> 85 -> 115ALB 가 healthy 로 판단하려면 이 중에서 연속 3번 성공이 필요하다.
가정해보자.
82초 걸린다.그러면 앱이 82초에 준비되더라도, 그 전까지의 ALB 체크는 모두 실패한다.
예를 들어 체크 타이밍이 아래와 같았다면:
40 -> 70 -> 100 -> 130 -> 160
즉, 정상 판정은 160초가 되어야 끝난다.
하지만 ECS 의 grace period 는 150초 였다.
그래서 앱은 사실상 정상적으로 올라왔음에도 불구하고,
ALB 의 healthy 판정이 확정되기 전에 ECS 가 "이 태스크는 유예 시간 안에 정상화되지 못했다"고 보고 종료 대상으로 판단한 것이다.
결국 앱의 실제 부팅 시간만 보면 안 되고,
ALB 가 연속 성공을 채우는 데 필요한 시간까지 포함해서 계산해야 한다.
당시 로그는 대략 이런 흐름이었다.
여기서 ALB 헬스 체크 실패로 서버 종료 결정 -> 서버 종료 요청 사이에 약 1분이 비는 이유도 처음엔 조금 헷갈렸다.
이 구간은 ALB 의 deregistration delay, 즉 draining 시간 때문이었다.
참고: Application Load Balancer 대상 그룹 속성 편집
타깃은 이 시간이 끝날 때까지 계속 draining 상태로 남아 있고,
그 후에 서버에 실제 종료 요청이 들어가며 종료가 처리된다.
결론은 단순했다.
healthCheckGracePeriodSeconds 시간을 더 넉넉하게 잡아야 한다.
정확히는 "애플리케이션 부팅 시간"만 보고 설정하면 안 되고,
ALB 헬스 체크가 healthy 로 확정되는 시간까지 포함해서 계산해야 한다.
내 기준에서는 아래처럼 이해하는 게 가장 명확했다.
ACTIVATING 시점부터 시작된다.healthCheckGracePeriodSeconds 는 이 합보다 더 넉넉하게 잡아야 한다.이 이슈는 서버가 죽었다기보다, 정상 서버로 인정받는 타이밍을 충분히 확보하지 못해서 생긴 문제에 가까웠다.
이번 기회에 ECS 태스크의 상태와 헬스체크에 대해 더 자세히 알아볼 수 있어서 좋았다. 자바 코드만 짜는 개발이 아닌, 인프라에도 관심을 가지도록 노력해야겠다.