Zettelkasten

Global Accelerator는 fail open 때 Weight 0 엔드포인트로도 트래픽을 보낸다

·수정 1회

요약

  • Global Accelerator의 endpoint weight 0은 평상시 라우팅 비중을 0으로 만들 뿐, 비활성화가 아니다. 엔드포인트 그룹에 weight가 0보다 큰 건강한 엔드포인트가 하나도 없으면 GA는 그룹 안의 아무 엔드포인트로나 트래픽을 보낸다. 문서에 fail open이라고 적혀 있다.
  • 그래서 타깃이 0개인 죽은 로드밸런서를 "weight 0으로 꺼놨으니 안전하다"며 엔드포인트 그룹에 남겨두면, 정상 엔드포인트가 헬스체크에 잠깐 실패하는 순간마다 실 트래픽이 그리로 흘러 ALB가 만든 503을 받는다.
  • 이미 맺힌 TCP 연결은 옮겨지지 않아서, 실패가 수십 초 단위로 뭉쳐 터진다.

본문

문서에 적힌 두 가지

첫째, weight 0은 보장이 아니다.

"Be aware that even when you've set endpoint weights in your accelerator, in specific, limited scenarios, Global Accelerator overrides those weights, to help ensure availability. ... For example, with accelerators where the client IP address is preserved, Global Accelerator might need to override an endpoint weight setting to help avoid connection collisions."

둘째, 건강한 엔드포인트가 없으면 아무 데나 보낸다.

"If there are no healthy endpoints in an endpoint group that have a weight greater than zero, Global Accelerator tries to fail over to a healthy endpoint with a weight greater than zero in another endpoint group. ... If Global Accelerator doesn't find a healthy endpoint with a weight greater than zero after trying the three closest endpoint groups, it routes traffic to a random endpoint in the endpoint group that is closest to the client. That is, it fails open."

엔드포인트 그룹이 하나면 fail open이 곧바로 걸린다

failover가 찾는 대상은 다른 endpoint group, 즉 다른 리전이다. 리전을 하나만 쓰면 찾아갈 곳이 없어서 실패 즉시 fail open으로 넘어간다. 단일 리전 구성일수록 죽은 엔드포인트를 남겨두는 비용이 크다.

관찰된 증상

단일 리전 엔드포인트 그룹에 엔드포인트가 둘 있었다. 하나는 운영 중인 ALB(weight 100, healthy), 다른 하나는 타깃 그룹에 등록된 인스턴스가 0개인 옛 ALB(weight 0, unhealthy)였다.

  • 죽은 ALB가 하루 1,5005,000건의 503을 만들고 있었다. 3090분 간격으로 2~3분씩 몰렸다.
  • 액세스 로그를 켜서 보니 그 요청들의 Host 헤더가 서비스 도메인이었고, User-Agent도 실제 모바일 앱 클라이언트였다. 스캐너가 아니라 실 사용자 트래픽이다.
  • 앱 로그와 APM에는 흔적이 없다. ALB가 스스로 만든 응답이라 앱을 거치지 않는다 — APM에는 로드밸런서가 대신 만든 5xx가 안 잡힌다.
  • 백엔드 서비스끼리의 내부 API 호출도 같이 맞았다. JSON을 기대하던 파서가 ALB 에러 페이지를 만나 Unexpected token '<'로 터졌다. ALB가 만드는 에러 페이지 본문은 <html>\r\n<head><title>503 Service Temporarily Unavailable</title></head>로 시작한다(CRLF).

확진 방법 — HealthyEndpointCount가 0으로 떨어진 분을 찾는다

AWS/GlobalAccelerator 네임스페이스에 HealthyEndpointCount와 UnhealthyEndpointCount가 있다. 두 지표 모두 Accelerator, Listener, EndpointGroup 조합으로 볼 수 있고, EndpointGroup 차원 값은 리전 이름이다. 통계는 Minimum이 유용하다.

aws cloudwatch get-metric-statistics --namespace AWS/GlobalAccelerator \
  --metric-name HealthyEndpointCount \
  --dimensions Name=Accelerator,Value=<accelerator-id> \
               Name=Listener,Value=<listener-id> \
               Name=EndpointGroup,Value=ap-northeast-2 \
  --region us-west-2 --period 60 --statistics Minimum \
  --start-time <from> --end-time <to>

GA 지표는 리전을 어디에 만들었든 us-west-2에서만 보인다. 문서에 "You must view CloudWatch metrics and logs for Global Accelerator in the US West (Oregon) Region"이라고 못박혀 있다. 다른 리전으로 조회하면 에러 없이 빈 결과가 나오므로, "지표가 없다"로 오해하기 쉽다.

위 사례에서 장애가 난 두 분(minute)에 정확히 HealthyEndpointCount가 0, UnhealthyEndpointCount가 2로 찍혔다. 나머지 전 구간은 1/1이었다. 다만 1분 해상도라 더 짧은 순간은 못 잡는다 — 지표가 0으로 안 떨어졌는데도 발생한 버스트가 있었고, 그건 위 첫 번째 인용(클라이언트 IP 보존 시 weight 무시)에 해당할 수 있으나 확인하지 못했다.

왜 몇 초 동안 뭉쳐서 실패하나

fail open이 걸린 순간에 새로 맺힌 TCP 연결이 죽은 엔드포인트에 붙으면, 그 연결은 끊길 때까지 계속 그쪽으로 간다.

"established active connections are not moved. They continue to route to the zero weight Region until the connection is reset by the client or the server, or until the client makes a new connection."

keep-alive를 쓰는 HTTP 클라이언트가 특히 이렇다. 실제로 출발 포트 하나가 10초 동안 네 번 연속 503을 받은 게 액세스 로그에 남았다. 라우팅 복구는 문서상 30초쯤이라는데, 연결이 살아 있는 동안은 계속 실패하니 체감 장애는 그보다 길다.

그래서 지켜야 할 것

  • 엔드포인트 그룹에 죽은 엔드포인트를 남기지 않는다. weight 0은 트래픽 비중을 점진적으로 옮길 때 쓰는 조절 손잡이지 스위치가 아니다.
  • 안 쓰는 로드밸런서는 GA 엔드포인트에서 빼고 리소스 자체를 삭제한다. GA에서 빼기만 하면 공인 IP와 DNS 이름이 남아 스캔 대상으로 계속 노출된다.
  • 죽은 엔드포인트가 애초에 없으면 fail open이 걸려도 살아 있는 엔드포인트로 가므로 아무 일도 일어나지 않는다. 이 함정은 GA의 동작이 아니라 구성이 만든다.

관련 노트

참고