22. 연속적 장애 다루기
27 Aug 2023 | Site Reliability Engineering
22. 연속적 장애 다루기
처음에 성공하지 못하면 그 이상으로 늦어지게 된다 - 댄 샌들러 (Google SW Engineer)
왜 사람들은 조금만 더 신경을 쓰면 된다는 사실을 늘 잊곤 하는가? - 에이드 오시니에 (구글 개발자 전도사)
- 연속적 장애(cascading failure)는 정상적인 것처럼 보여지는 응답때문에 시간이 지나면서 장애가 계속해서 가중되는 현상
- 전체 시스템의 일부에서 장애가 발생했을 때 주로 나타나며, 이로 인해 시스템의 다른 부분에서 장애가 발생할 가능성도 늘어나게 됨
연속적 장애의 원인과 그 대책
서버 과부하
- 연속적 장애를 유발하는 가장 일반적인 원인은 과부하다
- 대부분의 연속적 장애는 서버의 과부하가 직접적인 원인이거나 혹은 이로 인해 확장된 또는 변형된 장애들이다
- 특정 클러스터에서 장애가 발생하면, 다른 클러스터에 가용량 이상의 요청이 전달될 수 있다.
- 이로인해 자원이 부족하게 되어 충돌이 방생하거나 지연응답 혹은 오동작이 발생하게 된다
- 이처럼 성공적으로 완료된 작업의 수가 감소하면 그 여파는 도메인의 다른 시스템으로 퍼져나가 잠재적으로는 전체 시스템에 영향을 미칠 수 있다.
- 예를 들어 한 클러스터에서 과부하가 발생하면 그 서버들이 충돌로 인해 강제로 종료 -> 로드밸런싱 컨트롤러가 요청을 다른 클러 스터에 보냄 -> 그 쪽의 서버들에서 과부하가 발생 -> 서비스 전체에 걸치 과부하 장애 발생
- 위 상황은 그다지 오래 걸리지 않는다 (~분)
자원의 부족
- 서버의 어떤 자원이 부족해지는지, 그리고 서버가 어떻게 구성되어 있는지에 따라 자원의 부족은 서버의 비효율적 동작 혹은 충돌로 인한 서버의 강제 종료를 유발할 수 있고 로드밸런서는 그 즉시 자원 부족이라는 이슈를 다른 서버들로 퍼져나가게 함
CPU 자원이 부족한 경우
- 주로 모든 요청의 처리가 느려진다. 다음과 같은 부차적 현상 발생가능
처리중인 요청 수의 증가
- 이는 메모리, 활성 스ㄹ드 수, 파일 디스크립터 수 및 백엔드 자원 등 거의 모든 자원에 영향을 미침
비정상적으로 증가하는 큐의 크기
- 지연응답이 증가하고, 큐가 더 많은 메모리를 소비하게 됨
스레드 기아 (thread starvation)
- 스레드가 잠금을 기다리느라 더 이상 처리되지 않고 대시 상태가 되면 건강 상태 체크 종단점이 제시간에 처리되지 않아 실패할 가능성이 있다
CPU 혹은 요청 기아
- 서버 내부의 와치독이 서버가 더 이상 처리를 못하고 있다는 것을 탐지하면 CPU 기아 현상으로 인해 서버에서 충돌이 발생하기도 하고 와치독 이벤트가 원격에서 발생하고 요청 큐를 통해 처리되는 경우에는 요청 기아가 발생하기도 한다.
RPC 시간 초과
- RPC 응답이 늦어져 클라이언트에서 시간초과 발생가능
- 서버가 수행했던 작업은 아무런 소용이 없어지고 클라이언트는 RPC 요청을 재시도하여 더 많은 과부하 발생
CPU 캐시 이점 감소
- 더 많은 CPU가 사용될수록 더 많은 코어에서 누수가 발생할 가능성이 커지고 그 결과 로컬 캐시 및 CPU의 효율성이 떨어지게 됨
메모리
태스크 종료
- 예를 들어 container manager가 가용한 자원의 한계 혹은 애플리케이션의 특정한 충돌로 인해 태스크를 거부하면 이 태스크는 강제로 종료됨
자바의 GC 수행률 증가로 인한 CPU 사용률 증가
- GC가 비정상적으로 동작하면 가용한 CPU 자원이 줄어들고 이로 인해 요청의 처리가 느려지면서 RAM 사용량이 증가하고 -> GC가 더 자주돌고 ….
- 죽음의 GC 소용돌이 (GC death spiral)
캐시 활용률의 감소
- 가용한 RAM이 부족해지면 애플리케이션 수준의 캐시 활용률이 감소하고 그 결과 더 많은 RPC 요청이 백엔드로 전달되어 백엔드에 과부하를 초래
스레드
- 스레드 기아는 직접적으로 에러를 발생시키거나 혹은 건강 상태 점검의 실패를 야기한다.
- 서버가 필요한 만큼 스레드를 생성하면 스레드 과부하로 인해 너무 많은 RAM을 소비하게 된다
- 프로세스 ID를 모두 소비하게 될 수도 있음
파일 서술사
- fd가 부족해지면 네트워크 연결의 초기화가 불가능해져 건강 상태 점검이 실패하게 된
자원간의 의존성
- 자원의 부족은 또 다른 자원에 영향을 미침
- 과부하로 고통받고 있는 서비스는 부차적 증상을 유발하는데, 이것이 마치 근본 원인처럼 보여 디버깅이 더욱 어려워 짐
- 307p 참고
서비스 이용 불가
- 자원의 부족은 서버의 충돌을 유발
- 이 문제는 마치 눈덩이 처럼 불어나는 경향이 있어서 금세 모든 서버들에서 충돌이 발생
- 행여 서버가 복구된다 하더라도 복구가 되자마자 밀려있는 요청의 폭탄을 얻어맞기 깨문에 이 상활을 탈출하기란 상당히 어렵다.
서버 과부하 방지하기
서버 수용량 한계에 대한 부하 테스트 및 과부하 상태에서의 실패에 대한 테스트
- 실제 환경에서 테스트하지 않으면 정확히 어떤 자원이 부족해지는지, 그리고 자원의 부족이 어떤 영향을 미치는지를 예측하기가 매우 어렵다
- python Locust와 같은 부하테스트 프레임워크 활용 가능
경감된 응답 제공하기
- 품질은 낮지만 더 수월하게 연산할 수 있는 결과를 사용자에게 제공
- 311 페이지 “부하 제한과 적절한 퇴보” 참고
과부하 상태에서 요청을 거부하도록 서비스를 구현하기
- 서버는 과부하 및 충돌로부터 스스로를 보호할 수 있어야 함
- 최대한 빠르면서도 비용이 적게 드는 방법으로 실패를 처리해야 함
고수준의 시스템들이 서버에 과부하를 유발하지 않고 요청을 거부하도록 구현하기
- 리버스 프록시는 IP 주소 같은 조건들을 이용하여 요청의 전체 양을 제한 함으로써 DoS와 같은 공격의 영향을 최소화할 수 있음
- 로드밸런서는 서비스가 전체적인 과부하 상태일 때는 요청을 스스로 차단한다. 차단 방식은 무조건적 차단, 선택적 차단 등 여러방법이 있음
- 개별 작업들은 로드밸런서가 보내오는 요청의 변화에 의해 서버에 갑작스럽게 많은 요청이 몰리지 않도록 이를 제한한다.
수용량 계획을 실행하기
- 수용량 계획은 서비스가 어느 수준의 부하에서 장애가 발생하는지를 판단하기 위해 반드시 성능 테스트와 병행되어야 함
- N+2 공식
- 수용량 계획은 연속적 장애의 발생 가능성을 억제할 수는 있지만, 연속적 장애로부터 시스템을 보호하는 데는 크게 도움이 되지 않는다.
큐 관리하기
- 각 요청을 개별 스레드에서 처리하는 대부분의 서버는 요청을 처리하기 위해 스레드 풀 앞에 큐를 배치
- 대부분 이 큐가 가득 차면 서버는 새로운 요청을 거부
- 요청을 큐에 적재하면 더 많은 메모리를 소비, 지연응답 또한 증가
- 폭발적인 부하가 발생할 수 있는 시스템은 현재 사용 중인 스레드의 수, 각 요청의 처리 시간, 그리고 과부하의 크기와 빈도 등에 기초해서 큐의 크기를 결정하는 것이 좋다.
부하 제한과 적절한 퇴보
- 서버가 과부하 상태에 도달하게 되면 부하 제한 (load shedding)을 통해 유입되는 트래픽을 감소시켜 어느 정도의 부하를 덜어낼 수 있다.
- 서버의 메모리 부족이나 건강 상태 점검 실패, 지연응답의 극단적 증가 및 기타 과부하에 관련된 증상들이 서버에서 발생하는 것을 방지하고 서버가 최대한 자신의 작업을 계속해서 수행할 수 있도록 유지하는 것
- 부하는 배분하는 가장 직관적인 방법은 CPU, 메모리 혹은 큐의 길이에 따라 태스크별로 제한을 두는 것
- 일정 수 이상의 클라이언트 요청이 유입되면 그 이후의 요청에 대해서는 HTTP 503에러를 리턴
- FIFO로 동작하는 큐를 LIFO로 변경하면 처리할 필요가 없는 요청들을 제거함으로써 부하를 줄일 수 있음 (RPC 제한 시간 초과 요청)
- PRC 마감기한을 전체 스택에 전파하는 방법을 함께 활용하면 더욱 큰 효과를 발휘
- 어 깔끔한 방법은 처리를 거부할 작업과 더 중요하고 우선순위가 높은 요청을 선택할 때 해당 요쳥의 클라이언트를 함께 고려하는 방법
- 적절한 퇴보 (graceful deradation)는 부하 제한의 개념에서 한 걸을 더 나아가 실행해야 할 작업의 양을 감소시키는 방법
- 일부러 응답의 품질을 경감시켜 작업의 양이나 처리에 필요한 시간의 양을 현저히 줄일 수 있다
- 다음의 고려가 필요
- 어떤지표를 활용해서 부하 제한이나 적절한 퇴보의 적용 여부를 결정할 것인가?
- 서버가 퇴보 모드로 동작하게 되면 어떤 조치를 취해야 하는가?
- 부하 제한이나 적절한 퇴보는 어느 계층에서 구현해야 하는가?
- 선택할 수 있는 옵션을 평가하고 배포하는 동안에는 다음과 같은 내용들을 고려하자
- 적절한 퇴보는 너무 자주 발생해서는 안된다. 대부분 수용량 계획이 실패했거나 혹은 예상하지 못했던 부하의 이동이 발생하는 경우에만 적용되어야 함
- 어떤 코드 경로가 한 번도 사용된 적이 없다면 이 코드 경로는 동작하지 않을 것. 안정적인 상태에서 운영 중일 때는 적절한 퇴보 모드가 사용될 일이 없지만, 적적한 퇴보 모드 운영 경험을 쌓을 수 없다
- 이 모드에서 동작하는 서버가 너무 많아지지는 않는지 적절히 모니터링하고 경고 발송
- 부하 제한과 적절한 퇴보를 구현하는 로직이 복잡해지면 그 자체에 문제가 발생할 수 있다.
재시도
- 백엔드로 약간 어설프게 재시도를 요청하는 프런트엔드 코드가 있다고 가정해보자
- 이 재시도는 어떤 요청이 실패앴을 때 이루어지며 논리 요청당 백엔드 RPC는 20번으로 제한
func exampleRpcCall(client pb.ExampleClient, request pb.Request) *pb.Response{
// RPC 타임 5초
opts := grpc.WithTimeout(5 * time.Second)
// 최대 20번까지 RPC 호출 시도
attempts := 20
for attempts > 0 {
conn. err := grpc.Dial(*serverAddr, opts...)
if err != nil {
// 연결 설정 시 에러가 발생하면 재시도
attempts--
continue
}
defer conn.Close()
// 클라이언트 stub을 생성하고 RPC호출 시도
client := pb.NewBackendClient(conn)
response, err := client.MakeRequest(context.Background, request)
if err != nil {
// 호출 실패 시 재시도
attempts--
continue
}
return response
}
grpclog.Fatalf("재시도 횟수 초과")
}
- 이 시스템은 다음과 같은 과정을 거쳐 연속적 장애를 일으킬 수 있다
- 백엔드 한계가 태스크당 10,000 QPS이며, 적절한 최보가 적용되면 그 이후의 모든 요청을 거부된다고 가정
- 프런트엔드가
MakeRequest
함수를 10,100 QPS의 비율로 일정하게 호출하면 백엔드에 100 QPS의 과부하가 발생해서 백엔드가 요청을 거부하게 됨
MakeRequest
함수는 매 1,000 ms 마다 실패한 100 QPS의 요청을 재시도하고 이들이 성공적으로 처리됨. 이는 100 QPS의 추가 과부하가 되어 백엔드에 200 QPS의 과부하 발생
- 그 결과 재시도의 비율이 증가: 그래서 처음 시도에 비해 더 적은 수의 요청이 처리되고 결국 백엔드에 전달된 요청의 일부만이 처리됨
- 백엔드 태스크가 부하의 증가로 인해 더이상의 요청을 처리할 수 없게 되면 계속되는 요청과 재시도 때문에 결국 충돌이 발생 -> 이 충돌로 인해 처리되지 못한 요청들은 나머지 백엔드 태스크들이 감당해야 하는 부하가 되어 이 태스크들 역시 과부하 상태에 놓인다.
- 자동 재시도를 수행할 때는 다음과 같은 사항들을 고려해야 한다
- 대부분의 백엔드 보호전략은 “서버 과부하 방지하기” 절에서 설명. 특히 시스템에 대한 테스트는 문제를 조명하고 적절한 퇴보를 통해 백엔드에 대한 재시도의 영향을 줄일 수 있음
- 재시도를 실행할 때는 항상 임의의 값을 이용해 지수적으로 간격을 두어야 한다. AWS 아키텍처 블로그의 “지수에 기반한 가격 및 지터” 참고
- 재시도가 어느 간격 내에서 임의로 분포되지 않으면 작은 변화에도 재시도 요청이 동시에 쏟아질 수 있어 장애의 확산에 영향을 주게 됨
- 요청당 재시도 횟수에 제한은 둔다
- 서버 수준에서 재시도에 대한 한계 수치를 책정
- 서비스 전체를 살펴보고 특정 수준의 재시도가 정말로 필요한 것인지를 결정해야 함
- 특히 여러 부분에서 재시도를 요청함으로써 재시도 요청이 확대되는 상황은 피해야 한다
- 명확한 응답 코드를 사용하고 각기 다른 실패 모드를 어떻게 처리할 것인지를 생각해야 한다.
- 에러 상황에 따라 재시도를 수행할 것과 그렇지 않을 것을 구분해야 한다
지연응답과 마감기한
- 프런트엔드는 백엔드 서버로 RPF 요청을 보낼 때 그 응답을 기다리기 위해 자원을 소비
- RPC 마감기한은 프런트엔드가 응답을 얼마나 오래 기다릴 것인지를 정의해서 백엔드 때문에 프런트엔드의 자원이 소비되는 시간에 제한을 두기 위한 것
마감기한의 결정
- 마감기한이 너무 길면 스택의 상위 계층이 자원을 너무 많이 소비해서 스택의 하위 계층에서 문제가 발생하게 됨
- 마감기한이 너무 짧으면 자원을 많이 소비하는 요청이 계속해서 처리에 실패할 수 있음
마감기한의 상실
- 많은 연속적 장애들에서 찾아볼 수 있는 현상 중 하나는 서버가 클라이언트의 마감기한을 넘긴 요청들을 처리하느라 자원을 소비하고 있는 현상
- 클라이언트는 마감기한이 지나면 이미 해당 요청의 처리를 포기하므로 서버가 무슨 작업을 수행하는지는 전혀 신경 쓰지 않는다.
- 만일 요청의 처리가 여러 단계를 거쳐 실행된다면, 서버는 요청을 위해 다른 작업을 수행하기에 앞서 각 단계의 마감기한이 얼마나 남았는지를 먼저 확인해야 한다.
마감기한의 전파
- 백엔드에 RPC를 보낼 때 설정한 적절한 마감기한을 찾아내는 것보다는 서버들이 직접 마감기한을 전파하고 작업의 취소를 전파해야 한다.
- 마감 기한의 전파가 없다면,
- 서버 A가 10초의 마감기한을 설정한 RPC 요청을 서버 B로 보낸다.
- 서버 B는 요청의 처리를 시작하기까지 8초의 시간을 소요한 후 서버 C로 RPC 요청을 보냈다.
- 마감기한 전파를 제대로 구현했다면 서버 B가 RPC 요청에 2초의 마감기한을 설정해야 하겠지만 이 경우에는 20초라는 하드코드된 값을 대신 적용해서 RPC 요청을 서버 C에 보냈다고 가정해보자
- 서버 C는 5초 후에 큐에서 요청을 가져왔다.
- 서버 B가 마감기한 전파를 제대로 구현했더라면 서버 C는 이미 2초라는 마감기한을 넘겼으므로 요청 처리를 즉시 포기했을 것이다.
이중 지연응답
- 예제 319페이지 참고
- 이런 종류의 문제를 해결하는 데 도움이 되는 가이드라인
- 이 문제를 사전에 인지하기란 매우 어렵다. 특히 일시적인 지연응답 현상이 발생했을 때 이 장애의 원인이 이중 지연응답 (bimodal latency)이라는 것이 명확하게 드러나지 않는다. 지연응답이 증가하는 것이 확인되면 평균 값과 더불어 지연응답의 분포 역시 살펴보아야한다.
- 요청이 마감기한까지 기다리지 않고 일찌감치 에러를 리턴하면 이 문제는 자연스럽게 피해갈 수 있다. 예를 들어 백엔드가 사용할 수 없는 경우에는 백엔드를 다시 사용할 수 있을 때까지 자원을 소비하면서 기다리는 대신 즉시 에러를 리턴하는 것이 최선. RPC 계층이 이 옵션을 지원한다면 주저 없이 사용하자
- 통상적인 요청의ㅣ 지연응답에 비해 몇 배나 긴 마감기한을 설정하는 것은 좋지 않은 방법
- 공유 자원을 사용할 때는 일부 키 공강에 의해 해당 자원들이 고갈될 수 있음
느긋한 시작과 콜드 캐싱
- 어떤 프로세스든지 막 시작된 직후에는 안정적으로 동작하는 상태에 비해 응답이 느려지는 경향이 있다.
- 초기화가 필요한 경우
- 첫번 째 요청을 받을 때 필요한 백엔드와의 연결을 설정해야 하는 경우
- 일부 언어, 특히 자바의 경우 런타임 성능 향상을 위한 추가 작업이 실행되는 경우
- JIT 컴파일, 핫스팟 최적화 및 지연된 클래스 로딩 등이 수행되는 경우
- 마찬가지로 일부 바이너리들을 캐시가 채워져 있지 않으면 효율성이 떨어지는 경우가 있다
- 콜드 캐시를 갖는 경우
- 새로운 클러스터를 켜는 경우
- 이제 막 추가된 클러스터는 완전히 비어있는 캐시와 함께 실행된다.
- 유지보수 작업 후 클러스터를 서비스에 제공하는 경우
- 이 경우에는 캐시의 데이터가 그다지 유용하지 않을 수 있다.
- 서비스 재시작
- 캐시를 사용하는 태스크가 최근에 재시작되었다면 캐시를 다시 채우는 데 어느 정도의 시간이 필요
- 이때 서버가 아니라 memcache 같은 별도의 바이너리로 캐시를 이동한다면 지연응답이 약간 있긴하지만 다른 서버들과 캐시를 공유할 수 있다
- 만일 캐시가 서비스에 서비스에 중요한 영향을 미친다면, 다음의 전략 중 하나 혹은 그 이상을 채택할 수 있다.
- 서비스를 overprovision한다.
- 서비스 소유자는 서비스에 캐시를 추가하는 것에 대해 충분히 주의를 기울여야한다.
- 새로 투입되는 캐시가 지연응답 캐시인지, 아니면 용량 캐시로써 안정하게 잘 동작할 수 있도록 충분히 잘 구현된 캐시인지 확인 필요 (??? 질문하기)
- 간혹 서비스의 성능 향상을 위해 투입한 캐시 때문에 의존성 관리만 어려워지기도 함
- 일반적인 연속적 장애 방지 기법 적용
- 서버는 과부하가 발생하거나 혹은 적절한 퇴보 모드에 지입하면 유입되는 요청들의 처리를 거부해야 함
- 대량의 서버를 재시작하는 등의 일이 발생하면 서비스가 어떻게 동작하는지를 확인하기 위한 충분한 테스트가 수반되어야 함
- 클러스터에 부하를 위임할 때는 부하의 크기를 천천히 늘려야 함
- 최초로 유입되는 요청의 비율을 낮게 유지하면서 캐시가 채워질 시간적 여유를 가져야 함
- 일단 캐시가 채워진 후에 더 많은 트래픽을 구성하도록 처리
항상 스택의 아래쪽을 살펴보자
- 내부 계층간의 통신은 다음과 같은 여러가지 이유로 문제가 될 수 있음
- 분산 데드락 (distributed deadlock)의 영향을 받기 쉬움
- 백엔드가 원격 백엔드로부터 지속적으로 요청을 수신하기 위해 사용하는 스레드 풀을 원격 밴엔드로 보낸 RPC 요청의 응답을 기다리기 위해 사용할 수도 있음
- 이때 백엔드 A의 스레드 풀이 가득 찼다고 생각해보자. 그러면 백엔드 A에 요청을 보내는 백엔드 B는 백엔드 A의 스레트 풀에 여력이 생길 때까지 계속해서 스레드를 점유하게 됨 -> 스레드 기아
- 만일 어떤 장애나 과부하(예를 들면 과부하 상태에서 부하의 재분배가 더 활발하게 이루어지는 경우)로 인해 내부 계층 간의 통신이 증가하는 경우
- 어느 수준 이상으로 부하가 증가하면 내부 계층 간 요청이 빠르게 증가할 수 있다.
- 예를 들어 어떤 사용자의 요청이 특정 백엔드에서 처리되며, 이 백엔드는 다른 클러스터의 차순위 백엔드로 해당 사용자의 요청의 처리를 위임할 수 있도록 설정되어 있다고 가정하자
- 전체 시스템이 과부하 상태라면 최우선 백엔드가 차순위 백엔드로 요청을 위임하는 비율이 증가하게 되고 시스템의 부하가 더 높아지면 최우선 백엔드가 차순위 백엔드의 요청 처리 결과를 파싱하고 대기하는 비용이 증가하게 됨
- 계층 간 통신의 중요도에 따라 시스템의 부트스트랩이 더 복잡해질 수 있음
- 일반적으로 계층 간 통신(특히 통신의 경로가 계속 반복되는 경우)은 지양하는 것이 좋음
- 그 대신 클라이언트가 통신을 하도록 주도해야 한다
- 예를 들어 프런트엔드가 어떤 백엔드와 통신하는데 이 백엔드를 잘못 선택했다면 해당 백엔드는 올바른 백엔드로 요청을 위임해서는 안된다
- 백엔드는 프런트엔드에 올바른 백엔드로 요청을 다시 전달할 것을 지시해야 한다.
연속적 장애의 발생 요인
프로세스 중단
- 간혹 일부 서버 태스크들이 중단되어서 가용한 용량이 줄어드는 경우가 있다.
- 아주 사소한 몇가지로 인해 서비스가 중단되기 직전의 상황까지 몰릴 수도 있다.
프로세스 업데이트
- 새 버전의 바이너리를 배포하거나 설정을 수정했는데, 이건이 동시에 많은 수의 태스크에 영향을 주게 되면 연속적 장애를 유발할 수 있다.
- 이런 상황을 피하려면 서비스의 업데이트 인프라스트럭처를 설정할 때 이런 오버헤드를 고려해서 용량 게획을 세우거나 가장 사용량이 많은 시점을 피해야 한다.
새로운 배포
- 새 바이너리, 설정의 변경, 혹은 기반 인프라스트럭처 스택의 변경 등으로 인해 요청 프로파일, 자원 사용량과 제한, 백엔드 혹은 연속적 장애를 유발시킬 수 있는 다른 시스템 컴포넌트의 수 등에 변화가 생길 수 있음
- 문제가 생기면 이런 것들을 복원해보는 것이 좋다.
유기적 성장
- 대부분의 경우 연속적 장애는 어떤 특정한 서비스의 변화에 의해 발생하지는 않지만 사용량이 증가하는데, 그에 맞추어 수용량을 조정하지 않는다면 연속적 장애가 발생하기도 한다.
계획에 의한 변경, 자원의 감소 혹은 서버의 종료
- 만일 서비스가 멀티호밍(multihoming)을 사용하는 경우, 서비스의 수용량 중 일부가 클러스터의 유지보수나 장애로 인해 사용이 불가능해지기도 함
요청 프로파일의 변화
- 로드밸런싱 설정의 변경이나 트래픽의 혼합 또는 클러스터의 과적 등으로 인해 트래픽이 다른 프런트엔드 서비스로 옮겨지면서 해당 서비스와 연결된 백엔드 서비스가 다른 클러스터로부터 발신된 요청을 수신하게 되는 경우가 발생
- 프런트엔드 코드의 변경이나 설정의 변경 때문에 개별 페이로드를 처리하는 병균 비용이 변경되기도 함
자원의 제한
- 일부 클러스터 운영 시스템은 자원의 과다 투입을 허용하기도 함 (잉여 CPU)
- 서비스의 안전을 이 여분의 CPU에 의존하는 것은 매우 위험
연속적 장애 테스트하기
장애가 발생할 때까지 테스트하고 조치하기
- 부하테스트 조지기
- 부하테스트를 하면서 우리의 시스템이 얼마나 잘 설계되어있는지 파악하기
- 어느 지점에서 장애가 발생하는 지 파악하기
- 조치하기
- 한동안 과부하 상태에 놓였던 컴포넌트가 원래의 수준으로 돌아오면 어떻게 동작하는지 파악하기
- 과부하 상태에서 퇴보 모드에 들어간다면, 사람의 개입 없이 일반 모드로 돌아올 수 있는가?
- 과부하 상태에서 몇 개의 서버에서 충도링 발생한다면 시스템을 안정화시키기 위해서는 어느 정도의 부하가 절감되어야 하는가?
- 프로덕션 환경의 일부를 대상으로 장애 테스트를 실행하는 것을 고려해볼 수 있다 (아주 조심해야 함)
- 예상되는 트래픽 패턴과 더불어 태스크의 수를 빠르게 혹은 천천히 줄여본다
- 클러스터의 수용량을 신속하게 줄여본다.
- 다양한 백엔드를 시스템에서 감춰본다.
사용량이 높은 클라이언트 테스트하기
- 서비스가 다운된 동안 클라이언트는 얼마나 빠르게 작업을 처리하는가?
- 에러 상황에서 클라리언트가 임의의 지수 백오프를 사용하는가?
- 클라이언트가 대량의 부하를 유발하는 취약점을 가지고 있는가?
상대적으로 덜 중요한 백엔드의 테스트
- 상대적으로 덜 중요한 백에드가 사용 불가능한 상태라 하더라도 서비스의 중요한 컴포넌트들이 그에 따른 간섭을 받지 않는지 확인
연속적 장애를 처리하기 위한 즉각적인 대처
자원의 추가 투입
- 가장 적절한 방식
- 하지만 죽음의 소용돌이는 해결 못함
건강 상태 점검의 중지
- 서비스가 건강 상태가 아닌 경우 건강 상태 점검으로 인해 장애 모드로 들어갈 때가 있다.
서버의 재시작
- 자바 서버에서 죽음의 GC 소용돌이가 발생한 경우
- 일부 처리 중인 요청들에 특별한 마감기한이 없지만 자원을 소비 중이어서 스레드 차단이 발생한 경우
- 서버에서 데드락이 발생한 경우
- 한가지 명심해야 할 것은 서버를 재시작하기 전에 연속적 장애의 원인을 먼저 규명해야 한다는 점이다
- 만약 장애의 원인이 콜드캐시 같은 이슈라면 서버의 재시작으로 인해 연속적 장애가 더 크게 퍼져나갈 수도 있다.
트래픽의 경감
- 이 상황을 유발하는 기본적인 원인을 처리해야 한다.
- 충돌이 사라질 만큼 충분히 부하를 경감시켜야 한다
- 대부분의 서버가 양호한 상태가 된다
- 점진적으로 서버에 부하를 늘려간다
- 사용자는 장애를 경험할 수 밖에 없지만, 연속적 장애로부터 복구가 가능
퇴보 모드로 들어가기
- 이 방법은 반드시 서비스 내에 구현되어 있어야 함
일괄 작업 부타 (batch load) qowpgkrl
- 부하가 많을 때는 batch job 죽인다
문제가 있는 트래픽 배제하기
마무리하며
- 시스템에 과부하가 발생하면 그 상황을 해결하기 위한 조치가 필요
- 일단 서비스가 장애가 발생할 수 있는 수준에 도달하면 사용자의 모든 요청을 완벽하게 처리해 내기보다는 에러를 리턴하거나 혹은 평소 대비 낮은 품질의 결과를 리턴해야 함
- 연속적 장애를 피하려면 장애의 발생 지점을 이해하고 장애 발생 시 시스템이 어떻게 동작하는지 이해하는 것
- 충분한 주의를 기울이지 않는다면 백그라운드 에러를 줄이거나 안정적인 상태를 더 향상시키기 위한 시스템의 변경사항이 오히려 서비스 전체의 장애로 번질 위험이 있음
- 장애 시 재시도, 문제가 발생한 서버로부터 부하 위임, 문제가 발생한 서버의 중단, 캐시 추가 작업등은 일반적으로는 성능의 향상을 볼 수 있지만, 대량의 장애를 유발하기도 함.
22. 연속적 장애 다루기
처음에 성공하지 못하면 그 이상으로 늦어지게 된다 - 댄 샌들러 (Google SW Engineer) 왜 사람들은 조금만 더 신경을 쓰면 된다는 사실을 늘 잊곤 하는가? - 에이드 오시니에 (구글 개발자 전도사)
- 연속적 장애(cascading failure)는 정상적인 것처럼 보여지는 응답때문에 시간이 지나면서 장애가 계속해서 가중되는 현상
- 전체 시스템의 일부에서 장애가 발생했을 때 주로 나타나며, 이로 인해 시스템의 다른 부분에서 장애가 발생할 가능성도 늘어나게 됨
연속적 장애의 원인과 그 대책
서버 과부하
- 연속적 장애를 유발하는 가장 일반적인 원인은 과부하다
- 대부분의 연속적 장애는 서버의 과부하가 직접적인 원인이거나 혹은 이로 인해 확장된 또는 변형된 장애들이다
- 특정 클러스터에서 장애가 발생하면, 다른 클러스터에 가용량 이상의 요청이 전달될 수 있다.
- 이로인해 자원이 부족하게 되어 충돌이 방생하거나 지연응답 혹은 오동작이 발생하게 된다
- 이처럼 성공적으로 완료된 작업의 수가 감소하면 그 여파는 도메인의 다른 시스템으로 퍼져나가 잠재적으로는 전체 시스템에 영향을 미칠 수 있다.
- 예를 들어 한 클러스터에서 과부하가 발생하면 그 서버들이 충돌로 인해 강제로 종료 -> 로드밸런싱 컨트롤러가 요청을 다른 클러 스터에 보냄 -> 그 쪽의 서버들에서 과부하가 발생 -> 서비스 전체에 걸치 과부하 장애 발생
- 위 상황은 그다지 오래 걸리지 않는다 (~분)
자원의 부족
- 서버의 어떤 자원이 부족해지는지, 그리고 서버가 어떻게 구성되어 있는지에 따라 자원의 부족은 서버의 비효율적 동작 혹은 충돌로 인한 서버의 강제 종료를 유발할 수 있고 로드밸런서는 그 즉시 자원 부족이라는 이슈를 다른 서버들로 퍼져나가게 함
CPU 자원이 부족한 경우
- 주로 모든 요청의 처리가 느려진다. 다음과 같은 부차적 현상 발생가능
처리중인 요청 수의 증가
- 이는 메모리, 활성 스ㄹ드 수, 파일 디스크립터 수 및 백엔드 자원 등 거의 모든 자원에 영향을 미침
비정상적으로 증가하는 큐의 크기
- 지연응답이 증가하고, 큐가 더 많은 메모리를 소비하게 됨
스레드 기아 (thread starvation)
- 스레드가 잠금을 기다리느라 더 이상 처리되지 않고 대시 상태가 되면 건강 상태 체크 종단점이 제시간에 처리되지 않아 실패할 가능성이 있다
CPU 혹은 요청 기아
- 서버 내부의 와치독이 서버가 더 이상 처리를 못하고 있다는 것을 탐지하면 CPU 기아 현상으로 인해 서버에서 충돌이 발생하기도 하고 와치독 이벤트가 원격에서 발생하고 요청 큐를 통해 처리되는 경우에는 요청 기아가 발생하기도 한다.
RPC 시간 초과
- RPC 응답이 늦어져 클라이언트에서 시간초과 발생가능
- 서버가 수행했던 작업은 아무런 소용이 없어지고 클라이언트는 RPC 요청을 재시도하여 더 많은 과부하 발생
CPU 캐시 이점 감소
- 더 많은 CPU가 사용될수록 더 많은 코어에서 누수가 발생할 가능성이 커지고 그 결과 로컬 캐시 및 CPU의 효율성이 떨어지게 됨
메모리
태스크 종료
- 예를 들어 container manager가 가용한 자원의 한계 혹은 애플리케이션의 특정한 충돌로 인해 태스크를 거부하면 이 태스크는 강제로 종료됨
자바의 GC 수행률 증가로 인한 CPU 사용률 증가
- GC가 비정상적으로 동작하면 가용한 CPU 자원이 줄어들고 이로 인해 요청의 처리가 느려지면서 RAM 사용량이 증가하고 -> GC가 더 자주돌고 ….
- 죽음의 GC 소용돌이 (GC death spiral)
캐시 활용률의 감소
- 가용한 RAM이 부족해지면 애플리케이션 수준의 캐시 활용률이 감소하고 그 결과 더 많은 RPC 요청이 백엔드로 전달되어 백엔드에 과부하를 초래
스레드
- 스레드 기아는 직접적으로 에러를 발생시키거나 혹은 건강 상태 점검의 실패를 야기한다.
- 서버가 필요한 만큼 스레드를 생성하면 스레드 과부하로 인해 너무 많은 RAM을 소비하게 된다
- 프로세스 ID를 모두 소비하게 될 수도 있음
파일 서술사
- fd가 부족해지면 네트워크 연결의 초기화가 불가능해져 건강 상태 점검이 실패하게 된
자원간의 의존성
- 자원의 부족은 또 다른 자원에 영향을 미침
- 과부하로 고통받고 있는 서비스는 부차적 증상을 유발하는데, 이것이 마치 근본 원인처럼 보여 디버깅이 더욱 어려워 짐
- 307p 참고
서비스 이용 불가
- 자원의 부족은 서버의 충돌을 유발
- 이 문제는 마치 눈덩이 처럼 불어나는 경향이 있어서 금세 모든 서버들에서 충돌이 발생
- 행여 서버가 복구된다 하더라도 복구가 되자마자 밀려있는 요청의 폭탄을 얻어맞기 깨문에 이 상활을 탈출하기란 상당히 어렵다.
서버 과부하 방지하기
서버 수용량 한계에 대한 부하 테스트 및 과부하 상태에서의 실패에 대한 테스트
- 실제 환경에서 테스트하지 않으면 정확히 어떤 자원이 부족해지는지, 그리고 자원의 부족이 어떤 영향을 미치는지를 예측하기가 매우 어렵다
- python Locust와 같은 부하테스트 프레임워크 활용 가능
경감된 응답 제공하기
- 품질은 낮지만 더 수월하게 연산할 수 있는 결과를 사용자에게 제공
- 311 페이지 “부하 제한과 적절한 퇴보” 참고
과부하 상태에서 요청을 거부하도록 서비스를 구현하기
- 서버는 과부하 및 충돌로부터 스스로를 보호할 수 있어야 함
- 최대한 빠르면서도 비용이 적게 드는 방법으로 실패를 처리해야 함
고수준의 시스템들이 서버에 과부하를 유발하지 않고 요청을 거부하도록 구현하기
- 리버스 프록시는 IP 주소 같은 조건들을 이용하여 요청의 전체 양을 제한 함으로써 DoS와 같은 공격의 영향을 최소화할 수 있음
- 로드밸런서는 서비스가 전체적인 과부하 상태일 때는 요청을 스스로 차단한다. 차단 방식은 무조건적 차단, 선택적 차단 등 여러방법이 있음
- 개별 작업들은 로드밸런서가 보내오는 요청의 변화에 의해 서버에 갑작스럽게 많은 요청이 몰리지 않도록 이를 제한한다.
수용량 계획을 실행하기
- 수용량 계획은 서비스가 어느 수준의 부하에서 장애가 발생하는지를 판단하기 위해 반드시 성능 테스트와 병행되어야 함
- N+2 공식
- 수용량 계획은 연속적 장애의 발생 가능성을 억제할 수는 있지만, 연속적 장애로부터 시스템을 보호하는 데는 크게 도움이 되지 않는다.
큐 관리하기
- 각 요청을 개별 스레드에서 처리하는 대부분의 서버는 요청을 처리하기 위해 스레드 풀 앞에 큐를 배치
- 대부분 이 큐가 가득 차면 서버는 새로운 요청을 거부
- 요청을 큐에 적재하면 더 많은 메모리를 소비, 지연응답 또한 증가
- 폭발적인 부하가 발생할 수 있는 시스템은 현재 사용 중인 스레드의 수, 각 요청의 처리 시간, 그리고 과부하의 크기와 빈도 등에 기초해서 큐의 크기를 결정하는 것이 좋다.
부하 제한과 적절한 퇴보
- 서버가 과부하 상태에 도달하게 되면 부하 제한 (load shedding)을 통해 유입되는 트래픽을 감소시켜 어느 정도의 부하를 덜어낼 수 있다.
- 서버의 메모리 부족이나 건강 상태 점검 실패, 지연응답의 극단적 증가 및 기타 과부하에 관련된 증상들이 서버에서 발생하는 것을 방지하고 서버가 최대한 자신의 작업을 계속해서 수행할 수 있도록 유지하는 것
- 부하는 배분하는 가장 직관적인 방법은 CPU, 메모리 혹은 큐의 길이에 따라 태스크별로 제한을 두는 것
- 일정 수 이상의 클라이언트 요청이 유입되면 그 이후의 요청에 대해서는 HTTP 503에러를 리턴
- FIFO로 동작하는 큐를 LIFO로 변경하면 처리할 필요가 없는 요청들을 제거함으로써 부하를 줄일 수 있음 (RPC 제한 시간 초과 요청)
- PRC 마감기한을 전체 스택에 전파하는 방법을 함께 활용하면 더욱 큰 효과를 발휘
- 어 깔끔한 방법은 처리를 거부할 작업과 더 중요하고 우선순위가 높은 요청을 선택할 때 해당 요쳥의 클라이언트를 함께 고려하는 방법
- 적절한 퇴보 (graceful deradation)는 부하 제한의 개념에서 한 걸을 더 나아가 실행해야 할 작업의 양을 감소시키는 방법
- 일부러 응답의 품질을 경감시켜 작업의 양이나 처리에 필요한 시간의 양을 현저히 줄일 수 있다
- 다음의 고려가 필요
- 어떤지표를 활용해서 부하 제한이나 적절한 퇴보의 적용 여부를 결정할 것인가?
- 서버가 퇴보 모드로 동작하게 되면 어떤 조치를 취해야 하는가?
- 부하 제한이나 적절한 퇴보는 어느 계층에서 구현해야 하는가?
- 선택할 수 있는 옵션을 평가하고 배포하는 동안에는 다음과 같은 내용들을 고려하자
- 적절한 퇴보는 너무 자주 발생해서는 안된다. 대부분 수용량 계획이 실패했거나 혹은 예상하지 못했던 부하의 이동이 발생하는 경우에만 적용되어야 함
- 어떤 코드 경로가 한 번도 사용된 적이 없다면 이 코드 경로는 동작하지 않을 것. 안정적인 상태에서 운영 중일 때는 적절한 퇴보 모드가 사용될 일이 없지만, 적적한 퇴보 모드 운영 경험을 쌓을 수 없다
- 이 모드에서 동작하는 서버가 너무 많아지지는 않는지 적절히 모니터링하고 경고 발송
- 부하 제한과 적절한 퇴보를 구현하는 로직이 복잡해지면 그 자체에 문제가 발생할 수 있다.
재시도
- 백엔드로 약간 어설프게 재시도를 요청하는 프런트엔드 코드가 있다고 가정해보자
- 이 재시도는 어떤 요청이 실패앴을 때 이루어지며 논리 요청당 백엔드 RPC는 20번으로 제한
func exampleRpcCall(client pb.ExampleClient, request pb.Request) *pb.Response{
// RPC 타임 5초
opts := grpc.WithTimeout(5 * time.Second)
// 최대 20번까지 RPC 호출 시도
attempts := 20
for attempts > 0 {
conn. err := grpc.Dial(*serverAddr, opts...)
if err != nil {
// 연결 설정 시 에러가 발생하면 재시도
attempts--
continue
}
defer conn.Close()
// 클라이언트 stub을 생성하고 RPC호출 시도
client := pb.NewBackendClient(conn)
response, err := client.MakeRequest(context.Background, request)
if err != nil {
// 호출 실패 시 재시도
attempts--
continue
}
return response
}
grpclog.Fatalf("재시도 횟수 초과")
}
- 이 시스템은 다음과 같은 과정을 거쳐 연속적 장애를 일으킬 수 있다
- 백엔드 한계가 태스크당 10,000 QPS이며, 적절한 최보가 적용되면 그 이후의 모든 요청을 거부된다고 가정
- 프런트엔드가
MakeRequest
함수를 10,100 QPS의 비율로 일정하게 호출하면 백엔드에 100 QPS의 과부하가 발생해서 백엔드가 요청을 거부하게 됨 MakeRequest
함수는 매 1,000 ms 마다 실패한 100 QPS의 요청을 재시도하고 이들이 성공적으로 처리됨. 이는 100 QPS의 추가 과부하가 되어 백엔드에 200 QPS의 과부하 발생- 그 결과 재시도의 비율이 증가: 그래서 처음 시도에 비해 더 적은 수의 요청이 처리되고 결국 백엔드에 전달된 요청의 일부만이 처리됨
- 백엔드 태스크가 부하의 증가로 인해 더이상의 요청을 처리할 수 없게 되면 계속되는 요청과 재시도 때문에 결국 충돌이 발생 -> 이 충돌로 인해 처리되지 못한 요청들은 나머지 백엔드 태스크들이 감당해야 하는 부하가 되어 이 태스크들 역시 과부하 상태에 놓인다.
- 자동 재시도를 수행할 때는 다음과 같은 사항들을 고려해야 한다
- 대부분의 백엔드 보호전략은 “서버 과부하 방지하기” 절에서 설명. 특히 시스템에 대한 테스트는 문제를 조명하고 적절한 퇴보를 통해 백엔드에 대한 재시도의 영향을 줄일 수 있음
- 재시도를 실행할 때는 항상 임의의 값을 이용해 지수적으로 간격을 두어야 한다. AWS 아키텍처 블로그의 “지수에 기반한 가격 및 지터” 참고
- 재시도가 어느 간격 내에서 임의로 분포되지 않으면 작은 변화에도 재시도 요청이 동시에 쏟아질 수 있어 장애의 확산에 영향을 주게 됨
- 요청당 재시도 횟수에 제한은 둔다
- 서버 수준에서 재시도에 대한 한계 수치를 책정
- 서비스 전체를 살펴보고 특정 수준의 재시도가 정말로 필요한 것인지를 결정해야 함
- 특히 여러 부분에서 재시도를 요청함으로써 재시도 요청이 확대되는 상황은 피해야 한다
- 명확한 응답 코드를 사용하고 각기 다른 실패 모드를 어떻게 처리할 것인지를 생각해야 한다.
- 에러 상황에 따라 재시도를 수행할 것과 그렇지 않을 것을 구분해야 한다
지연응답과 마감기한
- 프런트엔드는 백엔드 서버로 RPF 요청을 보낼 때 그 응답을 기다리기 위해 자원을 소비
- RPC 마감기한은 프런트엔드가 응답을 얼마나 오래 기다릴 것인지를 정의해서 백엔드 때문에 프런트엔드의 자원이 소비되는 시간에 제한을 두기 위한 것
마감기한의 결정
- 마감기한이 너무 길면 스택의 상위 계층이 자원을 너무 많이 소비해서 스택의 하위 계층에서 문제가 발생하게 됨
- 마감기한이 너무 짧으면 자원을 많이 소비하는 요청이 계속해서 처리에 실패할 수 있음
마감기한의 상실
- 많은 연속적 장애들에서 찾아볼 수 있는 현상 중 하나는 서버가 클라이언트의 마감기한을 넘긴 요청들을 처리하느라 자원을 소비하고 있는 현상
- 클라이언트는 마감기한이 지나면 이미 해당 요청의 처리를 포기하므로 서버가 무슨 작업을 수행하는지는 전혀 신경 쓰지 않는다.
- 만일 요청의 처리가 여러 단계를 거쳐 실행된다면, 서버는 요청을 위해 다른 작업을 수행하기에 앞서 각 단계의 마감기한이 얼마나 남았는지를 먼저 확인해야 한다.
마감기한의 전파
- 백엔드에 RPC를 보낼 때 설정한 적절한 마감기한을 찾아내는 것보다는 서버들이 직접 마감기한을 전파하고 작업의 취소를 전파해야 한다.
- 마감 기한의 전파가 없다면,
- 서버 A가 10초의 마감기한을 설정한 RPC 요청을 서버 B로 보낸다.
- 서버 B는 요청의 처리를 시작하기까지 8초의 시간을 소요한 후 서버 C로 RPC 요청을 보냈다.
- 마감기한 전파를 제대로 구현했다면 서버 B가 RPC 요청에 2초의 마감기한을 설정해야 하겠지만 이 경우에는 20초라는 하드코드된 값을 대신 적용해서 RPC 요청을 서버 C에 보냈다고 가정해보자
- 서버 C는 5초 후에 큐에서 요청을 가져왔다.
- 서버 B가 마감기한 전파를 제대로 구현했더라면 서버 C는 이미 2초라는 마감기한을 넘겼으므로 요청 처리를 즉시 포기했을 것이다.
이중 지연응답
- 예제 319페이지 참고
- 이런 종류의 문제를 해결하는 데 도움이 되는 가이드라인
- 이 문제를 사전에 인지하기란 매우 어렵다. 특히 일시적인 지연응답 현상이 발생했을 때 이 장애의 원인이 이중 지연응답 (bimodal latency)이라는 것이 명확하게 드러나지 않는다. 지연응답이 증가하는 것이 확인되면 평균 값과 더불어 지연응답의 분포 역시 살펴보아야한다.
- 요청이 마감기한까지 기다리지 않고 일찌감치 에러를 리턴하면 이 문제는 자연스럽게 피해갈 수 있다. 예를 들어 백엔드가 사용할 수 없는 경우에는 백엔드를 다시 사용할 수 있을 때까지 자원을 소비하면서 기다리는 대신 즉시 에러를 리턴하는 것이 최선. RPC 계층이 이 옵션을 지원한다면 주저 없이 사용하자
- 통상적인 요청의ㅣ 지연응답에 비해 몇 배나 긴 마감기한을 설정하는 것은 좋지 않은 방법
- 공유 자원을 사용할 때는 일부 키 공강에 의해 해당 자원들이 고갈될 수 있음
느긋한 시작과 콜드 캐싱
- 어떤 프로세스든지 막 시작된 직후에는 안정적으로 동작하는 상태에 비해 응답이 느려지는 경향이 있다.
- 초기화가 필요한 경우
- 첫번 째 요청을 받을 때 필요한 백엔드와의 연결을 설정해야 하는 경우
- 일부 언어, 특히 자바의 경우 런타임 성능 향상을 위한 추가 작업이 실행되는 경우
- JIT 컴파일, 핫스팟 최적화 및 지연된 클래스 로딩 등이 수행되는 경우
- 초기화가 필요한 경우
- 마찬가지로 일부 바이너리들을 캐시가 채워져 있지 않으면 효율성이 떨어지는 경우가 있다
- 콜드 캐시를 갖는 경우
- 새로운 클러스터를 켜는 경우
- 이제 막 추가된 클러스터는 완전히 비어있는 캐시와 함께 실행된다.
- 유지보수 작업 후 클러스터를 서비스에 제공하는 경우
- 이 경우에는 캐시의 데이터가 그다지 유용하지 않을 수 있다.
- 서비스 재시작
- 캐시를 사용하는 태스크가 최근에 재시작되었다면 캐시를 다시 채우는 데 어느 정도의 시간이 필요
- 이때 서버가 아니라 memcache 같은 별도의 바이너리로 캐시를 이동한다면 지연응답이 약간 있긴하지만 다른 서버들과 캐시를 공유할 수 있다
- 새로운 클러스터를 켜는 경우
- 만일 캐시가 서비스에 서비스에 중요한 영향을 미친다면, 다음의 전략 중 하나 혹은 그 이상을 채택할 수 있다.
- 서비스를 overprovision한다.
- 서비스 소유자는 서비스에 캐시를 추가하는 것에 대해 충분히 주의를 기울여야한다.
- 새로 투입되는 캐시가 지연응답 캐시인지, 아니면 용량 캐시로써 안정하게 잘 동작할 수 있도록 충분히 잘 구현된 캐시인지 확인 필요 (??? 질문하기)
- 간혹 서비스의 성능 향상을 위해 투입한 캐시 때문에 의존성 관리만 어려워지기도 함
- 일반적인 연속적 장애 방지 기법 적용
- 서버는 과부하가 발생하거나 혹은 적절한 퇴보 모드에 지입하면 유입되는 요청들의 처리를 거부해야 함
- 대량의 서버를 재시작하는 등의 일이 발생하면 서비스가 어떻게 동작하는지를 확인하기 위한 충분한 테스트가 수반되어야 함
- 클러스터에 부하를 위임할 때는 부하의 크기를 천천히 늘려야 함
- 최초로 유입되는 요청의 비율을 낮게 유지하면서 캐시가 채워질 시간적 여유를 가져야 함
- 일단 캐시가 채워진 후에 더 많은 트래픽을 구성하도록 처리
- 서비스를 overprovision한다.
항상 스택의 아래쪽을 살펴보자
- 내부 계층간의 통신은 다음과 같은 여러가지 이유로 문제가 될 수 있음
- 분산 데드락 (distributed deadlock)의 영향을 받기 쉬움
- 백엔드가 원격 백엔드로부터 지속적으로 요청을 수신하기 위해 사용하는 스레드 풀을 원격 밴엔드로 보낸 RPC 요청의 응답을 기다리기 위해 사용할 수도 있음
- 이때 백엔드 A의 스레드 풀이 가득 찼다고 생각해보자. 그러면 백엔드 A에 요청을 보내는 백엔드 B는 백엔드 A의 스레트 풀에 여력이 생길 때까지 계속해서 스레드를 점유하게 됨 -> 스레드 기아
- 만일 어떤 장애나 과부하(예를 들면 과부하 상태에서 부하의 재분배가 더 활발하게 이루어지는 경우)로 인해 내부 계층 간의 통신이 증가하는 경우
- 어느 수준 이상으로 부하가 증가하면 내부 계층 간 요청이 빠르게 증가할 수 있다.
- 예를 들어 어떤 사용자의 요청이 특정 백엔드에서 처리되며, 이 백엔드는 다른 클러스터의 차순위 백엔드로 해당 사용자의 요청의 처리를 위임할 수 있도록 설정되어 있다고 가정하자
- 전체 시스템이 과부하 상태라면 최우선 백엔드가 차순위 백엔드로 요청을 위임하는 비율이 증가하게 되고 시스템의 부하가 더 높아지면 최우선 백엔드가 차순위 백엔드의 요청 처리 결과를 파싱하고 대기하는 비용이 증가하게 됨
- 계층 간 통신의 중요도에 따라 시스템의 부트스트랩이 더 복잡해질 수 있음
- 일반적으로 계층 간 통신(특히 통신의 경로가 계속 반복되는 경우)은 지양하는 것이 좋음
- 그 대신 클라이언트가 통신을 하도록 주도해야 한다
- 예를 들어 프런트엔드가 어떤 백엔드와 통신하는데 이 백엔드를 잘못 선택했다면 해당 백엔드는 올바른 백엔드로 요청을 위임해서는 안된다
- 백엔드는 프런트엔드에 올바른 백엔드로 요청을 다시 전달할 것을 지시해야 한다.
- 분산 데드락 (distributed deadlock)의 영향을 받기 쉬움
연속적 장애의 발생 요인
프로세스 중단
- 간혹 일부 서버 태스크들이 중단되어서 가용한 용량이 줄어드는 경우가 있다.
- 아주 사소한 몇가지로 인해 서비스가 중단되기 직전의 상황까지 몰릴 수도 있다.
프로세스 업데이트
- 새 버전의 바이너리를 배포하거나 설정을 수정했는데, 이건이 동시에 많은 수의 태스크에 영향을 주게 되면 연속적 장애를 유발할 수 있다.
- 이런 상황을 피하려면 서비스의 업데이트 인프라스트럭처를 설정할 때 이런 오버헤드를 고려해서 용량 게획을 세우거나 가장 사용량이 많은 시점을 피해야 한다.
새로운 배포
- 새 바이너리, 설정의 변경, 혹은 기반 인프라스트럭처 스택의 변경 등으로 인해 요청 프로파일, 자원 사용량과 제한, 백엔드 혹은 연속적 장애를 유발시킬 수 있는 다른 시스템 컴포넌트의 수 등에 변화가 생길 수 있음
- 문제가 생기면 이런 것들을 복원해보는 것이 좋다.
유기적 성장
- 대부분의 경우 연속적 장애는 어떤 특정한 서비스의 변화에 의해 발생하지는 않지만 사용량이 증가하는데, 그에 맞추어 수용량을 조정하지 않는다면 연속적 장애가 발생하기도 한다.
계획에 의한 변경, 자원의 감소 혹은 서버의 종료
- 만일 서비스가 멀티호밍(multihoming)을 사용하는 경우, 서비스의 수용량 중 일부가 클러스터의 유지보수나 장애로 인해 사용이 불가능해지기도 함
요청 프로파일의 변화
- 로드밸런싱 설정의 변경이나 트래픽의 혼합 또는 클러스터의 과적 등으로 인해 트래픽이 다른 프런트엔드 서비스로 옮겨지면서 해당 서비스와 연결된 백엔드 서비스가 다른 클러스터로부터 발신된 요청을 수신하게 되는 경우가 발생
- 프런트엔드 코드의 변경이나 설정의 변경 때문에 개별 페이로드를 처리하는 병균 비용이 변경되기도 함
자원의 제한
- 일부 클러스터 운영 시스템은 자원의 과다 투입을 허용하기도 함 (잉여 CPU)
- 서비스의 안전을 이 여분의 CPU에 의존하는 것은 매우 위험
연속적 장애 테스트하기
장애가 발생할 때까지 테스트하고 조치하기
- 부하테스트 조지기
- 부하테스트를 하면서 우리의 시스템이 얼마나 잘 설계되어있는지 파악하기
- 어느 지점에서 장애가 발생하는 지 파악하기
- 조치하기
- 한동안 과부하 상태에 놓였던 컴포넌트가 원래의 수준으로 돌아오면 어떻게 동작하는지 파악하기
- 과부하 상태에서 퇴보 모드에 들어간다면, 사람의 개입 없이 일반 모드로 돌아올 수 있는가?
- 과부하 상태에서 몇 개의 서버에서 충도링 발생한다면 시스템을 안정화시키기 위해서는 어느 정도의 부하가 절감되어야 하는가?
- 프로덕션 환경의 일부를 대상으로 장애 테스트를 실행하는 것을 고려해볼 수 있다 (아주 조심해야 함)
- 예상되는 트래픽 패턴과 더불어 태스크의 수를 빠르게 혹은 천천히 줄여본다
- 클러스터의 수용량을 신속하게 줄여본다.
- 다양한 백엔드를 시스템에서 감춰본다.
사용량이 높은 클라이언트 테스트하기
- 서비스가 다운된 동안 클라이언트는 얼마나 빠르게 작업을 처리하는가?
- 에러 상황에서 클라리언트가 임의의 지수 백오프를 사용하는가?
- 클라이언트가 대량의 부하를 유발하는 취약점을 가지고 있는가?
상대적으로 덜 중요한 백엔드의 테스트
- 상대적으로 덜 중요한 백에드가 사용 불가능한 상태라 하더라도 서비스의 중요한 컴포넌트들이 그에 따른 간섭을 받지 않는지 확인
연속적 장애를 처리하기 위한 즉각적인 대처
자원의 추가 투입
- 가장 적절한 방식
- 하지만 죽음의 소용돌이는 해결 못함
건강 상태 점검의 중지
- 서비스가 건강 상태가 아닌 경우 건강 상태 점검으로 인해 장애 모드로 들어갈 때가 있다.
서버의 재시작
- 자바 서버에서 죽음의 GC 소용돌이가 발생한 경우
- 일부 처리 중인 요청들에 특별한 마감기한이 없지만 자원을 소비 중이어서 스레드 차단이 발생한 경우
- 서버에서 데드락이 발생한 경우
- 한가지 명심해야 할 것은 서버를 재시작하기 전에 연속적 장애의 원인을 먼저 규명해야 한다는 점이다
- 만약 장애의 원인이 콜드캐시 같은 이슈라면 서버의 재시작으로 인해 연속적 장애가 더 크게 퍼져나갈 수도 있다.
트래픽의 경감
- 이 상황을 유발하는 기본적인 원인을 처리해야 한다.
- 충돌이 사라질 만큼 충분히 부하를 경감시켜야 한다
- 대부분의 서버가 양호한 상태가 된다
- 점진적으로 서버에 부하를 늘려간다
- 사용자는 장애를 경험할 수 밖에 없지만, 연속적 장애로부터 복구가 가능
퇴보 모드로 들어가기
- 이 방법은 반드시 서비스 내에 구현되어 있어야 함
일괄 작업 부타 (batch load) qowpgkrl
- 부하가 많을 때는 batch job 죽인다
문제가 있는 트래픽 배제하기
마무리하며
- 시스템에 과부하가 발생하면 그 상황을 해결하기 위한 조치가 필요
- 일단 서비스가 장애가 발생할 수 있는 수준에 도달하면 사용자의 모든 요청을 완벽하게 처리해 내기보다는 에러를 리턴하거나 혹은 평소 대비 낮은 품질의 결과를 리턴해야 함
- 연속적 장애를 피하려면 장애의 발생 지점을 이해하고 장애 발생 시 시스템이 어떻게 동작하는지 이해하는 것
- 충분한 주의를 기울이지 않는다면 백그라운드 에러를 줄이거나 안정적인 상태를 더 향상시키기 위한 시스템의 변경사항이 오히려 서비스 전체의 장애로 번질 위험이 있음
- 장애 시 재시도, 문제가 발생한 서버로부터 부하 위임, 문제가 발생한 서버의 중단, 캐시 추가 작업등은 일반적으로는 성능의 향상을 볼 수 있지만, 대량의 장애를 유발하기도 함.
Comments