Status | Authors | Coach | DRIs | Owning Stage | Created |
---|---|---|---|---|---|
proposed |
@qmnguyen0711
| devops enablement | 2023-05-30 |
Gitaly 적응형 동시성 제한
요약
Gitaly는 Git 서버로, 사건 발생 위험을 줄이기 위해 클라이언트에게 밀어넣어야 합니다. 과거에는 개별 RPC 동시성 제한 및 pack-objects 동시성 제한을 소개했습니다. 두 시스템 모두 성공적이었지만, 설정이 정적이어서 심각한 단점이 발생했습니다. 이 청사진은 정적 제한의 단점을 극복하기 위해 적응형 동시성 제한 시스템을 제안합니다. 알고리즘은 주로 가산 증가/곱셈 감소 접근법을 사용하여 일반 처리 중에 제한을 서서히 증가시키고, 사건 발생 시 빠르게 감소시키는 방식입니다. 이 알고리즘은 리소스 부족 및 심각한 지연 감소를 Gitaly가 문제가 있는지 판단하는 기준으로 중점을 둡니다.
동기
사건 발생 위험을 줄이고 자체를 보호하기 위해 Gitaly는 일부 제한이 도달했음을 판단하면 클라이언트에게 밀어넣을 수 있어야 합니다. 이전 시도에서는 개별 RPC 동시성 제한 및 pack-objects 동시성 제한을 소개하여 백프레셔(backpressure)의 기초를 마련했습니다.
개별 RPC 동시성 제한은 동시에 처리될 수 있는 최대 요청량을 구성할 수 있게 합니다. 이는 RPC 및 저장소 별로 제한을 설정합니다. pack-objects 동시성 제한은 IP별로 동시 Git 데이터 전송 요청을 제한합니다. 한 가지 주목할 점은 pack-objects 동시성 제한이 캐시 미스에만 적용된다는 것입니다. 이 제한을 초과하면 요청이 대기열에 들어가거나 대기열이 가득 차 있을 경우 거부됩니다. 요청이 너무 오래 대기열에 남아 있으면 거부됩니다.
이들 모두가 GitLab.com에서 유망한 결과를 얻었지만 특히 동시성 제한 값과 같은 설정은 정적입니다. 여기에는 몇 가지 단점이 있습니다.
- 동시성 제한에 합당한 값 유지하기가 까다로워집니다. 운영용 구성을 보면 각 제한이 서로 다른 소스의 단서를 바탕으로 조정됩니다. 전반적인 상황이 변하면 다시 조정해야 합니다.
- 정적 제한은 모든 사용 패턴에 적합하지 않습니다. 일관된 값을 선택하는 것은 현실적이지 않습니다. 제한이 너무 낮으면 큰 사용자들에게 영향을 미치게 됩니다. 값이 너무 느슨하면 보호 효과가 상실됩니다.
- 서버가 휴먼상태라도 요청이 거부될 수 있습니다. 이유는 속도가 서버에 가정되는 부하의 지표가 아니기 때문입니다.
동시성 제한의 이러한 모든 단점을 극복하면서 동시성 제한의 이점을 유지하는 가장 유망한 솔루션은 현재 노드의 처리 용량에 적응형 동시성 제한을 만드는 것입니다. 이 제안된 새로운 모드를 “적응형 동시성 제한”이라고 부릅니다.
목표
- Gitaly의 신중하고 실효적인 신뢰성과 강건성을 강화하기 위해 과부하 상태에서 트래픽을 밀어내는 방식을 더 나은 방법으로 만들기.
- Gitaly 포화 사건의 발생 빈도를 최소화하기.
- 클라이언트가 정확하지 않게 동시성 제한에 도달하는 가능성을 줄여서 ResourceExhausted 오류율을 낮추기.
- 동시성 제한의 원활한 또는 완전히 자동화된 보정을 용이하게 하기.
비목표
- 사용자나 관리자의 시스템 작업량이나 복잡성을 늘리지 않기. 여기에 제안된 적응성은 반대를 목표로 합니다.
제안
제안된 적응형 동시성 제한 알고리즘은 주로 가산 증가/곱셈 감소(AIMD) 접근법을 사용합니다. 이 방법은 일반 프로세스 기능 중에 제한을 서서히 증가시키고, 문제(백오프 이벤트)가 발생했을 때 빠르게 감소시킴을 수반합니다. Gitaly가 문제가 있는지 판단하는 기준은 다양합니다. 이 제안서에서는 두 가지에 중점을 두었습니다.
- Git 프로세스를 처리하는 데 필수적인 메모리 및 CPU 등의 리소스 부족.
- 심각한 지연 감소.
이 제안된 솔루션은이 분야의 다른 회사의 사람들에 의해 공유된 다양한 자료들에 크게 영향을 받았습니다. 특히 다음 자료들에서 영감을 받았습니다.
- TCP 혼잡 제어(RFC-2581, RFC-5681, RFC-9293, Computer Networks: A Systems Approach).
- Netflix 적응형 동시성 제한(블로그 글 및 구현)
- Envoy 적응형 동시성(문서)
신중한 고려 없이 솔루션을 맹목적으로 적용하고 완벽하게 작동할 것으로 기대할 수 없습니다. 제안된 접근은 cgroup 활용 및 upload-pack RPC 등을 포함한 Gitaly의 특정 제약 조건과 차별화 기능을 고려합니다.
제안된 솔루션은 Gitaly의 RPC 동시성 및 pack object 동시성을 대체하는 것을 목표로 하지 않지만 매개변수를 자동으로 조정합니다. 이는 대기, 대기 시간 초과, 대기열 길이, 파티셔닝 및 범위와 같은 다른 측면에 대해서는 변경되지 않습니다. 제안된 솔루션은 현재 동시성 제한의 값에만 초점을 맞춥니다.
디자인 및 구현 세부 정보
AIMD 알고리즘
적응 성능 제한 알고리즘(Adaptive Concurrency Limit algorithm)은 주로 증가-가산/감소-곱셈(AIMD) 접근법을 사용합니다. 이 방법은 일반적인 프로세스 기능 중에는 제한을 점진적으로 증가시키지만 문제가 발생할 때는 빠르게 줄이는 것을 포함합니다.
초기화 중에 다음 매개변수를 구성합니다.
-
initialLimit
: 시작할 때의 동시성 제한. 이 값은 본질적으로 현재 정적 동시성 제한과 동일합니다. -
maxLimit
: 최대 동시성 제한. -
minLimit
: 프로세스가 정상 작동으로 간주되기 위한 최소 동시성 제한. 값이 0이면 모든 다가오는 요청을 거부합니다. -
backoffFactor
: backoff 이벤트 발생 시 제한이 얼마나 빨리 감소하는지 (0 < backoff < 1
, 기본값은0.75
)
Gitaly 프로세스가 시작되면 limit = initialLimit
로 설정되며, 여기서 limit
는 한 번에 허용되는 최대 비행 중인 요청입니다.
주기적으로, 아마도 15초마다 한 번씩, limit
의 값을 다시 교정합니다.
- 마지막 교정 이후 backoff 이벤트가 없는 경우,
limit = limit + 1
. 새로운 제한은maxLimit
를 초과할 수 없습니다. - 그렇지 않은 경우,
limit = limit * backoffFactor
. 새로운 제한은minLimit
보다 낮아질 수 없습니다.
프로세스가 더 이상 요청을 처리할 수 없거나 곧 처리하지 못할 경우 이를 back-off 이벤트라고합니다. 이 이벤트를 최대한 오랫동안 유지하는 것이 이상적입니다. 이는 Gitaly가 최대 용량으로 작동하는 상태입니다.
이상적으로, 최소/최대 값은 방어 장치로, 운영 중에 일어나서는 안 되는 것입니다. 사실, 어느 쪽이든 마주치는 것은 어떤 문제가 있음을 의미하고 동적 알고리즘이 충분히 잘 작동하지 않는다는 것을 의미합니다.
요청 처리 방식
동시성 제한은 한 번에 전송되는 총 요청의 수를 제한합니다.
-
IFR < limit
인 경우, Gitaly는 대기하지 않고 즉시 새로운 요청을 처리합니다. 증가 후에, 필요한 경우 대기열에있는 후속 요청을 즉시 처리합니다. -
IFR = limit
인 경우, 제한이 도달한 것을 의미합니다. 차후 요청은 대기열에 들어가서 차례를 기다립니다. 대기열 길이가 구성된 제한에 도달하면, Gitaly는 새로운 요청을 즉시 거부합니다. 요청이 충분한 시간 동안 대기열에 머무르면, Gitaly에서 자동으로 해당 요청을 삭제합니다. -
IRF > limit
인 경우, 이는 적절히 backoff 이벤트의 결과입니다. 이것은 새롭게 지정된 제한보다 더 많은 요청을 Gitaly가 처리한다는 것을 의미합니다. 위의 경우와 유사하게 다가오는 요청을 대기열에 넣는 것 외에도, 이러한 상황이 오랫동안 해결되지 않으면 Gitaly는 현재 처리하는 요청을 로드 셰딩할 수 있습니다.
여러 차례 우리는 대기열 의미론을 변경할 지에 대해 고려해 왔습니다. 현재는 대기열의 가장 앞쪽(FIFO)에서 대기 중인 프로세스를 승인하지만, 대개 뒷쪽(LIFO)에서 승인하는 것이 더 좋을 것으로 제안되었습니다.
거절 이유에 상관없이 클라이언트는 ResourceExhausted
응답 코드를 받아서 후에 다시 백오프하고 다시 시도할 것이라는 신호를 받았습니다. 대부분의 Gitaly의 직접 클라이언트는 내부적이기 때문에, 특히 GitLab Shell 및 Workhorse는 실제 사용자가 친근한 메시지를 받았습니다. Gitaly는 내부 클라이언트가 백오프하도록 지수적 밀어닥침 헤더를 첨부할 수 있습니다. 그러나 조금 무참하며 예상치 못한 결과로 이어질 수 있습니다. 나중에 생각해 볼 수 있습니다.
백오프 이벤트
각 시스템에는 고유 한 신호 세트가 있으며, Gitaly의 경우 고려해야 할 두 가지 측면이 있습니다.
- 자원의 부족, 특히 메모리 및 CPU,이 Git 프로세스 처리에 필수적인 것들로 제한되거나 소진될 때. 이러한 리소스가 제한되거나 고갈 될 때 Gitaly가 더 많은 요청을 수락 할 이유가 없습니다. 그렇게하면 포화가 악화되고, Gitaly는 이 문제에 cgroups를 광범위하게 적용하여 해결합니다. 다음 섹션에서 cgroup를 사용하여 회계를 어떻게 수행할지에 대해 개요합니다.
- 심각한 지연 감소. Gitaly는
git-pack-objects(1)
와 같은 Git 프로세스 제어를 제공하는 것 외에도 다양한 목적을 위해 다양한 RPC를 제공합니다. 상대적으로 불확실한 지연에 대해 이일하기 어렵습니다. 전체적인 지연의 큰 감소는 Gitaly가 더 이상 요청을 수락해서는 안 된다는 것을 의미합니다. 아래의 다른 섹션에서 합리적으로 지연 감소를 어떻게 단정할 수 있는지에 대해 설명합니다.
위의 신호 이외에도 시스템을 더 똑똑하게 만들기 위해 미래에 더 많은 신호를 추가하는 것을 고려할 수 있습니다. 몇 가지 예로는 Go 가비지 수집기 통계, 네트워킹 통계, 파일 디스크립터 등이 있습니다. 일부 회사는 시간 드리프트 사용하여 CPU 포화 추정하기와 같은 똑똑한 속임수를 가지고 있습니다.
Upload Pack RPC의 Backoff 이벤트
Upload Pack RPC 및 해당 형제인 Pack Objects RPC는 Gitaly에 고유합니다. 이들은 가장 무거운 작업을 위한 것이며, 대량의 Git 데이터를 전송합니다. 각 작업은 몇 분 또는 몇 시간이 소요될 수 있습니다. 각 작업의 시간은 요청된 객체 수와 클라이언트의 인터넷 속도 등 여러 요소에 따라 달라집니다.
따라서 지연 시간은 백오프 이벤트를 결정하는 데 적합하지 않습니다. 이 유형의 RPC는 현재 단계에서는 리소스 회계에만 의존해야 합니다.
다른 RPC의 Backoff 이벤트
위에서 언급했듯이, Gitaly는 다양한 목적을 위해 다양한 RPC를 제공합니다. 이들은 허용 가능한 지연 시간 및 지연 시간 저하를 인식해야 하는 시기에 따라 다를 수 있습니다. 다행히도, 현재 RPC 동시성 제한 구현은 RPC 및 저장소 별로 구성을 제한합니다. 이러한 상황에서 지연 시간 신호가 의미를 가집니다.
지연 시간 외에도 리소스 사용이 중요한 역할을 합니다. 따라서 다른 RPC는 지연 시간 측정과 리소스 회계 신호를 모두 사용해야 합니다.
cgroup을 사용한 리소스 회계
포화 문제는 일반적으로 Gitaly 자체보다는 대부분의 작업을 처리하는 생성된 Git 프로세스에 의해 발생합니다. 이러한 프로세스는 cgroup 내에 포함되어 있으며, cgroup을 bucketing하는 알고리즘은 여기에서 찾을 수 있습니다. 일반적으로 Gitaly는 대상 저장소를 기준으로 적절한 cgroup을 선택합니다. 또한, 모든 저장소 수준의 cgroup이 속한 상위 cgroup도 있습니다.
cgroup 통계는 널리 접근 가능합니다. Gitaly는 cgroup 제어 파일의 다음 정보를 통해 리소스 용량 및 현재 리소스 사용량을 쉽게 가져올 수 있습니다.
memory.limit_in_bytes
memory.usage_in_bytes
cpu.cfs_period_us
cpu.cfs_quota_us
cpuacct.usage
이러한 통계를 가져오는 것은 약간의 오버헤드를 의미할 수 있습니다. 실시간으로 업데이트할 필요는 없으며, 따라서 제한 조정 주기에서 주기적으로 처리할 수 있습니다.
과거에 cgroup은 생성된 프로세스가 한도를 초과하지 못하도록 신뢰할 수 있었습니다. 생성된 프로세스가 설정한 한도에 도달하면(100%), 과부하가 발생할 수 있습니다. 이로 인해 페이지 폴트 증가, 시스템 호출 지연, 메모리 할당 문제 및 메모리 부족으로 이어질 수 있습니다. 이러한 사건의 결과는 이 예시에서 강조되고 있습니다. 비행 중인 요청이 심각하게 영향을 받아 받아 들일 수 없는 지연, 타임아웃, 심지어 취소로 이어집니다.
또한, 과거의 다양한 관측을 통해 일부 Git 프로세스(예: git-pack-objects(1)
)는 시간이 지남에 따라 메모리가 쌓입니다. git-pull(1)
요청이 몰릴 때 노드가 다양한 메모리를 필요로하는 프로세스로 가득 차는 문제가 발생합니다. 이러한 축적을 사전에 중지하는 것이 훨씬 나은 방법입니다.
결과적으로, 과부하를 피하기 위해 Gitaly는 메모리 용량의 75% 및 CPU 용량의 90%만 사용하도록 하는 소프트 제한 집합을 활용합니다. 이러한 소프트 제한에 도달하면 동시성 조정기가 동시성 제한을 곱셈 방식으로 줄입니다. 이 전략을 통해 노드가 과부하 사건을 처리할 충분한 여유 공간을 보장합니다.
이론적으로 cgroup 계층 구조를 통해 개별적으로 과부하 상태를 결정할 수 있습니다. 따라서 Gitaly는 각 저장소에 대해 동시성 제한을 조정할 수 있습니다. 그러나 이러한 접근 방식은 실제로는 복잡할 필요가 없습니다. 이와는 대조적으로 나중에 운영자에게 혼란을 야기할 수 있습니다.
좋은 출발으로, Gitaly는 다음 조건 중 하나 가 달성될 때 과부하 이벤트를 인식합니다.
- 상위 cgroup의 소프트 제한에 도달함.
- 저장소 cgroup 중 어느 것이라도 소프트 제한에 도달함.
두 번째 조건이 있어야 하는 것이 논리적입니다. 왜냐하면 저장소의 용량 제한이 상위 cgroup의 용량에 중요할 수 있기 때문입니다. 이는 저장소 cgroup이 한도에 도달할 때 다른 cgroup에 사용 가능한 리소스가 줄어들게 되기 때문입니다. 결과적으로 동시성 제한을 줄이면 과부하 상태가 발생하는 것을 지연시킵니다.
지연 시간 측정
RPC 외의 Upload Pack을 다시 보정할 때, 지연 시간이 고려됩니다. 지연 시간을 측정할 때 고려해야 할 두 가지 사항:
- 지연 시간을 기록하는 방법
- 지연 시간 저하를 인식하는 방법
Gitaly와 같은 강력한 gRPC 서버는 노드 당 초당 수천 개의 요청을 처리할 수 있습니다. Production 서버에서는 최대 수천 건의 요청을 처리할 수 있습니다. 응답 시간을 정확하게 추적하고 저장하는 것은 실용적이지 않습니다.
프로세스가 지연 시간 저하를 겪고 있는지를 결정하는 휴리스틱은 흥미롭습니다. 가장 단순한 해결책은 정적 지연 시간 임계값을 사전에 정의하는 것입니다. 각 RPC마다 다른 임계값이 있을 수 있습니다. 그러나 정적 동시성 제한과 비슷하게 합리적이고 최신값을 선정하는 것은 도전적이고 지루할 수 있습니다.
다행히도 이러한 문제에 대한 유명한 알고리즘들이 있으며, 주로 TCP 혼잡 제어의 영역에서 적용됩니다.
- Vegas Algorithm (CN: ASA - Chapter 6.4, 참조 구현)
- Gradient Algorithm (논문, 참조 구현)
이 두 알고리즘은 사전에 정의된 구성 없이 지연 시간 임계값을 자동으로 결정할 수 있습니다. 실제 시나리오에서 매우 효율적이고 통계적으로 신뢰할 수 있습니다. 내 의견으로는 두 알고리즘이 모두 특정한 우리의 사용 사례에 동일하게 적합하다고 생각합니다.
부하 분산
Gitaly가 오랫동안 과부하 상태에 갇혀있는 것은 두 가지 신호로 표시될 수 있습니다.
- 일정량의 연속적인 백오프 이벤트
- 일정 시간 동안 인플라이트 요청이 동시성 제한보다 많음
이러한 경우에는 특정 cgroup 또는 전체 Gitaly 노드가 임시로 사용할 수 없게 됩니다. 인플라이트 요청은 취소되거나 시간 초과될 가능성이 높습니다. GitLab.com 프로덕션 환경에서는 사건이 트리거되어 인간의 개입을 요청합니다. 이러한 상황을 개선하기 위해 부하 분산을 수행할 수 있습니다.
이 메커니즘은 의도적으로 인플라이트 요청을 선택적으로 종료시킴으로써 시작됩니다. 주요 목적은 모든 인플라이트 요청의 카스케이딩 실패를 방지하는 것입니다. 일부 요청이 삭제된 후에 특정 cgroup/node가 인간의 개입 없이 빠르게 정상 상태로 회복될 수 있기를 희망합니다. 결과적으로 이는 순 가용성 및 회복력 향상으로 이어집니다.
어떤 요청을 종료할지 선택하는 것은 까다로운 문제입니다. 많은 시스템에서는 요청의 중요도를 고려합니다. 다운스트림 요청은 중요도 점수가 할당됩니다. 점수가 낮은 요청이 먼저 대상이 됩니다. 유감스럽게도 GitLab에는 유사한 시스템이 없습니다. 우리는 긴급성 시스템을 사용하지만, 이는 중요도보다는 응답 시간을 보장하는 데 사용됩니다.
대신, 시스템을 가장 많이 피해를 주는 요청을 우선적으로 처리할 수 있습니다. 고려해야 할 몇 가지 기준은 다음과 같습니다.
- 상당한 메모리 소비하는 요청
- 시간이 지남에 따라 상당한 CPU 소비하는 요청
- 느린 클라이언트
- 최근 트래픽을 지배하는 IP에서의 요청
- 큐에 대기 중인 요청/초기 단계의 요청. 거의 완료된 요청을 거절하고 싶지는 않습니다.
시작하기 위해 먼저 위의 두 가지 기준을 선택할 수 있습니다. 나중에 프로덕션 환경에서 배우면 목록은 보강될 수 있습니다.
참고
- Linkedin HODOR 시스템
- https://www.linkedin.com/blog/engineering/infrastructure/hodor-overload-scenarios-and-the-evolution-of-their-detection-a
- Google SRE chapters about load balancing and overload:
- Netflix Performance Under Load
- Netflix Adaptive Concurrency Limit
- 적응형 동시성 제한을 사용한 NGINX를 이용한 부하 분산
- WeChat Microservices에서 과부하 제어
- ReactiveConf 2019 - Jay Phelps: Backpressure: Resistance is NOT Futile
- AWS re:Invent 2021 - Keeping Netflix reliable using prioritized load shedding
- 과부하 회피를 위한 부하 분산 사용
- “Rate Limiting을 중지하라! 올바른 용량 관리” by Jon Moore
- 성공적인 재난으로부터 생존하기 위한 부하 분산 사용하기 — CRE 삶에서 얻은 교훈들
- 웹 서비스에서의 부하 분산
- 분산 시스템에서의 부하 분산